From 538d098a791bd41a2cddd782def54d7c717d469c Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 21 Sep 2026 10:00:23 +0200 Subject: [PATCH] fix(datatables): read a roles answer only for the workspace it was asked in A fork and its parent each have their own roles on a data table of the same name, so an answer stamped with the name alone settled the role from the workspace the editor was acting on before. Also derive the AI table creation flag from the data replaced into the editor: data naming no data table left the flag on from before. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/components/DBManagerDrawer.svelte | 19 ++++++++++++++----- .../raw_apps/RawAppDataTableDrawer.svelte | 9 +++++++-- .../components/raw_apps/RawAppEditor.svelte | 2 ++ .../raw_apps/RawAppTemplatePicker.svelte | 13 ++++++++----- .../raw_apps/datatableUtils.svelte.ts | 19 ++++++++++++++----- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte index 71dda5e1d3..797c06b64f 100644 --- a/frontend/src/lib/components/DBManagerDrawer.svelte +++ b/frontend/src/lib/components/DBManagerDrawer.svelte @@ -58,6 +58,7 @@ if (!workspace || !datatable) return undefined try { return { + workspace, datatable, ...(await listUsableDatatableRoles(workspace, datatable)) } @@ -65,16 +66,24 @@ // Never leave the drawer waiting on this: fall back to the // unpermissioned shape so it opens and the server picks the role. console.error('Failed to load datatable roles:', e) - return { datatable, permissioned: false, roles: [], default_role: ADMIN_DATATABLE_ROLE } + return { + workspace, + datatable, + permissioned: false, + roles: [], + default_role: ADMIN_DATATABLE_ROLE + } } } ) - // A resource keeps its previous value while refetching, and roles are per data - // table: settling from the last one's answer would connect to the new data - // table as a role it may not even have. + // A resource keeps its previous value while refetching, and roles are per data table of one + // workspace: settling from another answer would connect to this data table as a role it may + // not even have. const rolesOfCurrent = $derived( - usableRoles.current?.datatable === selectedDatatable ? usableRoles.current : undefined + usableRoles.current?.datatable === selectedDatatable && usableRoles.current?.workspace === ws + ? usableRoles.current + : undefined ) // Nothing that connects runs until the role is settled: a first round sent without a role diff --git a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte index c3fd043198..7ccf89a275 100644 --- a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte @@ -85,6 +85,7 @@ if (!isOpen || !workspace || !datatable) return undefined try { return { + workspace, datatable, ...(await listUsableDatatableRoles(workspace, datatable)) } @@ -93,6 +94,7 @@ // that is refused. console.error('Failed to load datatable roles:', e) return { + workspace, datatable, permissioned: false, roles: [] as string[], @@ -102,9 +104,12 @@ } ) - // A resource keeps its previous value while it refetches, and roles are per data table. + // A resource keeps its previous value while it refetches, and roles are per data table of one + // workspace: a fork and its parent both have a `main`, with their own roles on it. const rolesOfCurrent = $derived( - usableRoles.current?.datatable === selectedDatatable ? usableRoles.current : undefined + usableRoles.current?.datatable === selectedDatatable && usableRoles.current?.workspace === opWs + ? usableRoles.current + : undefined ) // Mounting the manager fires its first queries, so it waits for the role: a round sent diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index e9b0dfa786..9a5eb001ec 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -747,6 +747,8 @@ data = next aiChatManager.datatableCreationPolicy = { ...aiChatManager.datatableCreationPolicy, + // As on load: data that names no data table leaves nothing to create tables in. + enabled: next.datatable !== undefined, datatable: next.datatable, schema: next.schema, roles: next.roles diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index 2851b7384a..0afef7744d 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -86,10 +86,13 @@ let selectedRole = $state(undefined) // Every reader waits for an answer stamped with the current selection: until then `current` - // belongs to the previous data table or role. + // belongs to the previous workspace, data table or role. // A data table whose name cannot carry a role in a reference is used as its default one. + const rolesAnswered = $derived( + roles.current.workspace === opWs && roles.current.datatable === selectedDatatable + ) const loadedRoles = $derived( - roles.current.datatable === selectedDatatable && + rolesAnswered && selectedDatatable !== undefined && datatableNameTakesRole(selectedDatatable) ? roles.current.roles @@ -114,8 +117,7 @@ const hasNoDatatables = $derived(availableDatatables?.length === 0) const rolesSettled = $derived( - hasNoDatatables || - (selectedDatatable !== undefined && roles.current.datatable === selectedDatatable) + hasNoDatatables || (selectedDatatable !== undefined && rolesAnswered) ) // A role is picked on one data table: two data tables can both define an `analyst` that @@ -123,7 +125,7 @@ let rolesPickedOn = $state(undefined) $effect(() => { const loaded = roles.current - if (loaded.datatable !== selectedDatatable) return + if (!rolesAnswered) return const switched = untrack(() => rolesPickedOn) !== selectedDatatable const current = untrack(() => selectedRole) if (switched || current === undefined || !loaded.roles.includes(current)) { @@ -173,6 +175,7 @@ hasNoDatatables || (rolesSettled && selectedDatatable !== undefined && + access.current.workspace === opWs && access.current.datatable === selectedDatatable && access.current.role === effectiveRole) ) diff --git a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts index 731b8120a6..566aac9484 100644 --- a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts +++ b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts @@ -40,8 +40,9 @@ export function createDatatablesResource(getWorkspace: () => string | undefined) } export type DatatableRoles = { - /** The data table this answers for: while a switch is in flight, `current` still holds the - * previous one's roles, which say nothing about the one now selected. */ + /** The workspace and data table this answers for: while a switch is in flight, `current` still + * holds the previous ones' roles, and a fork and its parent both have a `main` of their own. */ + workspace: string | undefined datatable: string | undefined /** Whether the data table is under roles. Without roles `roles` is empty because there is * nothing to pick, which is not the same as a permissioned one this caller may use no role of. */ @@ -61,6 +62,7 @@ export function createRolesResource( getWorkspace: () => string | undefined = () => get(workspaceStore) ) { const initialValue: DatatableRoles = { + workspace: undefined, datatable: undefined, permissioned: false, failed: false, @@ -71,7 +73,11 @@ export function createRolesResource( () => [getDatatable() ?? '', getWorkspace() ?? ''] as const, latestOnly( async ([datatableName, workspace]: readonly [string, string]): Promise => { - const empty = { ...initialValue, datatable: datatableName || undefined } + const empty = { + ...initialValue, + workspace: workspace || undefined, + datatable: datatableName || undefined + } if (!datatableName || !workspace) return empty try { const res = await listUsableDatatableRoles(workspace, datatableName) @@ -94,8 +100,9 @@ export function createRolesResource( } export type DatatableAccess = { - /** What this answers for. Until both match the selection, the schemas and the right to - * create one belong to another data table or another role. */ + /** What this answers for. Until all three match the selection, the schemas and the right to + * create one belong to another workspace, data table or role. */ + workspace: string | undefined datatable: string | undefined role: string | undefined /** The request failed, or the server kept the entry with an error (a role this caller may @@ -119,6 +126,7 @@ export function createDatatableAccessResource( getReady: () => boolean = () => true ) { const initialValue: DatatableAccess = { + workspace: undefined, datatable: undefined, role: undefined, failed: false, @@ -137,6 +145,7 @@ export function createDatatableAccessResource( ]): Promise => { const asked = { ...initialValue, + workspace: workspace || undefined, datatable: datatable || undefined, role: role || undefined }