mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
feat(frontend): App Schema Form component (#1533)
* feat(frontend): app static form * feat(frontend): working schemaForm * feat(frontend): add lightmode * feat(frontend): sync values direclty * feat(frontend): remove dev code
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let lightMode: boolean = false
|
||||
export let schema: Schema = emptySchema()
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
@@ -162,7 +163,7 @@
|
||||
<Button
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="sm"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
@@ -179,6 +180,7 @@
|
||||
options={{
|
||||
right: 'As JSON'
|
||||
}}
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<Tooltip
|
||||
@@ -199,9 +201,11 @@
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
<th>Required</th>
|
||||
{#if !lightMode}
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
<th>Required</th>
|
||||
{/if}
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
@@ -211,45 +215,50 @@
|
||||
<td>
|
||||
<SchemaEditorProperty {property} />
|
||||
</td>
|
||||
<td>{property.description ?? ''}</td>
|
||||
<td>{property.default ? JSON.stringify(property.default) : ''}</td>
|
||||
<td
|
||||
>{#if schema.required.includes(name)}
|
||||
<span class="text-red-600 font-bold text-lg">*</span>
|
||||
{/if}</td
|
||||
>
|
||||
{#if !lightMode}
|
||||
<td>{property.description ?? ''}</td>
|
||||
<td>{property.default ? JSON.stringify(property.default) : ''}</td>
|
||||
<td>
|
||||
{#if schema.required.includes(name)}
|
||||
<span class="text-red-600 font-bold text-lg">*</span>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td class="justify-end flex">
|
||||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => changePosition(i, true)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>
|
||||
↑</button
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
{/if}
|
||||
{#if i < Object.keys(schema.properties).length - 1}
|
||||
<button
|
||||
on:click={() => changePosition(i, false)}
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">↓</button
|
||||
>
|
||||
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
|
||||
>↓
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
variant="border"
|
||||
btnClasses="mx-2"
|
||||
size="sm"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => handleDeleteArgument(name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
size={lightMode ? 'xs' : 'sm'}
|
||||
startIcon={{ icon: faPen }}
|
||||
on:click={() => startEditArgument(name)}
|
||||
iconOnly={lightMode}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { initOutput, selectId } from '../../editor/appUtils'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let initializing: boolean | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { worldStore, connectingInput, app, selectedComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
loading: false,
|
||||
values: {}
|
||||
})
|
||||
|
||||
let result: Schema | undefined = undefined
|
||||
let args: Record<string, unknown> = {}
|
||||
|
||||
function handleArgsChange() {
|
||||
const newArgs: Record<string, unknown> = {}
|
||||
|
||||
for (const key in args) {
|
||||
if (result?.properties[key]) {
|
||||
newArgs[key] = args[key]
|
||||
}
|
||||
}
|
||||
|
||||
outputs.values.set(newArgs, true)
|
||||
}
|
||||
|
||||
$: args && handleArgsChange()
|
||||
</script>
|
||||
|
||||
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
|
||||
{#if result && Object.keys(result.properties).length > 0}
|
||||
<div
|
||||
class="m-2"
|
||||
on:pointerdown|stopPropagation={(e) =>
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
>
|
||||
<LightweightSchemaForm schema={result} bind:args />
|
||||
</div>
|
||||
{:else}
|
||||
<p class="m-2 italic"> Empty form </p>
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
@@ -39,6 +39,7 @@
|
||||
AppPdf
|
||||
} from '../../components'
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
import AppSchemaForm from '../../components/buttons/AppSchemaForm.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -460,6 +461,13 @@
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'schemaformcomponent'}
|
||||
<AppSchemaForm
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
{initializing}
|
||||
{render}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ export type HorizontalSplitPanesComponent = BaseComponent<'horizontalsplitpanesc
|
||||
panes: number[]
|
||||
}
|
||||
export type PdfComponent = BaseComponent<'pdfcomponent'>
|
||||
export type Schemaformcomponent = BaseComponent<'schemaformcomponent'>
|
||||
|
||||
export type TypedComponent =
|
||||
| DisplayComponent
|
||||
@@ -146,6 +147,7 @@ export type TypedComponent =
|
||||
| VerticalSplitPanesComponent
|
||||
| HorizontalSplitPanesComponent
|
||||
| PdfComponent
|
||||
| Schemaformcomponent
|
||||
|
||||
export type AppComponent = BaseAppComponent & TypedComponent
|
||||
|
||||
@@ -1656,6 +1658,27 @@ Hello \${ctx.username}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
schemaformcomponent: {
|
||||
name: 'Schema Form',
|
||||
icon: FileText,
|
||||
dims: '3:8-8:12' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { class: '', style: '' }
|
||||
},
|
||||
initialData: {
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'schema',
|
||||
value: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {},
|
||||
required: [],
|
||||
type: 'object'
|
||||
}
|
||||
},
|
||||
configuration: {}
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ const inputs: ComponentSet = {
|
||||
'checkboxcomponent',
|
||||
'selectcomponent',
|
||||
'resourceselectcomponent',
|
||||
'multiselectcomponent'
|
||||
'multiselectcomponent',
|
||||
'schemaformcomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
|
||||
@@ -550,6 +550,10 @@ export const quickStyleProperties: Record<
|
||||
pdfcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
schemaformcomponent: {
|
||||
container: containerDefaultProps,
|
||||
button: buttonDefaultProps
|
||||
},
|
||||
formcomponent: {
|
||||
container: containerDefaultProps,
|
||||
button: buttonDefaultProps
|
||||
|
||||
+5
@@ -11,6 +11,7 @@
|
||||
import { DollarSign } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
|
||||
export let componentInput: StaticInput<any> | undefined
|
||||
export let fieldType: InputType | undefined = undefined
|
||||
@@ -110,6 +111,10 @@
|
||||
{/if}
|
||||
{:else if fieldType === 'array'}
|
||||
<ArrayStaticInputEditor {subFieldType} bind:componentInput on:deleteArrayItem />
|
||||
{:else if fieldType === 'schema'}
|
||||
<div class="w-full">
|
||||
<SchemaEditor bind:schema={componentInput.value} lightMode />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<input
|
||||
|
||||
-2
@@ -23,7 +23,6 @@
|
||||
{isFrontend}
|
||||
{triggerEvents}
|
||||
{shoudlDisplayChangeEvents}
|
||||
isTriggerable={isTriggerable(appComponent.type)}
|
||||
/>
|
||||
{:else}
|
||||
<ScriptTriggers
|
||||
@@ -31,6 +30,5 @@
|
||||
{triggerEvents}
|
||||
{isFrontend}
|
||||
{shoudlDisplayChangeEvents}
|
||||
isTriggerable={isTriggerable(appComponent.type)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
+1
-2
@@ -13,7 +13,6 @@
|
||||
export let isFrontend: boolean = false
|
||||
export let dependencies: string[] = []
|
||||
export let shoudlDisplayChangeEvents: boolean = false
|
||||
export let isTriggerable: boolean = false
|
||||
|
||||
$: changeEvents = isFrontend
|
||||
? inlineScript?.refreshOn
|
||||
@@ -57,7 +56,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isFrontend && !isTriggerable}
|
||||
{#if isFrontend && shoudlDisplayChangeEvents}
|
||||
<div class="flex mb-4">
|
||||
<Button
|
||||
size="xs2"
|
||||
|
||||
@@ -20,6 +20,7 @@ export type InputType =
|
||||
| 'labeledresource'
|
||||
| 'labeledselect'
|
||||
| 'tab-select'
|
||||
| 'schema'
|
||||
|
||||
// Connection to an output of another component
|
||||
// defined by the id of the component and the path of the output
|
||||
@@ -160,6 +161,7 @@ export type AppInput =
|
||||
| AppInputSpec<'labeledselect', object>
|
||||
| AppInputSpec<'labeledresource', object>
|
||||
| AppInputSpec<'array', object[], 'tab-select'>
|
||||
| AppInputSpec<'schema', object>
|
||||
|
||||
export type RowAppInput = Extract<AppInput, { type: 'row' }>
|
||||
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
|
||||
|
||||
Reference in New Issue
Block a user