mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
fix: base a new fork on the dev workspace when forking from one (#10489)
* fix: base a new fork on the dev workspace when forking from one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry dev-workspace fields on the superadmin-synthesized entry 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:
co-authored by
Claude Opus 5
parent
f365929eaa
commit
db46d34727
@@ -12,6 +12,7 @@
|
||||
import {
|
||||
findWorkspaceDescendants,
|
||||
findCanonicalDevWorkspace,
|
||||
findDefaultForkBase,
|
||||
findWorkspaceRoot,
|
||||
buildWorkspaceHierarchy
|
||||
} from '$lib/utils/workspaceHierarchy'
|
||||
@@ -107,6 +108,9 @@
|
||||
const devOfRoot = $derived(
|
||||
root ? findCanonicalDevWorkspace(root.id, forkableWorkspaces) : undefined
|
||||
)
|
||||
// Base a new fork gets by default: the dev workspace when the selection sits in its subtree, the
|
||||
// family root otherwise.
|
||||
const defaultForkBase = $derived(findDefaultForkBase(effectiveId, forkableWorkspaces))
|
||||
const createForkLabel = 'Create new fork…'
|
||||
// Candidate bases ("targets") for a new fork: the root plus every fork/dev in the family, so a fork
|
||||
// can itself be the base — i.e. a fork of a fork. Root first, matching the list order below.
|
||||
@@ -205,8 +209,8 @@
|
||||
// Bare fork id, without the wm-fork- prefix. Forks have no separate display
|
||||
// name — the id is the name (it also becomes the git branch).
|
||||
let newForkId = $state('')
|
||||
// The base ("target") a new fork will branch from. Defaults to the root; the user can pick any fork
|
||||
// in the family (via the inline target selector) to create a fork of a fork.
|
||||
// The base ("target") a new fork will branch from. Defaults to `defaultForkBase`; the user can pick
|
||||
// any fork in the family (via the inline target selector) to create a fork of a fork.
|
||||
let createForkBaseId = $state<string | undefined>(undefined)
|
||||
|
||||
// Manual keyboard navigation, modelled after SelectDropdown. melt's
|
||||
@@ -266,8 +270,8 @@
|
||||
}
|
||||
|
||||
function enterCreateMode(initialId?: string, baseId?: string) {
|
||||
// Default the target to the root; the user can switch to any fork in the family (fork of a fork).
|
||||
createForkBaseId = baseId ?? root?.id
|
||||
// The user can switch to any fork in the family (fork of a fork).
|
||||
createForkBaseId = baseId ?? defaultForkBase?.id
|
||||
creatingFork = true
|
||||
newForkId = initialId ?? defaultForkId()
|
||||
// Focus + select is handled by the input's own autofocus (it mounts with
|
||||
@@ -338,8 +342,8 @@
|
||||
const wasOpen = lastDropdownOpen
|
||||
lastDropdownOpen = dropdownOpen
|
||||
if (dropdownOpen && !wasOpen && pendingFork && !creatingFork && showCreateFork) {
|
||||
// Preserve the base the fork was staged from; without this the re-entry defaults to the root
|
||||
// and silently re-parents a fork that was staged off another fork.
|
||||
// Preserve the base the fork was staged from; without this the re-entry falls back to the
|
||||
// default base and silently re-parents a fork that was staged off another workspace.
|
||||
void enterCreateMode(
|
||||
pendingFork.id.startsWith(WM_FORK_PREFIX)
|
||||
? pendingFork.id.slice(WM_FORK_PREFIX.length)
|
||||
@@ -475,8 +479,8 @@
|
||||
{#if showCreateFork}
|
||||
<div class="my-1 border-t border-border-light shrink-0"></div>
|
||||
{#if creatingFork}
|
||||
<!-- Small inline form with labels: fork id + base ("target") workspace. The base
|
||||
defaults to the root; picking a fork there creates a fork of a fork. -->
|
||||
<!-- Small inline form with labels: fork id + base ("target") workspace. Picking a
|
||||
fork as the base creates a fork of a fork. -->
|
||||
<div class="flex flex-col gap-2 px-2.5 py-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-2xs font-normal text-hint">Fork ID</span>
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
import {
|
||||
workspaceIsFork,
|
||||
findWorkspaceRoot,
|
||||
findWorkspaceDescendants
|
||||
findWorkspaceDescendants,
|
||||
findDefaultForkBase
|
||||
} from '$lib/utils/workspaceHierarchy'
|
||||
import { useForkableWorkspaces } from '$lib/utils/useForkableWorkspaces.svelte'
|
||||
import {
|
||||
@@ -115,7 +116,9 @@
|
||||
subtitle: w.is_dev_workspace ? 'dev workspace' : w.id === familyRoot?.id ? undefined : 'fork'
|
||||
}))
|
||||
)
|
||||
let defaultBaseWorkspaceId = $derived(familyRoot?.id)
|
||||
let defaultBaseWorkspaceId = $derived(
|
||||
findDefaultForkBase($workspaceStore, forkableWorkspaces)?.id
|
||||
)
|
||||
// Seed the base once the family is known; keep an explicit user choice as long as it stays valid.
|
||||
$effect(() => {
|
||||
if (!isFork) return
|
||||
@@ -896,8 +899,8 @@
|
||||
{#if createAsDevWorkspace}
|
||||
A dev workspace is always based on the root workspace.
|
||||
{:else}
|
||||
Workspace to fork from. Defaults to the root; pick an existing fork to create a fork
|
||||
of a fork (the new branch is based on the selected workspace's branch).
|
||||
Workspace to fork from: the new branch is based on the selected workspace's branch.
|
||||
Pick an existing fork to create a fork of a fork.
|
||||
{/if}
|
||||
</span>
|
||||
<Select
|
||||
|
||||
@@ -42,6 +42,10 @@ export function useForkableWorkspaces(args: {
|
||||
username: '',
|
||||
color: w.color ?? undefined,
|
||||
parent_workspace_id: w.parent_workspace_id,
|
||||
// Dev-ness travels with the entry: the fork-base default and the "dev" badge both read
|
||||
// it off the list, and a synthesized entry missing it reads as an ordinary fork.
|
||||
is_dev_workspace: w.is_dev_workspace,
|
||||
dev_workspace_label: w.dev_workspace_label,
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { findDefaultForkBase } from './workspaceHierarchy'
|
||||
import type { UserWorkspace } from '../stores'
|
||||
|
||||
function ws(id: string, parent?: string, extra: Partial<UserWorkspace> = {}): UserWorkspace {
|
||||
return { id, name: id, username: 'u', parent_workspace_id: parent, disabled: false, ...extra }
|
||||
}
|
||||
|
||||
const root = ws('prod')
|
||||
const dev = ws('devws', 'prod', { is_dev_workspace: true })
|
||||
const forkOfDev = ws('wm-fork-a', 'devws')
|
||||
const forkOfRoot = ws('wm-fork-b', 'prod')
|
||||
const family = [root, dev, forkOfDev, forkOfRoot]
|
||||
|
||||
describe('findDefaultForkBase', () => {
|
||||
it('bases a fork on the dev workspace from inside its subtree', () => {
|
||||
expect(findDefaultForkBase('devws', family)?.id).toBe('devws')
|
||||
expect(findDefaultForkBase('wm-fork-a', family)?.id).toBe('devws')
|
||||
})
|
||||
|
||||
it('bases a fork on the root outside the dev subtree', () => {
|
||||
expect(findDefaultForkBase('prod', family)?.id).toBe('prod')
|
||||
expect(findDefaultForkBase('wm-fork-b', family)?.id).toBe('prod')
|
||||
})
|
||||
|
||||
it('skips a dev workspace the user is disabled in', () => {
|
||||
const disabledDev = [root, { ...dev, disabled: true }, forkOfDev]
|
||||
expect(findDefaultForkBase('wm-fork-a', disabledDev)?.id).toBe('prod')
|
||||
})
|
||||
})
|
||||
@@ -168,6 +168,31 @@ export function findCanonicalDevWorkspace(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace a new fork should branch from by default. Inside a dev workspace's subtree the base is
|
||||
* that dev workspace, not the family root: the dev workspace carries the changes being worked on, and
|
||||
* a prod locked against forking (`lock_prod_forking`) rejects a root-based fork outright. Anywhere
|
||||
* else it is the family root, so an ad-hoc fork branches from prod rather than from whichever
|
||||
* throwaway fork happens to be open. A dev workspace the user is disabled in is skipped — forking from
|
||||
* it would fail — and the walk continues upwards.
|
||||
*/
|
||||
export function findDefaultForkBase(
|
||||
currentWorkspaceId: string | undefined,
|
||||
allWorkspaces: UserWorkspace[]
|
||||
): UserWorkspace | undefined {
|
||||
let current = allWorkspaces.find((w) => w.id === currentWorkspaceId)
|
||||
while (current) {
|
||||
if (current.is_dev_workspace && !current.disabled) return current
|
||||
if (!current.parent_workspace_id) break
|
||||
const parent: UserWorkspace | undefined = allWorkspaces.find(
|
||||
(w) => w.id === current!.parent_workspace_id
|
||||
)
|
||||
if (!parent) break
|
||||
current = parent
|
||||
}
|
||||
return findWorkspaceRoot(currentWorkspaceId, allWorkspaces)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to find all descendants of a workspace
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user