feat: allow labeled values in app multiselect

This commit is contained in:
Ruben Fiszel
2024-11-21 10:39:55 +01:00
parent c99d360c3c
commit d5da75c031
9 changed files with 168 additions and 101 deletions
@@ -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'
}
}
@@ -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>
@@ -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>