Compare commits

...

10 Commits

Author SHA1 Message Date
Faton Ramadani
a3373edddf Merge branch 'main' into app-components-outputs 2023-03-13 15:25:03 +01:00
Faton Ramadani
71da2f67e6 feat(frontend): improving connection 2023-03-13 15:24:47 +01:00
Faton Ramadani
0f4da49886 Merge branch 'main' into app-components-outputs 2023-03-13 09:01:48 +01:00
Faton Ramadani
ae81d7ee2c feat(frontend): wip 2023-03-13 09:01:36 +01:00
Faton Ramadani
98f8b845a6 feat(frontend): wip 2023-03-12 19:12:54 +01:00
Faton Ramadani
f521fb63fa feat(frontend): working animations 2023-03-12 14:17:25 +01:00
Faton Ramadani
fce464434b feat(frontend): working animations 2023-03-12 14:09:27 +01:00
Faton Ramadani
ee18804c87 feat(frontend): wip 2023-03-12 11:25:03 +01:00
Faton Ramadani
6b5963f9d2 feat(frontend): merge main 2023-03-11 00:14:49 +01:00
Faton Ramadani
d00a80a8f6 feat(frontend): hierarchical output panel WIP 2023-03-10 21:59:13 +01:00
7 changed files with 324 additions and 157 deletions

View File

@@ -185,13 +185,13 @@
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
<Pane size={15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<Pane size={70}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined

View File

@@ -1,5 +1,5 @@
import { getNextId } from '$lib/components/flows/flowStateUtils'
import type { App, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import type { App, ConnectingInput, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import { getRecommendedDimensionsByComponent, type AppComponent } from './component'
import { gridColumns } from '../gridUtils'
import { allItems } from '../utils'
@@ -34,6 +34,17 @@ export function findGridItemParentGrid(app: App, id: string): string | undefined
}
}
export function isIdInsideGriditem(app: App, gridItem: GridItem, id: string | undefined): boolean {
const path: string[] = []
let currentId = id
while (currentId) {
path.push(currentId)
currentId = findGridItemParentGrid(app, currentId)?.split('-')[0]
}
return path.includes(gridItem.id)
}
export function findGridItem(app: App, id: string): GridItem | undefined {
return findGridItemById(app.grid, app.subgrids, id)
}
@@ -47,7 +58,6 @@ export function getNextGridItemId(app: App): string {
}
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const newComponent = {
resizable: true,
draggable: true,
@@ -105,7 +115,6 @@ export function insertNewGridItem(
app.subgrids = {}
}
// We only want to set subgrids when we are not moving
if (!keepId) {
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
@@ -113,8 +122,9 @@ export function insertNewGridItem(
}
}
const key = focusedGrid ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` : undefined
const key = focusedGrid
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
: undefined
let grid = focusedGrid ? app.subgrids[key!] : app.grid
const newItem = createNewGridItem(grid, id, data)
@@ -177,8 +187,6 @@ export function deleteGridItem(
return components
}
type AvailableSpace = {
left: number
right: number
@@ -286,11 +294,14 @@ function isOverlapping(item1: any, item2: any) {
}
type Outputtable<Type> = {
-readonly [Property in keyof Type]: Output<Type[Property]>;
};
-readonly [Property in keyof Type]: Output<Type[Property]>
}
export function initOutput<I extends Record<string, any>>(world: World, id: string, init: I): Outputtable<I> {
export function initOutput<I extends Record<string, any>>(
world: World,
id: string,
init: I
): Outputtable<I> {
const output = world.outputsById[id] as Outputtable<I>
if (init) {
for (const key in init) {
@@ -322,3 +333,53 @@ export function expandGriditem(
item.w = item.w + left + right
item.h = item.h + top + bottom
}
export function sortGridItemsPosition(
gridItems: GridItem[],
breakpoint: EditorBreakpoint
): GridItem[] {
return gridItems.sort((a: GridItem, b: GridItem) => {
const width = breakpoint === 'lg' ? 12 : 3
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
export function connectInput(
connectingInput: ConnectingInput,
componentId: string,
path: string
): ConnectingInput {
if (connectingInput) {
connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
return connectingInput
}

View File

@@ -1,12 +1,11 @@
<script lang="ts">
import { getContext } from 'svelte'
import {
deleteGridItem,
findGridItem,
findGridItemParentGrid,
getAllSubgridsAndComponentIds,
getGridItems,
insertNewGridItem
insertNewGridItem,
sortGridItemsPosition
} from '../appUtils'
import type { AppEditorContext, AppViewerContext, EditorBreakpoint, GridItem } from '../../types'
import { push } from '$lib/history'
@@ -109,6 +108,7 @@
function handleArrowUp(event: KeyboardEvent) {
if (!$selectedComponent) return
let parentId = findGridItemParentGrid($app, $selectedComponent)?.split('-')[0]
if (parentId) {
$selectedComponent = parentId
} else {
@@ -182,6 +182,7 @@
case 'ArrowUp': {
handleArrowUp(event)
break
}
case 'ArrowDown': {
@@ -221,31 +222,6 @@
break
}
}
function sortGridItemsPosition(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
return gridItems.sort((a: GridItem, b: GridItem) => {
const width = breakpoint === 'lg' ? 12 : 3
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
</script>
<svelte:window on:keydown={keydown} />

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import { components } from '../component'
import { getContext } from 'svelte'
import type { AppViewerContext, GridItem } from '../../types'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
import { classNames } from '$lib/utils'
import { ChevronDown, ChevronRight, Lock } from 'lucide-svelte'
import { connectInput, isIdInsideGriditem } from '../appUtils'
import { slide } from 'svelte/transition'
import SubGridOutput from './SubGridOutput.svelte'
export let gridItem: GridItem
export let first: boolean = false
export let nested: boolean = false
export let parentId: string | undefined = undefined
export let expanded: boolean = false
const { app, staticOutputs, selectedComponent, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
const name = getComponentNameById(gridItem.id)
function getComponentNameById(componentId: string) {
if (gridItem?.data?.type) {
return components[gridItem?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {
return 'Background'
} else {
return 'Table action'
}
}
let manuallyOpened = false
$: if (expanded) {
manuallyOpened = true
} else {
manuallyOpened = false
}
$: subGrids = Array.from({ length: gridItem.data.numberOfSubgrids }).map(
(_, i) => `${gridItem.id}-${i}`
)
$: insideGrid = isIdInsideGriditem($app, gridItem, $selectedComponent)
$: isSelected = $selectedComponent === gridItem.id
$: opened = insideGrid || isSelected || manuallyOpened
function onHeaderClick() {
if (manuallyOpened) {
if (parentId) {
$selectedComponent = parentId
} else {
$selectedComponent = undefined
}
manuallyOpened = false
} else {
$selectedComponent = gridItem.id
manuallyOpened = true
}
}
</script>
{#if $staticOutputs[gridItem.id] || gridItem.data.numberOfSubgrids > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'flex items-center justify-between p-1 cursor-pointer hover:bg-indigo-100 hover:text-indigo-500 border-b',
isSelected ? 'bg-indigo-200' : 'bg-white',
first ? 'border-t' : '',
nested ? 'border-l' : ''
)}
on:click={onHeaderClick}
>
<div
class={classNames(
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
isSelected ? 'bg-indigo-500 text-white' : ' bg-indigo-50'
)}
>
{gridItem.id}
</div>
<div class="text-2xs font-bold flex flex-row gap-2 items-center">
{getComponentNameById(gridItem.id)}
{#if !opened && !manuallyOpened}
<ChevronRight size={14} />
{:else if manuallyOpened}
<Lock size={14} class="text-orange-600" />
{:else}
<ChevronDown size={14} />
{/if}
</div>
</div>
{#if opened}
<div class={classNames('border-b', nested ? 'border-l' : '')} transition:slide|local>
<div class="py-1">
<ComponentOutputViewer
componentId={gridItem.id}
outputs={$staticOutputs[gridItem.id]}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
}}
/>
</div>
<div>
<SubGridOutput {name} {expanded} {subGrids} parentId={gridItem.id} />
</div>
</div>
{/if}
{/if}

View File

@@ -30,6 +30,4 @@
{#if Object.keys(object).length > 0}
<ObjectViewer json={object} on:select topBrackets={false} />
{:else}
<div class="text-xs text-gray-500 px-4">No outputs</div>
{/if}

View File

@@ -1,34 +1,19 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { classNames } from '$lib/utils'
import { X } from 'lucide-svelte'
import { Maximize, Minimize, X } from 'lucide-svelte'
import { getContext } from 'svelte'
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
import type { AppViewerContext } from '../../types'
import { findGridItem } from '../appUtils'
import { connectInput, findGridItem, sortGridItemsPosition } from '../appUtils'
import { components } from '../component'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import ComponentOutput from './ComponentOutput.svelte'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
const { connectingInput, staticOutputs, worldStore, selectedComponent, app } =
const { staticOutputs, app, breakpoint, connectingInput, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
function connectInput(componentId: string, path: string) {
if ($connectingInput) {
$connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
}
function getComponentNameById(componentId: string) {
const component = findGridItem($app, componentId)
@@ -42,12 +27,18 @@
return 'Table action'
}
}
function toggleExpanded() {
expanded = !expanded
}
let search = ''
let expanded = false
$: panels = [['ctx', ['email', 'username', 'query', 'hash']] as [string, string[]]].concat(
Object.entries($staticOutputs)
)
let search = ''
// filter out outputs that don't match the search by name (computed by getComponentNameById) and id
// The output should be [string, string[]][]
$: filteredPanels = panels.filter(([componentId, outputs]) => {
@@ -60,99 +51,54 @@
</script>
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="Outputs">
<div class="overflow-auto h-full min-w-[150px] w-full relative flex flex-col">
<div class="sticky z-50 top-0 left-0 w-full bg-white px-2 pb-2">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (search = '')}
>
<X size="14" />
</button>
{/if}
</div>
</div>
<div class="relative p-2">
{#each filteredPanels as [componentId, outputs] (componentId)}
<div
animate:flip={{ duration: 300 }}
in:fade|local={{ duration: 100, delay: 50 }}
out:fade|local={{ duration: 100 }}
class="pb-2"
>
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
{@const name = getComponentNameById(componentId)}
<div>
<div
class="flex {$connectingInput?.opened
? 'bg-white z-50'
: ''} flex-row justify-between w-full"
>
<button
on:click|stopPropagation|preventDefault={$connectingInput.opened
? undefined
: () => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 border-t border-x font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500/90 text-white border-indigo-500/90'
: 'bg-gray-100 text-gray-500 border-gray-200'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-semibold rounded-t-sm w-fit',
'bg-gray-700 text-white'
)}
>
{name}
</span>
</div>
<div
class={classNames(
$connectingInput?.opened ? 'bg-white z-50' : '',
`w-full py-2 grow border relative break-all `,
$selectedComponent === componentId ? 'border border-indigo-500/90 ' : '',
$connectingInput.hoveredComponent === componentId
? 'outline outline-indigo-500/90'
: ''
)}
>
{#key $selectedComponent}
{#key $connectingInput?.opened}
<ComponentOutputViewer
outputs={$connectingInput?.opened && $selectedComponent === componentId
? name == 'Table'
? ['search']
: []
: outputs}
{componentId}
on:select={({ detail }) => {
connectInput(componentId, detail)
}}
/>
{/key}
{/key}
</div>
</div>
<div style="z-index:1000;" class="bg-white">
<div class="overflow-auto h-full min-w-[150px] w-full relative flex flex-col">
<div class="sticky z-50 top-0 left-0 w-full bg-white px-2 pb-2">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (search = '')}
>
<X size="14" />
</button>
{/if}
</div>
{:else}
<div
in:fade|local={{ duration: 50, delay: 100 }}
out:fade|local={{ duration: 50 }}
class="absolute left-0 top-0 w-full text-sm text-gray-500 text-center py-4 px-2"
>
No outputs found
</div>
</div>
<div class="p-1 ">
<Button on:click={toggleExpanded} color="light" size="xs">
{#if !expanded}
<Maximize size="14" />
{:else}
<Minimize size="14" />
{/if}
</Button>
</div>
<div
class={classNames(
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
$selectedComponent === 'ctx' ? 'bg-indigo-500 text-white' : ' bg-indigo-50'
)}
>
ctx
</div>
<ComponentOutputViewer
componentId={'ctx'}
outputs={['email', 'username', 'query', 'hash']}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, 'ctx', detail)
}}
/>
{#each sortGridItemsPosition($app.grid, $breakpoint) as gridItem, index}
<ComponentOutput {gridItem} first={index === 0} {expanded} />
{/each}
</div>
</div>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import { slide } from 'svelte/transition'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import { connectInput, sortGridItemsPosition } from '../appUtils'
import ComponentOutput from './ComponentOutput.svelte'
export let name: string | undefined = undefined
export let parentId: string
export let expanded: boolean = false
export let subGrids: string[]
const { app, connectingInput, breakpoint, worldStore } =
getContext<AppViewerContext>('AppViewerContext')
let selected = 0
$: outputs = $worldStore?.outputsById[parentId] as {
selectedTabIndex: Output<number>
}
$: if (outputs?.selectedTabIndex) {
outputs.selectedTabIndex.subscribe({
next: (value) => {
selected = value
}
})
}
</script>
{#each subGrids as subGridId, index}
<div class="ml-2 my-2">
{#if subGrids.length > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'px-1 py-0.5 flex justify-between items-center font-semibold text-xs border-l border-y w-full cursor-pointer',
selected === index ? 'bg-gray-200' : 'bg-gray-50'
)}
on:click={() => {
selected = index
}}
>
<div class="text-xs">
{name ? name : 'Should implement'}
{index + 1}
</div>
</div>
{/if}
{#if selected === index || name !== 'Tabs'}
<div transition:slide|local>
{#if $app.subgrids && $app.subgrids[subGridId].length > 0}
{#each sortGridItemsPosition($app.subgrids[subGridId], $breakpoint) as subGridItem, index}
<ComponentOutput
gridItem={subGridItem}
first={index === 0}
nested
{expanded}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, subGridItem.id, detail)
}}
/>
{/each}
{:else}
<div class="text-xs text-gray-500 border-y border-l p-1">No components</div>
{/if}
</div>
{/if}
</div>
{/each}