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:
Faton Ramadani
2024-06-12 18:37:18 +02:00
committed by GitHub
parent f634e6ff48
commit 402e7eaa9c
41 changed files with 2190 additions and 2196 deletions
+7 -19
View File
@@ -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(),
))
}
+12 -9
View File
@@ -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",
+4 -4
View File
@@ -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",
+1 -1
View File
@@ -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'
}
+53 -136
View File
@@ -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"
>
&uparrow;</button
>
{/if}
{#if editableSchema.i < editableSchema.total - 1}
<button
on:click={() => changePosition(editableSchema?.i ?? 0, false)}
class="text-lg mr-2">&downarrow;</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' : ''}"
>
&uparrow;</button
>
{/if}
{#if displayInfo.index < displayInfo.propertiesNumber - 1}
<button
on:click={() => changePosition(displayInfo.index, false)}
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">&downarrow;</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>
+47 -282
View File
@@ -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>
+167 -82
View File
@@ -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>
+6 -3
View File
@@ -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}
@@ -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}
/>
@@ -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>
-1
View File
@@ -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: '' }
+28
View File
@@ -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') {
+7 -2
View File
@@ -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>