Skip to content

Commit 677a2fa

Browse files
committed
fix(tables): preserve the persisted default owner
1 parent 7bbf51c commit 677a2fa

3 files changed

Lines changed: 59 additions & 13 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot } from 'react-dom/client'
16
import { renderToStaticMarkup } from 'react-dom/server'
27
import { describe, expect, it, vi } from 'vitest'
38
import type { TableViewWire } from '@/lib/api/contracts/tables'
@@ -14,6 +19,13 @@ const DEFAULT_VIEW: TableViewWire = {
1419
updatedAt: new Date('2026-08-15T01:00:00.000Z'),
1520
}
1621

22+
const SAVED_VIEW: TableViewWire = {
23+
...DEFAULT_VIEW,
24+
id: 'view-saved',
25+
name: 'Saved',
26+
isDefault: false,
27+
}
28+
1729
function renderMenu(views: TableViewWire[], activeViewId: string | null): string {
1830
return renderToStaticMarkup(
1931
<ViewsMenu
@@ -42,4 +54,30 @@ describe('ViewsMenu', () => {
4254
expect(markup).toContain('All')
4355
expect(markup).not.toContain('>View<')
4456
})
57+
58+
it('only offers deletion for non-default views', () => {
59+
const container = document.createElement('div')
60+
document.body.appendChild(container)
61+
const root = createRoot(container)
62+
63+
act(() => {
64+
root.render(
65+
<ViewsMenu
66+
views={[DEFAULT_VIEW, SAVED_VIEW]}
67+
activeViewId={DEFAULT_VIEW.id}
68+
onSelect={vi.fn()}
69+
onRename={vi.fn()}
70+
onDelete={vi.fn()}
71+
onNewView={vi.fn()}
72+
canEdit
73+
/>
74+
)
75+
})
76+
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())
77+
78+
expect(document.body.querySelectorAll('button[aria-label="Delete"]')).toHaveLength(1)
79+
80+
act(() => root.unmount())
81+
container.remove()
82+
})
4583
})

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/views-menu/views-menu.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,15 @@ export const ViewsMenu = memo(function ViewsMenu({
156156
label: 'Rename',
157157
onClick: () => runAndClose(() => onRename(view.id)),
158158
},
159-
{
160-
icon: Trash,
161-
label: 'Delete',
162-
onClick: () => runAndClose(() => onDelete(view.id)),
163-
},
159+
...(!view.isDefault
160+
? [
161+
{
162+
icon: Trash,
163+
label: 'Delete',
164+
onClick: () => runAndClose(() => onDelete(view.id)),
165+
},
166+
]
167+
: []),
164168
]
165169
: undefined
166170
}

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ export function Table({
256256
const [{ sort: sortColumn, dir: sortDirection, view: activeViewId }, setTableParams] =
257257
useQueryStates(tableDetailParsers, tableDetailUrlKeys)
258258

259-
// Read-only mirrors for the resolve effect: it must know whether the user has
260-
// already applied a filter / hidden columns without re-running when they change.
259+
// Read-only mirrors for the resolve effect and replaceFilter's echo check:
260+
// both must read the current values without re-running when they change.
261261
const filterRef = useRef(filter)
262262
filterRef.current = filter
263263
const hiddenColumnsRef = useRef(hiddenColumns)
@@ -418,9 +418,13 @@ export function Table({
418418
* this an open panel keeps showing the rules of the filter it replaced.
419419
*
420420
* The remount discards an unapplied draft, which is the point — the rules on
421-
* screen must be the rules in effect.
421+
* screen must be the rules in effect. An incoming filter identical to the
422+
* current one is skipped entirely: the resolve effect re-applies the config
423+
* after this client's own autosave settles, and letting that echo remount an
424+
* open panel would wipe keystrokes typed since the flush and steal focus.
422425
*/
423426
const replaceFilter = useCallback((next: TablePredicate | null) => {
427+
if (JSON.stringify(next) === JSON.stringify(filterRef.current)) return
424428
setFilter(next)
425429
setFilterSeed((seed) => seed + 1)
426430
}, [])
@@ -588,11 +592,7 @@ export function Table({
588592
// `sort` rides the same host URL, so when the view id is inherited the
589593
// sort beside it is too — not local work, and it must not suppress the
590594
// default view's own sort.
591-
const keep = inheritedParams
592-
? { ...localWork(), sort: false }
593-
: legacyAllWithDefault
594-
? undefined
595-
: localWork()
595+
const keep = inheritedParams ? { ...localWork(), sort: false } : localWork()
596596
if (defaultView) {
597597
appliedViewRevisionRef.current = getTableViewRevision(defaultView)
598598
setTableParams({ view: defaultView.id })
@@ -864,6 +864,10 @@ export function Table({
864864

865865
const handleDeleteView = useCallback(
866866
(viewId: string) => {
867+
if (views.some((view) => view.id === viewId && view.isDefault)) {
868+
toast.error('Set another view as default before deleting this view')
869+
return
870+
}
867871
deleteViewMutation.mutate(viewId, {
868872
onSuccess: () => {
869873
if (viewId !== activeViewId) return

0 commit comments

Comments
 (0)