mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat: Multiple triggers chip picks the trigger to edit; each folded trigger deletable from the node menu
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9c2122ddcd
commit
e6eda04e7b
@@ -1385,7 +1385,25 @@
|
||||
unsaved: d.unsaved as boolean | undefined,
|
||||
onOpen: () =>
|
||||
onselect?.({ kind: 'runnable', runnable_kind: d.runnable_kind, path: d.path })
|
||||
}))
|
||||
})),
|
||||
// Every script's own triggers; asset triggers show as edges instead.
|
||||
triggers: (u.multiple ? u.runnableIds : [])
|
||||
.flatMap((id) => {
|
||||
const d = runnables.get(id)?.data
|
||||
if (!d) return []
|
||||
return m.edges
|
||||
.filter((e) => e.kind === 'trigger-native' && e.target === id)
|
||||
.map((e) => m.nodes.find((n) => n.id === e.source))
|
||||
.filter((t) => t != undefined)
|
||||
.map((t) => ({
|
||||
...triggerChip(
|
||||
{ nodeId: t.id, kind: t.data.kind, data: t.data },
|
||||
d.path as string
|
||||
),
|
||||
script: d.path as string
|
||||
}))
|
||||
})
|
||||
.filter((t) => !t.hidden)
|
||||
}
|
||||
: {
|
||||
runnableId: u.runnableId,
|
||||
@@ -1427,16 +1445,25 @@
|
||||
: 'none' in upstream
|
||||
? 'External source'
|
||||
: 'multiple' in upstream
|
||||
? 'Multiple upstream nodes'
|
||||
? (upstream.triggers?.length ?? 0) > 1
|
||||
? 'Multiple triggers'
|
||||
: (upstream.triggers?.length ?? 0) === 1
|
||||
? (upstream.triggers?.[0]?.label ?? '')
|
||||
: ''
|
||||
: upstream.trigger.hidden
|
||||
? ''
|
||||
: `${upstream.trigger.label}${upstream.trigger.draft ? ' · draft' : ''}`,
|
||||
chipIcon: foldedErrors > 0,
|
||||
// Room for the chip's trailing icon: a pencil, or the chevron of a
|
||||
// several-trigger menu (about as wide).
|
||||
chipEdit:
|
||||
!foldedErrors &&
|
||||
'trigger' in upstream &&
|
||||
!!upstream.trigger?.onOpen &&
|
||||
!upstream.trigger.missing,
|
||||
('multiple' in upstream
|
||||
? (upstream.triggers?.length ?? 0) > 1 ||
|
||||
(!!upstream.triggers?.[0]?.onOpen && !upstream.triggers?.[0]?.missing)
|
||||
: 'trigger' in upstream &&
|
||||
!!upstream.trigger?.onOpen &&
|
||||
!upstream.trigger.missing),
|
||||
header:
|
||||
'runnableId' in upstream
|
||||
? {
|
||||
@@ -1474,6 +1501,28 @@
|
||||
.map((id) => runnables.get(id)?.data)
|
||||
.filter((d) => d?.runnable_kind === 'script')
|
||||
.map((d) => ({ path: d.path as string, unsaved: !!d.unsaved })),
|
||||
// Its scripts' own triggers: a row that starts only them, which
|
||||
// webhooks and data uploads (no row) and missing ones are not.
|
||||
triggerDeletes: scriptIds.flatMap((id) => {
|
||||
const scriptPath = runnables.get(id)?.data?.path
|
||||
return m.edges
|
||||
.filter((e) => e.kind === 'trigger-native' && e.target === id)
|
||||
.map((e) => m.nodes.find((n) => n.id === e.source)?.data)
|
||||
.filter(
|
||||
(t) =>
|
||||
t &&
|
||||
t.kind !== 'webhook' &&
|
||||
t.kind !== 'data_upload' &&
|
||||
t.ref &&
|
||||
!t.missing &&
|
||||
((t.runnable_paths ?? []) as string[]).every((p) => p === scriptPath)
|
||||
)
|
||||
.map((t) => ({
|
||||
kind: t.kind as NativeTriggerKind,
|
||||
path: t.ref as string,
|
||||
draft: !!t.draft
|
||||
}))
|
||||
}),
|
||||
deleteVerb: assetDeleteVerb,
|
||||
onDeleteUpstream: (t: AssetUpstreamDelete) => onDeleteAssetUpstream?.(t),
|
||||
// The No-asset card stands for its scripts; an asset is built by them.
|
||||
|
||||
@@ -45,6 +45,18 @@
|
||||
// `content` / `language` so the page-level run handler can dispatch to
|
||||
// `runScriptPreview` instead of `runScriptByPath` (which 404s for
|
||||
// non-deployed scripts).
|
||||
/** What starts a producing script, as its chip shows it. */
|
||||
export type TriggerChipData = {
|
||||
label: string
|
||||
/** No chip: the node's incoming edges already show what starts it. */
|
||||
hidden?: boolean
|
||||
missing?: boolean
|
||||
draft?: boolean
|
||||
/** Opens the trigger's editor; unset for what has none (manual, asset change). */
|
||||
onOpen?: () => void
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
export type AssetUpstreamChips =
|
||||
| { none: true }
|
||||
| {
|
||||
@@ -58,6 +70,8 @@
|
||||
unsaved?: boolean
|
||||
onOpen: () => void
|
||||
}>
|
||||
/** The scripts' own triggers, each named with its script. */
|
||||
triggers: Array<TriggerChipData & { script: string }>
|
||||
selected?: boolean
|
||||
}
|
||||
| {
|
||||
@@ -67,16 +81,7 @@
|
||||
language?: ScriptLang
|
||||
unsaved?: boolean
|
||||
runState?: RunnableRunState
|
||||
trigger: {
|
||||
label: string
|
||||
/** No chip: the node's incoming edges already show what starts it. */
|
||||
hidden?: boolean
|
||||
missing?: boolean
|
||||
draft?: boolean
|
||||
/** Opens the trigger's editor; unset for what has none (manual, asset change). */
|
||||
onOpen?: () => void
|
||||
selected?: boolean
|
||||
}
|
||||
trigger: TriggerChipData
|
||||
onOpen: () => void
|
||||
selected?: boolean
|
||||
}
|
||||
@@ -183,6 +188,7 @@
|
||||
/** When no combined delete is possible: the folded scripts, each deletable
|
||||
* on its own. */
|
||||
scriptDeletes?: Array<{ path: string; unsaved: boolean }>
|
||||
triggerDeletes?: Array<NonNullable<import('./assetsOnlyView').AssetUpstreamDelete['trigger']>>
|
||||
onDeleteUpstream?: (target: import('./assetsOnlyView').AssetUpstreamDelete) => void
|
||||
deleteVerb?: 'Delete' | 'Archive'
|
||||
/** Why nothing can be deleted, when the producer is shared. */
|
||||
@@ -236,10 +242,11 @@
|
||||
let menuItems: Item[] = $derived(
|
||||
data.onDeleteUpstream && data.upstreamDelete
|
||||
? deleteItems(data.upstreamDelete, data.onDeleteUpstream)
|
||||
: data.onDeleteUpstream && data.scriptDeletes?.length
|
||||
? data.scriptDeletes.map((script) =>
|
||||
deleteItems({ script }, data.onDeleteUpstream!)
|
||||
).flat()
|
||||
: data.onDeleteUpstream && (data.scriptDeletes?.length || data.triggerDeletes?.length)
|
||||
? [
|
||||
...(data.scriptDeletes ?? []).map((script) => ({ script })),
|
||||
...(data.triggerDeletes ?? []).map((trigger) => ({ trigger }))
|
||||
].flatMap((t) => deleteItems(t, data.onDeleteUpstream!))
|
||||
: data.deleteBlocked
|
||||
? [
|
||||
{
|
||||
@@ -305,6 +312,16 @@
|
||||
upstreamScriptItems(coProducers, (path) => coProducers.find((s) => s.path === path)?.onOpen())
|
||||
)
|
||||
let scriptMenuOpen = $state(false)
|
||||
let coTriggers = $derived(upstream && 'triggers' in upstream ? upstream.triggers : [])
|
||||
let coTriggerItems: Item[] = $derived(
|
||||
coTriggers.map((t) => ({
|
||||
displayName: `${t.label}${t.draft ? ' · draft' : ''}`,
|
||||
description: coProducers.length > 1 ? t.script : undefined,
|
||||
disabled: !t.onOpen,
|
||||
action: () => t.onOpen?.()
|
||||
}))
|
||||
)
|
||||
let triggerMenuOpen = $state(false)
|
||||
|
||||
// dbt badge. `materialized` is dbt's own word rather than the Windmill
|
||||
// strategy because `view` and `ephemeral` have no strategy, and showing the
|
||||
@@ -451,8 +468,40 @@
|
||||
fix={data.foldedFixes[0]}
|
||||
label={`${data.foldedFixes.length} error${data.foldedFixes.length === 1 ? '' : 's'} · Fix`}
|
||||
/>
|
||||
{:else if upstream && coTriggers.length > 1}
|
||||
<!-- Several triggers start what builds this asset: one chip, a menu to pick
|
||||
the one to edit. -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="min-w-0 flex"
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownV2
|
||||
items={coTriggerItems}
|
||||
placement="bottom-start"
|
||||
bind:open={triggerMenuOpen}
|
||||
fixedHeight={false}
|
||||
usePointerDownOutside
|
||||
enableFlyTransition
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<span
|
||||
class={twMerge(
|
||||
'flex items-center gap-1 min-w-0 rounded-md border px-1.5 py-1 text-3xs leading-none font-normal border-gray-300 dark:border-gray-600 text-secondary bg-surface hover:bg-surface-hover cursor-pointer',
|
||||
(triggerMenuOpen || coTriggers.some((t) => t.selected)) && CHIP_SELECTED
|
||||
)}
|
||||
title="Started by several triggers: pick one to edit"
|
||||
>
|
||||
<span class="truncate">Multiple triggers</span>
|
||||
<ChevronDown size={9} class="shrink-0 opacity-70" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
</div>
|
||||
{:else if upstream}
|
||||
{@const trigger = producer?.trigger}
|
||||
{@const trigger = producer?.trigger ?? coTriggers[0]}
|
||||
{@const triggerScript = producer?.path ?? coTriggers[0]?.script}
|
||||
{@const chipClass = twMerge(
|
||||
'truncate rounded-md border px-1.5 py-1 text-3xs leading-none font-normal',
|
||||
trigger?.missing
|
||||
@@ -467,7 +516,7 @@
|
||||
type="button"
|
||||
class={twMerge(chipClass, 'flex items-center gap-1 min-w-0')}
|
||||
title={trigger.missing
|
||||
? `No ${trigger.label} trigger targets ${producer?.path} yet: click to create one`
|
||||
? `No ${trigger.label} trigger targets ${triggerScript} yet: click to create one`
|
||||
: `Edit the ${trigger.label} trigger`}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
@@ -485,8 +534,6 @@
|
||||
External source
|
||||
{:else if trigger}
|
||||
{trigger.label}{trigger.draft ? ' · draft' : ''}
|
||||
{:else}
|
||||
Multiple upstream nodes
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -541,7 +588,10 @@
|
||||
: data.path}
|
||||
{selected}
|
||||
tone={data.error ? 'error' : undefined}
|
||||
subtitle={data.foldedFixes?.length || (upstream && !producer?.trigger?.hidden)
|
||||
subtitle={data.foldedFixes?.length ||
|
||||
(upstream &&
|
||||
!producer?.trigger?.hidden &&
|
||||
!('triggers' in upstream && upstream.triggers.length === 0))
|
||||
? upstreamRow
|
||||
: undefined}
|
||||
header={hasHeader ? scriptHeader : undefined}
|
||||
|
||||
Reference in New Issue
Block a user