Skip to content

Commit 985f9e6

Browse files
feat(tables): saved views with filter, sort, and column presets (#5961)
* feat(tables): saved views with filter, sort, and column presets * fix(tables): views own column layout, preserve deep-linked sort, seed update cache * fix(tables): merge view layout writes server-side, keep layout on save-from-All * fix(tables): send view saves as a merge patch so concurrent writes can't clobber * fix(tables): persist explicit All selection, prune view state for deleted columns * fix(tables): key-order-stable dirty check, reset layout when switching to All * fix(tables): return 404 when patching a missing view * improvement(tables): order the bar filter, sort, columns and drop the hidden count * fix(tables): record newly created columns in the active view's order * fix(tables): don't write layout as a reader, clear dead sort, reset dead view id * feat(tables): add New view to the views menu, starting from All * chore(api): bump route-count baseline to 981 after staging merge * fix(tables): guard default demotion, use the applied filter for dirty/save * fix(tables): clear state when the active view is deleted externally * revert(tables): drop the ineffective rows-query gate for view resolution * feat(tables): enable saved views in the embedded mothership table * fix(tables): resolve the views flag on the chat route too * improvement(tables): right-align the embedded run/stop control * fix(tables): live layout on new views, prune stored config, order cache writes * fix(tables): live layout on save-as-view, reset it per view, ignore inherited param when embedded * fix(tables): route undo/redo column-layout writes to the active view * fix(tables): scope layout undo to the view that recorded it Layout is view-owned, but UndoEntry carried no owner, so the persistence sink — rebound every render to the active view — decided the target at undo time rather than at record time. Reordering in view A then undoing from view B wrote A's column order into B and left A unchanged. Entries are now stamped with the active view id, and the three layout-bearing action types (create-column, delete-column, reorder-columns) are pruned from both stacks when the active view changes. Row and schema actions are table-scoped and survive the switch untouched. Inert when views are disabled: the id is always null, so nothing is ever pruned. * fix(tables): send only the layout keys an action changes Pin, reorder, and insert-column each shipped a full columnWidths snapshot they never modified. Both sinks merge at top-level key granularity, so an earlier-issued patch landing after a concurrent resize replaced the newer width map with its stale copy. Resize, auto-resize, and delete-column still send columnWidths — those genuinely change it. * fix(tables): don't strand layout writes while views load, keep schema undos across views Two more instances of layout being written without a known owner. The sink was left unbound until a view resolved, so a resize/reorder/pin (or the column-append effect) during the views fetch fell through to the table's shared metadata — corrupting All for a table about to adopt a view, and losing the edit to the re-seed. The sink is now bound while the query is in flight and suppresses the write. An error counts as settled, so a failed views fetch falls back to All instead of suppressing layout writes for the session. Pruning was also too broad: create-column and delete-column are table-scoped schema ops that merely have a layout side-effect, so dropping them on a view switch made a deleted column unrecoverable. Only reorder-columns is purely layout and still prunes; the other two survive and have just their layout half suppressed at replay when the recorded view isn't active. * fix(tables): flush layout buffered during load when the owner settles to All Suppressing the write while the views query was in flight stopped All from being corrupted, but nothing resolved the buffer afterwards, so a resize during load looked applied and vanished on refresh. Settling on All re-seeds nothing (viewLayoutKey never changed), so the gesture is still on screen and is now persisted to shared metadata. Adopting a view re-seeds the grid from that view and has already replaced the gesture on screen, so the buffer is dropped to match. * fix(tables): resolve undo layout ownership at write time, not dispatch time The layout writes for column create/delete happen in mutation success callbacks, and persistLayoutRef is rebound every render. Resolving entryOwnsLayout up front meant the guard could still hold from before a view switch while the sink it guarded already pointed at the destination, writing the recorded view's layout into whichever view was now active. Now a function, so the guard and the sink are read at the same moment: switch away mid-mutation and the schema half still lands while the layout half is dropped, matching the rule that undo only ever affects the view on screen. * fix(tables): read layout from the grid instead of mirroring it The wrapper kept two shadow copies of the grid's column layout — liveLayoutRef and pendingLayoutRef — and all three of this round's findings were that mirror going stale: - liveLayoutRef was only cleared on activeView.id change, so widths buffered before the views query settled survived into All, where layout writes bypass the mirror entirely. Both create paths spread it last, so a saved view stored those snapped-back widths. - currentViewConfig memoized a spread of that ref, and mutating a ref doesn't re-run a memo, so Save as view sent the pre-gesture layout. - The flush effect keyed on activeView, but adoption writes the view id through the URL, so for one render the query had settled while activeView was still null — flushing to All in exactly the case that had to drop. The grid owns this state, so it now publishes a reader through a sink ref and the wrapper asks at the moment it needs a value. Nothing to keep in sync, so nothing to go stale. Only whether an unowned change happened is tracked, and the resolve effect — which is what actually picks the owner — decides to persist or drop. * fix(tables): flush unowned layout when the views fetch fails The resolve effect is the only caller of resolvePendingLayout and gated on isSuccess, which never becomes true on a query error — so layout touched during the load window was never persisted on the error path, even though the table had already settled to All and later writes worked. The error branch now flushes to shared metadata, matching the rest of the error path's fall-back-to-All behavior. * fix(tables): gate the on-screen layout restore by ownership, not just the persist The undo success callbacks applied the recorded view's order/widths/pinning to the grid unconditionally and only gated the PATCH, so switching views before a column create/delete mutation resolved left the destination displaying the origin view's layout until switched away and back. Each callback now checks ownership where its layout work begins: three are purely layout and return at the top; delete-column undo restores cell data first (row data, runs everywhere) and gates only the layout block below it. In a non-owning view the restored column still appears via the grid's append effect — at the end, leaving that view's layout untouched. * fix(tables): route all layout writes through one owner-aware sink, reconcile order at seed Two holes closed: The sink binding toggled on activeView, leaving a render-frame gap after the views query settled but before the resolve effect adopted a default — writes in that gap fell through to shared metadata. The sink is now always bound while views are enabled and handlePersistLayout is the single router, reading the owner at call time: unresolved buffers, a view patches the view, All writes metadata. The error branch stamps the owner so post-error writes stop buffering. The append effect only fires when the schema changes, so a column that arrived while another source owned the layout rendered via the displayColumns fallback but was never written into the adopted owner's stored order until a drag happened to heal it. Seeding now reconciles the incoming order against the schema and persists the appended tail through the current sink. * fix(tables): capture the layout owner when a schema action is dispatched Insert-column and the delete chain persist layout from mutation callbacks through updateMetadataRef, which always targets the current sink — so a view switch mid-flight wrote the origin view's order/widths/pins into the destination. Undo got this guard already; the live paths never did. Both now capture viewLayoutKey at dispatch and compare at the callback. On a mismatch the layout work is skipped: the destination re-seeded its own layout, the new column lands there via the append effect when the refetch arrives, and the deleted column's dangling keys are pruned on read. pushUndo also takes the captured owner as an override — stamped at callback time it would record the destination, letting a later undo apply the origin's layout to it. * fix(tables): resolve views on list availability, not query success A failed background refetch flips isError while the cached list stays usable, and every view mutation invalidates the views query — so one blip made the resolve effect treat views as terminally failed and stop applying switches until the next successful refetch. The axis is now whether a list exists: error with no list ever fetched settles to All; error with a cached list resolves normally against the cache; owner is unknown only while the initial fetch is in flight. * ci: raise Build App timeout to 25 minutes The build outgrew the 15-minute cap over the last two days of merges: 10m02 (c6acc62), then 14m44 after the folders/desktop/library batch (adc557a, 16s under the limit), then two consecutive timeouts on 7b1af41 after the outlook merge. Staging's own latest runs show the same signature (one cancelled). GitHub labels a job timeout "cancelled", which is why these read as cancellations.
1 parent 0618541 commit 985f9e6

41 files changed

Lines changed: 20904 additions & 129 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ jobs:
222222
build:
223223
name: Build App
224224
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'linux-x64-8-core' }}
225-
timeout-minutes: 15
225+
# Build durations crossed 15 minutes as the app grew (10m02 on Jul 29 AM,
226+
# 14m44 after the folders/desktop/library merges, then two straight
227+
# timeouts) — GitHub reports a job timeout as "cancelled". 25 keeps
228+
# headroom without masking a genuine hang.
229+
timeout-minutes: 25
226230

227231
steps:
228232
- name: Checkout code
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { deleteTableViewContract, updateTableViewContract } from '@/lib/api/contracts/tables'
4+
import { parseRequest, validationErrorResponse } from '@/lib/api/server/validation'
5+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
6+
import { generateRequestId } from '@/lib/core/utils/request'
7+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8+
import type { TableSchema } from '@/lib/table'
9+
import { deleteTableView, TableViewValidationError, updateTableView } from '@/lib/table'
10+
import { accessError, checkAccess } from '@/app/api/table/utils'
11+
12+
const logger = createLogger('TableViewAPI')
13+
14+
interface TableViewRouteParams {
15+
params: Promise<{ tableId: string; viewId: string }>
16+
}
17+
18+
/** PATCH /api/table/[tableId]/views/[viewId] - Rename, overwrite config, or set as default. */
19+
export const PATCH = withRouteHandler(
20+
async (request: NextRequest, context: TableViewRouteParams) => {
21+
const requestId = generateRequestId()
22+
23+
try {
24+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
25+
if (!authResult.success || !authResult.userId) {
26+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
27+
}
28+
29+
const parsed = await parseRequest(updateTableViewContract, request, context, {
30+
validationErrorResponse: (error) => validationErrorResponse(error),
31+
})
32+
if (!parsed.success) return parsed.response
33+
34+
const { tableId, viewId } = parsed.data.params
35+
const { workspaceId, name, config, configPatch, isDefault } = parsed.data.body
36+
37+
const result = await checkAccess(tableId, authResult.userId, 'write')
38+
if (!result.ok) return accessError(result, requestId, tableId)
39+
40+
if (result.table.workspaceId !== workspaceId) {
41+
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
42+
}
43+
44+
const columns = (result.table.schema as TableSchema).columns ?? []
45+
const view = await updateTableView({
46+
viewId,
47+
tableId,
48+
name,
49+
config,
50+
configPatch,
51+
isDefault,
52+
columns,
53+
})
54+
if (!view) {
55+
return NextResponse.json({ error: 'View not found' }, { status: 404 })
56+
}
57+
58+
return NextResponse.json({ success: true, data: { view } })
59+
} catch (error) {
60+
if (error instanceof TableViewValidationError) {
61+
return NextResponse.json({ error: error.message }, { status: 400 })
62+
}
63+
logger.error(`[${requestId}] Error updating table view:`, error)
64+
return NextResponse.json({ error: 'Failed to update view' }, { status: 500 })
65+
}
66+
}
67+
)
68+
69+
/** DELETE /api/table/[tableId]/views/[viewId] - Remove a saved view. */
70+
export const DELETE = withRouteHandler(
71+
async (request: NextRequest, context: TableViewRouteParams) => {
72+
const requestId = generateRequestId()
73+
74+
try {
75+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
76+
if (!authResult.success || !authResult.userId) {
77+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
78+
}
79+
80+
const parsed = await parseRequest(deleteTableViewContract, request, context, {
81+
validationErrorResponse: (error) => validationErrorResponse(error),
82+
})
83+
if (!parsed.success) return parsed.response
84+
85+
const { tableId, viewId } = parsed.data.params
86+
const { workspaceId } = parsed.data.body
87+
88+
const result = await checkAccess(tableId, authResult.userId, 'write')
89+
if (!result.ok) return accessError(result, requestId, tableId)
90+
91+
if (result.table.workspaceId !== workspaceId) {
92+
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
93+
}
94+
95+
const deleted = await deleteTableView(viewId, tableId)
96+
if (!deleted) {
97+
return NextResponse.json({ error: 'View not found' }, { status: 404 })
98+
}
99+
100+
return NextResponse.json({ success: true, data: { deleted: true } })
101+
} catch (error) {
102+
logger.error(`[${requestId}] Error deleting table view:`, error)
103+
return NextResponse.json({ error: 'Failed to delete view' }, { status: 500 })
104+
}
105+
}
106+
)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { createTableViewContract, listTableViewsContract } from '@/lib/api/contracts/tables'
4+
import { parseRequest, validationErrorResponse } from '@/lib/api/server/validation'
5+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
6+
import { generateRequestId } from '@/lib/core/utils/request'
7+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8+
import type { TableSchema } from '@/lib/table'
9+
import { createTableView, listTableViews, TableViewValidationError } from '@/lib/table'
10+
import { accessError, checkAccess } from '@/app/api/table/utils'
11+
12+
const logger = createLogger('TableViewsAPI')
13+
14+
interface TableRouteParams {
15+
params: Promise<{ tableId: string }>
16+
}
17+
18+
/** GET /api/table/[tableId]/views - List every saved view on a table. */
19+
export const GET = withRouteHandler(async (request: NextRequest, context: TableRouteParams) => {
20+
const requestId = generateRequestId()
21+
22+
try {
23+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
24+
if (!authResult.success || !authResult.userId) {
25+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
26+
}
27+
28+
const parsed = await parseRequest(listTableViewsContract, request, context, {
29+
validationErrorResponse: (error) => validationErrorResponse(error),
30+
})
31+
if (!parsed.success) return parsed.response
32+
33+
const { tableId } = parsed.data.params
34+
const { workspaceId } = parsed.data.query
35+
36+
const result = await checkAccess(tableId, authResult.userId, 'read')
37+
if (!result.ok) return accessError(result, requestId, tableId)
38+
39+
if (result.table.workspaceId !== workspaceId) {
40+
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
41+
}
42+
43+
const columns = (result.table.schema as TableSchema).columns ?? []
44+
const views = await listTableViews(tableId, columns)
45+
46+
return NextResponse.json({ success: true, data: { views } })
47+
} catch (error) {
48+
logger.error(`[${requestId}] Error listing table views:`, error)
49+
return NextResponse.json({ error: 'Failed to list views' }, { status: 500 })
50+
}
51+
})
52+
53+
/** POST /api/table/[tableId]/views - Save the current filter/sort/layout as a named view. */
54+
export const POST = withRouteHandler(async (request: NextRequest, context: TableRouteParams) => {
55+
const requestId = generateRequestId()
56+
57+
try {
58+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
59+
if (!authResult.success || !authResult.userId) {
60+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
61+
}
62+
63+
const parsed = await parseRequest(createTableViewContract, request, context, {
64+
validationErrorResponse: (error) => validationErrorResponse(error),
65+
})
66+
if (!parsed.success) return parsed.response
67+
68+
const { tableId } = parsed.data.params
69+
const { workspaceId, name, config } = parsed.data.body
70+
71+
const result = await checkAccess(tableId, authResult.userId, 'write')
72+
if (!result.ok) return accessError(result, requestId, tableId)
73+
74+
if (result.table.workspaceId !== workspaceId) {
75+
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
76+
}
77+
78+
const columns = (result.table.schema as TableSchema).columns ?? []
79+
const view = await createTableView({
80+
tableId,
81+
workspaceId,
82+
name,
83+
config,
84+
userId: authResult.userId,
85+
columns,
86+
})
87+
88+
return NextResponse.json({ success: true, data: { view } })
89+
} catch (error) {
90+
if (error instanceof TableViewValidationError) {
91+
return NextResponse.json({ error: error.message }, { status: 400 })
92+
}
93+
logger.error(`[${requestId}] Error creating table view:`, error)
94+
return NextResponse.json({ error: 'Failed to create view' }, { status: 500 })
95+
}
96+
})

apps/sim/app/workspace/[workspaceId]/chat/[chatId]/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Metadata } from 'next'
33
import { getSession } from '@/lib/auth'
44
import { Home } from '@/app/workspace/[workspaceId]/home/home'
55
import { HomeFallback } from '@/app/workspace/[workspaceId]/home/home-fallback'
6+
import { resolveTableViewsEnabled } from '@/app/workspace/[workspaceId]/home/resolve-table-views-flag'
67

78
export const metadata: Metadata = {
89
title: 'Chat',
@@ -16,14 +17,17 @@ interface ChatPageProps {
1617
}
1718

1819
export default async function ChatPage({ params }: ChatPageProps) {
19-
const [{ chatId }, session] = await Promise.all([params, getSession()])
20+
const [{ workspaceId, chatId }, session] = await Promise.all([params, getSession()])
21+
const userId = session?.user?.id
22+
const tableViewsEnabled = await resolveTableViewsEnabled(workspaceId, userId)
2023
return (
2124
<Suspense fallback={<HomeFallback />}>
2225
<Home
2326
key={chatId}
2427
chatId={chatId}
2528
userName={session?.user?.name}
26-
userId={session?.user?.id}
29+
userId={userId}
30+
tableViewsEnabled={tableViewsEnabled}
2731
/>
2832
</Suspense>
2933
)

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ interface ResourceOptionsProps {
9191
* widgets; primary actions belong in the header's `actions`.
9292
*/
9393
aside?: ReactNode
94+
/**
95+
* Mirror of {@link aside} on the other side: rendered immediately to the RIGHT
96+
* of the filter/sort cluster and still grouped with it — e.g. the table editor's
97+
* Columns menu, which reads as the last item in the Filter/Sort/Columns row.
98+
*/
99+
asideEnd?: ReactNode
100+
/**
101+
* Control pinned to the far RIGHT of the bar, opposite the filter/sort cluster —
102+
* e.g. the table editor's Save-view button. Unlike `aside` it is a real action,
103+
* so it is separated from the menu group rather than joined to it.
104+
*/
105+
trailing?: ReactNode
94106
}
95107

96108
export const ResourceOptions = memo(function ResourceOptions({
@@ -99,6 +111,8 @@ export const ResourceOptions = memo(function ResourceOptions({
99111
filter,
100112
filterTags,
101113
aside,
114+
asideEnd,
115+
trailing,
102116
}: ResourceOptionsProps) {
103117
/**
104118
* Coordinates the Filter popover and Sort menu as a single menu bar: clicking
@@ -111,14 +125,23 @@ export const ResourceOptions = memo(function ResourceOptions({
111125
const isToggleFilter = filter?.mode === 'toggle'
112126
const popoverFilter = filter && filter.mode !== 'toggle' ? filter : null
113127

114-
const hasContent = search || sort || filter || aside || (filterTags && filterTags.length > 0)
128+
const hasContent =
129+
search ||
130+
sort ||
131+
filter ||
132+
aside ||
133+
asideEnd ||
134+
trailing ||
135+
(filterTags && filterTags.length > 0)
115136
if (!hasContent) return null
116137

117138
return (
118139
<div className={cn('border-[var(--border)] border-b py-2.5', search ? 'px-6' : 'px-4')}>
119140
<div className='flex items-center'>
120141
{search && <SearchSection search={search} />}
121-
<div className={cn('flex shrink-0 items-center gap-1.5', search && 'ml-auto')}>
142+
{/* `ml-auto` moves to `trailing` when present so the menu cluster stays put
143+
and only the trailing action is pushed to the far edge. */}
144+
<div className={cn('flex shrink-0 items-center gap-1.5', search && !trailing && 'ml-auto')}>
122145
{aside}
123146
<div className='flex items-center'>
124147
{filterTags?.map((tag) => (
@@ -177,7 +200,9 @@ export const ResourceOptions = memo(function ResourceOptions({
177200
) : null}
178201
{sort && (isToggleFilter || !popoverFilter) && <SortDropdown config={sort} />}
179202
</div>
203+
{asideEnd}
180204
</div>
205+
{trailing && <div className='ml-auto flex shrink-0 items-center gap-1.5'>{trailing}</div>}
181206
</div>
182207
</div>
183208
)

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ interface ResourceContentProps {
8484
isAgentResponding?: boolean
8585
genericResourceData?: GenericResourceData
8686
previewContextKey?: string
87+
/** Resolved server-side by the home page — the embedded table can't read
88+
* AppConfig itself, so the flag is threaded down rather than looked up. */
89+
tableViewsEnabled?: boolean
8790
onNotFound?: (resourceId: string) => void
8891
/**
8992
* Whether this resource is the one on screen. Only the persistent panels
@@ -153,6 +156,7 @@ export const ResourceContent = memo(function ResourceContent({
153156
isAgentResponding,
154157
genericResourceData,
155158
previewContextKey,
159+
tableViewsEnabled,
156160
onNotFound,
157161
visible = true,
158162
}: ResourceContentProps) {
@@ -223,7 +227,15 @@ export const ResourceContent = memo(function ResourceContent({
223227

224228
switch (resource.type) {
225229
case 'table':
226-
return <Table key={resource.id} workspaceId={workspaceId} tableId={resource.id} embedded />
230+
return (
231+
<Table
232+
key={resource.id}
233+
workspaceId={workspaceId}
234+
tableId={resource.id}
235+
embedded
236+
viewsEnabled={tableViewsEnabled}
237+
/>
238+
)
227239

228240
case 'file':
229241
return (

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ interface MothershipViewProps {
6767
previewSession?: FilePreviewSession | null
6868
isAgentResponding?: boolean
6969
genericResourceData?: GenericResourceData
70+
/** Resolved server-side by the home page; forwarded to the embedded table. */
71+
tableViewsEnabled?: boolean
7072
}
7173

7274
export const MothershipView = memo(
@@ -81,6 +83,7 @@ export const MothershipView = memo(
8183
previewSession,
8284
isAgentResponding,
8385
genericResourceData,
86+
tableViewsEnabled,
8487
}: MothershipViewProps,
8588
ref
8689
) {
@@ -189,6 +192,7 @@ export const MothershipView = memo(
189192
isAgentResponding={isAgentResponding}
190193
genericResourceData={active.type === 'generic' ? genericResourceData : undefined}
191194
previewContextKey={chatId}
195+
tableViewsEnabled={tableViewsEnabled}
192196
onNotFound={(resourceId) => removeResource('log', resourceId)}
193197
/>
194198
)}

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ interface HomeProps {
7171
chatId?: string
7272
userName?: string
7373
userId?: string
74+
/** Resolved server-side by the page — the embedded table can't reach AppConfig. */
75+
tableViewsEnabled?: boolean
7476
}
7577

76-
export function Home({ chatId, userName, userId }: HomeProps) {
78+
export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps) {
7779
useOAuthReturnRouter()
7880
const { workspaceId } = useParams<{ workspaceId: string }>()
7981
const router = useRouter()
@@ -523,6 +525,7 @@ export function Home({ chatId, userName, userId }: HomeProps) {
523525
previewSession={previewSession}
524526
isAgentResponding={isSending}
525527
genericResourceData={genericResourceData ?? undefined}
528+
tableViewsEnabled={tableViewsEnabled}
526529
className={skipResourceTransition ? '!transition-none' : undefined}
527530
/>
528531
</Suspense>

0 commit comments

Comments
 (0)