Files
windmill/frontend/src/lib/components/CompareTargetPicker.svelte
T
Ruben FiszelandClaude Opus 5 61f2d8dc6a feat: let the merge UI target an arbitrary workspace (#10417)
* feat: let the merge UI target an arbitrary workspace

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

* fix: keep the target picker reachable when a comparison fails

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

* fix: address review findings on the arbitrary merge target

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

* fix: collapse app/raw-app conversions and offer a comparison retry

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

* fix: make a re-scan replace the candidate set and keep retry reachable

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

* fix: drop destructive rows from the selection when a recompute flips them

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

* fix: keep bulk selection and refreshes out of the removal opt-in

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

* fix: serialize a full scan against dev attachment on the same pair

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

* fix: keep the compare view reachable from drafts and prune stale selections

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

* fix: make the destination badge the target picker and reorder the settings

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

* fix: render the destination trigger as the same badge as the source

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:46:59 +02:00

119 lines
3.4 KiB
Svelte

<script lang="ts">
import { Building, Target } from 'lucide-svelte'
import Button from './common/button/Button.svelte'
import Select from './select/Select.svelte'
import Popover from './meltComponents/Popover.svelte'
import { userWorkspaces } from '$lib/stores'
interface Props {
/** The workspace being merged from — never a valid destination. */
currentWorkspaceId: string
/** The destination currently being compared against. */
targetWorkspaceId: string
/** The lineage destination, when this workspace has one. Offered as the way
* back from an arbitrary target, since only it has a continuous tally. */
parentWorkspaceId?: string
disabled?: boolean
onSelected: (target: string) => void
/** Replaces the default "Change target" button. Lets a caller that already
* names the destination on screen turn that into the trigger, rather than
* repeating it in a button beside it. */
triggerContent?: import('svelte').Snippet
}
let {
currentWorkspaceId,
targetWorkspaceId,
parentWorkspaceId,
disabled = false,
onSelected,
triggerContent
}: Props = $props()
let picked = $state<string | undefined>(undefined)
// A direct child (fork or dev workspace) is excluded: the pair is a lineage pair,
// which the scan refuses, so it would offer a "Compute diff" that can only fail.
// The lineage parent stays — picking it returns to the tallied comparison.
let candidates = $derived(
$userWorkspaces
.filter((w) => w.id !== currentWorkspaceId && w.parent_workspace_id !== currentWorkspaceId)
.map((w) => ({
label:
w.id === parentWorkspaceId
? `${w.name} (${w.id}), parent workspace`
: `${w.name} (${w.id})`,
value: w.id
}))
)
</script>
<Popover
floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}
{disabled}
closeButton
usePointerDownOutside
contentClasses="p-4"
>
{#snippet trigger()}
{#if triggerContent}
{@render triggerContent()}
{:else}
<Button
variant="subtle"
unifiedSize="xs"
nonCaptureEvent
{disabled}
startIcon={{ icon: Target }}
title="Compare against a workspace other than {parentWorkspaceId ?? 'the default target'}"
>
Change target
</Button>
{/if}
{/snippet}
{#snippet content({ close })}
<div class="flex flex-col gap-3 w-80">
<!-- pr-5 keeps the first line clear of the popover's close button. -->
<div class="text-xs text-secondary pr-5">
Merging into a workspace outside this one's lineage is meant for one-off migrations.
Windmill only tracks changes continuously between a workspace and its parent, so comparing
against any other target computes a full diff over every item in both workspaces.
</div>
<Select
items={candidates}
bind:value={picked}
placeholder="Select a workspace"
clearable
disablePortal
/>
<div class="flex items-center gap-2">
<Button
variant="accent"
unifiedSize="xs"
disabled={!picked || picked === targetWorkspaceId}
onclick={() => {
if (!picked) return
onSelected(picked)
close()
}}
>
Compare
</Button>
{#if parentWorkspaceId && targetWorkspaceId !== parentWorkspaceId}
<Button
variant="subtle"
unifiedSize="xs"
startIcon={{ icon: Building }}
onclick={() => {
onSelected(parentWorkspaceId)
close()
}}
>
Back to {parentWorkspaceId}
</Button>
{/if}
</div>
</div>
{/snippet}
</Popover>