Skip to content

Phase 4A: Rendering engine enhancements — filter drawer, swipe actions, row selection, record navigation, responsive dashboard grid, widget data queries#9

Merged
hotlong merged 5 commits into
mainfrom
copilot/enhance-sdk-rendering-engine
Feb 8, 2026
Merged

Phase 4A: Rendering engine enhancements — filter drawer, swipe actions, row selection, record navigation, responsive dashboard grid, widget data queries#9
hotlong merged 5 commits into
mainfrom
copilot/enhance-sdk-rendering-engine

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 8, 2026

Completes Phase 4A of the roadmap — all features use existing @objectstack/client@1.1.0 APIs with no upstream blockers.

List view enhancements (ListViewRenderer)

  • Filter drawerFilterDrawer modal wraps existing QueryBuilder + useQueryBuilder; FilterButton shows active filter badge. Activated via showFilter + onFilterChange props.
  • Swipe actionsSwipeableRow using react-native-gesture-handler Swipeable reveals Edit/Delete on left-swipe. Props: onSwipeEdit, onSwipeDelete.
  • Row selection — Single/multi-select with checkbox UI, integrated with existing BatchActionBar. Props: selectionMode, selectedIds, onSelectionChange, onBatchDelete, onBatchEdit. Falls back to view.selection.type from server metadata.

Detail view (DetailViewRenderer)

  • Record navigationRecordNavigator sub-component with Previous/Next buttons and position label ("3 of 50"). Props: onPrevious, onNext, hasPrevious, hasNext, positionLabel.

Dashboard (DashboardViewRenderer)

  • Responsive griduseWindowDimensions drives 1-col (< 600dp) or 2-col layout. Widgets declare span: 2 for full-width. Row-packing algorithm respects span hints.
  • Live datauseWidgetQuery hook connects each widget to useQuery(), computing aggregates (count/sum/avg/min/max) from the widget's object + aggregate + valueField config.

New files

  • components/renderers/FilterDrawer.tsx
  • components/renderers/SwipeableRow.tsx
  • hooks/useDashboardData.ts

Usage example

<ListViewRenderer
  records={records}
  fields={fields}
  showFilter
  onFilterChange={(filter) => refetch({ filter })}
  onSwipeEdit={(rec) => router.push(`${rec.id}/edit`)}
  onSwipeDelete={(rec) => mutation.delete(rec.id)}
  selectionMode="multiple"
  onBatchDelete={(ids) => batchOps.batchDelete(ids)}
/>

<DetailViewRenderer
  record={record}
  onPrevious={() => navigateTo(prevId)}
  onNext={() => navigateTo(nextId)}
  hasPrevious={idx > 0}
  hasNext={idx < total - 1}
  positionLabel={`${idx + 1} of ${total}`}
/>

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 4 commits February 8, 2026 08:26
…ering

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…views

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…navigation, responsive dashboard grid, widget data hook

- 4A.1: Add FilterDrawer with dynamic filter UI based on useFields() field definitions
- 4A.2: Add SwipeableRow with swipe-to-reveal edit/delete actions
- 4A.3: Add row selection (single/multi) with BatchActionBar integration
- 4A.4: Add previous/next record navigation to DetailViewRenderer
- 4A.5: Implement responsive grid layout for dashboard widgets
- 4A.6: Add useDashboardData hook connecting widgets to useQuery()

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…WidgetData, extract constant

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Enhance SDK rendering engine and optimize batch operations Phase 4A: Rendering engine enhancements — filter drawer, swipe actions, row selection, record navigation, responsive dashboard grid, widget data queries Feb 8, 2026
Copilot AI requested a review from hotlong February 8, 2026 08:39
@hotlong hotlong marked this pull request as ready for review February 8, 2026 08:39
Copilot AI review requested due to automatic review settings February 8, 2026 08:39
@hotlong hotlong merged commit 3766733 into main Feb 8, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Completes Phase 4A rendering-engine enhancements by adding list view filtering/swipe/selection, detail record navigation, and a responsive dashboard grid with widget data querying.

Changes:

  • Adds FilterDrawer + FilterButton, SwipeableRow, and row selection + batch actions to ListViewRenderer.
  • Adds previous/next record navigation UI to DetailViewRenderer.
  • Implements responsive dashboard grid layout and introduces a widget query hook (useDashboardData.ts) plus span metadata.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
hooks/useDashboardData.ts Adds widget-level data querying / aggregation helper for dashboard widgets.
components/renderers/types.ts Extends dashboard widget metadata with span for grid layout.
components/renderers/index.ts Re-exports new renderers/types for swipe rows and filter drawer.
components/renderers/SwipeableRow.tsx Introduces swipe-to-reveal Edit/Delete actions for list rows.
components/renderers/ListViewRenderer.tsx Adds filter drawer entrypoint, swipe actions wrapping, and single/multi row selection with batch bar.
components/renderers/FilterDrawer.tsx Adds modal filter UI wrapping existing query builder with Apply/Clear actions.
components/renderers/DetailViewRenderer.tsx Adds previous/next record navigator header section.
components/renderers/DashboardViewRenderer.tsx Adds responsive 1–2 column grid with row packing honoring widget span.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

onEdit,
onDelete,
}: SwipeableRowProps) {
const swipeableRef = useRef<Swipeable>(null);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef<Swipeable>(null) fails under strictNullChecks because null isn’t assignable to Swipeable. Type the ref as nullable (e.g., Swipeable | null) or initialize with a non-null assertion so the file type-checks.

Suggested change
const swipeableRef = useRef<Swipeable>(null);
const swipeableRef = useRef<Swipeable | null>(null);

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,182 @@
import React, { useCallback, useMemo } from "react";
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMemo is imported but never used, which will fail eslint/typecheck under the current config. Remove the unused import.

Suggested change
import React, { useCallback, useMemo } from "react";
import React, { useCallback } from "react";

Copilot uses AI. Check for mistakes.
toggleRootLogic,
clearFilters,
serialize,
hasFilters,
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasFilters is destructured from useQueryBuilder() but never used. This triggers the unused-vars lint rule; drop it from the destructuring (or use it to drive UI state).

Suggested change
hasFilters,

Copilot uses AI. Check for mistakes.
Comment thread hooks/useDashboardData.ts
Comment on lines +33 to +34
const { data, isLoading } = useQuery(widget.object, {
top: widget.type === "list" || widget.type === "table" ? DEFAULT_LIST_PAGE_SIZE : 1,
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For metric/KPI widgets you set top: 1, but then compute sum/avg/min/max from data.records. With only 1 record fetched, these aggregates will be incorrect. Either fetch enough records for client-side aggregation (or page through) or use a server-side aggregate/count API if available.

Suggested change
const { data, isLoading } = useQuery(widget.object, {
top: widget.type === "list" || widget.type === "table" ? DEFAULT_LIST_PAGE_SIZE : 1,
// Determine appropriate page size / limit for the query.
// - Lists/tables: use a fixed page size.
// - Metric/KPI with non-count aggregate + valueField: do NOT force `top: 1`
// so we can aggregate over more than a single record.
// - Other cases: keep `top: 1` for efficiency.
let top: number | undefined;
if (widget.type === "list" || widget.type === "table") {
top = DEFAULT_LIST_PAGE_SIZE;
} else if (
(widget.type === "metric" || widget.type === "kpi") &&
widget.aggregate &&
widget.aggregate !== "count" &&
widget.valueField
) {
top = undefined;
} else {
top = 1;
}
const { data, isLoading } = useQuery(widget.object, {
top,

Copilot uses AI. Check for mistakes.
Comment thread hooks/useDashboardData.ts
Comment on lines +107 to +115
* Convenience wrapper that fetches data for **all** widgets in a
* dashboard. Returns a map keyed by widget name that can be passed
* directly to `<DashboardViewRenderer widgetData={…} />`.
*
* Because React hooks cannot be called in a loop, consumers should
* call `useWidgetQuery` per-widget inside a child component. This
* hook is provided for dashboards with a known, static widget list
* by wrapping each query in its own sub-component (see
* `DashboardDataProvider` pattern in the app route layer).
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc here says this hook fetches data for all dashboard widgets and returns a map keyed by widget name, but useWidgetData just calls useWidgetQuery for a single widget and returns a single payload. Update the documentation and/or implement the described map-returning helper to avoid misleading consumers.

Suggested change
* Convenience wrapper that fetches data for **all** widgets in a
* dashboard. Returns a map keyed by widget name that can be passed
* directly to `<DashboardViewRenderer widgetData={…} />`.
*
* Because React hooks cannot be called in a loop, consumers should
* call `useWidgetQuery` per-widget inside a child component. This
* hook is provided for dashboards with a known, static widget list
* by wrapping each query in its own sub-component (see
* `DashboardDataProvider` pattern in the app route layer).
* Convenience wrapper that fetches data for a single dashboard widget
* by delegating to {@link useWidgetQuery}.
*
* This is primarily provided for readability where a component wants
* to express that it is working with "widget data" rather than calling
* `useWidgetQuery` directly. It returns the same {@link WidgetDataPayload}
* that `useWidgetQuery` produces for the given widget.

Copilot uses AI. Check for mistakes.
Comment on lines 199 to +200
({ item }: { item: Record<string, unknown> }) => {
const recordId = String(item.id ?? item._id ?? "");
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Row selection uses recordId = String(item.id ?? item._id ?? ""), but keyExtractor falls back to String(index) when ids are missing. If a record lacks id/_id, selection will collapse multiple rows into the same empty-string id. Consider deriving recordId using the same fallback as keyExtractor (e.g., include index in renderItem).

Suggested change
({ item }: { item: Record<string, unknown> }) => {
const recordId = String(item.id ?? item._id ?? "");
({ item, index }: { item: Record<string, unknown>; index: number }) => {
const recordId = String(item.id ?? item._id ?? index);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants