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) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-13 08:58:15 -07:00
co-authored by Claude Opus 5.5
parent cd3efb86ed
commit d39d134222
2 changed files with 37 additions and 7 deletions
@@ -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 @@
</div>
{/each}
</div>
{#if (isCloudHosted() || $superadmin || canForkHere) && !strictWorkspaceSelect}
{#if (isCloudHosted() || $superadmin || canForkHere || forkBlockedHere) && !strictWorkspaceSelect}
<div class="py-1" role="none">
{#if isCloudHosted() || $superadmin}
<MenuItem href="/user/create_workspace" class={itemClass} {item}>
@@ -376,16 +385,25 @@
<Plus size={16} />
Workspace fork
</MenuItem>
{:else if forkBlockedHere}
<Tooltip class="block w-full" placement="right" openDelay={200}>
<div
class="{itemClass} opacity-60 cursor-not-allowed hover:bg-transparent"
aria-disabled="true"
>
<Plus size={16} />
Workspace fork
</div>
{#snippet text()}
<span class="block max-w-64 text-xs">{OPERATOR_FORK_BLOCKED_REASON}</span>
{/snippet}
</Tooltip>
{/if}
</div>
{/if}
{#if canManageWorkspace && !strictWorkspaceSelect}
<div class="py-1" role="none">
<MenuItem
href="/workspace_settings?workspace={$workspaceStore}"
class={itemClass}
{item}
>
<MenuItem href="/workspace_settings?workspace={$workspaceStore}" class={itemClass} {item}>
<Settings size={16} />
{(activeWorkspace && ambiguousNames.has(activeWorkspace.name)
? activeWorkspace.id
+12
View File
@@ -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':