From 2d8a99d2a32873f52bad379ed2dcaccea6567e51 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 4 Sep 2026 14:49:51 +0200 Subject: [PATCH] fix(datatables): an answer about another role settles nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What a role may create in is the question the access resource asks, so an answer computed as a different one is not an answer to it — and there is always one of those: the role getter is empty until the role list settles, so every switch fetches once with no role, which the server reads as the data table's configured default. That answer stamped the data table alone, passed the check, and its one-way flip took the modal off `New` for a role that can create schemas. Both halves of the key are stamped and compared now, and Start waits for that answer too: a new schema name checked against a list from another question is not checked. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n --- .../raw_apps/RawAppTemplatePicker.svelte | 27 +++++++++++++------ .../raw_apps/datatableUtils.svelte.ts | 19 +++++++------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index 53b9d91e20..89edb723ba 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -74,7 +74,7 @@ // What the picked role can reach, which is not what the data table holds. const access = createDatatableAccessResource( () => selectedDatatable, - () => (showRolePicker ? effectiveRole : undefined), + () => accessRole, () => opWs ) const roles = createRolesResource( @@ -122,15 +122,22 @@ }) const availableDatatables = $derived(datatables.current) - // Read like the role list: only once it answers for the data table selected. - // At mount it is the initial value, and during a switch it is the previous - // table's — neither says anything about this one. + // The role the access question is asked as. Until the role list settles this + // is `undefined`, which the server reads as the data table's configured + // default — a different question, and one whose answer says nothing about what + // the role finally selected may create. + const accessRole = $derived(showRolePicker ? effectiveRole : undefined) + // Read like the role list: only once it answers for what is selected now. At + // mount it is the initial value, during a switch the previous data table's, + // and in between the same data table asked as another role. + const accessSettled = $derived( + access.current.datatable === selectedDatatable && access.current.role === accessRole + ) const loadedAccess = $derived( - access.current.datatable === selectedDatatable + accessSettled ? access.current - : { datatable: selectedDatatable, schemas: [], canCreateSchema: false } + : { datatable: selectedDatatable, role: accessRole, schemas: [], canCreateSchema: false } ) - const accessSettled = $derived(access.current.datatable === selectedDatatable) const availableSchemas = $derived(loadedAccess.schemas) const canCreateSchema = $derived(loadedAccess.canCreateSchema) @@ -512,7 +519,10 @@ variant="default" size="sm" on:click={() => start(false)} - disabled={!templates[selectedTemplateIndex] || newSchemaAlreadyExists || !rolesSettled} + disabled={!templates[selectedTemplateIndex] || + newSchemaAlreadyExists || + !rolesSettled || + !accessSettled} > {$copilotInfo.workspaceDisabled ? 'Start' : 'Start without AI'} @@ -521,6 +531,7 @@ variant="accent" on:click={() => start(true)} disabled={!rolesSettled || + !accessSettled || !templates[selectedTemplateIndex] || !initialPrompt.trim() || newSchemaAlreadyExists} diff --git a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts index 8a9cb9574b..c411b2d707 100644 --- a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts +++ b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts @@ -90,15 +90,18 @@ export function createDatatableAccessResource( return resource( () => [getDatatable() ?? '', getRole() ?? '', getWorkspace() ?? ''] as const, async ([datatable, role, workspace]): Promise<{ - /** What this answers for: until it matches the selection, the schemas and - * the right to create one are the previous data table's, and at mount - * they are the initial value rather than an answer at all. */ + /** What this answers for — both halves of it. Until they match the + * selection, the schemas and the right to create one belong to another + * data table or to another role, and at mount they are the initial value + * rather than an answer at all. What a role may create in is exactly the + * question, so an answer computed as a different one settles nothing. */ datatable: string | undefined + role: string | undefined schemas: string[] canCreateSchema: boolean }> => { - if (!datatable || !workspace) - return { datatable: datatable || undefined, schemas: [], canCreateSchema: false } + const asked = { datatable: datatable || undefined, role: role || undefined } + if (!datatable || !workspace) return { ...asked, schemas: [], canCreateSchema: false } try { const tables = await WorkspaceService.listDataTableTables({ workspace, @@ -107,16 +110,16 @@ export function createDatatableAccessResource( }) const entry = tables.find((t) => t.datatable_name === datatable) return { - datatable, + ...asked, schemas: Object.keys(entry?.schemas ?? {}).sort(), canCreateSchema: !!entry?.can_create_schema } } catch (e) { console.error('Failed to load datatable access:', e) - return { datatable, schemas: [], canCreateSchema: false } + return { ...asked, schemas: [], canCreateSchema: false } } }, - { initialValue: { datatable: undefined, schemas: [], canCreateSchema: false } } + { initialValue: { datatable: undefined, role: undefined, schemas: [], canCreateSchema: false } } ) }