Skip to content

Commit 308a448

Browse files
fix(tables): views own column layout, preserve deep-linked sort, seed update cache
1 parent dd984b4 commit 308a448

3 files changed

Lines changed: 68 additions & 24 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { useParams } from 'next/navigation'
1010
import { usePostHog } from 'posthog-js/react'
1111
import type { RunLimit, RunMode, TableFindMatch } from '@/lib/api/contracts/tables'
1212
import { captureEvent } from '@/lib/posthog/client'
13-
import type { ColumnDefinition, Filter, TableRow as TableRowType, WorkflowGroup } from '@/lib/table'
13+
import type {
14+
ColumnDefinition,
15+
Filter,
16+
TableMetadata,
17+
TableRow as TableRowType,
18+
WorkflowGroup,
19+
} from '@/lib/table'
1420
import { getColumnId } from '@/lib/table/column-keys'
1521
import { TABLE_LIMITS } from '@/lib/table/constants'
1622
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -214,6 +220,16 @@ interface TableGridProps {
214220
* panel's Columns section edits the same list and the active view persists it.
215221
*/
216222
hiddenColumns?: string[]
223+
/** Active view's stored layout. When set it owns column order/width/pinning
224+
* instead of the table's shared `metadata`. */
225+
viewLayout?: TableMetadata | null
226+
/** Identity of `viewLayout`'s source (the view id, or `null` for "All"). Changing
227+
* it re-seeds the grid — comparing the config object itself would re-seed on
228+
* every refetch. */
229+
viewLayoutKey?: string | null
230+
/** Routes layout writes to the active view. Falls back to the table-metadata
231+
* mutation when absent. */
232+
onPersistLayout?: (patch: TableMetadata) => void
217233
/**
218234
* Ref the grid populates with its `handleColumnRename` so the wrapper's
219235
* sidebars can fire a column rename back into the grid (rewrites local
@@ -313,6 +329,9 @@ export function TableGrid({
313329
onSelectionChange,
314330
queryOptions,
315331
hiddenColumns,
332+
viewLayout,
333+
viewLayoutKey = null,
334+
onPersistLayout,
316335
columnRenameSinkRef,
317336
afterDeleteRowsSinkRef,
318337
afterDeleteAllSinkRef,
@@ -370,6 +389,8 @@ export function TableGrid({
370389
const pinnedColumnsRef = useRef(pinnedColumns)
371390
pinnedColumnsRef.current = pinnedColumns
372391
const metadataSeededRef = useRef(false)
392+
/** Which layout source the grid last seeded from, so a view switch re-seeds. */
393+
const seededLayoutKeyRef = useRef<string | null>(null)
373394
const containerRef = useRef<HTMLDivElement>(null)
374395
const scrollRef = useRef<HTMLDivElement>(null)
375396
const theadRef = useRef<HTMLTableSectionElement>(null)
@@ -1735,32 +1756,28 @@ export function TableGrid({
17351756
}, [tableData?.id])
17361757

17371758
useEffect(() => {
1738-
if (!tableData?.metadata) return
1739-
if (
1740-
!tableData.metadata.columnWidths &&
1741-
!tableData.metadata.columnOrder &&
1742-
!tableData.metadata.pinnedColumns
1743-
)
1759+
// With a view active its config owns the layout; otherwise the table's own
1760+
// metadata does. Switching views re-seeds unconditionally so the incoming
1761+
// view's layout replaces the outgoing one.
1762+
const source = viewLayout ?? tableData?.metadata
1763+
const switchedView = viewLayoutKey !== seededLayoutKeyRef.current
1764+
if (!source) return
1765+
if (!source.columnWidths && !source.columnOrder && !source.pinnedColumns && !switchedView)
17441766
return
1745-
// First load: seed all from the server and remember we've seeded.
1746-
if (!metadataSeededRef.current) {
1767+
1768+
if (!metadataSeededRef.current || switchedView) {
17471769
metadataSeededRef.current = true
1748-
if (tableData.metadata.columnWidths) {
1749-
setColumnWidths(tableData.metadata.columnWidths)
1750-
}
1751-
if (tableData.metadata.columnOrder) {
1752-
setColumnOrder(tableData.metadata.columnOrder)
1753-
}
1754-
if (tableData.metadata.pinnedColumns) {
1755-
setPinnedColumns(tableData.metadata.pinnedColumns)
1756-
}
1770+
seededLayoutKeyRef.current = viewLayoutKey
1771+
setColumnWidths(source.columnWidths ?? {})
1772+
setColumnOrder(source.columnOrder ?? null)
1773+
setPinnedColumns(source.pinnedColumns ?? [])
17571774
return
17581775
}
17591776
// After first load: only re-seed `columnOrder` when the *set of columns*
17601777
// changes (e.g. a workflow group adds/removes outputs server-side). Pure
17611778
// reorders are left alone so an in-flight optimistic drag isn't clobbered
17621779
// by a refetch returning the pre-drag order.
1763-
const serverOrder = tableData.metadata.columnOrder
1780+
const serverOrder = source.columnOrder
17641781
if (serverOrder) {
17651782
const localOrder = columnOrderRef.current
17661783
const serverSet = new Set(serverOrder)
@@ -1771,7 +1788,7 @@ export function TableGrid({
17711788
setColumnOrder(serverOrder)
17721789
}
17731790
}
1774-
}, [tableData?.metadata])
1791+
}, [tableData?.metadata, viewLayout, viewLayoutKey])
17751792

17761793
useEffect(() => {
17771794
if (!isColumnSelection || !selectionAnchor) return
@@ -2057,7 +2074,7 @@ export function TableGrid({
20572074
batchUpdateAsyncRef.current = batchUpdateRowsMutation.mutateAsync
20582075

20592076
const updateMetadataRef = useRef(updateMetadataMutation.mutate)
2060-
updateMetadataRef.current = updateMetadataMutation.mutate
2077+
updateMetadataRef.current = onPersistLayout ?? updateMetadataMutation.mutate
20612078

20622079
const deleteWorkflowGroupRef = useRef(deleteWorkflowGroupMutation.mutate)
20632080
deleteWorkflowGroupRef.current = deleteWorkflowGroupMutation.mutate

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
Filter,
1616
Sort,
1717
SortDirection,
18+
TableMetadata,
1819
TableRow as TableRowType,
1920
TableViewConfig,
2021
WorkflowGroup,
@@ -373,7 +374,7 @@ export function Table({
373374
if (defaultView) {
374375
seededViewIdRef.current = defaultView.id
375376
setTableParams({ view: defaultView.id })
376-
applyViewConfig(defaultView.config)
377+
applyViewConfig(defaultView.config, sortColumn !== null)
377378
return
378379
}
379380
// No view to adopt. Deliberately does NOT apply an empty config — that
@@ -410,9 +411,11 @@ export function Table({
410411
setTableParams,
411412
])
412413

414+
/** A view update replaces `config` wholesale, so the layout the grid auto-saves
415+
* is spread back in — otherwise Save would drop it. */
413416
const currentViewConfig = useMemo<TableViewConfig>(
414-
() => ({ filter, sort: sortQuery, hiddenColumns }),
415-
[filter, sortQuery, hiddenColumns]
417+
() => ({ ...activeView?.config, filter, sort: sortQuery, hiddenColumns }),
418+
[activeView, filter, sortQuery, hiddenColumns]
416419
)
417420

418421
/**
@@ -440,6 +443,20 @@ export function Table({
440443
setViewModal({ mode: 'rename', viewId })
441444
}, [])
442445

446+
/** Column order/width/pinning auto-saves into the active view as the user drags,
447+
* which is why `isSameViewConfig` excludes layout from the dirty check. With no
448+
* view active the grid keeps writing the table's shared metadata. */
449+
const handlePersistLayout = useCallback(
450+
(patch: TableMetadata) => {
451+
if (!activeView) return
452+
updateViewMutation.mutate(
453+
{ viewId: activeView.id, config: { ...activeView.config, ...patch } },
454+
{ onError: (error) => toast.error(getErrorMessage(error, 'Failed to save layout')) }
455+
)
456+
},
457+
[activeView]
458+
)
459+
443460
const handleSaveView = () => {
444461
if (activeView) {
445462
updateViewMutation.mutate(
@@ -972,6 +989,9 @@ export function Table({
972989
onSelectionChange={onSelectionChange}
973990
queryOptions={queryOptions}
974991
hiddenColumns={hiddenColumns}
992+
viewLayout={activeView?.config ?? null}
993+
viewLayoutKey={activeView?.id ?? null}
994+
onPersistLayout={activeView ? handlePersistLayout : undefined}
975995
columnRenameSinkRef={columnRenameSinkRef}
976996
afterDeleteRowsSinkRef={afterDeleteRowsSinkRef}
977997
afterDeleteAllSinkRef={afterDeleteAllSinkRef}

apps/sim/hooks/queries/tables.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,13 @@ export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext)
13391339
})
13401340
return response.data.view
13411341
},
1342+
// Without this the edited view's cached config stays stale until the refetch,
1343+
// so `isViewDirty` re-reads true and the Save chip flashes back after a save.
1344+
onSuccess: (view) => {
1345+
queryClient.setQueryData<TableViewWire[]>(tableKeys.views(tableId), (prev) =>
1346+
prev?.map((existing) => (existing.id === view.id ? view : existing))
1347+
)
1348+
},
13421349
onSettled: () => queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }),
13431350
})
13441351
}

0 commit comments

Comments
 (0)