Skip to content

Commit 9b72801

Browse files
authored
feat(tables): propagate shared saved-view changes to collaborators live (#6100)
Table views (named filter/sort/layout presets) are table-wide shared state — every reader sees every view — but view create/update/delete had no realtime signal, so a collaborator only saw another user's view changes on their own staleTime/focus refetch. Add a 'views' table event kind + signalTableViewsChanged, emitted from the views service (createTableView/updateTableView/deleteTableView, on real success only), and a client handler that invalidates the views query alone (no rows/definition refetch — a view is presentation state on the loaded table). Mirrors how row/schema/metadata changes already propagate.
1 parent 628d2e4 commit 9b72801

3 files changed

Lines changed: 35 additions & 1 deletion

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,12 @@ export function useTableEventStream({
406406
else if (entry.event?.kind === 'definition') {
407407
void queryClient.invalidateQueries({ queryKey: tableKeys.detail(tableId), exact: true })
408408
}
409+
// A collaborator changed the table's shared saved views (create/rename/delete/
410+
// re-save): refetch the views list alone. Views are presentation state layered on
411+
// the already-loaded table, so no rows/definition refetch is needed.
412+
else if (entry.event?.kind === 'views') {
413+
void queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) })
414+
}
409415
} catch (err) {
410416
logger.warn('Failed to parse table event', { tableId, err })
411417
}

apps/sim/lib/table/events.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ export type TableEvent =
147147
tableId: string
148148
reason: 'locks'
149149
}
150+
| {
151+
/** A user created, renamed, deleted, or re-saved a shared saved view (a named
152+
* filter/sort/layout preset). Views are table-wide collaborative state — every
153+
* reader of the table sees every view — so peers refetch the views list to pick
154+
* up the change live. Value-less, same refetch-in-own-format rationale as
155+
* {@link kind} `edit`; no rows/definition refetch, since a view is presentation
156+
* state layered on top of the already-loaded table. */
157+
kind: 'views'
158+
tableId: string
159+
}
150160

151161
export interface TableEventEntry {
152162
eventId: number
@@ -203,6 +213,15 @@ export function signalTableMetadataChanged(tableId: string): void {
203213
void appendTableEvent({ kind: 'metadata', tableId })
204214
}
205215

216+
/**
217+
* Signal collaborators that a user changed the table's shared saved views (created,
218+
* renamed, deleted, or re-saved one) so they refetch the views list live. Fire-and-forget
219+
* for the same reason as {@link signalTableRowsChanged}.
220+
*/
221+
export function signalTableViewsChanged(tableId: string): void {
222+
void appendTableEvent({ kind: 'views', tableId })
223+
}
224+
206225
/**
207226
* The latest eventId assigned for a table, or 0 when the buffer is empty or
208227
* expired. Used by the stream route to tail from "now" when a client connects

apps/sim/lib/table/views/service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { generateId } from '@sim/utils/id'
1616
import { and, asc, eq, ne, sql } from 'drizzle-orm'
1717
import { getColumnId } from '@/lib/table/column-keys'
1818
import { NAME_PATTERN } from '@/lib/table/constants'
19+
import { signalTableViewsChanged } from '@/lib/table/events'
1920
import { filterRulesToPredicate, filterToRules } from '@/lib/table/query-builder/converters'
2021
import type {
2122
ColumnDefinition,
@@ -181,6 +182,8 @@ export async function createTableView(data: CreateTableViewData): Promise<TableV
181182
.returning()
182183

183184
logger.info('Created table view', { tableId: data.tableId, viewId: row.id })
185+
// Views are table-wide shared state, so every open reader refetches the list live.
186+
signalTableViewsChanged(data.tableId)
184187
return toTableView(row, data.columns)
185188
}
186189

@@ -252,6 +255,8 @@ export async function updateTableView(data: UpdateTableViewData): Promise<TableV
252255
// route maps it to 404 the same way `deleteTableView`'s `false` does.
253256
if (!row) return null
254257

258+
// Only signal a real update — a no-op PATCH on a missing view (row === null) changed nothing.
259+
signalTableViewsChanged(data.tableId)
255260
return toTableView(row, data.columns)
256261
}
257262

@@ -262,6 +267,10 @@ export async function deleteTableView(viewId: string, tableId: string): Promise<
262267
.where(and(eq(tableViews.id, viewId), eq(tableViews.tableId, tableId)))
263268
.returning({ id: tableViews.id })
264269

265-
if (deleted.length > 0) logger.info('Deleted table view', { tableId, viewId })
270+
if (deleted.length > 0) {
271+
logger.info('Deleted table view', { tableId, viewId })
272+
// Only signal a real deletion — a missing view (nothing deleted) changed nothing.
273+
signalTableViewsChanged(tableId)
274+
}
266275
return deleted.length > 0
267276
}

0 commit comments

Comments
 (0)