WIP: unstable type handler

This commit is contained in:
Clement Zhang
2024-11-05 18:39:15 +01:00
parent 8e605a7f0f
commit a1038c32b7
8 changed files with 208 additions and 52 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 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>('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)
@@ -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: {
@@ -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<any[]> & { loading?: boolean }
export let componentInput: StaticInput<any[]> & { 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 @@
<div class="flex gap-2 flex-col mt-2 w-full">
{#if Array.isArray(items) && componentInput.value}
<!-- {#if componentInput.isLabeled}<Toggle
size="xs"
options={{ right: 'LabeledOption' }}
bind:checked={isLabeled}
/>
{/if} -->
<div class="flex flex-row items-center justify-between">
<div class="text-xs text-tertiary font-semibold">{pluralize(items.length, 'item')}</div>
@@ -255,6 +276,16 @@
bind:checked={raw}
/>
{/if}
{#if hasLabeledMode}
<Toggle
options={{
right: 'Labeled'
}}
size="xs"
bind:checked={labeled}
on:change={handleLabeledChange}
/>
{/if}
</div>
<section
use:dragHandleZone={{
@@ -273,7 +304,7 @@
<div class="grow min-w-0">
<SubTypeEditor
{id}
subFieldType={raw ? 'object' : subFieldType}
subFieldType={raw ? 'object' : !labeled ? 'simplestringselect' : subFieldType}
bind:componentInput
bind:value={item.value}
on:remove={() => deleteElementByType(index)}
@@ -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>('AppViewerContext')
@@ -171,6 +172,7 @@
{selectOptions}
{format}
{placeholder}
{hasLabeledMode}
bind:componentInput
/>
</div>
@@ -81,6 +81,7 @@
{recomputeOnInputChanged}
{showOnDemandOnlyToggle}
{securedContext}
hasLabeledMode={meta?.['hasLabeledMode']}
/>
{#if deletable}
<div class="flex flex-row-reverse -mt-4">
@@ -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>('AppViewerContext')
@@ -133,7 +134,7 @@
{/if}
{:else if fieldType === 'color'}
<ColorInput bind:value={componentInput.value} />
{: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')}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
@@ -161,16 +162,14 @@
/>
</div>
{/if}
{:else if fieldType === 'selectvalue'}
<div class="flex w-full flex-col">
<JsonEditor
small
bind:value={componentInput.value}
code={JSON.stringify(componentInput.value, null, 2)}
/>
</div>
{:else if fieldType === 'array'}
<ArrayStaticInputEditor {id} {subFieldType} bind:componentInput on:deleteArrayItem />
<ArrayStaticInputEditor
{id}
{subFieldType}
bind:componentInput
on:deleteArrayItem
{hasLabeledMode}
/>
{:else if fieldType === 'schema'}
<div class="w-full">
<EditableSchemaDrawer bind:schema={componentInput.value} />
@@ -20,7 +20,7 @@ export type InputType =
| 'any'
| 'labeledresource'
| 'labeledselect'
| 'selectvalue'
| 'simplestringselect'
| 'tab-select'
| 'schema'
| 'ag-grid'
@@ -176,6 +176,7 @@ type InputConfiguration<T extends InputType, V extends InputType> = {
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'>
@@ -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==