mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
d0f23cc523
* feat: add EditableInput component
* feat: add EditorHeader for flow editor with file picker entry point
* feat: WorkspaceItemPicker for editor header navigation
* feat: clickable breadcrumb in EditorHeader, scoped picker keyboard nav
* fix: reload flow on URL change and reset highlight in search mode
* feat: editor header layout polish and trigger removal
* feat: apply EditorHeader to script, app, and raw app editors
* fix: show generated initial path for new apps in EditorHeader
* fix: align EditorHeader new-app path with draft drawer's Path component
* fix: read page.params.path in loadApp to reload on URL change
* fix: remount AppEditor when navigating between apps
* fix: clear app/files on URL change so editor remounts with fresh data
* fix: route picker selections to /apps/edit or /apps_raw/edit based on raw_app
* fix: drop Save button from path popover; defer rename to deploy
* refactor: drop pathPopoverContent snippet, bind newEditedPath directly
* refactor: dedupe editor header plumbing (editPathFor, userPathPrefix, breadcrumb snippet)
* fix: freeze breadcrumb during path edit so popover doesn't drift
* fix: drop spinner from path dependency-check; render nothing when no usages
* fix: swallow 404 in checkFlowOnBehalfOf so renaming a flow doesn't toast
* Revert "fix: swallow 404 in checkFlowOnBehalfOf so renaming a flow doesn't toast"
This reverts commit 82dec462ae.
* refactor: drop moveRenameManager dep from EditorHeader; pass onBehalfOfEmail as prop
* refactor: replace breadcrumb-snapshot effect with open/close setter
* refactor: drop unused dirtyPath state from EditorHeader
* fix: surface Path validation error in pen popover
* fix: decouple Path validation error from hideFullPath toggle
* refactor: use InputError for path validation message (slide transition)
* fix: re-derive Path meta from external path changes (sibling sync)
* docs: note Path's meta could be replaced with function-form bindings
* fix: 'Exit & see details' uses deployed path, not live store
* fix: undo/redo shortcut uses shiftKey instead of fragile case-match
* fix: type errors and keep edit pen visible while popover is open
* chore: remove unused meltComponents/Accordion wrapper
* fix: focus search input on picker open via popover openFocus selector
* fix: refocus picker search input on every popover open
* fix: pre-seed picker loaded state from cache so accordion opens at the right place
* fix: breadcrumb and picker track savedPath, not draft-renamed live path
* fix: inject current draft item into picker so breadcrumb scope isn't empty
* docs: add component-level and prop-level docs to EditableInput
* feat: warn that a deploy is needed when path is edited on a saved item
* feat: show same path-change-needs-deploy message in flow/script settings
* fix(flows): persist draft-renamed path through reload and dirty check
* fix: include path in unsaved-changes diff so renames trigger the modal
* feat: nested folders in picker tree and breadcrumb
* fix: per-segment popover state so switching breadcrumbs closes the previous one
* refactor: replace accordion picker with drill-through picker
* refactor(picker): review fixes, drill polish, and breadcrumb collapse
* fix(picker): review fixes — banned bindable, drop sibling-sync, load races, mouse highlight
* fix(picker): smooth-scroll highlighted row into view on open
* fix(picker): second-pass review fixes — load races, cache invalidation, breadcrumb/picker a11y, raw_app routing
* fix(RowIcon): apply size prop to resource_type and fallback divs
* feat(picker): add cross-kind 'All' root; deeper breadcrumb segments open there
* fix(picker,editor): third-pass review — search loading state, pen autofocus, allow empty summary, a11y
* fix(picker,editor): fourth-pass review — own check, flowbuilder arrows, editableinput double-save, customui.path gate, invalidate races
* fix(editor,picker): codex review — fresh URL state per load, granular whitelabel topBar gates
* fix(flows): clear localStorage in auto-reload to break URL-state loop
170 lines
4.1 KiB
Svelte
170 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import Badge from './common/badge/Badge.svelte'
|
|
import Button from './common/button/Button.svelte'
|
|
import { Plus, Tag, X } from 'lucide-svelte'
|
|
import { workspaceStore } from '$lib/stores'
|
|
|
|
interface Props {
|
|
labels: string[] | undefined
|
|
onchange?: () => void
|
|
class?: string
|
|
}
|
|
|
|
let { labels = $bindable(), onchange, class: clazz = '' }: Props = $props()
|
|
|
|
let adding = $state(false)
|
|
let inputValue = $state('')
|
|
let inputEl: HTMLInputElement | undefined = $state()
|
|
let existingLabels: string[] = $state([])
|
|
let selectedIdx = $state(-1)
|
|
|
|
let suggestions = $derived(
|
|
existingLabels
|
|
.filter(
|
|
(l) =>
|
|
(!inputValue || l.toLowerCase().includes(inputValue.toLowerCase())) &&
|
|
!(labels ?? []).includes(l)
|
|
)
|
|
.slice(0, 8)
|
|
)
|
|
let trimmedInput = $derived(inputValue.trim())
|
|
let showCreateNew = $derived(
|
|
trimmedInput.length > 0 &&
|
|
!suggestions.some((s) => s.toLowerCase() === trimmedInput.toLowerCase()) &&
|
|
!(labels ?? []).includes(trimmedInput)
|
|
)
|
|
|
|
async function loadExistingLabels() {
|
|
try {
|
|
const resp = await fetch(`/api/w/${$workspaceStore}/labels/list`)
|
|
if (resp.ok) existingLabels = await resp.json()
|
|
} catch {}
|
|
}
|
|
|
|
function startAdding() {
|
|
adding = true
|
|
inputValue = ''
|
|
selectedIdx = -1
|
|
loadExistingLabels()
|
|
setTimeout(() => inputEl?.focus(), 0)
|
|
}
|
|
|
|
function addLabel(value?: string) {
|
|
const v = (value ?? inputValue).trim().slice(0, 50)
|
|
if (!v) {
|
|
adding = false
|
|
return
|
|
}
|
|
if (!labels) {
|
|
labels = []
|
|
}
|
|
if (!labels.includes(v)) {
|
|
labels = [...labels, v]
|
|
onchange?.()
|
|
}
|
|
inputValue = ''
|
|
adding = false
|
|
}
|
|
|
|
function removeLabel(label: string) {
|
|
if (labels) {
|
|
labels = labels.filter((l) => l !== label)
|
|
onchange?.()
|
|
}
|
|
}
|
|
|
|
function onKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
if (selectedIdx >= 0 && selectedIdx < suggestions.length) {
|
|
addLabel(suggestions[selectedIdx])
|
|
} else {
|
|
addLabel() // either "Create new" selected or free text
|
|
}
|
|
} else if (e.key === 'Escape') {
|
|
inputValue = ''
|
|
adding = false
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault()
|
|
const maxIdx = suggestions.length + (showCreateNew ? 1 : 0) - 1
|
|
selectedIdx = Math.min(selectedIdx + 1, maxIdx)
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault()
|
|
selectedIdx = Math.max(selectedIdx - 1, -1)
|
|
}
|
|
}
|
|
|
|
function onBlur() {
|
|
// Delay to allow click on suggestion
|
|
setTimeout(() => {
|
|
if (adding) addLabel()
|
|
}, 150)
|
|
}
|
|
</script>
|
|
|
|
<div class="inline-flex items-center gap-1 ml-0.5 h-5 {clazz}">
|
|
{#each labels ?? [] as label (label)}
|
|
<Badge color="blue" small>
|
|
{label}
|
|
<button class="ml-0.5 hover:text-red-500" onclick={() => removeLabel(label)}>
|
|
<X size={10} />
|
|
</button>
|
|
</Badge>
|
|
{/each}
|
|
{#if adding}
|
|
<div class="relative">
|
|
<input
|
|
bind:this={inputEl}
|
|
bind:value={inputValue}
|
|
onkeydown={onKeydown}
|
|
onblur={onBlur}
|
|
class="text-2xs border border-blue-300 rounded px-1.5 py-0 h-5 max-w-32 outline-none focus:ring-1 focus:ring-blue-400"
|
|
placeholder="label"
|
|
/>
|
|
{#if suggestions.length > 0 || showCreateNew}
|
|
<div
|
|
class="absolute top-6 left-0 z-50 bg-surface border border-light rounded shadow-md max-h-32 overflow-y-auto min-w-32"
|
|
>
|
|
{#each suggestions as suggestion, i}
|
|
<button
|
|
class="w-full text-left text-2xs px-2 py-1 hover:bg-surface-hover {i === selectedIdx
|
|
? 'bg-surface-hover'
|
|
: ''}"
|
|
onmousedown={(e) => {
|
|
e.preventDefault()
|
|
addLabel(suggestion)
|
|
}}
|
|
>
|
|
{suggestion}
|
|
</button>
|
|
{/each}
|
|
{#if showCreateNew}
|
|
<button
|
|
class="w-full text-left text-2xs px-2 py-1 hover:bg-surface-hover text-blue-600 {selectedIdx ===
|
|
suggestions.length
|
|
? 'bg-surface-hover'
|
|
: ''}"
|
|
onmousedown={(e) => {
|
|
e.preventDefault()
|
|
addLabel()
|
|
}}
|
|
>
|
|
+ Create "{trimmedInput}"
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<Button
|
|
variant="subtle"
|
|
unifiedSize="xs"
|
|
startIcon={{ icon: Tag }}
|
|
endIcon={{ icon: Plus, props: { size: 8 } }}
|
|
btnClasses="!gap-0.5"
|
|
aria-label="Add label"
|
|
onclick={startAdding}
|
|
/>
|
|
{/if}
|
|
</div>
|