feat(frontend): refactor inline script (#1480)

* feat(frontend): refactor inline script wip

* feat(frontend): refactor inline script wip

* feat(frontend): clean up

* feat(frontend): fix run configuration for triggerable

* feat(frontend): fix autoRefresh issues

* feat(frontend): fix autoRefresh issues

* feat(frontend): modify actions + remove reactivity

* feat(frontend): wip

* feat(frontend): fix RecomputeAllComponnet

* feat(frontend): add intermediate state when the language has not yet been selected

* feat(frontend): fix connection

* feat(frontend): fix wording

* feat(frontend): fix bg trigers list

* feat(frontend): restore

* feat(frontend): restore

* feat(frontend): add missing migration
This commit is contained in:
Faton Ramadani
2023-04-28 12:56:06 +02:00
committed by GitHub
parent cf59cc04ef
commit 05c837e64f
21 changed files with 640 additions and 470 deletions
@@ -8,7 +8,7 @@
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import { Bug } from 'lucide-svelte'
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, CancelablePromise, InlineScript } from '../../types'
@@ -60,38 +60,6 @@
let donePromise: (() => void) | undefined = undefined
const cancellableRun: (inlineScript?: InlineScript) => CancelablePromise<void> = (
inlineScript?: InlineScript
) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<void>> = new Promise<void>((resolve, reject) => {
rejectCb = reject
donePromise = resolve
executeComponent(true, inlineScript).catch(reject)
})
p.cancel = () => {
testJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
return p as CancelablePromise<void>
}
$runnableComponents[id] = {
autoRefresh: autoRefresh && recomputableByRefreshButton,
refreshOnStart,
cb: cancellableRun
}
if (!$initialized.initializedComponents.includes(id)) {
$initialized.initializedComponents = [...$initialized.initializedComponents, id]
}
onDestroy(() => {
$initialized.initializedComponents = $initialized.initializedComponents.filter((c) => c !== id)
})
$runnableComponents = $runnableComponents
let args: Record<string, any> | undefined = undefined
@@ -189,8 +157,6 @@
}
async function executeComponent(noToast = false, inlineScriptOverride?: InlineScript) {
console.debug('execute', id)
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
loading = true
try {
@@ -234,6 +200,11 @@
return
}
if (!testJobLoader) {
console.warn('No test job loader')
return
}
loading = true
try {
@@ -359,6 +330,40 @@
const event = e as unknown as PointerEvent
!$connectingInput.opened && selectId(event, id, selectedComponent, $app)
}
onMount(() => {
const cancellableRun: (inlineScript?: InlineScript) => CancelablePromise<void> = (
inlineScript?: InlineScript
) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<void>> = new Promise<void>((resolve, reject) => {
rejectCb = reject
donePromise = resolve
executeComponent(true, inlineScript).catch(reject)
})
p.cancel = () => {
testJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
return p as CancelablePromise<void>
}
$runnableComponents[id] = {
autoRefresh: autoRefresh && recomputableByRefreshButton,
refreshOnStart,
cb: cancellableRun
}
if (!$initialized.initializedComponents.includes(id)) {
$initialized.initializedComponents = [...$initialized.initializedComponents, id]
}
})
onDestroy(() => {
$initialized.initializedComponents = $initialized.initializedComponents.filter((c) => c !== id)
})
</script>
{#each Object.entries(fields ?? {}) as [key, v] (key)}
@@ -11,6 +11,7 @@
import InitializeComponent from './InitializeComponent.svelte'
export let componentInput: AppInput | undefined
export let id: string
export let result: any = undefined
export let initializing: boolean = true
@@ -112,7 +113,11 @@
bind:result
runnable={componentInput.runnable}
transformer={componentInput.transformer}
{autoRefresh}
autoRefresh={autoRefresh !== undefined
? autoRefresh
: componentInput?.type === 'runnable'
? componentInput.autoRefresh
: false}
bind:recomputeOnInputChanged={componentInput.recomputeOnInputChanged}
{id}
{extraQueryParams}
@@ -1,6 +1,4 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { getContext } from 'svelte'
import type { App, AppViewerContext } from '../types'
import { allItems } from '../utils'
@@ -8,7 +6,7 @@
import PanelSection from './settingsPanel/common/PanelSection.svelte'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
import BackgroundScriptTriggerList from './settingsPanel/triggerLists/BackgroundScriptTriggerList.svelte'
import BackgroundScriptSettings from './settingsPanel/script/BackgroundScriptSettings.svelte'
const { selectedComponent, app, stateId } = getContext<AppViewerContext>('AppViewerContext')
@@ -54,6 +52,22 @@
return undefined
}
$: {
if (hiddenInlineScript && hiddenInlineScript?.script.recomputeOnInputChanged === undefined) {
hiddenInlineScript.script.recomputeOnInputChanged = true
}
//TODO: remove after migration is done
if (
hiddenInlineScript &&
hiddenInlineScript?.script.doNotRecomputeOnInputChanged != undefined
) {
hiddenInlineScript.script.recomputeOnInputChanged =
!hiddenInlineScript.script.doNotRecomputeOnInputChanged
hiddenInlineScript.script.doNotRecomputeOnInputChanged = undefined
}
}
</script>
{#if componentSettings}
@@ -82,34 +96,12 @@
/>
{/key}
{:else if hiddenInlineScript}
<div class="min-h-full flex flex-col divide-y">
<PanelSection title={`Configuration`}>
<div class="flex items-center">
<Toggle
bind:checked={hiddenInlineScript.script.autoRefresh}
options={{ right: 'Run on start and app refresh' }}
/>
<Tooltip>
You may want to disable this so that the background script is only triggered by changes to
other values or triggered by another computation on a button (See 'Recompute Others')
</Tooltip>
</div>
</PanelSection>
<BackgroundScriptSettings
bind:script={hiddenInlineScript.script}
id={`bg_${hiddenInlineScript.index}`}
/>
<div class="p-2">
{#if hiddenInlineScript.script.inlineScript}
<BackgroundScriptTriggerList
fields={hiddenInlineScript.script.fields}
autoRefresh={hiddenInlineScript.script.autoRefresh}
id={`bg_${hiddenInlineScript.index}`}
bind:recomputeOnInputChanged={hiddenInlineScript.script.recomputeOnInputChanged}
bind:doNotRecomputeOnInputChanged={hiddenInlineScript.script.doNotRecomputeOnInputChanged}
bind:inlineScript={hiddenInlineScript.script.inlineScript}
/>
{:else}
<span class="text-gray-600 text-xs">No script defined</span>
{/if}
</div>
<div>
{#if Object.keys(hiddenInlineScript.script.fields).length > 0}
<PanelSection title={`Inputs`}>
{#key $stateId}
@@ -122,6 +114,13 @@
{/key}
</PanelSection>
{/if}
{#if hiddenInlineScript.script.inlineScript?.language === 'frontend'}
<PanelSection title={`Inputs`}>
<div class="text-xs"> Frontend cannot have inputs </div>
</PanelSection>
{/if}
<div class="grow shrink" />
</div>
{/if}
@@ -1,21 +1,21 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { ResultAppInput } from '../../inputType'
import type { AppEditorContext, AppViewerContext } from '../../types'
import type { AppViewerContext } from '../../types'
import { clearResultAppInput } from '../../utils'
import type { AppComponent } from '../component'
import ComponentTriggerList from './triggerLists/ComponentTriggerList.svelte'
import ComponentScriptSettings, { type ActionType } from './script/ComponentScriptSettings.svelte'
import { ExternalLink, X } from 'lucide-svelte'
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
export let appInput: ResultAppInput
export let appComponent: AppComponent
$: if (appInput.autoRefresh === undefined) {
appInput.autoRefresh = true
}
function detach() {
if (appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript) {
$app.unusedInlineScripts.push({
@@ -41,97 +41,40 @@
appInput.doNotRecomputeOnInputChanged = undefined
}
}
$: hasScript =
appInput?.runnable?.type === 'runnableByPath' ||
(appInput?.runnable?.type === 'runnableByName' && appInput.runnable?.inlineScript !== undefined)
function getActions(hasScript: boolean): ActionType[] {
if (hasScript) {
return [
{
label: 'Detach',
icon: ExternalLink,
color: 'light',
callback: detach
},
{
label: 'Clear',
icon: X,
color: 'red',
callback: clear
}
]
} else {
return [
{
label: 'Clear',
icon: X,
color: 'red',
callback: clear
}
]
}
}
$: actions = getActions(hasScript)
</script>
<div class="flex justify-between w-full items-center gap-1">
<span class="text-xs font-semibold truncate">
{#if appInput.runnable?.type === 'runnableByName'}
{appInput.runnable.name}
{:else if appInput.runnable?.type === 'runnableByPath'}
{appInput.runnable.path}
{/if}
</span>
<div class="flex gap-1 justify-center">
{#if appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript}
<Button size="xs" color="light" variant="border" on:click={detach}>
Detach&nbsp;
<Tooltip>
Detaching an inline script keep it for later to be reused by another component
</Tooltip>
</Button>
{/if}
<Button size="xs" color="red" variant="border" startIcon={{ icon: faClose }} on:click={clear}>
Clear
</Button>
</div>
</div>
<div>
<div class="w-full"><div class="mx-auto w-0">&downarrow;</div></div>
<div class="flex gap-1 justify-between items-center">
<span class="text-xs font-semibold truncate">
Transformer &nbsp;
<Tooltip>
A transformer is an optional frontend script that is executed right after the component's
script whose purpose is to do lightweight transformation in the browser. It takes the
previous computation's result as `result`
</Tooltip>
</span>
<div class="flex gap-1">
{#if !appInput.transformer}
<div>
<Button
variant="border"
color="light"
size="xs"
on:click={() => {
appInput.transformer = {
language: 'frontend',
content: 'return result'
}
$selectedComponentInEditor = appComponent.id + '_transformer'
}}
>
Add Transformer
</Button>
</div>
{:else}
<Button
size="xs"
color="red"
variant="border"
startIcon={{ icon: faClose }}
on:click={() => {
appInput.transformer = undefined
}}
>
Clear
</Button>
{/if}
</div></div
>
</div>
{#if !['buttoncomponent', 'formbuttoncomponent', 'formcomponent'].includes(appComponent.type)}
<div class="flex items-center">
<Toggle
size="xs"
bind:checked={appInput.recomputeOnInputChanged}
options={{ right: 'recompute on any input changes' }}
/>
</div>
{/if}
{#if appInput.runnable?.type === 'runnableByName'}
<div>
<ComponentTriggerList
bind:runnable={appInput.runnable}
{appComponent}
fields={appInput.fields}
recomputeOnInputChanged={appInput.recomputeOnInputChanged}
/>
</div>
{/if}
{#if appInput.runnable?.type === 'runnableByName' && !appInput.runnable.inlineScript}
<span class="text-xs text-gray-500">
Please configure the language in the inline script panel
</span>
{/if}
<ComponentScriptSettings bind:appInput bind:appComponent {hasScript} {actions} />
@@ -0,0 +1,31 @@
<script lang="ts">
import ScriptSettingHeader from './shared/ScriptSettingHeader.svelte'
import type { AppViewerContext, HiddenInlineScript } from '$lib/components/apps/types'
import ScriptRunConfiguration from './shared/ScriptRunConfiguration.svelte'
import BackgroundScriptTriggerBy from './shared/BackgroundScriptTriggerBy.svelte'
import { getContext } from 'svelte'
export let script: HiddenInlineScript
export let id: string
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
</script>
<div class={'border-y border-gray-200 divide-y'}>
<ScriptSettingHeader name={script.name} />
<ScriptRunConfiguration
bind:autoRefresh={script.autoRefresh}
bind:recomputeOnInputChanged={script.recomputeOnInputChanged}
canConfigureRecomputeOnInputChanged={script.inlineScript?.language !== 'frontend'}
on:updateAutoRefresh={() => {
const autoRefresh = !script.autoRefresh
if ($runnableComponents?.[id]?.autoRefresh !== autoRefresh && autoRefresh !== undefined) {
$runnableComponents[id] = {
...$runnableComponents[id],
autoRefresh
}
}
}}
/>
<BackgroundScriptTriggerBy bind:script recomputeOnInputChanged={script.recomputeOnInputChanged} />
</div>
@@ -0,0 +1,76 @@
<script lang="ts" context="module">
export type ActionType = {
label: string
icon: any
color: ButtonType.Color
callback: () => void
}
</script>
<script lang="ts">
import type { ResultAppInput } from '$lib/components/apps/inputType'
import type { ButtonType } from '$lib/components/common/button/model'
import { isTriggerable } from './utils'
import type { AppComponent } from '../../component'
import ScriptTransformer from './shared/ScriptTransformer.svelte'
import ScriptRunConfiguration from './shared/ScriptRunConfiguration.svelte'
import ComponentScriptTriggerBy from './shared/ComponentScriptTriggerBy.svelte'
import ScriptSettingHeader from './shared/ScriptSettingHeader.svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '$lib/components/apps/types'
import ScriptSettingsSection from './shared/ScriptSettingsSection.svelte'
export let appInput: ResultAppInput
export let appComponent: AppComponent
export let hasScript: boolean
let runnable = appInput.runnable
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
export let actions: ActionType[] = []
</script>
<div class={'border border-gray-200 divide-y'}>
<ScriptSettingHeader
name={runnable?.type === 'runnableByName'
? runnable.name
: runnable?.type === 'runnableByPath'
? runnable.path
: ''}
{actions}
/>
{#if hasScript}
<ScriptTransformer bind:appInput bind:appComponent />
<ScriptRunConfiguration
canConfigureRecomputeOnInputChanged={!isTriggerable(appComponent.type)}
canConfigureRunOnStart={!isTriggerable(appComponent.type)}
bind:autoRefresh={appInput.autoRefresh}
bind:recomputeOnInputChanged={appInput.recomputeOnInputChanged}
on:updateAutoRefresh={() => {
const autoRefresh = !(
appComponent.componentInput?.type === 'runnable' &&
appComponent?.componentInput?.autoRefresh
)
if (
appComponent.componentInput?.type === 'runnable' &&
$runnableComponents?.[appComponent.id]?.autoRefresh !== autoRefresh &&
!isTriggerable(appComponent.type) &&
autoRefresh !== undefined
) {
$runnableComponents[appComponent.id] = {
...$runnableComponents[appComponent.id],
autoRefresh
}
}
}}
/>
<ComponentScriptTriggerBy {appComponent} {appInput} />
{:else}
<ScriptSettingsSection title="Language selection">
<div class="text-xs"> Please configure the language in the inline script panel </div>
</ScriptSettingsSection>
{/if}
</div>
@@ -0,0 +1,124 @@
<script lang="ts">
import type { AppViewerContext, HiddenInlineScript } from '$lib/components/apps/types'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { classNames } from '$lib/utils'
import { Plus, X } from 'lucide-svelte'
import ScriptSettingsSection from './ScriptSettingsSection.svelte'
import { getContext } from 'svelte'
import type { InputConnection } from '$lib/components/apps/inputType'
import { getDependencies } from '../utils'
export let script: HiddenInlineScript
export let recomputeOnInputChanged: boolean | undefined = undefined
$: isFrontend = script.inlineScript?.language === 'frontend'
$: triggerEvents = script.autoRefresh ? ['start', 'refresh'] : []
$: changeEvents = isFrontend
? script.inlineScript?.refreshOn
? script.inlineScript.refreshOn.map((x) => `${x.id} - ${x.key}`)
: []
: getDependencies(script.fields)
$: hasNoTriggers =
triggerEvents.length === 0 && (changeEvents.length === 0 || !recomputeOnInputChanged)
const badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
const colors = {
green: 'text-green-800 border-green-600 bg-green-100',
indigo: 'text-indigo-800 border-indigo-600 bg-indigo-100',
blue: 'text-blue-800 border-blue-600 bg-blue-100'
}
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
function applyConnection(connection: InputConnection) {
const refresh = {
id: connection.componentId,
key: connection.path
}
if (!script.inlineScript) {
return
}
if (script.inlineScript.refreshOn?.find((y) => y.id === refresh.id && y.key === refresh.key)) {
return
}
if (!script.inlineScript.refreshOn) {
script.inlineScript.refreshOn = [refresh]
} else {
script.inlineScript.refreshOn.push(refresh)
}
script.inlineScript = JSON.parse(JSON.stringify(script.inlineScript))
$app = $app
}
</script>
<ScriptSettingsSection title="Triggers">
{#if isFrontend}
<div class="flex mb-4">
<Button
size="xs2"
color="dark"
on:click={() => {
$connectingInput = {
opened: true,
input: undefined,
hoveredComponent: undefined,
onConnect: applyConnection
}
}}
>
<div class="flex flex-row gap-1 items-center">
<Plus size={14} />
Add dependency
</div>
</Button>
</div>
{/if}
{#if hasNoTriggers}
<Alert type="warning" title="No triggers" size="xs">
This script has no triggers. It will never run.
</Alert>
{:else}
{#if triggerEvents.length > 0}
<div class="text-xs font-semibold text-slate-800 mb-1">Events</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each triggerEvents as triggerEvent}
<span class={classNames(badgeClass, colors['green'])}>{triggerEvent}</span>
{/each}
</div>
{/if}
{#if changeEvents.length > 0 && (recomputeOnInputChanged || isFrontend)}
<div class="text-xs font-semibold text-slate-800 mb-1 mt-2">Change on value</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each changeEvents as changeEvent}
<span class={classNames(badgeClass, colors['blue'])}>
{changeEvent}
{#if isFrontend}
<button
class="bg-blue-300 ml-2 p-0.5 rounded-md hover:bg-blue-400 cursor-pointer"
on:click={() => {
if (script.inlineScript?.refreshOn) {
script.inlineScript.refreshOn = script.inlineScript.refreshOn.filter(
(x) => `${x.id} - ${x.key}` !== changeEvent
)
script.inlineScript = JSON.parse(JSON.stringify(script.inlineScript))
}
}}
>
<X size="14" />
</button>
{/if}
</span>
<!-- delete button -->
{/each}
</div>
{/if}
{/if}
</ScriptSettingsSection>
@@ -0,0 +1,48 @@
<script lang="ts">
import type { ResultAppInput } from '$lib/components/apps/inputType'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { classNames } from '$lib/utils'
import type { AppComponent } from '../../../component'
import { getAllTriggerEvents, isTriggerable, getDependencies } from '../utils'
import ScriptSettingsSection from './ScriptSettingsSection.svelte'
export let appComponent: AppComponent
export let appInput: ResultAppInput
$: triggerEvents = getAllTriggerEvents(appComponent, appInput.autoRefresh)
$: changeEvents = getDependencies(appInput.fields)
$: hasNoTriggers =
triggerEvents.length === 0 && (changeEvents.length === 0 || !appInput.recomputeOnInputChanged)
const badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
const colors = {
green: 'text-green-800 border-green-600 bg-green-100',
indigo: 'text-indigo-800 border-indigo-600 bg-indigo-100',
blue: 'text-blue-800 border-blue-600 bg-blue-100'
}
</script>
<ScriptSettingsSection title="Triggers">
{#if hasNoTriggers}
<Alert type="warning" title="No triggers" size="xs">
This script has no triggers. It will never run.
</Alert>
{:else}
{#if triggerEvents.length > 0}
<div class="text-xs font-semibold text-slate-800 mb-1">Events</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each triggerEvents as triggerEvent}
<span class={classNames(badgeClass, colors['green'])}>{triggerEvent}</span>
{/each}
</div>
{/if}
{#if changeEvents.length > 0 && appInput.recomputeOnInputChanged && !isTriggerable(appComponent.type)}
<div class="text-xs font-semibold text-slate-800 mb-1 mt-2">Change on value</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each changeEvents as changeEvent}
<span class={classNames(badgeClass, colors['blue'])}>{changeEvent}</span>
{/each}
</div>
{/if}
{/if}
</ScriptSettingsSection>
@@ -0,0 +1,55 @@
<script lang="ts">
import Tooltip from '$lib/components/Tooltip.svelte'
import { createEventDispatcher } from 'svelte'
import ScriptSettingsSection from './ScriptSettingsSection.svelte'
export let autoRefresh: boolean | undefined = false
export let recomputeOnInputChanged: boolean | undefined = false
export let canConfigureRecomputeOnInputChanged: boolean = true
export let canConfigureRunOnStart: boolean = true
const dispatch = createEventDispatcher()
</script>
{#if canConfigureRecomputeOnInputChanged || canConfigureRunOnStart}
<ScriptSettingsSection title="Run configuration">
<div class="flex flex-col gap-2">
{#if autoRefresh !== undefined && canConfigureRunOnStart}
<div class="flex items-center justify-between w-full">
<div class="flex flex-row items-center gap-2 text-xs">
Run on start and app refresh
<Tooltip>
You may want to disable this so that the background script is only triggered by
changes to other values or triggered by another computation on a button (See
'Recompute Others')
</Tooltip>
</div>
<input
type="checkbox"
bind:checked={autoRefresh}
class="!w-4 !h-4 !rounded-sm"
on:click={() => {
dispatch('updateAutoRefresh')
}}
/>
</div>
{/if}
{#if recomputeOnInputChanged !== undefined && canConfigureRecomputeOnInputChanged}
<div class="flex items-center justify-between w-full">
<div class="flex flex-row items-center gap-2 text-xs">
Recompute on any input changes
</div>
<input
type="checkbox"
bind:checked={recomputeOnInputChanged}
class="!w-4 !h-4 !rounded-sm"
/>
</div>
{/if}
</div>
</ScriptSettingsSection>
{:else}
<ScriptSettingsSection title="Run configuration">
<div class="text-xs"> Triggerable component runs only when an interaction happens. </div>
</ScriptSettingsSection>
{/if}
@@ -0,0 +1,19 @@
<script lang="ts">
import { FunctionSquare } from 'lucide-svelte'
import type { ActionType } from '../ComponentScriptSettings.svelte'
import ScriptSettingsActions from './ScriptSettingsActions.svelte'
export let name: string
export let actions: ActionType[] = []
</script>
<div class="flex flex-row p-2 border-gray-200 justify-between bg-blue-50/60">
<div class="flex flex-row gap-2 items-center">
<FunctionSquare size={16} color="blue" />
<span class="text-xs font-semibold truncate">
{name}
</span>
</div>
<ScriptSettingsActions {actions} />
</div>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { ButtonType } from '$lib/components/common/button/model'
import Button from '$lib/components/common/button/Button.svelte'
import Popover from '$lib/components/Popover.svelte'
type ActionType = {
label: string
icon: any
color: ButtonType.Color
callback: () => void
}
export let actions: ActionType[] = []
</script>
<div class="flex flex-row gap-1 justify-end">
{#each actions as action, index (index)}
<Popover notClickable disappearTimeout={0}>
<svelte:fragment slot="text">
{action.label}
</svelte:fragment>
<Button color={action.color} on:click={action.callback} size="xs2" variant="border">
<div class="flex flex-row gap-1 items-center">
{#if action.icon}
<svelte:component this={action.icon} size={12} />
{/if}
</div>
</Button>
</Popover>
{/each}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import Tooltip from '$lib/components/Tooltip.svelte'
export let title: string
export let tooltip: string | undefined = undefined
</script>
<div class="p-2">
<div class="mb-2 text-sm font-semibold justify-between flex flex-row items-center">
{title}
{#if tooltip}
<Tooltip>
{tooltip}
</Tooltip>
{/if}
</div>
<slot />
</div>
@@ -0,0 +1,41 @@
<script lang="ts">
import type { ResultAppInput } from '$lib/components/apps/inputType'
import type { AppEditorContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import ScriptSettingsSection from './ScriptSettingsSection.svelte'
import type { AppComponent } from '../../../component'
import { Button } from '$lib/components/common'
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
export let appInput: ResultAppInput
export let appComponent: AppComponent
$: checked = Boolean(appInput.transformer)
</script>
<ScriptSettingsSection
title="Transformer"
tooltip={"A transformer is an optional frontend script that is executed right after the component's script whose purpose is to do lightweight transformation in the browser. It takes the previous computation's result as `result`"}
>
<div class="flex gap-1 justify-between items-center">
<Button
size="xs"
color={checked ? 'red' : 'light'}
variant="border"
on:click={() => {
if (appInput.transformer) {
appInput.transformer = undefined
} else {
appInput.transformer = {
language: 'frontend',
content: 'return result'
}
$selectedComponentInEditor = appComponent.id + '_transformer'
}
}}
>
{checked ? 'Remove' : 'Add a transformer'}
</Button>
</div>
</ScriptSettingsSection>
@@ -0,0 +1,63 @@
import type { Runnable } from '$lib/components/apps/inputType'
import type { AppComponent } from '../../component'
import type {
StaticAppInput,
ConnectedAppInput,
RowAppInput,
UserAppInput
} from '$lib/components/apps/inputType'
export function getDependencies(
fields:
| Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
| undefined
): string[] {
let dependencies: string[] = []
if (!fields) return dependencies
Object.values(fields).forEach((field) => {
if (field.type === 'connected' && dependencies && field.connection) {
dependencies.push(`${field.connection.componentId} - ${field.connection.path}`)
}
})
return dependencies
}
export function isFrontend(runnable: Runnable): boolean {
return runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend'
}
export function isTriggerable(componentType: string): boolean {
return ['buttoncomponent', 'formbuttoncomponent', 'formcomponent'].includes(componentType)
}
export function isTriggerOnAppLoad(appComponent: AppComponent): boolean {
return Boolean(
appComponent?.configuration?.triggerOnAppLoad != undefined &&
appComponent.configuration.triggerOnAppLoad.type == 'static' &&
appComponent.configuration.triggerOnAppLoad.value
)
}
export function getAllTriggerEvents(
appComponent: AppComponent,
autoRefresh: boolean | undefined
): string[] {
const events: string[] = []
const triggerOnAppLoad = isTriggerOnAppLoad(appComponent)
const isTriggerableComponent = isTriggerable(appComponent.type)
if (isTriggerableComponent) {
events.push('click')
if (triggerOnAppLoad) {
events.push('start')
}
} else if (autoRefresh) {
events.push('start')
events.push('refresh')
}
return events
}
@@ -1,52 +0,0 @@
<script lang="ts">
import type {
ConnectedAppInput,
RowAppInput,
StaticAppInput,
UserAppInput
} from '$lib/components/apps/inputType'
import type { InlineScript } from '$lib/components/apps/types'
import Toggle from '$lib/components/Toggle.svelte'
import TriggerBadgesList from './TriggerBadgesList.svelte'
import { getDependencies } from './triggerListUtils'
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let autoRefresh: boolean = false
export let id: string
export let inlineScript: InlineScript
export let recomputeOnInputChanged: boolean | undefined = true
export let doNotRecomputeOnInputChanged: undefined | boolean = undefined
//TODO: remove after migration is done
$: {
if (doNotRecomputeOnInputChanged != undefined) {
recomputeOnInputChanged = !doNotRecomputeOnInputChanged
doNotRecomputeOnInputChanged = undefined
}
if (recomputeOnInputChanged == undefined) {
recomputeOnInputChanged = true
}
}
$: dependencies = getDependencies(fields)
</script>
{#if inlineScript.language !== 'frontend'}
<div class="flex items-center px-1">
<Toggle
size="xs"
bind:checked={recomputeOnInputChanged}
options={{ right: 'recompute on any input changes' }}
/>
</div>
{/if}
<TriggerBadgesList
bind:inlineScript
{id}
inputDependencies={dependencies}
onLoad={autoRefresh}
{recomputeOnInputChanged}
/>
@@ -1,44 +0,0 @@
<script lang="ts">
import type {
ConnectedAppInput,
RowAppInput,
RunnableByName,
StaticAppInput,
UserAppInput
} from '$lib/components/apps/inputType'
import type { AppViewerContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import type { AppComponent } from '../../component'
import TriggerBadgesList from './TriggerBadgesList.svelte'
import { getDependencies } from './triggerListUtils'
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let appComponent: AppComponent
export let runnable: RunnableByName
export let recomputeOnInputChanged: boolean = false
const onClick = ['buttoncomponent', 'formbuttoncomponent', 'formcomponent'].includes(
appComponent.type
)
$: onLoad =
!onClick ||
(appComponent?.configuration?.triggerOnAppLoad != undefined &&
appComponent.configuration.triggerOnAppLoad.type == 'static' &&
appComponent.configuration.triggerOnAppLoad.value)
</script>
<TriggerBadgesList
inputDependencies={onClick ? [] : getDependencies(fields)}
bind:inlineScript={runnable.inlineScript}
{onLoad}
{recomputeOnInputChanged}
id={$selectedComponent?.[0]}
shouldHideAddDependencyButton={[
'buttoncomponent',
'formcomponent',
'formbuttoncomponent'
].includes(appComponent.type)}
{onClick}
/>
@@ -1,174 +0,0 @@
<script lang="ts">
import type { AppViewerContext, InlineScript } from '$lib/components/apps/types'
import { Button } from '$lib/components/common'
import { classNames } from '$lib/utils'
import { Plus, X } from 'lucide-svelte'
import { getContext } from 'svelte'
import { getAllRecomputeIdsForComponent } from '../../appUtils'
export let inputDependencies: string[] = []
export let inlineScript: InlineScript | undefined
export let onClick: boolean = false
export let onLoad: boolean = false
export let id: string | undefined = undefined
export let recomputeOnInputChanged: boolean = false
export let shouldHideAddDependencyButton: boolean = false
const colors = {
green: 'text-green-800 border-green-600 bg-green-100',
indigo: 'text-indigo-800 border-indigo-600 bg-indigo-100',
blue: 'text-blue-800 border-blue-600 bg-blue-100'
}
let badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
let addingDependency: boolean = false
function applyConnection() {
if (!$connectingInput.opened && $connectingInput.input !== undefined && addingDependency) {
if ($connectingInput.input.connection) {
const x = {
id: $connectingInput.input.connection.componentId,
key: $connectingInput.input.connection.path
}
if (!inlineScript) {
return
}
if (inlineScript.refreshOn?.find((y) => y.id === x.id && y.key === x.key)) {
return
}
if (!inlineScript.refreshOn) {
inlineScript.refreshOn = [x]
} else {
inlineScript.refreshOn.push(x)
}
inlineScript = JSON.parse(JSON.stringify(inlineScript))
addingDependency = false
}
$connectingInput = {
opened: false,
input: undefined,
hoveredComponent: undefined
}
$app = $app
}
}
$: frontendDependencies =
inlineScript?.language === 'frontend'
? inlineScript?.refreshOn?.map((x) => `${x.id} - ${x.key}`) ?? []
: undefined
$: $connectingInput && applyConnection()
$: recomputedBadges = getAllRecomputeIdsForComponent($app, id)
function deleteDep(index: number) {
if (inlineScript) {
inlineScript.refreshOn?.splice(index, 1)
}
inlineScript = inlineScript
}
</script>
<div class="flex w-full flex-col items-start gap-2 mt-2 mb-1">
{#if recomputedBadges.length === 0 && !onLoad && !onClick && inputDependencies?.length === 0 && !frontendDependencies}
<p class="text-xs font-semibold text-slate-800">
This script has no triggers. It will never run.
</p>
{:else}
<div class="text-sm font-semibold text-gray-800">Triggered by</div>
{#if onLoad || onClick}
<div class="w-full">
<div class="text-xs font-semibold text-slate-800 mb-1">Events</div>
<div class="flex flex-row gap-2 flex-wrap">
{#if onLoad}
<span class={classNames(badgeClass, colors['green'])}>Start</span>
<span class={classNames(badgeClass, colors['green'])}>Refresh</span>
{/if}
{#if onClick}
<span class={classNames(badgeClass, colors['green'])}>Click</span>
{/if}
</div>
</div>
{/if}
{#if inputDependencies.length > 0 && (recomputeOnInputChanged ?? true)}
<div class="w-full">
<div class="flex justify-between items-center mb-1">
<div class="text-xs font-semibold text-slate-800">Change on values</div>
</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each inputDependencies as label}
<span class={classNames(badgeClass, colors['blue'])}>
{label}
</span>
{/each}
</div>
</div>
{/if}
{#if recomputedBadges?.length > 0}
<div class="w-full">
<div class="text-xs font-semibold text-slate-800 mb-1">Computation of</div>
<div class="flex flex-row gap-2 flex-wrap">
{#each recomputedBadges as badge}
<span class={classNames(badgeClass, colors['indigo'])}>{badge}</span>
{/each}
</div>
</div>
{/if}
{/if}
{#if frontendDependencies && recomputeOnInputChanged}
<div class="w-full">
<div class="flex justify-between items-center">
<div class="text-xs font-semibold text-slate-800 mb-1">Change on values</div>
{#if inlineScript?.language === 'frontend' && !shouldHideAddDependencyButton}
<Button
variant="border"
size="xs"
color="light"
btnClasses="!px-1 !py-0.5"
on:click={() => {
addingDependency = true
$connectingInput = {
opened: true,
input: undefined,
hoveredComponent: undefined
}
}}
>
<div class="flex flex-row gap-1 items-center">
Add dependency
<Plus size={14} />
</div>
</Button>
{/if}
</div>
<div class="flex flex-row gap-2 flex-wrap mt-2">
{#each frontendDependencies as label, index}
<span class={classNames(badgeClass, colors['blue'])}>
{label}
<button
on:click={() => deleteDep(index)}
class="bg-blue-300 cursor-pointer hover:bg-blue-400 ml-1 rounded-md"
>
<X size={18} class="p-0.5" />
</button>
</span>
{/each}
</div>
</div>
{/if}
</div>
@@ -1,23 +0,0 @@
import type {
StaticAppInput,
ConnectedAppInput,
RowAppInput,
UserAppInput
} from '$lib/components/apps/inputType'
export function getDependencies(
fields:
| Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
| undefined
): string[] {
let dependencies: string[] = []
if (!fields) return dependencies
Object.values(fields).forEach((field) => {
if (field.type === 'connected' && dependencies && field.connection) {
dependencies.push(`${field.connection.componentId} - ${field.connection.path}`)
}
})
return dependencies
}
@@ -92,6 +92,7 @@ export type ResultInput = {
// kept for migration purposes
doNotRecomputeOnInputChanged?: boolean
recomputeOnInputChanged?: boolean
autoRefresh?: boolean
}
type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
+12 -10
View File
@@ -103,6 +103,17 @@ export type InlineScript = {
export type AppCssItemName = 'viewer' | 'grid' | AppComponent['type']
export type HiddenInlineScript = {
name: string
inlineScript: InlineScript | undefined
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
autoRefresh?: boolean
//deprecated and to be removed after migration
doNotRecomputeOnInputChanged?: boolean
recomputeOnInputChanged?: boolean
noBackendValue?: any
}
export type App = {
grid: GridItem[]
fullscreen: boolean
@@ -111,16 +122,7 @@ export type App = {
name: string
inlineScript: InlineScript
}>
hiddenInlineScripts: Array<{
name: string
inlineScript: InlineScript | undefined
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
autoRefresh?: boolean
//deprecated and to be removed after migration
doNotRecomputeOnInputChanged?: boolean
recomputeOnInputChanged?: boolean
noBackendValue?: any
}>
hiddenInlineScripts: Array<HiddenInlineScript>
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
subgrids?: Record<string, GridItem[]>
}
@@ -9,10 +9,10 @@
</MenuButton>
<Transition
show={open}
enter="transition ease-out duration-75"
enter="transition ease-out duration-[25ms]"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leave="transition ease-in duration-[25ms]"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>