feat: introduce container groups

This commit is contained in:
Ruben Fiszel
2023-09-08 09:48:28 +02:00
parent 973d11882e
commit 49c5553f3b
18 changed files with 219 additions and 43 deletions
@@ -0,0 +1,11 @@
<script lang="ts">
import { setContext } from 'svelte'
import type { Writable } from 'svelte/store'
import type { GroupContext } from '../types'
export let groupContext: Writable<Record<string, any>>
setContext<GroupContext>('GroupContext', groupContext)
</script>
<slot />
@@ -7,7 +7,7 @@
TemplateV2Input,
UploadAppInput
} from '../../inputType'
import type { AppViewerContext, ListContext, RichConfiguration } from '../../types'
import type { AppViewerContext, GroupContext, ListContext, RichConfiguration } from '../../types'
import { accessPropertyByPath } from '../../utils'
import { computeGlobalContext, eval_like } from './eval'
import deepEqualWithOrderedArray from './deepEqualWithOrderedArray'
@@ -26,6 +26,7 @@
const iterContext = getContext<ListContext>('ListWrapperContext')
const rowContext = getContext<ListContext>('RowWrapperContext')
const groupContext = getContext<GroupContext>('GroupContext')
let previousConnectedValue: any | undefined = undefined
@@ -33,19 +34,28 @@
$: fullContext = {
iter: iterContext ? $iterContext : undefined,
row: rowContext ? $rowContext : undefined
row: rowContext ? $rowContext : undefined,
group: groupContext ? $groupContext : undefined
}
$: lastInput?.type == 'evalv2' &&
(fullContext.iter != undefined || fullContext.row != undefined) &&
lastInput.connections.some((x) => x.componentId == 'row' || x.componentId == 'iter') &&
(fullContext.iter != undefined ||
fullContext.row != undefined ||
fullContext.group != undefined) &&
lastInput.connections.some(
(x) => x.componentId == 'row' || x.componentId == 'iter' || x.componentId == 'group'
) &&
debounceEval()
$: lastInput &&
lastInput.type == 'templatev2' &&
isCodeInjection(lastInput.eval) &&
(fullContext.iter != undefined || fullContext.row != undefined) &&
lastInput.connections.some((x) => x.componentId == 'row' || x.componentId == 'iter') &&
(fullContext.iter != undefined ||
fullContext.row != undefined ||
fullContext.group != undefined) &&
lastInput.connections.some(
(x) => x.componentId == 'row' || x.componentId == 'iter' || x.componentId == 'group'
) &&
debounceTemplate()
const dispatch = createEventDispatcher()
@@ -10,7 +10,13 @@
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, CancelablePromise, InlineScript, ListContext } from '../../types'
import type {
AppViewerContext,
CancelablePromise,
GroupContext,
InlineScript,
ListContext
} from '../../types'
import { computeGlobalContext, eval_like } from './eval'
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
@@ -65,6 +71,7 @@
} = getContext<AppViewerContext>('AppViewerContext')
const iterContext = getContext<ListContext>('ListWrapperContext')
const rowContext = getContext<ListContext>('RowWrapperContext')
const groupContext = getContext<GroupContext>('GroupContext')
const dispatch = createEventDispatcher()
@@ -179,7 +186,8 @@
runnable.inlineScript?.content,
computeGlobalContext($worldStore, {
iter: iterContext ? $iterContext : undefined,
row: rowContext ? $rowContext : undefined
row: rowContext ? $rowContext : undefined,
group: groupContext ? $groupContext : undefined
}),
false,
$state,
@@ -2,20 +2,28 @@
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
// import type { EvalV2AppInput, StaticAppInput } from '../../inputType'
import { writable } from 'svelte/store'
import { InputValue } from '../helpers'
import GroupWrapper from '../GroupWrapper.svelte'
export let id: string
export let componentContainerHeight: number
export let customCss: ComponentCustomCSS<'containercomponent'> | undefined = undefined
export let render: boolean
export let groupFields: RichConfigurations | undefined = undefined
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
let groupContext = writable({})
let outputs = initOutput($worldStore, id, { group: $groupContext })
$: outputs.group.set($groupContext, true)
function onFocus() {
$focusedGrid = {
@@ -29,21 +37,29 @@
<InitializeComponent {id} />
{#each Object.keys(groupFields ?? {}) as field}
{#if groupFields && field in groupFields}
<InputValue key={field} {id} input={groupFields[field]} bind:value={$groupContext[field]} />
{/if}
{/each}
<div class="w-full h-full">
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
<GroupWrapper {groupContext}>
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</GroupWrapper>
{/if}
</div>
@@ -13,7 +13,8 @@ import {
getRecommendedDimensionsByComponent,
type AppComponent,
type BaseComponent,
type InitialAppComponent
type InitialAppComponent,
type TypedComponent
} from './component'
import { gridColumns } from '../gridUtils'
import { allItems } from '../utils'
@@ -598,7 +599,8 @@ export function sortGridItemsPosition<T>(
export function connectInput(
connectingInput: ConnectingInput,
componentId: string,
path: string
path: string,
componentType?: TypedComponent['type']
): ConnectingInput {
if (connectingInput) {
if (connectingInput.onConnect) {
@@ -610,7 +612,8 @@ export function connectInput(
input: {
connection: {
componentId,
path
path,
componentType
},
type: 'connected'
},
@@ -15,7 +15,10 @@ export async function inferDeps(
const noutputs = outputs
.filter(
([componentId, id]) =>
componentId == 'row' || componentId == 'iter' || id in (worldOutputs[componentId] ?? {})
componentId == 'row' ||
componentId == 'iter' ||
componentId == 'group' ||
id in (worldOutputs[componentId] ?? {})
)
.map(([componentId, id]) => ({
componentId: componentId,
@@ -480,6 +480,7 @@
/>
{:else if component.type === 'containercomponent'}
<AppContainer
groupFields={component.groupFields}
id={component.id}
customCss={component.customCss}
{componentContainerHeight}
@@ -48,6 +48,7 @@ import type {
ComponentCustomCSS,
GridItem,
RichConfiguration,
RichConfigurations,
StaticRichConfigurations
} from '../../types'
import type { Size } from '../../svelte-grid/types'
@@ -115,7 +116,9 @@ export type TabsComponent = BaseComponent<'tabscomponent'> & {
disabledTabs: RichConfiguration[]
}
export type ListComponent = BaseComponent<'listcomponent'>
export type ContainerComponent = BaseComponent<'containercomponent'>
export type ContainerComponent = BaseComponent<'containercomponent'> & {
groupFields: RichConfigurations
}
export type DrawerComponent = BaseComponent<'drawercomponent'>
export type MapComponent = BaseComponent<'mapcomponent'>
export type VerticalSplitPanesComponent = BaseComponent<'verticalsplitpanescomponent'> & {
@@ -39,7 +39,11 @@
componentId={gridItem.id}
on:select={({ detail }) => {
if ($connectingInput.opened) {
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
let typ = gridItem?.data?.type
let splitted = detail?.split('.')
let componentId = typ == 'containercomponent' ? splitted?.[0] : gridItem.id
let path = typ == 'containercomponent' ? splitted?.[1] : detail
$connectingInput = connectInput($connectingInput, componentId, path, typ)
}
}}
/>
@@ -17,6 +17,7 @@
if (observableOutputs) {
Object.entries(observableOutputs).forEach(([k, output]) => {
object[k] = undefined
console.log('subscribing to', k, output)
output?.subscribe(
{
id: 'alloutputs' + componentId + '-' + k,
@@ -32,6 +32,7 @@
import { inferDeps } from '../appUtilsInfer'
import EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte'
import type { ResultAppInput } from '../../inputType'
import GridGroup from './GridGroup.svelte'
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
undefined
@@ -310,6 +311,11 @@
bind:component={componentSettings.item.data}
word="Step"
/>
{:else if componentSettings.item.data.type === 'containercomponent'}
<GridGroup
bind:groupFields={componentSettings.item.data.groupFields}
bind:component={componentSettings.item.data}
/>
{:else if componentSettings.item.data.type === 'conditionalwrapper'}
<GridCondition
bind:conditions={componentSettings.item.data.conditions}
@@ -333,7 +339,7 @@
userInputEnabled={false}
/>
</PanelSection>
{:else}
{:else if componentSettings.item.data.type != 'containercomponent'}
<div class="h-full w-full text-sm text-tertiary text-center py-8 px-2"
>{ccomponents[component.type].name} has no configuration</div
>
@@ -0,0 +1,104 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import type { RichConfigurations } from '../../types'
import type { AppComponent } from '../component'
import PanelSection from './common/PanelSection.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
export let groupFields: RichConfigurations | undefined
export let component: AppComponent
// const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
let fieldName: string = ''
function addField(name: string) {
if (name == '') return
groupFields = {
...(groupFields ?? {}),
[name]: {
type: 'static',
value: '',
fieldType: 'object'
}
}
}
</script>
<div class="flex p-2 gap-1 items-center">
<Toggle
size="xs"
checked={groupFields != undefined}
on:change={(e) => {
if (e.detail) {
groupFields = {}
} else {
groupFields = undefined
}
console.log(groupFields)
}}
options={{ right: 'container is a component group' }}
/>
<Tooltip
>Group fields allow inner components to depend on the group fields which make the container a
group of component that is encapsulated. Inside the group, it is possible to retrieve the values
using `group.<x />` where x is the group field name</Tooltip
>
</div>
{#if groupFields != undefined}
<PanelSection
title={`Group Fields ${
Object.keys(groupFields ?? {}).length > 0
? `(${Object.keys(groupFields ?? {}).length ?? 0})`
: ''
}`}
>
{#if Object.keys(groupFields ?? {}).length == 0}
<span class="text-xs text-tertiary">No group fields</span>
{/if}
<div class="w-full flex gap-2 flex-col mt-2">
<InputsSpecsEditor
on:delete={(e) => {
if (!groupFields) {
return
}
delete groupFields[e.detail]
groupFields = groupFields
}}
id={component.id}
shouldCapitalize={false}
displayType
deletable
bind:inputSpecs={groupFields}
/>
<div class="flex flex-row gap-2 items-center relative">
<input
type="text"
on:keydown|stopPropagation={(event) => {
switch (event.key) {
case 'Enter':
event.preventDefault()
addField(fieldName)
break
}
}}
placeholder="Group Field Name"
bind:value={fieldName}
/>
<Button
disabled={fieldName == ''}
size="sm"
color="light"
variant="border"
startIcon={{ icon: faPlus }}
on:click={() => addField(fieldName)}
iconOnly
/>
</div>
</div>
</PanelSection>
{/if}
@@ -17,15 +17,6 @@
export let canDisableTabs: boolean = false
// Migration code
$: if (tabs?.length !== disabledTabs?.length && canDisableTabs) {
disabledTabs = Array(tabs.length).fill({
type: 'static',
value: false,
fieldType: 'boolean'
})
}
export let word: string = 'Tab'
export let component: AppComponent
@@ -75,7 +75,7 @@
{/if}
</span>
{#if displayType}
<div class="text-xs text-tertiary">
<div class="text-xs text-tertiary mr-1">
{fieldType === 'array' && subFieldType
? `${fieldTypeToTsType(subFieldType)}[]`
: fieldTypeToTsType(fieldType)}
@@ -1,4 +1,6 @@
<script lang="ts">
import CloseButton from '$lib/components/common/CloseButton.svelte'
import { createEventDispatcher } from 'svelte'
import type { RichConfigurations } from '../../types'
import InputsSpecEditor from './InputsSpecEditor.svelte'
@@ -11,8 +13,11 @@
export let shouldCapitalize: boolean = true
export let resourceOnly = false
export let displayType = false
export let deletable = false
$: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs
const dispatch = createEventDispatcher()
</script>
{#if inputSpecs}
@@ -48,6 +53,10 @@
customTitle={meta?.['customTitle']}
{displayType}
/>
{#if deletable}
<div class="flex flex-row-reverse -mt-4">
<CloseButton noBg on:close={() => dispatch('delete', k)} />
</div>{/if}
{/if}
{/each}
</div>
@@ -1,4 +1,5 @@
import type { ReadFileAs } from '../common/fileInput/model'
import type { TypedComponent } from './editor/component'
import type { InlineScript } from './types'
export type InputType =
@@ -25,6 +26,7 @@ export type InputType =
// Connection to an output of another component
// defined by the id of the component and the path of the output
export type InputConnection = {
componentType?: TypedComponent['type']
componentId: string
path: string
}
@@ -163,6 +163,8 @@ export type ListContext = Writable<{
export type ListInputs = (id: string, value: any) => void
export type GroupContext = Writable<Record<string, any>>
export type AppViewerContext = {
worldStore: Writable<World>
app: Writable<App>
+3 -1
View File
@@ -185,7 +185,9 @@ declare function closeModal(id: string): void;
}
declare const state: ${JSON.stringify(state)};
declare const iter: {index: number, value: any};
declare const row: {index: number, value: any};
declare const row: Record<string, any>;
declare const group: Record<string, any>;
`
}