mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 16:09:39 +00:00
feat(frontend): add frontend (JS) scripts to apps
This commit is contained in:
@@ -1722,7 +1722,6 @@ fn list_completed_jobs_query(
|
||||
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
|
||||
}
|
||||
|
||||
tracing::info!("{:?}", sqlb.sql());
|
||||
sqlb
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { accessPropertyByPath } from '../../utils'
|
||||
import { computeGlobalContext, eval_like } from './eval'
|
||||
|
||||
type T = string | number | boolean | Record<string | number, any> | undefined
|
||||
|
||||
@@ -24,13 +25,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { worldStore, state } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: state = $worldStore?.state
|
||||
$: stateId = $worldStore?.state
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
const debounce_ms = 50
|
||||
function debounce(cb: () => void) {
|
||||
function debounce(cb: () => Promise<void>) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
@@ -40,20 +41,22 @@
|
||||
$: lastInput && $worldStore && debounce(handleConnection)
|
||||
$: lastInput &&
|
||||
lastInput.type == 'template' &&
|
||||
$stateId &&
|
||||
$state &&
|
||||
debounce(() => (value = getValue(lastInput)))
|
||||
debounce(async () => (value = await getValue(lastInput)))
|
||||
$: lastInput &&
|
||||
lastInput.type == 'eval' &&
|
||||
$state &&
|
||||
debounce(() => (value = evalExpr(lastInput)))
|
||||
$stateId &&
|
||||
state &&
|
||||
debounce(async () => (value = await evalExpr(lastInput)))
|
||||
|
||||
function handleConnection() {
|
||||
async function handleConnection() {
|
||||
if (lastInput.type === 'connected') {
|
||||
$worldStore?.connect<any>(lastInput, onValueChange)
|
||||
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
|
||||
value = getValue(lastInput)
|
||||
value = await getValue(lastInput)
|
||||
} else if (lastInput.type == 'eval') {
|
||||
value = evalExpr(lastInput as EvalAppInput)
|
||||
value = await evalExpr(lastInput as EvalAppInput)
|
||||
} else if (lastInput.type == 'upload') {
|
||||
value = (lastInput as UploadAppInput).value
|
||||
} else {
|
||||
@@ -61,9 +64,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function evalExpr(input: EvalAppInput) {
|
||||
async function evalExpr(input: EvalAppInput) {
|
||||
try {
|
||||
const r = eval_like(input.expr, computeGlobalContext())
|
||||
const r = await eval_like(
|
||||
input.expr,
|
||||
computeGlobalContext($worldStore, id, extraContext),
|
||||
true,
|
||||
$state
|
||||
)
|
||||
error = ''
|
||||
return r
|
||||
} catch (e) {
|
||||
@@ -72,26 +80,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function computeGlobalContext() {
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries($worldStore?.outputsById ?? {})
|
||||
.filter(([k, _]) => k != id)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
|
||||
]
|
||||
})
|
||||
),
|
||||
...extraContext
|
||||
}
|
||||
}
|
||||
|
||||
export function getValue(input: AppInput) {
|
||||
async function getValue(input: AppInput) {
|
||||
if (input.type === 'template' && isCodeInjection(input.eval)) {
|
||||
try {
|
||||
const r = eval_like('`' + input.eval + '`', computeGlobalContext())
|
||||
const r = await eval_like(
|
||||
'`' + input.eval + '`',
|
||||
computeGlobalContext($worldStore, id, extraContext),
|
||||
true,
|
||||
$state
|
||||
)
|
||||
error = ''
|
||||
return r
|
||||
} catch (e) {
|
||||
@@ -104,31 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function create_context_function_template(eval_string, context) {
|
||||
return `
|
||||
return function (context) {
|
||||
"use strict";
|
||||
${
|
||||
Object.keys(context).length > 0
|
||||
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
|
||||
: ``
|
||||
}
|
||||
return ${eval_string};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
function make_context_evaluator(eval_string, context) {
|
||||
let template = create_context_function_template(eval_string, context)
|
||||
let functor = Function(template)
|
||||
return functor()
|
||||
}
|
||||
|
||||
function eval_like(text, context = {}) {
|
||||
let evaluator = make_context_evaluator(text, context)
|
||||
return evaluator(context)
|
||||
}
|
||||
|
||||
function onValueChange(newValue: any): void {
|
||||
if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) {
|
||||
const { connection } = lastInput
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
import { Bug, Loader2 } from 'lucide-svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import type { AppInputs, Runnable } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { computeGlobalContext, eval_like } from './eval'
|
||||
import InputValue from './InputValue.svelte'
|
||||
import RefreshButton from './RefreshButton.svelte'
|
||||
|
||||
@@ -42,7 +44,8 @@
|
||||
noBackend,
|
||||
errorByComponent,
|
||||
mode,
|
||||
stateId
|
||||
stateId,
|
||||
state
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
onMount(() => {
|
||||
@@ -96,14 +99,7 @@
|
||||
let testJob: CompletedJob | undefined = undefined
|
||||
let testJobLoader: TestJobLoader | undefined = undefined
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<Array<any>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
|
||||
$: if (outputs?.loading != undefined) {
|
||||
outputs.loading.set(false, true)
|
||||
}
|
||||
$: outputs = initOutput($worldStore, id, { result: undefined, loading: false })
|
||||
|
||||
$: outputs?.loading?.set(testIsLoading)
|
||||
$: schemaStripped = stripSchema(fields, $stateId)
|
||||
@@ -144,6 +140,23 @@
|
||||
)
|
||||
|
||||
async function executeComponent(noToast = false) {
|
||||
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
|
||||
outputs?.loading?.set(true)
|
||||
try {
|
||||
const r = await eval_like(
|
||||
runnable.inlineScript?.content,
|
||||
computeGlobalContext($worldStore, id, {}),
|
||||
false,
|
||||
$state
|
||||
)
|
||||
setResult(r)
|
||||
$state = $state
|
||||
} catch (e) {
|
||||
sendUserToast('Error running frontend script: ' + e.message)
|
||||
}
|
||||
outputs?.loading?.set(false)
|
||||
return
|
||||
}
|
||||
if (noBackend) {
|
||||
if (!noToast) {
|
||||
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
|
||||
@@ -216,6 +229,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setResult(res: any) {
|
||||
outputs.result?.set(res)
|
||||
result = res
|
||||
|
||||
const previousJobId = Object.keys($errorByComponent).find(
|
||||
(key) => $errorByComponent[key].componentId === id
|
||||
)
|
||||
|
||||
if (previousJobId && !result?.error) {
|
||||
delete $errorByComponent[previousJobId]
|
||||
$errorByComponent = $errorByComponent
|
||||
}
|
||||
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
|
||||
if (gotoNewTab) {
|
||||
window.open(gotoUrl, '_blank')
|
||||
} else {
|
||||
goto(gotoUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
$: result?.error && recordError(result.error)
|
||||
</script>
|
||||
|
||||
@@ -232,24 +265,7 @@
|
||||
const startedAt = new Date(testJob.started_at).getTime()
|
||||
if (startedAt > lastStartedAt) {
|
||||
lastStartedAt = startedAt
|
||||
outputs.result?.set(testJob?.result)
|
||||
result = testJob.result
|
||||
|
||||
const previousJobId = Object.keys($errorByComponent).find(
|
||||
(key) => $errorByComponent[key].componentId === id
|
||||
)
|
||||
|
||||
if (previousJobId && !result?.error) {
|
||||
delete $errorByComponent[previousJobId]
|
||||
$errorByComponent = $errorByComponent
|
||||
}
|
||||
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
|
||||
if (gotoNewTab) {
|
||||
window.open(gotoUrl, '_blank')
|
||||
} else {
|
||||
goto(gotoUrl)
|
||||
}
|
||||
}
|
||||
setResult(e.detail.result)
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import type { World } from '../../rx'
|
||||
|
||||
export function computeGlobalContext(
|
||||
world: World | undefined,
|
||||
id: string | undefined,
|
||||
extraContext: any = {}
|
||||
) {
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(world?.outputsById ?? {})
|
||||
.filter(([k, _]) => k != id)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
|
||||
]
|
||||
})
|
||||
),
|
||||
...extraContext
|
||||
}
|
||||
}
|
||||
|
||||
function create_context_function_template(eval_string, context, noReturn: boolean) {
|
||||
return `
|
||||
return async function (context, state, goto) {
|
||||
"use strict";
|
||||
${
|
||||
Object.keys(context).length > 0
|
||||
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
|
||||
: ``
|
||||
}
|
||||
${noReturn ? `return ${eval_string}` : eval_string}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
function make_context_evaluator(
|
||||
eval_string,
|
||||
context,
|
||||
noReturn: boolean
|
||||
): (context, state, goto) => Promise<any> {
|
||||
let template = create_context_function_template(eval_string, context, noReturn)
|
||||
let functor = Function(template)
|
||||
return functor()
|
||||
}
|
||||
|
||||
export async function eval_like(text, context = {}, noReturn: boolean = true, state: any = {}) {
|
||||
let evaluator = make_context_evaluator(text, context, noReturn)
|
||||
return await evaluator(context, state, async (x) => {
|
||||
await goto(x)
|
||||
})
|
||||
}
|
||||
@@ -88,7 +88,8 @@
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid,
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0)
|
||||
parentWidth: writable(0),
|
||||
state: writable({})
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid: writable(undefined),
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0)
|
||||
parentWidth: writable(0),
|
||||
state: writable({})
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
|
||||
@@ -47,7 +47,6 @@ export function getNextGridItemId(app: App): string {
|
||||
}
|
||||
|
||||
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
|
||||
|
||||
const newComponent = {
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
@@ -105,7 +104,6 @@ export function insertNewGridItem(
|
||||
app.subgrids = {}
|
||||
}
|
||||
|
||||
|
||||
// We only want to set subgrids when we are not moving
|
||||
if (!keepId) {
|
||||
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
|
||||
@@ -113,8 +111,9 @@ export function insertNewGridItem(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const key = focusedGrid ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` : undefined
|
||||
const key = focusedGrid
|
||||
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
|
||||
: undefined
|
||||
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
||||
|
||||
const newItem = createNewGridItem(grid, id, data)
|
||||
@@ -177,8 +176,6 @@ export function deleteGridItem(
|
||||
return components
|
||||
}
|
||||
|
||||
|
||||
|
||||
type AvailableSpace = {
|
||||
left: number
|
||||
right: number
|
||||
@@ -286,11 +283,17 @@ function isOverlapping(item1: any, item2: any) {
|
||||
}
|
||||
|
||||
type Outputtable<Type> = {
|
||||
-readonly [Property in keyof Type]: Output<Type[Property]>;
|
||||
};
|
||||
-readonly [Property in keyof Type]: Output<Type[Property]>
|
||||
}
|
||||
|
||||
|
||||
export function initOutput<I extends Record<string, any>>(world: World, id: string, init: I): Outputtable<I> {
|
||||
export function initOutput<I extends Record<string, any>>(
|
||||
world: World | undefined,
|
||||
id: string,
|
||||
init: I
|
||||
): Outputtable<I> {
|
||||
if (!world) {
|
||||
return {} as any
|
||||
}
|
||||
const output = world.outputsById[id] as Outputtable<I>
|
||||
if (init) {
|
||||
for (const key in init) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
@@ -10,7 +11,7 @@
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
|
||||
|
||||
const { connectingInput, staticOutputs, worldStore, selectedComponent, app } =
|
||||
const { connectingInput, staticOutputs, worldStore, selectedComponent, app, state } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function connectInput(componentId: string, path: string) {
|
||||
@@ -155,5 +156,12 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="State">
|
||||
<div class="mx-2 px-1 border w-full mb-8">
|
||||
{#key $state}
|
||||
<ObjectViewer json={$state} />
|
||||
{/key}
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
+53
-21
@@ -3,6 +3,7 @@
|
||||
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
|
||||
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { Script, type Preview } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
@@ -145,7 +146,29 @@
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col px-4 py-2 gap-2 text-sm" in:fly={{ duration: 50 }}>
|
||||
<div>Choose a language:</div>
|
||||
<div class="mt-4 flex justify-between gap-8 mb-2">
|
||||
<div class="font-bold items-baseline">Choose a language:</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
on:click={() => picker?.openDrawer()}
|
||||
size="xs"
|
||||
color="blue"
|
||||
startIcon={{ icon: faCodeBranch }}
|
||||
btnClasses="truncate"
|
||||
>
|
||||
fork a detached script
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => dispatch('delete')}
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
btnClasses="truncate"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-row flex-wrap">
|
||||
{#each langs as lang}
|
||||
<FlowScriptPicker
|
||||
@@ -164,6 +187,7 @@
|
||||
createInlineScriptByLanguage(Script.language.DENO, name, 'pgsql')
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- <FlowScriptPicker
|
||||
label={`MySQL`}
|
||||
lang="mysql"
|
||||
@@ -172,26 +196,34 @@
|
||||
}}
|
||||
/> -->
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs mb-1 mt-2"
|
||||
>Frontend only script: <Tooltip
|
||||
>Frontend scripts are executed in the browser and can manipulate the app context directly</Tooltip
|
||||
></div
|
||||
>
|
||||
<FlowScriptPicker
|
||||
label={`JavaScript`}
|
||||
lang="javascript"
|
||||
on:click={() => {
|
||||
const newInlineScript = {
|
||||
content: `// read outputs and ctx
|
||||
console.log(ctx.email)
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
on:click={() => picker?.openDrawer()}
|
||||
size="xs"
|
||||
color="blue"
|
||||
startIcon={{ icon: faCodeBranch }}
|
||||
btnClasses="truncate"
|
||||
>
|
||||
or fork a detached/Workspace/Hub script
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => dispatch('delete')}
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrash }}
|
||||
btnClasses="truncate"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
// access a global state store
|
||||
if (!state.foo) { state.foo = 0 }
|
||||
state.foo += 1
|
||||
|
||||
// you can also navigate to another page
|
||||
//await goto("?foo=bar")
|
||||
|
||||
return state.foo`,
|
||||
language: 'frontend',
|
||||
path: 'frontend script',
|
||||
schema: undefined
|
||||
}
|
||||
dispatch('new', newInlineScript)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+75
-46
@@ -15,6 +15,8 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { buildExtraLib } from '../../utils'
|
||||
|
||||
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
|
||||
|
||||
@@ -25,7 +27,8 @@
|
||||
export let fields: Record<string, AppInput> = {}
|
||||
export let syncFields: boolean = false
|
||||
|
||||
const { runnableComponents, stateId } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { runnableComponents, stateId, worldStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let editor: Editor
|
||||
let validCode = true
|
||||
@@ -48,11 +51,13 @@
|
||||
|
||||
onMount(async () => {
|
||||
if (inlineScript && !inlineScript.schema) {
|
||||
inlineScript.schema = await inferInlineScriptSchema(
|
||||
inlineScript?.language,
|
||||
inlineScript?.content,
|
||||
emptySchema()
|
||||
)
|
||||
if (inlineScript.language != 'frontend') {
|
||||
inlineScript.schema = await inferInlineScriptSchema(
|
||||
inlineScript?.language,
|
||||
inlineScript?.content,
|
||||
emptySchema()
|
||||
)
|
||||
}
|
||||
}
|
||||
if (inlineScript.schema) {
|
||||
loadSchemaAndInputsByName()
|
||||
@@ -63,7 +68,7 @@
|
||||
|
||||
async function loadSchemaAndInputsByName() {
|
||||
if (syncFields) {
|
||||
const newSchema = inlineScript.schema
|
||||
const newSchema = inlineScript.schema ?? emptySchema()
|
||||
const newFields = computeFields(newSchema, defaultUserInput, fields)
|
||||
|
||||
if (!deepEqual(newFields, fields)) {
|
||||
@@ -74,9 +79,16 @@
|
||||
}
|
||||
|
||||
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
|
||||
|
||||
$: extraLib =
|
||||
inlineScript.language == 'frontend' && worldStore
|
||||
? buildExtraLib($worldStore?.outputsById ?? {}, id, false)
|
||||
: undefined
|
||||
</script>
|
||||
|
||||
<InlineScriptEditorDrawer {editor} bind:this={inlineScriptEditorDrawer} bind:inlineScript />
|
||||
{#if inlineScript.language != 'frontend'}
|
||||
<InlineScriptEditorDrawer {editor} bind:this={inlineScriptEditorDrawer} bind:inlineScript />
|
||||
{/if}
|
||||
|
||||
<div class="h-full flex flex-col gap-1">
|
||||
<div class="flex justify-between w-full gap-2 px-2 pt-1 flex-row items-center">
|
||||
@@ -109,20 +121,23 @@
|
||||
<svelte:fragment slot="text">Delete</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
<Popover notClickable placement="bottom">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
btnClasses="!px-2 !bg-gray-100 hover:!bg-gray-200"
|
||||
aria-label="Open full editor"
|
||||
on:click={() => {
|
||||
inlineScriptEditorDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
<svelte:fragment slot="text">Open full editor</svelte:fragment>
|
||||
</Popover>
|
||||
{#if inlineScript.language != 'frontend'}
|
||||
<Popover notClickable placement="bottom">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
btnClasses="!px-2 !bg-gray-100 hover:!bg-gray-200"
|
||||
aria-label="Open full editor"
|
||||
on:click={() => {
|
||||
inlineScriptEditorDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
<svelte:fragment slot="text">Open full editor</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
@@ -172,30 +187,44 @@
|
||||
</div>
|
||||
|
||||
<div class="border h-full">
|
||||
<Editor
|
||||
bind:this={editor}
|
||||
class="flex flex-1 grow h-full"
|
||||
lang={scriptLangToEditorLang(inlineScript?.language)}
|
||||
bind:code={inlineScript.content}
|
||||
fixedOverflowWidgets={true}
|
||||
cmdEnterAction={async () => {
|
||||
runLoading = true
|
||||
await $runnableComponents[id]?.()
|
||||
runLoading = false
|
||||
}}
|
||||
on:change={async (e) => {
|
||||
if (inlineScript) {
|
||||
const oldSchema = JSON.stringify(inlineScript.schema)
|
||||
if (inlineScript.schema == undefined) {
|
||||
inlineScript.schema = emptySchema()
|
||||
{#if inlineScript.language != 'frontend'}
|
||||
<Editor
|
||||
bind:this={editor}
|
||||
class="flex flex-1 grow h-full"
|
||||
lang={scriptLangToEditorLang(inlineScript?.language)}
|
||||
bind:code={inlineScript.content}
|
||||
fixedOverflowWidgets={true}
|
||||
cmdEnterAction={async () => {
|
||||
runLoading = true
|
||||
await $runnableComponents[id]?.()
|
||||
runLoading = false
|
||||
}}
|
||||
on:change={async (e) => {
|
||||
if (inlineScript && inlineScript.language != 'frontend') {
|
||||
const oldSchema = JSON.stringify(inlineScript.schema)
|
||||
if (inlineScript.schema == undefined) {
|
||||
inlineScript.schema = emptySchema()
|
||||
}
|
||||
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
|
||||
if (JSON.stringify(inlineScript.schema) != oldSchema) {
|
||||
inlineScript = inlineScript
|
||||
loadSchemaAndInputsByName()
|
||||
}
|
||||
}
|
||||
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
|
||||
if (JSON.stringify(inlineScript.schema) != oldSchema) {
|
||||
inlineScript = inlineScript
|
||||
loadSchemaAndInputsByName()
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<SimpleEditor
|
||||
cmdEnterAction={async () => {
|
||||
runLoading = true
|
||||
await $runnableComponents[id]?.()
|
||||
runLoading = false
|
||||
}}
|
||||
class="h-full"
|
||||
{extraLib}
|
||||
bind:code={inlineScript.content}
|
||||
lang="javascript"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import type Editor from '$lib/components/Editor.svelte'
|
||||
import ScriptEditor from '$lib/components/ScriptEditor.svelte'
|
||||
import type { Preview } from '$lib/gen'
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { InlineScript } from '../../types'
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
editor?.setCode(inlineScript.content)
|
||||
}}
|
||||
>
|
||||
{#if inlineScript}
|
||||
{#if inlineScript && inlineScript.language != 'frontend'}
|
||||
<ScriptEditor
|
||||
noSyncFromGithub
|
||||
lang={inlineScript.language}
|
||||
|
||||
@@ -92,7 +92,11 @@
|
||||
let isPointerUp = detail.isPointerUp
|
||||
let citems: FilledItem<T>[]
|
||||
if (isPointerUp) {
|
||||
citems = JSON.parse(JSON.stringify(initItems))
|
||||
try {
|
||||
citems = JSON.parse(JSON.stringify(initItems))
|
||||
} catch (e) {
|
||||
citems = JSON.parse(JSON.stringify(items))
|
||||
}
|
||||
initItems = undefined
|
||||
} else {
|
||||
if (initItems == undefined) {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
let anima
|
||||
|
||||
const inActivate = () => {
|
||||
if (shadowElement && shadow) {
|
||||
if (shadowElement && shadow != undefined) {
|
||||
let subgrid = shadowElement.closest('.subgrid')
|
||||
let irect = shadowElement.getBoundingClientRect()
|
||||
let shadowBound
|
||||
|
||||
@@ -44,14 +44,14 @@ export interface BaseAppComponent extends Partial<Aligned> {
|
||||
configuration: Record<
|
||||
string,
|
||||
GeneralAppInput &
|
||||
(
|
||||
| StaticAppInput
|
||||
| ConnectedAppInput
|
||||
| UserAppInput
|
||||
| RowAppInput
|
||||
| EvalAppInput
|
||||
| UploadAppInput
|
||||
)
|
||||
(
|
||||
| StaticAppInput
|
||||
| ConnectedAppInput
|
||||
| UserAppInput
|
||||
| RowAppInput
|
||||
| EvalAppInput
|
||||
| UploadAppInput
|
||||
)
|
||||
>
|
||||
card: boolean | undefined
|
||||
customCss?: ComponentCustomCSS
|
||||
@@ -84,9 +84,9 @@ export type GridItem = FilledItem<{
|
||||
|
||||
export type InlineScript = {
|
||||
content: string
|
||||
language: Preview.language
|
||||
path: string
|
||||
schema: Schema
|
||||
language: Preview.language | 'frontend'
|
||||
path?: string
|
||||
schema?: Schema
|
||||
}
|
||||
|
||||
export type App = {
|
||||
@@ -134,6 +134,7 @@ export type AppViewerContext = {
|
||||
focusedGrid: Writable<FocusedGrid | undefined>
|
||||
stateId: Writable<number>
|
||||
parentWidth: Writable<number>
|
||||
state: Writable<Record<string, any>>
|
||||
}
|
||||
|
||||
export type AppEditorContext = {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export let width = 16
|
||||
export let height = 16
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 630 630">
|
||||
<rect width="630" height="630" fill="#f7df1e" />
|
||||
<path
|
||||
d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -4,18 +4,23 @@
|
||||
import PostgresIcon from '$lib/components/icons/PostgresIcon.svelte'
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
import { BashIcon, GoIcon, PythonIcon, TypeScriptIcon } from './'
|
||||
import JavaScript from './JavaScript.svelte'
|
||||
|
||||
export let lang: SupportedLanguage | 'pgsql' | 'mysql'
|
||||
export let lang: SupportedLanguage | 'pgsql' | 'mysql' | 'javascript'
|
||||
export let width = 30
|
||||
export let height = 30
|
||||
export let scale = 1
|
||||
const langToComponent: Record<SupportedLanguage | 'pgsql' | 'mysql', typeof SvelteComponent> = {
|
||||
const langToComponent: Record<
|
||||
SupportedLanguage | 'pgsql' | 'mysql' | 'javascript',
|
||||
typeof SvelteComponent
|
||||
> = {
|
||||
go: GoIcon,
|
||||
python3: PythonIcon,
|
||||
deno: TypeScriptIcon,
|
||||
bash: BashIcon,
|
||||
pgsql: PostgresIcon,
|
||||
mysql: MySQLIcon
|
||||
mysql: MySQLIcon,
|
||||
javascript: JavaScript
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
export let disabled: boolean = false
|
||||
export let label: string
|
||||
export let lang: SupportedLanguage | 'pgsql' | 'mysql' | undefined = undefined
|
||||
export let lang: SupportedLanguage | 'pgsql' | 'mysql' | 'javascript' | undefined = undefined
|
||||
export let icon: IconDefinition | undefined = undefined
|
||||
export let iconColor: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user