Skip to content

Commit 42eac75

Browse files
committed
fix(tables): close persisted view lifecycle gaps
1 parent 0c86b21 commit 42eac75

4 files changed

Lines changed: 179 additions & 9 deletions

File tree

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
/**
2-
* @vitest-environment jsdom
3-
*/
41
import { renderToStaticMarkup } from 'react-dom/server'
52
import { describe, expect, it, vi } from 'vitest'
63
import type { TableViewWire } from '@/lib/api/contracts/tables'

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,9 @@ export function Table({
11091109
* user no way to see what was applied.
11101110
*/
11111111
const handleFilterByCellValue = (conditions: readonly Predicate[]) => {
1112-
replaceFilter(withCellValueFilter(effectiveFilter, conditions))
1112+
const next = withCellValueFilter(effectiveFilter, conditions)
1113+
replaceFilter(next)
1114+
persistActiveViewConfig({ filter: next })
11131115
setFilterOpen(true)
11141116
}
11151117

@@ -1355,8 +1357,8 @@ export function Table({
13551357
/>
13561358
) : null
13571359

1358-
/** Right-aligned slot. Left `undefined` when both are absent so the options bar
1359-
* doesn't render an empty flex row — a fragment would always read as truthy. */
1360+
/** Right-aligned slot. Left `undefined` when absent so the options bar
1361+
* doesn't render an empty flex row. */
13601362
const optionsTrailing = runStatus || undefined
13611363

13621364
return (
@@ -1395,8 +1397,8 @@ export function Table({
13951397
/>
13961398
)}
13971399
{/* Sort + filter render in both modes. In embedded (mothership) mode there's no
1398-
Resource.Header, so the run/stop control rides in the options bar — pinned
1399-
right, opposite the menu cluster, next to Save. */}
1400+
Resource.Header, so the run/stop control rides in the options bar — pinned
1401+
right, opposite the menu cluster. */}
14001402
<Resource.Options
14011403
sort={sortConfig}
14021404
filter={filterConfig}

apps/sim/ee/workspace-forking/lib/copy/copy-resources.test.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @vitest-environment node
33
*/
44

5-
import { folder as folderTable } from '@sim/db/schema'
5+
import { folder as folderTable, tableViews, userTableDefinitions } from '@sim/db/schema'
66
import { sha256Hex } from '@sim/security/hash'
77
import {
88
dbChainMockFns,
@@ -1211,6 +1211,127 @@ describe('copyForkResourceContent', () => {
12111211
})
12121212
})
12131213

1214+
describe('copyForkResourceContainers table views', () => {
1215+
it('copies saved views and seeds a default for a legacy table', async () => {
1216+
const now = new Date('2026-08-19T00:00:00.000Z')
1217+
const definitions = [
1218+
{
1219+
id: 'table-with-view',
1220+
workspaceId: 'src-ws',
1221+
folderId: null,
1222+
name: 'Configured table',
1223+
description: null,
1224+
schema: { columns: [{ id: 'col-name', name: 'Name', type: 'string' }] },
1225+
metadata: { columnOrder: ['col-name'] },
1226+
maxRows: 10000,
1227+
rowCount: 1,
1228+
rowsVersion: 1,
1229+
schemaLocked: false,
1230+
insertLocked: false,
1231+
updateLocked: false,
1232+
deleteLocked: false,
1233+
archivedAt: null,
1234+
createdBy: 'source-user',
1235+
createdAt: now,
1236+
updatedAt: now,
1237+
},
1238+
{
1239+
id: 'legacy-table',
1240+
workspaceId: 'src-ws',
1241+
folderId: null,
1242+
name: 'Legacy table',
1243+
description: null,
1244+
schema: { columns: [{ id: 'col-email', name: 'Email', type: 'string' }] },
1245+
metadata: { columnOrder: ['col-email'] },
1246+
maxRows: 10000,
1247+
rowCount: 0,
1248+
rowsVersion: 0,
1249+
schemaLocked: false,
1250+
insertLocked: false,
1251+
updateLocked: false,
1252+
deleteLocked: false,
1253+
archivedAt: null,
1254+
createdBy: 'source-user',
1255+
createdAt: now,
1256+
updatedAt: now,
1257+
},
1258+
]
1259+
const sourceViews = [
1260+
{
1261+
id: 'source-view',
1262+
tableId: 'table-with-view',
1263+
workspaceId: 'src-ws',
1264+
name: 'My view',
1265+
config: { hiddenColumns: ['col-name'] },
1266+
isDefault: true,
1267+
createdBy: 'source-user',
1268+
createdAt: now,
1269+
updatedAt: now,
1270+
},
1271+
]
1272+
const inserted = new Map<unknown, Array<Record<string, unknown>>>()
1273+
const tx = {
1274+
select: () => ({
1275+
from: (table: unknown) => ({
1276+
where: () =>
1277+
Promise.resolve(
1278+
table === userTableDefinitions ? definitions : table === tableViews ? sourceViews : []
1279+
),
1280+
}),
1281+
}),
1282+
insert: (table: unknown) => ({
1283+
values: (values: Array<Record<string, unknown>>) => {
1284+
inserted.set(table, values)
1285+
return Promise.resolve()
1286+
},
1287+
}),
1288+
}
1289+
1290+
const result = await copyForkResourceContainers({
1291+
tx: tx as unknown as DbOrTx,
1292+
sourceWorkspaceId: 'src-ws',
1293+
childWorkspaceId: 'child-ws',
1294+
userId: 'user-1',
1295+
now,
1296+
selection: {
1297+
customTools: [],
1298+
skills: [],
1299+
mcpServers: [],
1300+
workflowMcpServers: [],
1301+
tables: definitions.map((definition) => definition.id),
1302+
knowledgeBases: [],
1303+
},
1304+
workflowIdMap: new Map(),
1305+
documentMappingContext: { edgeChildWorkspaceId: 'child-ws', sourceIsParent: true },
1306+
})
1307+
1308+
const copiedTableId = result.idMap.get('table')?.get('table-with-view')
1309+
const legacyTableId = result.idMap.get('table')?.get('legacy-table')
1310+
const copiedViews = inserted.get(tableViews)
1311+
expect(copiedViews).toEqual(
1312+
expect.arrayContaining([
1313+
expect.objectContaining({
1314+
tableId: copiedTableId,
1315+
workspaceId: 'child-ws',
1316+
name: 'My view',
1317+
config: { hiddenColumns: ['col-name'] },
1318+
isDefault: true,
1319+
createdBy: 'user-1',
1320+
}),
1321+
expect.objectContaining({
1322+
tableId: legacyTableId,
1323+
workspaceId: 'child-ws',
1324+
name: 'Default',
1325+
config: { columnOrder: ['col-email'] },
1326+
isDefault: true,
1327+
createdBy: 'user-1',
1328+
}),
1329+
])
1330+
)
1331+
expect(copiedViews?.find((view) => view.name === 'My view')?.id).not.toBe('source-view')
1332+
})
1333+
})
1334+
12141335
describe('copyForkResourceContainers custom-tool code env rewrite', () => {
12151336
function makeContainerTx(rows: Array<Record<string, unknown>>) {
12161337
const inserted: Array<Record<string, unknown>> = []

apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
permissions,
1111
skill,
1212
skillMember,
13+
tableViews,
1314
userTableDefinitions,
1415
userTableRowSecretProvenance,
1516
userTableRows,
@@ -54,6 +55,7 @@ import {
5455
rebindKnowledgeDocumentSecretProvenance,
5556
replaceKnowledgeDocumentSecretProvenanceInTx,
5657
} from '@/lib/knowledge/secret-provenance'
58+
import { DEFAULT_TABLE_VIEW_NAME } from '@/lib/table/constants'
5759
import { nKeysBetween } from '@/lib/table/order-key'
5860
import {
5961
classifyTableRowSecretProvenanceForCopy,
@@ -635,6 +637,27 @@ export async function copyForkResourceContainers(
635637
isNull(userTableDefinitions.archivedAt)
636638
)
637639
)
640+
const sourceViews =
641+
definitions.length > 0
642+
? await tx
643+
.select()
644+
.from(tableViews)
645+
.where(
646+
and(
647+
inArray(
648+
tableViews.tableId,
649+
definitions.map((definition) => definition.id)
650+
),
651+
eq(tableViews.workspaceId, sourceWorkspaceId)
652+
)
653+
)
654+
: []
655+
const sourceViewsByTable = new Map<string, typeof sourceViews>()
656+
for (const view of sourceViews) {
657+
const views = sourceViewsByTable.get(view.tableId) ?? []
658+
views.push(view)
659+
sourceViewsByTable.set(view.tableId, views)
660+
}
638661
const tableFolderIdMap = await resolveForkFolderMapping({
639662
tx,
640663
sourceWorkspaceId,
@@ -647,6 +670,7 @@ export async function copyForkResourceContainers(
647670
for (const [source, target] of tableFolderIdMap) folderIdMap.set(source, target)
648671

649672
const inserts: (typeof userTableDefinitions.$inferInsert)[] = []
673+
const viewInserts: (typeof tableViews.$inferInsert)[] = []
650674
for (const definition of definitions) {
651675
const childTableId = generateId()
652676
const remappedSchema = remapForkTableWorkflowGroups(
@@ -684,11 +708,37 @@ export async function copyForkResourceContainers(
684708
createdAt: now,
685709
updatedAt: now,
686710
})
711+
const views = sourceViewsByTable.get(definition.id) ?? []
712+
for (const view of views) {
713+
viewInserts.push({
714+
...view,
715+
id: generateId(),
716+
tableId: childTableId,
717+
workspaceId: childWorkspaceId,
718+
createdBy: userId,
719+
createdAt: now,
720+
updatedAt: now,
721+
})
722+
}
723+
if (!views.some((view) => view.isDefault)) {
724+
viewInserts.push({
725+
id: generateId(),
726+
tableId: childTableId,
727+
workspaceId: childWorkspaceId,
728+
name: DEFAULT_TABLE_VIEW_NAME,
729+
config: definition.metadata ?? {},
730+
isDefault: true,
731+
createdBy: userId,
732+
createdAt: now,
733+
updatedAt: now,
734+
})
735+
}
687736
record('table', definition.id, childTableId)
688737
contentPlan.tables.push({ sourceId: definition.id, childId: childTableId })
689738
names.tables.push(definition.name)
690739
}
691740
if (inserts.length > 0) await tx.insert(userTableDefinitions).values(inserts)
741+
if (viewInserts.length > 0) await tx.insert(tableViews).values(viewInserts)
692742
}
693743

694744
if (selection.knowledgeBases.length > 0) {

0 commit comments

Comments
 (0)