mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-31 00:13:53 +00:00
729 lines
20 KiB
Svelte
729 lines
20 KiB
Svelte
<script lang="ts">
|
|
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
|
import { afterUpdate, onMount, setContext } from 'svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import { writable, type Writable } from 'svelte/store'
|
|
import { buildWorld } from '../rx'
|
|
import type {
|
|
App,
|
|
AppEditorContext,
|
|
AppViewerContext,
|
|
ConnectingInput,
|
|
ContextPanelContext,
|
|
EditorBreakpoint,
|
|
EditorMode,
|
|
FocusedGrid
|
|
} from '../types'
|
|
import AppEditorHeader from './AppEditorHeader.svelte'
|
|
import GridEditor from './GridEditor.svelte'
|
|
|
|
import { Button, Tab } from '$lib/components/common'
|
|
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
|
|
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
|
import { userStore, workspaceStore } from '$lib/stores'
|
|
import { classNames, encodeState } from '$lib/utils'
|
|
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
|
import AppPreview from './AppPreview.svelte'
|
|
import ComponentList from './componentsPanel/ComponentList.svelte'
|
|
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
|
|
|
import InlineScriptsPanel from './inlineScriptsPanel/InlineScriptsPanel.svelte'
|
|
|
|
import { page } from '$app/stores'
|
|
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
|
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
|
import { VariableService, type Policy } from '$lib/gen'
|
|
import { initHistory } from '$lib/history'
|
|
import { Component, Minus, Paintbrush, Plus } from 'lucide-svelte'
|
|
import { findGridItem, findGridItemParentGrid } from './appUtils'
|
|
import ComponentNavigation from './component/ComponentNavigation.svelte'
|
|
import CssSettings from './componentsPanel/CssSettings.svelte'
|
|
import ConnectionInstructions from './ConnectionInstructions.svelte'
|
|
import SettingsPanel from './SettingsPanel.svelte'
|
|
import {
|
|
SecondaryMenu,
|
|
secondaryMenuLeft,
|
|
secondaryMenuLeftStore,
|
|
secondaryMenuRight,
|
|
secondaryMenuRightStore
|
|
} from './settingsPanel/secondaryMenu'
|
|
import Popover from '../../Popover.svelte'
|
|
import { BG_PREFIX, migrateApp } from '../utils'
|
|
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
|
import { getTheme } from './componentsPanel/themeUtils'
|
|
import StylePanel from './settingsPanel/StylePanel.svelte'
|
|
|
|
export let app: App
|
|
export let path: string
|
|
export let policy: Policy
|
|
export let summary: string
|
|
export let fromHub: boolean = false
|
|
export let versions: number[]
|
|
|
|
migrateApp(app)
|
|
|
|
const appStore = writable<App>(app)
|
|
const selectedComponent = writable<string[] | undefined>(undefined)
|
|
const mode = writable<EditorMode>('dnd')
|
|
const breakpoint = writable<EditorBreakpoint>('lg')
|
|
const summaryStore = writable(summary)
|
|
const connectingInput = writable<ConnectingInput>({
|
|
opened: false,
|
|
input: undefined,
|
|
hoveredComponent: undefined
|
|
})
|
|
|
|
const cssEditorOpen = writable<boolean>(false)
|
|
|
|
const history = initHistory(app)
|
|
|
|
const jobsById = writable<
|
|
Record<
|
|
string,
|
|
{
|
|
job: string
|
|
component: string
|
|
result?: string
|
|
transformer?: { result?: string; error?: string }
|
|
started_at?: number
|
|
duration_ms?: number
|
|
}
|
|
>
|
|
>({})
|
|
const focusedGrid = writable<FocusedGrid | undefined>(undefined)
|
|
const pickVariableCallback: Writable<((path: string) => void) | undefined> = writable(undefined)
|
|
let context = {
|
|
email: $userStore?.email,
|
|
groups: $userStore?.groups,
|
|
username: $userStore?.username,
|
|
query: Object.fromEntries($page.url.searchParams.entries()),
|
|
hash: $page.url.hash,
|
|
workspace: $workspaceStore,
|
|
mode: 'editor'
|
|
}
|
|
const darkMode: Writable<boolean> = writable(document.documentElement.classList.contains('dark'))
|
|
|
|
const worldStore = buildWorld(context)
|
|
const previewTheme: Writable<string | undefined> = writable(undefined)
|
|
|
|
$secondaryMenuRightStore.isOpen = false
|
|
$secondaryMenuLeftStore.isOpen = false
|
|
|
|
setContext<AppViewerContext>('AppViewerContext', {
|
|
worldStore,
|
|
app: appStore,
|
|
summary: summaryStore,
|
|
initialized: writable({ initialized: false, initializedComponents: [] }),
|
|
selectedComponent,
|
|
mode,
|
|
connectingInput,
|
|
breakpoint,
|
|
runnableComponents: writable({}),
|
|
appPath: path,
|
|
workspace: $workspaceStore ?? '',
|
|
onchange: () => saveFrontendDraft(),
|
|
isEditor: true,
|
|
jobs: writable([]),
|
|
staticExporter: writable({}),
|
|
noBackend: false,
|
|
errorByComponent: writable({}),
|
|
jobsById,
|
|
openDebugRun: writable(undefined),
|
|
focusedGrid,
|
|
stateId: writable(0),
|
|
parentWidth: writable(0),
|
|
state: writable({}),
|
|
componentControl: writable({}),
|
|
hoverStore: writable(undefined),
|
|
allIdsInPath: writable([]),
|
|
darkMode,
|
|
cssEditorOpen,
|
|
previewTheme
|
|
})
|
|
|
|
let scale = writable(100)
|
|
|
|
const componentActive = writable(false)
|
|
|
|
let yTop = writable(0)
|
|
|
|
setContext<AppEditorContext>('AppEditorContext', {
|
|
yTop,
|
|
componentActive,
|
|
dndItem: writable({}),
|
|
refreshComponents: writable(undefined),
|
|
history,
|
|
pickVariableCallback,
|
|
movingcomponents: writable(undefined),
|
|
selectedComponentInEditor: writable(undefined),
|
|
jobsDrawerOpen: writable(false),
|
|
scale
|
|
})
|
|
|
|
let timeout: NodeJS.Timeout | undefined = undefined
|
|
|
|
$: $appStore && saveFrontendDraft()
|
|
|
|
function saveFrontendDraft() {
|
|
timeout && clearTimeout(timeout)
|
|
timeout = setTimeout(() => {
|
|
try {
|
|
localStorage.setItem(path != '' ? `app-${path}` : 'app', encodeState($appStore))
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}, 500)
|
|
}
|
|
|
|
function hashchange(e: HashChangeEvent) {
|
|
context.hash = e.newURL.split('#')[1]
|
|
context = context
|
|
}
|
|
|
|
$: context.mode = $mode == 'dnd' ? 'editor' : 'viewer'
|
|
|
|
$: width = $breakpoint === 'sm' ? 'min-w-[400px] max-w-[656px]' : 'min-w-[710px] w-full'
|
|
|
|
let selectedTab: 'insert' | 'settings' | 'css' = 'insert'
|
|
|
|
let befSelected: string | undefined = undefined
|
|
|
|
$: if ($selectedComponent?.[0] != befSelected && $selectedComponent?.[0] != undefined) {
|
|
befSelected = $selectedComponent?.[0]
|
|
selectedTab = 'settings'
|
|
|
|
if (befSelected) {
|
|
if (!['ctx', 'state'].includes(befSelected) && !befSelected?.startsWith(BG_PREFIX)) {
|
|
let item = findGridItem($appStore, befSelected)
|
|
if (item?.data.type === 'containercomponent' || item?.data.type === 'listcomponent') {
|
|
$focusedGrid = {
|
|
parentComponentId: befSelected,
|
|
subGridIndex: 0
|
|
}
|
|
} else if (item?.data.type === 'tabscomponent' || item?.data.type === 'steppercomponent') {
|
|
$focusedGrid = {
|
|
parentComponentId: befSelected,
|
|
subGridIndex:
|
|
($worldStore.outputsById?.[befSelected]?.selectedTabIndex?.peak() as number) ?? 0
|
|
}
|
|
} else {
|
|
let subgrid = findGridItemParentGrid($appStore, befSelected)
|
|
if (subgrid) {
|
|
try {
|
|
$focusedGrid = {
|
|
parentComponentId: subgrid.split('-')[0],
|
|
subGridIndex: parseInt(subgrid.split('-')[1])
|
|
}
|
|
} catch {}
|
|
} else {
|
|
$focusedGrid = undefined
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let itemPicker: ItemPicker | undefined = undefined
|
|
|
|
$: if ($pickVariableCallback) {
|
|
itemPicker?.openDrawer()
|
|
}
|
|
|
|
let variableEditor: VariableEditor | undefined = undefined
|
|
|
|
setContext<ContextPanelContext>('ContextPanel', {
|
|
search: writable<string>(''),
|
|
manuallyOpened: writable<Record<string, boolean>>({}),
|
|
hasResult: writable<Record<string, boolean>>({})
|
|
})
|
|
|
|
$: if ($connectingInput.opened) {
|
|
secondaryMenuRight.open(ConnectionInstructions, {}, () => {
|
|
$connectingInput.opened = false
|
|
})
|
|
secondaryMenuLeft.close()
|
|
} else {
|
|
secondaryMenuRight.close()
|
|
}
|
|
|
|
function onThemeChange() {
|
|
$darkMode = document.documentElement.classList.contains('dark')
|
|
}
|
|
|
|
let runnablePanelSize = 30
|
|
let gridPanelSize = 70
|
|
|
|
let leftPanelSize = 22
|
|
let centerPanelSize = 63
|
|
let rightPanelSize = 22
|
|
|
|
let tmpRunnablePanelSize = -1
|
|
let tmpGridPanelSize = -1
|
|
|
|
let tmpLeftPanelSize = -1
|
|
let tmpCenterPanelSize = -1
|
|
let tmpRightPanelSize = -1
|
|
|
|
let toggled = false
|
|
let cssToggled = false
|
|
|
|
$: if ($connectingInput.opened && !toggled) {
|
|
tmpRunnablePanelSize = runnablePanelSize
|
|
tmpGridPanelSize = gridPanelSize
|
|
|
|
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
|
|
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
|
|
|
|
toggled = true
|
|
} else if (!$connectingInput.opened && toggled) {
|
|
animateTo(
|
|
runnablePanelSize,
|
|
tmpRunnablePanelSize,
|
|
(newValue: number) => (runnablePanelSize = newValue)
|
|
)
|
|
animateTo(gridPanelSize, tmpGridPanelSize, (newValue: number) => (gridPanelSize = newValue))
|
|
|
|
tmpRunnablePanelSize = -1
|
|
tmpGridPanelSize = -1
|
|
|
|
toggled = false
|
|
}
|
|
|
|
// Animation logic for cssInput
|
|
$: animateCssInput($cssEditorOpen)
|
|
$: $cssEditorOpen && secondaryMenuLeft?.open(StylePanel, {})
|
|
|
|
function animateCssInput(cssEditorOpen: boolean) {
|
|
if (cssEditorOpen && !cssToggled) {
|
|
cssToggled = true
|
|
|
|
tmpLeftPanelSize = leftPanelSize
|
|
tmpCenterPanelSize = centerPanelSize
|
|
tmpRightPanelSize = rightPanelSize
|
|
|
|
animateTo(leftPanelSize, 20, (newValue: number) => (leftPanelSize = newValue))
|
|
animateTo(centerPanelSize, 55, (newValue: number) => (centerPanelSize = newValue))
|
|
animateTo(rightPanelSize, 25, (newValue: number) => (rightPanelSize = newValue))
|
|
|
|
tmpRunnablePanelSize = runnablePanelSize
|
|
tmpGridPanelSize = gridPanelSize
|
|
|
|
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
|
|
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
|
|
} else if (!cssEditorOpen && cssToggled) {
|
|
cssToggled = false
|
|
|
|
animateTo(leftPanelSize, tmpLeftPanelSize, (newValue: number) => (leftPanelSize = newValue))
|
|
animateTo(
|
|
centerPanelSize,
|
|
tmpCenterPanelSize,
|
|
(newValue: number) => (centerPanelSize = newValue)
|
|
)
|
|
animateTo(
|
|
rightPanelSize,
|
|
tmpRightPanelSize,
|
|
(newValue: number) => (rightPanelSize = newValue)
|
|
)
|
|
|
|
tmpLeftPanelSize = -1
|
|
tmpCenterPanelSize = -1
|
|
tmpRightPanelSize = -1
|
|
|
|
animateTo(
|
|
runnablePanelSize,
|
|
tmpRunnablePanelSize,
|
|
(newValue: number) => (runnablePanelSize = newValue)
|
|
)
|
|
animateTo(gridPanelSize, tmpGridPanelSize, (newValue: number) => (gridPanelSize = newValue))
|
|
|
|
tmpRunnablePanelSize = -1
|
|
tmpGridPanelSize = -1
|
|
}
|
|
}
|
|
|
|
function animateTo(start: number, end: number, onUpdate: (newValue: number) => void) {
|
|
const duration = 400
|
|
const startTime = performance.now()
|
|
|
|
function animate(time: number) {
|
|
const elapsed = time - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
const currentValue = start + (end - start) * easeInOut(progress)
|
|
onUpdate(currentValue)
|
|
if (progress < 1) {
|
|
requestAnimationFrame(animate)
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(animate)
|
|
}
|
|
|
|
function easeInOut(t: number) {
|
|
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
}
|
|
|
|
$: $cssEditorOpen && selectCss()
|
|
|
|
function selectCss() {
|
|
selectedTab !== 'css' && (selectedTab = 'css')
|
|
}
|
|
|
|
const cssId = 'wm-global-style'
|
|
|
|
$: addOrRemoveCss(true, $mode === 'preview')
|
|
|
|
let css: string | undefined = undefined
|
|
|
|
appStore.subscribe(async (currentAppStore) => {
|
|
if (!currentAppStore.theme) {
|
|
return
|
|
}
|
|
|
|
if (currentAppStore.theme.type === 'inlined') {
|
|
css = currentAppStore.theme.css
|
|
} else if (currentAppStore.theme.type === 'path' && currentAppStore.theme?.path) {
|
|
let loadedCss = await getTheme($workspaceStore!, currentAppStore.theme.path)
|
|
css = loadedCss.value
|
|
}
|
|
})
|
|
|
|
$: updateCssContent(css, $previewTheme)
|
|
|
|
function addOrRemoveCss(isPremium: boolean, isPreview: boolean = false) {
|
|
const existingElement = document.getElementById(cssId)
|
|
|
|
if (!isPremium && isPreview) {
|
|
if (existingElement) {
|
|
existingElement.remove()
|
|
}
|
|
} else {
|
|
if (!existingElement) {
|
|
const head = document.head
|
|
const link = document.createElement('style')
|
|
link.id = cssId
|
|
link.innerHTML = css ?? ''
|
|
head.appendChild(link)
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateCssContent(cssString: string | undefined, previewTheme: string | undefined) {
|
|
const theme = previewTheme ?? cssString ?? ''
|
|
|
|
const existingElement = document.getElementById(cssId)
|
|
if (existingElement && theme !== existingElement.innerHTML) {
|
|
existingElement.innerHTML = theme
|
|
}
|
|
}
|
|
|
|
let appEditorHeader: AppEditorHeader | undefined = undefined
|
|
|
|
export function triggerTutorial() {
|
|
appEditorHeader?.toggleTutorial()
|
|
}
|
|
|
|
let box
|
|
function parseScroll() {
|
|
$yTop = box?.scrollTop
|
|
}
|
|
|
|
let mounted = false
|
|
onMount(() => {
|
|
mounted = true
|
|
|
|
parseScroll()
|
|
})
|
|
|
|
$: setGridPanelSize($componentActive)
|
|
|
|
let lastComponentActive = false
|
|
|
|
afterUpdate(() => {
|
|
if ($componentActive != lastComponentActive) {
|
|
box.scrollTop = $yTop
|
|
lastComponentActive = $componentActive
|
|
}
|
|
})
|
|
|
|
function setGridPanelSize(x: boolean) {
|
|
if (mounted) {
|
|
if (x) {
|
|
gridPanelSize = 100
|
|
} else {
|
|
gridPanelSize = 100 - runnablePanelSize
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<DarkModeObserver on:change={onThemeChange} />
|
|
|
|
<svelte:window on:hashchange={hashchange} />
|
|
|
|
{#if !$userStore?.operator}
|
|
{#if $appStore}
|
|
<AppEditorHeader on:restore {versions} {policy} {fromHub} bind:this={appEditorHeader} />
|
|
{#if $mode === 'preview'}
|
|
<SplitPanesWrapper>
|
|
<div
|
|
class={twMerge(
|
|
'h-full w-full relative',
|
|
$appStore.css?.['app']?.['viewer']?.class,
|
|
'wm-app-viewer'
|
|
)}
|
|
style={$appStore.css?.['app']?.['viewer']?.style}
|
|
>
|
|
<AppPreview
|
|
workspace={$workspaceStore ?? ''}
|
|
summary={$summaryStore}
|
|
app={$appStore}
|
|
appPath={path}
|
|
{breakpoint}
|
|
{policy}
|
|
isEditor
|
|
{context}
|
|
noBackend={false}
|
|
/>
|
|
</div>
|
|
</SplitPanesWrapper>
|
|
{:else}
|
|
{#if $componentActive}
|
|
<div
|
|
class="absolute z-50 inset-0 h-full w-full bg-surface-secondary [background-size:16px_16px]"
|
|
/>
|
|
{/if}
|
|
|
|
<SplitPanesWrapper>
|
|
<Splitpanes id="o1" class="max-w-full">
|
|
<Pane bind:size={leftPanelSize} minSize={5} maxSize={33}>
|
|
<div
|
|
class={twMerge(
|
|
'w-full h-full relative',
|
|
$secondaryMenuLeftStore.isOpen ? 'overflow-hidden' : ''
|
|
)}
|
|
>
|
|
<!-- {yTop} -->
|
|
|
|
<SecondaryMenu right={false} />
|
|
<ContextPanel />
|
|
</div>
|
|
</Pane>
|
|
<Pane bind:size={centerPanelSize}>
|
|
<Splitpanes id="o2" horizontal class="!overflow-visible">
|
|
<Pane bind:size={gridPanelSize} class="ovisible">
|
|
<div
|
|
on:pointerdown={(e) => {
|
|
$selectedComponent = undefined
|
|
$focusedGrid = undefined
|
|
}}
|
|
class={twMerge(
|
|
'bg-surface-secondary h-full w-full relative',
|
|
$appStore.css?.['app']?.['viewer']?.class,
|
|
'wm-app-viewer z-[100] h-full overflow-visible'
|
|
)}
|
|
style={$appStore.css?.['app']?.['viewer']?.style}
|
|
>
|
|
<div class="absolute bottom-2 left-4 z-50">
|
|
<div class="flex flex-row gap-2 text-xs items-center">
|
|
<Button
|
|
color="light"
|
|
variant="border"
|
|
size="xs2"
|
|
disabled={$scale <= 30}
|
|
on:click={() => {
|
|
$scale -= 10
|
|
}}
|
|
>
|
|
<Minus size={14} />
|
|
</Button>
|
|
{$scale}%
|
|
<Button
|
|
color="light"
|
|
variant="border"
|
|
size="xs2"
|
|
disabled={$scale >= 100}
|
|
on:click={() => {
|
|
$scale += 10
|
|
}}
|
|
>
|
|
<Plus size={14} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="app-editor-top-level-drawer" />
|
|
<div
|
|
class="absolute inset-0 h-full w-full surface-secondary bg-[radial-gradient(#dbdbdb_1px,transparent_1px)] dark:bg-[radial-gradient(#666666_1px,transparent_1px)] [background-size:16px_16px]"
|
|
/>
|
|
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<div
|
|
bind:this={box}
|
|
on:scroll={parseScroll}
|
|
class={classNames(
|
|
'mx-auto w-full h-full z-50',
|
|
$appStore.fullscreen ? '' : 'max-w-7xl border-x',
|
|
$componentActive ? 'absolute' : 'overflow-auto'
|
|
)}
|
|
style={$componentActive ? `top: -${$yTop}px;` : ''}
|
|
>
|
|
{#if $appStore.grid}
|
|
<ComponentNavigation />
|
|
|
|
<div
|
|
on:pointerdown|stopPropagation
|
|
class={twMerge(width, 'mx-auto', 'z-10000')}
|
|
>
|
|
<GridEditor {policy} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Pane>
|
|
{#if $connectingInput?.opened == false && !$componentActive}
|
|
<Pane bind:size={runnablePanelSize}>
|
|
<div class="relative h-full w-full">
|
|
<InlineScriptsPanel />
|
|
</div>
|
|
</Pane>
|
|
{/if}
|
|
</Splitpanes>
|
|
</Pane>
|
|
<Pane bind:size={rightPanelSize} minSize={15} maxSize={33}>
|
|
<div class="relative flex flex-col h-full">
|
|
<Tabs bind:selected={selectedTab} wrapperClass="!min-h-[42px]" class="!h-full">
|
|
<Popover disappearTimeout={0} notClickable placement="bottom">
|
|
<svelte:fragment slot="text">Component library</svelte:fragment>
|
|
<Tab
|
|
value="insert"
|
|
size="xs"
|
|
class="h-full"
|
|
on:pointerdown={() => {
|
|
if ($cssEditorOpen) {
|
|
$cssEditorOpen = false
|
|
selectedTab = 'insert'
|
|
}
|
|
}}
|
|
id="app-editor-component-library-tab"
|
|
>
|
|
<div class="m-1 center-center">
|
|
<Plus size={18} />
|
|
</div>
|
|
</Tab>
|
|
</Popover>
|
|
<Popover disappearTimeout={0} notClickable placement="bottom">
|
|
<svelte:fragment slot="text">Component settings</svelte:fragment>
|
|
<Tab
|
|
value="settings"
|
|
size="xs"
|
|
class="h-full"
|
|
on:pointerdown={() => {
|
|
if ($cssEditorOpen) {
|
|
$cssEditorOpen = false
|
|
selectedTab = 'settings'
|
|
}
|
|
}}
|
|
>
|
|
<div class="m-1 center-center">
|
|
<Component size={18} />
|
|
</div>
|
|
</Tab>
|
|
</Popover>
|
|
<Popover disappearTimeout={0} notClickable placement="bottom">
|
|
<svelte:fragment slot="text">Global styling</svelte:fragment>
|
|
<Tab
|
|
value="css"
|
|
size="xs"
|
|
class="h-full"
|
|
on:pointerdown={() => {
|
|
if (!$cssEditorOpen) {
|
|
$cssEditorOpen = true
|
|
selectedTab = 'css'
|
|
}
|
|
}}
|
|
>
|
|
<div class="m-1 center-center">
|
|
<Paintbrush size={18} />
|
|
</div>
|
|
</Tab>
|
|
</Popover>
|
|
<div slot="content" class="h-full overflow-y-auto">
|
|
<TabContent class="overflow-auto h-full" value="settings">
|
|
{#if $selectedComponent !== undefined}
|
|
<SettingsPanel
|
|
on:delete={() => {
|
|
befSelected = undefined
|
|
selectedTab = 'insert'
|
|
}}
|
|
/>
|
|
<SecondaryMenu right />
|
|
{:else}
|
|
<div class="min-w-[150px] text-sm !text-secondary text-center py-8 px-2">
|
|
Select a component to see the settings for it
|
|
</div>
|
|
{/if}
|
|
</TabContent>
|
|
<TabContent value="insert">
|
|
<ComponentList />
|
|
</TabContent>
|
|
<TabContent value="css" class="h-full">
|
|
<CssSettings />
|
|
</TabContent>
|
|
</div>
|
|
</Tabs>
|
|
</div>
|
|
</Pane>
|
|
</Splitpanes>
|
|
</SplitPanesWrapper>
|
|
{/if}
|
|
{:else}
|
|
App is null
|
|
{/if}
|
|
{:else}
|
|
App editor not available to operators
|
|
{/if}
|
|
|
|
<ItemPicker
|
|
bind:this={itemPicker}
|
|
pickCallback={(path, _) => {
|
|
$pickVariableCallback?.(path)
|
|
}}
|
|
itemName="Variable"
|
|
extraField="path"
|
|
loadItems={async () =>
|
|
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
|
name: x.path,
|
|
...x
|
|
}))}
|
|
>
|
|
<div
|
|
slot="submission"
|
|
class="flex flex-row-reverse w-full bg-surface border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
|
>
|
|
<Button
|
|
variant="border"
|
|
color="blue"
|
|
size="sm"
|
|
startIcon={{ icon: faPlus }}
|
|
on:click={() => {
|
|
variableEditor?.initNew?.()
|
|
}}
|
|
>
|
|
New Variable
|
|
</Button>
|
|
</div>
|
|
</ItemPicker>
|
|
|
|
<VariableEditor bind:this={variableEditor} />
|
|
|
|
<style global>
|
|
#o1 > .splitpanes__pane {
|
|
overflow: visible !important;
|
|
}
|
|
#o2 > .splitpanes__pane {
|
|
overflow: visible !important;
|
|
}
|
|
</style>
|