Files
windmill/frontend/src/lib/components/Toggle.svelte
T
GuilhemandClaude Fable 5 b0c7e09173 fix: show draft badge and disable the toggle for draft-only triggers (#10155)
* fix: show draft badge and disable toggle for draft-only triggers

* fix: align draft badge visibility with local draft hint

* fix: refine draft trigger rows (badge by label, off toggle, hover hints)

* fix: match draft pill size to standard badge size

* fix: show not-allowed cursor on disabled toggles

* fix: show the draft-only badge in place of the trigger toggle

Draft-only triggers have nothing deployed to enable, so the row renders
the "Draft only" badge in the toggle's slot instead of a disabled toggle.
Deployed triggers that also have a draft keep their toggle and show the
"Draft" badge next to the label.

Applies to schedules and every trigger list page, including amqp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: place the draft badge left of the trigger toggle

Both badges now sit in the toggle's row: "Draft only" next to a disabled,
off toggle, and "Draft" next to the live toggle of a deployed trigger that
also has a draft. The label keeps only its `*` marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: show the draft badge hint as a tooltip when it has no actions

Without owner rows the popover renders a focus-ringed 256px card for a
single sentence. Route that case through Tooltip and keep the popover for
the owner list, whose View Diff / Load / Migrate buttons need click targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: drop the "edited by" label when there is no author

Draft-only rows are synthesized from the draft table and carry no author,
so the label rendered with nothing after it. Show it only when a name exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: render the draft badge on push-mode GCP and Azure triggers

Those rows have no mode toggle, and the badge slot was nested inside the
toggle's conditional, so they showed no badge at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: let the trigger control slot grow, and hint the suspended group too

The suspended three-state control is ~246px and overflowed the fixed 8rem
slot into the row's status and badges; the slot now treats 8rem as a
minimum so unsuspended rows still line up. Moving `title` onto the wrapper
also gives the suspended group the draft explanation the toggle already had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: drop the date's "the"/"at" prefix on authorless rows

Without an author the prefix dangled ("the 7/15, 03:42 PM"); those rows now
show a bare timestamp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address PR review — draft-only row state, and DraftBadge visibility

- Derive each trigger row's status indicator from one effective mode, so a
  synthesized draft-only row (mode 'enabled', no server_id) no longer claims
  the trigger is starting up next to an off, disabled toggle.
- Fold draft_only into DraftBadge's own visibility rule so call sites pass
  their state as-is instead of hard-coding is_draft={true} behind a guard
  that duplicated the rule.
- Apply the same call to variables and resources, which still carried the
  original is_draft={false} form and so rendered no badge at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: keep every mode control off draft-only rows

The row's overflow menu still offered "Suspend job execution" for draft-only
triggers, calling the mode API for a trigger with no deployment, and the
schedules enabled/disabled filter still read the raw `enabled` flag, so
draft-only schedules listed under Enabled while rendering an off toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: mount the draft diff drawer only where it can be opened

DraftBadge mounted a DiffDrawer on every instance, so unpaginated lists like
resources and variables carried a hidden drawer per row (measured: 4334 vs
3854 DOM nodes over 120 draft-less rows). The drawer is only reachable from
the popover's View Diff, which already requires `actionsEnabled`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 09:03:11 +02:00

154 lines
4.2 KiB
Svelte

<script lang="ts">
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { classNames } from '$lib/utils'
import { createEventDispatcher, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import Tooltip from './Tooltip.svelte'
import { AlertTriangle } from 'lucide-svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { inputBorderClass } from './text_input/TextInput.svelte'
interface Props {
options?: {
left?: string
leftTooltip?: string
right?: string
rightTooltip?: string
rightDocumentationLink?: string
title?: string
}
checked?: boolean
disabled?: boolean
textClass?: string
textStyle?: string
color?: 'blue' | 'red' | 'nord'
id?: any
lightMode?: boolean
eeOnly?: boolean
aiId?: string | undefined
aiDescription?: string | undefined
class?: string | undefined
size?: '2xs' | 'xs' | 'sm' | 'md'
textDisabled?: boolean
right?: import('svelte').Snippet
}
let {
options = {},
checked = $bindable(),
disabled = false,
textClass = '',
textStyle = '',
color = 'blue',
id = (Math.random() + 1).toString(36).substring(10),
lightMode = false,
eeOnly = false,
aiId = undefined,
aiDescription = undefined,
class: className = undefined,
size = 'sm',
textDisabled = false,
right
}: Props = $props()
const dispatch = createEventDispatcher<{ change: boolean }>()
const bothOptions = Boolean(untrack(() => options).left) && Boolean(untrack(() => options).right)
</script>
<label
for={id}
class="{className || ''} z-auto flex flex-row items-center duration-50 {disabled
? 'grayscale opacity-50 cursor-not-allowed'
: 'cursor-pointer'}"
title={options?.title}
>
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-normal duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === '2xs' ? 'text-2xs' : 'text-xs',
textClass
)}
style={textStyle}
>
{options?.left}
{#if options?.leftTooltip}
<Tooltip light={lightMode}>{options?.leftTooltip}</Tooltip>
{/if}
</span>
{/if}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="relative"
onclick={stopPropagation(bubble('click'))}
use:triggerableByAI={{
id: aiId,
description: aiDescription,
callback: () => {
checked = !checked
}
}}
>
<input
onfocus={bubble('focus')}
onclick={bubble('click')}
{disabled}
type="checkbox"
{id}
class="sr-only peer"
bind:checked
onchange={stopPropagation((e) => {
dispatch('change', !!checked)
})}
/>
<div
class={classNames(
"transition-all bg-surface-sunken rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all items-center",
color == 'red'
? 'peer-checked:bg-red-600'
: color == 'blue'
? 'peer-checked:bg-blue-400 '
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-900',
size === 'md'
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
: size === 'sm'
? 'w-9 h-5 after:top-0.5 after:left-[2px] after:h-4 after:w-4'
: size === '2xs'
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3',
inputBorderClass()
)}
></div>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-normal duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === '2xs' ? 'text-2xs' : 'text-xs',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip documentationLink={options.rightDocumentationLink}>
{options.rightTooltip}
</Tooltip>
{/if}
</span>
{/if}
{@render right?.()}
</label>
{#if eeOnly && disabled}
<span class="inline-flex text-xs items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8">
<AlertTriangle size={16} />
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
</span>
{/if}