mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
feat(frontend): event handlers (#3902)
* feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): done * feat(frontend): dark mode * feat(frontend): fix dark mode + width issue * feat(frontend): fix dark mode + width issue * feat(frontend): fix layout * feat(frontend): done * feat(frontend): fix wording
This commit is contained in:
@@ -13,8 +13,9 @@
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'fileinputcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let onFileChange: string[] | undefined = undefined
|
||||
|
||||
const { app, worldStore, componentControl, mode } =
|
||||
const { app, worldStore, componentControl, mode, runnableComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let acceptedFileTypes: string[] | undefined = undefined
|
||||
@@ -36,6 +37,7 @@
|
||||
})
|
||||
}
|
||||
outputs?.result.set(files)
|
||||
onFileChange?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
}
|
||||
|
||||
let css = initCss($app.css?.fileinputcomponent, customCss)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let customCss: ComponentCustomCSS<'s3fileinputcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let extraKey: string | undefined = undefined
|
||||
export let onFileChange: string[] | undefined = undefined
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['s3fileinputcomponent'].initialData.configuration,
|
||||
@@ -33,7 +34,8 @@
|
||||
}
|
||||
|
||||
let fileUploads: Writable<FileUploadData[]> = writable([])
|
||||
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, componentControl, runnableComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$componentControl[id] = {
|
||||
clearFiles: () => {
|
||||
@@ -125,6 +127,7 @@
|
||||
on:addition={(evt) => {
|
||||
const curr = outputs.result.peak()
|
||||
outputs.result.set(curr.concat(evt.detail))
|
||||
onFileChange?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
const curr = outputs.result.peak()
|
||||
|
||||
@@ -14,9 +14,17 @@
|
||||
export let customCss: ComponentCustomCSS<'conditionalwrapper'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let conditions: RichConfiguration[]
|
||||
export let onTabChange: string[] | undefined = undefined
|
||||
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const {
|
||||
app,
|
||||
focusedGrid,
|
||||
selectedComponent,
|
||||
worldStore,
|
||||
connectingInput,
|
||||
componentControl,
|
||||
runnableComponents
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
conditions: [] as boolean[],
|
||||
@@ -52,6 +60,7 @@
|
||||
|
||||
selectedConditionIndex = index
|
||||
outputs.selectedTabIndex.set(index)
|
||||
onTabChange?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
}
|
||||
|
||||
$: resolvedConditions && handleResolvedConditions()
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let extraQueryParams: Record<string, any> = {}
|
||||
export let componentInput: AppInput | undefined
|
||||
export let onNext: string[] | undefined = undefined
|
||||
export let onPrevious: string[] | undefined = undefined
|
||||
|
||||
const {
|
||||
app,
|
||||
@@ -30,7 +32,8 @@
|
||||
selectedComponent,
|
||||
componentControl,
|
||||
connectingInput,
|
||||
mode
|
||||
mode,
|
||||
runnableComponents
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let selected = tabs[0]
|
||||
@@ -206,6 +209,7 @@
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'left'
|
||||
onPrevious?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
runStep(selectedIndex - 1)
|
||||
}}
|
||||
>
|
||||
@@ -227,6 +231,7 @@
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'right'
|
||||
onNext?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
runStep(selectedIndex + 1)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let customCss: ComponentCustomCSS<'tabscomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let disabledTabs: RichConfiguration[]
|
||||
export let onTabChange: string[] | undefined = undefined
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['tabscomponent'].initialData.configuration,
|
||||
@@ -36,7 +37,8 @@
|
||||
selectedComponent,
|
||||
mode,
|
||||
componentControl,
|
||||
connectingInput
|
||||
connectingInput,
|
||||
runnableComponents
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let selected: string = tabs[0]
|
||||
@@ -50,6 +52,8 @@
|
||||
selectedIndex = tabs?.indexOf(selected)
|
||||
outputs?.selectedTabIndex.set(selectedIndex)
|
||||
|
||||
onTabChange?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
|
||||
if ($focusedGrid?.parentComponentId != id || $focusedGrid?.subGridIndex != selectedIndex) {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
import BackgroundScriptSettings from './settingsPanel/script/BackgroundScriptSettings.svelte'
|
||||
import Recompute from './settingsPanel/Recompute.svelte'
|
||||
import EventHandlerItem from './settingsPanel/EventHandlerItem.svelte'
|
||||
|
||||
const { selectedComponent, app, stateId } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { selectedComponent, app, stateId, runnableComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let firstComponent = $selectedComponent?.[0]
|
||||
|
||||
@@ -153,7 +154,17 @@
|
||||
</PanelSection>
|
||||
</div>
|
||||
{/if}
|
||||
<Recompute bind:recomputeIds={hiddenInlineScript.script.recomputeIds} ownId={id} />
|
||||
<PanelSection
|
||||
title={`Event handlers`}
|
||||
tooltip="Event handlers are used to trigger actions on other components when a specific event occurs. For example, you can trigger a recompute on a component when a script has successfully run."
|
||||
>
|
||||
<EventHandlerItem
|
||||
title="on success"
|
||||
tooltip="This event is triggered when the script runs successfully."
|
||||
items={Object.keys($runnableComponents).filter((_id) => _id !== id)}
|
||||
bind:value={hiddenInlineScript.script.recomputeIds}
|
||||
/>
|
||||
</PanelSection>
|
||||
<div class="grow shrink" />
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
@@ -606,6 +606,7 @@
|
||||
id={component.id}
|
||||
tabs={component.tabs}
|
||||
disabledTabs={component.disabledTabs}
|
||||
onTabChange={component.onTabChange}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
@@ -617,6 +618,8 @@
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
componentInput={component.componentInput}
|
||||
onNext={component.onNext}
|
||||
onPrevious={component.onPrevious}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'conditionalwrapper' && component.conditions}
|
||||
@@ -624,6 +627,7 @@
|
||||
id={component.id}
|
||||
conditions={component.conditions}
|
||||
customCss={component.customCss}
|
||||
onTabChange={component.onTabChange}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
@@ -675,6 +679,7 @@
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 's3fileinputcomponent'}
|
||||
@@ -682,6 +687,7 @@
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'imagecomponent'}
|
||||
|
||||
@@ -186,11 +186,15 @@ export type RadioComponent = BaseComponent<'radiocomponent'>
|
||||
export type IconComponent = BaseComponent<'iconcomponent'>
|
||||
export type HorizontalDividerComponent = BaseComponent<'horizontaldividercomponent'>
|
||||
export type VerticalDividerComponent = BaseComponent<'verticaldividercomponent'>
|
||||
export type FileInputComponent = BaseComponent<'fileinputcomponent'>
|
||||
export type FileInputComponent = BaseComponent<'fileinputcomponent'> & {
|
||||
onFileChange?: string[]
|
||||
}
|
||||
export type TabsComponent = BaseComponent<'tabscomponent'> & {
|
||||
tabs: string[]
|
||||
disabledTabs: RichConfiguration[]
|
||||
onTabChange?: string[]
|
||||
}
|
||||
|
||||
export type ListComponent = BaseComponent<'listcomponent'>
|
||||
export type ContainerComponent = BaseComponent<'containercomponent'> & {
|
||||
groupFields: RichConfigurations
|
||||
@@ -214,9 +218,12 @@ export type ModalComponent = BaseComponent<'modalcomponent'> & {
|
||||
}
|
||||
export type StepperComponent = BaseComponent<'steppercomponent'> & {
|
||||
tabs: string[]
|
||||
onNext?: string[]
|
||||
onPrevious?: string[]
|
||||
}
|
||||
export type ConditionalWrapperComponent = BaseComponent<'conditionalwrapper'> & {
|
||||
conditions: RichConfiguration[]
|
||||
onTabChange?: string[]
|
||||
}
|
||||
|
||||
export type Schemaformcomponent = BaseComponent<'schemaformcomponent'>
|
||||
@@ -234,7 +241,9 @@ export type DBExplorerComponent = BaseComponent<'dbexplorercomponent'> & {
|
||||
actions: TableAction[]
|
||||
}
|
||||
|
||||
export type S3FileInputComponent = BaseComponent<'s3fileinputcomponent'>
|
||||
export type S3FileInputComponent = BaseComponent<'s3fileinputcomponent'> & {
|
||||
onFileChange?: string[]
|
||||
}
|
||||
|
||||
export type DecisionTreeNode = {
|
||||
id: string
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
|
||||
import { classNames, getModifierKey, isMac } from '$lib/utils'
|
||||
import { buildExtraLib } from '../../utils'
|
||||
import Recompute from './Recompute.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ComponentInputTypeEditor from './ComponentInputTypeEditor.svelte'
|
||||
import AlignmentEditor from './AlignmentEditor.svelte'
|
||||
@@ -40,6 +39,7 @@
|
||||
import GridAgChartsLicenseKe from './GridAgChartsLicenseKe.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import ContextVariables from './ContextVariables.svelte'
|
||||
import EventHandlers from './EventHandlers.svelte'
|
||||
|
||||
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
||||
undefined
|
||||
@@ -402,46 +402,8 @@
|
||||
{ccomponents[component.type].name} has no configuration
|
||||
</div>
|
||||
{/if}
|
||||
{#if (`recomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.recomputeIds)) || componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent' || componentSettings.item.data.type === 'checkboxcomponent'}
|
||||
<Recompute
|
||||
bind:recomputeIds={componentSettings.item.data.recomputeIds}
|
||||
ownId={component.id}
|
||||
/>
|
||||
{/if}
|
||||
{#if (`onOpenRecomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.onOpenRecomputeIds)) || componentSettings.item.data.type === 'modalcomponent' || componentSettings.item.data.type === 'drawercomponent'}
|
||||
<Recompute
|
||||
bind:recomputeIds={componentSettings.item.data.onOpenRecomputeIds}
|
||||
ownId={component.id}
|
||||
title="Trigger runnables on open"
|
||||
tooltip="Select components to recompute after this component was opened"
|
||||
/>
|
||||
{/if}
|
||||
{#if (`onCloseRecomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.onCloseRecomputeIds)) || componentSettings.item.data.type === 'modalcomponent' || componentSettings.item.data.type === 'drawercomponent'}
|
||||
<Recompute
|
||||
bind:recomputeIds={componentSettings.item.data.onCloseRecomputeIds}
|
||||
ownId={component.id}
|
||||
title="Trigger runnables on close"
|
||||
tooltip="Select components to recompute after this component was closed"
|
||||
/>
|
||||
{/if}
|
||||
{#if componentSettings.item.data.type === 'checkboxcomponent'}
|
||||
<Recompute
|
||||
title="Recompute on toggle"
|
||||
tooltip={'Contrary to onSuccess, this will only trigger recompute when a human toggle the change, not if it set by a default value or by setValue'}
|
||||
documentationLink={undefined}
|
||||
bind:recomputeIds={componentSettings.item.data.onToggle}
|
||||
ownId={component.id}
|
||||
/>
|
||||
{/if}
|
||||
{#if componentSettings.item.data.type === 'resourceselectcomponent' || componentSettings.item.data.type === 'selectcomponent'}
|
||||
<Recompute
|
||||
title="Recompute on select"
|
||||
tooltip={'Contrary to onSuccess, this will only trigger recompute when a human select an item, not if it set by a default value or by setValue'}
|
||||
documentationLink={undefined}
|
||||
bind:recomputeIds={componentSettings.item.data.onSelect}
|
||||
ownId={component.id}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<EventHandlers bind:item={componentSettings.item} ownId={component.id} />
|
||||
|
||||
<div class="grow shrink" />
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelectWrapper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let items: string[]
|
||||
export let value: string[] | undefined = undefined
|
||||
export let title: string
|
||||
export let tooltip: string
|
||||
|
||||
$: width = 0
|
||||
const inputWidth = 280
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-col justify-between w-full',
|
||||
width < inputWidth ? 'flex-col gap-2' : 'flex-row gap-16 '
|
||||
)}
|
||||
bind:clientWidth={width}
|
||||
>
|
||||
<div class="flex items-center text-xs h-8 whitespace-nowrap">
|
||||
{title}
|
||||
<Tooltip light small>{tooltip}</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
{#if items.length === 0}
|
||||
<div class="text-xs text-secondary w-full flex items-center justify-end h-8">
|
||||
No components to recompute.
|
||||
</div>
|
||||
{:else}
|
||||
<MultiSelect {items} bind:value />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext, GridItem } from '../../types'
|
||||
import { getContext } from 'svelte'
|
||||
import EventHandlerItem from './EventHandlerItem.svelte'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
|
||||
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let item: GridItem
|
||||
export let ownId: string
|
||||
|
||||
const componentsWithEventHandler = [
|
||||
'modalcomponent',
|
||||
'drawercomponent',
|
||||
'buttoncomponent',
|
||||
'formcomponent',
|
||||
'formbuttoncomponent',
|
||||
'checkboxcomponent',
|
||||
'resourceselectcomponent',
|
||||
'selectcomponent',
|
||||
'tabscomponent',
|
||||
'conditionalwrapper',
|
||||
'fileinputcomponent',
|
||||
's3fileinputcomponent',
|
||||
'steppercomponent'
|
||||
]
|
||||
</script>
|
||||
|
||||
{#if componentsWithEventHandler.includes(item.data.type)}
|
||||
<PanelSection
|
||||
title="Event handlers"
|
||||
tooltip="Event handlers are used to trigger actions on other components when a specific event occurs. For example, you can trigger a recompute on a component when a button is clicked."
|
||||
>
|
||||
{#if (`onOpenRecomputeIds` in item.data && Array.isArray(item.data.onOpenRecomputeIds)) || item.data.type === 'modalcomponent' || item.data.type === 'drawercomponent'}
|
||||
<EventHandlerItem
|
||||
title="on open"
|
||||
tooltip="Select components to recompute after this component was opened"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onOpenRecomputeIds}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if (`onCloseRecomputeIds` in item.data && Array.isArray(item.data.onCloseRecomputeIds)) || item.data.type === 'modalcomponent' || item.data.type === 'drawercomponent'}
|
||||
<EventHandlerItem
|
||||
title="on close"
|
||||
tooltip="Select components to recompute after this component was closed"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onCloseRecomputeIds}
|
||||
/>
|
||||
{/if}
|
||||
{#if (`recomputeIds` in item.data && Array.isArray(item.data.recomputeIds)) || item.data.type === 'buttoncomponent' || item.data.type === 'formcomponent' || item.data.type === 'formbuttoncomponent' || item.data.type === 'checkboxcomponent'}
|
||||
<EventHandlerItem
|
||||
title="on success"
|
||||
tooltip="Select components to recompute after this runnable has successfully run"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.recomputeIds}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'checkboxcomponent'}
|
||||
<EventHandlerItem
|
||||
title="on toggle"
|
||||
tooltip="Contrary to onSuccess, this will only trigger recompute when a human toggle the change, not if it set by a default value or by setValue"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onToggle}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'resourceselectcomponent' || item.data.type === 'selectcomponent'}
|
||||
<EventHandlerItem
|
||||
title="on select"
|
||||
tooltip="Contrary to onSuccess, this will only trigger recompute when a human select an item, not if it set by a default value or by setValue"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onSelect}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'tabscomponent' || item.data.type === 'conditionalwrapper'}
|
||||
<EventHandlerItem
|
||||
title="on tab change"
|
||||
tooltip="Select components to recompute after the selected tab was changed"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onTabChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'fileinputcomponent' || item.data.type === 's3fileinputcomponent'}
|
||||
<EventHandlerItem
|
||||
title="on file change"
|
||||
tooltip="Select components to recompute after a file was selected"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onFileChange}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'steppercomponent'}
|
||||
<EventHandlerItem
|
||||
title="on next"
|
||||
tooltip="Select components to recompute after the next button was clicked"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onNext}
|
||||
/>
|
||||
{/if}
|
||||
{#if item.data.type === 'steppercomponent'}
|
||||
<EventHandlerItem
|
||||
title="on previous"
|
||||
tooltip="Select components to recompute after the previous button was clicked"
|
||||
items={Object.keys($runnableComponents).filter((id) => id !== ownId)}
|
||||
bind:value={item.data.onPrevious}
|
||||
/>
|
||||
{/if}
|
||||
</PanelSection>
|
||||
{/if}
|
||||
@@ -23,6 +23,11 @@
|
||||
$: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const mapping = {
|
||||
onSuccess: 'On success wizard',
|
||||
onError: 'On error wizard'
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if inputSpecs}
|
||||
@@ -38,7 +43,7 @@
|
||||
{:else if finalInputSpecsConfiguration[k]?.type == 'oneOf'}
|
||||
<OneOfInputSpecsEditor
|
||||
{acceptSelf}
|
||||
key={k}
|
||||
key={mapping[k] ?? k}
|
||||
bind:oneOf={inputSpecs[k]}
|
||||
{id}
|
||||
{shouldCapitalize}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let ownId: string
|
||||
export let title: string = 'Trigger runnables on success'
|
||||
export let tooltip: string =
|
||||
'Select components to recompute after this runnable has successfully run'
|
||||
export let documentationLink: string =
|
||||
'https://www.windmill.dev/docs/apps/app-runnable-panel#trigger-runnables-on-success'
|
||||
|
||||
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function onChange(checked: boolean, id: string) {
|
||||
if (checked) {
|
||||
recomputeIds = [...(recomputeIds ?? []), id]
|
||||
} else {
|
||||
recomputeIds = recomputeIds?.filter((x) => x !== id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection {title} {tooltip} {documentationLink}>
|
||||
{#if Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId).length > 0}
|
||||
<table class="divide-y border w-full">
|
||||
<thead class="bg-surface-secondary">
|
||||
<tr>
|
||||
<th scope="col" class="px-2 py-2 text-left text-xs font-medium text-tertiary">
|
||||
Component
|
||||
</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-tertiary">
|
||||
Recompute
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId) as id}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-2 text-xs">
|
||||
<Badge color="indigo">{id}</Badge>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap px-4">
|
||||
<Toggle
|
||||
class="windmillapp my-1"
|
||||
size="xs"
|
||||
on:change={(e) => onChange(e.detail, id)}
|
||||
checked={recomputeIds?.includes(id)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<div class="text-xs">No components to recompute. Create one and select it here.</div>
|
||||
{/if}
|
||||
</PanelSection>
|
||||
@@ -78,6 +78,7 @@
|
||||
export let ulSelectedClass: string = ``
|
||||
export let ulSelectedStyle: string | null = null
|
||||
export let value: Option | Option[] | null = null
|
||||
export let disableRemoveAll: boolean = false
|
||||
|
||||
const selected_to_value = (selected: Option[]) => {
|
||||
value = maxSelect === 1 ? selected[0] ?? null : selected
|
||||
@@ -507,7 +508,7 @@
|
||||
class="remove"
|
||||
>
|
||||
<slot name="remove-icon">
|
||||
<X size={20} class="text-primary p-0.5" />
|
||||
<X size={20} class="text-primary dark:text-primary-inverse p-0.5" />
|
||||
</slot>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -552,7 +553,7 @@
|
||||
{#if disabled}
|
||||
<slot name="disabled-icon">disable</slot>
|
||||
{:else if selected.length > 0}
|
||||
{#if maxSelect !== 1 && selected.length > 1}
|
||||
{#if maxSelect !== 1 && selected.length > 1 && !disableRemoveAll}
|
||||
<button
|
||||
type="button"
|
||||
class="remove remove-all"
|
||||
@@ -648,7 +649,7 @@
|
||||
on:blur={() => (option_msg_is_active = false)}
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
class="user-msg"
|
||||
class="user-msg p-1"
|
||||
style:cursor={{
|
||||
dupe: `not-allowed`,
|
||||
create: `pointer`,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
// @ts-ignore
|
||||
import Portal from 'svelte-portal'
|
||||
import { createFloatingActions } from 'svelte-floating-ui'
|
||||
import { tick } from 'svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
middleware: [offset(5), flip(), shift()]
|
||||
})
|
||||
|
||||
export let items: any[]
|
||||
export let value: string[] | undefined = [] as string[]
|
||||
let outerDiv: HTMLDivElement | undefined = undefined
|
||||
let portalRef: HTMLDivElement | undefined = undefined
|
||||
|
||||
function moveOptionsToPortal() {
|
||||
// Find ul element with class 'options' within the outerDiv
|
||||
const ul = outerDiv?.querySelector('.options')
|
||||
|
||||
if (ul) {
|
||||
// Move the ul element to the portal
|
||||
portalRef?.appendChild(ul)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (portalRef && outerDiv && items?.length > 0) {
|
||||
tick().then(() => {
|
||||
moveOptionsToPortal()
|
||||
})
|
||||
}
|
||||
|
||||
// bg-indigo-100 text-indigo-800 dark:bg-indigo-200 dark:text-indigo-900
|
||||
let darkMode: boolean = false
|
||||
|
||||
let w = 0
|
||||
let open: boolean = false
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
<div use:floatingRef bind:clientWidth={w}>
|
||||
{#if !value || Array.isArray(value)}
|
||||
<div class="border rounded-md border-gray-300 shadow-sm dark:border-gray-600 !w-full">
|
||||
<MultiSelect
|
||||
outerDivClass={`!text-xs`}
|
||||
ulSelectedClass="overflow-auto"
|
||||
bind:outerDiv
|
||||
--sms-border={'none'}
|
||||
--sms-min-height={'30px'}
|
||||
--sms-focus-border={'none'}
|
||||
--sms-selected-bg={darkMode ? '#c7d2fe' : '#e0e7ff'}
|
||||
--sms-selected-text-color={darkMode ? '#312e81' : '#3730a3'}
|
||||
bind:selected={value}
|
||||
options={items}
|
||||
on:close={() => {
|
||||
open = false
|
||||
}}
|
||||
on:open={() => {
|
||||
open = true
|
||||
}}
|
||||
let:option
|
||||
disableRemoveAll
|
||||
>
|
||||
<!-- needed because portal doesn't work for mouseup event en mobile -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full text-sm"
|
||||
on:mouseup|stopPropagation
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<Portal>
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={portalRef}
|
||||
class="multiselect"
|
||||
style={`min-width: ${w}px;`}
|
||||
on:click|stopPropagation
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
{:else}
|
||||
Value {value} is not an array
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user