From ad5eead0cb24c7216557aab8ff0d9c6c574fdb9b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 30 Jul 2026 12:04:50 -0700 Subject: [PATCH] feat(tables): propagate shared saved-view changes to collaborators live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../[tableId]/hooks/use-table-event-stream.ts | 6 ++++++ apps/sim/lib/table/events.ts | 19 +++++++++++++++++++ apps/sim/lib/table/views/service.ts | 11 ++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts index 0d4b46f1fb8..6d7fa21927e 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts @@ -406,6 +406,12 @@ export function useTableEventStream({ else if (entry.event?.kind === 'definition') { void queryClient.invalidateQueries({ queryKey: tableKeys.detail(tableId), exact: true }) } + // A collaborator changed the table's shared saved views (create/rename/delete/ + // re-save): refetch the views list alone. Views are presentation state layered on + // the already-loaded table, so no rows/definition refetch is needed. + else if (entry.event?.kind === 'views') { + void queryClient.invalidateQueries({ queryKey: tableKeys.views(tableId) }) + } } catch (err) { logger.warn('Failed to parse table event', { tableId, err }) } diff --git a/apps/sim/lib/table/events.ts b/apps/sim/lib/table/events.ts index 0fda87f6d1f..be2a16990a7 100644 --- a/apps/sim/lib/table/events.ts +++ b/apps/sim/lib/table/events.ts @@ -147,6 +147,16 @@ export type TableEvent = tableId: string reason: 'locks' } + | { + /** A user created, renamed, deleted, or re-saved a shared saved view (a named + * filter/sort/layout preset). Views are table-wide collaborative state — every + * reader of the table sees every view — so peers refetch the views list to pick + * up the change live. Value-less, same refetch-in-own-format rationale as + * {@link kind} `edit`; no rows/definition refetch, since a view is presentation + * state layered on top of the already-loaded table. */ + kind: 'views' + tableId: string + } export interface TableEventEntry { eventId: number @@ -203,6 +213,15 @@ export function signalTableMetadataChanged(tableId: string): void { void appendTableEvent({ kind: 'metadata', tableId }) } +/** + * Signal collaborators that a user changed the table's shared saved views (created, + * renamed, deleted, or re-saved one) so they refetch the views list live. Fire-and-forget + * for the same reason as {@link signalTableRowsChanged}. + */ +export function signalTableViewsChanged(tableId: string): void { + void appendTableEvent({ kind: 'views', tableId }) +} + /** * The latest eventId assigned for a table, or 0 when the buffer is empty or * expired. Used by the stream route to tail from "now" when a client connects diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts index 1c8684a414c..1487be7dd35 100644 --- a/apps/sim/lib/table/views/service.ts +++ b/apps/sim/lib/table/views/service.ts @@ -16,6 +16,7 @@ import { generateId } from '@sim/utils/id' import { and, asc, eq, ne, sql } from 'drizzle-orm' import { getColumnId } from '@/lib/table/column-keys' import { NAME_PATTERN } from '@/lib/table/constants' +import { signalTableViewsChanged } from '@/lib/table/events' import { filterRulesToPredicate, filterToRules } from '@/lib/table/query-builder/converters' import type { ColumnDefinition, @@ -181,6 +182,8 @@ export async function createTableView(data: CreateTableViewData): Promise 0) logger.info('Deleted table view', { tableId, viewId }) + if (deleted.length > 0) { + logger.info('Deleted table view', { tableId, viewId }) + // Only signal a real deletion — a missing view (nothing deleted) changed nothing. + signalTableViewsChanged(tableId) + } return deleted.length > 0 }