mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
fix: rework multiselect as app component (#1599)
This commit is contained in:
@@ -182,6 +182,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"shopify" => "ShopifyAPI",
|
||||
"seleniumwire" => "selenium-wire",
|
||||
"openbb-terminal" => "openbb[all]",
|
||||
"riskfolio" => "riskfolio-lib",
|
||||
};
|
||||
|
||||
fn replace_import(x: String) -> String {
|
||||
|
||||
Generated
+9
-10
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "windmill",
|
||||
"version": "1.101.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
@@ -76,7 +75,7 @@
|
||||
"svelte-awesome-color-picker": "^2.4.3",
|
||||
"svelte-check": "^3.3.2",
|
||||
"svelte-highlight": "^7.3.0",
|
||||
"svelte-multiselect": "^8.6.0",
|
||||
"svelte-multiselect": "^8.6.2",
|
||||
"svelte-overlay": "^1.4.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
@@ -6482,12 +6481,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-multiselect": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.0.tgz",
|
||||
"integrity": "sha512-ce1axNn5YrvDwpUA1R4pSKC7oCD8buP7If61VRoOEM5cxMU8t14tLp5fJf27qd8FBJ7vzvJokiN9BUl1u8oL6w==",
|
||||
"version": "8.6.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.2.tgz",
|
||||
"integrity": "sha512-lR7zc/B6yAi9oZxIZyuv8q2VTRDS9aT84HRjHiEcmWD7vbz64pfbmKJZgCUcH5oCtr0wN1NfYymXQvRMfolZkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"svelte": "^3.57.0"
|
||||
"svelte": "^3.59.1"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-overlay": {
|
||||
@@ -12040,12 +12039,12 @@
|
||||
"requires": {}
|
||||
},
|
||||
"svelte-multiselect": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.0.tgz",
|
||||
"integrity": "sha512-ce1axNn5YrvDwpUA1R4pSKC7oCD8buP7If61VRoOEM5cxMU8t14tLp5fJf27qd8FBJ7vzvJokiN9BUl1u8oL6w==",
|
||||
"version": "8.6.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.2.tgz",
|
||||
"integrity": "sha512-lR7zc/B6yAi9oZxIZyuv8q2VTRDS9aT84HRjHiEcmWD7vbz64pfbmKJZgCUcH5oCtr0wN1NfYymXQvRMfolZkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"svelte": "^3.57.0"
|
||||
"svelte": "^3.59.1"
|
||||
}
|
||||
},
|
||||
"svelte-overlay": {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"svelte-awesome-color-picker": "^2.4.3",
|
||||
"svelte-check": "^3.3.2",
|
||||
"svelte-highlight": "^7.3.0",
|
||||
"svelte-multiselect": "^8.6.0",
|
||||
"svelte-multiselect": "^8.6.2",
|
||||
"svelte-overlay": "^1.4.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
<script>
|
||||
// @ts-nocheck
|
||||
import { onMount } from 'svelte'
|
||||
import { fly } from 'svelte/transition'
|
||||
export let id = ''
|
||||
export let value = []
|
||||
export let readonly = false
|
||||
export let placeholder = ''
|
||||
|
||||
let input,
|
||||
inputValue,
|
||||
options = [],
|
||||
activeOption,
|
||||
showOptions = false,
|
||||
selected = {},
|
||||
first = true,
|
||||
slot
|
||||
const iconClearPath =
|
||||
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'
|
||||
|
||||
onMount(() => {
|
||||
slot.querySelectorAll('option').forEach((o) => {
|
||||
o.selected && !value.includes(o.value) && (value = [...value, o.value])
|
||||
options = [...options, { value: o.value, name: o.textContent }]
|
||||
})
|
||||
value &&
|
||||
(selected = options.reduce(
|
||||
(obj, op) => (value.includes(op.value) ? { ...obj, [op.value]: op } : obj),
|
||||
{}
|
||||
))
|
||||
first = false
|
||||
})
|
||||
|
||||
$: if (!first) value = Object.values(selected).map((o) => o.value)
|
||||
$: filtered = options.filter((o) =>
|
||||
inputValue ? o.name.toLowerCase().includes(inputValue.toLowerCase()) : o
|
||||
)
|
||||
$: if ((activeOption && !filtered.includes(activeOption)) || (!activeOption && inputValue))
|
||||
activeOption = filtered[0]
|
||||
|
||||
function add(token) {
|
||||
if (!readonly) selected[token.value] = token
|
||||
}
|
||||
|
||||
function remove(value) {
|
||||
if (!readonly) {
|
||||
const { [value]: val, ...rest } = selected
|
||||
selected = rest
|
||||
}
|
||||
}
|
||||
|
||||
function optionsVisibility(show) {
|
||||
if (readonly) return
|
||||
if (typeof show === 'boolean') {
|
||||
showOptions = show
|
||||
show && input.focus()
|
||||
} else {
|
||||
showOptions = !showOptions
|
||||
}
|
||||
if (!showOptions) {
|
||||
activeOption = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyup(e) {
|
||||
if (e.keyCode === 13) {
|
||||
Object.keys(selected).includes(activeOption.value)
|
||||
? remove(activeOption.value)
|
||||
: add(activeOption)
|
||||
inputValue = ''
|
||||
}
|
||||
if ([38, 40].includes(e.keyCode)) {
|
||||
// up and down arrows
|
||||
const increment = e.keyCode === 38 ? -1 : 1
|
||||
const calcIndex = filtered.indexOf(activeOption) + increment
|
||||
activeOption =
|
||||
calcIndex < 0
|
||||
? filtered[filtered.length - 1]
|
||||
: calcIndex === filtered.length
|
||||
? filtered[0]
|
||||
: filtered[calcIndex]
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
optionsVisibility(false)
|
||||
}
|
||||
|
||||
function handleTokenClick(e) {
|
||||
if (e.target.closest('.token-remove')) {
|
||||
e.stopPropagation()
|
||||
remove(e.target.closest('.token').dataset.id)
|
||||
} else if (e.target.closest('.remove-all')) {
|
||||
selected = []
|
||||
inputValue = ''
|
||||
} else {
|
||||
optionsVisibility(true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOptionMousedown(e) {
|
||||
const value = e.target.dataset.value
|
||||
if (selected[value]) {
|
||||
remove(value)
|
||||
} else {
|
||||
add(options.filter((o) => o.value === value)[0])
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="multiselect" class:readonly>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="tokens" class:showOptions on:click={handleTokenClick}>
|
||||
{#each Object.values(selected) as s}
|
||||
<div class="token" data-id={s.value}>
|
||||
<span>{s.name}</span>
|
||||
{#if !readonly}
|
||||
<div class="token-remove" title="Remove {s.name}">
|
||||
<svg
|
||||
class="icon-clear"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d={iconClearPath} />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="actions">
|
||||
{#if !readonly}
|
||||
<input
|
||||
{id}
|
||||
autocomplete="off"
|
||||
bind:value={inputValue}
|
||||
bind:this={input}
|
||||
on:keyup={handleKeyup}
|
||||
on:blur={handleBlur}
|
||||
{placeholder}
|
||||
/>
|
||||
<div class="remove-all" title="Remove All" class:hidden={!Object.keys(selected).length}>
|
||||
<svg
|
||||
class="icon-clear"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d={iconClearPath} />
|
||||
</svg>
|
||||
</div>
|
||||
<svg
|
||||
class="dropdown-arrow"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"><path d="M5 8l4 4 4-4z" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select bind:this={slot} type="multiple" class="hidden"><slot /></select>
|
||||
|
||||
{#if showOptions}
|
||||
<ul
|
||||
class="options"
|
||||
transition:fly|local={{ duration: 200, y: 5 }}
|
||||
on:mousedown|preventDefault={handleOptionMousedown}
|
||||
>
|
||||
{#each filtered as option}
|
||||
<li
|
||||
class:selected={selected[option.value]}
|
||||
class:active={activeOption === option}
|
||||
data-value={option.value}
|
||||
>
|
||||
{option.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.multiselect {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid hsl(0, 0%, 70%);
|
||||
position: relative;
|
||||
}
|
||||
.multiselect:not(.readonly):hover {
|
||||
border-bottom-color: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
.tokens {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
.tokens::after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: -1px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 2px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: hsl(45, 100%, 51%);
|
||||
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
.tokens.showOptions::after {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
.token {
|
||||
align-items: center;
|
||||
background-color: hsl(214, 17%, 92%);
|
||||
border-radius: 1.25rem;
|
||||
display: flex;
|
||||
margin: 0.25rem 0.5rem 0.25rem 0;
|
||||
max-height: 1.3rem;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.5rem;
|
||||
transition: background-color 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.token:hover {
|
||||
background-color: hsl(214, 15%, 88%);
|
||||
}
|
||||
.readonly .token {
|
||||
color: hsl(0, 0%, 40%);
|
||||
}
|
||||
.token-remove,
|
||||
.remove-all {
|
||||
align-items: center;
|
||||
background-color: hsl(214, 15%, 55%);
|
||||
border-radius: 50%;
|
||||
color: hsl(214, 17%, 92%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 1.25rem;
|
||||
margin-left: 0.25rem;
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
.token-remove:hover,
|
||||
.remove-all:hover {
|
||||
background-color: hsl(215, 21%, 43%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-arrow path {
|
||||
fill: hsl(0, 0%, 70%);
|
||||
}
|
||||
.multiselect:hover .dropdown-arrow path {
|
||||
fill: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
.icon-clear path {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.options {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1), 0px -2px 4px rgba(0, 0, 0, 0.1);
|
||||
left: 0;
|
||||
list-style: none;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
padding-inline-start: 0;
|
||||
position: absolute;
|
||||
top: calc(100% + 1px);
|
||||
width: 100%;
|
||||
}
|
||||
li {
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
li:last-child {
|
||||
border-bottom-left-radius: 0.2rem;
|
||||
border-bottom-right-radius: 0.2rem;
|
||||
}
|
||||
li:not(.selected):hover {
|
||||
background-color: hsl(214, 17%, 92%);
|
||||
}
|
||||
li.selected {
|
||||
background-color: hsl(232, 54%, 41%);
|
||||
color: white;
|
||||
}
|
||||
li.selected:nth-child(even) {
|
||||
background-color: hsl(232, 50%, 45%);
|
||||
color: white;
|
||||
}
|
||||
li.active {
|
||||
background-color: hsl(214, 17%, 88%);
|
||||
}
|
||||
li.selected.active,
|
||||
li.selected:hover {
|
||||
background-color: hsl(232, 48%, 50%);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -22,6 +22,7 @@
|
||||
import { initConfig, initOutput } from '$lib/components/apps/editor/appUtils'
|
||||
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
|
||||
import AppCheckbox from '../../inputs/AppCheckbox.svelte'
|
||||
import AppSelect from '../../inputs/AppSelect.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -297,7 +298,7 @@
|
||||
on:keypress={() => toggleRow(row, rowIndex)}
|
||||
on:click={() => toggleRow(row, rowIndex)}
|
||||
>
|
||||
<div class="center-center h-full w-full flex-wrap gap-1">
|
||||
<div class="center-center h-full w-full flex-wrap gap-1.5">
|
||||
{#each actionButtons as actionButton, actionIndex (actionButton?.id)}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
@@ -387,6 +388,19 @@
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
{:else if actionButton.type == 'selectcomponent'}
|
||||
<AppSelect
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row, rowIndex)
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
{/if}
|
||||
{:else if actionButton.type == 'buttoncomponent'}
|
||||
<AppButton
|
||||
@@ -415,6 +429,18 @@
|
||||
toggleRow(row, rowIndex)
|
||||
}}
|
||||
/>
|
||||
{:else if actionButton.type == 'selectcomponent'}
|
||||
<AppSelect
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row, rowIndex)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let horizontalAlignment: HorizontalAlignment | undefined = undefined
|
||||
export let verticalAlignment: VerticalAlignment | undefined = undefined
|
||||
export let noWFull = false
|
||||
export let hFull = false
|
||||
let c = ''
|
||||
export { c as class }
|
||||
export let style = ''
|
||||
@@ -16,7 +17,7 @@
|
||||
noWFull ? '' : 'w-full',
|
||||
tailwindHorizontalAlignment(horizontalAlignment),
|
||||
tailwindVerticalAlignment(verticalAlignment),
|
||||
verticalAlignment ? 'h-full' : '',
|
||||
verticalAlignment || hFull ? 'h-full' : '',
|
||||
c
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -24,13 +25,15 @@
|
||||
const { app, worldStore, componentControl, runnableComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['checkboxcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
if (controls) {
|
||||
$componentControl[id] = controls
|
||||
}
|
||||
|
||||
let defaultValue: boolean | undefined = undefined
|
||||
let labelValue: string = ''
|
||||
|
||||
// As the checkbox is a special case and has no input
|
||||
// we need to manually set the output
|
||||
|
||||
@@ -38,22 +41,28 @@
|
||||
result: false
|
||||
})
|
||||
|
||||
$: defaultValue != undefined && outputs?.result.set(defaultValue)
|
||||
$: resolvedConfig.defaultValue != undefined && outputs?.result.set(resolvedConfig.defaultValue)
|
||||
|
||||
$: css = concatCustomCss($app.css?.checkboxcomponent, customCss)
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.label} bind:value={labelValue} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
{#each Object.keys(components['checkboxcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{extraKey}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
|
||||
<Toggle
|
||||
size="sm"
|
||||
{extraKey}
|
||||
checked={defaultValue}
|
||||
options={{ right: labelValue }}
|
||||
checked={resolvedConfig.defaultValue}
|
||||
options={{ right: resolvedConfig.label }}
|
||||
textClass={css?.text?.class ?? ''}
|
||||
textStyle={css?.text?.style ?? ''}
|
||||
on:change={(e) => {
|
||||
|
||||
@@ -1,69 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import Select from 'svelte-select'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
// import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
// @ts-ignore
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: { label: string; value: string }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: string[]
|
||||
|
||||
let outputs = initOutput($worldStore, id, {
|
||||
result: [] as string[]
|
||||
})
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['multiselectcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
// $: outputs && handleOutputs()
|
||||
|
||||
// function handleOutputs() {
|
||||
// value = outputs.result.peak()
|
||||
// }
|
||||
|
||||
let value: { value: string }[] | undefined = outputs?.result.peak()
|
||||
let value: string[] | undefined = outputs?.result.peak()
|
||||
|
||||
$: labels && handleItems()
|
||||
|
||||
let labels: string[] | undefined = []
|
||||
$: resolvedConfig.items && handleItems()
|
||||
|
||||
function handleItems() {
|
||||
if (Array.isArray(labels)) {
|
||||
items = labels?.map((label) => {
|
||||
const stringLabel = typeof label === 'string' ? label : `NOT_STRING`
|
||||
|
||||
return {
|
||||
label: stringLabel,
|
||||
value: label
|
||||
}
|
||||
if (Array.isArray(resolvedConfig.items)) {
|
||||
items = resolvedConfig.items?.map((label) => {
|
||||
return typeof label === 'string' ? label : `NOT_STRING`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: value ? outputs?.result.set(value.map((v) => v.value)) : outputs?.result.set([])
|
||||
$: value ? outputs?.result.set(value) : outputs?.result.set([])
|
||||
|
||||
$: css = concatCustomCss($app.css?.multiselectcomponent, customCss)
|
||||
|
||||
$: outerDiv && css?.multiselect?.style && outerDiv.setAttribute('style', css?.multiselect?.style)
|
||||
let outerDiv: HTMLDivElement | undefined = undefined
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.items} bind:value={labels} />
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
{#each Object.keys(components['multiselectcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
|
||||
<AlignWrapper {render} hFull>
|
||||
<div
|
||||
class="app-select w-full"
|
||||
style="height: 100%; overflow: auto;"
|
||||
class="app-select w-full h-full"
|
||||
on:pointerdown={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.stopPropagation()
|
||||
@@ -71,30 +74,16 @@
|
||||
}}
|
||||
>
|
||||
{#if !value || Array.isArray(value)}
|
||||
<Select
|
||||
--border-radius="0"
|
||||
--border-color="#999"
|
||||
multiple
|
||||
on:change={(e) => e.stopPropagation()}
|
||||
{items}
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={'border-color: #999; min-height: 100%;' +
|
||||
SELECT_INPUT_DEFAULT_STYLE.containerStyles +
|
||||
css?.input?.style}
|
||||
bind:value
|
||||
{placeholder}
|
||||
on:click={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
}}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
}}
|
||||
floatingConfig={{
|
||||
strategy: 'fixed'
|
||||
<MultiSelect
|
||||
bind:outerDiv
|
||||
outerDivClass={' h-full'}
|
||||
ulSelectedClass={`${resolvedConfig.allowOverflow ? '' : 'overflow-auto'} max-h-full`}
|
||||
bind:selected={value}
|
||||
options={items}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
allowUserOptions={resolvedConfig.create}
|
||||
on:open={() => {
|
||||
$selectedComponent = [id]
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
@@ -106,6 +95,7 @@
|
||||
<style global>
|
||||
.app-select .value-container {
|
||||
padding: 0 !important;
|
||||
overflow: auto;
|
||||
}
|
||||
.svelte-select-list {
|
||||
z-index: 1000 !important;
|
||||
|
||||
@@ -5,22 +5,40 @@
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } 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 { initOutput } from '../../editor/appUtils'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'selectcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let extraKey: string | undefined = undefined
|
||||
export let preclickAction: (() => Promise<void>) | undefined = undefined
|
||||
export let recomputeIds: string[] | undefined = undefined
|
||||
export let controls: { left: () => boolean; right: () => boolean | string } | undefined =
|
||||
undefined
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: { label: string; value: any; created?: boolean }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
const {
|
||||
app,
|
||||
worldStore,
|
||||
connectingInput,
|
||||
selectedComponent,
|
||||
runnableComponents,
|
||||
componentControl
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
if (controls) {
|
||||
$componentControl[id] = controls
|
||||
}
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['selectcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
let outputs = initOutput($worldStore, id, {
|
||||
result: undefined as string | undefined
|
||||
@@ -28,13 +46,13 @@
|
||||
|
||||
let value: string | undefined = outputs?.result.peak()
|
||||
|
||||
$: items && handleItems()
|
||||
$: resolvedConfig.items && handleItems()
|
||||
|
||||
let listItems: { label: string; value: string; created?: boolean }[] = []
|
||||
|
||||
function handleItems() {
|
||||
listItems = Array.isArray(items)
|
||||
? items.map((item) => {
|
||||
listItems = Array.isArray(resolvedConfig.items)
|
||||
? resolvedConfig.items.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
value: JSON.stringify(item.value)
|
||||
@@ -42,10 +60,10 @@
|
||||
})
|
||||
: []
|
||||
let rawValue
|
||||
if (defaultValue !== undefined) {
|
||||
rawValue = defaultValue
|
||||
if (resolvedConfig.defaultValue !== undefined) {
|
||||
rawValue = resolvedConfig.defaultValue
|
||||
} else if (listItems.length > 0) {
|
||||
rawValue = items[0].value
|
||||
rawValue = resolvedConfig.items[0].value
|
||||
}
|
||||
if (rawValue !== undefined) {
|
||||
value = JSON.stringify(rawValue)
|
||||
@@ -56,27 +74,29 @@
|
||||
function onChange(e: CustomEvent) {
|
||||
e?.stopPropagation()
|
||||
|
||||
if (create) {
|
||||
if (resolvedConfig.create) {
|
||||
listItems = listItems.map((i) => {
|
||||
delete i.created
|
||||
return i
|
||||
})
|
||||
}
|
||||
|
||||
preclickAction?.()
|
||||
let result: any = undefined
|
||||
try {
|
||||
result = JSON.parse(e.detail?.['value'])
|
||||
} catch (_) {}
|
||||
value = e.detail?.['value']
|
||||
outputs?.result.set(result)
|
||||
if (recomputeIds) {
|
||||
recomputeIds.forEach((id) => $runnableComponents?.[id]?.cb())
|
||||
}
|
||||
}
|
||||
|
||||
$: css = concatCustomCss($app.css?.selectcomponent, customCss)
|
||||
|
||||
let defaultValue: any = undefined
|
||||
|
||||
function handleFilter(e) {
|
||||
if (create) {
|
||||
if (resolvedConfig.create) {
|
||||
if (e.detail.length === 0 && filterText.length > 0) {
|
||||
const prev = listItems.filter((i) => !i.created)
|
||||
listItems = [
|
||||
@@ -87,26 +107,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: defaultValue && handleDefault()
|
||||
$: resolvedConfig.defaultValue && handleDefault()
|
||||
|
||||
function handleDefault() {
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
outputs?.result.set(defaultValue)
|
||||
if (resolvedConfig.defaultValue) {
|
||||
value = JSON.stringify(resolvedConfig.defaultValue)
|
||||
outputs?.result.set(resolvedConfig.defaultValue)
|
||||
}
|
||||
}
|
||||
let create = false
|
||||
let filterText = ''
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.items} bind:value={items} />
|
||||
<InputValue {id} input={configuration.placeholder} bind:value={placeholder} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
<InputValue {id} input={configuration.create} bind:value={create} />
|
||||
|
||||
{#each Object.keys(components['selectcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{extraKey}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
|
||||
<AlignWrapper {render} {verticalAlignment}>
|
||||
<div
|
||||
class="app-select w-full"
|
||||
style="height: 34px;"
|
||||
@@ -124,23 +147,21 @@
|
||||
on:clear={onChange}
|
||||
on:change={onChange}
|
||||
items={listItems}
|
||||
listAutoWidth={false}
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={'border-color: #999;' +
|
||||
SELECT_INPUT_DEFAULT_STYLE.containerStyles +
|
||||
css?.input?.style}
|
||||
{value}
|
||||
{placeholder}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div slot="item" let:item>
|
||||
{#if create}
|
||||
{item.created ? 'Add new: ' : ''}
|
||||
{/if}
|
||||
{item.label}
|
||||
<div slot="item" let:item
|
||||
>{#if resolvedConfig.create}{item.created ? 'Add new: ' : ''}{/if}{item.label}
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -252,9 +252,9 @@
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
<AppSelect
|
||||
recomputeIds={component.recomputeIds}
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
@@ -262,8 +262,6 @@
|
||||
{:else if component.type === 'multiselectcomponent'}
|
||||
<AppMultiSelect
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
|
||||
@@ -90,8 +90,9 @@ export type AggridComponent = BaseComponent<'aggridcomponent'>
|
||||
export type DisplayComponent = BaseComponent<'displaycomponent'>
|
||||
export type ImageComponent = BaseComponent<'imagecomponent'>
|
||||
export type InputComponent = BaseComponent<'inputcomponent'>
|
||||
export type SelectComponent = BaseComponent<'resourceselectcomponent'>
|
||||
export type ResourceSelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type SelectComponent = BaseComponent<'selectcomponent'> & RecomputeOthersSource
|
||||
export type ResourceSelectComponent = BaseComponent<'resourceselectcomponent'> &
|
||||
RecomputeOthersSource
|
||||
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
|
||||
export type CheckboxComponent = BaseComponent<'checkboxcomponent'> & RecomputeOthersSource
|
||||
export type RadioComponent = BaseComponent<'radiocomponent'>
|
||||
@@ -470,7 +471,7 @@ export const components = {
|
||||
fields: {},
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
recomputeIds: true,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
@@ -480,7 +481,6 @@ export const components = {
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
selectOptions: selectOptions.buttonColorOptions,
|
||||
value: 'blue',
|
||||
tooltip: 'Theses presets can be overwritten with custom styles.'
|
||||
@@ -562,7 +562,6 @@ export const components = {
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
selectOptions: selectOptions.buttonColorOptions,
|
||||
value: 'blue'
|
||||
},
|
||||
@@ -610,7 +609,7 @@ export const components = {
|
||||
fields: {},
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
recomputeIds: true,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
@@ -620,7 +619,6 @@ export const components = {
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
value: 'dark',
|
||||
selectOptions: selectOptions.buttonColorOptions
|
||||
},
|
||||
@@ -653,7 +651,7 @@ export const components = {
|
||||
fields: {},
|
||||
runnable: undefined
|
||||
},
|
||||
recomputeIds: undefined,
|
||||
recomputeIds: true,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
@@ -663,7 +661,6 @@ export const components = {
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
value: 'dark',
|
||||
selectOptions: buttonColorOptions,
|
||||
tooltip: 'Theses presets can be overwritten with custom styles.'
|
||||
@@ -1060,7 +1057,7 @@ Hello \${ctx.username}
|
||||
initialData: {
|
||||
...defaultAlignement,
|
||||
componentInput: undefined,
|
||||
recomputeIds: undefined,
|
||||
recomputeIds: true,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
@@ -1132,9 +1129,13 @@ Hello \${ctx.username}
|
||||
dims: '2:1-3:1' as AppComponentDimensions,
|
||||
|
||||
customCss: {
|
||||
input: { style: '' }
|
||||
input: {
|
||||
style: '',
|
||||
tooltip: 'https://github.com/rob-balfre/svelte-select/blob/master/docs/theming_variables.md'
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
recomputeIds: true,
|
||||
verticalAlignment: 'center',
|
||||
componentInput: undefined,
|
||||
configuration: {
|
||||
@@ -1153,7 +1154,7 @@ Hello \${ctx.username}
|
||||
value: false,
|
||||
onlyStatic: true,
|
||||
tooltip: 'Allows user to manually add new value',
|
||||
customTitle: 'Manually add new value '
|
||||
customTitle: 'User creatable'
|
||||
},
|
||||
placeholder: {
|
||||
type: 'static',
|
||||
@@ -1175,7 +1176,11 @@ Hello \${ctx.username}
|
||||
dims: '2:1-3:1' as AppComponentDimensions,
|
||||
|
||||
customCss: {
|
||||
input: { style: '' }
|
||||
multiselect: {
|
||||
style: '',
|
||||
tooltip:
|
||||
'See https://multiselect.janosh.dev/#with-css-variables for the available variables'
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
componentInput: undefined,
|
||||
@@ -1191,6 +1196,22 @@ Hello \${ctx.username}
|
||||
fieldType: 'text',
|
||||
value: 'Select items',
|
||||
onlyStatic: true
|
||||
},
|
||||
create: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
value: false,
|
||||
onlyStatic: true,
|
||||
tooltip: 'Allows user to manually add new value',
|
||||
customTitle: 'User creatable'
|
||||
},
|
||||
allowOverflow: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
value: true,
|
||||
onlyStatic: true,
|
||||
tooltip:
|
||||
'If too many items, the box overflow its container instead of having an internal scroll'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1718,7 +1739,6 @@ Hello \${ctx.username}
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
selectOptions: buttonColorOptions,
|
||||
value: 'blue',
|
||||
tooltip:
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
export let forceClass: boolean = false
|
||||
export let quickStyleProperties: PropertyGroup[] | undefined = undefined
|
||||
export let componentType: TypedComponent['type'] | undefined = undefined
|
||||
export let tooltip: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let isQuickMenuOpen = false
|
||||
|
||||
@@ -33,6 +35,9 @@
|
||||
</div>
|
||||
{#if value}
|
||||
<div class="px-3">
|
||||
{#if tooltip}
|
||||
<div class="text-gray-600 text-2xs py-2">{tooltip}</div>
|
||||
{/if}
|
||||
{#if value.style !== undefined || forceStyle}
|
||||
<div class="pb-2">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
<TableActions id={component.id} bind:components={componentSettings.item.data.actionButtons} />
|
||||
{/if}
|
||||
|
||||
{#if componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent' || componentSettings.item.data.type === 'checkboxcomponent'}
|
||||
{#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}
|
||||
@@ -306,6 +306,7 @@
|
||||
<CssProperty
|
||||
forceStyle={ccomponents[component.type].customCss[name].style != undefined}
|
||||
forceClass={ccomponents[component.type].customCss[name].class != undefined}
|
||||
tooltip={ccomponents[component.type].customCss[name].tooltip}
|
||||
{name}
|
||||
bind:value={componentSettings.item.data.customCss[name]}
|
||||
/>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</script>
|
||||
|
||||
<PanelSection
|
||||
title="Recompute others"
|
||||
tooltip="Select components to recompute after running this script"
|
||||
title="Trigger Runnable"
|
||||
tooltip="Select components to recompute after running this runnable as a success"
|
||||
documentationLink="https://docs.windmill.dev/docs/apps/app_settings#recompute-others"
|
||||
>
|
||||
{#if Object.keys($runnableComponents ?? {}).filter((id) => id !== ownId).length > 0}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
quickStyleProperties={quickStyleProperties?.[component.type]?.[name]}
|
||||
forceStyle={ccomponents[component.type].customCss[name].style !== undefined}
|
||||
forceClass={ccomponents[component.type].customCss[name].class !== undefined}
|
||||
tooltip={ccomponents[component.type].customCss[name].tooltip}
|
||||
{name}
|
||||
componentType={component.type}
|
||||
bind:value={component.customCss[name]}
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
clearErrorByComponentId,
|
||||
clearJobsByComponentId
|
||||
} from '../appUtils'
|
||||
import type { ButtonComponent, CheckboxComponent } from '../component'
|
||||
import type { ButtonComponent, CheckboxComponent, SelectComponent } from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import TableActionLabel from './TableActionLabel.svelte'
|
||||
import { Inspect, ToggleRightIcon } from 'lucide-svelte'
|
||||
import { Inspect, List, ToggleRightIcon } from 'lucide-svelte'
|
||||
|
||||
export let components: (BaseAppComponent & (ButtonComponent | CheckboxComponent))[]
|
||||
export let components: (BaseAppComponent &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent))[]
|
||||
export let id: string
|
||||
|
||||
const { selectedComponent, app, errorByComponent, jobs } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function addComponent(typ: 'buttoncomponent' | 'checkboxcomponent') {
|
||||
function addComponent(typ: 'buttoncomponent' | 'checkboxcomponent' | 'selectcomponent') {
|
||||
const actionId = getNextId(components.map((x) => x.id.split('_')[1]))
|
||||
|
||||
const newComponent = {
|
||||
@@ -96,5 +97,15 @@
|
||||
>
|
||||
+ <ToggleRightIcon size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-gray-600"
|
||||
wrapperClasses="w-full"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => addComponent('selectcomponent')}
|
||||
title="Add Select"
|
||||
>
|
||||
+ <List size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@
|
||||
Run on start and app refresh
|
||||
<Tooltip>
|
||||
You may want to disable this so that the background runnable is only triggered by
|
||||
changes to other values or triggered by another computation on a button (See
|
||||
'Recompute Others')
|
||||
changes to other values or triggered by another computation on a button (See 'Trigger
|
||||
Runnables')
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Toggle
|
||||
|
||||
Reference in New Issue
Block a user