fix(frontend): call a dev workspace a dev workspace in the merge UI (#10605)

* fix(frontend): call a dev workspace a dev workspace in the merge UI

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(frontend): drop the unreachable dev-workspace guard on the fork modals

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-08-10 19:30:24 +02:00
committed by GitHub
parent 85916cedf8
commit c725d62fb0
6 changed files with 52 additions and 21 deletions
@@ -29,7 +29,8 @@
useWorkspaceDrafts
} from '$lib/workspaceDrafts.svelte'
import type { Kind as LayoutKind } from '$lib/utils_deployable'
import { userStore } from '$lib/stores'
import { userStore, userWorkspaces } from '$lib/stores'
import { childWorkspaceNoun } from '$lib/utils/devWorkspaceLabel'
interface Props {
currentWorkspaceId: string
@@ -161,6 +162,10 @@
// (unrelated to the fork's own work) are the common case worth hiding.
let hideUnchanged = $state(true)
const currentNoun = $derived(
childWorkspaceNoun($userWorkspaces.find((w) => w.id === currentWorkspaceId))
)
// The list (and, in the default view, the Draft Count) come from the Workspace
// Drafts module; deploy/discard invalidate the resource, so the list refetches
// and deployed items drop off without a manual reload here.
@@ -663,8 +668,7 @@
size="xs"
options={{
right: 'Hide unchanged drafts',
rightTooltip:
"Hide drafts identical to the parent workspace. A fork inherits the parent's drafts when it's created; those are unrelated to the changes made in this fork."
rightTooltip: `Hide drafts identical to the parent workspace. A ${currentNoun} inherits the parent's drafts when it's created; those are unrelated to the changes made in this ${currentNoun}.`
}}
/>
{/if}
@@ -68,6 +68,7 @@
import CompareModeToggle, { type CompareMode } from './CompareModeToggle.svelte'
import CompareTargetPicker from './CompareTargetPicker.svelte'
import { displayDate } from '$lib/utils'
import { childWorkspaceNoun } from '$lib/utils/devWorkspaceLabel'
import { editUrlFor } from './sessions/forkEditUrl'
import { diffInMask } from './sessions/modifiedItemsMask'
import DatatableSchemaDiff from './DatatableSchemaDiff.svelte'
@@ -400,6 +401,8 @@
let currentWorkspaceInfo = $derived($userWorkspaces.find((w) => w.id == currentWorkspaceId))
let parentWorkspaceInfo = $derived($userWorkspaces.find((w) => w.id == parentWorkspaceId))
let currentNoun = $derived(childWorkspaceNoun(currentWorkspaceInfo))
// An arbitrary target is one-way: the pair has no tally, so nothing distinguishes
// a change made here from one made there, and the "update current" direction
// would list every difference as an incoming change.
@@ -448,7 +451,7 @@
? `No changes between this workspace and ${parentWorkspaceId}.`
: mergeIntoParent
? `Nothing to deploy — ${parentWorkspaceId} already has every change from this workspace.`
: `Nothing to update — this fork is up to date with ${parentWorkspaceId}.`
: `Nothing to update — this ${currentNoun} is up to date with ${parentWorkspaceId}.`
)
let conflictingDiffs = $derived(
@@ -1437,7 +1440,8 @@
<span>
{#if mergeIntoParent}
This workspace has {draftCount} undeployed draft{draftCount !== 1 ? 's' : ''}.
Only deployed versions in this fork can be sent to {parentWorkspaceId} deploy
Only deployed versions in this {currentNoun} can be sent to {parentWorkspaceId}
deploy
{draftCount !== 1 ? 'them' : 'it'} first, otherwise those changes won't be included.
{:else}
This workspace has {draftCount} undeployed draft{draftCount !== 1 ? 's' : ''}.
@@ -1461,14 +1465,14 @@
<Alert title="Conflicting changes detected" type="warning" class="mt-2">
<span>
{conflictingDiffs.length} item{conflictingDiffs.length !== 1 ? 's have' : ' has'} conflicting
changes, it was modified on the original workspace while changes were made on this fork.
Make sure to resolve these before merging.
changes, it was modified on the original workspace while changes were made on this
{currentNoun}. Make sure to resolve these before merging.
</span>
</Alert>
{/if}
{#if hasBehindChanges && hasAheadChanges && !(mergeIntoParent && !canDeployToParent)}
<Alert
title="This fork is behind {parentWorkspaceId} and needs to be up to date before deploying"
title="This {currentNoun} is behind {parentWorkspaceId} and needs to be up to date before deploying"
type="warning"
class="my-2"
>
@@ -13,6 +13,7 @@
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { sendUserToast } from '$lib/toast'
import { userWorkspaces } from '$lib/stores'
import { childWorkspaceNoun } from '$lib/utils/devWorkspaceLabel'
import { runScriptAndPollResult } from '$lib/components/jobs/utils'
import { writingJobOptions } from '$lib/components/jobs/writingJob'
import YAML from 'yaml'
@@ -34,6 +35,11 @@
let { currentWorkspaceId, parentWorkspaceId }: Props = $props()
let currentNoun = $derived(
childWorkspaceNoun($userWorkspaces.find((w) => w.id === currentWorkspaceId))
)
let currentNounCap = $derived(currentNoun.charAt(0).toUpperCase() + currentNoun.slice(1))
let loading = $state(true)
let error: string | undefined = $state(undefined)
let diffs: DatatableDiff[] = $state([])
@@ -321,8 +327,9 @@
<div class="border-t divide-y">
{#if diff.aheadChanges.length > 0}
<div class="px-3 py-1.5">
<div class="text-2xs font-semibold text-blue-500 mb-1">Fork changes (ahead)</div
>
<div class="text-2xs font-semibold text-blue-500 mb-1">
{currentNounCap} changes (ahead)
</div>
{#each diff.aheadChanges as change}
<div class="flex items-center gap-2 text-xs py-0.5">
{#if change.kind === 'added'}
@@ -393,8 +400,8 @@
<DrawerContent
on:close={() => (drawerOpen = false)}
title="{drawerChange.schemaName}.{drawerChange.tableName} ({drawerDirection === 'ahead'
? 'Fork → Parent'
: 'Parent → Fork'})"
? `${currentNounCap} → Parent`
: `Parent → ${currentNounCap}`})"
>
{#snippet actions()}
<Button
@@ -422,7 +429,7 @@
<!-- Diff section -->
<div style="height: 45%;">
<div class="py-1.5 text-2xs font-semibold text-secondary">
Schema diff (parent ↔ fork)
Schema diff (parent ↔ {currentNoun})
</div>
<div class="h-[calc(100%-28px)] border rounded-md overflow-clip">
{#await import('$lib/components/DiffEditor.svelte')}
@@ -8,7 +8,7 @@
import { goto } from '$app/navigation'
import { onMount, untrack } from 'svelte'
import { useWorkspaceDrafts } from '$lib/workspaceDrafts.svelte'
import { devLabelWord } from '$lib/utils/devWorkspaceLabel'
import { childWorkspaceNoun, devLabelWord } from '$lib/utils/devWorkspaceLabel'
import { diffActionableInDirection } from '$lib/utils_workspace_deploy'
let loading = $state(false)
@@ -26,6 +26,7 @@
// prefix) also avoids a parentless "Fork of ()" banner when the linkage is dropped.
let isFork = $derived(parentWorkspaceId != null)
let isDevWorkspace = $derived(currentWorkspaceData?.is_dev_workspace ?? false)
let currentNoun = $derived(childWorkspaceNoun(currentWorkspaceData))
// Operators run scripts and flows, they never deploy a fork, so the banner and
// its CTA are noise for them. Gates the fetches too, not just the markup: the
// fork/parent comparison is an expensive tally no operator can act on.
@@ -233,7 +234,7 @@
function forkAheadBehindMessage(changesAhead: number, changesBehind: number) {
let msg: string[] = []
if (changesAhead > 0 || changesBehind > 0) {
msg.push('This fork is ')
msg.push(`This ${currentNoun} is `)
if (changesAhead > 0)
msg.push(`${changesAhead} change${changesAhead > 1 ? 's' : ''} ahead of `)
if (changesAhead > 0 && changesBehind > 0) msg.push('and ')
@@ -400,7 +401,7 @@
{/if}
{:else if comparison.skipped_comparison}
<span class="text-blue-600 dark:text-blue-200">
This fork was created before the addition of certain windmill features, and
This {currentNoun} was created before the addition of certain windmill features, and
therefore the changes with its parent workspace cannot be displayed.</span
>
{:else if showDraftsOnly}
@@ -425,7 +426,7 @@
{:else if !hasAnswer || changesAhead > 0}
Review & Deploy Changes
{:else}
Review & Update fork
Review & Update {currentNoun}
{/if}
</Button>
</div>
@@ -41,3 +41,14 @@ export function devLabelWord(label: string | null | undefined): string {
export function devLabelNoun(label: string | null | undefined): string {
return `${devLabelKey(label)} workspace`
}
/**
* How to name a child workspace in prose: its environment noun when it is a dev workspace
* ("dev workspace", "staging workspace"), "fork" otherwise. A dev workspace is a standing
* environment its whole team works in, so calling it a fork misreads it as throwaway.
*/
export function childWorkspaceNoun(
workspace: { is_dev_workspace?: boolean; dev_workspace_label?: string | null } | undefined
): string {
return workspace?.is_dev_workspace ? devLabelNoun(workspace.dev_workspace_label) : 'fork'
}
@@ -51,6 +51,10 @@
// prefix. Distinct from having a compare target: a root workspace has no parent
// yet can still be pointed at an arbitrary one.
const isFork = $derived(!!parentWorkspaceId)
// A dev workspace is a standing environment, torn down by detaching it in the
// dev-workspace settings — never by an archive/delete button sitting next to the
// merge it is here to perform.
const isDevWorkspace = $derived(!!currentWorkspaceData?.is_dev_workspace)
const hasCompareTarget = $derived(!!compareTargetId)
// Mode is seeded from the URL (?mode=draft|fork). `draft` is valid for any
@@ -389,10 +393,10 @@
<CenteredPage>
<PageHeader title="Compare & Deploy">
<div class="flex flex-row gap-2 items-center">
<!-- The merged compare toggle (fork direction + deployed↔draft) now lives
inside each comparison card; only the fork lifecycle actions remain
in the page header. -->
{#if isFork}
<!-- The merged compare toggle (fork direction + deployed↔draft) lives inside
each comparison card; only the fork lifecycle actions remain in the page
header, and only for a throwaway fork. -->
{#if isFork && !isDevWorkspace}
<Button
variant="default"
color="light"