mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
feat(frontend): add creatable select
This commit is contained in:
Generated
+7
-7
@@ -45,7 +45,7 @@
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelte-grid": "^5.1.5",
|
||||
"@windmill-labs/svelte-grid": "^5.1.6",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
@@ -1318,9 +1318,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@windmill-labs/svelte-grid": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.5.tgz",
|
||||
"integrity": "sha512-aCMaOghJHlFbw+Q9xg3zm+/zRDve6Ub+BMDyfm8cVJeJ2GL7arLxh+G6s46kqxfOBz388hHSKowqVNzQxDxw6A==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
|
||||
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@windmill-labs/svelvet": {
|
||||
@@ -8225,9 +8225,9 @@
|
||||
}
|
||||
},
|
||||
"@windmill-labs/svelte-grid": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.5.tgz",
|
||||
"integrity": "sha512-aCMaOghJHlFbw+Q9xg3zm+/zRDve6Ub+BMDyfm8cVJeJ2GL7arLxh+G6s46kqxfOBz388hHSKowqVNzQxDxw6A==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
|
||||
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@windmill-labs/svelvet": {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelte-grid": "^5.1.5",
|
||||
"@windmill-labs/svelte-grid": "^5.1.6",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
|
||||
Vendored
+1
@@ -47,6 +47,7 @@ declare module '@windmill-labs/svelte-grid' {
|
||||
onTopId?: string
|
||||
scroller?: undefined
|
||||
sensor?: number
|
||||
parentWidth?: number
|
||||
}
|
||||
|
||||
export interface Slots<T> {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import Select from 'svelte-select'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
|
||||
import { stopPropagation } from 'ol/events/Event'
|
||||
|
||||
export const staticOutputs: string[] = ['result']
|
||||
export let id: string
|
||||
export let configuration: Record<string, AppInput>
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
let items: { label: string; value: string }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<string[] | undefined>
|
||||
}
|
||||
|
||||
// $: outputs && handleOutputs()
|
||||
|
||||
// function handleOutputs() {
|
||||
// value = outputs.result.peak()
|
||||
// }
|
||||
|
||||
let value: { value: string }[] | undefined = outputs?.result.peak()
|
||||
|
||||
$: labels && handleItems()
|
||||
|
||||
let labels: string[] | undefined = []
|
||||
|
||||
function handleItems() {
|
||||
if (labels) {
|
||||
items = labels?.map((label) => {
|
||||
return {
|
||||
label,
|
||||
value: label
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: value && outputs?.result.set(value.map((v) => v.value))
|
||||
|
||||
$: css = concatCustomCss($app.css?.selectcomponent, customCss)
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.items} bind:value={labels} />
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<div class="app-select w-full mx-0.5" style="height: 34px" on:pointerdown|stopPropagation>
|
||||
{#if !value || Array.isArray(value)}
|
||||
<Select
|
||||
multiple
|
||||
on:change={(e) => e.stopPropagation()}
|
||||
{items}
|
||||
class={css?.input?.class ?? ''}
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles + css?.input?.style}
|
||||
bind:value
|
||||
{placeholder}
|
||||
on:click={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = id
|
||||
}
|
||||
}}
|
||||
on:focus={() => {
|
||||
$selectedComponent = id
|
||||
}}
|
||||
floatingConfig={{
|
||||
strategy: 'fixed'
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
Value {value} is not an array
|
||||
{/if}
|
||||
</div>
|
||||
</AlignWrapper>
|
||||
|
||||
<style global>
|
||||
.app-select .value-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.svelte-select-list {
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -18,25 +18,18 @@
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
let items: { label: string; value: any }[]
|
||||
let multiple: boolean = false
|
||||
let items: { label: string; value: any; created?: boolean }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<string | undefined>
|
||||
result: Output<any | undefined>
|
||||
}
|
||||
|
||||
// $: outputs && handleOutputs()
|
||||
|
||||
// function handleOutputs() {
|
||||
// value = outputs.result.peak()
|
||||
// }
|
||||
|
||||
let value: string | undefined = outputs?.result.peak()
|
||||
|
||||
$: items && handleItems()
|
||||
|
||||
let listItems: { label: string; value: string }[] = []
|
||||
let listItems: { label: string; value: string; created?: boolean }[] = []
|
||||
|
||||
function handleItems() {
|
||||
listItems = Array.isArray(items)
|
||||
@@ -47,21 +40,22 @@
|
||||
}
|
||||
})
|
||||
: []
|
||||
const valuePeak = outputs?.result.peak()
|
||||
if (!valuePeak) {
|
||||
const value0 = items?.[0]?.['value']
|
||||
if (value0 != value) {
|
||||
value = JSON.stringify(value0)
|
||||
outputs?.result.set(value0)
|
||||
}
|
||||
} else {
|
||||
value = JSON.stringify(valuePeak)
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(e: CustomEvent) {
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
|
||||
if (create) {
|
||||
listItems = listItems.map((i) => {
|
||||
delete i.created
|
||||
return i
|
||||
})
|
||||
}
|
||||
|
||||
let result: any = undefined
|
||||
try {
|
||||
result = JSON.parse(e.detail?.['value'])
|
||||
@@ -73,23 +67,40 @@
|
||||
|
||||
let defaultValue: any = undefined
|
||||
|
||||
$: {
|
||||
function handleFilter(e) {
|
||||
if (create) {
|
||||
if (e.detail.length === 0 && filterText.length > 0) {
|
||||
const prev = listItems.filter((i) => !i.created)
|
||||
listItems = [
|
||||
...prev,
|
||||
{ value: JSON.stringify(filterText), label: filterText, created: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: defaultValue && handleDefault()
|
||||
|
||||
function handleDefault() {
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
outputs?.result.set(defaultValue)
|
||||
}
|
||||
}
|
||||
let create = false
|
||||
let filterText = ''
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.items} bind:value={items} />
|
||||
<InputValue {id} input={configuration.multiple} bind:value={multiple} />
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
<InputValue {id} input={configuration.create} bind:value={create} />
|
||||
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<div class="app-select w-full mx-0.5" style="height: 34px" on:pointerdown|stopPropagation>
|
||||
<Select
|
||||
{multiple}
|
||||
bind:filterText
|
||||
on:filter={handleFilter}
|
||||
on:clear={onChange}
|
||||
on:change={onChange}
|
||||
items={listItems}
|
||||
@@ -109,7 +120,15 @@
|
||||
floatingConfig={{
|
||||
strategy: 'fixed'
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div slot="item" let:item>
|
||||
{#if create}
|
||||
{item.created ? 'Add new: ' : ''}
|
||||
{/if}
|
||||
|
||||
{item.label}
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
</AlignWrapper>
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
selectedTabIndex: Output<number | null>
|
||||
}
|
||||
|
||||
$: outputs?.selectedTabIndex && handleOutputs()
|
||||
|
||||
function handleTabSelection() {
|
||||
outputs?.selectedTabIndex.set(selectedIndex)
|
||||
if ($selectedComponent != id) {
|
||||
@@ -60,6 +62,10 @@
|
||||
$: $selectedComponent === id && selectedIndex >= 0 && onFocus()
|
||||
|
||||
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
|
||||
|
||||
function handleOutputs() {
|
||||
outputs?.selectedTabIndex.set(outputs.selectedTabIndex.peak())
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
|
||||
|
||||
@@ -81,7 +81,8 @@
|
||||
errorByComponent,
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid,
|
||||
stateId: writable(0)
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0)
|
||||
})
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
errorByComponent: writable({}),
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid: writable(undefined),
|
||||
stateId: writable(0)
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0)
|
||||
})
|
||||
|
||||
let mounted = false
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
staticOutputs,
|
||||
runnableComponents,
|
||||
summary,
|
||||
focusedGrid
|
||||
focusedGrid,
|
||||
parentWidth
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
// The drag is disabled when the user is connecting an input
|
||||
@@ -148,6 +149,7 @@
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
bind:clientWidth={$parentWidth}
|
||||
>
|
||||
<div class={!$focusedGrid && $mode !== 'preview' ? 'border-gray-400 border border-dashed' : ''}>
|
||||
<Grid
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const { app, connectingInput, selectedComponent, focusedGrid, mode } =
|
||||
const { app, connectingInput, selectedComponent, focusedGrid, mode, parentWidth } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: highlight = id === $focusedGrid?.parentComponentId && shouldHighlight
|
||||
@@ -82,9 +82,7 @@
|
||||
on:pointerup={onpointerup}
|
||||
style="height: {containerHeight}px; {style ?? ''}"
|
||||
>
|
||||
<div
|
||||
class={highlight && $mode !== 'preview' ? 'border-gray-400 border border-dashed h-full' : ''}
|
||||
>
|
||||
<div class={highlight && $mode !== 'preview' ? 'border-gray-400 border border-dashed' : ''}>
|
||||
<Grid
|
||||
bind:items={subGrid}
|
||||
let:dataItem
|
||||
@@ -93,6 +91,7 @@
|
||||
fastStart={true}
|
||||
gap={[4, 2]}
|
||||
scroller={container}
|
||||
parentWidth={$parentWidth - 17}
|
||||
>
|
||||
{#each subGrid as gridComponent (gridComponent.id)}
|
||||
{#if gridComponent?.data?.id && gridComponent?.data?.id === dataItem?.data?.id}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
AppMap,
|
||||
AppSplitpanes
|
||||
} from '../../components'
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -203,6 +204,15 @@
|
||||
customCss={component.customCss}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'multiselectcomponent'}
|
||||
<AppMultiSelect
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'formcomponent'}
|
||||
<AppForm
|
||||
id={component.id}
|
||||
|
||||
@@ -72,6 +72,7 @@ export type DisplayComponent = BaseComponent<'displaycomponent'>
|
||||
export type ImageComponent = BaseComponent<'imagecomponent'>
|
||||
export type InputComponent = BaseComponent<'inputcomponent'>
|
||||
export type SelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
|
||||
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
|
||||
export type RadioComponent = BaseComponent<'radiocomponent'>
|
||||
export type IconComponent = BaseComponent<'iconcomponent'>
|
||||
@@ -110,6 +111,7 @@ export type AppComponent = BaseAppComponent &
|
||||
| PieChartComponent
|
||||
| ScatterChartComponent
|
||||
| SelectComponent
|
||||
| MultiSelectComponent
|
||||
| CheckboxComponent
|
||||
| FormComponent
|
||||
| FormButtonComponent
|
||||
@@ -871,7 +873,7 @@ Hello \${ctx.username}
|
||||
{ value: 'bar', label: 'Bar' }
|
||||
]
|
||||
},
|
||||
multiple: {
|
||||
create: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
value: false,
|
||||
@@ -896,6 +898,38 @@ Hello \${ctx.username}
|
||||
softWrap: true
|
||||
}
|
||||
},
|
||||
multiselectcomponent: {
|
||||
name: 'MultiSelect',
|
||||
icon: List,
|
||||
dims: '2:1-3:1',
|
||||
data: {
|
||||
verticalAlignment: 'center',
|
||||
id: '',
|
||||
type: 'multiselectcomponent',
|
||||
componentInput: undefined,
|
||||
configuration: {
|
||||
items: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'text',
|
||||
value: [
|
||||
"Foo", "Bar"
|
||||
]
|
||||
},
|
||||
placeholder: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: 'Select items',
|
||||
onlyStatic: true
|
||||
}
|
||||
},
|
||||
customCss: {
|
||||
input: { style: '' }
|
||||
} as const,
|
||||
card: false,
|
||||
softWrap: true
|
||||
}
|
||||
},
|
||||
numberinputcomponent: {
|
||||
name: 'Number',
|
||||
icon: Binary,
|
||||
|
||||
@@ -30,7 +30,8 @@ const inputs: ComponentSet = {
|
||||
'dateinputcomponent',
|
||||
'fileinputcomponent',
|
||||
'checkboxcomponent',
|
||||
'selectcomponent'
|
||||
'selectcomponent',
|
||||
'multiselectcomponent',
|
||||
]
|
||||
} as const
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ export type AppEditorContext = {
|
||||
openDebugRun: Writable<((componentID: string) => void) | undefined>
|
||||
focusedGrid: Writable<FocusedGrid | undefined>
|
||||
stateId: Writable<number>
|
||||
parentWidth: Writable<number>
|
||||
}
|
||||
|
||||
export type FocusedGrid = { parentComponentId: string; subGridIndex: number }
|
||||
|
||||
Reference in New Issue
Block a user