Files
windmill/frontend/src/lib/components/graph/GroupNodeCard.svelte
2026-03-04 14:47:46 +00:00

162 lines
4.4 KiB
Svelte

<script lang="ts">
import { Group } from 'lucide-svelte'
import { getNodeColorClasses } from '$lib/components/graph'
import { NOTE_COLORS, NoteColor } from './noteColors'
import { twMerge } from 'tailwind-merge'
import { preventDefault, stopPropagation } from 'svelte/legacy'
import GroupNoteArea from './GroupNoteArea.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
import GroupModuleIcons from './GroupModuleIcons.svelte'
import type { FlowModule } from '$lib/gen'
interface Props {
summary?: string
selected?: boolean
stepCount?: number
fullWidth?: boolean
color?: string
note?: string
showNote?: boolean
editMode?: boolean
modules?: FlowModule[]
onExpand?: () => void
onSummaryUpdate?: (text: string) => void
onNoteUpdate?: (text: string) => void
onHeightChange?: (height: number) => void
}
let {
summary,
selected = false,
stepCount,
fullWidth = false,
color,
note,
showNote = false,
editMode = false,
modules,
onExpand,
onSummaryUpdate,
onNoteUpdate,
onHeightChange
}: Props = $props()
let noteColorConfig = $derived(
color ? (NOTE_COLORS[color as NoteColor] ?? NOTE_COLORS[NoteColor.BLUE]) : undefined
)
let defaultColorClasses = $derived(getNodeColorClasses(undefined, selected))
// Inline summary editing
let editingSummary = $state(false)
let summaryInput = $state('')
let textInputComponent: TextInput | undefined = $state(undefined)
function startEditingSummary() {
if (!editMode) return
editingSummary = true
summaryInput = summary ?? ''
requestAnimationFrame(() => {
textInputComponent?.focus()
textInputComponent?.select()
})
}
function saveSummary() {
editingSummary = false
const trimmed = summaryInput.trim()
if (trimmed !== (summary ?? '')) {
onSummaryUpdate?.(trimmed)
}
}
function handleSummaryKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
saveSummary()
} else if (event.key === 'Escape') {
editingSummary = false
}
}
// Reset height to 0 when note is hidden
$effect(() => {
if (!showNote) {
onHeightChange?.(0)
}
})
</script>
<div
class={twMerge(
'w-full module cursor-pointer max-w-full',
fullWidth ? 'rounded-t-md' : 'shadow-sm border rounded-md overflow-clip',
'bg-surface-tertiary'
)}
style={fullWidth ? '' : 'width: 275px;'}
>
<div
class={twMerge(
'absolute z-0 outline-offset-0 inset-0',
fullWidth ? 'rounded-t-md' : 'rounded-md',
noteColorConfig ? noteColorConfig.outline : defaultColorClasses.outline
)}
></div>
<div class="flex items-center w-full gap-1.5 px-2 h-[34px] relative z-1">
{#if modules && modules.length > 0}
<GroupModuleIcons {modules} />
{:else}
<Group size={14} />
{/if}
{#if editingSummary}
<TextInput
bind:this={textInputComponent}
bind:value={summaryInput}
size="xs"
class="!bg-transparent !border-transparent !shadow-none !text-2xs !font-medium !p-0 !m-0 !min-w-0 flex-1 text-center !min-h-0 !h-auto nodrag nowheel"
inputProps={{
placeholder: 'Group',
onblur: saveSummary,
onkeydown: handleSummaryKeydown,
spellcheck: false
}}
/>
{:else}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="text-2xs font-medium truncate flex-1 text-center {editMode
? 'cursor-text rounded px-0.5 -mx-0.5 hover:bg-black/10 dark:hover:bg-white/10'
: ''}"
onclick={editMode ? stopPropagation(preventDefault(startEditingSummary)) : undefined}
onpointerdown={editMode ? stopPropagation(preventDefault(() => {})) : undefined}
>{summary || 'Group'}</span
>
{/if}
{#if stepCount != null}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="text-3xs opacity-60 shrink-0 whitespace-nowrap {noteColorConfig
? noteColorConfig.text
: ''} {onExpand
? 'cursor-pointer hover:opacity-100 hover:text-blue-500 dark:hover:text-blue-400'
: ''}"
onclick={onExpand ? stopPropagation(preventDefault(onExpand)) : undefined}
>{stepCount} node{stepCount !== 1 ? 's' : ''}</span
>
{/if}
</div>
{#if showNote}
<div class="relative z-1">
<GroupNoteArea
note={note ?? ''}
{color}
{editMode}
onHeightChange={(h) => onHeightChange?.(h)}
onNoteUpdate={(text) => onNoteUpdate?.(text)}
/>
</div>
{/if}
</div>