Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/lib/table/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion apps/sim/lib/table/views/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -181,6 +182,8 @@ export async function createTableView(data: CreateTableViewData): Promise<TableV
.returning()

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

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

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

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

if (deleted.length > 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
}
Loading