feat: improve large apps performances (#5265)

This commit is contained in:
Ruben Fiszel
2025-02-11 18:05:24 +01:00
parent 5022b0c596
commit f44917feab
30 changed files with 2374 additions and 2144 deletions
@@ -89,75 +89,79 @@
configuration={configuration[key]}
/>
{/each}
<AlwaysMountedModal {css} title={resolvedConfig.modalTitle ?? ''} bind:this={modal}>
<div class="flex flex-col gap-2 px-4 w-full pt-2">
<RunnableWrapper
bind:this={runnableWrapper}
bind:loading
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
{id}
{extraQueryParams}
autoRefresh={false}
forceSchemaDisplay={true}
runnableClass="!block"
{outputs}
doOnSuccess={resolvedConfig.onSuccess}
doOnError={resolvedConfig.onError}
{errorHandledByComponent}
>
{#if noInputs}
<div class="text-secondary italic text-sm my-4">
Run forms are associated with a runnable that has user inputs.
<br />
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
<strong>
User Input
<User size={20} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
</strong>
{#if render}
<AlwaysMountedModal {css} title={resolvedConfig.modalTitle ?? ''} bind:this={modal}>
<div class="flex flex-col gap-2 px-4 w-full pt-2">
<RunnableWrapper
bind:this={runnableWrapper}
bind:loading
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
{id}
{extraQueryParams}
autoRefresh={false}
forceSchemaDisplay={true}
runnableClass="!block"
{outputs}
doOnSuccess={resolvedConfig.onSuccess}
doOnError={resolvedConfig.onError}
{errorHandledByComponent}
>
{#if noInputs}
<div class="text-secondary italic text-sm my-4">
Run forms are associated with a runnable that has user inputs.
<br />
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
<strong>
User Input
<User size={20} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
</strong>
</div>
{/if}
<div class="flex justify-end gap-3 p-2">
<Button
{loading}
btnClasses="my-1"
on:pointerdown={(e) => {
e?.stopPropagation()
}}
on:click={async () => {
if (!runnableComponent) {
runnableWrapper?.handleSideEffect(true)
} else {
await runnableComponent?.runComponent()
}
modal?.close()
}}
size="xs"
color="dark"
>
Submit
</Button>
</div>
{/if}
<div class="flex justify-end gap-3 p-2">
<Button
{loading}
btnClasses="my-1"
on:pointerdown={(e) => {
e?.stopPropagation()
}}
on:click={async () => {
if (!runnableComponent) {
runnableWrapper?.handleSideEffect(true)
} else {
await runnableComponent?.runComponent()
}
modal?.close()
}}
size="xs"
color="dark"
>
Submit
</Button>
</div>
</RunnableWrapper>
</div>
</AlwaysMountedModal>
</RunnableWrapper>
</div>
</AlwaysMountedModal>
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
{#if errorsMessage}
<div class="text-red-500 text-xs">{errorsMessage}</div>
{/if}
<Button
disabled={resolvedConfig.disabled ?? false}
size={resolvedConfig.size ?? 'md'}
color={resolvedConfig.color}
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-form-button')}
style={css?.button?.style ?? ''}
on:click={(e) => {
modal?.open()
}}
>
{resolvedConfig.label}
</Button>
</AlignWrapper>
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
{#if errorsMessage}
<div class="text-red-500 text-xs">{errorsMessage}</div>
{/if}
<Button
disabled={resolvedConfig.disabled ?? false}
size={resolvedConfig.size ?? 'md'}
color={resolvedConfig.color}
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-form-button')}
style={css?.button?.style ?? ''}
on:click={(e) => {
modal?.open()
}}
>
{resolvedConfig.label}
</Button>
</AlignWrapper>
{:else}
<RunnableWrapper {outputs} {render} {componentInput} {id} />
{/if}
@@ -18,17 +18,21 @@
export let initializing: boolean | undefined
export let componentContainerHeight: number
type AccordionListValue = { header: string; [key: string]: any };
type AccordionListValue = { header: string; [key: string]: any }
type InternalAccordionListInput = AppInput & {
value: AccordionListValue[];
};
value: AccordionListValue[]
}
$: accordionInput = componentInput as InternalAccordionListInput;
$: accordionInput = componentInput as InternalAccordionListInput
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
let everRender = render
$: render && !everRender && (everRender = true)
let activeIndex: number = 0
const outputs = initOutput($worldStore, id, {
@@ -61,7 +65,6 @@
activeIndex = activeIndex === index ? -1 : index
outputs.activeIndex.set(activeIndex)
}
</script>
{#each Object.keys(css ?? {}) as key (key)}
@@ -85,27 +88,27 @@
bind:initializing
bind:result
>
<div class="w-full flex flex-col overflow-auto max-h-full">
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#each result ?? [] as value, index}
<div class="border-b">
<button
on:pointerdown|stopPropagation
on:click={() => toggleAccordion(index)}
class={twMerge(
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
'wm-tabs-alltabs',
activeIndex === index
? twMerge('bg-surface text-primary ', 'wm-tabs-selectedTab')
: 'text-secondary'
)}
>
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
{result[index]?.header || `Header ${index}`}
</button>
{#if activeIndex === index}
<div class="p-2 overflow-auto w-full">
{#if everRender}
<div class="w-full flex flex-col overflow-auto max-h-full">
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#each result ?? [] as value, index}
<div class="border-b">
<button
on:pointerdown|stopPropagation
on:click={() => toggleAccordion(index)}
class={twMerge(
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
'wm-tabs-alltabs',
activeIndex === index
? twMerge('bg-surface text-primary ', 'wm-tabs-selectedTab')
: 'text-secondary'
)}
>
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
{result[index]?.header || `Header ${index}`}
</button>
<div class="overflow-auto w-full">
<ListWrapper
onSet={(id, value) => {
if (!inputs[id]) {
@@ -133,8 +136,8 @@
>
<SubGridEditor
{id}
visible={render}
class={twMerge(css?.container?.class, 'wm-accordion')}
visible={render && index === activeIndex}
class={twMerge(css?.container?.class, 'wm-accordion p-2')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight -
@@ -148,17 +151,21 @@
/>
</ListWrapper>
</div>
{/if}
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
{/if}
{/if}
{/if}
{/if}
</div>
</div>
{:else if $app.subgrids}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{/if}
</RunnableWrapper>
@@ -27,6 +27,9 @@
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
let everRender = render
$: render && !everRender && (everRender = true)
const outputs = initOutput($worldStore, id, {
result: undefined,
loading: false,
@@ -90,117 +93,123 @@
bind:initializing
bind:result
>
<div class="w-full flex flex-wrap overflow-auto divide-y max-h-full">
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#key result}
<Carousel
particlesToShow={1}
particlesToScroll={1}
autoplay={false}
autoplayProgressVisible={false}
timingFunction={resolvedConfig.timingFunction}
dots={true}
arrows={true}
swiping={false}
bind:this={carousel}
on:pageChange={(event) => {
currentPageIndex = event.detail
$focusedGrid = {
parentComponentId: id,
subGridIndex: event.detail
}
}}
>
<div slot="prev" class="h-full flex justify-center flex-col p-2">
<div>
<Button
color="light"
on:click={() => {
const pagesCount = result?.length ?? 0
{#if everRender}
<div class="w-full flex flex-wrap overflow-auto divide-y max-h-full">
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#key result}
<Carousel
particlesToShow={1}
particlesToScroll={1}
autoplay={false}
autoplayProgressVisible={false}
timingFunction={resolvedConfig.timingFunction}
dots={true}
arrows={true}
swiping={false}
bind:this={carousel}
on:pageChange={(event) => {
currentPageIndex = event.detail
$focusedGrid = {
parentComponentId: id,
subGridIndex: event.detail
}
}}
>
<div slot="prev" class="h-full flex justify-center flex-col p-2">
<div>
<Button
color="light"
on:click={() => {
const pagesCount = result?.length ?? 0
if (currentPageIndex > 0) {
carousel.goTo(currentPageIndex - 1)
} else {
carousel.goTo(pagesCount - 1)
}
}}
>
<ArrowLeftCircle size={16} />
</Button>
</div>
</div>
<div slot="next" class="h-full flex justify-center flex-col p-2">
<div>
<Button
color="light"
on:click={() => {
const pagesCount = result?.length ?? 0
if (currentPageIndex < pagesCount - 1) {
carousel.goTo(currentPageIndex + 1)
} else {
carousel.goTo(0)
}
}}
>
<ArrowRightCircle size={16} />
</Button>
</div>
</div>
{#each result ?? [] as value, index}
<div class="overflow-auto w-full">
<ListWrapper
onSet={(id, value) => {
if (!inputs[id]) {
inputs[id] = { [index]: value }
} else {
inputs[id] = { ...inputs[id], [index]: value }
}
outputs?.inputs.set(inputs, true)
}}
onRemove={(id) => {
if (inputs?.[id] == undefined) {
return
}
if (index == 0) {
delete inputs[id]
inputs = { ...inputs }
} else {
delete inputs[id][index]
inputs[id] = { ...inputs[id] }
}
outputs?.inputs.set(inputs, true)
}}
{value}
{index}
>
<SubGridEditor
{id}
visible={render}
class={twMerge(css?.container?.class, 'wm-carousel')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight - 40}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
if (currentPageIndex > 0) {
carousel.goTo(currentPageIndex - 1)
} else {
carousel.goTo(pagesCount - 1)
}
onFocus()
}}
/>
</ListWrapper>
>
<ArrowLeftCircle size={16} />
</Button>
</div>
</div>
{/each}
</Carousel>
{/key}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
<div slot="next" class="h-full flex justify-center flex-col p-2">
<div>
<Button
color="light"
on:click={() => {
const pagesCount = result?.length ?? 0
if (currentPageIndex < pagesCount - 1) {
carousel.goTo(currentPageIndex + 1)
} else {
carousel.goTo(0)
}
}}
>
<ArrowRightCircle size={16} />
</Button>
</div>
</div>
{#each result ?? [] as value, index}
<div class="overflow-auto w-full">
<ListWrapper
onSet={(id, value) => {
if (!inputs[id]) {
inputs[id] = { [index]: value }
} else {
inputs[id] = { ...inputs[id], [index]: value }
}
outputs?.inputs.set(inputs, true)
}}
onRemove={(id) => {
if (inputs?.[id] == undefined) {
return
}
if (index == 0) {
delete inputs[id]
inputs = { ...inputs }
} else {
delete inputs[id][index]
inputs[id] = { ...inputs[id] }
}
outputs?.inputs.set(inputs, true)
}}
{value}
{index}
>
<SubGridEditor
{id}
visible={render}
class={twMerge(css?.container?.class, 'wm-carousel')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight - 40}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</ListWrapper>
</div>
{/each}
</Carousel>
{/key}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
{/if}
{/if}
{/if}
{/if}
</div>
</div>
{:else if $app.subgrids}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{/if}
</RunnableWrapper>
@@ -119,4 +119,6 @@
<div id={divId} />
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} />
{/if}
@@ -35,25 +35,29 @@
/>
{/each}
<div
on:pointerdown={(e) => {
e?.preventDefault()
}}
class="h-full w-full"
>
<RunnableWrapper
{outputs}
{render}
autoRefresh
{componentInput}
{id}
bind:initializing
bind:result
{#if render}
<div
on:pointerdown={(e) => {
e?.preventDefault()
}}
class="h-full w-full"
>
<div class="w-full h-full overflow-auto">
{#key result}
{@html result}
{/key}
</div>
</RunnableWrapper>
</div>
<RunnableWrapper
{outputs}
{render}
autoRefresh
{componentInput}
{id}
bind:initializing
bind:result
>
<div class="w-full h-full overflow-auto">
{#key result}
{@html result}
{/key}
</div>
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} render={false} autoRefresh {componentInput} {id} />
{/if}
@@ -62,32 +62,36 @@
/>
{/each}
<div
on:pointerdown={(e) => {
e?.preventDefault()
}}
class={classNames(
'h-full w-full overflow-y-auto prose max-w-full',
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
css?.container?.class,
' dark:prose-invert',
'wm-markdown'
)}
style={css?.container?.style}
>
<RunnableWrapper
{outputs}
{render}
autoRefresh
{componentInput}
{id}
bind:initializing
bind:result
{#if render}
<div
on:pointerdown={(e) => {
e?.preventDefault()
}}
class={classNames(
'h-full w-full overflow-y-auto prose max-w-full',
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
css?.container?.class,
' dark:prose-invert',
'wm-markdown'
)}
style={css?.container?.style}
>
{#if result}
{#key result}
<Markdown md={result} plugins={[gfmPlugin()]} />
{/key}
{/if}
</RunnableWrapper>
</div>
<RunnableWrapper
{outputs}
render={true}
autoRefresh
{componentInput}
{id}
bind:initializing
bind:result
>
{#if result}
{#key result}
<Markdown md={result} plugins={[gfmPlugin()]} />
{/key}
{/if}
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} render={false} autoRefresh {componentInput} {id} />
{/if}
@@ -217,7 +217,7 @@
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex flex-wrap gap-2 pb-0.5 w-full {$mode === 'dnd' &&
class="flex flex-wrap gap-0.5 pb-0.5 w-full {$mode === 'dnd' &&
(componentInput?.type == 'template' || componentInput?.type == 'templatev2')
? 'cursor-text'
: 'overflow-auto'}"
@@ -225,7 +225,7 @@
<svelte:element
this={component}
class={twMerge(
'whitespace-pre-wrap w-full',
'whitespace-pre-wrap grow',
css?.text?.class,
classes,
@@ -244,7 +244,7 @@
</svelte:element>
{#if resolvedConfig.copyButton && result}
<div class="flex">
<div class="flex items-center">
<Button
title="Copy to clipboard"
variant="border"
@@ -103,15 +103,19 @@
/>
{/each}
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
{#if error != ''}
<div class="flex flex-col h-full w-full overflow-auto">
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
</Alert>
</div>
{/if}
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{#if render}
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
{#if error != ''}
<div class="flex flex-col h-full w-full overflow-auto">
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
</Alert>
</div>
{/if}
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} {render} {componentInput} {id} />
{/if}
@@ -169,15 +169,19 @@
{/each}
{/if}
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
{#if error != ''}
<div class="flex flex-col h-full w-full overflow-auto">
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
</Alert>
</div>
{/if}
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{#if render}
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
{#if error != ''}
<div class="flex flex-col h-full w-full overflow-auto">
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
</Alert>
</div>
{/if}
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} {render} {componentInput} {id} />
{/if}
@@ -67,8 +67,12 @@
<InputValue key={'canvas'} {id} input={configuration.canvas} bind:value={canvas} />
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{#if render}
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>
{:else}
<RunnableWrapper {outputs} {render} {componentInput} {id} />
{/if}
@@ -695,6 +695,7 @@
{/if}
</div>
</RunnableWrapper>
<Portal name="db-explorer">
<Drawer bind:this={insertDrawer} size="800px">
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
@@ -84,6 +84,4 @@
<RefreshIndicator {loading} />
</div>
</div>
{:else}
<div class="w-full h-full" />
{/if}
@@ -31,6 +31,9 @@
selectedTabIndex: 0
})
let everRender = render
$: render && !everRender && (everRender = true)
function onFocus() {
$focusedGrid = {
parentComponentId: id,
@@ -97,23 +100,29 @@
<InitializeComponent {id} />
<div class="w-full h-full">
{#if $app.subgrids}
{#each resolvedConditions ?? [] as _res, i}
<SubGridEditor
visible={render && i == selectedConditionIndex}
{id}
class={twMerge(css?.container?.class, 'wm-conditional-tabs')}
style={css?.container?.style}
subGridId={`${id}-${i}`}
containerHeight={componentContainerHeight}
on:focus={(e) => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
{/each}
{/if}
</div>
{#if everRender}
<div class="w-full h-full">
{#if $app.subgrids}
{#each resolvedConditions ?? [] as _res, i}
<SubGridEditor
visible={render && i == selectedConditionIndex}
{id}
class={twMerge(css?.container?.class, 'wm-conditional-tabs')}
style={css?.container?.style}
subGridId={`${id}-${i}`}
containerHeight={componentContainerHeight}
on:focus={(e) => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
{/each}
{/if}
</div>
{:else if $app.subgrids}
{#each resolvedConditions ?? [] as _res, i}
<SubGridEditor visible={false} {id} subGridId={`${id}-${i}`} />
{/each}
{/if}
@@ -21,6 +21,9 @@
const { app, focusedGrid, selectedComponent, worldStore, connectingInput, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
let everRender = render
$: render && !everRender && (everRender = true)
let groupContext = writable({})
let outputs = initOutput($worldStore, id, { group: $groupContext })
@@ -66,23 +69,29 @@
{/if}
{/each}
<div class="w-full h-full">
{#if $app.subgrids?.[`${id}-0`]}
<GroupWrapper {id} context={groupContext}>
<SubGridEditor
visible={render}
{id}
class={twMerge(css?.container?.class, 'wm-container')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</GroupWrapper>
{/if}
</div>
{#if everRender}
<div class="w-full h-full">
{#if $app.subgrids?.[`${id}-0`]}
<GroupWrapper {id} context={groupContext}>
<SubGridEditor
visible={render}
{id}
class={twMerge(css?.container?.class, 'wm-container')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</GroupWrapper>
{/if}
</div>
{:else if $app.subgrids?.[`${id}-0`]}
<GroupWrapper {id} context={groupContext}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</GroupWrapper>
{/if}
@@ -20,6 +20,9 @@
export let render: boolean
export let nodes: DecisionTreeNode[]
let everRender = render
$: render && !everRender && (everRender = true)
const {
app,
focusedGrid,
@@ -182,55 +185,63 @@
<InitializeComponent {id} />
<div class="w-full overflow-auto">
<div class="w-full">
{#if $app.subgrids}
{#each Object.values(nodes) ?? [] as node, i}
<SubGridEditor
visible={render && node.id === currentNodeId}
{id}
class={twMerge(css?.container?.class, 'wm-decision-tree')}
style={css?.container?.style}
subGridId={`${id}-${i}`}
containerHeight={componentContainerHeight - 40}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus(i)
}}
/>
{/each}
{/if}
{#if everRender}
<div class="w-full overflow-auto">
<div class="w-full">
{#if $app.subgrids}
{#each Object.values(nodes) ?? [] as node, i}
<SubGridEditor
visible={render && node.id === currentNodeId}
{id}
class={twMerge(css?.container?.class, 'wm-decision-tree')}
style={css?.container?.style}
subGridId={`${id}-${i}`}
containerHeight={componentContainerHeight - 40}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus(i)
}}
/>
{/each}
{/if}
</div>
</div>
</div>
<div class="h-8 flex flex-row gap-2 justify-end items-center px-2 bg-surface-primary z-50">
{#if isDebugging($debuggingComponents, id)}
<Badge color="red" size="xs2">
{`Debugging. Actions are disabled.`}
</Badge>
{#if render}
<div class="h-8 flex flex-row gap-2 justify-end items-center px-2 bg-surface-primary z-50">
{#if isDebugging($debuggingComponents, id)}
<Badge color="red" size="xs2">
{`Debugging. Actions are disabled.`}
</Badge>
{/if}
{#if getFirstNode(nodes)?.id !== currentNodeId}
<Button
on:click={prev}
size="xs2"
color="light"
startIcon={{ icon: ArrowLeft }}
disabled={isDebugging($debuggingComponents, id)}
>
Prev
</Button>
{/if}
<Button
on:click={next}
size="xs2"
color="dark"
endIcon={{ icon: ArrowRight }}
disabled={isNextDisabled ||
currentNodeId === lastNodeId ||
isDebugging($debuggingComponents, id)}
>
Next
</Button>
</div>
{/if}
{#if getFirstNode(nodes)?.id !== currentNodeId}
<Button
on:click={prev}
size="xs2"
color="light"
startIcon={{ icon: ArrowLeft }}
disabled={isDebugging($debuggingComponents, id)}
>
Prev
</Button>
{/if}
<Button
on:click={next}
size="xs2"
color="dark"
endIcon={{ icon: ArrowRight }}
disabled={isNextDisabled ||
currentNodeId === lastNodeId ||
isDebugging($debuggingComponents, id)}
>
Next
</Button>
</div>
{:else if $app.subgrids}
{#each Object.values(nodes) ?? [] as _node, i}
<SubGridEditor visible={false} {id} subGridId={`${id}-${i}`} />
{/each}
{/if}
@@ -24,6 +24,9 @@
export let onOpenRecomputeIds: string[] | undefined = undefined
export let onCloseRecomputeIds: string[] | undefined = undefined
let everRender = render
$: render && !everRender && (everRender = true)
const {
app,
focusedGrid,
@@ -77,98 +80,103 @@
{/each}
<InitializeComponent {id} />
<div class="h-full w-full">
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
btnClasses={twMerge(css?.button?.class, 'wm-drawer-button')}
wrapperClasses={twMerge(
css?.container?.class,
'wm-drawer-button-container',
resolvedConfig?.fillContainer ? 'w-full h-full' : '',
resolvedConfig?.hideButtonOnView && $mode == 'preview'
? 'invisible h-0 overflow-hidden'
: ''
)}
wrapperStyle={css?.container?.style}
disabled={resolvedConfig?.disabled}
on:pointerdown={(e) => {
e?.stopPropagation()
}}
on:click={async (e) => {
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
appDrawer.toggleDrawer()
}}
size={resolvedConfig.size}
color={resolvedConfig.color}
style={css?.button?.style}
>
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
</Button>
</AlignWrapper>
</div>
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
<Drawer
let:open
bind:this={appDrawer}
size="800px"
alwaysOpen
positionClass={$mode == 'dnd' ? '!absolute' : '!fixed'}
shouldUsePortal={false}
on:open={() => {
outputs?.open.set(true)
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
on:close={() => {
outputs?.open.set(false)
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
>
<DrawerContent
title={resolvedConfig.drawerTitle}
on:close={() => {
appDrawer?.toggleDrawer()
$focusedGrid = undefined
}}
fullScreen={$mode !== 'dnd'}
>
<div
class={twMerge('h-full', css?.drawer?.class, 'wm-drawer')}
style={css?.drawer?.style}
{#if render}
<div class="h-full w-full">
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
btnClasses={twMerge(css?.button?.class, 'wm-drawer-button')}
wrapperClasses={twMerge(
css?.container?.class,
'wm-drawer-button-container',
resolvedConfig?.fillContainer ? 'w-full h-full' : '',
resolvedConfig?.hideButtonOnView && $mode == 'preview'
? 'invisible h-0 overflow-hidden'
: ''
)}
wrapperStyle={css?.container?.style}
disabled={resolvedConfig?.disabled}
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
on:click={async (e) => {
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
appDrawer.toggleDrawer()
}}
size={resolvedConfig.size}
color={resolvedConfig.color}
style={css?.button?.style}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
/>
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
</div>
</DrawerContent>
</Drawer>
</Portal>
</Button>
</AlignWrapper>
</div>
{/if}
{#if everRender}
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
<Drawer
let:open
bind:this={appDrawer}
size="800px"
alwaysOpen
positionClass={$mode == 'dnd' ? '!absolute' : '!fixed'}
shouldUsePortal={false}
on:open={() => {
outputs?.open.set(true)
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
on:close={() => {
outputs?.open.set(false)
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
>
<DrawerContent
title={resolvedConfig.drawerTitle}
on:close={() => {
appDrawer?.toggleDrawer()
$focusedGrid = undefined
}}
fullScreen={$mode !== 'dnd'}
>
<div
class={twMerge('h-full', css?.drawer?.class, 'wm-drawer')}
style={css?.drawer?.style}
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
/>
{/if}
</div>
</DrawerContent>
</Drawer>
</Portal>
{:else if $app.subgrids?.[`${id}-0`]}
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
{/if}
@@ -26,6 +26,9 @@
getContext<AppViewerContext>('AppViewerContext')
let page = 0
let everRender = render
$: render && !everRender && (everRender = true)
const outputs = initOutput($worldStore, id, {
result: undefined,
loading: false,
@@ -72,8 +75,8 @@
const l = initialData ? initialData.length : 0
if (mode === 'auto') {
const pageSize: number = configuration.auto.pageSize ?? 0
const shouldDisplayPagination = pageSize < l ?? false
const total = Math.ceil(l / pageSize ?? 0)
const shouldDisplayPagination = (pageSize ?? 0) < l
const total = Math.ceil(l / (pageSize ?? 0))
return {
shouldDisplayPagination,
@@ -136,136 +139,144 @@
bind:result
bind:loading
>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-list')}
style={css?.container?.style}
>
{#if everRender}
<div
class="w-full h-full shrink flex {$allIdsInPath.includes(id) && $mode == 'dnd'
? 'overflow-visible'
: 'overflow-auto'} {isCard
? 'gap-2 flex-wrap'
: resolvedConfig?.displayBorders
? 'divide-y flex-col'
: 'flex-col'}"
class={twMerge('w-full h-full', css?.container?.class, 'wm-list')}
style={css?.container?.style}
>
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#each result ?? [] as value, index (index)}
{@const inRange = index <= pagination.maxIndex && index >= pagination.indexOffset}
<div
style={inRange
? `${
isCard
? `min-width: ${resolvedConfig.width?.configuration?.card?.minWidthPx}px; `
: ''
} max-height: ${resolvedConfig.heightPx}px;`
: ''}
class={inRange
? `${
$allIdsInPath.includes(id)
? 'overflow-visible'
: resolvedConfig.heightPx
? 'overflow-auto'
: ''
} ${!isCard ? 'w-full' : resolvedConfig?.displayBorders ? 'border' : ''}`
: 'h-0 float overflow-hidden invisible absolute'}
>
<ListWrapper
onSet={(id, value) => {
if (!inputs[id]) {
inputs[id] = { [index]: value }
} else {
inputs[id] = { ...inputs[id], [index]: value }
}
outputs?.inputs.set(inputs, true)
}}
onRemove={(id) => {
if (inputs?.[id] == undefined) {
return
}
if (index == 0) {
delete inputs[id]
inputs = { ...inputs }
} else {
delete inputs[id][index]
inputs[id] = { ...inputs[id] }
}
outputs?.inputs.set(inputs, true)
}}
{value}
{index}
<div
class="w-full h-full shrink flex {$allIdsInPath.includes(id) && $mode == 'dnd'
? 'overflow-visible'
: 'overflow-auto'} {isCard
? 'gap-2 flex-wrap'
: resolvedConfig?.displayBorders
? 'divide-y flex-col'
: 'flex-col'}"
>
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#each result ?? [] as value, index (index)}
{@const inRange = index <= pagination.maxIndex && index >= pagination.indexOffset}
<div
style={inRange
? `${
isCard
? `min-width: ${resolvedConfig.width?.configuration?.card?.minWidthPx}px; `
: ''
} max-height: ${resolvedConfig.heightPx}px;`
: ''}
class={inRange
? `${
$allIdsInPath.includes(id)
? 'overflow-visible'
: resolvedConfig.heightPx
? 'overflow-auto'
: ''
} ${!isCard ? 'w-full' : resolvedConfig?.displayBorders ? 'border' : ''}`
: 'h-0 float overflow-hidden invisible absolute'}
>
<SubGridEditor
visible={render && inRange}
{id}
subGridId={`${id}-0`}
containerHeight={resolvedConfig.heightPx}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
<ListWrapper
onSet={(id, value) => {
if (!inputs[id]) {
inputs[id] = { [index]: value }
} else {
inputs[id] = { ...inputs[id], [index]: value }
}
onFocus()
outputs?.inputs.set(inputs, true)
}}
/>
</ListWrapper>
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
onRemove={(id) => {
if (inputs?.[id] == undefined) {
return
}
if (index == 0) {
delete inputs[id]
inputs = { ...inputs }
} else {
delete inputs[id][index]
inputs[id] = { ...inputs[id] }
}
outputs?.inputs.set(inputs, true)
}}
{value}
{index}
>
<SubGridEditor
visible={render && inRange}
{id}
subGridId={`${id}-0`}
containerHeight={resolvedConfig.heightPx}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</ListWrapper>
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
{/if}
{/if}
{/if}
</div>
{#if pagination.shouldDisplayPagination}
<div
class="bg-surface-secondary z-20 h-8 flex flex-row gap-1 p-1 items-center wm-list-pagination absolute bottom-0 w-full"
>
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isPreviousLoading = true
page = page - 1
outputs?.page.set(page, true)
}}
disabled={page === 0}
>
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</Button>
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isNextLoading = true
page = page + 1
outputs?.page.set(page, true)
}}
disabled={pagination.disableNext && pagination.total > 0}
>
Next
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</Button>
<div class="text-xs"
>{page + 1} {pagination.total > 0 ? `of ${pagination.total}` : ''}</div
>
</div>
{/if}
</div>
{#if pagination.shouldDisplayPagination}
<div
class="bg-surface-secondary z-20 h-8 flex flex-row gap-1 p-1 items-center wm-list-pagination absolute bottom-0 w-full"
>
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isPreviousLoading = true
page = page - 1
outputs?.page.set(page, true)
}}
disabled={page === 0}
>
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</Button>
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isNextLoading = true
page = page + 1
outputs?.page.set(page, true)
}}
disabled={pagination.disableNext && pagination.total > 0}
>
Next
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</Button>
<div class="text-xs">{page + 1} {pagination.total > 0 ? `of ${pagination.total}` : ''}</div>
</div>
{/if}
</div>
{:else if $app.subgrids}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{/if}
</RunnableWrapper>
@@ -40,6 +40,9 @@
breakpoint
} = getContext<AppViewerContext>('AppViewerContext')
let everRender = render
$: render && !everRender && (everRender = true)
//used so that we can count number of outputs setup for first refresh
const outputs = initOutput($worldStore, id, {
open: false
@@ -110,7 +113,7 @@
/>
{/each}
{#if render}
{#if everRender}
<div class="h-full w-full">
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
@@ -144,92 +147,94 @@
</Button>
</AlignWrapper>
</div>
{/if}
<Portal target="#app-editor-top-level-drawer" name="app-modal">
<Disposable
{id}
let:handleClickAway
let:zIndex
let:open
bind:this={disposable}
on:open={() => {
outputs?.open.set(true)
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
on:close={() => {
outputs?.open.set(false)
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
>
<div
class={twMerge(
`${
$mode == 'dnd' ? 'absolute' : 'fixed'
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible'
)}
style="z-index: {zIndex}"
bind:clientHeight={wrapperHeight}
<Portal target="#app-editor-top-level-drawer" name="app-modal">
<Disposable
{id}
let:handleClickAway
let:zIndex
let:open
bind:this={disposable}
on:open={() => {
outputs?.open.set(true)
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
on:close={() => {
outputs?.open.set(false)
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
>
<div
style={css?.popup?.style}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
on:click_outside={(e) => {
if ($mode !== 'dnd' && !unclickableOutside) {
handleClickAway(e)
}
}}
class={twMerge(
`${
$mode == 'dnd' ? 'absolute' : 'fixed'
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible'
)}
style="z-index: {zIndex}"
bind:clientHeight={wrapperHeight}
>
<div
class="px-4 py-2 border-b flex justify-between items-center"
bind:clientHeight={headerHeight}
>
<div>{resolvedConfig.modalTitle}</div>
<div class="w-8">
<button
on:click|stopPropagation={() => {
disposable?.closeDrawer()
}}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
</button>
</div>
</div>
<div
class={twMerge('wm-modal h-full', 'overflow-y-auto')}
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
style={css?.popup?.style}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
on:click_outside={(e) => {
if ($mode !== 'dnd' && !unclickableOutside) {
handleClickAway(e)
}
}}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
{containerHeight}
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
<div
class="px-4 py-2 border-b flex justify-between items-center"
bind:clientHeight={headerHeight}
>
<div>{resolvedConfig.modalTitle}</div>
<div class="w-8">
<button
on:click|stopPropagation={() => {
disposable?.closeDrawer()
}}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
</button>
</div>
</div>
<div
class={twMerge('wm-modal h-full', 'overflow-y-auto')}
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}}
/>
{/if}
}
}}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
{containerHeight}
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
/>
{/if}
</div>
</div>
</div>
</div>
</Disposable>
</Portal>
</Disposable>
</Portal>
{:else if $app.subgrids?.[`${id}-0`]}
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
{/if}
@@ -25,6 +25,10 @@
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
let everRender = render
$: render && !everRender && (everRender = true)
function onFocus() {
$focusedGrid = {
parentComponentId: id,
@@ -82,47 +86,53 @@
<InitializeComponent {id} />
<div class="h-full w-full border" on:pointerdown={onFocus}>
{#key sumedup}
<Splitpanes {horizontal}>
{#each sumedup as paneSize, index (index)}
<Pane size={paneSize} minSize={20}>
<div
class="w-full h-full"
on:pointerdown|stopPropagation={() => {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: index
}
}}
>
{#if $app.subgrids?.[`${id}-${index}`]}
<SubGridEditor
visible={render}
{id}
shouldHighlight={$focusedGrid?.subGridIndex === index}
class={twMerge(
css?.container?.class,
horizontal ? 'wm-horizontal-split-panes' : 'wm-vertical-split-panes'
)}
style={css?.container?.style}
subGridId={`${id}-${index}`}
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: index
{#if everRender}
<div class="h-full w-full border" on:pointerdown={onFocus}>
{#key sumedup}
<Splitpanes {horizontal}>
{#each sumedup as paneSize, index (index)}
<Pane size={paneSize} minSize={20}>
<div
class="w-full h-full"
on:pointerdown|stopPropagation={() => {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: index
}
}}
>
{#if $app.subgrids?.[`${id}-${index}`]}
<SubGridEditor
visible={render}
{id}
shouldHighlight={$focusedGrid?.subGridIndex === index}
class={twMerge(
css?.container?.class,
horizontal ? 'wm-horizontal-split-panes' : 'wm-vertical-split-panes'
)}
style={css?.container?.style}
subGridId={`${id}-${index}`}
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: index
}
}
}
}}
/>
{/if}
</div>
</Pane>
{/each}
</Splitpanes>
{/key}
</div>
}}
/>
{/if}
</div>
</Pane>
{/each}
</Splitpanes>
{/key}
</div>
{:else}
{#each sumedup as _paneSize, index (index)}
<SubGridEditor visible={false} {id} subGridId={`${id}-${index}`} />
{/each}
{/if}
@@ -36,6 +36,9 @@
runnableComponents
} = getContext<AppViewerContext>('AppViewerContext')
let everRender = render
$: render && !everRender && (everRender = true)
let selected = tabs[0]
let tabHeight: number = 0
let footerHeight: number = 0
@@ -155,97 +158,109 @@
bind:result
errorHandledByComponent={true}
>
<div class="w-full overflow-auto">
<div bind:clientHeight={tabHeight}>
<Stepper
on:click={(e) => {
const index = e.detail.index
if (index <= maxReachedIndex || $mode === 'dnd') {
runStep(index)
}
}}
{tabs}
{selectedIndex}
{maxReachedIndex}
{statusByStep}
hasValidations={Boolean(runnableComponent)}
/>
</div>
<div class="w-full">
{#if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={twMerge(css?.container?.class, 'wm-stepper')}
style={css?.container?.style}
containerHeight={componentContainerHeight - tabHeight - footerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
{#if everRender}
<div class="w-full overflow-auto">
{#if render}
<div bind:clientHeight={tabHeight}>
<Stepper
on:click={(e) => {
const index = e.detail.index
if (index <= maxReachedIndex || $mode === 'dnd') {
runStep(index)
}
}}
{tabs}
{selectedIndex}
{maxReachedIndex}
{statusByStep}
hasValidations={Boolean(runnableComponent)}
/>
{/each}
</div>
{/if}
<div class="w-full">
{#if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={twMerge(css?.container?.class, 'wm-stepper')}
style={css?.container?.style}
containerHeight={componentContainerHeight - tabHeight - footerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
}
}}
/>
{/each}
{/if}
</div>
{#if render}
<div bind:clientHeight={footerHeight}>
<div class="flex justify-between h-10 p-2">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-tertiary">
Step {selectedIndex + 1} of {tabs.length}
</span>
</div>
<div class="flex items-center gap-2">
<Button
size="xs"
color="light"
variant="contained"
disabled={selectedIndex === 0}
on:click={(e) => {
e.preventDefault()
directionClicked = 'left'
onPrevious?.forEach((id) =>
$runnableComponents?.[id]?.cb?.forEach((cb) => cb?.())
)
runStep(selectedIndex - 1)
}}
>
<div class="flex flex-row gap-2">
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'left'}
<Loader2 class="w-4 h-4 animate-spin" />
{:else}
<ArrowLeftIcon class="w-4 h-4" />
{/if}
Previous
</div>
</Button>
<Button
size="xs"
color="dark"
variant="contained"
disabled={lastStep}
on:click={(e) => {
e.preventDefault()
directionClicked = 'right'
onNext?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
runStep(selectedIndex + 1)
}}
>
<div class="flex flex-row gap-2">
Next
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'right'}
<Loader2 class="w-4 h-4 animate-spin" />
{:else}
<ArrowRightIcon class="w-4 h-4" />
{/if}
</div>
</Button>
</div>
</div>
</div>
{/if}
</div>
<div bind:clientHeight={footerHeight}>
<div class="flex justify-between h-10 p-2">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-tertiary">
Step {selectedIndex + 1} of {tabs.length}
</span>
</div>
<div class="flex items-center gap-2">
<Button
size="xs"
color="light"
variant="contained"
disabled={selectedIndex === 0}
on:click={(e) => {
e.preventDefault()
directionClicked = 'left'
onPrevious?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
runStep(selectedIndex - 1)
}}
>
<div class="flex flex-row gap-2">
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'left'}
<Loader2 class="w-4 h-4 animate-spin" />
{:else}
<ArrowLeftIcon class="w-4 h-4" />
{/if}
Previous
</div>
</Button>
<Button
size="xs"
color="dark"
variant="contained"
disabled={lastStep}
on:click={(e) => {
e.preventDefault()
directionClicked = 'right'
onNext?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
runStep(selectedIndex + 1)
}}
>
<div class="flex flex-row gap-2">
Next
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'right'}
<Loader2 class="w-4 h-4 animate-spin" />
{:else}
<ArrowRightIcon class="w-4 h-4" />
{/if}
</div>
</Button>
</div>
</div>
</div>
</div>
{:else if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor {id} visible={false} subGridId={`${id}-${i}`} />
{/each}
{/if}
</RunnableWrapper>
@@ -29,6 +29,8 @@
components['tabscomponent'].initialData.configuration,
configuration
)
let everRender = render
$: render && !everRender && (everRender = true)
const {
app,
@@ -117,128 +119,134 @@
/>
{/each}
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full h-full' : 'w-full'}>
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
<div bind:clientHeight={tabHeight}>
<Tabs
bind:selected
class={twMerge(css?.tabRow?.class, 'wm-tabs-tabRow')}
{#if everRender}
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full h-full' : 'w-full'}>
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
<div bind:clientHeight={tabHeight}>
<Tabs
bind:selected
class={twMerge(css?.tabRow?.class, 'wm-tabs-tabRow')}
style={css?.tabRow?.style}
>
{#each tabs ?? [] as res, index}
<Tab
value={res}
class={twMerge(css?.allTabs?.class, 'wm-tabs-alltabs')}
style={css?.allTabs?.style}
selectedClass={twMerge(css?.selectedTab?.class, 'wm-tabs-selectedTab')}
selectedStyle={css?.selectedTab?.style}
disabled={resolvedDisabledTabs[index]}
>
<span class="font-semibold">{res}</span>
</Tab>
{/each}
</Tabs>
</div>
{:else if resolvedConfig.tabsKind == 'sidebar'}
<div
class={twMerge(
'flex gap-y-2 flex-col w-1/6 max-w-[160px] bg-surface text-secondary opacity-80 px-4 pt-4 border-r ',
css?.tabRow?.class,
'wm-tabs-tabRow'
)}
style={css?.tabRow?.style}
>
{#each tabs ?? [] as res, index}
<Tab
value={res}
class={twMerge(css?.allTabs?.class, 'wm-tabs-alltabs')}
style={css?.allTabs?.style}
selectedClass={twMerge(css?.selectedTab?.class, 'wm-tabs-selectedTab')}
selectedStyle={css?.selectedTab?.style}
disabled={resolvedDisabledTabs[index]}
>
<span class="font-semibold">{res}</span>
</Tab>
{/each}
</Tabs>
</div>
{:else if resolvedConfig.tabsKind == 'sidebar'}
<div
class={twMerge(
'flex gap-y-2 flex-col w-1/6 max-w-[160px] bg-surface text-secondary opacity-80 px-4 pt-4 border-r ',
css?.tabRow?.class,
'wm-tabs-tabRow'
)}
style={css?.tabRow?.style}
>
{#each tabs ?? [] as res}
<button
on:pointerdown|stopPropagation
on:click={() => (selected = res)}
class={twMerge(
'rounded-sm !truncate text-sm hover:text-primary px-1 py-2',
css?.allTabs?.class,
'wm-tabs-alltabs',
selected == res
? twMerge(
'border-r border-primary border-l bg-surface text-primary',
css?.selectedTab?.class,
'wm-tabs-selectedTab'
)
: ''
)}
style={selected == res
? [css?.allTabs?.style, css?.selectedTab?.style].filter(Boolean).join(';')
: css?.allTabs?.style}
>
{res}
</button>
{/each}
</div>
{/if}
{#if resolvedConfig.tabsKind == 'accordion'}
<div class="flex flex-col w-full">
{#each tabs ?? [] as res, index}
<div class="border-b">
{#each tabs ?? [] as res}
<button
on:pointerdown|stopPropagation
on:click={() => (selected = res)}
class={twMerge(
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
'rounded-sm !truncate text-sm hover:text-primary px-1 py-2',
css?.allTabs?.class,
'wm-tabs-alltabs',
selected == res
? twMerge(
'bg-surface text-primary ',
'border-r border-primary border-l bg-surface text-primary',
css?.selectedTab?.class,
'wm-tabs-selectedTab'
)
: 'text-secondary'
: ''
)}
style={selected == res
? [css?.allTabs?.style, css?.selectedTab?.style].filter(Boolean).join(';')
: css?.allTabs?.style}
>
<span class="mr-2 w-8 font-mono">{selected == res ? '-' : '+'}</span>
{res}
</button>
{#if selected == res}
<div class="p-2 border-t">
<SubGridEditor
{id}
visible={render && index === selectedIndex}
subGridId={`${id}-${index}`}
class={twMerge(css?.container?.class, 'wm-tabs-container')}
style={css?.container?.style}
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
}
}}
/>
</div>
{/if}
</div>
{/each}
</div>
{:else}
<div class="w-full">
{#if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={twMerge(css?.container?.class, 'wm-tabs-container')}
style={css?.container?.style}
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
? componentContainerHeight - tabHeight
: componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
}
}}
/>
{/each}
{/if}
</div>
{/if}
</div>
</div>
{/if}
{#if resolvedConfig.tabsKind == 'accordion'}
<div class="flex flex-col w-full">
{#each tabs ?? [] as res, index}
<div class="border-b">
<button
on:pointerdown|stopPropagation
on:click={() => (selected = res)}
class={twMerge(
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
css?.allTabs?.class,
'wm-tabs-alltabs',
selected == res
? twMerge(
'bg-surface text-primary ',
css?.selectedTab?.class,
'wm-tabs-selectedTab'
)
: 'text-secondary'
)}
>
<span class="mr-2 w-8 font-mono">{selected == res ? '-' : '+'}</span>
{res}
</button>
{#if selected == res}
<div class="p-2 border-t">
<SubGridEditor
{id}
visible={render && index === selectedIndex}
subGridId={`${id}-${index}`}
class={twMerge(css?.container?.class, 'wm-tabs-container')}
style={css?.container?.style}
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
}
}}
/>
</div>
{/if}
</div>
{/each}
</div>
{:else}
<div class="w-full">
{#if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={twMerge(css?.container?.class, 'wm-tabs-container')}
style={css?.container?.style}
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
? componentContainerHeight - tabHeight
: componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
handleTabSelection()
}
}}
/>
{/each}
{/if}
</div>
{/if}
</div>
{:else if $app.subgrids}
{#each tabs ?? [] as _res, i}
<SubGridEditor {id} visible={false} subGridId={`${id}-${i}`} />
{/each}
{/if}
@@ -15,7 +15,6 @@
import GridViewer from './GridViewer.svelte'
import Component from './component/Component.svelte'
import { twMerge } from 'tailwind-merge'
import { columnConfiguration } from '../gridUtils'
import { deepEqual } from 'fast-equals'
import { dfs, maxHeight } from './appUtils'
import { BG_PREFIX, migrateApp } from '../utils'
@@ -292,8 +291,6 @@
allIdsInPath={$allIdsInPath}
items={app.grid}
let:dataItem
let:hidden
cols={columnConfiguration}
{maxRow}
breakpoint={$breakpoint}
>
@@ -308,7 +305,6 @@
selected={false}
locked={true}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
{hidden}
/>
</div>
</GridViewer>
@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext, AppViewerContext } from '../types'
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
import { twMerge } from 'tailwind-merge'
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
@@ -195,11 +195,9 @@
}}
root
let:dataItem
let:hidden
let:overlapped
let:moveMode
let:componentDraggedId
cols={columnConfiguration}
on:dropped={(e) => {
const { id, overlapped, x, y } = e.detail
@@ -252,7 +250,6 @@
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
>
<Component
{hidden}
render={true}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
@@ -1,9 +1,11 @@
<script lang="ts">
import { columnConfiguration, WIDE_GRID_COLUMNS } from '../gridUtils'
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT } from './appUtils'
import type { EditorBreakpoint } from '../types'
import type { AppViewerContext, EditorBreakpoint } from '../types'
import { onMount, createEventDispatcher } from 'svelte'
import { onMount, createEventDispatcher, getContext } from 'svelte'
import type { FilledItem } from '../svelte-grid/types'
import { getColumn, throttle } from '../svelte-grid/utils/other'
@@ -14,9 +16,10 @@
type T = $$Generic
const { app } = getContext<AppViewerContext>('AppViewerContext')
export let items: FilledItem<T>[]
export let rowHeight: number = ROW_HEIGHT
export let cols: [number, number][]
export let gap = [ROW_GAP_X, ROW_GAP_Y]
export let throttleUpdate = 100
export let maxRow: number
@@ -27,12 +30,19 @@
export let parentWidth: number | undefined = undefined
let getComputedCols
const cols = columnConfiguration
let showSkeleton = false
let getComputedCols: 3 | 12 | undefined =
$app.mobileViewOnSmallerScreens == false ? WIDE_GRID_COLUMNS : undefined
let container
$: [gapX, gapY] = gap
let xPerPx = 0
let yPerPx = rowHeight
$: containerHeight = getContainerHeight(items, yPerPx, getComputedCols)
@@ -48,15 +58,19 @@
}, throttleUpdate)
onMount(() => {
setTimeout(() => {
showSkeleton = true
}, 100)
const sizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
let width = entries[0].contentRect.width
if (width === containerWidth) return
if ($app.mobileViewOnSmallerScreens != false || !getComputedCols) {
getComputedCols = getColumn(parentWidth ?? width, cols)
}
getComputedCols = getColumn(parentWidth ?? width, cols)
xPerPx = width / getComputedCols
xPerPx = width / getComputedCols!
if (!containerWidth) {
items = specifyUndefinedColumns(items, getComputedCols, cols)
@@ -81,7 +95,7 @@
</script>
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
{#if xPerPx}
{#if xPerPx && getComputedCols}
{#each items as item (item.id)}
{@const onTop = allIdsInPath?.includes(item.id)}
{@const width =
@@ -103,10 +117,14 @@
: ''} top: {top}px; left: {left}px;"
>
{#if item[getComputedCols]}
<slot dataItem={item} item={item[getComputedCols]} hidden={false} />
<slot dataItem={item} item={item[getComputedCols]} />
{/if}
</div>
{/each}
{:else if showSkeleton}
<div
class="h-full w-full flex-col animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
/>
{/if}
</div>
@@ -3,7 +3,7 @@
import { classNames } from '$lib/utils'
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
import Grid from '../svelte-grid/Grid.svelte'
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
import {
@@ -53,6 +53,10 @@
let isActive = false
let sber = editorContext?.componentActive?.subscribe((x) => (isActive = x))
let everVisible = visible
$: visible && !everVisible && (everVisible = true)
onDestroy(() => {
sber?.()
})
@@ -173,160 +177,156 @@
}
</script>
<div
class="translate-x-0 translate-y-0 w-full subgrid {visible
? 'visible'
: 'invisible h-0 overflow-hidden'}"
bind:this={container}
on:pointerdown={onpointerdown}
>
{#if everVisible}
<div
class={twMerge(
$allIdsInPath.includes(id) && $mode == 'dnd' ? 'overflow-visible' : 'overflow-auto',
noYPadding ? '' : 'py-2',
classes ?? '',
noPadding ? 'px-0' : 'px-2'
)}
style="{containerHeight ? `height: ${containerHeight - 2}px;` : ''} {style ?? ''}"
class="translate-x-0 translate-y-0 w-full subgrid {visible
? 'visible'
: 'invisible h-0 overflow-hidden'}"
bind:this={container}
on:pointerdown={onpointerdown}
>
{#if $mode !== 'preview'}
<div
class={highlight
? `animate-border border-dashed border-2 min-h-full ${
isActive && !$selectedComponent?.includes(id)
? 'border-orange-600'
: 'dark:border-gray-600 border-gray-400'
}`
: ''}
>
<Grid
<div
class={twMerge(
$allIdsInPath.includes(id) && $mode == 'dnd' ? 'overflow-visible' : 'overflow-auto',
noYPadding ? '' : 'py-2',
classes ?? '',
noPadding ? 'px-0' : 'px-2'
)}
style="{containerHeight ? `height: ${containerHeight - 2}px;` : ''} {style ?? ''}"
>
{#if $mode !== 'preview'}
<div
class={highlight
? `outline !outline-dashed outline-2 min-h-full ${
isActive && !$selectedComponent?.includes(id)
? 'outline-orange-600'
: 'outline-gray-400 dark:outline-gray-600'
}`
: ''}
>
<Grid
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
on:redraw={(e) => {
push(editorContext?.history, $app)
if ($app.subgrids) {
$app.subgrids[subGridId] = e.detail
}
}}
selectedIds={$selectedComponent}
let:dataItem
let:overlapped
let:moveMode
let:componentDraggedId
scroller={container}
parentWidth={$parentWidth - 17}
{containerWidth}
on:dropped={(e) => {
const { id, overlapped, x, y } = e.detail
if (!overlapped) {
moveToRoot(id, { x, y })
} else {
const overlappedComponent = findGridItem($app, overlapped)
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
return
}
if (!overlapped) {
return
}
if (id === overlapped) {
return
}
moveComponentBetweenSubgrids(
id,
overlapped,
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
{ x, y }
)
}
}}
disableMove={!!$connectingInput.opened}
>
<ComponentWrapper
id={dataItem.id}
type={dataItem.data.type}
class={classNames(
'h-full w-full center-center',
$selectedComponent?.includes(dataItem.id) ? 'active-grid-item' : '',
'top-0'
)}
>
<GridEditorMenu id={dataItem.id}>
<Component
{overlapped}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
$selectedComponent = [dataItem.id]
push(editorContext?.history, $app)
expandGriditem(
$app.subgrids?.[subGridId] ?? [],
dataItem.id,
$breakpoint,
parentGridItem
)
$app = $app
}}
on:fillHeight={() => {
const gridItem = findGridItem($app, dataItem.id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}}
{moveMode}
{componentDraggedId}
/>
</GridEditorMenu>
</ComponentWrapper>
</Grid>
</div>
{:else}
<GridViewer
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
on:redraw={(e) => {
push(editorContext?.history, $app)
if ($app.subgrids) {
$app.subgrids[subGridId] = e.detail
}
}}
selectedIds={$selectedComponent}
let:dataItem
let:hidden
let:overlapped
let:moveMode
let:componentDraggedId
cols={columnConfiguration}
scroller={container}
breakpoint={$breakpoint}
parentWidth={$parentWidth - 17}
{containerWidth}
on:dropped={(e) => {
const { id, overlapped, x, y } = e.detail
if (!overlapped) {
moveToRoot(id, { x, y })
} else {
const overlappedComponent = findGridItem($app, overlapped)
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
return
}
if (!overlapped) {
return
}
if (id === overlapped) {
return
}
moveComponentBetweenSubgrids(
id,
overlapped,
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
{ x, y }
)
}
}}
disableMove={!!$connectingInput.opened}
{maxRow}
>
<ComponentWrapper
id={dataItem.id}
type={dataItem.data.type}
class={classNames(
'h-full w-full center-center',
$selectedComponent?.includes(dataItem.id) ? 'active-grid-item' : '',
'top-0'
)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown|stopPropagation={(e) => selectComponent(e, dataItem.id)}
class={classNames('h-full w-full center-center', 'top-0')}
>
<GridEditorMenu id={dataItem.id}>
<Component
{hidden}
{overlapped}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
$selectedComponent = [dataItem.id]
push(editorContext?.history, $app)
expandGriditem(
$app.subgrids?.[subGridId] ?? [],
dataItem.id,
$breakpoint,
parentGridItem
)
$app = $app
}}
on:fillHeight={() => {
const gridItem = findGridItem($app, dataItem.id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}}
{moveMode}
{componentDraggedId}
/>
</GridEditorMenu>
</ComponentWrapper>
</Grid>
</div>
{:else}
<GridViewer
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
let:dataItem
let:hidden
cols={columnConfiguration}
breakpoint={$breakpoint}
parentWidth={$parentWidth - 17}
{containerWidth}
{maxRow}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown|stopPropagation={(e) => selectComponent(e, dataItem.id)}
class={classNames('h-full w-full center-center', 'top-0')}
>
<Component
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
{hidden}
/>
</div>
</GridViewer>
{/if}
<Component
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={true}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
/>
</div>
</GridViewer>
{/if}
</div>
</div>
</div>
{/if}
@@ -1,969 +1,39 @@
<script lang="ts" context="module">
let outTimeout: NodeJS.Timeout | undefined = undefined
</script>
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppEditorContext, AppViewerContext } from '../../types'
import ComponentHeader from '../ComponentHeader.svelte'
import ComponentInner from './ComponentInner.svelte'
import ComponentRendered from './ComponentRendered.svelte'
import type { AppComponent } from './components'
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
import AppMultiSelectV2 from '../../components/inputs/AppMultiSelectV2.svelte'
import AppModal from '../../components/layout/AppModal.svelte'
import AppSchemaForm from '../../components/buttons/AppSchemaForm.svelte'
import AppStepper from '../../components/layout/AppStepper.svelte'
import AppSelectTab from '../../components/inputs/AppSelectTab.svelte'
import AppConditionalWrapper from '../../components/layout/AppConditionalWrapper.svelte'
import AppSelectStep from '../../components/inputs/AppSelectStep.svelte'
import AppDownload from '../../components/display/AppDownload.svelte'
import AppLogsComponent from '../../components/display/AppLogsComponent.svelte'
import AppFlowStatusComponent from '../../components/display/AppFlowStatusComponent.svelte'
import AppChartJs from '../../components/display/AppChartJs.svelte'
import AppChartJsV2 from '../../components/display/AppChartJsV2.svelte'
import AppQuillEditor from '../../components/inputs/AppQuillEditor.svelte'
import AppList from '../../components/layout/AppList.svelte'
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
import AppAccordionList from '../../components/display/AppAccordionList.svelte'
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
import AppStatCard from '../../components/display/AppStatCard.svelte'
import AppMenu from '../../components/display/AppMenu.svelte'
import AppDecisionTree from '../../components/layout/AppDecisionTree.svelte'
import AppAgCharts from '../../components/display/charts/AppAgCharts.svelte'
import AppDbExplorer from '../../components/display/dbtable/AppDbExplorer.svelte'
import AppS3FileInput from '../../components/inputs/AppS3FileInput.svelte'
import AppAlert from '../../components/display/AppAlert.svelte'
import AppDateSliderInput from '../../components/inputs/AppDateSliderInput.svelte'
import AppTimeInput from '../../components/inputs/AppTimeInput.svelte'
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
import AppDisplayComponent from '../../components/display/AppDisplayComponent.svelte'
import AppTimeseries from '../../components/display/AppTimeseries.svelte'
import AppHtml from '../../components/display/AppHtml.svelte'
import AppMarkdown from '../../components/display/AppMarkdown.svelte'
import VegaLiteHtml from '../../components/display/VegaLiteHtml.svelte'
import PlotlyHtml from '../../components/display/PlotlyHtml.svelte'
import PlotlyHtmlV2 from '../../components/display/PlotlyHtmlV2.svelte'
import AppScatterChart from '../../components/display/AppScatterChart.svelte'
import AppPieChart from '../../components/display/AppPieChart.svelte'
import AppTable from '../../components/display/table/AppTable.svelte'
import AppAggridTable from '../../components/display/table/AppAggridTable.svelte'
import AppText from '../../components/display/AppText.svelte'
import AppButton from '../../components/buttons/AppButton.svelte'
import AppForm from '../../components/buttons/AppForm.svelte'
import AppFormButton from '../../components/buttons/AppFormButton.svelte'
import AppCheckbox from '../../components/inputs/AppCheckbox.svelte'
import AppTextInput from '../../components/inputs/AppTextInput.svelte'
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
import AppSelect from '../../components/inputs/AppSelect.svelte'
import AppBarChart from '../../components/display/AppBarChart.svelte'
import AppDivider from '../../components/layout/AppDivider.svelte'
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
import AppTabs from '../../components/layout/AppTabs.svelte'
import AppContainer from '../../components/layout/AppContainer.svelte'
import AppSplitpanes from '../../components/layout/AppSplitpanes.svelte'
import AppIcon from '../../components/display/AppIcon.svelte'
import AppFileInput from '../../components/inputs/AppFileInput.svelte'
import AppImage from '../../components/display/AppImage.svelte'
import AppDrawer from '../../components/layout/AppDrawer.svelte'
import AppMap from '../../components/display/AppMap.svelte'
import AppPdf from '../../components/display/AppPdf.svelte'
import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte'
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
import AppNavbar from '../../components/display/AppNavbar.svelte'
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
import AppDisplayComponentByJobId from '../../components/display/AppRecomputeAll.svelte'
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
import AppUserResource from '../../components/inputs/AppUserResource.svelte'
import { Anchor } from 'lucide-svelte'
import { findGridItemParentGrid, isContainer } from '../appUtils'
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
export let render: boolean
export let hidden: boolean
export let fullHeight: boolean
export let overlapped: string | undefined = undefined
export let moveMode: string | undefined = undefined
export let componentDraggedId: string | undefined = undefined
const { mode, app, hoverStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
const componentActive = editorContext?.componentActive
export let moveMode: string | undefined = undefined
let everRender = render
const movingcomponents = editorContext?.movingcomponents
$: ismoving =
movingcomponents != undefined && $mode == 'dnd' && $movingcomponents?.includes(component.id)
let initializing: boolean | undefined = undefined
let errorHandledByComponent: boolean = false
let componentContainerHeight: number = 0
let componentContainerWidth: number = 0
let inlineEditorOpened: boolean = false
function mouseOut() {
outTimeout && clearTimeout(outTimeout)
outTimeout = setTimeout(() => {
if ($hoverStore !== undefined) {
// In order to avoid flickering when hovering over table actions,
// we leave the actions to manage the hover state
if ($hoverStore.startsWith(`${component.id}_`)) {
return
}
$hoverStore = undefined
}
}, 50)
}
function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) {
let parentGrid = findGridItemParentGrid($app, componentDraggedId)
return !parentGrid?.startsWith(`${componentId}-`)
}
function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) {
return (
findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId)
)
}
let cachedComponentDraggedIsNotChild: boolean | undefined
let cachedAreOnTheSameSubgrid: boolean | undefined
function updateCache(componentDraggedId: string | undefined) {
if (componentDraggedId) {
cachedComponentDraggedIsNotChild = componentDraggedIsNotChild(
componentDraggedId,
component.id
)
cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id)
} else {
cachedComponentDraggedIsNotChild = undefined
cachedAreOnTheSameSubgrid = undefined
}
}
$: updateCache(componentDraggedId)
$: render && !everRender && (everRender = true)
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseover|stopPropagation={() => {
outTimeout && clearTimeout(outTimeout)
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
on:mouseout|stopPropagation={mouseOut}
class={twMerge(
'h-full flex flex-col w-full component relative',
initializing ? 'overflow-hidden h-0' : '',
hidden && $mode === 'preview' ? 'hidden' : ''
)}
data-connection-button
>
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
<div
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
>
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
<Anchor size={24} class="text-primary " />
<div class="text-xs"> Anchored: The component cannot be moved. </div>
</div>
</div>
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
<div
class={twMerge(
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
)}
/>
{/if}
{#if $mode !== 'preview'}
<ComponentHeader
on:mouseover={() => {
outTimeout && clearTimeout(outTimeout)
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
hover={$hoverStore === component.id}
{component}
{selected}
{fullHeight}
connecting={$connectingInput.opened}
on:lock
on:expand
on:fillHeight
{locked}
{inlineEditorOpened}
hasInlineEditor={component.type === 'textcomponent' &&
component.componentInput &&
component.componentInput.type !== 'connected'}
on:triggerInlineEditor={() => {
inlineEditorOpened = !inlineEditorOpened
}}
{errorHandledByComponent}
{componentContainerWidth}
/>
{/if}
{#if ismoving}
<div class="absolute -top-8 w-40">
<button
class="border p-0.5 text-xs"
on:click={() => {
$movingcomponents = undefined
}}
>
Cancel move
</button>
</div>
{/if}
<div
class={twMerge(
'h-full outline-1',
$mode === 'dnd' ? 'bg-surface/40' : '',
$hoverStore === component.id && $mode !== 'preview'
? $connectingInput.opened
? 'outline outline-[#f8aa4b]'
: 'outline outline-blue-400'
: '',
selected && $mode !== 'preview' ? 'outline outline-blue-600' : '',
$mode != 'preview' ? 'cursor-pointer' : '',
'relative z-auto',
$app.css?.['app']?.['component']?.class,
'wm-app-component',
ismoving ? 'animate-pulse' : ''
)}
style={$app.css?.['app']?.['component']?.style}
bind:clientHeight={componentContainerHeight}
bind:clientWidth={componentContainerWidth}
>
{#if component.type === 'displaycomponent'}
<AppDisplayComponent
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
configuration={component.configuration}
{render}
/>
{:else if component.type === 'logcomponent'}
<AppLogsComponent />
{:else if component.type === 'jobidlogcomponent'}
<AppJobIdLogComponent
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'flowstatuscomponent'}
<AppFlowStatusComponent />
{:else if component.type === 'jobidflowstatuscomponent'}
<AppJobIdFlowStatus
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'barchartcomponent'}
<AppBarChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'timeseriescomponent'}
<AppTimeseries
id={component.id}
customCss={component.customCss}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'htmlcomponent'}
<AppHtml
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'customcomponent'}
<AppCustomComponent
customComponent={component.customComponent}
id={component.id}
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'mardowncomponent'}
<AppMarkdown
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
configuration={component.configuration}
{render}
/>
{:else if component.type === 'vegalitecomponent'}
<VegaLiteHtml
configuration={component.configuration}
id={component.id}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'plotlycomponent'}
<PlotlyHtml
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'plotlycomponentv2'}
<PlotlyHtmlV2
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'scatterchartcomponent'}
<AppScatterChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'piechartcomponent'}
<AppPieChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'agchartscomponent'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'agchartscomponentee'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
license={component.license}
ee={true}
{render}
/>
{:else if component.type === 'tablecomponent'}
<AppTable
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
actionButtons={component.actionButtons}
{render}
/>
{:else if component.type === 'dbexplorercomponent'}
<AppDbExplorer
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
actions={component.actions ?? []}
bind:initializing
{render}
/>
{:else if component.type === 'aggridcomponent'}
<AppAggridTable
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
actionsOrder={component.actionsOrder ?? undefined}
{render}
/>
{:else if component.type === 'aggridcomponentee'}
<AppAggridTableEe
license={component.license}
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
actionsOrder={component.actionsOrder ?? undefined}
{render}
/>
{:else if component.type === 'aggridinfinitecomponent'}
<AppAggridInfiniteTable
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'aggridinfinitecomponentee'}
<AppAggridInfiniteTableEe
license={component.license}
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'textcomponent'}
<AppText
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:initializing
bind:editorMode={inlineEditorOpened}
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'buttoncomponent'}
<AppButton
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'downloadcomponent'}
<AppDownload
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
<AppSelect
recomputeIds={component.recomputeIds}
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
onSelect={component.onSelect}
{render}
/>
{:else if component.type === 'userresourcecomponent'}
<AppUserResource
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'multiselectcomponent'}
<AppMultiSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'multiselectcomponentv2'}
<AppMultiSelectV2
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'formcomponent'}
<AppForm
id={component.id}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'formbuttoncomponent'}
<AppFormButton
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'checkboxcomponent'}
<AppCheckbox
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
recomputeIds={component.recomputeIds}
onToggle={component.onToggle}
{render}
/>
{:else if component.type === 'textinputcomponent'}
<AppTextInput
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'quillcomponent'}
<AppQuillEditor id={component.id} configuration={component.configuration} {render} />
{:else if component.type === 'textareainputcomponent'}
<AppTextInput
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
inputType="textarea"
appCssKey="textareainputcomponent"
{render}
/>
{:else if component.type === 'emailinputcomponent'}
<AppTextInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="email"
appCssKey="emailinputcomponent"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'passwordinputcomponent'}
<AppTextInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="password"
appCssKey="passwordinputcomponent"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'dateinputcomponent'}
<AppDateInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="date"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'timeinputcomponent'}
<AppTimeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'datetimeinputcomponent'}
<AppDateTimeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="date"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'numberinputcomponent'}
<AppNumberInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'currencycomponent'}
<AppCurrencyInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'slidercomponent'}
<AppSliderInputs
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'dateslidercomponent'}
<AppDateSliderInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'horizontaldividercomponent'}
<AppDivider
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
position="horizontal"
{render}
/>
{:else if component.type === 'verticaldividercomponent'}
<AppDivider
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
position="vertical"
{render}
/>
{:else if component.type === 'rangecomponent'}
<AppRangeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'tabscomponent' && component.tabs}
<AppTabs
configuration={component.configuration}
id={component.id}
tabs={component.tabs}
disabledTabs={component.disabledTabs}
onTabChange={component.onTabChange}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'steppercomponent' && component.tabs}
<AppStepper
id={component.id}
tabs={component.tabs}
customCss={component.customCss}
{componentContainerHeight}
componentInput={component.componentInput}
onNext={component.onNext}
onPrevious={component.onPrevious}
{render}
/>
{:else if component.type === 'conditionalwrapper' && component.conditions}
<AppConditionalWrapper
id={component.id}
conditions={component.conditions}
customCss={component.customCss}
onTabChange={component.onTabChange}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'containercomponent'}
<AppContainer
groupFields={component.groupFields}
id={component.id}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'listcomponent'}
<AppList
id={component.id}
customCss={component.customCss}
configuration={component.configuration}
componentInput={component.componentInput}
{render}
bind:initializing
/>
{:else if component.type === 'verticalsplitpanescomponent'}
<AppSplitpanes
id={component.id}
customCss={component.customCss}
panes={component.panes}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'horizontalsplitpanescomponent'}
<AppSplitpanes
id={component.id}
customCss={component.customCss}
panes={component.panes}
{componentContainerHeight}
horizontal={true}
{render}
/>
{:else if component.type === 'iconcomponent'}
<AppIcon
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'fileinputcomponent'}
<AppFileInput
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onFileChange={component.onFileChange}
{render}
/>
{:else if component.type === 's3fileinputcomponent'}
<AppS3FileInput
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onFileChange={component.onFileChange}
{render}
/>
{:else if component.type === 'imagecomponent'}
<AppImage
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'drawercomponent'}
<AppDrawer
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onOpenRecomputeIds={component.onOpenRecomputeIds}
onCloseRecomputeIds={component.onCloseRecomputeIds}
{render}
/>
{:else if component.type === 'mapcomponent'}
<AppMap
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'pdfcomponent'}
<AppPdf
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'modalcomponent'}
<AppModal
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onOpenRecomputeIds={component.onOpenRecomputeIds}
onCloseRecomputeIds={component.onCloseRecomputeIds}
{render}
/>
{:else if component.type === 'schemaformcomponent'}
<AppSchemaForm
id={component.id}
componentInput={component.componentInput}
configuration={component.configuration}
customCss={component.customCss}
{initializing}
{render}
/>
{:else if component.type === 'selecttabcomponent'}
<AppSelectTab
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'selectstepcomponent'}
<AppSelectStep
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'chartjscomponent'}
<AppChartJs
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'chartjscomponentv2'}
<AppChartJsV2
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'carousellistcomponent'}
<AppCarouselList
id={component.id}
configuration={component.configuration}
componentInput={component.componentInput}
customCss={component.customCss}
{componentContainerHeight}
{render}
bind:initializing
/>
{:else if component.type === 'accordionlistcomponent'}
<AppAccordionList
id={component.id}
componentInput={component.componentInput}
customCss={component.customCss}
{componentContainerHeight}
{render}
bind:initializing
/>
{:else if component.type === 'statcomponent'}
<AppStatCard
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'menucomponent'}
<AppMenu
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
menuItems={component.menuItems}
{render}
/>
{:else if component.type === 'decisiontreecomponent' && component.nodes}
<AppDecisionTree
id={component.id}
nodes={component.nodes}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'alertcomponent'}
<AppAlert
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'navbarcomponent'}
<AppNavbar
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
navbarItems={component.navbarItems}
{render}
/>
{:else if component.type === 'dateselectcomponent'}
<AppDateSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'jobiddisplaycomponent'}
<AppDisplayComponentByJobId
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'recomputeallcomponent'}
<AppRecomputeAll
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
horizontalAlignment={component.horizontalAlignment}
{render}
/>
{/if}
</div>
</div>
{#if initializing}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseover|stopPropagation={() => {
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
on:mouseout|stopPropagation={() => {
if ($hoverStore !== undefined) {
$hoverStore = undefined
}
}}
class="absolute inset-0 center-center flex-col bg- border animate-skeleton"
{#if everRender}
<ComponentRendered
{moveMode}
{componentDraggedId}
{overlapped}
{render}
{component}
{selected}
{locked}
{fullHeight}
/>
{:else}
<ComponentInner
{component}
render={false}
componentContainerHeight={0}
errorHandledByComponent={false}
inlineEditorOpened={false}
/>
{/if}
@@ -0,0 +1,778 @@
<script lang="ts">
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
import AppMultiSelectV2 from '../../components/inputs/AppMultiSelectV2.svelte'
import AppModal from '../../components/layout/AppModal.svelte'
import AppSchemaForm from '../../components/buttons/AppSchemaForm.svelte'
import AppStepper from '../../components/layout/AppStepper.svelte'
import AppSelectTab from '../../components/inputs/AppSelectTab.svelte'
import AppConditionalWrapper from '../../components/layout/AppConditionalWrapper.svelte'
import AppSelectStep from '../../components/inputs/AppSelectStep.svelte'
import AppDownload from '../../components/display/AppDownload.svelte'
import AppLogsComponent from '../../components/display/AppLogsComponent.svelte'
import AppFlowStatusComponent from '../../components/display/AppFlowStatusComponent.svelte'
import AppChartJs from '../../components/display/AppChartJs.svelte'
import AppChartJsV2 from '../../components/display/AppChartJsV2.svelte'
import AppQuillEditor from '../../components/inputs/AppQuillEditor.svelte'
import AppList from '../../components/layout/AppList.svelte'
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
import AppAccordionList from '../../components/display/AppAccordionList.svelte'
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
import AppStatCard from '../../components/display/AppStatCard.svelte'
import AppMenu from '../../components/display/AppMenu.svelte'
import AppDecisionTree from '../../components/layout/AppDecisionTree.svelte'
import AppAgCharts from '../../components/display/charts/AppAgCharts.svelte'
import AppDbExplorer from '../../components/display/dbtable/AppDbExplorer.svelte'
import AppS3FileInput from '../../components/inputs/AppS3FileInput.svelte'
import AppAlert from '../../components/display/AppAlert.svelte'
import AppDateSliderInput from '../../components/inputs/AppDateSliderInput.svelte'
import AppTimeInput from '../../components/inputs/AppTimeInput.svelte'
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
import AppDisplayComponent from '../../components/display/AppDisplayComponent.svelte'
import AppTimeseries from '../../components/display/AppTimeseries.svelte'
import AppHtml from '../../components/display/AppHtml.svelte'
import AppMarkdown from '../../components/display/AppMarkdown.svelte'
import VegaLiteHtml from '../../components/display/VegaLiteHtml.svelte'
import PlotlyHtml from '../../components/display/PlotlyHtml.svelte'
import PlotlyHtmlV2 from '../../components/display/PlotlyHtmlV2.svelte'
import AppScatterChart from '../../components/display/AppScatterChart.svelte'
import AppPieChart from '../../components/display/AppPieChart.svelte'
import AppTable from '../../components/display/table/AppTable.svelte'
import AppAggridTable from '../../components/display/table/AppAggridTable.svelte'
import AppText from '../../components/display/AppText.svelte'
import AppButton from '../../components/buttons/AppButton.svelte'
import AppForm from '../../components/buttons/AppForm.svelte'
import AppFormButton from '../../components/buttons/AppFormButton.svelte'
import AppCheckbox from '../../components/inputs/AppCheckbox.svelte'
import AppTextInput from '../../components/inputs/AppTextInput.svelte'
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
import AppSelect from '../../components/inputs/AppSelect.svelte'
import AppBarChart from '../../components/display/AppBarChart.svelte'
import AppDivider from '../../components/layout/AppDivider.svelte'
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
import AppTabs from '../../components/layout/AppTabs.svelte'
import AppContainer from '../../components/layout/AppContainer.svelte'
import AppSplitpanes from '../../components/layout/AppSplitpanes.svelte'
import AppIcon from '../../components/display/AppIcon.svelte'
import AppFileInput from '../../components/inputs/AppFileInput.svelte'
import AppImage from '../../components/display/AppImage.svelte'
import AppDrawer from '../../components/layout/AppDrawer.svelte'
import AppMap from '../../components/display/AppMap.svelte'
import AppPdf from '../../components/display/AppPdf.svelte'
import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte'
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
import AppNavbar from '../../components/display/AppNavbar.svelte'
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
import AppDisplayComponentByJobId from '../../components/display/AppRecomputeAll.svelte'
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
import AppUserResource from '../../components/inputs/AppUserResource.svelte'
import type { AppComponent } from './components'
export let component: AppComponent
export let render: boolean
export let componentContainerHeight: number
export let initializing: boolean | undefined = undefined
export let errorHandledByComponent: boolean
export let inlineEditorOpened: boolean
</script>
{#if component.type === 'displaycomponent'}
<AppDisplayComponent
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
configuration={component.configuration}
{render}
/>
{:else if component.type === 'logcomponent'}
<AppLogsComponent />
{:else if component.type === 'jobidlogcomponent'}
<AppJobIdLogComponent
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'flowstatuscomponent'}
<AppFlowStatusComponent />
{:else if component.type === 'jobidflowstatuscomponent'}
<AppJobIdFlowStatus
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'barchartcomponent'}
<AppBarChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'timeseriescomponent'}
<AppTimeseries
id={component.id}
customCss={component.customCss}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'htmlcomponent'}
<AppHtml
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'customcomponent'}
<AppCustomComponent
customComponent={component.customComponent}
id={component.id}
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'mardowncomponent'}
<AppMarkdown
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
configuration={component.configuration}
{render}
/>
{:else if component.type === 'vegalitecomponent'}
<VegaLiteHtml
configuration={component.configuration}
id={component.id}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'plotlycomponent'}
<PlotlyHtml
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'plotlycomponentv2'}
<PlotlyHtmlV2
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'scatterchartcomponent'}
<AppScatterChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'piechartcomponent'}
<AppPieChart
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'agchartscomponent'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'agchartscomponentee'}
<AppAgCharts
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
license={component.license}
ee={true}
{render}
/>
{:else if component.type === 'tablecomponent'}
<AppTable
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
actionButtons={component.actionButtons}
{render}
/>
{:else if component.type === 'dbexplorercomponent'}
<AppDbExplorer
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
actions={component.actions ?? []}
bind:initializing
{render}
/>
{:else if component.type === 'aggridcomponent'}
<AppAggridTable
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
actionsOrder={component.actionsOrder ?? undefined}
{render}
/>
{:else if component.type === 'aggridcomponentee'}
<AppAggridTableEe
license={component.license}
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
actionsOrder={component.actionsOrder ?? undefined}
{render}
/>
{:else if component.type === 'aggridinfinitecomponent'}
<AppAggridInfiniteTable
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'aggridinfinitecomponentee'}
<AppAggridInfiniteTableEe
license={component.license}
id={component.id}
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
customCss={component.customCss}
actions={component.actions ?? []}
{render}
/>
{:else if component.type === 'textcomponent'}
<AppText
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:initializing
bind:editorMode={inlineEditorOpened}
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'buttoncomponent'}
<AppButton
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'downloadcomponent'}
<AppDownload
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
<AppSelect
recomputeIds={component.recomputeIds}
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
onSelect={component.onSelect}
{render}
/>
{:else if component.type === 'userresourcecomponent'}
<AppUserResource
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'multiselectcomponent'}
<AppMultiSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'multiselectcomponentv2'}
<AppMultiSelectV2
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'formcomponent'}
<AppForm
id={component.id}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'formbuttoncomponent'}
<AppFormButton
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
recomputeIds={component.recomputeIds}
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'checkboxcomponent'}
<AppCheckbox
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
recomputeIds={component.recomputeIds}
onToggle={component.onToggle}
{render}
/>
{:else if component.type === 'textinputcomponent'}
<AppTextInput
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'quillcomponent'}
<AppQuillEditor id={component.id} configuration={component.configuration} {render} />
{:else if component.type === 'textareainputcomponent'}
<AppTextInput
id={component.id}
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
inputType="textarea"
appCssKey="textareainputcomponent"
{render}
/>
{:else if component.type === 'emailinputcomponent'}
<AppTextInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="email"
appCssKey="emailinputcomponent"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'passwordinputcomponent'}
<AppTextInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="password"
appCssKey="passwordinputcomponent"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'dateinputcomponent'}
<AppDateInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="date"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'timeinputcomponent'}
<AppTimeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'datetimeinputcomponent'}
<AppDateTimeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
inputType="date"
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'numberinputcomponent'}
<AppNumberInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'currencycomponent'}
<AppCurrencyInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'slidercomponent'}
<AppSliderInputs
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'dateslidercomponent'}
<AppDateSliderInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'horizontaldividercomponent'}
<AppDivider
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
position="horizontal"
{render}
/>
{:else if component.type === 'verticaldividercomponent'}
<AppDivider
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
position="vertical"
{render}
/>
{:else if component.type === 'rangecomponent'}
<AppRangeInput
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'tabscomponent' && component.tabs}
<AppTabs
configuration={component.configuration}
id={component.id}
tabs={component.tabs}
disabledTabs={component.disabledTabs}
onTabChange={component.onTabChange}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'steppercomponent' && component.tabs}
<AppStepper
id={component.id}
tabs={component.tabs}
customCss={component.customCss}
{componentContainerHeight}
componentInput={component.componentInput}
onNext={component.onNext}
onPrevious={component.onPrevious}
{render}
/>
{:else if component.type === 'conditionalwrapper' && component.conditions}
<AppConditionalWrapper
id={component.id}
conditions={component.conditions}
customCss={component.customCss}
onTabChange={component.onTabChange}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'containercomponent'}
<AppContainer
groupFields={component.groupFields}
id={component.id}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'listcomponent'}
<AppList
id={component.id}
customCss={component.customCss}
configuration={component.configuration}
componentInput={component.componentInput}
{render}
bind:initializing
/>
{:else if component.type === 'verticalsplitpanescomponent'}
<AppSplitpanes
id={component.id}
customCss={component.customCss}
panes={component.panes}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'horizontalsplitpanescomponent'}
<AppSplitpanes
id={component.id}
customCss={component.customCss}
panes={component.panes}
{componentContainerHeight}
horizontal={true}
{render}
/>
{:else if component.type === 'iconcomponent'}
<AppIcon
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'fileinputcomponent'}
<AppFileInput
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onFileChange={component.onFileChange}
{render}
/>
{:else if component.type === 's3fileinputcomponent'}
<AppS3FileInput
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onFileChange={component.onFileChange}
{render}
/>
{:else if component.type === 'imagecomponent'}
<AppImage
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'drawercomponent'}
<AppDrawer
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onOpenRecomputeIds={component.onOpenRecomputeIds}
onCloseRecomputeIds={component.onCloseRecomputeIds}
{render}
/>
{:else if component.type === 'mapcomponent'}
<AppMap
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'pdfcomponent'}
<AppPdf
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'modalcomponent'}
<AppModal
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
onOpenRecomputeIds={component.onOpenRecomputeIds}
onCloseRecomputeIds={component.onCloseRecomputeIds}
{render}
/>
{:else if component.type === 'schemaformcomponent'}
<AppSchemaForm
id={component.id}
componentInput={component.componentInput}
configuration={component.configuration}
customCss={component.customCss}
{initializing}
{render}
/>
{:else if component.type === 'selecttabcomponent'}
<AppSelectTab
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'selectstepcomponent'}
<AppSelectStep
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'chartjscomponent'}
<AppChartJs
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'chartjscomponentv2'}
<AppChartJsV2
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'carousellistcomponent'}
<AppCarouselList
id={component.id}
configuration={component.configuration}
componentInput={component.componentInput}
customCss={component.customCss}
{componentContainerHeight}
{render}
bind:initializing
/>
{:else if component.type === 'accordionlistcomponent'}
<AppAccordionList
id={component.id}
componentInput={component.componentInput}
customCss={component.customCss}
{componentContainerHeight}
{render}
bind:initializing
/>
{:else if component.type === 'statcomponent'}
<AppStatCard
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'menucomponent'}
<AppMenu
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
menuItems={component.menuItems}
{render}
/>
{:else if component.type === 'decisiontreecomponent' && component.nodes}
<AppDecisionTree
id={component.id}
nodes={component.nodes}
customCss={component.customCss}
{componentContainerHeight}
{render}
/>
{:else if component.type === 'alertcomponent'}
<AppAlert
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'navbarcomponent'}
<AppNavbar
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
navbarItems={component.navbarItems}
{render}
/>
{:else if component.type === 'dateselectcomponent'}
<AppDateSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'jobiddisplaycomponent'}
<AppDisplayComponentByJobId
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{:else if component.type === 'recomputeallcomponent'}
<AppRecomputeAll
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
horizontalAlignment={component.horizontalAlignment}
{render}
/>
{/if}
@@ -0,0 +1,219 @@
<script lang="ts" context="module">
let outTimeout: NodeJS.Timeout | undefined = undefined
</script>
<script lang="ts">
import { getContext, onMount } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppEditorContext, AppViewerContext } from '../../types'
import ComponentHeader from '../ComponentHeader.svelte'
import type { AppComponent } from './components'
import { Anchor } from 'lucide-svelte'
import { findGridItemParentGrid, isContainer } from '../appUtils'
import ComponentInner from './ComponentInner.svelte'
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
export let fullHeight: boolean
export let overlapped: string | undefined = undefined
export let moveMode: string | undefined = undefined
export let componentDraggedId: string | undefined = undefined
export let render: boolean = false
const { mode, app, hoverStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
const componentActive = editorContext?.componentActive
const movingcomponents = editorContext?.movingcomponents
$: ismoving =
movingcomponents != undefined && $mode == 'dnd' && $movingcomponents?.includes(component.id)
let initializing: boolean | undefined = undefined
let errorHandledByComponent: boolean = false
let componentContainerHeight: number = 0
let componentContainerWidth: number = 0
let inlineEditorOpened: boolean = false
let showSkeleton = false
onMount(() => {
setTimeout(() => {
showSkeleton = true
}, 100)
})
function mouseOut() {
outTimeout && clearTimeout(outTimeout)
outTimeout = setTimeout(() => {
if ($hoverStore !== undefined) {
// In order to avoid flickering when hovering over table actions,
// we leave the actions to manage the hover state
if ($hoverStore.startsWith(`${component.id}_`)) {
return
}
$hoverStore = undefined
}
}, 50)
}
function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) {
let parentGrid = findGridItemParentGrid($app, componentDraggedId)
return !parentGrid?.startsWith(`${componentId}-`)
}
function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) {
return (
findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId)
)
}
let cachedComponentDraggedIsNotChild: boolean | undefined
let cachedAreOnTheSameSubgrid: boolean | undefined
function updateCache(componentDraggedId: string | undefined) {
if (componentDraggedId) {
cachedComponentDraggedIsNotChild = componentDraggedIsNotChild(
componentDraggedId,
component.id
)
cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id)
} else {
cachedComponentDraggedIsNotChild = undefined
cachedAreOnTheSameSubgrid = undefined
}
}
$: updateCache(componentDraggedId)
</script>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseover|stopPropagation={() => {
outTimeout && clearTimeout(outTimeout)
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
on:mouseout|stopPropagation={mouseOut}
class={twMerge(
'h-full flex flex-col w-full component relative',
initializing ? 'overflow-hidden h-0' : ''
)}
data-connection-button
>
{#if render}
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
<div
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
>
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
<Anchor size={24} class="text-primary " />
<div class="text-xs"> Anchored: The component cannot be moved. </div>
</div>
</div>
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
<div
class={twMerge(
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
)}
/>
{/if}
{#if $mode !== 'preview'}
<ComponentHeader
on:mouseover={() => {
outTimeout && clearTimeout(outTimeout)
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
hover={$hoverStore === component.id}
{component}
{selected}
{fullHeight}
connecting={$connectingInput.opened}
on:lock
on:expand
on:fillHeight
{locked}
{inlineEditorOpened}
hasInlineEditor={component.type === 'textcomponent' &&
component.componentInput &&
component.componentInput.type !== 'connected'}
on:triggerInlineEditor={() => {
inlineEditorOpened = !inlineEditorOpened
}}
{errorHandledByComponent}
{componentContainerWidth}
/>
{/if}
{#if ismoving}
<div class="absolute -top-8 w-40">
<button
class="border p-0.5 text-xs"
on:click={() => {
$movingcomponents = undefined
}}
>
Cancel move
</button>
</div>
{/if}
{/if}
<div
class={twMerge(
render ? 'h-full outline-1' : 'h-0 overflow-hidden',
$mode === 'dnd' ? 'bg-surface/40' : '',
$hoverStore === component.id && $mode !== 'preview'
? $connectingInput.opened
? 'outline outline-[#f8aa4b]'
: 'outline outline-blue-400'
: '',
selected && $mode !== 'preview' ? 'outline outline-blue-600' : '',
$mode != 'preview' ? 'cursor-pointer' : '',
'relative z-auto',
$app.css?.['app']?.['component']?.class,
'wm-app-component',
ismoving ? 'animate-pulse' : ''
)}
style={$app.css?.['app']?.['component']?.style}
bind:clientHeight={componentContainerHeight}
bind:clientWidth={componentContainerWidth}
>
<ComponentInner
{component}
{render}
{componentContainerHeight}
bind:initializing
bind:errorHandledByComponent
{inlineEditorOpened}
/>
</div>
</div>
{#if initializing && render && showSkeleton}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseover|stopPropagation={() => {
if (component.id !== $hoverStore) {
$hoverStore = component.id
}
}}
on:mouseout|stopPropagation={() => {
if ($hoverStore !== undefined) {
$hoverStore = undefined
}
}}
class="absolute inset-0 center-center flex-col border animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
/>
{/if}
@@ -9,6 +9,8 @@
</script>
<script lang="ts">
import { columnConfiguration, WIDE_GRID_COLUMNS } from '../gridUtils'
import gridHelp from './utils/helper'
import type { AppViewerContext, GridItem } from '../types'
import { twMerge } from 'tailwind-merge'
@@ -41,7 +43,7 @@
export let items: FilledItem<T>[]
export let rowHeight: number = ROW_HEIGHT
export let cols: [number, number][]
export let gap = [ROW_GAP_X, ROW_GAP_Y]
export let throttleUpdate = 100
export let throttleResize = 100
@@ -53,7 +55,10 @@
export let root: boolean = false
export let parentWidth: number | undefined = undefined
export let disableMove: boolean = false
let getComputedCols
const cols = columnConfiguration
let getComputedCols: 3 | 12 | undefined =
$app.mobileViewOnSmallerScreens == false ? WIDE_GRID_COLUMNS : undefined
let container
$: [gapX, gapY] = gap
@@ -73,16 +78,22 @@
})
}, throttleUpdate)
let mounted = false
onMount(() => {
const sizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
let width = entries[0].contentRect.width
if (width === containerWidth) return
if (width === containerWidth) {
mounted = true
return
}
getComputedCols = getColumn(parentWidth ?? width, cols)
xPerPx = width / getComputedCols
if ($app.mobileViewOnSmallerScreens != false || !getComputedCols) {
getComputedCols = getColumn(parentWidth ?? width, cols)
}
xPerPx = width / getComputedCols!
if (!containerWidth) {
items = specifyUndefinedColumns(items, getComputedCols, cols)
@@ -97,6 +108,7 @@
}
containerWidth = width
mounted = true
})
})
@@ -106,7 +118,7 @@
})
let sortedItems: FilledItem<T>[] = []
$: sortedItems = JSON.parse(JSON.stringify(items)).sort((a, b) => a.id.localeCompare(b.id))
$: sortedItems = smartCopy(items).sort((a, b) => a.id.localeCompare(b.id))
let resizing: boolean = false
@@ -123,21 +135,31 @@
let initItems: FilledItem<T>[] | undefined = undefined
function smartCopy(items: FilledItem<T>[]) {
return getComputedCols != undefined
? items.map((item) => {
return {
...item,
[getComputedCols as number]: structuredClone(item[getComputedCols as number])
}
})
: []
}
const updateMatrix = ({ detail }) => {
let isPointerUp = detail.isPointerUp
let citems: FilledItem<T>[]
if (isPointerUp) {
try {
citems = JSON.parse(JSON.stringify(initItems))
} catch (e) {
citems = JSON.parse(JSON.stringify(sortedItems))
if (initItems == undefined) {
citems = smartCopy(sortedItems)
} else {
citems = smartCopy(initItems)
}
initItems = undefined
} else {
if (initItems == undefined) {
initItems = JSON.parse(JSON.stringify(sortedItems))
initItems = smartCopy(sortedItems)
}
citems = JSON.parse(JSON.stringify(initItems))
citems = smartCopy(initItems)
}
let nselectedIds = selectedIds ?? []
if (detail.id && !selectedIds?.includes(detail.id)) {
@@ -147,7 +169,7 @@
for (let id of nselectedIds) {
let activeItem = getItemById(id, sortedItems)
if (activeItem) {
if (activeItem && getComputedCols) {
activeItem = {
...activeItem,
[getComputedCols]: {
@@ -202,11 +224,8 @@
}
}
if (isPointerUp) {
dispatch(
'redraw',
sortGridItemsPosition(JSON.parse(JSON.stringify(sortedItems)), getComputedCols)
)
if (isPointerUp && getComputedCols) {
dispatch('redraw', sortGridItemsPosition(smartCopy(sortedItems), getComputedCols))
}
}
@@ -254,7 +273,7 @@
export function handleMove({ detail }) {
Object.entries(moveResizes).forEach(([id, moveResize]) => {
if (selectedIds?.includes(id)) {
moveResize?.updateMove(JSON.parse(JSON.stringify(detail.cordDiff)), detail.eventY)
moveResize?.updateMove(structuredClone(detail.cordDiff), detail.eventY)
}
})
@@ -275,7 +294,7 @@
) {
const draggedItem = sortedItems.find((item) => item.id === $componentDraggedIdStore)
if (draggedItem) {
if (draggedItem && getComputedCols) {
draggedItem[getComputedCols].x = detail.shadow.x
draggedItem[getComputedCols].y = detail.shadow.y
}
@@ -391,33 +410,33 @@
</div>
{/if}
{/if}
{#if xPerPx > 0 && getComputedCols}
{#each sortedItems as item (item.id)}
{#if item[getComputedCols] != undefined}
{#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore}
{@const columnGap = gapX}
<!-- gap between the columns in px -->
{@const containerBorder = 0.5 * 16}
<!-- 0.5rem converted to px (1rem = 16px) -->
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
<!-- subtract borders -->
{@const availableWidth = gridTotalWidth - 11 * columnGap}
<!-- subtract gaps between the 12 columns (11 gaps) -->
{@const columnWidthPx = availableWidth / 12}
<!-- divide by the number of columns -->
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
{#each sortedItems as item (item.id)}
{#if item[getComputedCols] != undefined}
{#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore}
{@const columnGap = gapX}
<!-- gap between the columns in px -->
{@const containerBorder = 0.5 * 16}
<!-- 0.5rem converted to px (1rem = 16px) -->
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
<!-- subtract borders -->
{@const availableWidth = gridTotalWidth - 11 * columnGap}
<!-- subtract gaps between the 12 columns (11 gaps) -->
{@const columnWidthPx = availableWidth / 12}
<!-- divide by the number of columns -->
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
<div
class="absolute"
style={`
<div
class="absolute"
style={`
left: ${item[getComputedCols].x * xPerPx + gapX}px;
top: ${item[getComputedCols].y * yPerPx + gapY}px;
`}
>
<div class="relative h-full w-full">
<div
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
style={`
>
<div class="relative h-full w-full">
<div
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
style={`
left: calc(${
Math.min($fakeShadowStore.x, maxX) * $fakeShadowStore.xPerPx + gapX
}px + 0.5rem + ${getDeltaXByComponent(item.data['type'])});
@@ -427,68 +446,73 @@
width: ${$fakeShadowStore.w * $fakeShadowStore.xPerPx - gapX * 2}px;
height: ${$fakeShadowStore.h * $fakeShadowStore.yPerPx - gapY * 2}px;
`}
/>
/>
</div>
</div>
</div>
{/if}
<MoveResize
on:initmove={() => handleInitMove(item.id)}
on:move={handleMove}
bind:shadow={shadows[item.id]}
bind:this={moveResizes[item.id]}
on:repaint={handleRepaint}
on:resizeStart={() => (resizing = true)}
on:resizeEnd={() => (resizing = false)}
onTop={Boolean(allIdsInPath?.includes(item.id))}
id={item.id}
{xPerPx}
{yPerPx}
fakeShadow={$fakeShadowStore}
on:dropped={(e) => {
$componentDraggedIdStore = undefined
$componentDraggedParentIdStore = undefined
$overlappedStore = undefined
$fakeShadowStore = undefined
lastDetail = undefined
if (!$isCtrlOrMetaPressedStore) {
return
}
dispatch('dropped', e.detail)
}}
width={xPerPx == 0
? 0
: Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
gapX * 2}
height={(item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
top={(item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
left={(item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
item={item[getComputedCols]}
cols={getComputedCols}
{gapX}
{gapY}
{sensor}
container={scroller}
nativeContainer={container}
overlapped={$overlappedStore}
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
type={item.data['type']}
{disableMove}
>
{#if item[getComputedCols]}
<slot
dataItem={item}
hidden={false}
overlapped={$overlappedStore}
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
componentDraggedId={$componentDraggedIdStore}
/>
{/if}
</MoveResize>
{/if}
{/each}
<MoveResize
{mounted}
on:initmove={() => handleInitMove(item.id)}
on:move={handleMove}
bind:shadow={shadows[item.id]}
bind:this={moveResizes[item.id]}
on:repaint={handleRepaint}
on:resizeStart={() => (resizing = true)}
on:resizeEnd={() => (resizing = false)}
onTop={Boolean(allIdsInPath?.includes(item.id))}
id={item.id}
{xPerPx}
{yPerPx}
fakeShadow={$fakeShadowStore}
on:dropped={(e) => {
$componentDraggedIdStore = undefined
$componentDraggedParentIdStore = undefined
$overlappedStore = undefined
$fakeShadowStore = undefined
lastDetail = undefined
if (!$isCtrlOrMetaPressedStore) {
return
}
dispatch('dropped', e.detail)
}}
width={xPerPx == 0
? 0
: Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
gapX * 2}
height={(item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
top={(item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
left={(item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
item={item[getComputedCols]}
cols={getComputedCols}
{gapX}
{gapY}
{sensor}
container={scroller}
nativeContainer={container}
overlapped={$overlappedStore}
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
type={item.data['type']}
{disableMove}
>
{#if item[getComputedCols]}
<slot
dataItem={item}
hidden={false}
overlapped={$overlappedStore}
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
componentDraggedId={$componentDraggedIdStore}
/>
{/if}
</MoveResize>
{/if}
{/each}
{:else if root}
<div
class="h-full w-full flex-col animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
/>
{/if}
</div>
<style>
@@ -39,6 +39,7 @@
export let type: string | undefined = undefined
export let fakeShadow: GridShadow | undefined = undefined
export let disableMove: boolean = true
export let mounted: boolean = false
const ctx = getContext<AppEditorContext>('AppEditorContext')
const { mode, app } = getContext<AppViewerContext>('AppViewerContext')
@@ -504,7 +505,7 @@
: trans
? `transform: translate(${cordDiff.x}px, ${cordDiff.y}px); position:absolute; transition: width 0.2s, height 0.2s;`
: `${
xPerPx > 0 ? 'transition: transform 0.1s, opacity 0.1s;' : ''
xPerPx > 0 && mounted ? 'transition: transform 0.1s, opacity 0.1s;' : ''
} transform: translate(${left}px, ${top}px); `} "
>
<slot />