mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 08:02:53 +00:00
* refactor: make the acting workspace and user explicit in the entity editors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: resolve the acting user in new-item mode and for the navigation workspace Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * 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 * refactor: own the acting-user resolution in one composable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: key the acting-user cache by a Map and re-ask after a failed lookup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: re-ask a failed acting-user lookup when an editor opens a new session Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: forget a failed acting-user lookup when its workspace stops being the acting one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: drop a stale acting-user refusal on arrival rather than on departure Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * fix: let the navigation user answer for the navigation workspace unconditionally Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY * docs: mark prototype-key workspace ids as unsupported by the entity editors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLxwAsiXJ1Au8CBDBmH7iY --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
import { untrack } from 'svelte'
|
|
import { fromStore } from 'svelte/store'
|
|
import { SvelteMap } from 'svelte/reactivity'
|
|
import { userStore, workspaceStore, type UserExt } from '$lib/stores'
|
|
import { getWorkspaceRole, type RoleLookup } from '$lib/user'
|
|
|
|
/**
|
|
* The user acting in a workspace that is not necessarily the one the top nav points at — an AI
|
|
* session or a workspace-specific variant acts on a workspace the nav deliberately is not on.
|
|
*
|
|
* `$userStore` answers for the navigation workspace at no cost, exactly as every permission check
|
|
* in the app did before this hook existed — including when it holds nobody, which reads as unknown
|
|
* and refuses. Every other workspace is looked up, and an unresolved user there is `undefined`: it
|
|
* must never fall back to the navigation user, whose rights belong to another workspace.
|
|
* `canWrite`/`isOwner` refuse for an unknown user, which is the only safe answer. A caller that
|
|
* must not render that refusal as a denial asks `resolved` first.
|
|
*/
|
|
export function useActingUser(workspace: () => string | undefined) {
|
|
const navWorkspace = fromStore(workspaceStore)
|
|
const navUser = fromStore(userStore)
|
|
const looked = new SvelteMap<string, RoleLookup>()
|
|
// The workspace this effect last acted on, so arriving at one is distinguishable from the
|
|
// effect re-running while already there.
|
|
let asking: string | undefined
|
|
|
|
$effect(() => {
|
|
const ws = workspace()
|
|
if (asking !== ws) {
|
|
asking = ws
|
|
// Dropped on the way *in*, not on the way out: a lookup that fails after the acting
|
|
// workspace has already moved on has no entry to clear at the moment it is left, so
|
|
// clearing it there would keep a refusal that no attempt is behind any more.
|
|
if (ws && untrack(() => looked.get(ws)?.kind) === 'lookup_failed') looked.delete(ws)
|
|
}
|
|
if (!ws || ws === navWorkspace.current) return
|
|
// Any settled answer stops the asking, a failure included — otherwise recording one
|
|
// would re-enter this effect and loop.
|
|
if (looked.has(ws)) return
|
|
untrack(() => {
|
|
// Memoized process-wide, so two components pointed at the same workspace share one
|
|
// request rather than each issuing their own.
|
|
getWorkspaceRole(ws).then((lookup) => looked.set(ws, lookup))
|
|
})
|
|
})
|
|
|
|
function userIn(ws: string | undefined): UserExt | undefined {
|
|
if (!ws) return undefined
|
|
if (ws === navWorkspace.current) return navUser.current
|
|
const lookup = looked.get(ws)
|
|
return lookup?.kind === 'resolved' ? lookup.user : undefined
|
|
}
|
|
|
|
return {
|
|
/** The acting user in `ws`, or `undefined` when it is not known. Only workspaces this
|
|
* hook has been pointed at are looked up; the rest read as unknown. */
|
|
in: userIn,
|
|
/** Whether `ws` has an answer at all — a user, or a lookup that came back without one.
|
|
* The navigation workspace always has one: `$userStore`, "nobody" included. */
|
|
resolved: (ws: string | undefined): boolean =>
|
|
!!ws && (ws === navWorkspace.current || looked.has(ws)),
|
|
get current(): UserExt | undefined {
|
|
return userIn(workspace())
|
|
},
|
|
/** Drop the lookups that came back empty so they are asked again. Arriving at a
|
|
* workspace already does this; a long-lived editor must call this too when it starts a
|
|
* fresh session on the workspace it is already on, or a `whoami` that happened to fail
|
|
* pins it to "unknown user" for as long as it stays there. */
|
|
forgetFailures(): void {
|
|
for (const [ws, lookup] of looked) {
|
|
if (lookup.kind === 'lookup_failed') looked.delete(ws)
|
|
}
|
|
}
|
|
}
|
|
}
|