}
{label}
- {isDefault && (
-
- Default
-
- )}
- {actions && (
+ {actionCount > 0 && (
)}
- {actions && (
-
- {actions.map((action) => (
+ {actionCount > 0 && (
+
+ {actions?.map((action) => (
))}
+ {defaultState && (
+
+ )}
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
index 812b5eb3209..968f2f5955d 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx
@@ -80,6 +80,7 @@ import {
type SelectionSnapshot,
TableActionBar,
TableFilter,
+ type TableFilterHandle,
TableGrid,
ViewsMenu,
type WorkflowConfig,
@@ -240,6 +241,7 @@ export function Table({
})
const [filter, setFilter] = useState
(null)
const [filterOpen, setFilterOpen] = useState(false)
+ const tableFilterRef = useRef(null)
/** Bumped whenever the filter is replaced from outside the panel, to re-seed
* its rule rows. See {@link replaceFilter}. */
const [filterSeed, setFilterSeed] = useState(0)
@@ -669,6 +671,7 @@ export function Table({
const handleSelectView = useCallback(
(viewId: string | null) => {
+ tableFilterRef.current?.flush()
setTableParams({ view: viewId ?? ALL_VIEW_PARAM })
},
[setTableParams]
@@ -678,6 +681,15 @@ export function Table({
setViewModal({ mode: 'rename', viewId })
}, [])
+ const handleSetDefaultView = useCallback((viewId: string) => {
+ updateViewMutation.mutate(
+ { viewId, isDefault: true },
+ {
+ onError: (error) => toast.error(getErrorMessage(error, 'Failed to set default view')),
+ }
+ )
+ }, [])
+
const handleNewView = useCallback(() => {
setViewModal({ mode: 'new' })
}, [])
@@ -1089,10 +1101,13 @@ export function Table({
[columnOptions, sortColumn, sortDirection, handleSortColumn, handleClearSort]
)
- const handleFilterApply = (next: TablePredicate | null) => {
- setFilter(next)
- persistActiveViewConfig({ filter: next })
- }
+ const handleFilterChange = useCallback(
+ (next: TablePredicate | null) => {
+ setFilter(next)
+ persistActiveViewConfig({ filter: next })
+ },
+ [persistActiveViewConfig]
+ )
const handleHiddenColumnsChange = (next: string[]) => {
setHiddenColumns(next)
@@ -1329,7 +1344,10 @@ export function Table({
// Stable identity so the memoized Resource.Options can bail — an inline
// object literal (with an inline arrow) would defeat its memo every render.
- const handleToggleFilter = useCallback(() => setFilterOpen((prev) => !prev), [])
+ const handleToggleFilter = useCallback(() => {
+ if (filterOpen) tableFilterRef.current?.flush()
+ setFilterOpen(!filterOpen)
+ }, [filterOpen])
const filterConfig = useMemo(
() => ({
mode: 'toggle' as const,
@@ -1404,6 +1422,7 @@ export function Table({
activeViewId={activeView?.id ?? null}
onSelect={handleSelectView}
onRename={handleRenameView}
+ onSetDefault={handleSetDefaultView}
onDelete={handleDeleteView}
onNewView={handleNewView}
canEdit={userPermissions.canEdit}
@@ -1424,11 +1443,11 @@ export function Table({
/>
{filterOpen && (
setFilterOpen(false)}
+ onChange={handleFilterChange}
/>
)}
({
toast: { error: vi.fn(), success: vi.fn() },
}))
+import type { TableViewWire } from '@/lib/api/contracts/tables'
import {
tableRowsInfiniteOptions,
tableRowsParamsKey,
@@ -105,6 +106,37 @@ describe('useUpdateTableView autosave ordering', () => {
queryKey: tableKeys.views(TABLE_ID),
})
})
+
+ it('optimistically demotes the previous default when a view is promoted', () => {
+ const previousDefault: TableViewWire = {
+ id: 'view-default',
+ tableId: TABLE_ID,
+ name: 'Default',
+ config: {},
+ isDefault: true,
+ createdBy: 'user-1',
+ createdAt: new Date('2026-08-15T01:00:00.000Z'),
+ updatedAt: new Date('2026-08-15T01:00:00.000Z'),
+ }
+ const promoted: TableViewWire = {
+ ...previousDefault,
+ id: 'view-promoted',
+ name: 'My view',
+ updatedAt: new Date('2026-08-15T02:00:00.000Z'),
+ }
+ setCache(tableKeys.views(TABLE_ID), [
+ previousDefault,
+ { ...promoted, isDefault: false, updatedAt: previousDefault.updatedAt },
+ ])
+
+ const hook = useUpdateTableView({ workspaceId: WORKSPACE_ID, tableId: TABLE_ID })
+ hook.onSuccess?.(promoted, { viewId: promoted.id, isDefault: true }, undefined, undefined)
+
+ expect(getCache(tableKeys.views(TABLE_ID))).toEqual([
+ { ...previousDefault, isDefault: false },
+ promoted,
+ ])
+ })
})
describe('useDeleteColumn optimistic update', () => {
diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts
index c2c9c4651d8..8198e79829f 100644
--- a/apps/sim/hooks/queries/tables.ts
+++ b/apps/sim/hooks/queries/tables.ts
@@ -1565,6 +1565,9 @@ export function useUpdateTableView({ workspaceId, tableId }: RowMutationContext)
onSuccess: (view) => {
queryClient.setQueryData(tableKeys.views(tableId), (prev) =>
prev?.map((existing) => {
+ if (view.isDefault && existing.id !== view.id && existing.isDefault) {
+ return { ...existing, isDefault: false }
+ }
if (existing.id !== view.id) return existing
// Layout and view controls auto-save concurrently, and their
// responses can arrive out of order. The DB merge is authoritative, so