mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat(frontend): app components output panel (#1283)
* feat(frontend): hierarchical output panel WIP * feat(frontend): wip * feat(frontend): working animations * feat(frontend): working animations * feat(frontend): wip * feat(frontend): wip * feat(frontend): improving connection * feat(frontend): fix search * feat(frontend): output panel v2 * feat(frontend): support table actions * feat(frontend): support table actions * feat(frontend): support background script * feat(frontend): fix background scripts * feat(frontend): simplify code * feat(frontend): fix animation * feat(frontend): fix wording * feat(frontend): fix bg script click * feat(frontend): fix bg script click * feat(frontend): fix bg script click
This commit is contained in:
@@ -186,13 +186,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -325,3 +336,76 @@ 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
|
||||
}
|
||||
|
||||
export function recursivelyFilterKeyInJSON(
|
||||
json: object,
|
||||
search: string,
|
||||
extraSearch?: string | undefined
|
||||
): object {
|
||||
let filteredJSON = {}
|
||||
Object.keys(json).forEach((key) => {
|
||||
if (
|
||||
key.toLowerCase().includes(search.toLowerCase()) ||
|
||||
extraSearch?.toLowerCase().includes(search.toLowerCase())
|
||||
) {
|
||||
filteredJSON[key] = json[key]
|
||||
} else if (typeof json[key] === 'object') {
|
||||
const res = recursivelyFilterKeyInJSON(json[key], search, extraSearch)
|
||||
|
||||
if (Object.keys(res).length !== 0) {
|
||||
filteredJSON[key] = res
|
||||
}
|
||||
}
|
||||
})
|
||||
return filteredJSON
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { components } from '../component'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext, GridItem } from '../../types'
|
||||
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
|
||||
import { connectInput, isIdInsideGriditem } from '../appUtils'
|
||||
import SubGridOutput from './SubGridOutput.svelte'
|
||||
import OutputHeader from './components/OutputHeader.svelte'
|
||||
import TableActionsOutput from './components/TableActionsOutput.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'
|
||||
}
|
||||
}
|
||||
|
||||
$: subGrids = Array.from({ length: gridItem.data.numberOfSubgrids }).map(
|
||||
(_, i) => `${gridItem.id}-${i}`
|
||||
)
|
||||
|
||||
$: insideGrid = isIdInsideGriditem($app, gridItem, $selectedComponent)
|
||||
$: isSelected = $selectedComponent === gridItem.id
|
||||
$: shouldOpen = insideGrid || isSelected
|
||||
|
||||
function onHeaderClick(manuallyOpen: boolean) {
|
||||
if (manuallyOpen) {
|
||||
if (parentId) {
|
||||
$selectedComponent = parentId
|
||||
} else {
|
||||
$selectedComponent = undefined
|
||||
}
|
||||
} else {
|
||||
$selectedComponent = gridItem.id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $staticOutputs[gridItem.id] || gridItem.data.numberOfSubgrids > 1}
|
||||
<OutputHeader
|
||||
{shouldOpen}
|
||||
on:handleClick={(e) => {
|
||||
onHeaderClick(e.detail.manuallyOpen)
|
||||
}}
|
||||
id={gridItem.id}
|
||||
name={getComponentNameById(gridItem.id)}
|
||||
{first}
|
||||
{nested}
|
||||
{expanded}
|
||||
>
|
||||
<div class="py-1">
|
||||
<ComponentOutputViewer
|
||||
componentId={gridItem.id}
|
||||
outputs={$staticOutputs[gridItem.id]}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubGridOutput {name} {expanded} {subGrids} parentId={gridItem.id} />
|
||||
<TableActionsOutput {gridItem} {expanded} />
|
||||
</OutputHeader>
|
||||
{/if}
|
||||
@@ -1,19 +1,22 @@
|
||||
<script lang="ts">
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { recursivelyFilterKeyInJSON } from '../appUtils'
|
||||
|
||||
export let outputs: string[] = []
|
||||
export let componentId: string
|
||||
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { search } = getContext<{ search: Writable<string> }>('searchCtx')
|
||||
|
||||
let object = {}
|
||||
|
||||
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>>) {
|
||||
if (observableOutputs) {
|
||||
outputs.forEach((output: string) => {
|
||||
outputs?.forEach((output: string) => {
|
||||
object[output] = undefined
|
||||
observableOutputs[output]?.subscribe({
|
||||
next: (value) => {
|
||||
@@ -26,10 +29,12 @@
|
||||
|
||||
$: $worldStore?.outputsById[componentId] &&
|
||||
subscribeToAllOutputs($worldStore.outputsById[componentId])
|
||||
|
||||
$: filtered = recursivelyFilterKeyInJSON(object, $search, componentId)
|
||||
</script>
|
||||
|
||||
{#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 Object.keys(filtered).length > 0}
|
||||
<ObjectViewer json={filtered} on:select topBrackets={false} />
|
||||
{:else if $search.length > 0}
|
||||
<div class="text-xs pl-2">No results</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,167 +1,99 @@
|
||||
<script lang="ts">
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { 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 { components } from '../component'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const { connectingInput, staticOutputs, worldStore, selectedComponent, app, state } =
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { connectInput, recursivelyFilterKeyInJSON, sortGridItemsPosition } from '../appUtils'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import ComponentOutput from './ComponentOutput.svelte'
|
||||
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
|
||||
import BackgroundScriptsOutput from './components/BackgroundScriptsOutput.svelte'
|
||||
import MinMaxButton from './components/MinMaxButton.svelte'
|
||||
import OutputHeader from './components/OutputHeader.svelte'
|
||||
|
||||
const { connectingInput, breakpoint, app, state } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function connectInput(componentId: string, path: string) {
|
||||
if ($connectingInput) {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: {
|
||||
connection: {
|
||||
componentId,
|
||||
path
|
||||
},
|
||||
type: 'connected'
|
||||
},
|
||||
hoveredComponent: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
let search = writable<string>('')
|
||||
let expanded = false
|
||||
let ctxOpened = true
|
||||
let stateOpened = true
|
||||
|
||||
function getComponentNameById(componentId: string) {
|
||||
const component = findGridItem($app, componentId)
|
||||
|
||||
if (component?.data?.type) {
|
||||
return components[component?.data.type].name
|
||||
} else if (componentId == 'ctx') {
|
||||
return 'Context'
|
||||
} else if (componentId.startsWith('bg_')) {
|
||||
return 'Background'
|
||||
} else {
|
||||
return 'Table action'
|
||||
}
|
||||
}
|
||||
$: 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]) => {
|
||||
const name = getComponentNameById(componentId)
|
||||
return (
|
||||
name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
componentId.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
setContext('searchCtx', {
|
||||
search
|
||||
})
|
||||
|
||||
$: expanded && !ctxOpened && (ctxOpened = true)
|
||||
$: expanded && !stateOpened && (stateOpened = true)
|
||||
|
||||
$: filteredState = recursivelyFilterKeyInJSON($state, $search)
|
||||
</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 w-full h-full">
|
||||
<div class="min-w-[150px]">
|
||||
<div class="sticky z-50 top-0 left-0 w-full bg-white p-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>
|
||||
{/each}
|
||||
</div>
|
||||
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="State">
|
||||
<div class="mx-2 px-1 border w-full mb-8">
|
||||
{#key $state}
|
||||
<ObjectViewer json={$state} />
|
||||
{/key}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<div class="p-1 ">
|
||||
<MinMaxButton bind:expanded />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<span class="text-sm font-bold p-2">State & Context</span>
|
||||
|
||||
<OutputHeader id={'ctx'} name={'App Context'} first color="blue" {expanded}>
|
||||
<ComponentOutputViewer
|
||||
componentId={'ctx'}
|
||||
outputs={['email', 'username', 'query', 'hash']}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, 'ctx', detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
|
||||
<OutputHeader id={'State'} name={'State'} color="blue" {expanded}>
|
||||
{#key $state}
|
||||
{#if Object.keys(filteredState).length > 0}
|
||||
<ObjectViewer json={filteredState} />
|
||||
{:else}
|
||||
<div class="text-xs pl-2">No results</div>
|
||||
{/if}
|
||||
{/key}
|
||||
</OutputHeader>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-bold p-2">Components</span>
|
||||
{#each sortGridItemsPosition($app.grid, $breakpoint) as gridItem, index}
|
||||
<ComponentOutput {gridItem} first={index === 0} {expanded} />
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-bold p-2">Background scripts</span>
|
||||
<div class="border-t">
|
||||
<BackgroundScriptsOutput {expanded} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<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 name'}
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selected === index || name !== 'Tabs'}
|
||||
<div transition:slide|local class="border-l">
|
||||
{#if $app.subgrids && $app.subgrids[subGridId].length > 0}
|
||||
{#each sortGridItemsPosition($app.subgrids[subGridId], $breakpoint) as subGridItem, index}
|
||||
<ComponentOutput
|
||||
gridItem={subGridItem}
|
||||
first={index === 0}
|
||||
{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}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { getContext } from 'svelte'
|
||||
import { connectInput } from '../../appUtils'
|
||||
import ComponentOutputViewer from '../ComponentOutputViewer.svelte'
|
||||
import OutputHeader from './OutputHeader.svelte'
|
||||
|
||||
const { selectedComponent, connectingInput } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
export let expanded: boolean = false
|
||||
export let first: boolean = false
|
||||
|
||||
function onHeaderClick(manuallyOpen: boolean) {
|
||||
if (manuallyOpen) {
|
||||
$selectedComponent = undefined
|
||||
} else {
|
||||
$selectedComponent = id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<OutputHeader
|
||||
{id}
|
||||
{name}
|
||||
color="blue"
|
||||
{first}
|
||||
{expanded}
|
||||
on:handleClick={(e) => {
|
||||
onHeaderClick(e.detail.manuallyOpen)
|
||||
}}
|
||||
shouldOpen={$selectedComponent === id}
|
||||
>
|
||||
<ComponentOutputViewer
|
||||
componentId={id}
|
||||
outputs={['loading', 'result']}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, id, detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { getContext } from 'svelte'
|
||||
import OutputHeader from './OutputHeader.svelte'
|
||||
import BackgroundScriptOutput from './BackgroundScriptOutput.svelte'
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let expanded: boolean = false
|
||||
</script>
|
||||
|
||||
{#each $app.hiddenInlineScripts as action, index}
|
||||
<BackgroundScriptOutput id={`bg_${index}`} name={action.name} {expanded} />
|
||||
{/each}
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Maximize, Minimize } from 'lucide-svelte'
|
||||
|
||||
export let expanded: boolean = false
|
||||
</script>
|
||||
|
||||
<Button
|
||||
on:click={() => {
|
||||
expanded = !expanded
|
||||
}}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
>
|
||||
{#if !expanded}
|
||||
<div class="flex flex-row gap-2">
|
||||
<Maximize size="14" />
|
||||
Expand all
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2">
|
||||
<Minimize size="14" />
|
||||
Collapse all
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
export let first: boolean = false
|
||||
export let nested: boolean = false
|
||||
export let color: 'blue' | 'indigo' = 'indigo'
|
||||
export let expanded: boolean = false
|
||||
export let shouldOpen: boolean = false
|
||||
|
||||
$: open = shouldOpen
|
||||
let manuallyOpen = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: if (expanded) {
|
||||
manuallyOpen = true
|
||||
} else {
|
||||
manuallyOpen = false
|
||||
}
|
||||
|
||||
const hoverColor = {
|
||||
blue: 'hover:bg-blue-300 hover:text-blue-600',
|
||||
indigo: 'hover:bg-indigo-300 hover:text-indigo-600'
|
||||
}
|
||||
|
||||
const openBackground = {
|
||||
blue: 'bg-blue-50',
|
||||
indigo: 'bg-indigo-50'
|
||||
}
|
||||
|
||||
const manuallyOpenColor = {
|
||||
blue: 'text-blue-600',
|
||||
indigo: 'text-indigo-600'
|
||||
}
|
||||
|
||||
const idClass = {
|
||||
blue: 'bg-blue-500 text-white',
|
||||
indigo: 'bg-indigo-500 text-white'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'flex items-center justify-between p-1 cursor-pointer border-b',
|
||||
hoverColor[color],
|
||||
open && !manuallyOpen ? openBackground[color] : 'bg-white',
|
||||
first ? 'border-t' : '',
|
||||
nested ? 'border-l' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
dispatch('handleClick', { manuallyOpen })
|
||||
manuallyOpen = !manuallyOpen
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
|
||||
open ? idClass[color] : ' bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
<div class="text-2xs font-bold flex flex-row gap-2 items-center">
|
||||
{name}
|
||||
{#if !open && !manuallyOpen}
|
||||
<ChevronDown size={14} />
|
||||
{:else if manuallyOpen}
|
||||
<ChevronUp size={14} class={manuallyOpenColor[color]} strokeWidth={4} />
|
||||
{:else}
|
||||
<ChevronUp size={14} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if open || manuallyOpen}
|
||||
<div class="py-1 border-b" transition:slide|local>
|
||||
<div class={classNames(nested ? 'border-l ml-2' : '')}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { getContext } from 'svelte'
|
||||
import { connectInput } from '../../appUtils'
|
||||
import ComponentOutputViewer from '../ComponentOutputViewer.svelte'
|
||||
import OutputHeader from './OutputHeader.svelte'
|
||||
|
||||
const { staticOutputs, connectingInput } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let id: string
|
||||
export let expanded: boolean = false
|
||||
export let first: boolean = false
|
||||
</script>
|
||||
|
||||
<OutputHeader {id} name={'Table action'} {first} {expanded}>
|
||||
<ComponentOutputViewer
|
||||
componentId={id}
|
||||
outputs={$staticOutputs[id]}
|
||||
on:select={({ detail }) => {
|
||||
$connectingInput = connectInput($connectingInput, id, detail)
|
||||
}}
|
||||
/>
|
||||
</OutputHeader>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { GridItem } from '$lib/components/apps/types'
|
||||
import TableActionOutput from './TableActionOutput.svelte'
|
||||
|
||||
export let gridItem: GridItem
|
||||
export let expanded: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="my-1">
|
||||
{#if gridItem.data.type === 'tablecomponent' && gridItem.data.actionButtons.length > 0}
|
||||
<div class="ml-2 border-l">
|
||||
{#each gridItem.data.actionButtons as action, index}
|
||||
<TableActionOutput id={action.id} {expanded} first={index === 0} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user