* feat(frontend): Add documentation link in the component settings * feat(frontend): Add missing type
375 lines
13 KiB
Svelte
375 lines
13 KiB
Svelte
<script lang="ts">
|
|
import Button from '$lib/components/common/button/Button.svelte'
|
|
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
|
import { getContext } from 'svelte'
|
|
import type { AppEditorContext, AppViewerContext, GridItem, RichConfiguration } from '../../types'
|
|
import PanelSection from './common/PanelSection.svelte'
|
|
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
|
|
import TableActions from './TableActions.svelte'
|
|
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
|
|
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
|
|
import { classNames, getModifierKey, isMac } from '$lib/utils'
|
|
import { buildExtraLib } from '../../utils'
|
|
import Recompute from './Recompute.svelte'
|
|
import Tooltip from '$lib/components/Tooltip.svelte'
|
|
import ComponentInputTypeEditor from './ComponentInputTypeEditor.svelte'
|
|
import AlignmentEditor from './AlignmentEditor.svelte'
|
|
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
|
|
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
|
|
import { ccomponents, components } from '../component'
|
|
import CssProperty from '../componentsPanel/CssProperty.svelte'
|
|
import GridTab from './GridTab.svelte'
|
|
import { clearErrorByComponentId, clearJobsByComponentId, deleteGridItem } from '../appUtils'
|
|
import GridPane from './GridPane.svelte'
|
|
import { slide } from 'svelte/transition'
|
|
import { push } from '$lib/history'
|
|
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
|
|
import { secondaryMenu } from './secondaryMenu'
|
|
import StylePanel from './StylePanel.svelte'
|
|
import { Delete, ExternalLink } from 'lucide-svelte'
|
|
import GridCondition from './GridCondition.svelte'
|
|
import { isTriggerable } from './script/utils'
|
|
|
|
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
|
undefined
|
|
export let rowColumns = false
|
|
export let onDelete: (() => void) | undefined = undefined
|
|
export let noGrid = false
|
|
export let duplicateMoveAllowed = true
|
|
|
|
const {
|
|
app,
|
|
runnableComponents,
|
|
selectedComponent,
|
|
worldStore,
|
|
focusedGrid,
|
|
stateId,
|
|
state,
|
|
errorByComponent,
|
|
jobs,
|
|
componentControl
|
|
} = getContext<AppViewerContext>('AppViewerContext')
|
|
|
|
const { history, movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
|
|
|
|
function removeGridElement() {
|
|
push(history, $app)
|
|
|
|
const id = componentSettings?.item?.id
|
|
const onDeleteComponentControl = id ? $componentControl[id]?.onDelete : undefined
|
|
if (onDeleteComponentControl) {
|
|
onDeleteComponentControl()
|
|
}
|
|
if (onDelete) {
|
|
onDelete()
|
|
}
|
|
|
|
if (componentSettings?.item.id) {
|
|
delete $worldStore.outputsById[componentSettings?.item.id]
|
|
$errorByComponent = clearErrorByComponentId(componentSettings?.item.id, $errorByComponent)
|
|
$jobs = clearJobsByComponentId(componentSettings?.item.id, $jobs)
|
|
|
|
if ($movingcomponents?.includes(componentSettings?.item.id)) {
|
|
$movingcomponents = $movingcomponents.filter((id) => id !== componentSettings?.item.id)
|
|
}
|
|
}
|
|
|
|
$selectedComponent = undefined
|
|
$focusedGrid = undefined
|
|
if (componentSettings?.item && !noGrid) {
|
|
let ids = deleteGridItem($app, componentSettings?.item.data, componentSettings?.parent)
|
|
for (const key of ids) {
|
|
delete $runnableComponents[key]
|
|
}
|
|
}
|
|
|
|
if (componentSettings?.item?.data?.id) {
|
|
delete $runnableComponents[componentSettings?.item?.data?.id]
|
|
}
|
|
$app = $app
|
|
$runnableComponents = $runnableComponents
|
|
|
|
onDelete?.()
|
|
}
|
|
|
|
let viewCssOptions = false
|
|
|
|
$: extraLib =
|
|
componentSettings?.item?.data?.componentInput?.type === 'template' && $worldStore
|
|
? buildExtraLib(
|
|
$worldStore?.outputsById ?? {},
|
|
componentSettings?.item?.data?.id,
|
|
false,
|
|
$state,
|
|
false
|
|
)
|
|
: undefined
|
|
|
|
function keydown(event: KeyboardEvent) {
|
|
const { key, metaKey } = event
|
|
if (key === 'Delete' || (key === 'Backspace' && metaKey)) {
|
|
removeGridElement()
|
|
event.stopPropagation()
|
|
}
|
|
}
|
|
|
|
const initialConfiguration = componentSettings?.item?.data?.type
|
|
? ccomponents[componentSettings.item.data.type].initialData.configuration
|
|
: {}
|
|
|
|
const componentInput: RichConfiguration | undefined = componentSettings?.item?.data?.type
|
|
? ccomponents[componentSettings?.item?.data?.type].initialData.componentInput
|
|
: undefined
|
|
|
|
$: componentSettings?.item?.data && ($app = $app)
|
|
|
|
const hasInteraction = componentSettings?.item.data.type
|
|
? isTriggerable(componentSettings?.item.data.type)
|
|
: false
|
|
</script>
|
|
|
|
<svelte:window on:keydown={keydown} />
|
|
|
|
{#if componentSettings?.item?.data}
|
|
{@const component = componentSettings.item.data}
|
|
<div class="flex justify-between items-center px-3 py-2 bg-gray-100">
|
|
<div class="text-xs text-gray-900 font-semibold"
|
|
>{components[componentSettings.item.data.type].name}</div
|
|
>
|
|
<a
|
|
href={components[componentSettings.item.data.type].documentationLink}
|
|
target="_blank"
|
|
class="text-frost-500 font-semibold text-xs"
|
|
>
|
|
<div class="flex flex-row gap-2">
|
|
See documentation
|
|
<ExternalLink size="16" />
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="flex min-h-full flex-col min-w-[150px] w-full divide-y">
|
|
{#if component.componentInput}
|
|
<PanelSection
|
|
title={componentSettings?.item.data.type == 'steppercomponent'
|
|
? 'Validations'
|
|
: hasInteraction
|
|
? 'Event handler'
|
|
: 'Data source'}
|
|
>
|
|
<svelte:fragment slot="action">
|
|
<span
|
|
class={classNames(
|
|
'text-white px-2 text-2xs py-0.5 font-bold rounded-sm w-fit',
|
|
'bg-indigo-500'
|
|
)}
|
|
>
|
|
{`${component.id}`}
|
|
</span>
|
|
</svelte:fragment>
|
|
|
|
{#if componentSettings.item.data.componentInput}
|
|
<ComponentInputTypeEditor
|
|
bind:componentInput={componentSettings.item.data.componentInput}
|
|
/>
|
|
|
|
<div class="flex flex-col w-full gap-2 my-2">
|
|
{#if componentSettings.item.data.componentInput.type === 'static'}
|
|
<StaticInputEditor
|
|
fieldType={componentInput?.fieldType}
|
|
subFieldType={componentInput?.subFieldType}
|
|
format={componentInput?.format}
|
|
bind:componentInput={componentSettings.item.data.componentInput}
|
|
noVariablePicker
|
|
/>
|
|
{:else if componentSettings.item.data.componentInput.type === 'template' && componentSettings.item.data.componentInput !== undefined}
|
|
<div class="py-1 min-h-[28px] rounded border border-1 border-gray-500">
|
|
<TemplateEditor
|
|
fontSize={12}
|
|
bind:code={componentSettings.item.data.componentInput.eval}
|
|
{extraLib}
|
|
/>
|
|
</div>
|
|
{:else if componentSettings.item.data.componentInput.type === 'connected' && component.componentInput !== undefined}
|
|
<ConnectedInputEditor
|
|
bind:componentInput={componentSettings.item.data.componentInput}
|
|
/>
|
|
{:else if componentSettings.item.data.componentInput?.type === 'runnable' && component.componentInput !== undefined}
|
|
<RunnableInputEditor
|
|
appComponent={component}
|
|
bind:appInput={componentSettings.item.data.componentInput}
|
|
defaultUserInput={component.type == 'formcomponent' ||
|
|
component.type == 'formbuttoncomponent'}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#key $stateId}
|
|
{#if componentSettings.item.data.componentInput?.type === 'runnable'}
|
|
{#if Object.keys(componentSettings.item.data.componentInput.fields ?? {}).length > 0}
|
|
<div class="border w-full">
|
|
<PanelSection
|
|
title={`Runnable Inputs (${
|
|
Object.keys(componentSettings.item.data.componentInput.fields ?? {}).length
|
|
})`}
|
|
>
|
|
<svelte:fragment slot="action">
|
|
<Tooltip>
|
|
The runnable inputs are inferred from the inputs of the flow or script
|
|
parameters this component is attached to.
|
|
</Tooltip>
|
|
</svelte:fragment>
|
|
|
|
<InputsSpecsEditor
|
|
id={component.id}
|
|
shouldCapitalize={false}
|
|
displayType
|
|
bind:inputSpecs={componentSettings.item.data.componentInput.fields}
|
|
userInputEnabled={component.type === 'formcomponent' ||
|
|
component.type === 'formbuttoncomponent'}
|
|
{rowColumns}
|
|
/>
|
|
</PanelSection>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/key}
|
|
</PanelSection>
|
|
{/if}
|
|
{#if Object.values(initialConfiguration).length > 0}
|
|
<PanelSection title={`Configuration (${Object.values(initialConfiguration).length})`}>
|
|
<InputsSpecsEditor
|
|
{rowColumns}
|
|
id={component.id}
|
|
inputSpecsConfiguration={initialConfiguration}
|
|
bind:inputSpecs={componentSettings.item.data.configuration}
|
|
userInputEnabled={false}
|
|
/>
|
|
</PanelSection>
|
|
{:else}
|
|
<div class="h-full w-full text-sm text-gray-500 text-center py-8 px-2"
|
|
>{ccomponents[component.type].name} has no configuration</div
|
|
>
|
|
{/if}
|
|
|
|
{#if componentSettings.item.data.type === 'tabscomponent'}
|
|
<GridTab
|
|
bind:tabs={componentSettings.item.data.tabs}
|
|
bind:disabledTabs={componentSettings.item.data.disabledTabs}
|
|
bind:component={componentSettings.item.data}
|
|
canDisableTabs
|
|
/>
|
|
{:else if componentSettings.item.data.type === 'steppercomponent'}
|
|
<GridTab
|
|
bind:tabs={componentSettings.item.data.tabs}
|
|
bind:component={componentSettings.item.data}
|
|
word="Step"
|
|
/>
|
|
{:else if componentSettings.item.data.type === 'conditionalwrapper'}
|
|
<GridCondition
|
|
bind:conditions={componentSettings.item.data.conditions}
|
|
bind:component={componentSettings.item.data}
|
|
/>
|
|
{:else if componentSettings.item.data.type === 'verticalsplitpanescomponent' || componentSettings.item.data.type === 'horizontalsplitpanescomponent'}
|
|
<GridPane
|
|
bind:panes={componentSettings.item.data.panes}
|
|
bind:component={componentSettings.item.data}
|
|
/>
|
|
{:else if componentSettings.item.data.type === 'tablecomponent' && Array.isArray(componentSettings.item.data.actionButtons)}
|
|
<TableActions id={component.id} bind:components={componentSettings.item.data.actionButtons} />
|
|
{/if}
|
|
|
|
{#if (`recomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.recomputeIds)) || componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent' || componentSettings.item.data.type === 'checkboxcomponent'}
|
|
<Recompute
|
|
bind:recomputeIds={componentSettings.item.data.recomputeIds}
|
|
ownId={component.id}
|
|
/>
|
|
{/if}
|
|
|
|
<div class="grow shrink" />
|
|
|
|
{#if Object.keys(ccomponents[component.type].customCss ?? {}).length > 0}
|
|
<PanelSection title="Styling">
|
|
<div slot="action" class="flex justify-end flex-wrap gap-1">
|
|
<Button
|
|
color="light"
|
|
size="xs"
|
|
variant="border"
|
|
on:click={() => (viewCssOptions = !viewCssOptions)}
|
|
>
|
|
{viewCssOptions ? 'Hide' : 'Show'}
|
|
</Button>
|
|
<Button
|
|
color="light"
|
|
size="xs"
|
|
variant="border"
|
|
endIcon={{ icon: faChevronRight }}
|
|
on:click={() => secondaryMenu.open(StylePanel, { component })}
|
|
>
|
|
Rich Editor
|
|
</Button>
|
|
</div>
|
|
<AlignmentEditor bind:component={componentSettings.item.data} />
|
|
{#if viewCssOptions}
|
|
<div transition:slide|local class="w-full">
|
|
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
|
|
{#if componentSettings.item.data?.customCss != undefined}
|
|
<div class="w-full mb-2">
|
|
<CssProperty
|
|
forceStyle={ccomponents[component.type].customCss[name].style != undefined}
|
|
forceClass={ccomponents[component.type].customCss[name].class != undefined}
|
|
tooltip={ccomponents[component.type].customCss[name].tooltip}
|
|
{name}
|
|
bind:value={componentSettings.item.data.customCss[name]}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</PanelSection>
|
|
{/if}
|
|
|
|
{#if duplicateMoveAllowed}
|
|
<PanelSection title="Copy/Move">
|
|
<div slot="action">
|
|
<Button size="xs" color="red" variant="border" on:click={removeGridElement}>
|
|
Delete
|
|
{#if isMac()}
|
|
<Kbd kbdClass="center-center">
|
|
<span class="text-lg leading-none">⌘</span>
|
|
<span class="px-0.5">+</span>
|
|
<Delete size={16} />
|
|
</Kbd>
|
|
{:else}
|
|
<Kbd>Del</Kbd>
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<div>
|
|
<span class="text-gray-600 text-xs mr-2"> Copy:</span>
|
|
<Kbd>{getModifierKey()}</Kbd>+<Kbd>C</Kbd>,
|
|
<Kbd>{getModifierKey()}</Kbd>+<Kbd>V</Kbd>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 text-xs mr-2">Move: </span>
|
|
<Kbd>{getModifierKey()}</Kbd>+<Kbd>X</Kbd>,
|
|
<Kbd>{getModifierKey()}</Kbd>+<Kbd>V</Kbd>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 text-xs mr-2">Navigate:</span>
|
|
<Kbd>←</Kbd>
|
|
<Kbd>↑</Kbd><Kbd>→</Kbd>
|
|
<Kbd>ESC</Kbd>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600 text-xs mr-2">Add to selection:</span>
|
|
<Kbd>⇑</Kbd>+<Kbd>click</Kbd>
|
|
</div>
|
|
</div>
|
|
</PanelSection>
|
|
{/if}
|
|
</div>
|
|
{/if}
|