fix: discard acting-user lookups that no longer describe the acting workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY
This commit is contained in:
Diego Imbert
2026-09-08 20:28:59 +02:00
co-authored by Claude Opus 5
parent abd50563c6
commit 9166bc06d3
4 changed files with 25 additions and 11 deletions
@@ -336,9 +336,12 @@
if (!ws || !initialPath) return
if (ws in states) return
untrack(() => {
// `actingUserIn` answers from `$userStore` for the navigation workspace, so only
// another one is worth asking.
const needsUser = ws !== $workspaceStore
Promise.all([
ResourceService.getResource({ workspace: ws, path: initialPath, getDraft: true }),
getUserExt(ws)
needsUser ? getUserExt(ws) : undefined
]).then(([r, user]) => {
// `.draft` already holds the editor's `ResourceState` shape.
const savedDraftState = (r as any).draft as ResourceState | undefined
@@ -366,7 +369,7 @@
// Draft-only paths (`no_deployed`) have no row — saving must
// CREATE, not update (update 404s).
existedInitially[ws] = !(r as any).no_deployed
perWsUser[ws] = user
if (needsUser) perWsUser[ws] = user
// Keep resource_type in sync for the base workspace (controls the schema)
if (ws === effectiveWorkspace) {
resource_type = r.resource_type
@@ -69,9 +69,13 @@
() => (path && historyWorkspace !== $workspaceStore ? historyWorkspace : undefined),
async (ws) => (ws ? await getUserExt(ws) : undefined)
)
const historyUser = $derived(
historyWorkspace === $workspaceStore ? $userStore : otherWsUser.current
)
const historyUser = $derived.by(() => {
if (historyWorkspace === $workspaceStore) return $userStore
const u = otherWsUser.current
// `resource` keeps the previous result across a refetch, and a superseded lookup can
// still land last, so a user only answers for the workspace they were fetched for.
return u?.workspace_id === historyWorkspace ? u : undefined
})
// Clearing is irreversible and the backend gates it on ownership, not write access, so the
// verdict has to come from the membership `historyWorkspace` knows about. An unresolved
// user gets no Clear button rather than one computed from another workspace's rights.
@@ -181,6 +181,9 @@
if (!ws || !p) return
if (ws in states) return
untrack(() => {
// `actingUserIn` answers from `$userStore` for the navigation workspace, so only
// another one is worth asking.
const needsUser = ws !== $workspaceStore
Promise.all([
VariableService.getVariable({
workspace: ws,
@@ -188,7 +191,7 @@
decryptSecret: false,
getDraft: true
}),
getUserExt(ws)
needsUser ? getUserExt(ws) : undefined
]).then(([v, user]) => {
// `.draft` already holds the editor's `VariableState` shape.
const savedDraftState = (v as any).draft as VariableState | undefined
@@ -212,7 +215,7 @@
// CREATE, not update (update 404s).
existedInitially[ws] = !(v as any).no_deployed
extraPerms[ws] = v.extra_perms ?? {}
perWsUser[ws] = user
if (needsUser) perWsUser[ws] = user
})
})
})
@@ -147,9 +147,13 @@
)
// `undefined` while that lookup is in flight or after it failed; the checks below then
// refuse rather than fall back to rights that belong to another workspace.
const actingUser: UserExt | undefined = $derived(
wsId === $workspaceStore ? $userStore : otherWsUser.current
)
const actingUser: UserExt | undefined = $derived.by(() => {
if (wsId === $workspaceStore) return $userStore
const u = otherWsUser.current
// `resource` keeps the previous result across a refetch, and a superseded lookup can
// still land last, so a user only answers for the workspace they were fetched for.
return u?.workspace_id === wsId ? u : undefined
})
const can_write = $derived(
permsPath === undefined ? true : canWrite(permsPath, extraPerms, actingUser)
)
@@ -859,7 +863,7 @@
namePlaceholder="schedule"
kind="schedule"
disableEditing={!can_write}
{actingUser}
actingUser={actingUser ?? null}
/>
{:else}
<div class="flex justify-start w-full">