mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
feat(frontend): Generated UI editor + Schema Form complete refactor (#3835)
* feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): add type editor * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * feat(frontend): dnd * feat(frontend): add missing error * feat(frontend): fix build * feat(frontend): fix build * feat(frontend): clean up * feat(frontend): fix layout * feat(frontend): fix dark mode * feat(frontend): Fix Dnd + resource s3 and objects * feat(frontend): handle inner objets * feat(frontend): wip * feat(frontend): wip * feat(frontend): wip * fix(frontend): UI nits * fix(frontend): fix type selection * fix(frontend): Correctly handle subproperties * fix(frontend): fix autosize + height * fix(frontend): replace everywhere * fix(frontend): fix dnd * fix(frontend): fix list refresh + height issues * fix(frontend): fix order * fix(frontend): restore default behavior * fix(frontend): Move the rename input * fix(frontend): Fix rename wrt order array * fix(frontend): Fix hub * fix(frontend): Fix renam * fix(frontend): Fix UI Nits + fix SchemaForm order * fix(frontend): Handle nested order * fix(frontend): Fix saving properties * fix(frontend): Improve approval form * feat(frontend): done * feat(frontend): merge main * feat(frontend): done * feat(frontend): wip * feat(frontend): SchemaFormDND * feat(frontend): SchemaFormDND wip * feat(frontend): improve displayWebhookWarning * feat(frontend): DND done * feat(frontend): DND done * feat(frontend): improve name * feat(frontend): improve dnd * feat(frontend): done * first batch * new try * nits * all * drag handle in more places * push everything * migrate all * all * fix * fix * fix henri bug --------- Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use axum::{routing::get, Json, Router};
|
||||
use hmac::Mac;
|
||||
use hyper::HeaderMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
use oauth2::{Client as OClient, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
@@ -49,8 +50,8 @@ pub enum InstanceEvent {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientWithScopes {
|
||||
_client: OClient,
|
||||
scopes: Vec<String>,
|
||||
extra_params: Option<HashMap<String, String>>,
|
||||
_scopes: Vec<String>,
|
||||
_extra_params: Option<HashMap<String, String>>,
|
||||
_extra_params_callback: Option<HashMap<String, String>>,
|
||||
_allowed_domains: Option<Vec<String>>,
|
||||
_userinfo_url: Option<String>,
|
||||
@@ -120,25 +121,12 @@ async fn list_logins() -> error::JsonResult<Logins> {
|
||||
return Ok(Json(Logins { oauth: vec![], saml: None }));
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ScopesAndParams {
|
||||
scopes: Vec<String>,
|
||||
extra_params: Option<HashMap<String, String>>,
|
||||
}
|
||||
async fn list_connects() -> error::JsonResult<HashMap<String, ScopesAndParams>> {
|
||||
async fn list_connects() -> error::JsonResult<Vec<String>> {
|
||||
Ok(Json(
|
||||
(&OAUTH_CLIENTS.read().await.connects)
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_owned(),
|
||||
ScopesAndParams {
|
||||
scopes: v.scopes.clone(),
|
||||
extra_params: v.extra_params.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<String, ScopesAndParams>>(),
|
||||
.keys()
|
||||
.map(|x| x.to_owned())
|
||||
.collect_vec(),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Generated
+12
-9
@@ -15,6 +15,7 @@
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
"@windmill-labs/svelte-dnd-action": "latest",
|
||||
"ag-charts-community": "^9.0.1",
|
||||
"ag-charts-enterprise": "^9.0.1",
|
||||
"ag-grid-community": "^31.0.0",
|
||||
@@ -42,7 +43,6 @@
|
||||
"quill": "^1.3.7",
|
||||
"svelte-carousel": "^1.0.25",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-dnd-action": "^0.9.38",
|
||||
"svelte-exmarkdown": "^3.0.3",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-portal": "^2.2.1",
|
||||
@@ -117,6 +117,9 @@
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"../svelte-dnd-action": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
|
||||
@@ -2197,6 +2200,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||
},
|
||||
"node_modules/@windmill-labs/svelte-dnd-action": {
|
||||
"version": "0.9.48",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-dnd-action/-/svelte-dnd-action-0.9.48.tgz",
|
||||
"integrity": "sha512-A6pWayH3nOi79DZohTscGj5t2PuNtHlKM5WIAj9WlVDx5pS1A+MsyfuNZi8oCpj2590wFmQl+58G4y4xZqDa1Q==",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3.23.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zerodevx/svelte-toast": {
|
||||
"version": "0.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.9.5.tgz",
|
||||
@@ -9244,14 +9255,6 @@
|
||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.38",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.38.tgz",
|
||||
"integrity": "sha512-L80YR+aknn54LmjPKFELYGDkZhed5ReBzH2xe61zFNGfQyoeXbFB1hs69QWgBIYufZPWUhAiVXIMhP7UQWs1dw==",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.33.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"quill": "^1.3.7",
|
||||
"svelte-carousel": "^1.0.25",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-dnd-action": "^0.9.38",
|
||||
"@windmill-labs/svelte-dnd-action": "latest",
|
||||
"svelte-exmarkdown": "^3.0.3",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-portal": "^2.2.1",
|
||||
@@ -245,9 +245,9 @@
|
||||
"default": "./package/components/SchemaForm.svelte"
|
||||
},
|
||||
"./components/SchemaEditor.svelte": {
|
||||
"types": "./package/components/SchemaEditor.svelte.d.ts",
|
||||
"svelte": "./package/components/SchemaEditor.svelte",
|
||||
"default": "./package/components/SchemaEditor.svelte"
|
||||
"types": "./package/components/EditableSchemaForm.svelte.d.ts",
|
||||
"svelte": "./package/components/EditableSchemaForm.svelte",
|
||||
"default": "./package/components/EditableSchemaForm.svelte"
|
||||
},
|
||||
"./components/scriptEditor/LogPanel.svelte": {
|
||||
"types": "./package/components/scriptEditor/LogPanel.svelte.d.ts",
|
||||
|
||||
@@ -43,7 +43,7 @@ export const action = (node) => {
|
||||
}
|
||||
|
||||
const setHeight = () => {
|
||||
node.style.height = '0px'
|
||||
node.style.height = 'auto'
|
||||
node.style.height = Math.max(node.scrollHeight ?? 0, 30) + 2 + 'px'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import { setInputCat as computeInputCat, emptyString, shouldDisplayPlaceholder } from '$lib/utils'
|
||||
import { ChevronDown, DollarSign, Pipette, Plus, X } from 'lucide-svelte'
|
||||
import { setInputCat as computeInputCat, emptyString } from '$lib/utils'
|
||||
import { DollarSign, Pipette, Plus, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import { fade } from 'svelte/transition'
|
||||
@@ -9,19 +9,14 @@
|
||||
import { Badge, Button, SecondsInput } from './common'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import type ItemPicker from './ItemPicker.svelte'
|
||||
import NumberTypeNarrowing from './NumberTypeNarrowing.svelte'
|
||||
import ObjectResourceInput from './ObjectResourceInput.svelte'
|
||||
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
|
||||
import Range from './Range.svelte'
|
||||
import ResourcePicker from './ResourcePicker.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ArgEnum from './ArgEnum.svelte'
|
||||
import ArrayTypeNarrowing from './ArrayTypeNarrowing.svelte'
|
||||
import DateTimeInput from './DateTimeInput.svelte'
|
||||
import DateInput from './DateInput.svelte'
|
||||
import S3FilePicker from './S3FilePicker.svelte'
|
||||
@@ -30,12 +25,11 @@
|
||||
import autosize from '$lib/autosize'
|
||||
import PasswordArgInput from './PasswordArgInput.svelte'
|
||||
import Password from './Password.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
|
||||
export let defaultValue: any = undefined
|
||||
|
||||
export let description: string = ''
|
||||
@@ -47,7 +41,6 @@
|
||||
export let valid = true
|
||||
export let enum_: string[] | undefined = undefined
|
||||
export let disabled = false
|
||||
export let editableSchema: { i: number; total: number } | undefined = undefined
|
||||
export let itemsType:
|
||||
| {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object'
|
||||
@@ -78,22 +71,19 @@
|
||||
export let nullable: boolean = false
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let order: string[] | undefined = undefined
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
export let orderEditable = false
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let ignoreValueUndefined = false
|
||||
|
||||
let error: string = ''
|
||||
|
||||
let s3FilePicker: S3FilePicker
|
||||
let s3FileUploadRawMode: false
|
||||
let isListJson = false
|
||||
|
||||
let el: HTMLTextAreaElement | undefined = undefined
|
||||
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
|
||||
let inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
|
||||
|
||||
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
|
||||
@@ -215,10 +205,6 @@
|
||||
let itemsLimit = 50
|
||||
|
||||
$: validateInput(pattern, value, required)
|
||||
|
||||
function changePosition(i: number, up: boolean) {
|
||||
dispatch('changePosition', { i, up })
|
||||
}
|
||||
</script>
|
||||
|
||||
<S3FilePicker
|
||||
@@ -244,114 +230,8 @@
|
||||
{format}
|
||||
{simpleTooltip}
|
||||
/>
|
||||
|
||||
{#if editableSchema}
|
||||
<span class="mx-8" />
|
||||
{#if editableSchema.i > 0}
|
||||
<button
|
||||
on:click={() => changePosition(editableSchema?.i ?? 0, true)}
|
||||
class="text-lg mr-2"
|
||||
>
|
||||
↑</button
|
||||
>
|
||||
{/if}
|
||||
{#if editableSchema.i < editableSchema.total - 1}
|
||||
<button
|
||||
on:click={() => changePosition(editableSchema?.i ?? 0, false)}
|
||||
class="text-lg mr-2">↓</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if editableSchema}
|
||||
<Label label="Description">
|
||||
<textarea
|
||||
class="mb-1"
|
||||
use:autosize
|
||||
rows="1"
|
||||
bind:value={description}
|
||||
on:keydown={onKeyDown}
|
||||
placeholder="Field description"
|
||||
/>
|
||||
</Label>
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<Label label="Custom Title" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>Will be displayed in the UI instead of the field name.</Tooltip>
|
||||
</svelte:fragment>
|
||||
<input class="mb-1" bind:value={title} on:keydown={onKeyDown} placeholder="Field title" />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if type == 'array'}
|
||||
<ArrayTypeNarrowing bind:itemsType />
|
||||
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
|
||||
<div class="p-2 my-2 mv-1 text-xs border-solid border rounded-lg">
|
||||
<div class="w-min">
|
||||
<Button
|
||||
on:click={() => {
|
||||
seeEditable = !seeEditable
|
||||
}}
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: twMerge('rotate-0 duration-300', seeEditable ? '!rotate-180' : '')
|
||||
}}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
Customize
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if seeEditable}
|
||||
<div class="mt-2">
|
||||
{#if type == 'string'}
|
||||
<StringTypeNarrowing
|
||||
bind:customErrorMessage
|
||||
bind:format
|
||||
bind:pattern
|
||||
bind:enum_
|
||||
bind:contentEncoding
|
||||
bind:password={extra['password']}
|
||||
bind:minRows={extra['minRows']}
|
||||
bind:disableCreate={extra['disableCreate']}
|
||||
bind:disableVariablePicker={extra['disableVariablePicker']}
|
||||
bind:dateFormat={extra['dateFormat']}
|
||||
/>
|
||||
{:else if type == 'number' || type == 'integer'}
|
||||
<NumberTypeNarrowing
|
||||
bind:min={extra['min']}
|
||||
bind:max={extra['max']}
|
||||
bind:currency={extra['currency']}
|
||||
bind:currencyLocale={extra['currencyLocale']}
|
||||
/>
|
||||
{:else if type == 'object'}
|
||||
<ObjectTypeNarrowing bind:format />
|
||||
{/if}
|
||||
</div>
|
||||
{#if shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern)}
|
||||
<Label label="Placeholder" class="pt-2">
|
||||
<textarea placeholder="Enter a placeholder" rows="1" bind:value={placeholder} />
|
||||
</Label>
|
||||
{/if}
|
||||
{#if !required && type === 'string'}
|
||||
<div class="mt-2 border-t pt-4">
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Nullable',
|
||||
rightTooltip:
|
||||
'If enabled, the default value will be null and not an empty string.'
|
||||
}}
|
||||
size="xs"
|
||||
bind:checked={extra.nullable}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-2xs font-semibold">Preview:</span>
|
||||
{/if}
|
||||
{#if description}
|
||||
<div class="text-xs italic pb-1 text-secondary">
|
||||
<pre class="font-main whitespace-normal">{description}</pre>
|
||||
@@ -538,7 +418,14 @@
|
||||
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
|
||||
<span class="text-2xs text-tertiary">Loading resource types...</span>
|
||||
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
|
||||
<ObjectResourceInput selectFirst {disablePortal} {format} bind:value {showSchemaExplorer} />
|
||||
<ObjectResourceInput
|
||||
{defaultValue}
|
||||
selectFirst
|
||||
{disablePortal}
|
||||
{format}
|
||||
bind:value
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
{:else if inputCat == 'resource-object' && format.split('-').length > 1 && format
|
||||
.replace('resource-', '')
|
||||
.replace('_', '')
|
||||
@@ -594,14 +481,42 @@
|
||||
</div>
|
||||
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
|
||||
{#if properties && Object.keys(properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
<SchemaForm
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
schema={{ properties, $schema: '', required: nestedRequired ?? [], type: 'object' }}
|
||||
bind:args={value}
|
||||
/>
|
||||
<div class="p-4 pl-8 border rounded-md w-full">
|
||||
{#if orderEditable}
|
||||
<SchemaFormDnd
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
schema={{
|
||||
properties,
|
||||
$schema: '',
|
||||
required: nestedRequired ?? [],
|
||||
type: 'object',
|
||||
order
|
||||
}}
|
||||
args={value}
|
||||
dndType={`nested-${title}`}
|
||||
on:reorder={(e) => {
|
||||
const keys = e.detail
|
||||
order = keys
|
||||
}}
|
||||
on:change
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
schema={{
|
||||
properties,
|
||||
order,
|
||||
$schema: '',
|
||||
required: nestedRequired ?? [],
|
||||
type: 'object'
|
||||
}}
|
||||
bind:args={value}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if disabled}
|
||||
<textarea disabled />
|
||||
@@ -682,6 +597,7 @@
|
||||
selectFirst
|
||||
{disablePortal}
|
||||
bind:value
|
||||
initialValue={defaultValue}
|
||||
resourceType={format && format.split('-').length > 1
|
||||
? format.substring('resource-'.length)
|
||||
: undefined}
|
||||
@@ -768,6 +684,7 @@
|
||||
{/if}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
{#if !compact || (error && error != '')}
|
||||
<div class="text-right text-xs text-red-600 dark:text-red-400">
|
||||
{#if disabled || error === ''}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Plus, X } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
export let itemsType:
|
||||
| {
|
||||
@@ -19,34 +20,35 @@
|
||||
: 'string'
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value={selected}
|
||||
on:change={() => {
|
||||
if (selected == 'enum') {
|
||||
itemsType = { type: 'string', enum: [] }
|
||||
} else if (selected == 'string') {
|
||||
itemsType = { type: 'string' }
|
||||
} else if (selected == 'number') {
|
||||
itemsType = { type: 'number' }
|
||||
} else if (selected == 'object') {
|
||||
itemsType = { type: 'object' }
|
||||
} else if (selected == 'bytes') {
|
||||
itemsType = { type: 'string', contentEncoding: 'base64' }
|
||||
} else {
|
||||
itemsType = undefined
|
||||
}
|
||||
}}
|
||||
id="array-type-narrowing"
|
||||
>
|
||||
<option value="string"> Items are strings</option>
|
||||
<option value="enum">Items are strings from an enum</option>
|
||||
<option value="object"> Items are objects (JSON)</option>
|
||||
<option value="number">Items are numbers</option>
|
||||
<option value="bytes">Items are bytes</option>
|
||||
</select>
|
||||
<Label label="Items type">
|
||||
<select
|
||||
bind:value={selected}
|
||||
on:change={() => {
|
||||
if (selected == 'enum') {
|
||||
itemsType = { type: 'string', enum: [] }
|
||||
} else if (selected == 'string') {
|
||||
itemsType = { type: 'string' }
|
||||
} else if (selected == 'number') {
|
||||
itemsType = { type: 'number' }
|
||||
} else if (selected == 'object') {
|
||||
itemsType = { type: 'object' }
|
||||
} else if (selected == 'bytes') {
|
||||
itemsType = { type: 'string', contentEncoding: 'base64' }
|
||||
} else {
|
||||
itemsType = undefined
|
||||
}
|
||||
}}
|
||||
id="array-type-narrowing"
|
||||
>
|
||||
<option value="string"> Items are strings</option>
|
||||
<option value="enum">Items are strings from an enum</option>
|
||||
<option value="object"> Items are objects (JSON)</option>
|
||||
<option value="number">Items are numbers</option>
|
||||
<option value="bytes">Items are bytes</option>
|
||||
</select>
|
||||
</Label>
|
||||
{#if Array.isArray(itemsType?.enum)}
|
||||
<div class="pt-1" />
|
||||
<label for="input" class="mb-2 text-secondary text-xs">
|
||||
<label for="input" class="text-secondary text-xs">
|
||||
Enums
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each itemsType?.enum || [] as e}
|
||||
|
||||
@@ -2,52 +2,49 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
import { getResourceTypes } from './resourceTypesStore'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { Pen, Plus, X } from 'lucide-svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
|
||||
import PropertyEditor from './schema/PropertyEditor.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
export let schemaFieldTooltip: Record<string, string> = {}
|
||||
export let args: Record<string, any> = {}
|
||||
|
||||
export let autofocus = false
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let compact = false
|
||||
export let noVariablePicker = false
|
||||
export let flexWrap = false
|
||||
export let noDelete = false
|
||||
export let prettifyHeader = false
|
||||
export let disablePortal = false
|
||||
export let uiOnly: boolean = false
|
||||
export let isFlowInput: boolean = false
|
||||
export let noPreview: boolean = false
|
||||
export let offset = 48 + 31 + 31 + 16 + 1
|
||||
export let jsonEnabled: boolean = true
|
||||
export let isAppInput: boolean = false
|
||||
export let lightweightMode: boolean = false
|
||||
export let displayWebhookWarning: boolean = false
|
||||
export let dndType: string | undefined = undefined
|
||||
|
||||
const moveAnimationDuration = 1000
|
||||
|
||||
function changePosition(i: number, up: boolean): any {
|
||||
const entries = Object.entries(schema.properties)
|
||||
var element = entries[i]
|
||||
entries.splice(i, 1)
|
||||
entries.splice(up ? i - 1 : i + 1, 0, element)
|
||||
schema.properties = Object.fromEntries(entries)
|
||||
syncOrders()
|
||||
}
|
||||
|
||||
function syncOrders() {
|
||||
if (schema) {
|
||||
schema.order = Object.keys(schema.properties ?? {})
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
|
||||
$: if (args == undefined || typeof args !== 'object') {
|
||||
args = {}
|
||||
}
|
||||
@@ -57,17 +54,8 @@
|
||||
|
||||
Object.keys(schema?.properties ?? {}).forEach((key) => {
|
||||
if (schema?.properties[key].default != undefined && args[key] == undefined) {
|
||||
nargs[key] = schema?.properties[key].default
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = args[key]
|
||||
let value = schema?.properties[key].default
|
||||
nargs[key] = value === 'object' ? JSON.parse(JSON.stringify(value)) : value
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
@@ -77,91 +65,416 @@
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
if (!noDelete) {
|
||||
removeExtraKey()
|
||||
let keys: string[] = Array.isArray(schema?.order)
|
||||
? [...schema.order]
|
||||
: Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {})
|
||||
|
||||
$: schema && onSchemaChange()
|
||||
|
||||
function alignOrderWithProperties(schema: {
|
||||
properties: Record<string, any>
|
||||
order: string[] | undefined
|
||||
}) {
|
||||
if (schema.order == undefined && !Array.isArray(schema.order)) {
|
||||
schema.order = []
|
||||
}
|
||||
let norder = [...schema.order]
|
||||
let properties = Object.keys(schema?.properties ?? {})
|
||||
let index = 0
|
||||
let hasChanged = false
|
||||
for (let k of properties) {
|
||||
if (schema.properties[k].type === 'object' && schema.properties[k].properties) {
|
||||
hasChanged = hasChanged || alignOrderWithProperties(schema.properties[k])
|
||||
}
|
||||
if (!norder.includes(k)) {
|
||||
console.log('adding', k, index)
|
||||
norder = [...norder.slice(0, index), k, ...norder.slice(index)]
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
norder = norder.filter((x) => properties.includes(x))
|
||||
if (!deepEqual(schema.order, norder)) {
|
||||
schema.order = norder
|
||||
console.log('b')
|
||||
return true
|
||||
}
|
||||
return hasChanged
|
||||
}
|
||||
function onSchemaChange() {
|
||||
dispatch('change', schema)
|
||||
if (alignOrderWithProperties(schema)) {
|
||||
schema = schema
|
||||
}
|
||||
let lkeys = schema?.order ?? Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && !deepEqual(lkeys, keys)) {
|
||||
keys = [...lkeys]
|
||||
schema = schema
|
||||
if (opened == undefined) {
|
||||
opened = keys[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let resourceTypes: string[] | undefined = undefined
|
||||
let renderCount: number = 0
|
||||
let opened: string | undefined = keys[0]
|
||||
let selected = ''
|
||||
|
||||
async function loadResourceTypes() {
|
||||
resourceTypes = await getResourceTypes()
|
||||
export function openField(key: string) {
|
||||
opened = key
|
||||
}
|
||||
|
||||
reorder()
|
||||
loadResourceTypes()
|
||||
|
||||
function reorder() {
|
||||
if (schema?.order && Array.isArray(schema.order)) {
|
||||
const n = {}
|
||||
|
||||
;(schema.order as string[]).forEach((x) => {
|
||||
if (schema.properties && schema.properties[x] != undefined) {
|
||||
n[x] = schema.properties[x]
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(schema.properties ?? {})
|
||||
.filter((x) => !schema.order?.includes(x))
|
||||
.forEach((x) => {
|
||||
n[x] = schema.properties[x]
|
||||
})
|
||||
schema.properties = n
|
||||
export function deleteField(key: string) {
|
||||
delete args[key]
|
||||
delete schema.properties[key]
|
||||
if (schema.required?.includes(key)) {
|
||||
schema.required = schema.required?.filter((x) => x !== key)
|
||||
}
|
||||
if (schema.order) {
|
||||
schema.order = schema.order.filter((x) => x !== key)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (opened && schema.properties[opened]) {
|
||||
selected = opened
|
||||
? schema.properties[opened].type !== 'object'
|
||||
? schema.properties[opened].type
|
||||
: schema.properties[opened].format === 'resource-s3_object'
|
||||
? 'S3'
|
||||
: 'object'
|
||||
: ''
|
||||
}
|
||||
|
||||
function renameProperty(oldName: string, key: string) {
|
||||
const el = document.getElementById(key) as HTMLInputElement
|
||||
const newName = el.value
|
||||
if (oldName === newName) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(schema.properties ?? {}).includes(newName)) {
|
||||
sendUserToast('There is already an argument with this name', true)
|
||||
|
||||
// clear the input
|
||||
el.value = oldName
|
||||
} else {
|
||||
args[newName] = args[oldName]
|
||||
delete args[oldName]
|
||||
|
||||
schema.properties[newName] = schema.properties[oldName]
|
||||
delete schema.properties[oldName]
|
||||
|
||||
if (schema.required?.includes(oldName)) {
|
||||
schema.required = schema.required?.map((x) => (x === oldName ? newName : x))
|
||||
}
|
||||
|
||||
// Replace the old name with the new name in the order array
|
||||
if (schema.order) {
|
||||
const index = schema.order.indexOf(oldName)
|
||||
if (index !== -1) {
|
||||
schema.order[index] = newName
|
||||
}
|
||||
}
|
||||
|
||||
opened = newName
|
||||
|
||||
schema = schema
|
||||
sendUserToast('Argument renamed successfully')
|
||||
}
|
||||
}
|
||||
|
||||
let jsonView: boolean = false
|
||||
let schemaString: string = JSON.stringify(schema, null, '\t')
|
||||
let error: string | undefined = undefined
|
||||
let editor: SimpleEditor | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''}">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
<div animate:flip={{ duration: moveAnimationDuration }} class={'pb-6'}>
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<ArgInput
|
||||
{disablePortal}
|
||||
{resourceTypes}
|
||||
{prettifyHeader}
|
||||
autofocus={i == 0 && autofocus}
|
||||
label={argName}
|
||||
bind:description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
editableSchema={{ i, total: keys.length }}
|
||||
on:changePosition={(event) => changePosition(event.detail.i, event.detail.up)}
|
||||
{compact}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
bind:extra={schema.properties[argName]}
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
<div style={offset ? `height: calc(100vh - ${offset}px);` : 'h-full'}>
|
||||
<Splitpanes>
|
||||
{#if !noPreview}
|
||||
<Pane size={50} minSize={20}>
|
||||
<div class="p-4">
|
||||
{#key renderCount}
|
||||
<SchemaFormDnd
|
||||
{schema}
|
||||
{dndType}
|
||||
bind:args
|
||||
on:click={(e) => {
|
||||
opened = e.detail
|
||||
}}
|
||||
on:reorder={(e) => {
|
||||
schema.order = e.detail
|
||||
}}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{lightweightMode}
|
||||
prettifyHeader={isAppInput}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Pane>
|
||||
{/if}
|
||||
<Pane size={noPreview ? 100 : 50} minSize={noPreview ? 100 : 20}>
|
||||
{#if jsonEnabled}
|
||||
<div class="w-full p-2 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON Editor',
|
||||
rightTooltip:
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !jsonView}
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} divide-y">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
|
||||
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
if (opened === argName) {
|
||||
opened = undefined
|
||||
} else {
|
||||
opened = argName
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
{argName}
|
||||
{#if !uiOnly}
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
nonCaptureEvent
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<Label label="Name">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
value={argName}
|
||||
id={argName + i}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if schema.required?.includes(argName)}
|
||||
<span class="text-red-500 text-xs"> Required </span>
|
||||
{/if}
|
||||
|
||||
{#if !uiOnly}
|
||||
<button
|
||||
class="rounded-full p-1 text-gray-500 bg-white
|
||||
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
|
||||
aria-label="Clear"
|
||||
on:click={() => {
|
||||
dispatch('delete', argName)
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if opened === argName}
|
||||
<div class="p-4 border-t">
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<PropertyEditor
|
||||
bind:description={schema.properties[argName].description}
|
||||
type={schema.properties[argName].type}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
bind:extra={schema.properties[argName]}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
{isFlowInput}
|
||||
{isAppInput}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="typeeditor">
|
||||
{#if isFlowInput || isAppInput}
|
||||
<Label label="Type">
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
|
||||
selected = e.detail
|
||||
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
enum_: undefined,
|
||||
pattern: undefined,
|
||||
default: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
currency: undefined,
|
||||
currencyLocale: undefined,
|
||||
multiselect: undefined,
|
||||
password: undefined,
|
||||
dateFormat: undefined,
|
||||
...(e.detail == 'array' ? { items: { type: 'string' } } : {}),
|
||||
showExpr: undefined,
|
||||
nullable: undefined,
|
||||
required: undefined
|
||||
}
|
||||
|
||||
if (isS3) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
} else {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
format: undefined,
|
||||
type: e.detail
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isFlowInput || isAppInput}
|
||||
<FlowPropertyEditor
|
||||
bind:defaultValue={schema.properties[argName].default}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
{lightweightMode}
|
||||
bind:nullable={schema.properties[argName].nullable}
|
||||
type={schema.properties[argName].type}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
password={schema.properties[argName].password}
|
||||
propsNames={schema.properties[argName].propsNames}
|
||||
bind:showExpr={schema.properties[argName].showExpr}
|
||||
extra={schema.properties[argName]}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
itemsType={schema.properties[argName].items}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
bind:requiredProperty={schema.properties[argName].required}
|
||||
{displayWebhookWarning}
|
||||
on:requiredChange={(event) => {
|
||||
if (event.detail.required) {
|
||||
schema.required = schema.required ?? []
|
||||
schema.required.push(argName)
|
||||
} else {
|
||||
schema.required = schema.required?.filter((x) => x !== argName)
|
||||
}
|
||||
}}
|
||||
on:schemaChange={() => {
|
||||
renderCount++
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</PropertyEditor>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-secondary text-sm p-2">No inputs</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-secondary text-sm">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-2">
|
||||
<div class="border rounded h-full">
|
||||
<SimpleEditor
|
||||
bind:this={editor}
|
||||
small
|
||||
fixedOverflowWidgets={false}
|
||||
on:change={() => {
|
||||
try {
|
||||
schema = JSON.parse(schemaString)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}}
|
||||
bind:code={schemaString}
|
||||
lang="json"
|
||||
autoHeight
|
||||
automaticLayout
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(error)}
|
||||
<div class="text-red-400 text-xs">{error}</div>
|
||||
{:else}
|
||||
<div><br /> </div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
{#if !noVariablePicker}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Label from './Label.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { selectOptions } from './apps/editor/component'
|
||||
|
||||
export let min: number | undefined
|
||||
@@ -12,51 +13,77 @@
|
||||
let maxChecked: boolean = max != undefined
|
||||
</script>
|
||||
|
||||
<div class="my-2" />
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Toggle
|
||||
bind:checked={minChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
min = 0
|
||||
} else {
|
||||
min = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'min' }}
|
||||
/>
|
||||
<input type="number" bind:value={min} disabled={!minChecked} />
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Label label="Min" class="w-full col-span-1">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light small>
|
||||
Set a minimum value for the number. If both min and max are set, the input will render as
|
||||
a range slider.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
bind:checked={minChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
min = 0
|
||||
} else {
|
||||
min = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Enabled' }}
|
||||
size="xs"
|
||||
/>
|
||||
</svelte:fragment> <input type="number" bind:value={min} disabled={!minChecked} />
|
||||
</Label>
|
||||
|
||||
<Label label="Max" class="w-full col-span-1 ">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light small>
|
||||
Set a maximum value for the number. If both min and max are set, the input will render as
|
||||
a range slider.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
bind:checked={maxChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
max = 42
|
||||
} else {
|
||||
max = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'Enabled' }}
|
||||
size="xs"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<input type="number" bind:value={max} disabled={!maxChecked} />
|
||||
</Label>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Toggle
|
||||
bind:checked={maxChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
max = 42
|
||||
} else {
|
||||
max = undefined
|
||||
}
|
||||
}}
|
||||
options={{ right: 'max' }}
|
||||
/>
|
||||
<input type="number" bind:value={max} disabled={!maxChecked} />
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<Label label="Currency" class="w-full col-span-2">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light small>
|
||||
Select a currency to display the number in. If a currency is selected, you can also select
|
||||
a locale to format the number according to that locale.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<select bind:value={currency}>
|
||||
<option value={undefined}> No currency </option>
|
||||
{#each selectOptions.currencyOptions as c}
|
||||
<option value={c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
<Label label="Currency locale" class="w-full">
|
||||
<select bind:value={currencyLocale} disabled={!currency}>
|
||||
<option value={undefined}> No locale </option>
|
||||
{#each selectOptions.localeOptions as c}
|
||||
<option value={c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
</div>
|
||||
<Label label="Currency">
|
||||
<select class="mt-1" bind:value={currency}>
|
||||
<option value={undefined}> No currency </option>
|
||||
{#each selectOptions.currencyOptions as c}
|
||||
<option value={c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
<Label label="Currency locale">
|
||||
<select class="mt-1" bind:value={currencyLocale}>
|
||||
<option value={undefined}> No locale </option>
|
||||
{#each selectOptions.localeOptions as c}
|
||||
<option value={c}>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let disablePortal = false
|
||||
export let showSchemaExplorer = false
|
||||
export let selectFirst = false
|
||||
export let defaultValue: any
|
||||
|
||||
function isString(value: any) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
@@ -47,6 +48,7 @@
|
||||
resourceToValue()
|
||||
}}
|
||||
bind:value={path}
|
||||
initialValue={defaultValue}
|
||||
resourceType={format.split('-').length > 1 ? format.substring('resource-'.length) : undefined}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
: undefined
|
||||
</script>
|
||||
|
||||
<div class="mt-2" />
|
||||
<ResourceTypePicker
|
||||
on:click={(e) => {
|
||||
resource = e.detail
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Required from './Required.svelte'
|
||||
import type { PropertyDisplayInfo } from '$lib/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SchemaEditorProperty from './SchemaEditorProperty.svelte'
|
||||
import { Button } from './common'
|
||||
import { Pen, Trash } from 'lucide-svelte'
|
||||
import { truncate } from '$lib/utils'
|
||||
|
||||
export let displayInfo: PropertyDisplayInfo
|
||||
export let isAnimated: boolean
|
||||
export let lightMode: boolean
|
||||
|
||||
let depth = displayInfo.path.length
|
||||
const required = displayInfo.isRequired
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function startEditArgument(name: string) {
|
||||
dispatch('startEditArgument', name)
|
||||
}
|
||||
|
||||
function handleDeleteArgument(argPath: string[]) {
|
||||
dispatch('deleteArgument', argPath)
|
||||
}
|
||||
|
||||
function changePosition(i: number, up: boolean) {
|
||||
dispatch('changePosition', { i, up })
|
||||
}
|
||||
|
||||
function getArgPath(displayInfo: PropertyDisplayInfo): string[] {
|
||||
return [...displayInfo.path, displayInfo.name]
|
||||
}
|
||||
|
||||
const indentStyle = depth > 0 ? `margin-left :${depth}rem` : ''
|
||||
</script>
|
||||
|
||||
<td class="font-bold">
|
||||
<span style={indentStyle}>
|
||||
{displayInfo.name}
|
||||
<Required {required} class="!ml-0" />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<SchemaEditorProperty property={displayInfo.property} />
|
||||
</td>
|
||||
{#if !lightMode}
|
||||
<td class="truncate max-w-xs !text-2xs"
|
||||
>{truncate(
|
||||
displayInfo.property.default ? JSON.stringify(displayInfo.property.default) : '',
|
||||
100
|
||||
)}</td
|
||||
>
|
||||
<td class="truncate max-w-xs !text-2xs"
|
||||
>{truncate(displayInfo.property.description ?? '', 100)}</td
|
||||
>
|
||||
{/if}
|
||||
<td />
|
||||
<td class="justify-end flex">
|
||||
{#if depth === 0}
|
||||
{#if displayInfo.index > 0}
|
||||
<button
|
||||
on:click={() => changePosition(displayInfo.index, true)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>
|
||||
↑</button
|
||||
>
|
||||
{/if}
|
||||
{#if displayInfo.index < displayInfo.propertiesNumber - 1}
|
||||
<button
|
||||
on:click={() => changePosition(displayInfo.index, false)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">↓</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: Pen }}
|
||||
on:click={() => startEditArgument(displayInfo.name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
color="red"
|
||||
variant="border"
|
||||
btnClasses="mx-2"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: Trash }}
|
||||
on:click={() => handleDeleteArgument(getArgPath(displayInfo))}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
@@ -1,305 +1,70 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { fly, fade } from 'svelte/transition'
|
||||
<script lang="ts">
|
||||
import RangeSlider from 'svelte-range-slider-pips'
|
||||
|
||||
// Props
|
||||
export let min = 0
|
||||
export let max = 100
|
||||
export let initialValue = 0
|
||||
export let id = null
|
||||
export let value = typeof initialValue === 'string' ? parseInt(initialValue) : initialValue
|
||||
export let disabled: boolean = false
|
||||
export let step: number = 1
|
||||
export let axisStep: number = 1
|
||||
|
||||
// Node Bindings
|
||||
let container = null
|
||||
let thumb = null
|
||||
let progressBar = null
|
||||
let element = null
|
||||
let slider: HTMLElement
|
||||
|
||||
// Internal State
|
||||
let elementX = null
|
||||
let currentThumb = null
|
||||
let holding = false
|
||||
let thumbHover = false
|
||||
let keydownAcceleration = 0
|
||||
let accelerationTimer = null
|
||||
|
||||
// Dispatch 'change' events
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Mouse shield used onMouseDown to prevent any mouse events penetrating other elements,
|
||||
// ie. hover events on other elements while dragging. Especially for Safari
|
||||
const mouseEventShield = document.createElement('div')
|
||||
mouseEventShield.setAttribute('class', 'mouse-over-shield')
|
||||
mouseEventShield.addEventListener('mouseover', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
function resizeWindow() {
|
||||
elementX = element.getBoundingClientRect().left
|
||||
}
|
||||
|
||||
// Allows both bind:value and on:change for parent value retrieval
|
||||
function setValue(val) {
|
||||
value = val
|
||||
dispatch('change', { value })
|
||||
}
|
||||
|
||||
function onTrackEvent(e) {
|
||||
// Update value immediately before beginning drag
|
||||
updateValueOnEvent(e)
|
||||
onDragStart(e)
|
||||
}
|
||||
|
||||
function onDragStart(e) {
|
||||
// If mouse event add a pointer events shield
|
||||
if (e.type === 'mousedown') document.body.append(mouseEventShield)
|
||||
currentThumb = thumb
|
||||
}
|
||||
|
||||
function onDragEnd(e) {
|
||||
// If using mouse - remove pointer event shield
|
||||
if (e.type === 'mouseup') {
|
||||
if (document.body.contains(mouseEventShield)) document.body.removeChild(mouseEventShield)
|
||||
// Needed to check whether thumb and mouse overlap after shield removed
|
||||
if (isMouseInElement(e, thumb)) thumbHover = true
|
||||
}
|
||||
currentThumb = null
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (document.body.contains(mouseEventShield)) document.body.removeChild(mouseEventShield)
|
||||
})
|
||||
|
||||
// Check if mouse event cords overlay with an element's area
|
||||
function isMouseInElement(event, element) {
|
||||
let rect = element.getBoundingClientRect()
|
||||
let { clientX: x, clientY: y } = event
|
||||
if (x < rect.left || x >= rect.right) return false
|
||||
if (y < rect.top || y >= rect.bottom) return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Accessible keypress handling
|
||||
function onKeyPress(e) {
|
||||
// Max out at +/- 10 to value per event (50 events / 5)
|
||||
// 100 below is to increase the amount of events required to reach max velocity
|
||||
if (keydownAcceleration < 50) keydownAcceleration++
|
||||
let throttled = Math.ceil(keydownAcceleration / 5)
|
||||
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
|
||||
if (value + throttled > max || value >= max) {
|
||||
setValue(max)
|
||||
} else {
|
||||
setValue(value + throttled)
|
||||
}
|
||||
}
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
|
||||
if (value - throttled < min || value <= min) {
|
||||
setValue(min)
|
||||
} else {
|
||||
setValue(value - throttled)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset acceleration after 100ms of no events
|
||||
clearTimeout(accelerationTimer)
|
||||
accelerationTimer = setTimeout(() => (keydownAcceleration = 1), 100)
|
||||
}
|
||||
|
||||
function calculateNewValue(clientX) {
|
||||
resizeWindow()
|
||||
// Find distance between cursor and element's left cord (20px / 2 = 10px) - Center of thumb
|
||||
let delta = clientX - (elementX + 10)
|
||||
|
||||
// Use width of the container minus (5px * 2 sides) offset for percent calc
|
||||
let percent = (delta * 100) / (container.clientWidth - 10)
|
||||
|
||||
// Limit percent 0 -> 100
|
||||
percent = percent < 0 ? 0 : percent > 100 ? 100 : percent
|
||||
|
||||
// Limit value min -> max
|
||||
setValue(Math.round((percent * (max - min)) / 100 + min))
|
||||
}
|
||||
|
||||
// Handles both dragging of touch/mouse as well as simple one-off click/touches
|
||||
function updateValueOnEvent(e) {
|
||||
// touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
|
||||
if (!currentThumb && e.type !== 'touchstart' && e.type !== 'mousedown') return false
|
||||
|
||||
if (e.stopPropagation) e.stopPropagation()
|
||||
if (e.preventDefault) e.preventDefault()
|
||||
|
||||
// Get client's x cord either touch or mouse
|
||||
const clientX =
|
||||
e.type === 'touchmove' || e.type === 'touchstart' ? e.touches[0].clientX : e.clientX
|
||||
|
||||
calculateNewValue(clientX)
|
||||
}
|
||||
|
||||
// React to left position of element relative to window
|
||||
$: if (element) elementX = element.getBoundingClientRect().left
|
||||
|
||||
// Set a class based on if dragging
|
||||
$: holding = Boolean(currentThumb)
|
||||
|
||||
// Update progressbar and thumb styles to represent value
|
||||
$: if (progressBar && thumb) {
|
||||
// Limit value min -> max
|
||||
value = value > min ? value : min
|
||||
value = value < max ? value : max
|
||||
|
||||
let percent = ((value - min) * 100) / (max - min)
|
||||
let offsetLeft = (container.clientWidth - 10) * (percent / 100) + 5
|
||||
|
||||
// Update thumb position + active range track width
|
||||
thumb.style.left = `${offsetLeft}px`
|
||||
progressBar.style.width = `${offsetLeft}px`
|
||||
const format = (v, i, p) => {
|
||||
return `${v}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:touchmove|nonpassive={updateValueOnEvent}
|
||||
on:touchcancel={onDragEnd}
|
||||
on:touchend={onDragEnd}
|
||||
on:mousemove={updateValueOnEvent}
|
||||
on:mouseup={onDragEnd}
|
||||
on:resize={resizeWindow}
|
||||
/>
|
||||
<div class="range">
|
||||
<div
|
||||
class="range__wrapper"
|
||||
tabindex="0"
|
||||
on:keydown={onKeyPress}
|
||||
bind:this={element}
|
||||
role="slider"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
{id}
|
||||
on:pointerdown|stopPropagation
|
||||
on:mousedown|stopPropagation={onTrackEvent}
|
||||
on:touchstart|stopPropagation={onTrackEvent}
|
||||
>
|
||||
<div class="range__track" bind:this={container}>
|
||||
<div class="range__track--highlighted" bind:this={progressBar} />
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
class="range__thumb"
|
||||
class:range__thumb--holding={holding}
|
||||
bind:this={thumb}
|
||||
on:touchstart={onDragStart}
|
||||
on:mousedown={onDragStart}
|
||||
on:mouseover={() => (thumbHover = true)}
|
||||
on:mouseout={() => (thumbHover = false)}
|
||||
>
|
||||
{#if holding || thumbHover}
|
||||
<div class="range__tooltip" in:fly={{ y: 7, duration: 200 }} out:fade={{ duration: 100 }}>
|
||||
{value}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={'grow'}
|
||||
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; "
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<RangeSlider
|
||||
id="range-slider"
|
||||
springValues={{ stiffness: 1, damping: 1 }}
|
||||
bind:slider
|
||||
min={min == undefined ? 0 : +min}
|
||||
max={max == undefined ? 1 : +max}
|
||||
on:change={(e) => {
|
||||
value = e.detail.value
|
||||
}}
|
||||
{disabled}
|
||||
values={[value]}
|
||||
pips
|
||||
float
|
||||
first="label"
|
||||
last="label"
|
||||
step={step ?? 1}
|
||||
pipstep={(axisStep ?? 1) / (step ?? 1)}
|
||||
formatter={format}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
.mouse-over-shield {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 0, 0, 0);
|
||||
z-index: 10000;
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
.range {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
:global(#range-slider.rangeSlider) {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.range__wrapper {
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
:global(.dark #range-slider.rangeSlider) {
|
||||
background-color: #3b4252;
|
||||
}
|
||||
|
||||
.range__wrapper:focus-visible > .range__track {
|
||||
box-shadow: 0 0 0 2px white, 0 0 0 3px var(--track-focus, #6185ff);
|
||||
:global(#range-slider.rangeSlider .rangeHandle) {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.range__track {
|
||||
height: 6px;
|
||||
background-color: var(--track-bgcolor, #d0d0d0);
|
||||
border-radius: 999px;
|
||||
:global(#range-slider.rangeSlider .rangeFloat) {
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.range__track--highlighted {
|
||||
background-color: var(--track-highlight-bgcolor, #6185ff);
|
||||
background: var(--track-highlight-bg, linear-gradient(90deg, #6185ff, #9c65ff));
|
||||
width: 0;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.range__thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--thumb-bgcolor, white);
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
margin-top: -8px;
|
||||
transition: box-shadow 100ms;
|
||||
user-select: none;
|
||||
box-shadow: var(
|
||||
--thumb-boxshadow,
|
||||
0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 0px 2px 1px rgba(0, 0, 0, 0.2)
|
||||
);
|
||||
}
|
||||
|
||||
.range__thumb--holding {
|
||||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 2px 1px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 6px var(--thumb-holding-outline, rgba(113, 119, 250, 0.3));
|
||||
}
|
||||
|
||||
.range__tooltip {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -33px;
|
||||
color: var(--tooltip-text, white);
|
||||
width: 38px;
|
||||
padding: 4px 0;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
background-color: var(--tooltip-bgcolor, #6185ff);
|
||||
background: var(--tooltip-bg, linear-gradient(45deg, #6185ff, #9c65ff));
|
||||
}
|
||||
|
||||
.range__tooltip::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 7px;
|
||||
width: 7px;
|
||||
background-color: var(--tooltip-bgcolor, #6185ff);
|
||||
bottom: -3px;
|
||||
left: calc(50% - 3px);
|
||||
clip-path: polygon(0% 0%, 100% 100%, 0% 100%);
|
||||
transform: rotate(-45deg);
|
||||
border-radius: 0 0 0 3px;
|
||||
:global(.dark #range-slider.rangeSlider > .rangePips > .pip) {
|
||||
color: #eeeeee;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
}
|
||||
: undefined
|
||||
|
||||
$: if (value === undefined && initialValue) {
|
||||
value = initialValue
|
||||
}
|
||||
|
||||
let collection = valueSelect ? [valueSelect] : []
|
||||
|
||||
async function loadResources(resourceType: string | undefined) {
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import { Button, ClearableInput } from './common'
|
||||
|
||||
let resources: string[] = []
|
||||
import { Button, ClearableInput, Popup } from './common'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import Label from './Label.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
|
||||
export let value: string | undefined
|
||||
|
||||
export let notPickable = false
|
||||
export let nonePickable = false
|
||||
|
||||
let resources: string[] = []
|
||||
|
||||
async function loadResources() {
|
||||
resources = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
|
||||
}
|
||||
@@ -32,38 +35,86 @@
|
||||
$: filteredResources = resources.filter((r) => r.toLowerCase().includes(search.toLowerCase()))
|
||||
</script>
|
||||
|
||||
<div class="mb-2">
|
||||
<ClearableInput bind:value={search} placeholder="Search resource..." />
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
|
||||
{#if nonePickable && search === ''}
|
||||
{@const isPicked = value === undefined}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => onClick(undefined)}
|
||||
>
|
||||
None
|
||||
</Button>
|
||||
{/if}
|
||||
{#each filteredResources as r}
|
||||
{@const isPicked = value === r}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => onClick(r)}
|
||||
>
|
||||
<IconedResourceType name={r} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/each}
|
||||
<Label label="Resource type" class="w-full col-span-2">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light small>Select a resource type to narrow down the object type.</Tooltip>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if filteredResources.length === 0 && search !== ''}
|
||||
<div class="text-tertiary text-sm">No resources found</div>
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:fragment slot="action">
|
||||
<div class="flex flex-row gap-1">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={() => onClick(undefined)}
|
||||
disabled={notPickable || value === undefined}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Popup
|
||||
floatingConfig={{
|
||||
strategy: 'fixed',
|
||||
placement: 'left-end',
|
||||
middleware: [offset(8), flip(), shift()]
|
||||
}}
|
||||
containerClasses="border rounded-lg shadow-lg bg-surface p-4 w-[500px] h-[500px] "
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button nonCaptureEvent size="xs" color="dark">Select resource type</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<ClearableInput bind:value={search} placeholder="Search resource..." />
|
||||
|
||||
<div class="overflow-y-scroll h-full">
|
||||
<div
|
||||
class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center overflow-x-hidden"
|
||||
>
|
||||
{#if nonePickable && search === ''}
|
||||
{@const isPicked = value === undefined}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(undefined)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
None
|
||||
</Button>
|
||||
{/if}
|
||||
{#each filteredResources as r}
|
||||
{@const isPicked = value === r}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(r)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={r} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
{#if filteredResources.length === 0 && search !== ''}
|
||||
<div class="text-tertiary text-sm">No resources found</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-row items-center w-full justify-between">
|
||||
<Badge color={!value ? 'gray' : 'blue'}>
|
||||
{value ?? 'None'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type Schema,
|
||||
type SchemaProperty,
|
||||
type PropertyDisplayInfo,
|
||||
modalToSchema,
|
||||
type ModalSchemaProperty
|
||||
} from '$lib/common'
|
||||
import { emptySchema, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { Button } from './common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SchemaModal, { DEFAULT_PROPERTY, schemaToModal } from './SchemaModal.svelte'
|
||||
import PropertyRow from './PropertyRow.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import Portal from 'svelte-portal'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
export let isFlowInput = false
|
||||
export let variableEditor: VariableEditor | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let lightMode: boolean = false
|
||||
|
||||
const moveAnimationDuration = 300
|
||||
|
||||
export let schema: Schema | any = emptySchema()
|
||||
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
}
|
||||
|
||||
let schemaModal: SchemaModal
|
||||
let schemaString: string = ''
|
||||
|
||||
// Internal state: bound to args builder modal
|
||||
let argError = ''
|
||||
let editing = false
|
||||
let oldArgName: string | undefined // when editing argument and changing name
|
||||
|
||||
let viewJsonSchema = false
|
||||
let jsonEditor: SimpleEditor
|
||||
|
||||
// Binding is not enough because monaco Editor does not support two-way binding
|
||||
export function getSchema(): Schema {
|
||||
try {
|
||||
if (viewJsonSchema) {
|
||||
schema = JSON.parse(schemaString)
|
||||
return schema
|
||||
} else {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
return schema
|
||||
}
|
||||
} catch (err) {
|
||||
throw Error(`Error: input is not a valid schema: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
reorder()
|
||||
|
||||
function reorder() {
|
||||
if (schema.order && Array.isArray(schema.order)) {
|
||||
const n = {}
|
||||
|
||||
;(schema.order as string[]).forEach((x) => {
|
||||
if (schema.properties && schema.properties[x] != undefined) {
|
||||
n[x] = schema.properties[x]
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(schema.properties ?? {})
|
||||
.filter((x) => !schema.order?.includes(x))
|
||||
.forEach((x) => {
|
||||
n[x] = schema.properties[x]
|
||||
})
|
||||
schema.properties = n
|
||||
}
|
||||
}
|
||||
|
||||
function syncOrders() {
|
||||
if (schema) {
|
||||
schema.order = Object.keys(schema.properties ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
modalProperty.name = modalProperty.name.trim()
|
||||
|
||||
if (modalProperty.name.length === 0) {
|
||||
argError = 'Arguments need to have a name'
|
||||
} else if (
|
||||
Object.keys(schema.properties ?? {}).includes(modalProperty.name) &&
|
||||
(!editing || (editing && oldArgName && oldArgName !== modalProperty.name))
|
||||
) {
|
||||
argError = 'There is already an argument with this name'
|
||||
} else {
|
||||
if (!schema.properties) {
|
||||
schema.properties = {}
|
||||
}
|
||||
if (!schema.required) {
|
||||
schema.required = []
|
||||
}
|
||||
if (!schema.order || !Array.isArray(schema.order)) {
|
||||
syncOrders()
|
||||
}
|
||||
schema.properties[modalProperty.name] = modalToSchema(modalProperty)
|
||||
if (modalProperty.required) {
|
||||
if (!schema.required.includes(modalProperty.name)) {
|
||||
schema.required.push(modalProperty.name)
|
||||
}
|
||||
} else if (schema.required.includes(modalProperty.name)) {
|
||||
const index = schema.required.indexOf(modalProperty.name, 0)
|
||||
if (index > -1) {
|
||||
schema.required.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (editing && oldArgName && oldArgName !== modalProperty.name) {
|
||||
let oldPosition = schema.order.indexOf(oldArgName)
|
||||
schema.order[oldPosition] = modalProperty.name
|
||||
reorder()
|
||||
handleDeleteArgument([oldArgName])
|
||||
}
|
||||
|
||||
if (!schema.order?.includes(modalProperty.name)) {
|
||||
schema.order.push(modalProperty.name)
|
||||
}
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
editing = false
|
||||
oldArgName = undefined
|
||||
|
||||
schemaModal.closeDrawer()
|
||||
}
|
||||
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
jsonEditor?.setCode(schemaString)
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
function handleStartEditEvent(event: CustomEvent): void {
|
||||
startEditArgument(event.detail)
|
||||
}
|
||||
|
||||
function startEditArgument(argName: string): void {
|
||||
argError = ''
|
||||
if (Object.keys(schema.properties).includes(argName)) {
|
||||
editing = true
|
||||
const modalProperty = schemaToModal(
|
||||
schema.properties[argName],
|
||||
argName,
|
||||
schema.required.includes(argName)
|
||||
)
|
||||
oldArgName = argName
|
||||
schemaModal.openDrawer(modalProperty)
|
||||
} else {
|
||||
sendUserToast(`This argument does not exist and can't be edited`, true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteEvent(event: CustomEvent): void {
|
||||
handleDeleteArgument(event.detail)
|
||||
}
|
||||
|
||||
function handleDeleteArgument(argPath: string[]): void {
|
||||
try {
|
||||
let modifiedObject: Schema | SchemaProperty = schema
|
||||
let modifiedProperties = modifiedObject.properties as object
|
||||
let argName = argPath.pop() as string
|
||||
|
||||
argPath.forEach((property) => {
|
||||
if (Object.keys(modifiedProperties).includes(property)) {
|
||||
modifiedObject = modifiedProperties[property]
|
||||
modifiedProperties = modifiedObject.properties as object
|
||||
} else {
|
||||
throw Error('Nested argument not found!')
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
}
|
||||
syncOrders()
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete argument: ${err}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(): void {
|
||||
if (viewJsonSchema) {
|
||||
if (schemaString === '') {
|
||||
schemaString = JSON.stringify(emptySchema(), null, 4)
|
||||
}
|
||||
viewJsonSchema = false
|
||||
} else {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
viewJsonSchema = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePositionEvent(event: CustomEvent): void {
|
||||
changePosition(event.detail.i, event.detail.up)
|
||||
}
|
||||
|
||||
function changePosition(i: number, up: boolean): any {
|
||||
isAnimated = true
|
||||
setTimeout(() => {
|
||||
isAnimated = false
|
||||
}, moveAnimationDuration)
|
||||
const entries = Object.entries(schema.properties)
|
||||
var element = entries[i]
|
||||
entries.splice(i, 1)
|
||||
entries.splice(up ? i - 1 : i + 1, 0, element)
|
||||
schema.properties = Object.fromEntries(entries)
|
||||
|
||||
syncOrders()
|
||||
}
|
||||
|
||||
let isAnimated = false
|
||||
let error = ''
|
||||
|
||||
function schemaPropertiesToDisplay(schema: Schema): PropertyDisplayInfo[] {
|
||||
return propertiesToDisplay(schema.properties, schema.required, [])
|
||||
}
|
||||
|
||||
function propertiesToDisplay(
|
||||
properties: { [name: string]: SchemaProperty },
|
||||
required: string[],
|
||||
path: string[]
|
||||
): PropertyDisplayInfo[] {
|
||||
return Object.entries(properties)
|
||||
.map(([name, property], index) => {
|
||||
const isRequired = required.includes(name)
|
||||
const displayInfo = {
|
||||
property: property,
|
||||
name: name,
|
||||
isRequired: isRequired,
|
||||
path: path,
|
||||
index: index,
|
||||
propertiesNumber: Object.entries(properties).length
|
||||
}
|
||||
if (property?.type === 'object') {
|
||||
const newPath = [...path, name]
|
||||
return [
|
||||
displayInfo,
|
||||
...propertiesToDisplay(property.properties || {}, property.required || [], newPath)
|
||||
]
|
||||
} else {
|
||||
return [displayInfo]
|
||||
}
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
/* Small hash function to generate a unique key for each property */
|
||||
function displayInfoKey(displayInfo: PropertyDisplayInfo): string {
|
||||
const pathLengthString = displayInfo.path.length.toString()
|
||||
return pathLengthString + [...displayInfo.path, displayInfo.name].join(pathLengthString)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center gap-x-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size={lightMode ? 'xs2' : 'sm'}
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => {
|
||||
schemaModal.openDrawer(Object.assign({}, DEFAULT_PROPERTY))
|
||||
}}
|
||||
id="flow-editor-add-property"
|
||||
>
|
||||
Add Argument
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<Toggle
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
on:change={() => switchTab()}
|
||||
options={{
|
||||
right: 'As JSON'
|
||||
}}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing#script-parameters-to-json-schema"
|
||||
>
|
||||
Arguments can be edited either using the wizard, or by editing their JSON Schema.
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--json schema or table view-->
|
||||
<div class="h-full">
|
||||
{#if !viewJsonSchema}
|
||||
<div class="h-full overflow-auto">
|
||||
{#if schema.properties && Object.keys(schema.properties).length > 0 && schema.required}
|
||||
<TableCustom wFull>
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
{#if !lightMode}
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
{/if}
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#key schema.required}
|
||||
{#each schemaPropertiesToDisplay(schema) as displayInfo (displayInfoKey(displayInfo))}
|
||||
<tr class="w-full" animate:flip={{ duration: moveAnimationDuration }}>
|
||||
<PropertyRow
|
||||
{displayInfo}
|
||||
{isAnimated}
|
||||
{lightMode}
|
||||
on:startEditArgument={handleStartEditEvent}
|
||||
on:deleteArgument={handleDeleteEvent}
|
||||
on:changePosition={handleChangePositionEvent}
|
||||
/>
|
||||
</tr>
|
||||
{/each}
|
||||
{/key}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{:else}
|
||||
<div class="text-secondary text-xs italic mt-2">This schema has no arguments.</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded p-2">
|
||||
<SimpleEditor
|
||||
small
|
||||
bind:this={jsonEditor}
|
||||
fixedOverflowWidgets={false}
|
||||
on:change={() => {
|
||||
try {
|
||||
schema = JSON.parse(schemaString)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}}
|
||||
bind:code={schemaString}
|
||||
lang="json"
|
||||
class="small-editor"
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(error)}
|
||||
<div class="text-red-400">{error}</div>
|
||||
{:else}
|
||||
<div><br /></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Portal>
|
||||
<SchemaModal
|
||||
{isFlowInput}
|
||||
bind:this={schemaModal}
|
||||
bind:error={argError}
|
||||
on:save={(e) => handleAddOrEditArgument(e.detail)}
|
||||
bind:editing
|
||||
bind:oldArgName
|
||||
propsNames={Object.keys(schema.properties ?? {})}
|
||||
{variableEditor}
|
||||
/>
|
||||
</Portal>
|
||||
@@ -3,7 +3,6 @@
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue, computeShow } from '$lib/utils'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
@@ -11,7 +10,12 @@
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { getResourceTypes } from './resourceTypesStore'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import LightweightArgInput from './LightweightArgInput.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { dragHandleZone, type Options as DndOptions } from '@windmill-labs/svelte-dnd-action'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -35,9 +39,11 @@
|
||||
export let showSchemaExplorer = false
|
||||
export let showReset = false
|
||||
export let onlyMaskPassword = false
|
||||
export let lightweightMode: boolean = false
|
||||
export let dndConfig: DndOptions | undefined = undefined
|
||||
export let items: { id: string; value: string }[] | undefined = undefined
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
|
||||
@@ -50,7 +56,8 @@
|
||||
|
||||
Object.keys(schema?.properties ?? {}).forEach((key) => {
|
||||
if (schema?.properties[key].default != undefined && args[key] == undefined) {
|
||||
nargs[key] = schema?.properties[key].default
|
||||
let value = schema?.properties[key].default
|
||||
nargs[key] = value === 'object' ? JSON.parse(JSON.stringify(value)) : value
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
@@ -89,10 +96,11 @@
|
||||
function hasExtraKeys() {
|
||||
return Object.keys(args ?? {}).some((x) => !keys.includes(x))
|
||||
}
|
||||
|
||||
function reorder() {
|
||||
dispatch('change')
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (!deepEqual(schema?.order, lkeys) || !deepEqual(keys, lkeys)) {
|
||||
console.debug('reorder')
|
||||
if (schema?.order && Array.isArray(schema.order)) {
|
||||
const n = {}
|
||||
|
||||
@@ -116,96 +124,173 @@
|
||||
removeExtraKey()
|
||||
}
|
||||
}
|
||||
|
||||
$: fields = items ?? keys.map((x) => ({ id: x, value: x }))
|
||||
</script>
|
||||
|
||||
{#if showReset}
|
||||
<div class="flex flex-row-reverse w-full">
|
||||
<Button size="xs" color="light" on:click={() => setDefaults()}
|
||||
>Reset args to runnable's defaults</Button
|
||||
>
|
||||
<Button size="xs" color="light" on:click={() => setDefaults()}>
|
||||
Reset args to runnable's defaults
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''}">
|
||||
|
||||
<div
|
||||
class="w-full {$$props.class} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''}"
|
||||
use:dragHandleZone={dndConfig ?? { items: [], dragDisabled: true }}
|
||||
on:finalize
|
||||
on:consider
|
||||
>
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
<div>
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
{#if computeShow(argName, schema?.properties[argName].showExpr, args)}
|
||||
<ArgInput
|
||||
{disablePortal}
|
||||
{resourceTypes}
|
||||
{prettifyHeader}
|
||||
autofocus={i == 0 && autofocus ? true : null}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName].required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{compact}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
password={linkedSecret == argName}
|
||||
extra={schema.properties[argName]}
|
||||
{showSchemaExplorer}
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
{onlyMaskPassword}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={linkedSecret == argName}
|
||||
on:selected={(e) => {
|
||||
if (e.detail) {
|
||||
linkedSecret = argName
|
||||
} else if (linkedSecret == argName) {
|
||||
linkedSecret = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value={false}
|
||||
size="sm"
|
||||
label="Inlined"
|
||||
tooltip="The value is inlined in the resource and thus has no special treatment."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value={true}
|
||||
size="sm"
|
||||
label="Secret"
|
||||
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>{/if}</svelte:fragment
|
||||
>
|
||||
</ArgInput>
|
||||
{#each fields as item, i (item.id)}
|
||||
{@const argName = item.value}
|
||||
<div animate:flip={{ duration: 200 }}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex flex-row items-center bg-surface"
|
||||
on:click={() => {
|
||||
dispatch('click', argName)
|
||||
}}
|
||||
>
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
{#if computeShow(argName, schema?.properties[argName].showExpr, args)}
|
||||
{#if lightweightMode}
|
||||
<LightweightArgInput
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema?.required?.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
nestedRequired={schema.properties[argName]?.required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
extra={schema.properties[argName]}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<slot name="actions" />
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={linkedSecret == argName}
|
||||
on:selected={(e) => {
|
||||
if (e.detail) {
|
||||
linkedSecret = argName
|
||||
} else if (linkedSecret == argName) {
|
||||
linkedSecret = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value={false}
|
||||
size="sm"
|
||||
label="Inlined"
|
||||
tooltip="The value is inlined in the resource and thus has no special treatment."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value={true}
|
||||
size="sm"
|
||||
label="Secret"
|
||||
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>{/if}
|
||||
</svelte:fragment>
|
||||
</LightweightArgInput>
|
||||
{:else}
|
||||
<ArgInput
|
||||
on:change={() => dispatch('change')}
|
||||
{disablePortal}
|
||||
{resourceTypes}
|
||||
{prettifyHeader}
|
||||
autofocus={i == 0 && autofocus ? true : null}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema?.required?.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
nestedRequired={schema.properties[argName]?.required}
|
||||
itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{compact}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
password={linkedSecret == argName}
|
||||
extra={schema.properties[argName]}
|
||||
{showSchemaExplorer}
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
{onlyMaskPassword}
|
||||
nullable={schema.properties[argName].nullable}
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
orderEditable={dndConfig != undefined}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<slot name="actions" />
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={linkedSecret == argName}
|
||||
on:selected={(e) => {
|
||||
if (e.detail) {
|
||||
linkedSecret = argName
|
||||
} else if (linkedSecret == argName) {
|
||||
linkedSecret = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value={false}
|
||||
size="sm"
|
||||
label="Inlined"
|
||||
tooltip="The value is inlined in the resource and thus has no special treatment."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value={true}
|
||||
size="sm"
|
||||
label="Secret"
|
||||
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>{/if}</svelte:fragment
|
||||
>
|
||||
</ArgInput>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-secondary text-sm">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !noVariablePicker}
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
import { type SchemaProperty, type ModalSchemaProperty, modalToSchema } from '$lib/common'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import SchemaEditor from './SchemaEditor.svelte'
|
||||
|
||||
export const ARG_TYPES = [
|
||||
'integer',
|
||||
'number',
|
||||
'string',
|
||||
'boolean',
|
||||
'object',
|
||||
'array',
|
||||
'S3'
|
||||
] as const
|
||||
|
||||
const argTypesLabels = {
|
||||
integer: 'Integer',
|
||||
number: 'Number',
|
||||
string: 'String',
|
||||
boolean: 'Boolean',
|
||||
object: 'Object',
|
||||
array: 'Array',
|
||||
S3: 'S3 Object'
|
||||
} as const
|
||||
|
||||
export type ArgType = (typeof ARG_TYPES)[number]
|
||||
|
||||
export function schemaToModal(
|
||||
schema: SchemaProperty,
|
||||
name: string,
|
||||
required: boolean
|
||||
): ModalSchemaProperty {
|
||||
return {
|
||||
name,
|
||||
selectedType: schema.type,
|
||||
description: schema.description ?? '',
|
||||
pattern: schema.pattern,
|
||||
default: schema.default,
|
||||
contentEncoding: schema.contentEncoding,
|
||||
format: schema.format,
|
||||
enum_: schema.enum,
|
||||
min: schema.min,
|
||||
max: schema.max,
|
||||
currency: schema.currency,
|
||||
currencyLocale: schema.currencyLocale,
|
||||
multiselect: schema.multiselect,
|
||||
items: schema.items?.type
|
||||
? { type: schema.items.type as 'string' | 'number' | undefined, enum: schema.items.enum }
|
||||
: undefined,
|
||||
required,
|
||||
schema:
|
||||
schema.type == 'object'
|
||||
? {
|
||||
$schema: undefined,
|
||||
type: schema.type,
|
||||
properties: schema.properties ?? {},
|
||||
required: schema.required ?? []
|
||||
}
|
||||
: undefined,
|
||||
showExpr: schema.showExpr,
|
||||
password: schema.password,
|
||||
nullable: schema.nullable,
|
||||
dateFormat: schema.format,
|
||||
title: schema.title,
|
||||
placeholder: schema.placeholder
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
|
||||
selectedType: 'string',
|
||||
description: '',
|
||||
name: '',
|
||||
required: false
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
|
||||
import Required from './Required.svelte'
|
||||
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
|
||||
import { Alert, Button } from './common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
import ArrayTypeNarrowing from './ArrayTypeNarrowing.svelte'
|
||||
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
|
||||
import NumberTypeNarrowing from './NumberTypeNarrowing.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import { shouldDisplayPlaceholder } from '$lib/utils'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let error = ''
|
||||
export let editing = false
|
||||
export let oldArgName: string | undefined = undefined
|
||||
export let isFlowInput = false
|
||||
export let propsNames: string[] = []
|
||||
export let variableEditor: VariableEditor | undefined = undefined
|
||||
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
let drawer: Drawer
|
||||
|
||||
let property: ModalSchemaProperty = DEFAULT_PROPERTY
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
if (key === 'Enter') {
|
||||
dispatch('save', property)
|
||||
}
|
||||
}
|
||||
|
||||
export function openDrawer(nproperty: ModalSchemaProperty): void {
|
||||
drawer.openDrawer()
|
||||
error = ''
|
||||
property = nproperty
|
||||
}
|
||||
|
||||
export function closeDrawer(): void {
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
function clearModal(): void {
|
||||
error = ''
|
||||
editing = false
|
||||
oldArgName = undefined
|
||||
property.name = DEFAULT_PROPERTY.name
|
||||
property.default = DEFAULT_PROPERTY.default
|
||||
property.description = DEFAULT_PROPERTY.description
|
||||
property.required = DEFAULT_PROPERTY.required
|
||||
property.selectedType = DEFAULT_PROPERTY.selectedType
|
||||
property.format = undefined
|
||||
property.schema = undefined
|
||||
property.min = undefined
|
||||
property.max = undefined
|
||||
property.currency = undefined
|
||||
property.currencyLocale = undefined
|
||||
property.multiselect = undefined
|
||||
property.items = undefined
|
||||
property.showExpr = undefined
|
||||
property.password = undefined
|
||||
property.nullable = false
|
||||
property.dateFormat = undefined
|
||||
property.title = undefined
|
||||
property.placeholder = undefined
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
$: if (property.name == '') {
|
||||
error = 'Name is required'
|
||||
} else {
|
||||
error = ''
|
||||
}
|
||||
|
||||
let title = ''
|
||||
$: title = editing ? `Edit ${oldArgName} argument` : 'Add an argument'
|
||||
|
||||
function getResourceTypesFromFormat(format: string | undefined): string[] {
|
||||
if (format?.startsWith('resource-')) {
|
||||
return [format.split('-')[1]]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function shouldDisplayPlaceholderForProperty(property: ModalSchemaProperty): boolean {
|
||||
return shouldDisplayPlaceholder(
|
||||
property.selectedType,
|
||||
property.format,
|
||||
property.enum_,
|
||||
property.contentEncoding,
|
||||
property.pattern
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} placement="right">
|
||||
<DrawerContent on:close={clearModal} {title}>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<label class="block">
|
||||
<div class="mb-1 font-semibold text-secondary">
|
||||
Name
|
||||
<Required required={true} />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="schema-modal-name"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter a name"
|
||||
bind:value={property.name}
|
||||
on:keyup={handleKeyUp}
|
||||
class={error === ''
|
||||
? ''
|
||||
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
|
||||
/>
|
||||
</label>
|
||||
<div class="text-red-600 text-2xs">{error}</div>
|
||||
</div>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 font-semibold text-secondary">
|
||||
Description
|
||||
<Required required={false} />
|
||||
</div>
|
||||
<textarea placeholder="Enter a description" rows="3" bind:value={property.description} />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 font-semibold text-secondary"> Custom Title </div>
|
||||
<textarea placeholder="Enter a custom title" rows="1" bind:value={property.title} />
|
||||
</label>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold text-secondary">Type<Required required={true} /></div>
|
||||
<div class="grid sm:grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-1 items-center mb-2 w-full">
|
||||
{#each ARG_TYPES as argType}
|
||||
{@const isSelected =
|
||||
(argType === 'S3' && property.format === 'resource-s3_object') ||
|
||||
(argType === property.selectedType && property.format !== 'resource-s3_object')}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isSelected ? 'blue' : 'light'}
|
||||
btnClasses={isSelected ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
const isS3 = argType == 'S3'
|
||||
|
||||
if (isS3) {
|
||||
property.selectedType = 'object'
|
||||
property.format = 'resource-s3_object'
|
||||
} else {
|
||||
property.format = undefined
|
||||
property.selectedType = argType
|
||||
}
|
||||
|
||||
property.contentEncoding = undefined
|
||||
property.enum_ = undefined
|
||||
property.pattern = undefined
|
||||
property.default = undefined
|
||||
property.min = undefined
|
||||
property.max = undefined
|
||||
property.currency = undefined
|
||||
property.currencyLocale = undefined
|
||||
property.multiselect = undefined
|
||||
property.password = undefined
|
||||
property.dateFormat = undefined
|
||||
if (argType == 'array') {
|
||||
property.items = { type: 'string' }
|
||||
} else {
|
||||
property.items = undefined
|
||||
}
|
||||
property.showExpr = undefined
|
||||
}}
|
||||
id={`schema-modal-type-${argType}`}
|
||||
>
|
||||
{argTypesLabels[argType]}
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={!property.selectedType ? 'blue' : 'light'}
|
||||
btnClasses={!property.selectedType ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
property.selectedType = undefined
|
||||
}}
|
||||
>
|
||||
any
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-row gap-x-4 items-center">
|
||||
<ArgInput
|
||||
{itemPicker}
|
||||
resourceTypes={getResourceTypesFromFormat(property.format)}
|
||||
label="Default"
|
||||
bind:value={property.default}
|
||||
type={property.password ? 'string' : property.selectedType}
|
||||
pattern={property.pattern}
|
||||
customErrorMessage={property.customErrorMessage}
|
||||
itemsType={property.items}
|
||||
contentEncoding={property.contentEncoding}
|
||||
format={property.format}
|
||||
extra={property.password ? {} : property}
|
||||
nullable={property.nullable}
|
||||
title={property.title}
|
||||
placeholder={property.placeholder}
|
||||
{variableEditor}
|
||||
/>
|
||||
<div>
|
||||
<Toggle
|
||||
options={{ right: 'Required' }}
|
||||
size="xs"
|
||||
bind:checked={property.required}
|
||||
on:change={(event) => {
|
||||
if (event?.detail) {
|
||||
property.nullable = false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if property?.selectedType === 'string'}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Nullable',
|
||||
rightTooltip:
|
||||
'If enabled, the default value will be null and not an empty string.'
|
||||
}}
|
||||
size="xs"
|
||||
bind:checked={property.nullable}
|
||||
disabled={property?.required}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if isFlowInput}
|
||||
<Alert type="info" title="Default not used by webhooks" size="xs" collapsible>
|
||||
If this flow is triggered by a webhook, the default value will not replace a missing
|
||||
value from the payload. It will only be used as the default value in the autogenerated
|
||||
UI. We recommend using default values in the signature of the steps where this value is
|
||||
used (using `x=default`) to have the desired behavior.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-secondary mb-1">Advanced</div>
|
||||
{#if property.selectedType == 'string'}
|
||||
<StringTypeNarrowing
|
||||
bind:customErrorMessage={property.customErrorMessage}
|
||||
bind:format={property.format}
|
||||
bind:pattern={property.pattern}
|
||||
bind:enum_={property.enum_}
|
||||
bind:contentEncoding={property.contentEncoding}
|
||||
bind:password={property.password}
|
||||
bind:dateFormat={property.dateFormat}
|
||||
noExtra
|
||||
/>
|
||||
{:else if property.selectedType == 'array'}
|
||||
<ArrayTypeNarrowing bind:itemsType={property.items} />
|
||||
{:else if property.selectedType == 'number' || property.selectedType == 'integer'}
|
||||
<NumberTypeNarrowing
|
||||
bind:min={property.min}
|
||||
bind:max={property.max}
|
||||
bind:currency={property.currency}
|
||||
bind:currencyLocale={property.currencyLocale}
|
||||
/>
|
||||
{:else if property.selectedType == 'object' && property.format !== 'resource-s3'}
|
||||
<Tabs selected="resource">
|
||||
<Tab value="resource">Resource</Tab>
|
||||
<Tab value="custom-object">Custom Object</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="pt-2">
|
||||
<TabContent value="custom-object">
|
||||
<SchemaEditor bind:schema={property.schema} />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="resource">
|
||||
<h3 class="mb-2 font-bold mt-4">Resource type</h3>
|
||||
<ObjectTypeNarrowing bind:format={property.format} />
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
{#if property && shouldDisplayPlaceholderForProperty(property)}
|
||||
<Label label="Placeholder" class="pt-2">
|
||||
<textarea
|
||||
placeholder="Enter a placeholder"
|
||||
rows="1"
|
||||
bind:value={property.placeholder}
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="pt-2">
|
||||
<Toggle
|
||||
options={{ right: 'Show this field only when conditions are met' }}
|
||||
class="!justify-start"
|
||||
checked={Boolean(property.showExpr)}
|
||||
on:change={() => {
|
||||
property.showExpr = property.showExpr ? undefined : 'true //fields.foo == 42'
|
||||
}}
|
||||
/>
|
||||
{#if property.showExpr != undefined}
|
||||
<div class="border">
|
||||
<SimpleEditor
|
||||
extraLib={`declare const fields: Record<${propsNames
|
||||
.filter((x) => x != property.name)
|
||||
.map((x) => `"${x}"`)
|
||||
.join(' | ')}, any>;\n`}
|
||||
lang="javascript"
|
||||
bind:code={property.showExpr}
|
||||
shouldBindKey={false}
|
||||
fixedOverflowWidgets={false}
|
||||
autoHeight
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse text-2xs text-tertiary"
|
||||
><div
|
||||
>Other fields are available under <code>fields</code> (e.g:
|
||||
<code>fields.foo == 42</code>)</div
|
||||
></div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-semibold text-secondary mb-1 pt-4">Preview</div>
|
||||
<LightweightSchemaForm
|
||||
displayType={false}
|
||||
schema={{
|
||||
properties: {
|
||||
[property.name]: modalToSchema(property)
|
||||
},
|
||||
required: property.required ? [property.name] : []
|
||||
}}
|
||||
/>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="h-10" />
|
||||
<Button
|
||||
color="dark"
|
||||
disabled={!property.name || error != ''}
|
||||
on:click={() => {
|
||||
dispatch('save', property)
|
||||
}}
|
||||
id="schema-modal-save"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{#if isFlowInput}
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
if (property) {
|
||||
property.default = '$var:' + path
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
tooltip="Variables are dynamic values that have a key associated to them and can be retrieved during the execution of a Script or Flow."
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
>
|
||||
<div slot="submission">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => variableEditor?.initNew?.()}
|
||||
>
|
||||
New Variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
{/if}
|
||||
@@ -306,6 +306,7 @@
|
||||
if (initialPath != '' && !savedScript) {
|
||||
return
|
||||
}
|
||||
|
||||
if (savedScript) {
|
||||
const draftOrDeployed = cleanValueProperties(savedScript.draft || savedScript)
|
||||
const current = cleanValueProperties(script)
|
||||
@@ -458,7 +459,11 @@
|
||||
<slot />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<Drawer placement="right" bind:open={metadataOpen} size="800px">
|
||||
<Drawer
|
||||
placement="right"
|
||||
bind:open={metadataOpen}
|
||||
size={selectedTab === 'ui' ? '1200px' : '800px'}
|
||||
>
|
||||
<DrawerContent noPadding title="Settings" on:close={() => (metadataOpen = false)}>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
@@ -475,7 +480,10 @@
|
||||
</Tab>
|
||||
<Tab value="schedule" active={$scheduleStore.enabled}>Schedule</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="p-4">
|
||||
<div
|
||||
class={selectedTab === 'ui' ? 'p-0' : 'p-4'}
|
||||
style={selectedTab === 'ui' ? `height: calc(100% - 32px);` : ''}
|
||||
>
|
||||
<TabContent value="metadata">
|
||||
<div class="flex flex-col gap-8">
|
||||
<Section label="Metadata">
|
||||
@@ -967,8 +975,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="ui">
|
||||
<div class="mt-4" />
|
||||
<TabContent value="ui" class="h-full">
|
||||
<ScriptSchema bind:schema={script.schema} />
|
||||
</TabContent>
|
||||
<TabContent value="schedule">
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
|
||||
import Highlight from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import EditableSchemaForm from './EditableSchemaForm.svelte'
|
||||
|
||||
export let schema: Schema | any
|
||||
@@ -14,20 +8,7 @@
|
||||
export function setSchema(newSchema: Schema) {
|
||||
schema = newSchema
|
||||
}
|
||||
const yOffset = 80
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<Tabs selected="ui">
|
||||
<Tab value="ui">Arguments</Tab>
|
||||
<Tab value="jsonschema">JSON Schema</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="ui">
|
||||
<div class="mt-4" />
|
||||
<EditableSchemaForm bind:schema />
|
||||
</TabContent>
|
||||
<TabContent value="jsonschema">
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
<EditableSchemaForm bind:schema uiOnly offset={yOffset} />
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { computeKind } from '$lib/utils'
|
||||
import Label from './Label.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Button } from './common'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import ClearableInput from './common/clearableInput/ClearableInput.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import RegexGen from './copilot/RegexGen.svelte'
|
||||
|
||||
export let pattern: string | undefined
|
||||
@@ -78,196 +79,201 @@
|
||||
]
|
||||
</script>
|
||||
|
||||
<RadioButton
|
||||
label="Kind"
|
||||
options={[
|
||||
['None', 'none'],
|
||||
['File (base64)', 'base64'],
|
||||
['Enum', 'enum'],
|
||||
['Format', 'format'],
|
||||
['Pattern', 'pattern']
|
||||
]}
|
||||
bind:value={kind}
|
||||
on:change={(e) => {
|
||||
if (e.detail != 'enum') {
|
||||
enum_ = undefined
|
||||
}
|
||||
if (e.detail == 'none') {
|
||||
pattern = undefined
|
||||
format = undefined
|
||||
contentEncoding = undefined
|
||||
customErrorMessage = undefined
|
||||
minRows = undefined
|
||||
disableCreate = undefined
|
||||
disableVariablePicker = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="my-2" />
|
||||
|
||||
{#if kind == 'pattern'}
|
||||
<label for="input" class="mb-2 text-secondary text-xs">
|
||||
Pattern (Regex)
|
||||
<div class="flex flex-row gap-1">
|
||||
<input
|
||||
id="input"
|
||||
type="text"
|
||||
placeholder="^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
|
||||
bind:value={patternStr}
|
||||
class="text-sm"
|
||||
/>
|
||||
<RegexGen
|
||||
on:gen={(e) => {
|
||||
const { res, prompt } = e.detail
|
||||
patternStr = res
|
||||
customErrorMessage = 'does not match: ' + prompt
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
patternStr = ''
|
||||
}}
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Custom error message' }}
|
||||
checked={customErrorMessage != undefined && customErrorMessage != ''}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
customErrorMessage = 'Custom error message'
|
||||
} else {
|
||||
customErrorMessage = undefined
|
||||
}
|
||||
}}
|
||||
>Custom error message
|
||||
</Toggle>
|
||||
<input type="text" bind:value={customErrorMessage} />
|
||||
</div>
|
||||
</label>
|
||||
{:else if kind == 'enum'}
|
||||
<label for="input" class="mb-2 text-secondary text-xs">
|
||||
Enums
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each enum_ || [] as e}
|
||||
<div class="flex flex-row max-w-md">
|
||||
<input id="input" type="text" bind:value={e} />
|
||||
<Button size="sm" btnClasses="ml-6" on:click={() => remove(e)}>-</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-row my-1">
|
||||
<Button color="light" size="sm" on:click={add}>+</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
btnClasses="ml-2"
|
||||
on:click={() => (enum_ = undefined)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
{#if !noExtra}
|
||||
<Toggle
|
||||
size="sm"
|
||||
options={{ right: 'Disallow creating custom values' }}
|
||||
checked={disableCreate != undefined && disableCreate}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
disableCreate = true
|
||||
} else {
|
||||
disableCreate = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if kind == 'resource'}
|
||||
<div class="mt-1" />
|
||||
<ResourceTypePicker bind:value={resource} />
|
||||
{:else if kind == 'format'}
|
||||
<select class="mt-1" bind:value={format}>
|
||||
<option value={undefined} />
|
||||
{#each FORMATS as f}
|
||||
<option value={f}>{f}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if format == 'date'}
|
||||
<div class="mt-1" />
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Label label="Date format passed to script" class="col-span-2">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting the date output format will allow you to specify how the date will be passed to
|
||||
the script.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<ClearableInput type="text" bind:value={dateFormat} placeholder="yyyy-MM-dd" />
|
||||
</Label>
|
||||
<Label label="Presets">
|
||||
<select
|
||||
bind:value={dateFormat}
|
||||
disabled={dateFormat ? !presetOptions.map((f) => f.format).includes(dateFormat) : false}
|
||||
>
|
||||
{#each presetOptions as f}
|
||||
<option value={f.format}>{f.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if kind == 'none'}
|
||||
{#if !noExtra}
|
||||
<label
|
||||
>min textarea rows:
|
||||
<input type="number" bind:value={minRows} />
|
||||
</label>
|
||||
{/if}
|
||||
{:else if kind === 'base64'}
|
||||
<Alert
|
||||
type="warning"
|
||||
title="S3 Object recommended"
|
||||
collapsible
|
||||
size="xs"
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/large_data_files"
|
||||
>
|
||||
For large files, we recommend using the S3 Object type instead of the base64 string type.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if (kind == 'none' || kind == 'pattern' || kind == 'format') && !noExtra}
|
||||
<div class="mt-1" />
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Disable variable picker' }}
|
||||
checked={disableVariablePicker != undefined && disableVariablePicker}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
disableVariablePicker = true
|
||||
} else {
|
||||
<div class="flex flex-col gap-2">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={kind}
|
||||
on:selected={(e) => {
|
||||
if (e.detail != 'enum') {
|
||||
enum_ = undefined
|
||||
}
|
||||
if (e.detail == 'none') {
|
||||
pattern = undefined
|
||||
format = undefined
|
||||
contentEncoding = undefined
|
||||
customErrorMessage = undefined
|
||||
minRows = undefined
|
||||
disableCreate = undefined
|
||||
disableVariablePicker = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
>
|
||||
{#each [['None', 'none'], ['File', 'base64', 'Encoded as Base 64'], ['Enum', 'enum'], ['Format', 'format'], ['Pattern', 'pattern']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} tooltip={x[2]} showTooltipIcon={Boolean(x[2])} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
{#if kind == 'pattern'}
|
||||
<Label label="Pattern (Regex)">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting a pattern will allow you to specify a regular expression that the input should
|
||||
adhere to. You can use the regex generator to help you create a pattern.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-row gap-1">
|
||||
<ClearableInput
|
||||
id="input"
|
||||
type="text"
|
||||
placeholder="^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
|
||||
bind:value={patternStr}
|
||||
/>
|
||||
<RegexGen
|
||||
on:gen={(e) => {
|
||||
const { res, prompt } = e.detail
|
||||
patternStr = res
|
||||
customErrorMessage = 'does not match: ' + prompt
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Label label="Custom error message" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting a custom error message will allow you to specify a message that will be shown when
|
||||
the input does not match the pattern.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Enable' }}
|
||||
checked={customErrorMessage != undefined && customErrorMessage != ''}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
customErrorMessage = 'Custom error message'
|
||||
} else {
|
||||
customErrorMessage = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={customErrorMessage}
|
||||
disabled={customErrorMessage == undefined || customErrorMessage == ''}
|
||||
/>
|
||||
</Label>
|
||||
{:else if kind == 'enum'}
|
||||
<Label label="Enums">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting enums will allow you to specify a list of values that the input can take. If you
|
||||
want to allow custom values, you can disable the option below.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Button color="light" size="xs" btnClasses="ml-2" on:click={() => (enum_ = undefined)}>
|
||||
Clear enums
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each enum_ || [] as e}
|
||||
<div class="flex flex-row w-full gap-2">
|
||||
<input id="input" type="text" bind:value={e} />
|
||||
<Button size="sm" on:click={() => remove(e)}>-</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-row my-1">
|
||||
<Button color="light" size="sm" on:click={add}>+</Button>
|
||||
</div>
|
||||
</Label>
|
||||
{#if !noExtra}
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Disallow creating custom values' }}
|
||||
checked={disableCreate != undefined && disableCreate}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
disableCreate = true
|
||||
} else {
|
||||
disableCreate = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if kind == 'resource'}
|
||||
<div class="mt-1" />
|
||||
<ResourceTypePicker bind:value={resource} />
|
||||
{:else if kind == 'format'}
|
||||
<Label label="Format">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting the format will allow you to specify a format that the input should adhere to.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<select bind:value={format}>
|
||||
<option value={undefined} />
|
||||
{#each FORMATS as f}
|
||||
<option value={f}>{f}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
{#if format == 'date'}
|
||||
<div class="mt-1" />
|
||||
|
||||
{#if kind == 'none' || kind == 'pattern'}
|
||||
<div class="mt-1" />
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Is Password' }}
|
||||
checked={password}
|
||||
on:change={(e) => {
|
||||
password = e.detail
|
||||
}}
|
||||
/>
|
||||
<div class="mb-4" />
|
||||
{/if}
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Label label="Date format passed to script" class="col-span-2">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Setting the date output format will allow you to specify how the date will be passed
|
||||
to the script.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<ClearableInput type="text" bind:value={dateFormat} placeholder="yyyy-MM-dd" />
|
||||
</Label>
|
||||
<Label label="Presets">
|
||||
<select
|
||||
bind:value={dateFormat}
|
||||
disabled={dateFormat ? !presetOptions.map((f) => f.format).includes(dateFormat) : false}
|
||||
>
|
||||
{#each presetOptions as f}
|
||||
<option value={f.format}>{f.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if kind == 'none'}
|
||||
{#if !noExtra}
|
||||
<Label label="Min textarea rows">
|
||||
<input type="number" bind:value={minRows} />
|
||||
</Label>
|
||||
{/if}
|
||||
{:else if kind === 'base64'}
|
||||
<Alert
|
||||
type="warning"
|
||||
title="S3 Object recommended"
|
||||
size="xs"
|
||||
tooltip="Check out the documentation for more information:"
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage#large-data-files-s3-r2-minio-azure-blob"
|
||||
>
|
||||
For large files, we recommend using the S3 Object type instead of the base64 string type.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if (kind == 'none' || kind == 'pattern' || kind == 'format') && !noExtra}
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Disable variable picker' }}
|
||||
checked={disableVariablePicker != undefined && disableVariablePicker}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
disableVariablePicker = true
|
||||
} else {
|
||||
disableVariablePicker = undefined
|
||||
}
|
||||
}}
|
||||
disabled={['email', 'yaml', 'sql', 'date-time', 'date'].includes(format ?? '')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if kind == 'none' || kind == 'pattern'}
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Is Password' }}
|
||||
checked={password}
|
||||
on:change={(e) => {
|
||||
password = e.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let textStyle = ''
|
||||
export let color: 'blue' | 'red' = 'blue'
|
||||
export let id = (Math.random() + 1).toString(36).substring(10)
|
||||
export let lightMode: boolean = false
|
||||
|
||||
export let size: 'sm' | 'xs' = 'sm'
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
>
|
||||
{options?.left}
|
||||
{#if options?.leftTooltip}
|
||||
<Tooltip>{options?.leftTooltip}</Tooltip>
|
||||
<Tooltip light={lightMode}>{options?.leftTooltip}</Tooltip>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -64,7 +65,9 @@
|
||||
<div
|
||||
class={classNames(
|
||||
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all ",
|
||||
color == 'red' ? 'peer-checked:bg-red-600' : 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500',
|
||||
color == 'red'
|
||||
? 'peer-checked:bg-red-600'
|
||||
: 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500',
|
||||
size === 'sm'
|
||||
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
@@ -83,7 +86,7 @@
|
||||
>
|
||||
{options?.right}
|
||||
{#if options?.rightTooltip}
|
||||
<Tooltip>{options?.rightTooltip}</Tooltip>
|
||||
<Tooltip light={lightMode}>{options?.rightTooltip}</Tooltip>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
+5
-43
@@ -4,7 +4,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { InputType, StaticInput, StaticOptions } from '../../inputType'
|
||||
import SubTypeEditor from './SubTypeEditor.svelte'
|
||||
import { dndzone, SOURCES, TRIGGERS } from 'svelte-dnd-action'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import { generateRandomString, pluralize } from '$lib/utils'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import QuickAddColumn from './QuickAddColumn.svelte'
|
||||
@@ -178,48 +178,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
let dragDisabled = true
|
||||
|
||||
let dragging = false
|
||||
function handleConsider(e) {
|
||||
const {
|
||||
items: newItems,
|
||||
info: { source, trigger }
|
||||
} = e.detail
|
||||
const { items: newItems } = e.detail
|
||||
items = newItems
|
||||
// Ensure dragging is stopped on drag finish via keyboard
|
||||
if (source === SOURCES.KEYBOARD && trigger === TRIGGERS.DRAG_STOPPED) {
|
||||
dragDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleFinalize(e) {
|
||||
const {
|
||||
items: newItems,
|
||||
info: { source }
|
||||
} = e.detail
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
|
||||
// Ensure dragging is stopped on drag finish via pointer (mouse, touch)
|
||||
if (source === SOURCES.POINTER) {
|
||||
dragDisabled = true
|
||||
}
|
||||
|
||||
const reorderedValues = items.map((item) => item.value)
|
||||
componentInput.value = reorderedValues
|
||||
dragging = false
|
||||
}
|
||||
|
||||
function startDrag(e) {
|
||||
dragging = true
|
||||
// preventing default to prevent lag on touch devices (because of the browser checking for screen scrolling)
|
||||
e.preventDefault()
|
||||
dragDisabled = false
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && dragDisabled) dragDisabled = false
|
||||
}
|
||||
|
||||
let items = (Array.isArray(componentInput.value) ? componentInput.value : [])
|
||||
@@ -272,9 +242,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
<section
|
||||
use:dndzone={{
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
dragDisabled,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
@@ -293,19 +262,12 @@
|
||||
bind:componentInput
|
||||
bind:value={item.value}
|
||||
on:remove={() => deleteElementByType(index)}
|
||||
{dragging}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
tabindex={dragDisabled ? 0 : -1}
|
||||
class="w-4 h-4 cursor-move"
|
||||
on:mousedown={startDrag}
|
||||
on:touchstart={startDrag}
|
||||
on:keydown={handleKeyDown}
|
||||
>
|
||||
<div class="w-4 h-4 cursor-move handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{#if subFieldType !== 'db-explorer'}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { AppInputSpec } from '../../inputType'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import { dndzone } from 'svelte-dnd-action'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import { GripVertical, Plus, X } from 'lucide-svelte'
|
||||
import InputsSpecEditor from './InputsSpecEditor.svelte'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
export let conditions: RichConfiguration[] = []
|
||||
export let component: AppComponent
|
||||
let dragDisabled = true
|
||||
|
||||
let items = conditions.slice(0, -1).map((condition, index) => {
|
||||
return { value: condition, id: generateRandomString(), originalIndex: index }
|
||||
@@ -69,19 +68,6 @@
|
||||
$componentControl[component.id]?.setTab?.(targetIndex)
|
||||
})
|
||||
}
|
||||
|
||||
dragDisabled = true
|
||||
}
|
||||
|
||||
function startDrag(event) {
|
||||
event.preventDefault()
|
||||
dragDisabled = false
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && dragDisabled) {
|
||||
dragDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSubgrid(index: number) {
|
||||
@@ -150,11 +136,10 @@
|
||||
{/if}
|
||||
<div class="w-full flex flex-col mt-2">
|
||||
<section
|
||||
use:dndzone={{
|
||||
use:dragHandleZone={{
|
||||
items: items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {},
|
||||
dragDisabled
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
@@ -163,27 +148,23 @@
|
||||
{@const condition = item.value}
|
||||
<div class="w-full flex flex-row gap-2 items-center relative">
|
||||
<div class={twMerge('grow border p-3 my-2 rounded-md bg-surface relative')}>
|
||||
{#if dragDisabled}
|
||||
<InputsSpecEditor
|
||||
key={`condition${index + 1}`}
|
||||
bind:componentInput={item.value}
|
||||
id={component.id}
|
||||
userInputEnabled={false}
|
||||
shouldCapitalize={true}
|
||||
resourceOnly={false}
|
||||
fieldType={condition?.['fieldType']}
|
||||
subFieldType={condition?.['subFieldType']}
|
||||
format={condition?.['format']}
|
||||
selectOptions={condition?.['selectOptions']}
|
||||
tooltip={condition?.['tooltip']}
|
||||
fileUpload={condition?.['fileUpload']}
|
||||
placeholder={condition?.['placeholder']}
|
||||
customTitle={condition?.['customTitle']}
|
||||
displayType={false}
|
||||
/>
|
||||
{:else}
|
||||
<pre><code>{condition?.['expr']}</code></pre>
|
||||
{/if}
|
||||
<InputsSpecEditor
|
||||
key={`condition${index + 1}`}
|
||||
bind:componentInput={item.value}
|
||||
id={component.id}
|
||||
userInputEnabled={false}
|
||||
shouldCapitalize={true}
|
||||
resourceOnly={false}
|
||||
fieldType={condition?.['fieldType']}
|
||||
subFieldType={condition?.['subFieldType']}
|
||||
format={condition?.['format']}
|
||||
selectOptions={condition?.['selectOptions']}
|
||||
tooltip={condition?.['tooltip']}
|
||||
fileUpload={condition?.['fileUpload']}
|
||||
placeholder={condition?.['placeholder']}
|
||||
customTitle={condition?.['customTitle']}
|
||||
displayType={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center gap-2">
|
||||
@@ -195,15 +176,7 @@
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
tabindex={dragDisabled ? 0 : -1}
|
||||
class="w-4 h-4"
|
||||
on:mousedown={startDrag}
|
||||
on:touchstart={startDrag}
|
||||
on:keydown={handleKeyDown}
|
||||
aria-label="drag-handle"
|
||||
style={dragDisabled ? 'cursor: grab' : 'cursor: grabbing'}
|
||||
>
|
||||
<div use:dragHandle class="w-4 h-4 handle" aria-label="drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { deleteGridItem } from '../appUtils'
|
||||
import type { AppComponent } from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import { dndzone } from 'svelte-dnd-action'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import { GripVertical, Plus } from 'lucide-svelte'
|
||||
import GridTabDisabled from './GridTabDisabled.svelte'
|
||||
@@ -135,21 +135,6 @@
|
||||
$componentControl[component.id]?.setTab?.(targetIndex)
|
||||
})
|
||||
}
|
||||
|
||||
dragDisabled = true
|
||||
}
|
||||
|
||||
let dragDisabled = true
|
||||
|
||||
function startDrag(event) {
|
||||
event.preventDefault()
|
||||
dragDisabled = false
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && dragDisabled) {
|
||||
dragDisabled = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -159,10 +144,9 @@
|
||||
{/if}
|
||||
<div class="w-full flex gap-2 flex-col mt-2">
|
||||
<section
|
||||
use:dndzone={{
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs: 200,
|
||||
dragDisabled,
|
||||
dropTargetStyle: {}
|
||||
}}
|
||||
on:consider={handleConsider}
|
||||
@@ -185,15 +169,7 @@
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
tabindex={dragDisabled ? 0 : -1}
|
||||
class="w-4 h-4"
|
||||
on:mousedown={startDrag}
|
||||
on:touchstart={startDrag}
|
||||
on:keydown={handleKeyDown}
|
||||
aria-label="drag-handle"
|
||||
style={dragDisabled ? 'cursor: grab' : 'cursor: grabbing'}
|
||||
>
|
||||
<div use:dragHandle class="handle w-4 h-4" aria-label="drag-handle">
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
export let componentInput: StaticInput<any>
|
||||
export let subFieldType: InputType | undefined
|
||||
export let id: string | undefined
|
||||
export let dragging: boolean = false
|
||||
|
||||
let fakeComponentInput: StaticInput<any> = {
|
||||
...componentInput,
|
||||
@@ -22,5 +21,4 @@
|
||||
fieldType={subFieldType}
|
||||
bind:componentInput={fakeComponentInput}
|
||||
on:remove
|
||||
{dragging}
|
||||
/>
|
||||
|
||||
+7
-14
@@ -9,7 +9,6 @@
|
||||
import ColorInput from './ColorInput.svelte'
|
||||
import TabSelectInput from './TabSelectInput.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Settings } from 'lucide-svelte'
|
||||
@@ -22,6 +21,7 @@
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte'
|
||||
import DBTableSelect from './DBTableSelect.svelte'
|
||||
import EditableSchemaDrawer from '$lib/components/schema/EditableSchemaDrawer.svelte'
|
||||
|
||||
export let componentInput: StaticInput<any> | undefined
|
||||
export let fieldType: InputType | undefined = undefined
|
||||
@@ -30,7 +30,6 @@
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let format: string | undefined = undefined
|
||||
export let id: string | undefined
|
||||
export let dragging: boolean = false
|
||||
|
||||
const { onchange } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -154,24 +153,18 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex w-full flex-col">
|
||||
{#if dragging}
|
||||
<div class="text-xs text-secondary whitespace-pre border p-1"
|
||||
>{JSON.stringify(componentInput.value, null, 2)}</div
|
||||
>
|
||||
{:else}
|
||||
<JsonEditor
|
||||
small
|
||||
bind:value={componentInput.value}
|
||||
code={JSON.stringify(componentInput.value, null, 2)}
|
||||
/>
|
||||
{/if}
|
||||
<JsonEditor
|
||||
small
|
||||
bind:value={componentInput.value}
|
||||
code={JSON.stringify(componentInput.value, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if fieldType === 'array'}
|
||||
<ArrayStaticInputEditor {id} {subFieldType} bind:componentInput on:deleteArrayItem />
|
||||
{:else if fieldType === 'schema'}
|
||||
<div class="w-full">
|
||||
<SchemaEditor bind:schema={componentInput.value} lightMode />
|
||||
<EditableSchemaDrawer bind:schema={componentInput.value} />
|
||||
</div>
|
||||
{:else if fieldType === 'ag-grid'}
|
||||
<div class="flex flex-row rounded-md bg-surface items-center h-full">
|
||||
|
||||
@@ -128,7 +128,8 @@
|
||||
width: 100%;
|
||||
@apply bg-surface;
|
||||
z-index: 3;
|
||||
transition: transform var(--duration) ease;
|
||||
transition: transform var(--duration) ease, max-width var(--duration) ease,
|
||||
max-height var(--duration) ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let selected: any
|
||||
export let noWFull: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let tabListClass: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const selectedContent = writable(selected)
|
||||
@@ -42,7 +43,9 @@
|
||||
$$props.class
|
||||
)}
|
||||
>
|
||||
<TabList class="flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full ">
|
||||
<TabList
|
||||
class={twMerge('flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full ', tabListClass)}
|
||||
>
|
||||
<slot />
|
||||
</TabList>
|
||||
</TabGroup>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, DrawerContent } from '$lib/components/common'
|
||||
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import { copyFirstStepSchema } from '../flowStore'
|
||||
@@ -13,6 +11,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
|
||||
import AddProperty from '$lib/components/schema/AddProperty.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
|
||||
@@ -23,6 +22,7 @@
|
||||
let inputLibraryDrawer: Drawer
|
||||
let jsonPayload: Drawer
|
||||
let pendingJson: string
|
||||
let addProperty: AddProperty | undefined = undefined
|
||||
|
||||
function importJson() {
|
||||
const parsed = JSON.parse(pendingJson)
|
||||
@@ -36,64 +36,73 @@
|
||||
|
||||
jsonPayload.closeDrawer()
|
||||
}
|
||||
const yOffset = 191
|
||||
</script>
|
||||
|
||||
<CapturePayload bind:this={capturePayload} />
|
||||
|
||||
<FlowCard {noEditor} title="Flow Input">
|
||||
<div class="p-6">
|
||||
<div class="flex flex-row items-center gap-2 pb-2 border-b border-gray-400">
|
||||
<div>Copy input's schema from</div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
capturePayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A request
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
jsonPayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A JSON
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Past Runs/Input library
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={$flowStore.value.modules.length === 0 ||
|
||||
$flowStore.value.modules[0].value.type == 'identity'}
|
||||
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
|
||||
>
|
||||
First step's inputs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<SchemaEditor
|
||||
isFlowInput
|
||||
bind:schema={$flowStore.schema}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
|
||||
<div>Copy input's schema from</div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
capturePayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A request
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
jsonPayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A JSON
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Past Runs/Input library
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={$flowStore.value.modules.length === 0 ||
|
||||
$flowStore.value.modules[0].value.type == 'identity'}
|
||||
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
|
||||
>
|
||||
First step's inputs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<h2 class="mb-4">Customize Flow Inputs</h2>
|
||||
<EditableSchemaForm bind:schema={$flowStore.schema} />
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty
|
||||
bind:schema={$flowStore.schema}
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditableSchemaForm
|
||||
bind:schema={$flowStore.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
displayWebhookWarning
|
||||
/>
|
||||
</FlowCard>
|
||||
|
||||
<Drawer bind:this={jsonPayload} size="800px">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import InputTransformForm from '$lib/components/InputTransformForm.svelte'
|
||||
@@ -16,6 +15,7 @@
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import SuspendDrawer from './SuspendDrawer.svelte'
|
||||
import EditableSchemaDrawer from '$lib/components/schema/EditableSchemaDrawer.svelte'
|
||||
|
||||
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const result = $flowStateStore[$selectedId]?.previewResult ?? {}
|
||||
@@ -50,36 +50,36 @@
|
||||
</script>
|
||||
|
||||
<Section label="Suspend/Approval/Prompt" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<SuspendDrawer text="Approval/Prompt helpers" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/flow_approval">
|
||||
If defined, at the end of the step, the flow will be suspended until it receives external
|
||||
requests to be resumed or canceled. This is most useful to implement approval steps but can be
|
||||
used flexibly for other purposes.
|
||||
</Tooltip>
|
||||
<div class="ml-4">
|
||||
<div class="flex">
|
||||
<SuspendDrawer text="Approval/Prompt helpers" />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/flow_approval">
|
||||
If defined, at the end of the step, the flow will be suspended until it receives external
|
||||
requests to be resumed or canceled. This is most useful to implement approval steps but can
|
||||
be used flexibly for other purposes.
|
||||
</Tooltip>
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={isSuspendEnabled}
|
||||
on:change={() => {
|
||||
if (isSuspendEnabled && flowModule.suspend != undefined) {
|
||||
flowModule.suspend = undefined
|
||||
} else {
|
||||
flowModule.suspend = {
|
||||
required_events: 1,
|
||||
timeout: 1800
|
||||
}
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Suspend flow execution until events/approvals received'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<Toggle
|
||||
checked={isSuspendEnabled}
|
||||
on:change={() => {
|
||||
if (isSuspendEnabled && flowModule.suspend != undefined) {
|
||||
flowModule.suspend = undefined
|
||||
} else {
|
||||
flowModule.suspend = {
|
||||
required_events: 1,
|
||||
timeout: 1800
|
||||
}
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Suspend flow execution until events/approvals received'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="overflow-x-auto scrollbar-hidden">
|
||||
<Tabs bind:selected={suspendTabSelected}>
|
||||
<Tab size="xs" value="core" disabled={!isSuspendEnabled}>
|
||||
@@ -190,46 +190,60 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col mt-4 gap-4">
|
||||
{#if flowModule.suspend}
|
||||
{#if emptyString($enterpriseLicense)}
|
||||
<Alert type="warning" title="Adding a form to the approval page is an EE feature" />
|
||||
{/if}
|
||||
<div class="grid grid-cols-4 mt-4 gap-2">
|
||||
<div class="col-span-2 flex flex-col gap-2">
|
||||
{#if flowModule.suspend}
|
||||
{#if emptyString($enterpriseLicense)}
|
||||
<Alert type="warning" title="Adding a form to the approval page is an EE feature" />
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4">
|
||||
<Toggle
|
||||
checked={Boolean(flowModule.suspend.resume_form)}
|
||||
options={{
|
||||
right: 'Add a form to the approval page'
|
||||
}}
|
||||
disabled={emptyString($enterpriseLicense)}
|
||||
on:change={(e) => {
|
||||
if (flowModule.suspend) {
|
||||
if (e.detail) {
|
||||
flowModule.suspend.resume_form = {
|
||||
schema: emptySchema()
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
checked={Boolean(flowModule.suspend.resume_form)}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'Add a form to the approval page'
|
||||
}}
|
||||
disabled={emptyString($enterpriseLicense)}
|
||||
on:change={(e) => {
|
||||
if (flowModule.suspend) {
|
||||
if (e.detail) {
|
||||
flowModule.suspend.resume_form = {
|
||||
schema: emptySchema()
|
||||
}
|
||||
} else {
|
||||
flowModule.suspend.resume_form = undefined
|
||||
}
|
||||
} else {
|
||||
flowModule.suspend.resume_form = undefined
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="flex">
|
||||
<SuspendDrawer text="Default args & Dynamic enums help" />
|
||||
}}
|
||||
/>
|
||||
<div class="flex">
|
||||
<SuspendDrawer text="Default args & Dynamic enums help" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if flowModule.suspend?.resume_form}
|
||||
<Toggle
|
||||
bind:checked={flowModule.suspend.hide_cancel}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'Hide cancel button on approval page'
|
||||
}}
|
||||
/>
|
||||
<SchemaEditor bind:schema={flowModule.suspend.resume_form.schema} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if flowModule.suspend}
|
||||
<Toggle
|
||||
bind:checked={flowModule.suspend.hide_cancel}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'Hide cancel button on approval page'
|
||||
}}
|
||||
disabled={!Boolean(flowModule?.suspend?.resume_form)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
{#if flowModule?.suspend?.resume_form}
|
||||
<EditableSchemaDrawer bind:schema={flowModule.suspend.resume_form.schema} />
|
||||
{:else}
|
||||
<div
|
||||
class="bg-gray-50 border-gray-200 border dark:bg-gray-900/40 dark:border-gray-700/40 rounded-md p-2 text-xs"
|
||||
>
|
||||
No form
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type Schema,
|
||||
type SchemaProperty,
|
||||
modalToSchema,
|
||||
type ModalSchemaProperty
|
||||
} from '$lib/common'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import AddPropertyForm from './AddPropertyForm.svelte'
|
||||
|
||||
export let schema: Schema | any = emptySchema()
|
||||
|
||||
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
|
||||
selectedType: 'string',
|
||||
description: '',
|
||||
name: '',
|
||||
required: false
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
}
|
||||
|
||||
let schemaString: string = ''
|
||||
|
||||
// Internal state: bound to args builder modal
|
||||
let argError = ''
|
||||
let editing = false
|
||||
let oldArgName: string | undefined // when editing argument and changing name
|
||||
let jsonEditor: SimpleEditor
|
||||
|
||||
reorder()
|
||||
|
||||
function reorder() {
|
||||
if (schema.order && Array.isArray(schema.order)) {
|
||||
const n = {}
|
||||
|
||||
;(schema.order as string[]).forEach((x) => {
|
||||
if (schema.properties && schema.properties[x] != undefined) {
|
||||
n[x] = schema.properties[x]
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(schema.properties ?? {})
|
||||
.filter((x) => !schema.order?.includes(x))
|
||||
.forEach((x) => {
|
||||
n[x] = schema.properties[x]
|
||||
})
|
||||
schema.properties = n
|
||||
}
|
||||
}
|
||||
|
||||
function syncOrders() {
|
||||
if (schema) {
|
||||
schema.order = Object.keys(schema.properties ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
modalProperty.name = modalProperty.name.trim()
|
||||
|
||||
if (modalProperty.name.length === 0) {
|
||||
argError = 'Arguments need to have a name'
|
||||
} else if (
|
||||
Object.keys(schema.properties ?? {}).includes(modalProperty.name) &&
|
||||
(!editing || (editing && oldArgName && oldArgName !== modalProperty.name))
|
||||
) {
|
||||
argError = 'There is already an argument with this name'
|
||||
} else {
|
||||
if (!schema.properties) {
|
||||
schema.properties = {}
|
||||
}
|
||||
if (!schema.required) {
|
||||
schema.required = []
|
||||
}
|
||||
if (!schema.order || !Array.isArray(schema.order)) {
|
||||
syncOrders()
|
||||
}
|
||||
schema.properties[modalProperty.name] = modalToSchema(modalProperty)
|
||||
if (modalProperty.required) {
|
||||
if (!schema.required.includes(modalProperty.name)) {
|
||||
schema.required.push(modalProperty.name)
|
||||
}
|
||||
} else if (schema.required.includes(modalProperty.name)) {
|
||||
const index = schema.required.indexOf(modalProperty.name, 0)
|
||||
if (index > -1) {
|
||||
schema.required.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (editing && oldArgName && oldArgName !== modalProperty.name) {
|
||||
let oldPosition = schema.order.indexOf(oldArgName)
|
||||
schema.order[oldPosition] = modalProperty.name
|
||||
reorder()
|
||||
handleDeleteArgument([oldArgName])
|
||||
}
|
||||
|
||||
if (!schema.order?.includes(modalProperty.name)) {
|
||||
schema.order.push(modalProperty.name)
|
||||
}
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
editing = false
|
||||
oldArgName = undefined
|
||||
}
|
||||
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
jsonEditor?.setCode(schemaString)
|
||||
|
||||
if (argError !== '') {
|
||||
sendUserToast(argError, true)
|
||||
}
|
||||
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
export function handleDeleteArgument(argPath: string[]): void {
|
||||
try {
|
||||
let modifiedObject: Schema | SchemaProperty = schema
|
||||
let modifiedProperties = modifiedObject.properties as object
|
||||
let argName = argPath.pop() as string
|
||||
|
||||
argPath.forEach((property) => {
|
||||
if (Object.keys(modifiedProperties).includes(property)) {
|
||||
modifiedObject = modifiedProperties[property]
|
||||
modifiedProperties = modifiedObject.properties as object
|
||||
} else {
|
||||
throw Error('Nested argument not found!')
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
}
|
||||
syncOrders()
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete argument: ${err}`, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<AddPropertyForm
|
||||
on:add={(e) => {
|
||||
try {
|
||||
handleAddOrEditArgument({
|
||||
...DEFAULT_PROPERTY,
|
||||
selectedType: 'string',
|
||||
name: e.detail.name
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not add argument: ${err}`, true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '../common'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
|
||||
let name: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function addField() {
|
||||
dispatch('add', { name })
|
||||
name = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<input
|
||||
bind:value={name}
|
||||
placeholder="Field name"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
addField()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: Plus }}
|
||||
id="flow-editor-add-property"
|
||||
on:click={addField}
|
||||
disabled={!name}
|
||||
>
|
||||
Add field
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { GripVertical, Pen } from 'lucide-svelte'
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import { Drawer, DrawerContent } from '../common'
|
||||
import AddProperty from './AddProperty.svelte'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import Label from '../Label.svelte'
|
||||
|
||||
export let schema: Schema | undefined | any
|
||||
export let parentId: string | undefined = undefined
|
||||
|
||||
const flipDurationMs = 200
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let addProperty: AddProperty | undefined = undefined
|
||||
let schemaFormDrawer: Drawer | undefined = undefined
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = undefined
|
||||
|
||||
$: items = ((schema?.order ?? Object.keys(schema.properties ?? {}))?.map((item, index) => {
|
||||
return { value: item, id: generateRandomString() }
|
||||
}) ?? []) as Array<{
|
||||
value: string
|
||||
id: string
|
||||
}>
|
||||
|
||||
function handleConsider(e) {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
}
|
||||
|
||||
function handleFinalize(e) {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
|
||||
const keys = items.map((item) => item.value)
|
||||
|
||||
schema.properties = keys.reduce((acc, key) => {
|
||||
acc[key] = schema.properties[key]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
schema.order = keys
|
||||
|
||||
schema = { ...schema }
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
const yOffset = 49
|
||||
</script>
|
||||
|
||||
<AddProperty on:change bind:schema bind:this={addProperty} />
|
||||
|
||||
<div
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: parentId ? `app-editor-fields-${parentId}` : 'app-editor-fields'
|
||||
}}
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
class="gap-1 flex flex-col mt-2"
|
||||
>
|
||||
{#if items?.length > 0}
|
||||
{#each items as item (item.id)}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
class="w-full flex flex-col justify-between border items-center py-1 px-2 rounded-md bg-surface text-sm"
|
||||
>
|
||||
{#if schema.properties?.[item.value]}
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{`${item.value}${
|
||||
schema.properties?.[item.value]?.title
|
||||
? ` (title: ${schema.properties?.[item.value]?.title})`
|
||||
: ''
|
||||
} `}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex flex-row gap-1 item-center h-full justify-center">
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
on:click={() => {
|
||||
schemaFormDrawer?.openDrawer()
|
||||
|
||||
tick().then(() => {
|
||||
editableSchemaForm?.openField(item.value)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="cursor-move flex items-center handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if schema.properties[item.value]?.type === 'object'}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Label label="Nested Properties">
|
||||
<svelte:self
|
||||
on:change={() => (schema = schema)}
|
||||
schema={schema.properties[item.value]}
|
||||
parentId={item.value}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-tertiary"> Value is undefined </div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={schemaFormDrawer} size="1200px">
|
||||
<DrawerContent noPadding title="UI Customisation" on:close={schemaFormDrawer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<AddProperty bind:schema />
|
||||
</svelte:fragment>
|
||||
<EditableSchemaForm
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
isAppInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
lightweightMode
|
||||
dndType="drawer"
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import AddProperty from './AddProperty.svelte'
|
||||
|
||||
export let schema: Schema | undefined | any
|
||||
export let offset: number = 0
|
||||
export let uiOnly: boolean = false
|
||||
export let noPreview: boolean = false
|
||||
export let fullHeight: boolean = true
|
||||
export let lightweightMode: boolean = false
|
||||
|
||||
let addProperty: AddProperty | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class={twMerge(fullHeight ? 'h-full' : 'h-80', 'border overflow-y-auto rounded-md')}>
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty bind:schema bind:this={addProperty} />
|
||||
</div>
|
||||
<EditableSchemaForm
|
||||
bind:schema
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
{offset}
|
||||
{uiOnly}
|
||||
{noPreview}
|
||||
{lightweightMode}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import type ItemPicker from '../ItemPicker.svelte'
|
||||
import type VariableEditor from '../VariableEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ArgInput from '../ArgInput.svelte'
|
||||
import ObjectTypeNarrowing from '../ObjectTypeNarrowing.svelte'
|
||||
import Tabs from '../common/tabs/Tabs.svelte'
|
||||
import { Tab, TabContent } from '../common'
|
||||
import LightweightArgInput from '../LightweightArgInput.svelte'
|
||||
import EditableSchemaDrawer from './EditableSchemaDrawer.svelte'
|
||||
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let required = false
|
||||
export let pattern: undefined | string = undefined
|
||||
export let password = false
|
||||
export let variableEditor: VariableEditor | undefined = undefined
|
||||
export let itemPicker: ItemPicker | undefined = undefined
|
||||
export let nullable: boolean = false
|
||||
export let defaultValue: any = undefined
|
||||
export let propsNames: any = []
|
||||
export let showExpr: string | undefined = undefined
|
||||
export let extra: Record<string, any> = {}
|
||||
export let customErrorMessage: string | undefined = undefined
|
||||
export let itemsType:
|
||||
| {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object'
|
||||
contentEncoding?: 'base64'
|
||||
enum?: string[]
|
||||
multiselect?: string[]
|
||||
}
|
||||
| undefined = undefined
|
||||
export let properties: Record<string, any> = {}
|
||||
export let order: string[] = []
|
||||
export let requiredProperty: string[] = []
|
||||
export let displayWebhookWarning: boolean = true
|
||||
export let lightweightMode: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getResourceTypesFromFormat(format: string | undefined): string[] {
|
||||
if (format?.startsWith('resource-')) {
|
||||
return [format.split('-')[1]]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
let schema = { properties, order, required: requiredProperty }
|
||||
|
||||
$: if (schema) {
|
||||
properties = schema.properties
|
||||
order = schema.order
|
||||
requiredProperty = schema.required
|
||||
|
||||
dispatch('schemaChange')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if type === 'object' && format !== 'resource-s3_object'}
|
||||
<Tabs
|
||||
selected="resource"
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'custom-object') {
|
||||
format = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab value="resource">Resource</Tab>
|
||||
<Tab value="custom-object">Custom Object</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="pt-2">
|
||||
<TabContent value="custom-object">
|
||||
<EditableSchemaDrawer bind:schema />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="resource">
|
||||
<ObjectTypeNarrowing bind:format />
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
<Label label="Default">
|
||||
{#if lightweightMode}
|
||||
<LightweightArgInput
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
/>
|
||||
{:else}
|
||||
<ArgInput
|
||||
{itemPicker}
|
||||
resourceTypes={getResourceTypesFromFormat(format)}
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
{nullable}
|
||||
{variableEditor}
|
||||
compact
|
||||
noMargin
|
||||
/>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
options={{ right: 'Required' }}
|
||||
size="xs"
|
||||
disabled={type === 'boolean'}
|
||||
on:change={(event) => {
|
||||
dispatch('requiredChange', { required: event?.detail })
|
||||
}}
|
||||
checked={required}
|
||||
on:change={(event) => {
|
||||
if (event?.detail) {
|
||||
nullable = false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if type === 'string'}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Nullable',
|
||||
rightTooltip: 'If enabled, the default value will be null and not an empty string.'
|
||||
}}
|
||||
lightMode
|
||||
size="xs"
|
||||
bind:checked={nullable}
|
||||
disabled={required}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if displayWebhookWarning}
|
||||
<Alert type="info" title="Default not used by webhooks" size="xs" collapsible>
|
||||
If this flow is triggered by a webhook, the default value will not replace a missing value
|
||||
from the payload. It will only be used as the default value in the autogenerated UI. We
|
||||
recommend using default values in the signature of the steps where this value is used (using
|
||||
`x=default`) to have the desired behavior.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'Show this field only when conditions are met' }}
|
||||
checked={Boolean(showExpr)}
|
||||
on:change={() => {
|
||||
showExpr = showExpr ? undefined : 'true //fields.foo == 42'
|
||||
}}
|
||||
/>
|
||||
{#if showExpr != undefined}
|
||||
<div class="border">
|
||||
<SimpleEditor
|
||||
extraLib={`declare const fields: Record<${propsNames
|
||||
?.filter((x) => x != name)
|
||||
.map((x) => `"${x}"`)
|
||||
.join(' | ')}, any>;\n`}
|
||||
lang="javascript"
|
||||
bind:code={showExpr}
|
||||
shouldBindKey={false}
|
||||
fixedOverflowWidgets={false}
|
||||
autoHeight
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse text-2xs text-tertiary">
|
||||
<div>
|
||||
Other fields are available under <code>fields</code> (e.g:
|
||||
<code>fields.foo == 42</code>)
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import autosize from '$lib/autosize'
|
||||
import { shouldDisplayPlaceholder } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ArrayTypeNarrowing from '../ArrayTypeNarrowing.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import NumberTypeNarrowing from '../NumberTypeNarrowing.svelte'
|
||||
import StringTypeNarrowing from '../StringTypeNarrowing.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let description: string = ''
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let pattern: undefined | string = undefined
|
||||
export let enum_: string[] | undefined = undefined
|
||||
export let extra: Record<string, any> = {}
|
||||
export let minW = true
|
||||
export let customErrorMessage: string | undefined = undefined
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let properties: Record<string, any> = {}
|
||||
export let isFlowInput: boolean = false
|
||||
export let isAppInput: boolean = false
|
||||
export let order: string[] = []
|
||||
export let itemsType:
|
||||
| {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object'
|
||||
contentEncoding?: 'base64'
|
||||
enum?: string[]
|
||||
multiselect?: string[]
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
let el: HTMLTextAreaElement | undefined = undefined
|
||||
|
||||
export function focus() {
|
||||
el?.focus()
|
||||
if (el) {
|
||||
el.style.height = '5px'
|
||||
el.style.height = el.scrollHeight + 50 + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key == 'Enter') {
|
||||
return
|
||||
}
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
let schema = {
|
||||
properties,
|
||||
order
|
||||
}
|
||||
|
||||
$: (properties || order) && updateSchema()
|
||||
|
||||
function updateSchema() {
|
||||
if (!deepEqual(schema.properties, properties) || !deepEqual(schema.order, order)) {
|
||||
schema = {
|
||||
properties,
|
||||
order
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row items-center justify-between w-full gap-2">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<div class={twMerge('flex flex-col w-full', 'gap-4', minW ? 'min-w-[250px]' : '')}>
|
||||
<slot name="typeeditor" />
|
||||
|
||||
<Label label="Description">
|
||||
<textarea
|
||||
use:autosize
|
||||
rows="2"
|
||||
bind:value={description}
|
||||
on:keydown={onKeyDown}
|
||||
placeholder="Field description"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Custom Title" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>Will be displayed in the UI instead of the field name.</Tooltip>
|
||||
</svelte:fragment>
|
||||
<input bind:value={title} on:keydown={onKeyDown} placeholder="Field title" />
|
||||
</Label>
|
||||
|
||||
<Label label="Placeholder">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip light>
|
||||
Will be displayed in the input field when the field is empty. If not set, the default
|
||||
value will be used. The placeholder is disabled depending on the field typ, format, etc.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
|
||||
<textarea
|
||||
placeholder="Enter a placeholder"
|
||||
rows="1"
|
||||
bind:value={placeholder}
|
||||
disabled={!shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern, extra)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if type == 'array'}
|
||||
<ArrayTypeNarrowing bind:itemsType />
|
||||
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
|
||||
<div>
|
||||
<Label label="Field settings">
|
||||
<div>
|
||||
{#if type == 'string'}
|
||||
<StringTypeNarrowing
|
||||
bind:customErrorMessage
|
||||
bind:format
|
||||
bind:pattern
|
||||
bind:enum_
|
||||
bind:contentEncoding
|
||||
bind:password={extra['password']}
|
||||
bind:minRows={extra['minRows']}
|
||||
bind:disableCreate={extra['disableCreate']}
|
||||
bind:disableVariablePicker={extra['disableVariablePicker']}
|
||||
bind:dateFormat={extra['dateFormat']}
|
||||
/>
|
||||
{:else if type == 'number' || type == 'integer'}
|
||||
<NumberTypeNarrowing
|
||||
bind:min={extra['min']}
|
||||
bind:max={extra['max']}
|
||||
bind:currency={extra['currency']}
|
||||
bind:currencyLocale={extra['currencyLocale']}
|
||||
/>
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm on:change noPreview bind:schema uiOnly jsonEnabled={false} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-xs">No settings available for this field type</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { dragHandle } from '@windmill-labs/svelte-dnd-action'
|
||||
import SchemaForm from '../SchemaForm.svelte'
|
||||
import { GripVertical } from 'lucide-svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let dndType: string | undefined = undefined
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> = {}
|
||||
export let prettifyHeader: boolean = false
|
||||
export let lightweightMode: boolean = false
|
||||
export let onlyMaskPassword: boolean = false
|
||||
export let disablePortal: boolean = false
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 200
|
||||
|
||||
let items = computeItems()
|
||||
|
||||
let dragDisabled = true
|
||||
$: schema && dragDisabled && updateItems()
|
||||
|
||||
function computeItems() {
|
||||
return (
|
||||
(schema.order ?? Object.keys(schema.properties ?? {}) ?? []).map((key) => ({
|
||||
id: key,
|
||||
value: key
|
||||
})) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
function updateItems() {
|
||||
const newItems = computeItems()
|
||||
if (!deepEqual(newItems, items)) {
|
||||
items = newItems
|
||||
}
|
||||
}
|
||||
|
||||
function handleConsider(e) {
|
||||
dragDisabled = false
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
items = newItems
|
||||
}
|
||||
|
||||
function handleFinalize(e) {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
dragDisabled = true
|
||||
items = newItems
|
||||
|
||||
dispatch(
|
||||
'reorder',
|
||||
items.map((item) => item.value)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(schema)}
|
||||
{dragDisabled} -->
|
||||
<!-- {JSON.stringify(items)} -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<SchemaForm
|
||||
on:click
|
||||
on:change
|
||||
on:reorder
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
{lightweightMode}
|
||||
{args}
|
||||
{prettifyHeader}
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
bind:schema
|
||||
dndConfig={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: dndType ?? 'top-level'
|
||||
}}
|
||||
{items}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="w-4 h-8 cursor-move ml-2 handle" aria-label="drag-handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaForm>
|
||||
@@ -135,7 +135,6 @@ export async function inferArgs(
|
||||
schema.required = []
|
||||
const oldProperties = JSON.parse(JSON.stringify(schema.properties))
|
||||
schema.properties = {}
|
||||
|
||||
for (const arg of inferedSchema.args) {
|
||||
if (!(arg.name in oldProperties)) {
|
||||
schema.properties[arg.name] = { description: '', type: '' }
|
||||
|
||||
@@ -66,6 +66,34 @@ export function argSigToJsonSchemaType(
|
||||
newS.type = 'object'
|
||||
}
|
||||
|
||||
const preservedFields = [
|
||||
'description',
|
||||
'pattern',
|
||||
'default',
|
||||
'enum',
|
||||
'format',
|
||||
'items',
|
||||
'min',
|
||||
'max',
|
||||
'currency',
|
||||
'currencyLocale',
|
||||
'multiselect',
|
||||
'customErrorMessage',
|
||||
'required',
|
||||
'showExpr',
|
||||
'password',
|
||||
'order',
|
||||
'nullable',
|
||||
'dateFormat',
|
||||
'title',
|
||||
'placeholder'
|
||||
]
|
||||
preservedFields.forEach((field) => {
|
||||
if (oldS[field] !== undefined) {
|
||||
newS[field] = oldS[field]
|
||||
}
|
||||
})
|
||||
|
||||
if (oldS.type != newS.type) {
|
||||
for (const prop of Object.getOwnPropertyNames(newS)) {
|
||||
if (prop != 'description') {
|
||||
|
||||
@@ -881,7 +881,8 @@ export function shouldDisplayPlaceholder(
|
||||
format: string | undefined,
|
||||
enum_: string[] | undefined,
|
||||
contentEncoding: 'base64' | 'binary' | undefined,
|
||||
pattern: string | undefined
|
||||
pattern: string | undefined,
|
||||
extra: Record<string, any> | undefined
|
||||
): boolean {
|
||||
if (type === 'string') {
|
||||
const kind = computeKind(enum_, contentEncoding, pattern, format)
|
||||
@@ -894,5 +895,9 @@ export function shouldDisplayPlaceholder(
|
||||
return kind === 'none' || kind === 'pattern'
|
||||
}
|
||||
|
||||
return type === 'number' || type === 'integer' || type === undefined
|
||||
if (type === 'number' || type === 'integer') {
|
||||
return extra?.['min'] === undefined || extra?.['max'] === undefined
|
||||
}
|
||||
|
||||
return type === undefined
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
|
||||
import { resourceTypesStore } from '$lib/components/resourceTypesStore'
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
@@ -31,7 +30,7 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import type { ResourceType } from '$lib/gen'
|
||||
import { OauthService, ResourceService, type ListableResource } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { canWrite, classNames, emptySchema, removeMarkdown, truncate } from '$lib/utils'
|
||||
import { convert } from '@redocly/json-to-json-schema'
|
||||
@@ -50,6 +49,7 @@
|
||||
} from 'lucide-svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import EditableSchemaWrapper from '$lib/components/schema/EditableSchemaWrapper.svelte'
|
||||
|
||||
type ResourceW = ListableResource & { canWrite: boolean; marked?: string }
|
||||
type ResourceTypeW = ResourceType & { canWrite: boolean }
|
||||
@@ -414,13 +414,13 @@
|
||||
>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold text-secondary">Schema</div>
|
||||
<SchemaEditor bind:schema={editResourceType.schema} />
|
||||
<EditableSchemaWrapper bind:schema={editResourceType.schema} noPreview />
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={resourceTypeDrawer} size="800px">
|
||||
<Drawer bind:this={resourceTypeDrawer} size="1200px">
|
||||
<DrawerContent title="Create resource type" on:close={resourceTypeDrawer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button startIcon={{ icon: Save }} on:click={addResourceType}>Save</Button>
|
||||
@@ -479,8 +479,8 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SchemaEditor bind:schema={newResourceType.schema} />
|
||||
</div>
|
||||
<EditableSchemaWrapper bind:schema={newResourceType.schema} fullHeight />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
Reference in New Issue
Block a user