mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
fix(frontend): Refactor apps to support multiple breakpoints (#957)
* fix(frontend): Refactor apps to support multiples breakpoints * fix(frontend): WIP * fix(frontend): redo apps page * fix(frontend): Hide schema for buttons * fix(frontend): Add apps menu * fix(frontend): Properly handle runnable delete * fix(frontend): simplify ui * fix(frontend): unify flow * fix(frontend): table action design * fix(frontend): reework runnable pick * fix(frontend): fix apps action * fix(frontend): Highlight output * fix(frontend): Fix dimensions * fix(frontend): Hide alert on button component * fix(frontend): add number of actions when a table is selected
This commit is contained in:
@@ -7,9 +7,8 @@
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import { AppService, type CompletedJob } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faArrowsRotate, faRefresh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, InputsSpec } from '../../types'
|
||||
import { loadSchema, schemaToInputsSpec } from '../../utils'
|
||||
@@ -200,11 +199,15 @@
|
||||
bind:this={testJobLoader}
|
||||
/>
|
||||
|
||||
{#if schemaStripped !== undefined}
|
||||
{#if schemaStripped !== undefined && autoRefresh}
|
||||
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
|
||||
{/if}
|
||||
|
||||
{#if autoRefresh === true}
|
||||
{#if inlineScriptName === undefined && path === undefined && runType === undefined && autoRefresh}
|
||||
<Alert type="warning" size="xs" class="mt-2" title="Missing runnable">
|
||||
Please select a runnable
|
||||
</Alert>
|
||||
{:else if autoRefresh === true}
|
||||
{#if isValid}
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
import { Pane } from 'svelte-splitpanes'
|
||||
import { writable } from 'svelte/store'
|
||||
import { buildWorld, type World } from '../rx'
|
||||
import type { App, AppEditorContext, ConnectingInput, EditorMode, InputType } from '../types'
|
||||
import type {
|
||||
App,
|
||||
AppEditorContext,
|
||||
ConnectingInput,
|
||||
EditorBreakpoint,
|
||||
EditorMode,
|
||||
InputType
|
||||
} from '../types'
|
||||
import AppEditorHeader from './AppEditorHeader.svelte'
|
||||
import GridEditor from './GridEditor.svelte'
|
||||
|
||||
@@ -17,15 +24,19 @@
|
||||
import { faPlus, faSliders } from '@fortawesome/free-solid-svg-icons'
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
export let app: App
|
||||
export let path: string
|
||||
export let initialMode: EditorMode = 'dnd'
|
||||
|
||||
const appStore = writable<App>(app)
|
||||
const worldStore = writable<World | undefined>(undefined)
|
||||
const staticOutputs = writable<Record<string, string[]>>({})
|
||||
const selectedComponent = writable<string | undefined>(undefined)
|
||||
const mode = writable<EditorMode>('dnd')
|
||||
const mode = writable<EditorMode>(initialMode)
|
||||
const breakpoint = writable<EditorBreakpoint>('lg')
|
||||
|
||||
const connectingInput = writable<ConnectingInput<InputType, any>>({
|
||||
opened: false,
|
||||
input: undefined
|
||||
@@ -37,7 +48,8 @@
|
||||
app: appStore,
|
||||
selectedComponent,
|
||||
mode,
|
||||
connectingInput
|
||||
connectingInput,
|
||||
breakpoint
|
||||
})
|
||||
|
||||
function clearSelectionOnPreview() {
|
||||
@@ -52,21 +64,27 @@
|
||||
})
|
||||
|
||||
$: mounted && ($worldStore = buildWorld($staticOutputs))
|
||||
|
||||
$: $mode && $selectedComponent && clearSelectionOnPreview()
|
||||
$: selectedTab = $selectedComponent ? 'settings' : 'insert'
|
||||
$: previewing = $mode === 'preview'
|
||||
|
||||
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'w-full '
|
||||
</script>
|
||||
|
||||
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} />
|
||||
{#if initialMode !== 'preview'}
|
||||
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
|
||||
{/if}
|
||||
|
||||
<SplitPanesWrapper class="max-w-full overflow-hidden">
|
||||
<Pane size={previewing ? 0 : 20} minSize={previewing ? 0 : 20} maxSize={40}>
|
||||
<ContextPanel appPath={path} />
|
||||
</Pane>
|
||||
<Pane size={previewing ? 100 : 60}>
|
||||
<div class="p-4 bg-gray-100 h-full" id="faton">
|
||||
<div class="p-4 bg-gray-100 h-full w-full">
|
||||
{#if $appStore.grid}
|
||||
<GridEditor />
|
||||
<div class={classNames('mx-auto h-full', width)}>
|
||||
<GridEditor />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import { AppService, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { faDisplay, faExternalLink, faHand } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faDesktopAlt,
|
||||
faDisplay,
|
||||
faExternalLink,
|
||||
faHand,
|
||||
faMobileAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import { sendUserToast } from '../../../utils'
|
||||
import type { AppEditorContext, EditorMode } from '../types'
|
||||
import type { AppEditorContext, EditorBreakpoint, EditorMode } from '../types'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
export let title: string = $app.title || ''
|
||||
export let mode: EditorMode
|
||||
export let breakpoint: EditorBreakpoint
|
||||
const loading = {
|
||||
publish: false,
|
||||
save: false
|
||||
@@ -32,27 +39,42 @@
|
||||
on_behalf_of: `u/${$userStore?.username}`
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
sendUserToast('Saved successfully.')
|
||||
}).catch(() => {
|
||||
sendUserToast('Error during saving. Please try again later.', true)
|
||||
}).finally(() => {
|
||||
loading.save = false
|
||||
})
|
||||
.then(() => {
|
||||
sendUserToast('Saved successfully.')
|
||||
})
|
||||
.catch(() => {
|
||||
sendUserToast('Error during saving. Please try again later.', true)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.save = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="border-b flex flex-row justify-between py-1 px-4 items-center">
|
||||
<input class="text-sm w-64" bind:value={title} />
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={mode}>
|
||||
<ToggleButton position="left" value="dnd" startIcon={{ icon: faHand }} size="xs">
|
||||
Component editor
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="preview" startIcon={{ icon: faDisplay }} size="xs">
|
||||
Preview
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex gap-8">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={mode}>
|
||||
<ToggleButton position="left" value="dnd" startIcon={{ icon: faHand }} size="xs">
|
||||
Editor
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="preview" startIcon={{ icon: faDisplay }} size="xs">
|
||||
Preview
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={breakpoint}>
|
||||
<ToggleButton position="left" value="sm" startIcon={{ icon: faMobileAlt }} size="xs">
|
||||
Mobile
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="lg" startIcon={{ icon: faDesktopAlt }} size="xs">
|
||||
Desktop
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button color="dark" size="xs" variant="border" startIcon={{ icon: faExternalLink }}>
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'p-2 border overflow-auto cursor-pointer h-full bg-white',
|
||||
' border overflow-auto cursor-pointer h-full bg-white',
|
||||
selected ? 'border-blue-500' : 'border-white',
|
||||
$mode === 'preview' ? 'border-white' : 'hover:border-blue-500'
|
||||
$mode === 'preview' ? 'border-white' : 'hover:border-blue-500',
|
||||
component.card ? 'p-2' : ''
|
||||
)}
|
||||
>
|
||||
{#if component.type === 'displaycomponent'}
|
||||
|
||||
@@ -4,37 +4,28 @@
|
||||
import Grid from 'svelte-grid'
|
||||
import ComponentEditor from './ComponentEditor.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { columnConfiguration, disableDrag, enableDrag } from '../gridUtils'
|
||||
|
||||
const { selectedComponent, app, mode } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
const COLS = 6
|
||||
const cols: [number, number][] = [[1200, COLS]]
|
||||
|
||||
$: if ($mode === 'preview') {
|
||||
$app.grid.map((c) => {
|
||||
c[COLS].customDragger = true
|
||||
c[COLS].customResizer = true
|
||||
return c
|
||||
})
|
||||
$app.grid.map((gridItem) => disableDrag(gridItem))
|
||||
} else {
|
||||
$app.grid.map((c) => {
|
||||
c[COLS].customDragger = false
|
||||
c[COLS].customResizer = false
|
||||
return c
|
||||
})
|
||||
$app.grid.map((gridItem) => enableDrag(gridItem))
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="bg-white h-full" on:click|preventDefault={() => $selectedComponent = undefined}>
|
||||
<Grid bind:items={$app.grid} rowHeight={32} let:dataItem {cols}>
|
||||
<div class="bg-white h-full" on:click|preventDefault={() => ($selectedComponent = undefined)}>
|
||||
<Grid bind:items={$app.grid} rowHeight={64} let:dataItem cols={columnConfiguration}>
|
||||
{#each $app.grid as gridComponent (gridComponent.id)}
|
||||
{#if gridComponent.data.id === dataItem.data.id}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'h-full w-full flex justify-center align-center border border-gray-100'
|
||||
'h-full w-full flex justify-center align-center',
|
||||
gridComponent.data.card ? 'border border-gray-100' : ''
|
||||
)}
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
$selectedComponent = dataItem.data.id
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import type { Size } from 'svelte-grid'
|
||||
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { isOpenStore } from './store'
|
||||
import { gridColumns } from '../../gridUtils'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const COLS = 6
|
||||
@@ -18,81 +19,91 @@
|
||||
function addComponent(
|
||||
appComponent: AppComponent,
|
||||
defaultDimensions: Size,
|
||||
minDimensions: Size = { w: 1, h: 1 },
|
||||
maxDimensions: Size = { w: 6, h: 12 }
|
||||
minDimensions: Size = { w: 2, h: 1 },
|
||||
maxDimensions: Size = { w: 12, h: 12 }
|
||||
) {
|
||||
const grid = $app.grid ?? []
|
||||
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
|
||||
|
||||
appComponent.id = id
|
||||
|
||||
let newItem: GridItem = {
|
||||
[COLS]: {
|
||||
fixed: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
customDragger: false,
|
||||
customResizer: false,
|
||||
min: minDimensions,
|
||||
max: maxDimensions,
|
||||
x: 0,
|
||||
y: 0,
|
||||
...defaultDimensions
|
||||
},
|
||||
const newComponent = {
|
||||
fixed: false,
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
customDragger: false,
|
||||
customResizer: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
...defaultDimensions
|
||||
}
|
||||
|
||||
const newItem: GridItem = {
|
||||
data: JSON.parse(JSON.stringify(appComponent)),
|
||||
id: id
|
||||
}
|
||||
|
||||
let findOutPosition = gridHelp.findSpace(newItem, grid, COLS)
|
||||
|
||||
newItem = {
|
||||
...newItem,
|
||||
[COLS]: {
|
||||
...newItem[COLS],
|
||||
...findOutPosition
|
||||
function getMinMaxDimensions(column) {
|
||||
if (column === 3) {
|
||||
return {
|
||||
min: { w: 1, h: 1 },
|
||||
max: { w: 3, h: 12 }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
min: minDimensions,
|
||||
max: maxDimensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gridColumns.forEach((column) => {
|
||||
newItem[column] = newComponent
|
||||
const position = gridHelp.findSpace(newItem, grid, column)
|
||||
const dimensions = getMinMaxDimensions(column)
|
||||
newItem[column] = { ...newItem[column], ...position, ...dimensions }
|
||||
})
|
||||
|
||||
$app.grid = [...grid, newItem]
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
isOpenStore.addItems(componentSets.map(set => ({ [set.title]: true })))
|
||||
isOpenStore.addItems(componentSets.map((set) => ({ [set.title]: true })))
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each componentSets as {title, components}, index (index)}
|
||||
{#each componentSets as { title, components }, index (index)}
|
||||
{@const isOpen = $isOpenStore[title]}
|
||||
<section class="mt-1 mb-2 px-2">
|
||||
<button
|
||||
on:click|preventDefault={() => isOpenStore.toggle(title)}
|
||||
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
|
||||
<section class="mt-1 mb-2 px-2">
|
||||
<button
|
||||
on:click|preventDefault={() => isOpenStore.toggle(title)}
|
||||
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
|
||||
rounded-sm hover:bg-gray-100"
|
||||
>
|
||||
<h1 class="text-sm font-semibold text-left">{title}</h1>
|
||||
<Icon data={faAngleDown} class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
|
||||
</button>
|
||||
{#if isOpen}
|
||||
<div transition:slide|local={{ duration: 300 }}>
|
||||
{#if components.length}
|
||||
<div class="grid grid-cols-3 gap-1 p-2">
|
||||
{#each components as item, componentIndex (componentIndex)}
|
||||
<button
|
||||
on:click={() => addComponent(item, { w: 2, h: 2 })}
|
||||
class="border shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
>
|
||||
<h1 class="text-sm font-semibold text-left">{title}</h1>
|
||||
<Icon data={faAngleDown} class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
|
||||
</button>
|
||||
{#if isOpen}
|
||||
<div transition:slide|local={{ duration: 300 }}>
|
||||
{#if components.length}
|
||||
<div class="grid grid-cols-3 gap-1 p-2">
|
||||
{#each components as item, componentIndex (componentIndex)}
|
||||
<button
|
||||
on:click={() => addComponent(item, { w: 2, h: 2 })}
|
||||
class="border shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
justify-center bg-white rounded-md scale-100 hover:scale-105 ease-in duration-75"
|
||||
>
|
||||
<Icon data={displayData[item.type].icon} scale={1.6} class="text-blue-800" />
|
||||
<div class="text-xs">{displayData[item.type].name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-gray-500 text-center py-1">
|
||||
There are no components in this group yet
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
>
|
||||
<Icon data={displayData[item.type].icon} scale={1.6} class="text-blue-800" />
|
||||
<div class="text-xs">{displayData[item.type].name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-gray-500 py-1 px-2">
|
||||
There are no components in this group yet
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
@@ -23,16 +23,12 @@ const windmillComponents: ComponentSet = {
|
||||
|
||||
const textInputs: ComponentSet = {
|
||||
title: 'Text Inputs',
|
||||
components: [
|
||||
|
||||
]
|
||||
components: []
|
||||
}
|
||||
|
||||
const numberInputs: ComponentSet = {
|
||||
title: 'Number Inputs',
|
||||
components: [
|
||||
|
||||
]
|
||||
components: []
|
||||
}
|
||||
|
||||
const buttons: ComponentSet = {
|
||||
@@ -67,8 +63,9 @@ const buttons: ComponentSet = {
|
||||
defaultValue: 'md'
|
||||
}
|
||||
},
|
||||
runnable: true
|
||||
},
|
||||
runnable: true,
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -88,16 +85,15 @@ const selectInputs: ComponentSet = {
|
||||
fieldType: 'textarea',
|
||||
defaultValue: 'Lorem ipsum'
|
||||
}
|
||||
}
|
||||
},
|
||||
card: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const dateTimeInputs: ComponentSet = {
|
||||
title: 'Date and Time Inputs',
|
||||
components: [
|
||||
|
||||
]
|
||||
components: []
|
||||
}
|
||||
|
||||
const dataDisplay: ComponentSet = {
|
||||
@@ -155,16 +151,10 @@ const dataDisplay: ComponentSet = {
|
||||
type: 'barchartcomponent',
|
||||
runnable: true,
|
||||
card: true
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const componentSets = [
|
||||
windmillComponents,
|
||||
textInputs,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
export let appPath: string
|
||||
|
||||
const { connectingInput, staticOutputs, app, worldStore } =
|
||||
const { connectingInput, staticOutputs, app, worldStore, selectedComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
let newScriptPath: string
|
||||
let ignorePathError = false
|
||||
@@ -57,7 +57,7 @@
|
||||
[newScriptPath]: inlineScript
|
||||
}
|
||||
}
|
||||
scriptCreationDrawer.closeDrawer()
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
function afterCreateScript() {
|
||||
@@ -70,16 +70,17 @@
|
||||
| undefined = undefined
|
||||
let scriptEditorDrawer: Drawer
|
||||
|
||||
let scriptCreationDrawer: Drawer
|
||||
let scriptCreationDrawer: Drawer | undefined = undefined
|
||||
|
||||
function closeDrawer() {
|
||||
if (scriptEditorDrawer.closeDrawer) {
|
||||
scriptEditorDrawer.closeDrawer()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={scriptCreationDrawer} size="600px" on:afterClose={afterCreateScript}>
|
||||
<DrawerContent
|
||||
title="Script creation"
|
||||
on:close={() => {
|
||||
scriptCreationDrawer.closeDrawer()
|
||||
}}
|
||||
>
|
||||
<DrawerContent title="Script creation" on:close={closeDrawer}>
|
||||
<label for="pathInput" class="text-sm font-semibold"> Script name </label>
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
@@ -88,7 +89,7 @@
|
||||
id="pathInput"
|
||||
class="grow min-w-[150px]"
|
||||
bind:value={newScriptPath}
|
||||
on:keypress={e => e.key === 'Enter' && createScript()}
|
||||
on:keypress={(e) => e.key === 'Enter' && createScript()}
|
||||
/>
|
||||
<Button on:click={createScript} size="sm" disabled={isTakenPath} startIcon={{ icon: faPlus }}>
|
||||
Create
|
||||
@@ -103,14 +104,7 @@
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={scriptEditorDrawer} size="1200px">
|
||||
<DrawerContent
|
||||
title="Script Editor"
|
||||
noPadding
|
||||
forceOverflowVisible
|
||||
on:close={() => {
|
||||
scriptEditorDrawer.closeDrawer()
|
||||
}}
|
||||
>
|
||||
<DrawerContent title="Script Editor" noPadding forceOverflowVisible on:close={closeDrawer}>
|
||||
{#if selectedScript}
|
||||
<ScriptEditor
|
||||
lang={selectedScript.language}
|
||||
@@ -127,15 +121,12 @@
|
||||
<svelte:fragment slot="action">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
scriptCreationDrawer?.openDrawer()
|
||||
}}
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={closeDrawer}
|
||||
startIcon={{ icon: faPlus }}
|
||||
>
|
||||
<span>Add script</span>
|
||||
</Button>
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="w-full border rounded-sm">
|
||||
@@ -157,7 +148,7 @@
|
||||
on:click={() => {
|
||||
if (value) {
|
||||
selectedScript = value
|
||||
scriptEditorDrawer.openDrawer()
|
||||
scriptEditorDrawer.openDrawer && scriptEditorDrawer.openDrawer()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -185,7 +176,14 @@
|
||||
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
|
||||
<Badge color="blue">Component: {componentId}</Badge>
|
||||
|
||||
<div class="w-full p-2 rounded-xs border">
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full p-2 rounded-xs border',
|
||||
$selectedComponent === componentId
|
||||
? 'outline-1 outline outline-offset-2 outline-blue-500'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<ComponentOutputViewer
|
||||
{outputs}
|
||||
{componentId}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
faClose,
|
||||
faPen,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
@@ -19,6 +18,10 @@
|
||||
import gridHelp from 'svelte-grid/build/helper/index.mjs'
|
||||
import PickInlineScript from './PickInlineScript.svelte'
|
||||
import TableActions from './TableActions.svelte'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { gridColumns } from '../../gridUtils'
|
||||
import { Plus } from 'svelte-lucide'
|
||||
|
||||
export let component: AppComponent | undefined
|
||||
export let onDelete: (() => void) | undefined = undefined
|
||||
@@ -34,9 +37,11 @@
|
||||
// Delete static inputs
|
||||
} else {
|
||||
if (component) {
|
||||
const COLS = 6
|
||||
$app.grid = $app.grid.filter((gridComponent) => gridComponent.data.id !== component?.id)
|
||||
$app.grid = gridHelp.adjust($app.grid, COLS)
|
||||
|
||||
gridColumns.forEach((colIndex) => {
|
||||
$app.grid = gridHelp.adjust($app.grid, colIndex)
|
||||
})
|
||||
|
||||
// Delete static inputs
|
||||
delete $staticOutputs[component.id]
|
||||
@@ -50,27 +55,39 @@
|
||||
<div class="flex flex-col w-full divide-y">
|
||||
{#if component.runnable}
|
||||
<PanelSection title="Runnable">
|
||||
{#if component['inlineScriptName']}
|
||||
<span class="text-xs">{component['inlineScriptName']}</span>
|
||||
<div class="w-full flex flex-row gap-2">
|
||||
{#if component.runnable && component['inlineScriptName']}
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<span class="text-xs">{component['inlineScriptName']}</span>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
startIcon={{ icon: faPen }}
|
||||
color="red"
|
||||
startIcon={{ icon: faClose }}
|
||||
on:click={() => {
|
||||
alert('TODO')
|
||||
if (component) {
|
||||
component['inlineScriptName'] = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if component.runnable && component['path']}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
<Badge color="blue">{capitalize(component['runType'])}</Badge>
|
||||
<span class="text-xs">{component['path']}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faClose }}
|
||||
on:click={() => {
|
||||
alert('TODO')
|
||||
if (component) {
|
||||
component['inlineScriptName'] = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
@@ -79,33 +96,53 @@
|
||||
{/if}
|
||||
|
||||
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
|
||||
<span class="text-sm">Select a script or a flow to continue</span>
|
||||
<PickInlineScript
|
||||
scripts={(Object.keys($app.inlineScripts) || []).map((summary) => ({ summary }))}
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
// @ts-ignore
|
||||
component.inlineScriptName = detail.summary
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PickScript
|
||||
kind="script"
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
component['path'] = detail.path
|
||||
component['runType'] = 'script'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PickFlow
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
component['path'] = detail.path
|
||||
component['runType'] = 'flow'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="text-sm">Inline scripts:</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
btnClasses="w-24 truncate"
|
||||
size="sm"
|
||||
spacingSize="md"
|
||||
variant="border"
|
||||
color="light"
|
||||
>
|
||||
<div class="flex justify-center flex-col items-center gap-2">
|
||||
<Plus size="18px" />
|
||||
|
||||
<span class="text-xs">Create</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<PickInlineScript
|
||||
scripts={(Object.keys($app.inlineScripts) || []).map((summary) => ({ summary }))}
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
// @ts-ignore
|
||||
component.inlineScriptName = detail.summary
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">Pick from workspace:</div>
|
||||
<div class="flex gap-2">
|
||||
<PickScript
|
||||
kind="script"
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
component['path'] = detail.path
|
||||
component['runType'] = 'script'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PickFlow
|
||||
on:pick={({ detail }) => {
|
||||
if (component?.runnable) {
|
||||
component['path'] = detail.path
|
||||
component['runType'] = 'flow'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</PanelSection>
|
||||
{/if}
|
||||
|
||||
@@ -51,9 +51,8 @@
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold py-1.5 px-2 cursor-pointer transition-all justify-between flex items-center border border-gray-3 rounded-md',
|
||||
openedProp === inputSpecKey
|
||||
? 'bg-gray-700 hover:bg-gray-900 focus:bg-gray-900 text-white'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100 focus:bg-gray-100 text-gray-700'
|
||||
'bg-white border-gray-300 hover:bg-gray-100 focus:bg-gray-100 text-gray-700',
|
||||
openedProp === inputSpecKey ? 'outline outline-gray-500 outline-offset-1' : ''
|
||||
)}
|
||||
on:keypress
|
||||
on:click={() => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Wind } from 'svelte-lucide'
|
||||
|
||||
type Item = { summary: String; path: String; version?: String }
|
||||
|
||||
@@ -25,6 +26,17 @@
|
||||
{loadItems}
|
||||
/>
|
||||
|
||||
<Button size="xs" color="dark" on:click={() => itemPicker.openDrawer()}>
|
||||
Pick flow from workspace
|
||||
<Button
|
||||
on:click={() => itemPicker.openDrawer()}
|
||||
btnClasses="w-24 truncate"
|
||||
size="sm"
|
||||
spacingSize="md"
|
||||
variant="border"
|
||||
color="light"
|
||||
>
|
||||
<div class="flex justify-center flex-col items-center gap-2">
|
||||
<Wind size="18px" />
|
||||
|
||||
<span class="text-xs">Flow</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { MousePointerClick } from 'svelte-lucide'
|
||||
|
||||
export let scripts: Item[]
|
||||
|
||||
@@ -27,6 +28,17 @@
|
||||
{loadItems}
|
||||
/>
|
||||
|
||||
<Button size="xs" color="dark" on:click={() => itemPicker.openDrawer()}>
|
||||
Pick an inline script
|
||||
<Button
|
||||
on:click={() => itemPicker.openDrawer()}
|
||||
btnClasses="w-24 truncate"
|
||||
size="sm"
|
||||
spacingSize="md"
|
||||
variant="border"
|
||||
color="light"
|
||||
>
|
||||
<div class="flex justify-center flex-col items-center gap-2">
|
||||
<MousePointerClick size="18px" />
|
||||
|
||||
<span class="text-xs">Pick</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/common'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { getNextId } from '$lib/components/flows/flowStateUtils'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { ButtonComponent, AppEditorContext, BaseAppComponent } from '../../types'
|
||||
@@ -16,7 +19,7 @@
|
||||
const grid = $app.grid ?? []
|
||||
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
|
||||
|
||||
const newComponent: (BaseAppComponent & ButtonComponent) = {
|
||||
const newComponent: BaseAppComponent & ButtonComponent = {
|
||||
...defaultProps,
|
||||
id,
|
||||
type: 'buttoncomponent',
|
||||
@@ -50,23 +53,63 @@
|
||||
|
||||
components = [...components, newComponent]
|
||||
}
|
||||
|
||||
let openedComponentId: string | undefined = components[0]?.id
|
||||
</script>
|
||||
|
||||
<PanelSection title="Table actions">
|
||||
<PanelSection title={`Table actions ${components.length > 0 ? `(${components.length})` : ''}`}>
|
||||
<svelte:fragment slot="action">
|
||||
<Button size="xs" color="dark" startIcon={{ icon: faPlus }} on:click={addComponent}>
|
||||
Create an action
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={addComponent}
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{#each components as component}
|
||||
<div class="w-full border">
|
||||
<div class="border-b py-1 px-2 text-sm text-white bg-gray-500">Component: {component.id}</div>
|
||||
<ComponentPanel
|
||||
bind:component
|
||||
onDelete={() => {
|
||||
components = components.filter((c) => c.id !== component.id)
|
||||
}}
|
||||
/>
|
||||
{#if components.length > 0}
|
||||
<div class="w-full">
|
||||
<Alert title="Special argument" size="xs">
|
||||
A "row" argument is automatically added to the script. It contains the row data.
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
{#each components as component}
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold py-1.5 px-2 cursor-pointer transition-all justify-between flex items-center border border-gray-3 rounded-md',
|
||||
'bg-white border-gray-300 hover:bg-gray-100 focus:bg-gray-100 text-gray-700',
|
||||
openedComponentId === component.id ? 'outline outline-gray-500 outline-offset-1' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
if (openedComponentId === component.id) {
|
||||
openedComponentId = undefined
|
||||
} else {
|
||||
openedComponentId = component.id
|
||||
}
|
||||
}}
|
||||
on:keypress
|
||||
>
|
||||
<div
|
||||
>{component.componentInputs.label.type === 'static'
|
||||
? component.componentInputs.label.value
|
||||
: ''}
|
||||
</div>
|
||||
<Badge color="dark-blue">
|
||||
Component: {component.id}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{#if openedComponentId === component.id}
|
||||
<div class="w-full border">
|
||||
<ComponentPanel
|
||||
bind:component
|
||||
onDelete={() => {
|
||||
components = components.filter((c) => c.id !== component.id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</PanelSection>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { GridItem } from './types'
|
||||
|
||||
type ColumnConfiguration = [number, number][]
|
||||
|
||||
const Breakpoints = {
|
||||
sm: 640,
|
||||
lg: 1024
|
||||
}
|
||||
|
||||
const columnConfiguration: ColumnConfiguration = [
|
||||
[Breakpoints.lg, 12],
|
||||
[Breakpoints.sm, 3]
|
||||
]
|
||||
|
||||
const gridColumns = columnConfiguration.map((value) => value[1])
|
||||
|
||||
function disableDrag(component: GridItem): GridItem {
|
||||
gridColumns.forEach((column: number) => {
|
||||
component[column].customDragger = true
|
||||
component[column].customResizer = true
|
||||
})
|
||||
return component
|
||||
}
|
||||
|
||||
function enableDrag(component: GridItem): GridItem {
|
||||
gridColumns.forEach((column: number) => {
|
||||
component[column].customDragger = false
|
||||
component[column].customResizer = false
|
||||
})
|
||||
return component
|
||||
}
|
||||
|
||||
export { gridColumns, columnConfiguration, disableDrag, enableDrag, Breakpoints }
|
||||
@@ -142,12 +142,16 @@ export type GridItem = FilledItem<{
|
||||
id: string
|
||||
}>
|
||||
|
||||
export type InlineScript = {
|
||||
content: string
|
||||
language: Preview.language
|
||||
path: string
|
||||
schema: Schema
|
||||
}
|
||||
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
inlineScripts: Record<
|
||||
string,
|
||||
{ content: string; language: Preview.language; path: string; schema: Schema }
|
||||
>
|
||||
inlineScripts: Record<string, InlineScript>
|
||||
title: string
|
||||
}
|
||||
|
||||
@@ -163,9 +167,11 @@ export type AppEditorContext = {
|
||||
selectedComponent: Writable<string | undefined>
|
||||
mode: Writable<EditorMode>
|
||||
connectingInput: Writable<ConnectingInput<any, any>>
|
||||
breakpoint: Writable<EditorBreakpoint>
|
||||
}
|
||||
|
||||
export type EditorMode = 'dnd' | 'preview'
|
||||
export type EditorBreakpoint = 'sm' | 'lg'
|
||||
|
||||
type FieldID = string
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
export let type: AlertType = 'info'
|
||||
export let title: string
|
||||
|
||||
export let size: 'xs' | 'sm' = 'sm'
|
||||
|
||||
const icons: Record<AlertType, IconDefinition> = {
|
||||
info: faInfoCircle,
|
||||
warning: faWarning,
|
||||
@@ -47,15 +49,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={classNames('rounded-md p-4 ', classes[type].bgClass, $$props.class)}>
|
||||
<div
|
||||
class={classNames(
|
||||
'rounded-md',
|
||||
size === 'sm' ? 'p-4' : 'p-2 ',
|
||||
classes[type].bgClass,
|
||||
$$props.class
|
||||
)}
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full">
|
||||
<Icon data={icons[type]} class={classes[type].iconClass} />
|
||||
</div>
|
||||
|
||||
<div class="ml-2 w-full">
|
||||
<span class={classNames('text-sm font-medium', classes[type].titleClass)}>{title}</span>
|
||||
<div class={classNames('mt-2 text-sm', classes[type].descriptionClass)}>
|
||||
<span
|
||||
class={classNames(
|
||||
size === 'sm' ? 'text-sm' : 'text-xs ',
|
||||
'font-medium',
|
||||
classes[type].titleClass
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<div
|
||||
class={classNames(
|
||||
size === 'sm' ? 'text-sm' : 'text-xs ',
|
||||
'mt-2',
|
||||
classes[type].descriptionClass
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Code } from 'svelte-lucide'
|
||||
|
||||
export let kind: string
|
||||
|
||||
@@ -27,6 +28,17 @@
|
||||
{loadItems}
|
||||
/>
|
||||
|
||||
<Button size="xs" color="dark" on:click={() => itemPicker.openDrawer()}
|
||||
>Pick script from workflow</Button
|
||||
<Button
|
||||
on:click={() => itemPicker.openDrawer()}
|
||||
btnClasses="w-24 truncate"
|
||||
size="sm"
|
||||
spacingSize="md"
|
||||
variant="border"
|
||||
color="light"
|
||||
>
|
||||
<div class="flex justify-center flex-col items-center gap-2">
|
||||
<Code size="18px" />
|
||||
|
||||
<span class="text-xs">Script</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
faCog,
|
||||
faStar
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faAppStore, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import MenuLink from './MenuLink.svelte'
|
||||
import { starStore, userStore, workspaceStore } from '$lib/stores'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
|
||||
+153
-64
@@ -1,24 +1,66 @@
|
||||
<script context="module">
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Scripts' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import type { App } from '$lib/components/apps/types'
|
||||
import { faCodeFork, faEdit, faPlay, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { AppService, Policy, type ListableApp } from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, sendUserToast } from '$lib/utils'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { AppService, ListableApp, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { faEdit, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Badge, Skeleton, Drawer, DrawerContent } from '$lib/components/common'
|
||||
|
||||
type ListableAppMarked = ListableApp & { canWrite: boolean; marked?: string }
|
||||
|
||||
let apps: ListableAppMarked[] = []
|
||||
let preFilteredApps: ListableAppMarked[] = []
|
||||
let filteredApps: ListableAppMarked[] = []
|
||||
let filter = ''
|
||||
let loading = true
|
||||
|
||||
let path: string = ''
|
||||
let initialPath = ''
|
||||
let pathError = ''
|
||||
|
||||
let apps: ListableApp[] | undefined = undefined
|
||||
async function loadApps(): Promise<void> {
|
||||
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((x) => {
|
||||
return {
|
||||
canWrite:
|
||||
x.path && x.extra_perms
|
||||
? canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore
|
||||
: false,
|
||||
...x
|
||||
}
|
||||
})
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import type { App } from '$lib/components/apps/types'
|
||||
import { goto } from '$app/navigation'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
|
||||
$: owners = Array.from(
|
||||
new Set(filteredApps?.map((x) => x.path?.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
$: preFilteredApps =
|
||||
ownerFilter != undefined ? apps.filter((x) => x.path?.startsWith(ownerFilter ?? '')) : apps
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && ($userStore || $superadmin)) {
|
||||
loadApps()
|
||||
}
|
||||
}
|
||||
async function createApp() {
|
||||
const appJson: App = {
|
||||
grid: [],
|
||||
@@ -48,21 +90,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApps(): Promise<void> {
|
||||
apps = await AppService.listApps({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
let drawerOpen = false
|
||||
|
||||
function closeDrawer() {
|
||||
drawerOpen = false
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadApps()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:open={drawerOpen} size="800px">
|
||||
@@ -79,57 +111,114 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={preFilteredApps}
|
||||
bind:filteredItems={filteredApps}
|
||||
f={(x) => x.summary + ' (' + x.path + ')'}
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader title="Apps">
|
||||
<Button size="sm" startIcon={{ icon: faPlus }} on:click={() => (drawerOpen = true)}>
|
||||
<PageHeader
|
||||
title="Apps"
|
||||
tooltip="A Script can be used standalone or as part of a Flow.
|
||||
When standalone, it has webhooks and an auto-generated UI from its parameters whom you can access clicking on 'Run'.
|
||||
Scripts have owners (users or groups) and can be shared to users and groups."
|
||||
>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={() => (drawerOpen = true)}>
|
||||
New app
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div class="p-4 border ">
|
||||
{#if !apps}
|
||||
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 mt-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[8.5]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if apps?.length == 0}
|
||||
<p class="text-xs text-gray-600 italic mt-2">No scripts yet</p>
|
||||
{:else if apps}
|
||||
<div class="grid md:grid-cols-2 gap-4 sm:grid-cols-1 xl:grid-cols-3 mt-2">
|
||||
{#each apps as { summary, path, extra_perms }}
|
||||
<a
|
||||
class="border p-4 rounded-sm shadow-sm space-y-2 hover:border-blue-600 text-gray-800 flex flex-col justify-between"
|
||||
href="/apps/get/{path}"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a href="/apps/get/{path}" class="px-6">
|
||||
<div class="font-semibold text-gray-700">
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
<div class="mb-1" />
|
||||
|
||||
<input type="text" placeholder="Search Scripts" bind:value={filter} class="text-2xl mt-2" />
|
||||
|
||||
<div class="gap-2 w-full flex flex-wrap pb-1 pt-2">
|
||||
{#each owners as owner}
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
ownerFilter = ownerFilter == owner ? undefined : owner
|
||||
}}
|
||||
color={owner === ownerFilter ? 'blue' : 'gray'}
|
||||
>
|
||||
{owner}
|
||||
{#if owner === ownerFilter}✗{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mt-2 w-full">
|
||||
{#if !loading}
|
||||
{#each filteredApps as { summary, path, extra_perms, marked, canWrite }, index (`${path}-${index}`)}
|
||||
<a
|
||||
class="border border-gray-400 p-2 rounded-sm shadow-sm hover:border-blue-600 text-gray-800"
|
||||
href="/apps/get/{path}"
|
||||
>
|
||||
<div class="flex flex-col gap-1 w-full h-full">
|
||||
<div class="font-semibold text-gray-700 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full grow gap-2 items-start">
|
||||
<div class="text-gray-700 text-xs flex flex-row flex-wrap gap-x-1 items-center">
|
||||
{path}
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
<div class="flex flex-col items-end grow pt-4">
|
||||
<div class="flex flex-row-reverse place gap-x-2 items-end">
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: faPlay }}
|
||||
href="/apps/get/{path}"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
size="xs"
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => {
|
||||
if ($workspaceStore && path) {
|
||||
AppService.deleteApp({
|
||||
workspace: $workspaceStore,
|
||||
path
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-gray-700 text-xs">
|
||||
{path}
|
||||
</p>
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-1 px-6">
|
||||
<SharedBadge canWrite={true} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse items-end w-full gap-2 pr-2 mt-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[[4]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
import gridHelp from 'svelte-grid/build/helper/index.mjs'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
let path = $page.params.path
|
||||
|
||||
|
||||
@@ -8,16 +8,21 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { AppService, AppWithLastVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faArrowLeft, faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
|
||||
async function loadApp() {
|
||||
app = await AppService.getAppByPath({ workspace: $workspaceStore!, path: $page.params.path })
|
||||
}
|
||||
|
||||
$: if ($workspaceStore) {
|
||||
loadApp()
|
||||
}
|
||||
@@ -26,10 +31,21 @@
|
||||
<Skeleton loading={app == undefined} layout={[10]} />
|
||||
|
||||
<CenteredPage>
|
||||
<a href="/apps">Back to apps</a>
|
||||
|
||||
{#if app}
|
||||
<a href="/apps/edit/{$page.params.path}">Edit</a>
|
||||
<div>{JSON.stringify(app, null, 4)} </div>
|
||||
<div class="flex justify-between my-2 items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<Button size="xs" href="/apps" startIcon={{ icon: faArrowLeft }} color="dark">
|
||||
Back to apps
|
||||
</Button>
|
||||
<div>{app.value.title}</div>
|
||||
</div>
|
||||
<Button size="xs" href="/apps/edit/{$page.params.path}" startIcon={{ icon: faPen }}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="h-screen">
|
||||
<AppEditor app={app.value} path={app.path} initialMode="preview" />
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
Reference in New Issue
Block a user