mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat: allow labeled values in app multiselect
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -30,7 +31,7 @@
|
||||
|
||||
const { app, worldStore, selectedComponent, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: string[]
|
||||
let items: (string | { value: string; label: any })[] = []
|
||||
|
||||
const resolvedConfig = initConfig(
|
||||
components['multiselectcomponent'].initialData.configuration,
|
||||
@@ -41,12 +42,33 @@
|
||||
result: [] as string[]
|
||||
})
|
||||
|
||||
let value: string[] | undefined = [...new Set(outputs?.result.peak())] as string[]
|
||||
let selectedItems: (string | { value: string; label: any })[] | undefined = [
|
||||
...new Set(outputs?.result.peak())
|
||||
] as string[]
|
||||
|
||||
function setResultsFromSelectedItems() {
|
||||
outputs?.result.set([
|
||||
...(selectedItems?.map((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return item?.value ?? `NOT_STRING`
|
||||
} else if (typeof item == 'string') {
|
||||
return item
|
||||
} else if (typeof item == 'object' && item.label != undefined) {
|
||||
return item.label
|
||||
} else {
|
||||
return 'NOT_STRING'
|
||||
}
|
||||
}) ?? [])
|
||||
])
|
||||
}
|
||||
|
||||
$componentControl[id] = {
|
||||
setValue(nvalue: string[]) {
|
||||
value = [...new Set(nvalue)]
|
||||
outputs?.result.set([...(value ?? [])])
|
||||
if (Array.isArray(nvalue)) {
|
||||
setSelectedItemsFromValues(nvalue)
|
||||
} else {
|
||||
console.error('Invalid value for multiselect component, expected array', nvalue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,21 +76,33 @@
|
||||
|
||||
function handleItems() {
|
||||
if (Array.isArray(resolvedConfig.items)) {
|
||||
items = resolvedConfig.items?.map((label) => {
|
||||
return typeof label === 'string' ? label : `NOT_STRING`
|
||||
items = resolvedConfig.items?.map((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return item
|
||||
}
|
||||
return typeof item === 'string' ? item : `NOT_STRING`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: resolvedConfig.defaultItems && handleDefaultItems()
|
||||
$: resolvedConfig.defaultItems && setSelectedItemsFromValues(resolvedConfig.defaultItems)
|
||||
|
||||
function handleDefaultItems() {
|
||||
if (Array.isArray(resolvedConfig.defaultItems)) {
|
||||
const nvalue = resolvedConfig.defaultItems?.map((label) => {
|
||||
return typeof label === 'string' ? label : `NOT_STRING`
|
||||
})
|
||||
value = [...new Set(nvalue)]
|
||||
outputs?.result.set([...(value ?? [])])
|
||||
function setSelectedItemsFromValues(values: any[]) {
|
||||
if (Array.isArray(values)) {
|
||||
const nvalue = values
|
||||
.map((value) => {
|
||||
return (
|
||||
items.find((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return deepEqual(item.value, value)
|
||||
}
|
||||
return item == value
|
||||
}) ?? (typeof value == 'string' ? value : undefined)
|
||||
)
|
||||
})
|
||||
.filter((item) => item != undefined)
|
||||
selectedItems = [...new Set(nvalue)]
|
||||
setResultsFromSelectedItems()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +177,7 @@
|
||||
use:floatingRef
|
||||
bind:clientWidth={w}
|
||||
>
|
||||
{#if !value || Array.isArray(value)}
|
||||
{#if !selectedItems || Array.isArray(selectedItems)}
|
||||
<MultiSelect
|
||||
bind:outerDiv
|
||||
outerDivClass={`${resolvedConfig.allowOverflow ? '' : 'h-full'}`}
|
||||
@@ -151,7 +185,7 @@
|
||||
--sms-border={'none'}
|
||||
--sms-min-height={'32px'}
|
||||
--sms-focus-border={'none'}
|
||||
bind:selected={value}
|
||||
bind:selected={selectedItems}
|
||||
options={items}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
allowUserOptions={resolvedConfig.create}
|
||||
@@ -159,7 +193,7 @@
|
||||
if (event?.detail?.type === 'removeAll') {
|
||||
outputs?.result.set([])
|
||||
} else {
|
||||
outputs?.result.set([...(value ?? [])])
|
||||
setResultsFromSelectedItems()
|
||||
}
|
||||
}}
|
||||
on:open={() => {
|
||||
@@ -181,7 +215,7 @@
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
{typeof option == 'object' ? option?.label ?? 'NO_LABEL' : option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
<Portal name="app-multiselect-v2">
|
||||
@@ -197,7 +231,7 @@
|
||||
</div>
|
||||
</Portal>
|
||||
{:else}
|
||||
Value {value} is not an array
|
||||
Value {selectedItems} is not an array
|
||||
{/if}
|
||||
</div>
|
||||
</AlignWrapper>
|
||||
|
||||
@@ -92,7 +92,12 @@
|
||||
function handleItems() {
|
||||
listItems = Array.isArray(resolvedConfig.items)
|
||||
? resolvedConfig.items.map((item) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
if (typeof item == 'string') {
|
||||
return {
|
||||
label: item,
|
||||
value: JSON.stringify(item)
|
||||
}
|
||||
} else if (!item || typeof item !== 'object') {
|
||||
console.error('Select component items should be an array of objects')
|
||||
return {
|
||||
label: 'not object',
|
||||
@@ -113,7 +118,12 @@
|
||||
if (resolvedConfig.defaultValue !== undefined) {
|
||||
rawValue = resolvedConfig.defaultValue
|
||||
} else if (listItems.length > 0 && resolvedConfig?.preselectFirst) {
|
||||
rawValue = resolvedConfig.items[0].value
|
||||
let firstItem = resolvedConfig.items[0]
|
||||
if (typeof firstItem === 'string') {
|
||||
rawValue = firstItem
|
||||
} else {
|
||||
rawValue = resolvedConfig.items[0].value
|
||||
}
|
||||
}
|
||||
if (rawValue !== undefined && rawValue !== null) {
|
||||
value = JSON.stringify(rawValue)
|
||||
|
||||
@@ -37,10 +37,11 @@
|
||||
$componentControl[id] = {
|
||||
setValue(nvalue: string) {
|
||||
selected = nvalue
|
||||
selectedIndex = resolvedConfig.items.findIndex((item) => item.value === nvalue)
|
||||
selectedIndex = resolvedConfig.items.findIndex((item) => getValue(item) === nvalue)
|
||||
},
|
||||
setTab(index) {
|
||||
selected = resolvedConfig.items?.[index]?.value
|
||||
let item = resolvedConfig.items?.[index]
|
||||
selected = getValue(item)
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
@@ -48,11 +49,11 @@
|
||||
function setDefaultValue() {
|
||||
if (resolvedConfig.defaultValue != undefined) {
|
||||
selectedIndex = resolvedConfig.items.findIndex(
|
||||
(item) => item.value === resolvedConfig.defaultValue
|
||||
(item) => getValue(item) === resolvedConfig.defaultValue
|
||||
)
|
||||
}
|
||||
if (selectedIndex === -1 || resolvedConfig.defaultValue == undefined) {
|
||||
selected = resolvedConfig.items[0].value
|
||||
selected = getValue(resolvedConfig.items[0])
|
||||
} else if (resolvedConfig.defaultValue) {
|
||||
selected = resolvedConfig.items[selectedIndex].value
|
||||
}
|
||||
@@ -72,6 +73,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getValue(item: string | { label: string; value: string }) {
|
||||
return typeof item == 'string' ? item : item.value
|
||||
}
|
||||
function getLabel(item: string | { label: string; value: string }) {
|
||||
return typeof item == 'string' ? item : item.label
|
||||
}
|
||||
|
||||
let css = initCss($app.css?.selectstepcomponent, customCss)
|
||||
$: selected && handleSelection(selected)
|
||||
</script>
|
||||
@@ -106,14 +114,15 @@
|
||||
>
|
||||
<div class="w-full" on:pointerdown={onPointerDown}>
|
||||
<Stepper
|
||||
tabs={(resolvedConfig?.items ?? []).map((item) => item.label)}
|
||||
tabs={(resolvedConfig?.items ?? []).map((item) => getLabel(item))}
|
||||
hasValidations={false}
|
||||
allowStepNavigation={true}
|
||||
{selectedIndex}
|
||||
on:click={(e) => {
|
||||
const index = e.detail.index
|
||||
selectedIndex = index
|
||||
outputs?.result.set(resolvedConfig?.items[index].value)
|
||||
let item = resolvedConfig?.items[index]
|
||||
outputs?.result.set(getValue(item))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2199,13 +2199,12 @@ This is a paragraph.
|
||||
items: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'text',
|
||||
value: ['Foo', 'Bar']
|
||||
subFieldType: 'labeledselect',
|
||||
value: ['Foo', 'Bar'] as (string | { label: string; value: string })[]
|
||||
} as StaticAppInput,
|
||||
defaultItems: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'text',
|
||||
fieldType: 'object',
|
||||
value: []
|
||||
} as StaticAppInput,
|
||||
placeholder: {
|
||||
@@ -3384,7 +3383,7 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
value: [
|
||||
{ value: 'foo', label: 'Foo' },
|
||||
{ value: 'bar', label: 'Bar' }
|
||||
]
|
||||
] as (string | { label: string; value: string })[]
|
||||
} as StaticAppInput,
|
||||
|
||||
defaultValue: {
|
||||
@@ -3419,14 +3418,11 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'labeledselect',
|
||||
value: [
|
||||
{ value: 'foo', label: 'Foo' },
|
||||
{ value: 'bar', label: 'Bar' }
|
||||
]
|
||||
value: ['Foo', 'Bar'] as (string | { label: string; value: string })[]
|
||||
} as StaticAppInput,
|
||||
defaultValue: {
|
||||
type: 'static',
|
||||
value: undefined as { value: string; label: string } | undefined,
|
||||
value: undefined as any,
|
||||
fieldType: 'object'
|
||||
}
|
||||
}
|
||||
|
||||
+72
-60
@@ -218,33 +218,43 @@
|
||||
}
|
||||
|
||||
let raw: boolean = false
|
||||
// let mounted = false
|
||||
|
||||
// $: if (componentInput.value && mounted) {
|
||||
// const newItems = (Array.isArray(componentInput.value) ? componentInput.value : [])
|
||||
// .filter((x) => x != undefined)
|
||||
// .map((item, index) => {
|
||||
// return { value: item, id: generateRandomString() }
|
||||
// })
|
||||
|
||||
// if (
|
||||
// JSON.stringify(newItems.map((i) => i.value)) !== JSON.stringify(items.map((i) => i.value))
|
||||
// ) {
|
||||
// items = newItems
|
||||
// }
|
||||
// }
|
||||
|
||||
// onMount(() => {
|
||||
// mounted = true
|
||||
// })
|
||||
let refreshCount = 0
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 flex-col mt-2 w-full">
|
||||
{#if Array.isArray(items) && componentInput.value}
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-row items-center justify-between mr-1">
|
||||
<div class="text-xs text-tertiary font-semibold">{pluralize(items.length, 'item')}</div>
|
||||
|
||||
{#if subFieldType === 'ag-grid' || subFieldType === 'table-column'}
|
||||
{#if subFieldType == 'labeledselect'}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'w/ value'
|
||||
}}
|
||||
size="xs"
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
items = items.map((item) => {
|
||||
if (typeof item.value === 'string') {
|
||||
return { ...item, value: { label: item.value, value: item.value } }
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
} else {
|
||||
items = items.map((item) => {
|
||||
if (typeof item.value === 'object' && item.value.hasOwnProperty('label')) {
|
||||
return { ...item, value: item.value.value }
|
||||
} else {
|
||||
return { ...item, value: JSON.stringify(item.value) }
|
||||
}
|
||||
})
|
||||
}
|
||||
refreshCount += 1
|
||||
}}
|
||||
checked={items.some((x) => typeof x.value != 'string')}
|
||||
/>
|
||||
{:else if subFieldType === 'ag-grid' || subFieldType === 'table-column'}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Raw'
|
||||
@@ -254,49 +264,51 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<section
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
>
|
||||
{#each items as item, index (item.id)}
|
||||
<div class="border-0 outline-none w-full">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
{#key refreshCount}
|
||||
<section
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
>
|
||||
{#each items as item, index (item.id)}
|
||||
<div class="border-0 outline-none w-full">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
|
||||
<div class="flex flex-row gap-2 items-center relative my-1 w-full">
|
||||
<div class="grow min-w-0">
|
||||
<SubTypeEditor
|
||||
{id}
|
||||
subFieldType={raw ? 'object' : subFieldType}
|
||||
bind:componentInput
|
||||
bind:value={item.value}
|
||||
on:remove={() => deleteElementByType(index)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-4 h-4 cursor-move handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
<div class="flex flex-row gap-2 items-center relative my-1 w-full">
|
||||
<div class="grow min-w-0">
|
||||
<SubTypeEditor
|
||||
{id}
|
||||
subFieldType={raw ? 'object' : subFieldType}
|
||||
bind:componentInput
|
||||
bind:value={item.value}
|
||||
on:remove={() => deleteElementByType(index)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-4 h-4 cursor-move handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{#if subFieldType !== 'db-explorer'}
|
||||
<button
|
||||
class="z-10 rounded-full p-1 duration-200 hover:bg-surface-hover"
|
||||
aria-label="Remove item"
|
||||
on:click|preventDefault|stopPropagation={() => deleteElementByType(index)}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if subFieldType !== 'db-explorer'}
|
||||
<button
|
||||
class="z-10 rounded-full p-1 duration-200 hover:bg-surface-hover"
|
||||
aria-label="Remove item"
|
||||
on:click|preventDefault|stopPropagation={() => deleteElementByType(index)}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/each}
|
||||
</section>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if subFieldType === 'db-explorer'}
|
||||
{#if componentInput.loading}
|
||||
|
||||
@@ -484,7 +484,7 @@
|
||||
Show
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex gap-2 items-center flex-wrap">
|
||||
<div class="!text-2xs">Full height</div>
|
||||
{#if componentSettings?.item?.[12]?.fullHeight !== undefined}
|
||||
<Toggle
|
||||
@@ -543,7 +543,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-1 text-tertiary">
|
||||
<div class="overflow-auto grid grid-cols-2 gap-1 text-tertiary">
|
||||
<div>
|
||||
<span class="text-secondary text-xs">Copy:</span>
|
||||
</div>
|
||||
|
||||
+6
@@ -152,6 +152,12 @@
|
||||
: undefined}
|
||||
showSchemaExplorer
|
||||
/>
|
||||
{:else if fieldType == 'labeledselect' && typeof componentInput.value == 'string'}
|
||||
<input
|
||||
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
|
||||
bind:value={componentInput.value}
|
||||
placeholder="Label"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex w-full flex-col">
|
||||
<JsonEditor
|
||||
|
||||
@@ -206,7 +206,7 @@ export type AppInput =
|
||||
| AppInputSpec<'array', object[], 'object'>
|
||||
| (AppInputSpec<'array', string[], 'select'> & StaticOptions)
|
||||
| AppInputSpec<'array', object[], 'labeledresource'>
|
||||
| AppInputSpec<'array', object[], 'labeledselect'>
|
||||
| AppInputSpec<'array', (object | string)[], 'labeledselect'>
|
||||
| AppInputSpec<'labeledselect', object>
|
||||
| AppInputSpec<'labeledresource', object>
|
||||
| AppInputSpec<'array', object[], 'tab-select'>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
dispatch('select', fullKey)
|
||||
}
|
||||
|
||||
$: keyLimit = isArray ? 1 : 100
|
||||
$: keyLimit = isArray ? 5 : 100
|
||||
|
||||
$: fullyCollapsed = keys.length > 1 && collapsed
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user