fix(frontend): make runs-page job links use the job's workspace

In the cross-workspace runs view a job can belong to a workspace other than
the active one. Several job-derived links resolved to the active workspace
(or, since the central sync, to a bare URL that the layout fills with the
active workspace), sending users to the wrong workspace or a 404.

Use job.workspace_id (fallback $workspaceStore) for:
- run detail page: "Go to script/flow" (viewHref), edit link, and the
  runs-filter button (runsHref)
- JobDetailHeader: parent-job link and all JobDetailFieldConfig getHref
  calls (were passed $workspaceStore)
- RunBadges: script-hash link
- FlowMetadata: parent/step links

Same-workspace jobs are unaffected (job.workspace_id == active); foreign
jobs now link correctly and the rendered hrefs are explicit for
copy/middle-click/new-tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-06-01 20:47:50 +02:00
co-authored by Claude Opus 4.8
parent 5ba9b80d4d
commit 73d7e1d922
4 changed files with 15 additions and 11 deletions
@@ -58,7 +58,9 @@
<BarsStaggered size={SMALL_ICON_SIZE} class="min-w-3.5" />
<span class="whitespace-nowrap">
Step of flow
<a href={`${base}/run/${job.parent_job}?workspace=${$workspaceStore}`}>
<a
href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id ?? $workspaceStore}`}
>
{truncateRev(job.parent_job, 18)}
</a>
</span>
@@ -68,7 +70,9 @@
<Bot size={SMALL_ICON_SIZE} class="min-w-3.5" />
<span class="whitespace-nowrap">
Triggered by parent
<a href={`${base}/run/${job.parent_job}?workspace=${$workspaceStore}`}>
<a
href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id ?? $workspaceStore}`}
>
{job.parent_job}</a
>
</span>
@@ -240,7 +240,7 @@
Triggered by
{/if}
<a
href={`${base}/run/${job.parent_job}?workspace=${$workspaceStore}`}
href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id ?? $workspaceStore}`}
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1"
@@ -362,7 +362,7 @@
{#each expandedFields as config}
{@const displayValue = getDisplayValue(config, job)}
{@const fullValue = getFullValue(config, job)}
{@const href = config.getHref?.(job, $workspaceStore || '')}
{@const href = config.getHref?.(job, job.workspace_id ?? $workspaceStore ?? '')}
<div class="flex items-baseline gap-1 text-xs">
<span class="text-secondary flex-shrink-0">{config.label}</span>
@@ -467,7 +467,7 @@
{#each fields as config}
{@const displayValue = getDisplayValue(config, job)}
{@const fullValue = getFullValue(config, job)}
{@const href = config.getHref?.(job, $workspaceStore || '')}
{@const href = config.getHref?.(job, job.workspace_id ?? $workspaceStore ?? '')}
<div class="flex items-baseline gap-3 text-xs">
<span class="text-secondary min-w-[110px] flex-shrink-0">
@@ -520,7 +520,7 @@
{#each fields as config (config.field)}
{@const displayValue = getDisplayValue(config, job)}
{@const fullValue = getFullValue(config, job)}
{@const href = config.getHref?.(job, $workspaceStore || '')}
{@const href = config.getHref?.(job, job.workspace_id ?? $workspaceStore ?? '')}
<!-- Field -->
<div class="flex items-baseline gap-1 text-xs min-w-0">
@@ -568,7 +568,7 @@
{#each expandedFields as config}
{@const displayValue = getDisplayValue(config, job)}
{@const fullValue = getFullValue(config, job)}
{@const href = config.getHref?.(job, $workspaceStore || '')}
{@const href = config.getHref?.(job, job.workspace_id ?? $workspaceStore ?? '')}
<div class="flex items-baseline gap-3 text-xs">
<span class="text-secondary min-w-[110px] flex-shrink-0">
@@ -33,7 +33,7 @@
{#if job.script_hash && showScriptHash && job.job_kind !== 'aiagent'}
{#if job.job_kind == 'script'}
<a href="{base}/scripts/get/{job.script_hash}?workspace={$workspaceStore}"
<a href="{base}/scripts/get/{job.script_hash}?workspace={job.workspace_id ?? $workspaceStore}"
><Badge color="gray" title={`Script hash: ${job.script_hash}`} {large}
>{truncateHash(job.script_hash)}
</Badge></a
@@ -507,7 +507,7 @@
{#snippet right()}
{@const isScript = job?.job_kind === 'script'}
{@const isHubFlowPreview = isFlowPreview(job?.job_kind) && isHubFlowPath(job?.script_path)}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{@const runsHref = `/runs/${job?.script_path}?workspace=${job?.workspace_id ?? $workspaceStore}${!isScript ? '&jobKind=flow' : ''}`}
{#if job && 'deleted' in job && !job?.deleted && ($superadmin || ($userStore?.is_admin ?? false))}
<Dropdown
items={[
@@ -536,7 +536,7 @@
{/if}
{/if}
{@const stem = job?.job_kind === 'script_hub' ? '/scripts' : `/${job?.job_kind}s`}
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`}
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}?workspace=${job?.workspace_id ?? $workspaceStore}`}
{#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined}
<div class="inline">
<Dropdown
@@ -676,7 +676,7 @@
{#if !$userStore?.operator}
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button
href={`${stem}/edit/${job?.script_path}${isScript ? `` : `?nodraft=true`}`}
href={`${stem}/edit/${job?.script_path}?workspace=${job?.workspace_id ?? $workspaceStore}${isScript ? `` : `&nodraft=true`}`}
on:click={() => {
$initialArgsStore = job?.args
}}