diff --git a/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte b/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte index 67847ee49c..ec0c1d00fe 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppMultiSelectV2.svelte @@ -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 type { ObjectOption } from '../../../multiselect/types' export let id: string export let configuration: RichConfigurations @@ -23,6 +24,11 @@ export let render: boolean export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined + // every option is labeled, or no one is. + type Options = ObjectOption[] | string[] + + $: resolvedConfig.items && handleItems() + const [floatingRef, floatingContent] = createFloatingActions({ strategy: 'absolute', middleware: [offset(5), flip(), shift()] @@ -30,7 +36,7 @@ const { app, worldStore, selectedComponent, componentControl } = getContext('AppViewerContext') - let items: { label: string; value: string; created?: boolean }[] + let items: Options = [] const resolvedConfig = initConfig( components['multiselectcomponentv2'].initialData.configuration, @@ -38,59 +44,110 @@ ) const outputs = initOutput($worldStore, id, { - result: [] as { label: string; value: string; created?: boolean }[] + result: [] as Options }) - let value: { label: string; value: string; created?: boolean }[] | undefined = [ - ...new Set(outputs?.result.peak()) - ] as { label: string; value: string; created?: boolean }[] + let value: Options | undefined = isLabeledArray(outputs?.result.peak()) + ? ([...new Set(outputs?.result.peak())] as ObjectOption[]) + : ([...new Set(outputs?.result.peak())] as string[]) $componentControl[id] = { - setValue(nvalue: { label: string; value: string; created?: boolean }[]) { - value = [...new Set(nvalue)] - outputs?.result.set([...(value ?? [])]) + setValue(nvalue: Options) { + if (isLabeledArray(nvalue)) { + value = [...new Set(nvalue)] as ObjectOption[] + outputs?.result.set([...(value ?? [])]) + } } } - $: resolvedConfig.items && handleItems() + function isObjectOption(item: Options[number]): item is ObjectOption { + if (typeof item === 'string') { + return false + } + if (item.label === undefined || item.label === null) { + return false + } + if (typeof item.label !== 'string' && typeof item.label !== 'number') { + return false + } + return true + } - function handleItems() { + function isLabeledArray(arr: Options): arr is ObjectOption[] { + return arr.every((item) => isObjectOption(item)) + } + + function isStringArray(arr: Options): arr is string[] { + return arr.every((item) => typeof item === 'string') + } + + function handleLabeledItems() { if (!Array.isArray(resolvedConfig.items)) { items = [] - } else { - items = resolvedConfig.items?.map((item) => { - if (!item || typeof item !== 'object') { - console.error('Select component items should be an array of objects') - return { - label: 'not object', - value: 'not object' - } - } + return + } + items = resolvedConfig.items?.map((item) => { + if (!item || typeof item !== 'object') { + console.error( + 'When labeled, MultiSelect component items should be an array of { label: string, value: string }.' + ) return { - label: item?.label ?? 'undefined', - value: - typeof item?.value === 'object' - ? JSON.stringify(item.value) - : item?.value ?? 'undefined' + label: 'not object', + value: 'not object' } - }) + } + return { + label: item?.label ?? 'undefined', + value: + typeof item?.value === 'object' ? JSON.stringify(item.value) : item?.value ?? 'undefined' + } + }) + } + + function handleStringItems() { + if (!Array.isArray(resolvedConfig.items)) { + items = [] + return + } + items = resolvedConfig.items?.map((item) => { + if (!item || typeof item !== 'string') { + console.error( + 'When not labeled, MultiSelect component items should be an array of strings.' + ) + return 'not string' + } + return item + }) + } + + function handleItems() { + if (isLabeledArray(resolvedConfig.items)) { + handleLabeledItems() + } else if (isStringArray(resolvedConfig.items)) { + handleStringItems() } } $: resolvedConfig.defaultItems && handleDefaultItems() + // todo function handleDefaultItems() { - let nvalue: { label: string; value: string; created?: boolean }[] + let nvalue: typeof items if (!Array.isArray(resolvedConfig.defaultItems)) { nvalue = [] - } else { - let rawNvalue = new Set( - resolvedConfig.defaultItems?.filter((value) => typeof value === 'string') - ) - nvalue = items?.filter((item) => rawNvalue.has(item.value)) + outputs?.result.set([]) + return + } + let rawNvalue = new Set(resolvedConfig.defaultItems?.filter((v) => typeof v === 'string')) + if (isLabeledArray(items)) { + nvalue = items?.filter((item) => rawNvalue.has(item.value)) as ObjectOption[] + value = [...new Set(nvalue)] + outputs?.result.set([...(value ?? [])]) + } else if (isStringArray(items)) { + nvalue = items?.filter((label) => rawNvalue.has(label)) as string[] + value = [...new Set(nvalue)] + outputs?.result.set([...(value ?? [])]) } - value = [...new Set(nvalue)] - outputs?.result.set([...(value ?? [])]) } let css = initCss($app.css?.multiselectcomponent, customCss) diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index 7d4448baf7..ca1b45d7d5 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -2203,12 +2203,13 @@ This is a paragraph. value: [ { value: 'foo', label: 'Foo' }, { value: 'bar', label: 'Bar' } - ] + ], + hasLabeledMode: true } as StaticAppInput, defaultItems: { type: 'static', fieldType: 'array', - subFieldType: 'selectvalue', + subFieldType: 'simplestringselect', value: undefined } as StaticAppInput, placeholder: { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte index 4b63b97584..bea3cf4deb 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte @@ -2,7 +2,7 @@ import { Button } from '$lib/components/common' import { GripVertical, Loader2, Plus, X } from 'lucide-svelte' import { createEventDispatcher } from 'svelte' - import type { InputType, StaticInput, StaticOptions } from '../../inputType' + import type { InputType, LabeledOption, StaticInput, StaticOptions } from '../../inputType' import SubTypeEditor from './SubTypeEditor.svelte' import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action' import { generateRandomString, pluralize } from '$lib/utils' @@ -10,14 +10,26 @@ import QuickAddColumn from './QuickAddColumn.svelte' import RefreshDatabaseStudioTable from './RefreshDatabaseStudioTable.svelte' - export let componentInput: StaticInput & { loading?: boolean } + export let componentInput: StaticInput & { loading?: boolean; isLabeled?: boolean } export let subFieldType: InputType | undefined = undefined export let selectOptions: StaticOptions['selectOptions'] | undefined = undefined export let id: string | undefined + export let hasLabeledMode: boolean = false const dispatch = createEventDispatcher() const flipDurationMs = 200 + console.log(componentInput.value) + + // transform string[] to LabeledOption[], or the opposite way + function handleLabeledChange() { + if (labeled) { + componentInput.value = componentInput.value?.map((option: LabeledOption) => option?.label) + } else if (!labeled) { + componentInput.value = componentInput.value?.map((label) => ({ label, value: label })) + } + } + function addElementByType() { if (!Array.isArray(componentInput.value)) { componentInput.value = [] @@ -33,13 +45,12 @@ value.push({}) } else if (subFieldType === 'labeledresource' || subFieldType === 'labeledselect') { value.push({ value: 'value', label: 'label' }) - } else if (subFieldType === 'selectvalue') { - value.push('') } else if (subFieldType === 'tab-select') { value.push({ id: '', index: 0 }) } else if ( subFieldType === 'text' || subFieldType === 'textarea' || + subFieldType === 'simplestringselect' || // TODO: Add support for these types subFieldType === 'date' || subFieldType === 'time' || @@ -220,6 +231,10 @@ } let raw: boolean = false + let labeled: boolean = false + + $: labeled + // let mounted = false // $: if (componentInput.value && mounted) { @@ -243,6 +258,12 @@
{#if Array.isArray(items) && componentInput.value} +
{pluralize(items.length, 'item')}
@@ -255,6 +276,16 @@ bind:checked={raw} /> {/if} + {#if hasLabeledMode} + + {/if}
deleteElementByType(index)} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index 02cfce4ea4..067d5f9178 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -43,6 +43,7 @@ export let documentationLink: string | undefined = undefined export let markdownTooltip: string | undefined = undefined export let securedContext = false + export let hasLabeledMode = false const { connectingInput, app } = getContext('AppViewerContext') @@ -171,6 +172,7 @@ {selectOptions} {format} {placeholder} + {hasLabeledMode} bind:componentInput />
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index 9b76c3c759..b8a6a9f870 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -81,6 +81,7 @@ {recomputeOnInputChanged} {showOnDemandOnlyToggle} {securedContext} + hasLabeledMode={meta?.['hasLabeledMode']} /> {#if deletable}
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte index 24a33be398..041835022b 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte @@ -31,6 +31,7 @@ export let placeholder: string | undefined = undefined export let format: string | undefined = undefined export let id: string | undefined + export let hasLabeledMode: boolean = false const { onchange } = getContext('AppViewerContext') @@ -133,7 +134,7 @@ {/if} {:else if fieldType === 'color'} - {:else if fieldType === 'object' || fieldType == 'labeledselect'} + {:else if fieldType === 'object' || fieldType == 'labeledselect' || fieldType === 'simplestringselect'} {#if format?.startsWith('resource-') && (componentInput.value == undefined || typeof componentInput.value == 'string')}
{/if} - {:else if fieldType === 'selectvalue'} -
- -
{:else if fieldType === 'array'} - + {:else if fieldType === 'schema'}
diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index aeb10ccaba..5e913a0c1d 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -20,7 +20,7 @@ export type InputType = | 'any' | 'labeledresource' | 'labeledselect' - | 'selectvalue' + | 'simplestringselect' | 'tab-select' | 'schema' | 'ag-grid' @@ -176,6 +176,7 @@ type InputConfiguration = { noStatic?: boolean onDemandOnly?: boolean hideRefreshButton?: boolean + isLabeled?: boolean } export type StaticOptions = { @@ -208,7 +209,7 @@ export type AppInput = | (AppInputSpec<'array', string[], 'select'> & StaticOptions) | AppInputSpec<'array', object[], 'labeledresource'> | AppInputSpec<'array', object[], 'labeledselect'> - | AppInputSpec<'array', object[], 'selectvalue'> + | AppInputSpec<'array', object[], 'simplestringselect'> | AppInputSpec<'labeledselect', object> | AppInputSpec<'labeledresource', object> | AppInputSpec<'array', object[], 'tab-select'> diff --git a/frontend/vite.config.js.timestamp-1730798937364-ccdd2c7ad7887.mjs b/frontend/vite.config.js.timestamp-1730798937364-ccdd2c7ad7887.mjs new file mode 100644 index 0000000000..871df13b64 --- /dev/null +++ b/frontend/vite.config.js.timestamp-1730798937364-ccdd2c7ad7887.mjs @@ -0,0 +1,64 @@ +// vite.config.js +import { sveltekit } from "file:///home/guest-windmill/windmill/frontend/node_modules/@sveltejs/kit/src/exports/vite/index.js"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import circleDependency from "file:///home/guest-windmill/windmill/frontend/node_modules/vite-plugin-circular-dependency/dist/index.mjs"; +import importMetaUrlPlugin from "file:///home/guest-windmill/windmill/frontend/node_modules/@windmill-labs/esbuild-import-meta-url-plugin/dist/esbuildImportMetaUrlPlugin.js"; +var __vite_injected_original_import_meta_url = "file:///home/guest-windmill/windmill/frontend/vite.config.js"; +var file = fileURLToPath(new URL("package.json", __vite_injected_original_import_meta_url)); +var json = readFileSync(file, "utf8"); +var version = JSON.parse(json); +var config = { + server: { + https: false, + port: 3e3, + proxy: { + "^/api/.*": { + target: process.env.REMOTE ?? "https://app.windmill.dev/", + changeOrigin: true, + cookieDomainRewrite: "localhost" + }, + "^/ws/.*": { + target: process.env.REMOTE_LSP ?? "https://app.windmill.dev", + changeOrigin: true, + ws: true + }, + "^/ws_mp/.*": { + target: process.env.REMOTE_MP ?? "https://app.windmill.dev", + changeOrigin: true, + ws: true + } + } + }, + preview: { + port: 3e3 + }, + plugins: [sveltekit(), circleDependency({ circleImportThrowErr: false })], + define: { + __pkg__: version + }, + optimizeDeps: { + include: ["highlight.js", "highlight.js/lib/core", "monaco-vim"], + exclude: [ + "@codingame/monaco-vscode-standalone-typescript-language-features", + "@codingame/monaco-vscode-standalone-languages", + "monaco-graphql" + ], + esbuildOptions: { + plugins: [importMetaUrlPlugin] + } + }, + resolve: { + alias: { + path: "path-browserify", + "vscode/vscode/vs/editor/contrib/hover/browser/hover": "vscode/vscode/vs/editor/contrib/hover/browser/hoverController" + }, + dedupe: ["vscode", "monaco-editor"] + }, + assetsInclude: ["**/*.wasm"] +}; +var vite_config_default = config; +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9ndWVzdC13aW5kbWlsbC93aW5kbWlsbC9mcm9udGVuZFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvZ3Vlc3Qtd2luZG1pbGwvd2luZG1pbGwvZnJvbnRlbmQvdml0ZS5jb25maWcuanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvZ3Vlc3Qtd2luZG1pbGwvd2luZG1pbGwvZnJvbnRlbmQvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBzdmVsdGVraXQgfSBmcm9tICdAc3ZlbHRlanMva2l0L3ZpdGUnXG5pbXBvcnQgeyByZWFkRmlsZVN5bmMgfSBmcm9tICdmcydcbmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICd1cmwnXG5pbXBvcnQgY2lyY2xlRGVwZW5kZW5jeSBmcm9tICd2aXRlLXBsdWdpbi1jaXJjdWxhci1kZXBlbmRlbmN5J1xuLy8gaW1wb3J0IG1rY2VydCBmcm9tICd2aXRlLXBsdWdpbi1ta2NlcnQnXG5pbXBvcnQgaW1wb3J0TWV0YVVybFBsdWdpbiBmcm9tICdAd2luZG1pbGwtbGFicy9lc2J1aWxkLWltcG9ydC1tZXRhLXVybC1wbHVnaW4nXG5cbmNvbnN0IGZpbGUgPSBmaWxlVVJMVG9QYXRoKG5ldyBVUkwoJ3BhY2thZ2UuanNvbicsIGltcG9ydC5tZXRhLnVybCkpXG5jb25zdCBqc29uID0gcmVhZEZpbGVTeW5jKGZpbGUsICd1dGY4JylcbmNvbnN0IHZlcnNpb24gPSBKU09OLnBhcnNlKGpzb24pXG5cbi8qKiBAdHlwZSB7aW1wb3J0KCd2aXRlJykuVXNlckNvbmZpZ30gKi9cbmNvbnN0IGNvbmZpZyA9IHtcblx0c2VydmVyOiB7XG5cdFx0aHR0cHM6IGZhbHNlLFxuXHRcdHBvcnQ6IDMwMDAsXG5cdFx0cHJveHk6IHtcblx0XHRcdCdeL2FwaS8uKic6IHtcblx0XHRcdFx0dGFyZ2V0OiBwcm9jZXNzLmVudi5SRU1PVEUgPz8gJ2h0dHBzOi8vYXBwLndpbmRtaWxsLmRldi8nLFxuXHRcdFx0XHRjaGFuZ2VPcmlnaW46IHRydWUsXG5cdFx0XHRcdGNvb2tpZURvbWFpblJld3JpdGU6ICdsb2NhbGhvc3QnXG5cdFx0XHR9LFxuXHRcdFx0J14vd3MvLionOiB7XG5cdFx0XHRcdHRhcmdldDogcHJvY2Vzcy5lbnYuUkVNT1RFX0xTUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fSxcblx0XHRcdCdeL3dzX21wLy4qJzoge1xuXHRcdFx0XHR0YXJnZXQ6IHByb2Nlc3MuZW52LlJFTU9URV9NUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fVxuXHRcdH1cblx0fSxcblx0cHJldmlldzoge1xuXHRcdHBvcnQ6IDMwMDBcblx0fSxcblx0cGx1Z2luczogW3N2ZWx0ZWtpdCgpLCBjaXJjbGVEZXBlbmRlbmN5KHsgY2lyY2xlSW1wb3J0VGhyb3dFcnI6IGZhbHNlIH0pXSxcblx0ZGVmaW5lOiB7XG5cdFx0X19wa2dfXzogdmVyc2lvblxuXHR9LFxuXHRvcHRpbWl6ZURlcHM6IHtcblx0XHRpbmNsdWRlOiBbJ2hpZ2hsaWdodC5qcycsICdoaWdobGlnaHQuanMvbGliL2NvcmUnLCAnbW9uYWNvLXZpbSddLFxuXHRcdGV4Y2x1ZGU6IFtcblx0XHRcdCdAY29kaW5nYW1lL21vbmFjby12c2NvZGUtc3RhbmRhbG9uZS10eXBlc2NyaXB0LWxhbmd1YWdlLWZlYXR1cmVzJyxcblx0XHRcdCdAY29kaW5nYW1lL21vbmFjby12c2NvZGUtc3RhbmRhbG9uZS1sYW5ndWFnZXMnLFxuXHRcdFx0J21vbmFjby1ncmFwaHFsJ1xuXHRcdF0sXG5cdFx0ZXNidWlsZE9wdGlvbnM6IHtcblx0XHRcdHBsdWdpbnM6IFtpbXBvcnRNZXRhVXJsUGx1Z2luXVxuXHRcdH1cblx0fSxcblx0cmVzb2x2ZToge1xuXHRcdGFsaWFzOiB7XG5cdFx0XHRwYXRoOiAncGF0aC1icm93c2VyaWZ5Jyxcblx0XHRcdCd2c2NvZGUvdnNjb2RlL3ZzL2VkaXRvci9jb250cmliL2hvdmVyL2Jyb3dzZXIvaG92ZXInOlxuXHRcdFx0XHQndnNjb2RlL3ZzY29kZS92cy9lZGl0b3IvY29udHJpYi9ob3Zlci9icm93c2VyL2hvdmVyQ29udHJvbGxlcidcblx0XHR9LFxuXHRcdGRlZHVwZTogWyd2c2NvZGUnLCAnbW9uYWNvLWVkaXRvciddXG5cdH0sXG5cdGFzc2V0c0luY2x1ZGU6IFsnKiovKi53YXNtJ11cbn1cblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQW9TLFNBQVMsaUJBQWlCO0FBQzlULFNBQVMsb0JBQW9CO0FBQzdCLFNBQVMscUJBQXFCO0FBQzlCLE9BQU8sc0JBQXNCO0FBRTdCLE9BQU8seUJBQXlCO0FBTG9KLElBQU0sMkNBQTJDO0FBT3JPLElBQU0sT0FBTyxjQUFjLElBQUksSUFBSSxnQkFBZ0Isd0NBQWUsQ0FBQztBQUNuRSxJQUFNLE9BQU8sYUFBYSxNQUFNLE1BQU07QUFDdEMsSUFBTSxVQUFVLEtBQUssTUFBTSxJQUFJO0FBRy9CLElBQU0sU0FBUztBQUFBLEVBQ2QsUUFBUTtBQUFBLElBQ1AsT0FBTztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ04sWUFBWTtBQUFBLFFBQ1gsUUFBUSxRQUFRLElBQUksVUFBVTtBQUFBLFFBQzlCLGNBQWM7QUFBQSxRQUNkLHFCQUFxQjtBQUFBLE1BQ3RCO0FBQUEsTUFDQSxXQUFXO0FBQUEsUUFDVixRQUFRLFFBQVEsSUFBSSxjQUFjO0FBQUEsUUFDbEMsY0FBYztBQUFBLFFBQ2QsSUFBSTtBQUFBLE1BQ0w7QUFBQSxNQUNBLGNBQWM7QUFBQSxRQUNiLFFBQVEsUUFBUSxJQUFJLGFBQWE7QUFBQSxRQUNqQyxjQUFjO0FBQUEsUUFDZCxJQUFJO0FBQUEsTUFDTDtBQUFBLElBQ0Q7QUFBQSxFQUNEO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUixNQUFNO0FBQUEsRUFDUDtBQUFBLEVBQ0EsU0FBUyxDQUFDLFVBQVUsR0FBRyxpQkFBaUIsRUFBRSxzQkFBc0IsTUFBTSxDQUFDLENBQUM7QUFBQSxFQUN4RSxRQUFRO0FBQUEsSUFDUCxTQUFTO0FBQUEsRUFDVjtBQUFBLEVBQ0EsY0FBYztBQUFBLElBQ2IsU0FBUyxDQUFDLGdCQUFnQix5QkFBeUIsWUFBWTtBQUFBLElBQy9ELFNBQVM7QUFBQSxNQUNSO0FBQUEsTUFDQTtBQUFBLE1BQ0E7QUFBQSxJQUNEO0FBQUEsSUFDQSxnQkFBZ0I7QUFBQSxNQUNmLFNBQVMsQ0FBQyxtQkFBbUI7QUFBQSxJQUM5QjtBQUFBLEVBQ0Q7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNSLE9BQU87QUFBQSxNQUNOLE1BQU07QUFBQSxNQUNOLHVEQUNDO0FBQUEsSUFDRjtBQUFBLElBQ0EsUUFBUSxDQUFDLFVBQVUsZUFBZTtBQUFBLEVBQ25DO0FBQUEsRUFDQSxlQUFlLENBQUMsV0FBVztBQUM1QjtBQUVBLElBQU8sc0JBQVE7IiwKICAibmFtZXMiOiBbXQp9Cg==