From d39d134222774b4f9b67b65855ea88f7049cd9de Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 13 Oct 2026 08:58:15 -0700 Subject: [PATCH] feat: show operators a disabled fork entry explaining the ruleset When no ruleset allows operators to fork, the workspace picker keeps a greyed-out "Workspace fork" entry whose tooltip says an admin can turn on "Allow operators to fork". It stays hidden when forking is disabled for everyone, as it is for developers. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../sidebar/WorkspacePickerBody.svelte | 32 +++++++++++++++---- frontend/src/lib/utils/editInFork.ts | 12 +++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte b/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte index 3719e35d35..5a893316e5 100644 --- a/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte +++ b/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte @@ -36,7 +36,11 @@ findWorkspaceAncestors, isForkOwner } from '$lib/utils/workspaceHierarchy' - import { canCreateFork } from '$lib/utils/editInFork' + import { + canCreateFork, + OPERATOR_FORK_BLOCKED_REASON, + operatorForkBlocked + } from '$lib/utils/editInFork' import { workspaceRootId } from '$lib/components/sessions/sessionScope.svelte' import DevWorkspaceBadge from '$lib/components/DevWorkspaceBadge.svelte' @@ -156,6 +160,11 @@ const canForkHere = $derived( (!isCloudHosted() || $maybePremium) && $workspaceStore !== 'admins' && canCreateFork($userStore) ) + const forkBlockedHere = $derived( + (!isCloudHosted() || $maybePremium) && + $workspaceStore !== 'admins' && + operatorForkBlocked($userStore) + ) // The row mechanics below — which family to expand, which collapsed row carries the tick — key on // a depth-0 row of the list, which `buildWorkspaceHierarchy` puts at the highest workspace the // caller can see: parentless, or with a parent absent from their list. `findWorkspaceAncestors` @@ -363,7 +372,7 @@ {/each} - {#if (isCloudHosted() || $superadmin || canForkHere) && !strictWorkspaceSelect} + {#if (isCloudHosted() || $superadmin || canForkHere || forkBlockedHere) && !strictWorkspaceSelect}
{#if isCloudHosted() || $superadmin} @@ -376,16 +385,25 @@ Workspace fork + {:else if forkBlockedHere} + +
+ + Workspace fork +
+ {#snippet text()} + {OPERATOR_FORK_BLOCKED_REASON} + {/snippet} +
{/if}
{/if} {#if canManageWorkspace && !strictWorkspaceSelect}
- + {(activeWorkspace && ambiguousNames.has(activeWorkspace.name) ? activeWorkspace.id diff --git a/frontend/src/lib/utils/editInFork.ts b/frontend/src/lib/utils/editInFork.ts index db38537115..2fe79ec03f 100644 --- a/frontend/src/lib/utils/editInFork.ts +++ b/frontend/src/lib/utils/editInFork.ts @@ -76,6 +76,18 @@ export function canCreateFork(user: UserExt | undefined): boolean { ) } +export const OPERATOR_FORK_BLOCKED_REASON = + 'Operators cannot fork this workspace. A workspace admin can allow it by turning on "Allow operators to fork" in a ruleset, under Workspace settings > Rulesets.' + +/** + * Whether the only thing refusing this user a fork is the missing `AllowOperatorForking`. The fork + * entries then stay visible, disabled, so the operator learns an admin can turn it on; when forking + * is disabled for everyone they stay hidden, as they are for developers. + */ +export function operatorForkBlocked(user: UserExt | undefined): boolean { + return !!user?.operator && !canCreateFork(user) && !isRuleActive('DisableWorkspaceForking') +} + function editPathFor(itemType: ItemType, itemPath: string): string { switch (itemType) { case 'script':