feat(frontend): add support for background scripts + add FormButtonCo… (#1124)

* feat(frontend): add support for background scripts + add FormButtonComponnet

* feat(frontend): add background script fields

* feat(frontend): Handle deletion + display of results

* feat(frontend): Fix background script ids: Runnable ids needs to be valid js expression, because they are used in the evaluator.

* feat(frontend): add a portal to display modal

* feat(frontend): fix build

* feat(frontend): Add missing props

* feat(frontend): Fix run button + add tooltip + fix icon
This commit is contained in:
Faton Ramadani
2023-01-19 14:52:32 +01:00
committed by GitHub
parent e371fda1ce
commit e969af9e44
21 changed files with 644 additions and 157 deletions
+11
View File
@@ -24,6 +24,7 @@
"monaco-yaml": "^4.0.2",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
"svelte-portal": "^2.2.0",
"svelte-select": "^5.0.2",
"vscode-ws-jsonrpc": "^2.0.1"
},
@@ -6256,6 +6257,11 @@
"svelte": ">=3"
}
},
"node_modules/svelte-portal": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/svelte-portal/-/svelte-portal-2.2.0.tgz",
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"node_modules/svelte-preprocess": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
@@ -11548,6 +11554,11 @@
"dev": true,
"requires": {}
},
"svelte-portal": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/svelte-portal/-/svelte-portal-2.2.0.tgz",
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"svelte-preprocess": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
+2 -1
View File
@@ -21,8 +21,8 @@
"@sveltejs/package": "^1.0.1",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/vscode": "~1.74.0",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelvet": "^4.0.20",
@@ -77,6 +77,7 @@
"monaco-yaml": "^4.0.2",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
"svelte-portal": "^2.2.0",
"svelte-select": "^5.0.2",
"vscode-ws-jsonrpc": "^2.0.1"
},
@@ -0,0 +1,28 @@
<script lang="ts">
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
import type { InlineScript } from '../types'
import RunnableComponent from './helpers/RunnableComponent.svelte'
export let id: string
export let name: string
export let inlineScript: InlineScript | undefined
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableComponent
{id}
bind:fields
bind:result
runnable={{
name,
inlineScript,
type: 'runnableByName'
}}
wrapperClass="hidden"
>
<slot />
</RunnableComponent>
@@ -86,11 +86,11 @@
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={(e) => {
on:click={async (e) => {
e?.stopPropagation()
e?.preventDefault()
ownClick = true
runnableComponent?.runComponent()
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
@@ -0,0 +1,159 @@
<script lang="ts">
import { Button, type ButtonType } from '$lib/components/common'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import Portal from 'svelte-portal'
import Modal from '$lib/components/common/modal/Modal.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let recomputeIds: string[] | undefined = undefined
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['loading', 'result']
const { runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
let size: ButtonType.Size
let runnableComponent: RunnableComponent
let disabled: boolean | undefined = undefined
let isLoading: boolean = false
let ownClick: boolean = false
let errors: Record<string, string> = {}
$: errorsMessage = Object.values(errors)
.filter((x) => x != '')
.join('\n')
$: noInputs =
componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
$: outputs?.loading.subscribe({
next: (value) => {
isLoading = value
if (ownClick && !value) {
ownClick = false
}
}
})
$: loading = isLoading && ownClick
let open: boolean = false
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue {id} input={configuration.color} bind:value={color} />
<InputValue {id} input={configuration.size} bind:value={size} />
<InputValue
row={extraQueryParams['row']}
{id}
input={configuration.disabled}
bind:value={disabled}
bind:error={errors.disabled}
/>
<Portal>
<Modal
{open}
title={labelValue}
on:canceled={() => {
open = false
}}
on:confirmed={() => {
open = false
}}
>
<RunnableWrapper
defaultUserInput
noMinH
bind:runnableComponent
bind:componentInput
{id}
{extraQueryParams}
autoRefresh={false}
forceSchemaDisplay={true}
runnableClass="!block"
>
<div class="flex flex-col gap-2 px-4 w-full">
<div>
{#if noInputs}
<div class="text-gray-600 italic text-sm my-4">
Run forms are associated with a runnable that has user inputs.
<br />
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
<strong>
User Input
<Icon data={faUser} scale={1.3} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
</strong>
</div>
{/if}
</div>
<div class="flex justify-end">
<Button
{loading}
btnClasses="my-1"
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={async () => {
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
open = false
}}
size="xs"
color="dark"
>
Submit
</Button>
</div>
</div>
</RunnableWrapper>
</Modal>
</Portal>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if errorsMessage}
<div class="text-red-500 text-xs">{errorsMessage}</div>
{/if}
<Button
{disabled}
{size}
{color}
on:click={(e) => {
open = true
}}
>
{labelValue}
</Button>
</AlignWrapper>
@@ -19,6 +19,7 @@
import AppTimeseries from '../components/dataDisplay/AppTimeseries.svelte'
import AppHtml from '../components/dataDisplay/AppHtml.svelte'
import AppSliderInputs from '../components/numberInputs/AppSliderInputs.svelte'
import AppFormButton from '../components/form/AppFormButton.svelte'
import VegaLiteHtml from '../components/dataDisplay/VegaLiteHtml.svelte'
export let component: AppComponent
@@ -124,6 +125,12 @@
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'formbuttoncomponent'}
<AppFormButton
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'checkboxcomponent'}
<CheckboxComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'textinputcomponent'}
@@ -8,6 +8,7 @@
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
import type { Policy } from '$lib/gen'
import HiddenComponent from '../components/HiddenComponent.svelte'
export let policy: Policy
@@ -176,6 +177,18 @@
</div>
</div>
{#each $app.hiddenInlineScripts as script, index}
{#if script}
<HiddenComponent
id={`bg_${index}`}
inlineScript={script.inlineScript}
name={script.name}
bind:fields={script.fields}
bind:staticOutputs={$staticOutputs[`bg_${index}`]}
/>
{/if}
{/each}
<style>
:global(.svlt-grid-shadow) {
/* Back shadow */
@@ -1,10 +1,12 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext } from '../types'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
import TablePanel from './TablePanel.svelte'
const { selectedComponent, lazyGrid } = getContext<AppEditorContext>('AppEditorContext')
const { selectedComponent, lazyGrid, app } = getContext<AppEditorContext>('AppEditorContext')
</script>
{#each $lazyGrid as gridItem (gridItem.data.id)}
@@ -14,3 +16,16 @@
<TablePanel bind:component={gridItem.data} />
{/if}
{/each}
{#each $app.hiddenInlineScripts as script, index (script.name)}
{#if $selectedComponent === `bg_${index}`}
<PanelSection title={`Background script inputs`}>
<InputsSpecsEditor
id={`bg_${index}`}
shouldCapitalize={false}
bind:inputSpecs={script.fields}
userInputEnabled={false}
/>
</PanelSection>
{/if}
{/each}
@@ -15,8 +15,8 @@ const inputs: ComponentSet = {
type: 'static',
value: 'Type...',
fieldType: 'text',
onlyStatic: true,
},
onlyStatic: true
}
},
card: false
},
@@ -31,8 +31,8 @@ const inputs: ComponentSet = {
type: 'static',
value: 'Password',
fieldType: 'text',
onlyStatic: true,
},
onlyStatic: true
}
},
card: false
},
@@ -47,8 +47,8 @@ const inputs: ComponentSet = {
type: 'static',
value: 'Type...',
fieldType: 'text',
onlyStatic: true,
},
onlyStatic: true
}
},
card: false
},
@@ -63,14 +63,14 @@ const inputs: ComponentSet = {
type: 'static',
value: 0,
fieldType: 'number',
onlyStatic: true,
onlyStatic: true
},
max: {
type: 'static',
value: 42,
fieldType: 'number',
onlyStatic: true,
},
onlyStatic: true
}
},
card: false
},
@@ -84,15 +84,15 @@ const inputs: ComponentSet = {
minDate: {
type: 'static',
value: '',
fieldType: 'date',
fieldType: 'date'
},
maxDate: {
type: 'static',
value: '',
fieldType: 'date',
fieldType: 'date'
}
},
card: false,
card: false
},
{
...defaultAlignement,
@@ -104,7 +104,7 @@ const inputs: ComponentSet = {
label: {
type: 'static',
value: 'Label',
fieldType: 'text',
fieldType: 'text'
}
},
card: false
@@ -148,8 +148,7 @@ const buttons: ComponentSet = {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined,
runnable: undefined
},
recomputeIds: undefined,
configuration: {
@@ -189,29 +188,64 @@ const buttons: ComponentSet = {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined,
runnable: undefined
},
recomputeIds: undefined,
configuration: {
label: {
type: 'static',
value: 'Submit',
fieldType: 'text',
fieldType: 'text'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions',
optionValuesKey: 'buttonColorOptions'
},
size: {
fieldType: 'select',
type: 'static',
value: 'xs',
onlyStatic: true,
optionValuesKey: 'buttonSizeOptions',
optionValuesKey: 'buttonSizeOptions'
}
},
card: true
},
{
horizontalAlignment: 'center',
verticalAlignment: 'center',
id: 'formbuttoncomponent',
type: 'formbuttoncomponent',
componentInput: {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined
},
recomputeIds: undefined,
configuration: {
label: {
type: 'static',
value: 'Open popup',
fieldType: 'text'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions'
},
size: {
fieldType: 'select',
type: 'static',
value: 'xs',
onlyStatic: true,
optionValuesKey: 'buttonSizeOptions'
}
},
@@ -235,7 +269,7 @@ const display: ComponentSet = {
>
<h1 class="absolute top-4 left-2 text-white">
Hello \${ctx.username}
</h1>`,
</h1>`
},
configuration: {},
card: false
@@ -253,7 +287,7 @@ const display: ComponentSet = {
{ a: 'A', b: 28 },
{ a: 'B', b: 55 },
{ a: 'C', b: 43 },
{ a: 'D', b: 91 },
{ a: 'D', b: 91 }
]
},
mark: 'bar',
@@ -269,9 +303,8 @@ const display: ComponentSet = {
onlyStatic: true,
fieldType: 'boolean',
value: false,
tooltip: "use the canvas renderer instead of the svg one for more interactive plots"
tooltip: 'use the canvas renderer instead of the svg one for more interactive plots'
}
},
card: false
},
@@ -284,7 +317,7 @@ const display: ComponentSet = {
componentInput: {
type: 'static',
fieldType: 'template',
value: 'Hello ${ctx.username}',
value: 'Hello ${ctx.username}'
},
configuration: {
style: {
@@ -298,19 +331,18 @@ const display: ComponentSet = {
type: 'static',
fieldType: 'text',
value: '',
tooltip: 'CSS rules like "color: blue;"',
tooltip: 'CSS rules like "color: blue;"'
},
copyButton: {
type: 'static',
value: false,
fieldType: 'boolean',
onlyStatic: true,
},
onlyStatic: true
}
},
card: false
},
{
id: 'tablecomponent',
type: 'tablecomponent',
configuration: {
@@ -337,8 +369,7 @@ const display: ComponentSet = {
name: 'A briefer cell',
age: 84
}
],
]
},
card: true,
actionButtons: []
@@ -385,7 +416,6 @@ const display: ComponentSet = {
fieldType: 'boolean',
value: false
}
},
componentInput: {
type: 'static',
@@ -409,30 +439,32 @@ const display: ComponentSet = {
onlyStatic: true,
fieldType: 'boolean',
value: false
},
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [{
label: 'foo',
data: [
{ x: 25, y: 50 },
{ x: 23, y: 23 },
{ x: 12, y: 37 }
],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [
{ x: 32, y: 32 },
{ x: 25, y: 42 },
{ x: 3, y: 27 }
],
backgroundColor: 'orange'
}]
value: [
{
label: 'foo',
data: [
{ x: 25, y: 50 },
{ x: 23, y: 23 },
{ x: 12, y: 37 }
],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [
{ x: 32, y: 32 },
{ x: 25, y: 42 },
{ x: 3, y: 27 }
],
backgroundColor: 'orange'
}
]
},
card: true
},
@@ -457,40 +489,50 @@ const display: ComponentSet = {
onlyStatic: true,
fieldType: 'boolean',
value: false
},
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [{
label: 'foo',
data: [{
x: '2021-11-06 23:39:30',
y: 50
}, {
x: '2021-11-07 01:00:28',
y: 60
}, {
x: '2021-11-07 09:00:28',
y: 20
}],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [{
x: '2021-11-06 23:39:30',
y: 20
}, {
x: '2021-11-07 01:00:28',
y: 13
}, {
x: '2021-11-07 09:00:28',
y: 45
}],
backgroundColor: 'orange'
}]
value: [
{
label: 'foo',
data: [
{
x: '2021-11-06 23:39:30',
y: 50
},
{
x: '2021-11-07 01:00:28',
y: 60
},
{
x: '2021-11-07 09:00:28',
y: 20
}
],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [
{
x: '2021-11-06 23:39:30',
y: 20
},
{
x: '2021-11-07 01:00:28',
y: 13
},
{
x: '2021-11-07 09:00:28',
y: 45
}
],
backgroundColor: 'orange'
}
]
},
card: true
},
@@ -500,13 +542,11 @@ const display: ComponentSet = {
componentInput: {
type: 'static',
fieldType: 'object',
value: { "foo": 42 },
value: { foo: 42 }
},
configuration: {},
card: false
},
}
]
}
@@ -34,6 +34,8 @@
return displayData[component?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {
return 'Background'
} else {
return 'Table action'
}
@@ -82,7 +82,7 @@
>
Format&nbsp;<Tooltip>Ctrl+S</Tooltip>
</Button>
{#if id.startsWith('unused-')}
{#if id.startsWith('unused-') || id.startsWith('bg_')}
<Button
size="xs"
color="light"
@@ -91,7 +91,8 @@
startIcon={{ icon: faTrash }}
on:click={() => dispatch('delete')}
/>
{:else if $runnableComponents[id] != undefined}
{/if}
{#if $runnableComponents[id] != undefined}
<Button
loading={runLoading}
size="xs"
@@ -6,8 +6,9 @@
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
import InlineScriptEditorPanel from './InlineScriptEditorPanel.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
import EmptyInlineScript from './EmptyInlineScript.svelte'
const { lazyGrid, app } = getContext<AppEditorContext>('AppEditorContext')
const { lazyGrid, app, staticOutputs } = getContext<AppEditorContext>('AppEditorContext')
let selectedScriptComponentId: string | undefined = undefined
</script>
@@ -51,6 +52,33 @@
/>
{/if}
{/each}
{#each $app.hiddenInlineScripts as hiddenInlineScript, index (index)}
{#if `bg_${index}` === selectedScriptComponentId}
{#if hiddenInlineScript.inlineScript}
<InlineScriptEditor
id={`bg_${index}`}
bind:inlineScript={hiddenInlineScript.inlineScript}
bind:name={hiddenInlineScript.name}
on:delete={() => {
// remove the script from the array at the index
$app.hiddenInlineScripts.splice(index, 1)
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
delete $staticOutputs[`bg_${index}`]
$staticOutputs = $staticOutputs
}}
/>
{:else}
<EmptyInlineScript
id={`b_${index}`}
name={hiddenInlineScript.name}
on:new={(e) => {
hiddenInlineScript.inlineScript = e.detail
}}
/>
{/if}
{/if}
{/each}
</Pane>
</Splitpanes>
</SplitPanesWrapper>
@@ -1,8 +1,11 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import { Badge, Button } from '$lib/components/common'
import Tooltip from '$lib/components/Tooltip.svelte'
import { classNames } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import { getAllScriptNames } from '../../utils'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import { getAppScripts } from './utils'
@@ -11,7 +14,7 @@
function selectInlineScript(id: string) {
selectedScriptComponentId = id
if (!id.startsWith('unused-')) {
if (!id.startsWith('unused-') || !id.startsWith('bg_')) {
$selectedComponent = selectedScriptComponentId
}
}
@@ -24,6 +27,29 @@
selectedScriptComponentId = $selectedComponent
}
}
function createBackgroundScript() {
let index = 0
let newScriptPath = `Background Script ${index}`
const names = getAllScriptNames($app)
// Find a name that is not used by any other inline script
while (names.includes(newScriptPath)) {
newScriptPath = `Background Script ${++index}`
}
if (!$app.hiddenInlineScripts) {
$app.hiddenInlineScripts = []
}
$app.hiddenInlineScripts.push({
name: newScriptPath,
inlineScript: undefined,
fields: {}
})
$app.hiddenInlineScripts = $app.hiddenInlineScripts
}
</script>
<div class="min-h-full flex flex-col gap-4">
@@ -96,4 +122,37 @@
{/if}
</div>
</PanelSection>
<PanelSection title="Background scripts" smallPadding>
<svelte:fragment slot="action">
<Button size="xs" color="dark" startIcon={{ icon: faPlus }} on:click={createBackgroundScript}>
Add
</Button>
<Tooltip>
Background scripts are triggered upon global refresh or when their input changes. The result
of a background script can be shared among many components.
</Tooltip>
</svelte:fragment>
<div class="flex flex-col gap-2 w-full">
{#if $app.hiddenInlineScripts?.length > 0}
<div class="flex gap-2 flex-col ">
{#each $app.hiddenInlineScripts as { name }, index (index)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedScriptComponentId === `bg_${index}` ? 'bg-blue-100 text-blue-600' : ''
)},"
on:click={() => selectInlineScript(`bg_${index}`)}
>
<span class="text-xs truncate">{name}</span>
<Badge color="yellow">Background</Badge>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-gray-500">No items</div>
{/if}
</div>
</PanelSection>
</div>
@@ -163,7 +163,7 @@
{/if}
<AlignmentEditor bind:component />
{#if component.type === 'buttoncomponent' || component.type === 'formcomponent'}
{#if component.type === 'buttoncomponent' || component.type === 'formcomponent' || component.type === 'formbuttoncomponent'}
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
{/if}
@@ -10,7 +10,7 @@
import type { AppEditorContext, GridItem, InlineScript } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import type { Schema } from '$lib/common'
import { loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
import { getAllScriptNames, loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
import { emptySchema } from '$lib/utils'
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
@@ -94,33 +94,10 @@
let index = 0
let newScriptPath = `Inline Script ${index}`
const names = $app.grid.reduce((acc, gridItem: GridItem) => {
const { componentInput } = gridItem.data
if (
componentInput?.type === 'runnable' &&
componentInput?.runnable?.type === 'runnableByName'
) {
acc.push(componentInput.runnable.name)
}
if (componentInput?.type === 'tablecomponent') {
componentInput.actionButtons.forEach((actionButton) => {
if (actionButton.componentInput?.type === 'runnable') {
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
acc.push(actionButton.componentInput.runnable.name)
}
}
})
}
return acc
}, [] as string[])
const unusedNames = $app.unusedInlineScripts.map((x) => x.name)
const names = getAllScriptNames($app)
// Find a name that is not used by any other inline script
while (names.includes(newScriptPath) || unusedNames.includes(newScriptPath)) {
while (names.includes(newScriptPath)) {
newScriptPath = `Inline Script ${++index}`
}
+27 -14
View File
@@ -34,6 +34,10 @@ export type FormComponent = BaseComponent<'formcomponent'> & {
recomputeIds: string[] | undefined
}
export type FormButtonComponent = BaseComponent<'formbuttoncomponent'> & {
recomputeIds: string[] | undefined
}
export type RunFormComponent = BaseComponent<'runformcomponent'>
export type BarChartComponent = BaseComponent<'barchartcomponent'>
export type PieChartComponent = BaseComponent<'piechartcomponent'>
@@ -60,11 +64,14 @@ export type Aligned = {
export interface BaseAppComponent extends Partial<Aligned> {
id: ComponentID
componentInput: AppInput | undefined
configuration: Record<string, (StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput | EvalAppInput) & {
onlyStatic?: boolean,
evaluatedValue?: boolean,
tooltip?: string
}>
configuration: Record<
string,
(StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput | EvalAppInput) & {
onlyStatic?: boolean
evaluatedValue?: boolean
tooltip?: string
}
>
card: boolean | undefined
/**
* If `true` then the wrapper will allow items to flow outside of it's borders.
@@ -98,6 +105,7 @@ export type AppComponent = BaseAppComponent &
| CheckboxComponent
| RadioComponent
| FormComponent
| FormButtonComponent
| VegaLiteComponent
)
@@ -132,6 +140,11 @@ export type App = {
name: string
inlineScript: InlineScript
}>
hiddenInlineScripts: Array<{
name: string
inlineScript: InlineScript | undefined
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
}>
}
export type ConnectingInput = {
@@ -143,21 +156,21 @@ export type ConnectingInput = {
export type AppEditorContext = {
worldStore: Writable<World | undefined>
staticOutputs: Writable<Record<string, string[]>>,
lazyGrid: Writable<GridItem[]>,
app: Writable<App>,
summary: Writable<string>,
staticOutputs: Writable<Record<string, string[]>>
lazyGrid: Writable<GridItem[]>
app: Writable<App>
summary: Writable<string>
selectedComponent: Writable<string | undefined>
mode: Writable<EditorMode>
connectingInput: Writable<ConnectingInput>
breakpoint: Writable<EditorBreakpoint>
runnableComponents: Writable<Record<string, () => Promise<void>>>
staticExporter: Writable<Record<string, () => any>>
appPath: string,
workspace: string,
onchange: (() => void) | undefined,
isEditor: boolean,
jobs: Writable<{ job: string, component: string }[]>,
appPath: string
workspace: string
onchange: (() => void) | undefined
isEditor: boolean
jobs: Writable<{ job: string; component: string }[]>
noBackend: boolean
}
+66 -17
View File
@@ -21,11 +21,12 @@ import {
ToggleLeft,
GripHorizontal,
Code2,
SlidersHorizontal
SlidersHorizontal,
PlusSquare
} from 'lucide-svelte'
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
import type { Output } from './rx'
import type { App, AppComponent } from './types'
import type { App, AppComponent, GridItem } from './types'
export async function loadSchema(
workspace: string,
@@ -50,7 +51,11 @@ export async function loadSchema(
const script = await ScriptService.getHubScriptByPath({
path
})
if (script.schema == undefined || Object.keys(script.schema).length == 0 || typeof script.schema != 'object') {
if (
script.schema == undefined ||
Object.keys(script.schema).length == 0 ||
typeof script.schema != 'object'
) {
script.schema = emptySchema()
}
@@ -59,14 +64,16 @@ export async function loadSchema(
}
}
export function schemaToInputsSpec(schema: Schema, defaultUserInput: boolean): Record<string, StaticAppInput> {
export function schemaToInputsSpec(
schema: Schema,
defaultUserInput: boolean
): Record<string, StaticAppInput> {
if (schema?.properties == undefined) {
return {}
}
return Object.keys(schema.properties).reduce((accu, key) => {
const property = schema.properties[key]
accu[key] = {
type: defaultUserInput && !property.format?.startsWith('resource-') ? 'user' : 'static',
value: property.default,
@@ -74,7 +81,6 @@ export function schemaToInputsSpec(schema: Schema, defaultUserInput: boolean): R
format: property.format
}
return accu
}, {})
}
@@ -96,6 +102,10 @@ export const displayData: Record<AppComponent['type'], { name: string; icon: any
name: 'Form',
icon: FormInput
},
formbuttoncomponent: {
name: 'Modal Form',
icon: PlusSquare
},
piechartcomponent: {
name: 'Pie Chart',
icon: PieChart
@@ -239,8 +249,11 @@ export function clearResultAppInput(appInput: ResultAppInput): ResultAppInput {
return appInput
}
export function toStatic(app: App, staticExporter: Record<string, () => any>, summary: string): { app: App; summary: string } {
export function toStatic(
app: App,
staticExporter: Record<string, () => any>,
summary: string
): { app: App; summary: string } {
const newApp: App = JSON.parse(JSON.stringify(app))
newApp.grid.forEach((x) => {
let c: AppComponent = x.data
@@ -251,16 +264,52 @@ export function toStatic(app: App, staticExporter: Record<string, () => any>, su
return { app: newApp, summary }
}
export function buildExtraLib(components: Record<string, Record<string, Output<any>>>, idToExclude: string, hasRows: boolean): string {
return Object.entries(components)
.filter(([k, v]) => k != idToExclude)
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
.map(
([k, v]) => `
export function buildExtraLib(
components: Record<string, Record<string, Output<any>>>,
idToExclude: string,
hasRows: boolean
): string {
return (
Object.entries(components)
.filter(([k, v]) => k != idToExclude)
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
.map(
([k, v]) => `
declare const ${k} = ${JSON.stringify(v)};
`
)
.join('\n') + (hasRows ? 'declare const row: Record<string, any>;' : '')
}
)
.join('\n') + (hasRows ? 'declare const row: Record<string, any>;' : '')
)
}
export function getAllScriptNames(app: App): string[] {
const names = app.grid.reduce((acc, gridItem: GridItem) => {
const { componentInput } = gridItem.data
if (
componentInput?.type === 'runnable' &&
componentInput?.runnable?.type === 'runnableByName'
) {
acc.push(componentInput.runnable.name)
}
if (componentInput?.type === 'tablecomponent') {
componentInput.actionButtons.forEach((actionButton) => {
if (actionButton.componentInput?.type === 'runnable') {
if (actionButton.componentInput.runnable?.type === 'runnableByName') {
acc.push(actionButton.componentInput.runnable.name)
}
}
})
}
return acc
}, [] as string[])
const unusedNames = app.unusedInlineScripts.map((x) => x.name)
const backgroundNames = app.hiddenInlineScripts?.map((x) => x.name) ?? []
return [...names, ...unusedNames, ...backgroundNames]
}
@@ -28,12 +28,19 @@
}
}
}
function fadeFast(node: HTMLElement) {
return fade(node, { duration: 100 })
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if open}
<div transition:fade={{ duration: 100 }} class={'relative z-50'} role="dialog">
<div
transition:fadeFast|local
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
role="dialog"
>
<div
class={classNames(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
@@ -48,9 +48,9 @@
}}
>
<div class="flex flex-col w-full space-y-4">
<span
>Are you sure you want to discard change you have made? (A draft has been temporarily and
locally saved)</span
>
<span>
Are you sure you want to discard change you have made? (A draft has been temporarily and
locally saved)
</span>
</div>
</ConfirmationModal>
@@ -0,0 +1,76 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import Button from '../button/Button.svelte'
import Badge from '../badge/Badge.svelte'
export let title: string
export let open: boolean = false
const dispatch = createEventDispatcher()
function onKeyDown(event: KeyboardEvent) {
if (open) {
event.stopPropagation()
event.preventDefault()
switch (event.key) {
case 'Enter':
dispatch('confirmed')
break
case 'Escape':
dispatch('canceled')
break
}
}
}
function fadeFast(node: HTMLElement) {
return fade(node, { duration: 100 })
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if open}
<div
transition:fadeFast|local
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
role="dialog"
>
<div
class={classNames(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
open ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
)}
/>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
class={classNames(
'relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
open
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
>
<div class="flex">
<div class="ml-4 text-left flex-1">
<h3 class="text-lg font-medium text-gray-900">
{title}
</h3>
<div class="mt-2 text-sm text-gray-500">
<slot />
</div>
</div>
</div>
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<Button on:click={() => dispatch('canceled')} color="light" size="sm">
<span class="gap-2">Cancel <Badge color="dark-gray">Escape</Badge></span>
</Button>
</div>
</div>
</div>
</div>
</div>
{/if}
@@ -25,7 +25,8 @@
let value: App = {
grid: [],
fullscreen: false,
unusedInlineScripts: []
unusedInlineScripts: [],
hiddenInlineScripts: []
}
if (nodraft) {