Compare commits

...
Author SHA1 Message Date
Guilhem LemouelandClaude Opus 5 18003b37a0 fix: drop the unreachable no-ruleset tooltip fallback
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:35:37 +02:00
Guilhem Lemouel 6e4d853a8f Merge remote-tracking branch 'origin/main' into glm/edit-dropdown
# Conflicts:
#	frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte
2026-08-14 09:32:51 +02:00
Guilhem LemouelandClaude Opus 5 6a06eb54f4 fix: name the rule blocking the run page edit entry
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:34:46 +02:00
Guilhem LemouelandClaude Opus 5 8213774fa8 fix: explain why the run page edit entry is disabled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:04:41 +02:00
Guilhem LemouelandClaude Opus 5 be9d09ce95 refactor: move run page edit button into view script/flow dropdown
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 09:43:00 +02:00
@@ -107,9 +107,12 @@
import JobOtelTraces from '$lib/components/JobOtelTraces.svelte'
import {
canUserBypassRuleKind,
getActiveRulesetsForKind,
isRuleActive,
protectionRulesState
} from '$lib/workspaceProtectionRules.svelte'
import { findCanonicalDevWorkspace } from '$lib/utils/workspaceHierarchy'
import { devLabelNoun } from '$lib/utils/devWorkspaceLabel'
import {
buildForkEditUrl,
editInForkAllowed,
@@ -563,6 +566,62 @@
let showEditButton = $derived(!isRuleActive('DisableDirectDeployment'))
// What blocks the edit, for the disabled entry's tooltip: the dev workspace edits are funneled
// through if there is one, else the rules holding this workspace shut. Same split as
// NoDirectDeployAlert's popover, which is where the user goes for the bypass toggle.
let editDisabledReason = $derived.by(() => {
const dev = findCanonicalDevWorkspace($workspaceStore, $userWorkspaces)
if (dev) {
return `Edits to this workspace are made in its ${devLabelNoun(dev.dev_workspace_label)} ${dev.name} (${dev.id}) and promoted here.`
}
// Non-empty wherever this is read: the entry is only disabled when the same rulesets say so.
const rulesets = getActiveRulesetsForKind('DisableDirectDeployment')
const names = rulesets.map((r) => r.name).join(', ')
return rulesets.length > 1
? `The rules ${names} restrict direct edits to this workspace.`
: `The rule ${names} restricts direct edits to this workspace.`
})
// Ways to edit what this run executed, offered from the "View" button's dropdown. Only a
// deployed script or flow has an editor to open — hub scripts and previews have no path in
// this workspace to edit.
let editDropdownItems = $derived.by(() => {
const kind = job?.job_kind
const path = job?.script_path ?? ''
if ((kind !== 'script' && kind !== 'flow') || $userStore?.operator) return []
return [
...(canWrite(path, {}, $userStore)
? [
{
label: `Edit ${kind}`,
icon: Pen,
href: `/${kind}s/edit/${path}?workspace=${$workspaceStore}`,
disabled: !showEditButton,
// Only while disabled: an enabled item renders this as an ⓘ next to the label,
// which would be noise on an entry that needs no explanation.
tooltip: showEditButton ? undefined : editDisabledReason,
onClick: () => {
$initialArgsStore = job?.args
}
}
]
: []),
...(!showEditButton && !isCloudHosted() && editInForkAllowed($workspaceStore, $userWorkspaces)
? [
{
label: editInForkLabel($workspaceStore, $userWorkspaces),
icon: Pen,
// The href is what a modifier click opens, and where the no-dev-workspace case
// lands; a plain click preventDefaults synchronously and resolves the real
// destination asynchronously instead — hence `hasHref`.
href: buildForkEditUrl(kind, path),
onClick: (e?: Event) => onEditInForkClick(e, kind, path, { hasHref: true })
}
]
: [])
]
})
// Admins always pass the backend gate. Everyone else fails closed while the rulesets
// are still loading, so the item is never briefly offered to a restricted user.
let canSharePublicly = $derived(
@@ -928,53 +987,24 @@
Run again
</Button>
{/if}
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
{#if !$userStore?.operator}
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button
href={`${stem}/edit/${job?.script_path}?workspace=${$workspaceStore}`}
on:click={() => {
$initialArgsStore = job?.args
}}
unifiedSize="md"
variant="default"
disabled={!showEditButton}
size="sm"
startIcon={{ icon: Pen }}>Edit</Button
>
{#if showEditButton}
<!-- Opens the deployed runnable at this job's path, like Edit — unlike
"View script", which pins the hash this run executed. Same gate as
Edit: where direct deployment is off, the way in is "Edit in fork". -->
<OpenInSessionButton
source={{
target: { kind: isScript ? 'script' : 'flow', path: job?.script_path ?? '' },
workspaceId: $workspaceStore ?? undefined
}}
btnProps={{ unifiedSize: 'md' }}
/>
{/if}
{/if}
{#if !showEditButton && !isCloudHosted() && editInForkAllowed($workspaceStore, $userWorkspaces)}
<Button
href={buildForkEditUrl(isScript ? 'script' : 'flow', job?.script_path ?? '')}
onClick={(e) =>
onEditInForkClick(e, isScript ? 'script' : 'flow', job?.script_path ?? '', {
hasHref: true
})}
unifiedSize="md"
variant="default"
size="sm"
startIcon={{ icon: Pen }}>{editInForkLabel($workspaceStore, $userWorkspaces)}</Button
>
{/if}
{/if}
{#if (job?.job_kind === 'script' || job?.job_kind === 'flow') && !$userStore?.operator && canWrite(job?.script_path ?? '', {}, $userStore) && showEditButton}
<!-- Opens the deployed runnable at this job's path, like the dropdown's Edit entry —
unlike "View script", which pins the hash this run executed. Same gate as Edit:
where direct deployment is off, the way in is "Edit in fork". -->
<OpenInSessionButton
source={{
target: { kind: isScript ? 'script' : 'flow', path: job?.script_path ?? '' },
workspaceId: $workspaceStore ?? undefined
}}
btnProps={{ unifiedSize: 'md' }}
/>
{/if}
{#if job?.job_kind === 'script' || job?.job_kind === 'script_hub' || job?.job_kind === 'flow'}
<Button
href={viewHref}
unifiedSize="md"
variant="accent"
dropdownItems={editDropdownItems}
startIcon={{
icon:
job?.job_kind === 'script' || job?.job_kind === 'script_hub'