fix(datatables): an answer about another role settles nothing

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
This commit is contained in:
Diego Imbert
2026-09-04 14:49:51 +02:00
co-authored by Claude Opus 5
parent 8868c5ff72
commit 2d8a99d2a3
2 changed files with 30 additions and 16 deletions
@@ -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'}
</Button>
@@ -521,6 +531,7 @@
variant="accent"
on:click={() => start(true)}
disabled={!rolesSettled ||
!accessSettled ||
!templates[selectedTemplateIndex] ||
!initialPrompt.trim() ||
newSchemaAlreadyExists}
@@ -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 } }
)
}