Skip to content

Airtable feature parity: Gallery/Gantt views, view config panels, self-service fields, rollups, hardening#5

Merged
akim136 merged 16 commits into
mainfrom
feat/airtable-parity
Jun 12, 2026
Merged

Airtable feature parity: Gallery/Gantt views, view config panels, self-service fields, rollups, hardening#5
akim136 merged 16 commits into
mainfrom
feat/airtable-parity

Conversation

@akim136

@akim136 akim136 commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Syncs the full feature set from upstream development into gridbase:

New view types + config UX

  • Gallery view: responsive card grid (primary-field title, Fields-editor card fields, optional URL-field cover) + toolbar config panel
  • Gantt view: per-record timeline bars from start/end date fields with month ticks + config panel
  • View config panels for Kanban (stack field + drag-to-reorder column order) and Calendar (date field) — no more "not configured" dead-ends
  • Field drag-and-drop reorder, load-more pagination, row selection + bulk delete

Schema self-service

  • Rollup field type: COUNT/SUM/AVG/MIN/MAX across a link, resolved server-side (IN-clause chunked under D1's bound-param cap)
  • Add/delete fields from the UI: POST/DELETE /v1/tables/:id/fields; stored fields get an additive ALTER TABLE ADD COLUMN, computed fields are metadata-only; delete guards the primary field and referenced fields

Hardening (review + security passes)

  • Widget config validation: widgetId required + unique, numeric stored metric for non-count aggs, stored group-by for charts, numeric position
  • Malformed PATCH bodies → 400 (was silent no-op 200) on views + dashboards
  • loadDashboards tolerates a missing meta_dashboards table (deploy-before-migrate can't take every route down)
  • Rollup field references pinned to the link chain (a dangling cross-table reference could break a table's reads)
  • Gantt pins zone-less datetimes to UTC; save-race controls disabled during in-flight dashboard PATCHes
  • Shared single-sources: VIEW_TYPES/AGG_FNS consts, recordTitle/FieldSelect/previewValue/WidgetResult, unified meta_dashboards row mapper

Verification

  • packages/api: typecheck + 32 vitest tests green
  • apps/web: typecheck + next build green
  • No personal/identifying references (scanned); only .dev.vars.example (no secrets) tracked

🤖 Generated with Claude Code

akim136 and others added 12 commits June 12, 2026 06:18
Dashboard view type (recharts), synced from upstream as the first real
upstream→OSS port:
- lib/dashboard.ts (+test), DashboardView + MetricChart, ViewSwitcher entry,
  app dispatch, packages/api types + VIEW_TYPES allowlist, recharts dep, a demo
  "Trends" dashboard on Deals. 27 tests pass; apps/web builds.

Public-repo polish:
- LICENSE owner filled in; CI workflow (typecheck/test/build); SECURITY.md;
  issue + PR templates; README badges.

Sync workflow:
- scripts/sync-from-upstream.mjs — opt-in, dry-run-by-default port of the generic
  shared surface; OSS-specific files denied; a STRUCTURAL scrub gate (id shapes,
  no literal private values) plus an optional gitignored .sync-scrub for exact
  project strings. docs/SYNC.md documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt A)

Backend for a workspace-level reports/dashboards composer.

- GET /v1/tables/:id/aggregate — grouped aggregation (count/sum/avg/min/max,
  optional date bucket day/week/month/year, filter). aggregate.ts builds the SQL
  from fixed allowlists (agg fn, bucket format) + registry-validated columns
  (assertIdent) + bound filter values — no user input interpolated. 9 tests.
- meta_dashboards table (0004) — workspace-level (NOT table-bound); dashboards.ts
  CRUD (POST/PATCH/DELETE /v1/dashboards) validates every widget's table/field
  refs against the registry. loadDashboards in the registry; /v1/meta exposes them.
- Verified on local D1 with the demo CRM: sum(amount) by stage + by month(close_date)
  return correct numbers; migration applies; 36 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A /dashboards surface to compose a page of widgets from any table.

- Routes: /d (list + create), /d/[dashboardId] (render). Sidebar "Dashboards" nav.
- DashboardRenderer (add/edit/remove/reorder widgets → PATCH config + refresh);
  DashboardWidget (KPI number · line · bar · small table); WidgetBuilder modal
  (type → table → measure/metric → group-by → date bucket → title); BarMini
  (recharts), reuses MetricChart. Per-widget data computed server-side
  (computeWidgets) so the API secret stays off the client; client mutates via BFF.
- A demo "Sales pipeline" dashboard seeded over the CRM (KPIs + by-stage bar +
  amount-by-month line + deals table).
- Review hardening (no high/med findings): aggregate filter now bounded
  (shared assertFilterBounds), widget filter field refs validated.

36 tests pass; apps/web builds; typecheck clean. Stacked on #2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From running code-review + security-review + simplify across the branches:

- seed.mjs: rename the dashboard-block `DEALS` const (it shadowed the demo-rows
  `DEALS` array → SyntaxError that broke `node scripts/seed.mjs` entirely).
- KPI correctness: computeWidgets never groups a KPI (ignores any stale
  groupBy/bucket), so a KPI always shows the grand total, not the first group's
  value; WidgetBuilder also clears groupBy/bucket when switching to KPI/table.
- dashboards validateConfig: fail fast on un-runnable widgets (sum/avg/min/max
  without a metric; line/bar without a group-by) instead of erroring at query
  time; cap widgets per dashboard (MAX_WIDGETS=50) to bound the per-render
  query fan-out.

Security review: no exploitable issues; public-repo leak check clean (only the
owner's public handle appears). 36 tests pass; build + typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nfig, branding

Port of the four grid feature gaps into gridbase (packages/api + apps/web),
mirroring search-system's grid implementation.

A) Sort/filter by a linked record's sub-field
   - types.ts: FilterCondition + sort entries gain optional linkedFieldId; lookups
     traverse their via->target automatically.
   - filter.ts: new resolveLinkTarget/compare/linkedFieldClause; buildWhere emits a
     correlated EXISTS joining the link join-table to the linked table; buildOrderBy
     emits a correlated scalar subquery (LIMIT 1). Both now take the Registry.
   - repo.ts + aggregate.ts: thread reg into buildWhere/buildOrderBy.
   - index.ts parseListOpts: parse "fieldId>linkedFieldId:dir" sort token.
   - apps/web: lib/types.ts mirror; gridApi.ts serializes the linked sort token;
     ViewToolbar filter/sort UI gains a linked sub-field dropdown (default = primary);
     link/lookup fields are no longer excluded from filter/sort dropdowns.

B) Kanban card field config
   - KanbanView: card fields now follow the view FieldEditor (visibleFields) minus the
     stack + primary; removed the hardcoded link exclusion and slice(0,3); added
     view.config.kanban.maxPreviewFields (default 8); link/lookup arrays render via labels.

C) Nested AND/OR filter (Airtable-style, one level)
   - types.ts: FilterSpec.conditions is Array<FilterCondition | FilterGroup> with an
     isFilterGroup guard; old flat specs stay valid.
   - filter.ts buildWhere: groups recurse one level, parenthesized, joined by the
     group conjunction.
   - index.ts assertFilterBounds: cap leaf conditions + group count + depth=1.
   - dashboards.ts validateConfig: flatten one group level to validate leaf fields.
   - ViewToolbar FilterEditor: Add condition / Add group; group conjunction toggle.

D) Branding
   - app/icon.png + public/grid.png (from Downloads/grid.png); layout metadata title/
     description/icon; Sidebar logo (next/image) in expanded + collapsed states; app
     name kept as "gridbase".

Tests: packages/api filter.test.ts extended (19 passing) — nested-group WHERE,
linked-field WHERE EXISTS, linked-field ORDER BY subquery, lookup traversal.
@gridbase/api test (28 passing) + typecheck clean; @gridbase/web typecheck + next
build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… rollup fields

Mirror of the grid UX + rollup work (built & verified in search-system/apps/grid first):
- #1 Field reorder: drag-and-drop (@dnd-kit/sortable) in FieldEditor.
- #2 Pagination: client TableView appends pages via offset cursor (loadMoreRecords).
- #3 Bulk delete: checkbox column + select-all + bulk Delete; chunked deleteRecords.
- #4 Kanban config: Kanban toolbar panel to set the stack field; columns from
  choices ∪ present values.
- #5 Rollup field type: COUNT/SUM/AVG/MIN/MAX across a link (resolveRollups), read-only;
  IN(...) chunked under D1's 100-param cap. Example rollups added to the demo schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror of the grid handle + Kanban column-order changes:
- Field reorder handle on the left, enlarged, touch-none, scrollbar-clear.
- Kanban config gains a drag-to-reorder "Column order" list (config.kanban.columnOrder);
  KanbanView honors it, Uncategorized last.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ields menu

Mirror of the field self-service feature:
- packages/api: POST/DELETE /v1/tables/:id/fields (fields.ts). Computed fields
  insert a meta row; stored fields ALTER TABLE ADD COLUMN. Delete guards the primary
  field + referenced fields and DROP COLUMNs stored ones. Schema cache invalidated.
- UI: "+ Add field" form (with a rollup builder) + hover-✕ delete in the Fields menu;
  new fields auto-show in the current view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror: a Calendar toolbar panel to pick the date field (config.calendar.dateFieldId),
paralleling the Kanban stack-field picker, replacing the "no date field configured"
dead-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ream)

- GalleryView: responsive card grid (primary-field title, Fields-editor card
  fields, optional url-field cover, maxPreviewFields cap) + Gallery toolbar panel.
- GanttView: per-record timeline bars from start/end date fields with month
  ticks + Gantt toolbar panel (start/end pickers).
- Registered in ViewSwitcher, view page, ViewConfig types, VIEW_TYPES allowlist;
  previewValue extracted to lib/preview (shared by Kanban + Gallery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rom upstream)

- WidgetBuilder materializes the bucket default + clears fieldIds/filter on table
  change; DashboardRenderer disables controls during in-flight saves.
- validateConfig: widgetId required+unique, numeric stored metric for non-count
  aggs, stored group-by for charts, numeric position; PATCH rejects malformed JSON.
- loadDashboards tolerates a missing meta_dashboards table (deploy-before-migrate).
- createField pins rollup references: via on the field's table, target a number
  field on the linked table (a dangling reference 500'd every read of the table).
- GanttView pins zone-less datetimes to UTC; single dated/undated pass; tick left%
  precomputed. Detail views regain overflow-hidden.
- Cleanups: shared meta_dashboards row mapper; VIEW_TYPES/AGG_FNS const single
  sources; ViewToolbar config panels collapsed into a typeEditors map; CountInput
  commit-on-blur; DashboardWidget reuses previewValue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recordTitle + FieldSelect shared helpers, WidgetResult single declaration,
fieldsForTable in WidgetBuilder/DashboardWidget (+ isComputed group-by filter
that was missing rollup), ViewSwitcher TYPE_INFO registry keyed by ViewType,
views/dashboards PATCH reject malformed JSON consistently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@akim136 akim136 force-pushed the feat/airtable-parity branch from 3256d25 to 6381371 Compare June 12, 2026 10:19
akim136 and others added 4 commits June 12, 2026 06:51
…ed from upstream)

- Drag-to-resize column widths persisted to config.fields[].width
- h-dvh + viewport-fit=cover + safe-area padding; wrapping header/toolbar rows;
  viewport-clamped popovers
- Layout views (kanban/calendar/gallery/gantt) page through the whole record set
  (bounded) instead of showing only the first 100

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mobile sidebar (synced)

- Grouped rows via config.groupBy with collapsible section headers
- Column header ▾ menu: sort asc/desc, group by, hide field
- Kanban per-column quick-add + collapsible columns (config.kanban.collapsedColumns)
- Mobile sidebar: icon-rail default + overlay expansion with backdrop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… row colors, side peek (synced)

- Sortable cards within kanban columns (config.kanban.cardOrder), cross-column
  drops slot into position
- Cell cursor: arrows / Enter / Esc / ⌘C / ⌘V on the grid
- Sticky summary footer (config.summaries) with count/sum/avg/min/max
- Row tinting by a select field (config.colorBy) via the header menu
- Record side-peek drawer with inline editing (⌘-click for full page)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es, dashboards, rollups, mobile)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@akim136 akim136 merged commit 79770c3 into main Jun 12, 2026
1 check failed
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.

1 participant