Phase 4A: Rendering engine enhancements — filter drawer, swipe actions, row selection, record navigation, responsive dashboard grid, widget data queries#9
Conversation
…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>
There was a problem hiding this comment.
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 toListViewRenderer. - Adds previous/next record navigation UI to
DetailViewRenderer. - Implements responsive dashboard grid layout and introduces a widget query hook (
useDashboardData.ts) plusspanmetadata.
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); |
There was a problem hiding this comment.
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.
| const swipeableRef = useRef<Swipeable>(null); | |
| const swipeableRef = useRef<Swipeable | null>(null); |
| @@ -0,0 +1,182 @@ | |||
| import React, { useCallback, useMemo } from "react"; | |||
There was a problem hiding this comment.
useMemo is imported but never used, which will fail eslint/typecheck under the current config. Remove the unused import.
| import React, { useCallback, useMemo } from "react"; | |
| import React, { useCallback } from "react"; |
| toggleRootLogic, | ||
| clearFilters, | ||
| serialize, | ||
| hasFilters, |
There was a problem hiding this comment.
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).
| hasFilters, |
| const { data, isLoading } = useQuery(widget.object, { | ||
| top: widget.type === "list" || widget.type === "table" ? DEFAULT_LIST_PAGE_SIZE : 1, |
There was a problem hiding this comment.
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.
| 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, |
| * 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). |
There was a problem hiding this comment.
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.
| * 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. |
| ({ item }: { item: Record<string, unknown> }) => { | ||
| const recordId = String(item.id ?? item._id ?? ""); |
There was a problem hiding this comment.
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).
| ({ 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); |
Completes Phase 4A of the roadmap — all features use existing
@objectstack/client@1.1.0APIs with no upstream blockers.List view enhancements (
ListViewRenderer)FilterDrawermodal wraps existingQueryBuilder+useQueryBuilder;FilterButtonshows active filter badge. Activated viashowFilter+onFilterChangeprops.SwipeableRowusingreact-native-gesture-handlerSwipeablereveals Edit/Delete on left-swipe. Props:onSwipeEdit,onSwipeDelete.BatchActionBar. Props:selectionMode,selectedIds,onSelectionChange,onBatchDelete,onBatchEdit. Falls back toview.selection.typefrom server metadata.Detail view (
DetailViewRenderer)RecordNavigatorsub-component with Previous/Next buttons and position label ("3 of 50"). Props:onPrevious,onNext,hasPrevious,hasNext,positionLabel.Dashboard (
DashboardViewRenderer)useWindowDimensionsdrives 1-col (< 600dp) or 2-col layout. Widgets declarespan: 2for full-width. Row-packing algorithm respects span hints.useWidgetQueryhook connects each widget touseQuery(), computing aggregates (count/sum/avg/min/max) from the widget'sobject+aggregate+valueFieldconfig.New files
components/renderers/FilterDrawer.tsxcomponents/renderers/SwipeableRow.tsxhooks/useDashboardData.tsUsage example
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.