mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
feat(frontend): add menu component (#2721)
* feat(frontend): add menu component * wip * feat(frontend): menu component done * feat(frontend): Fix styling + handle menu items everywhere * feat(frontend): Fix styling + handle menu items everywhere
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
}
|
||||
|
||||
export let items: Item[] | (() => Item[]) = []
|
||||
export let justifyEnd: boolean = true
|
||||
|
||||
function computeItems(): Item[] {
|
||||
if (typeof items === 'function') {
|
||||
@@ -24,7 +25,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu placement="bottom-end" justifyEnd on:close on:open>
|
||||
<Menu placement="bottom-end" {justifyEnd} on:close on:open>
|
||||
<div slot="trigger">
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
export let render: boolean
|
||||
export let errorHandledByComponent: boolean | undefined = false
|
||||
export let extraKey: string | undefined = undefined
|
||||
export let isMenuItem: boolean = false
|
||||
|
||||
export let controls: { left: () => boolean; right: () => boolean | string } | undefined =
|
||||
undefined
|
||||
@@ -178,11 +179,18 @@
|
||||
{#key css}
|
||||
<Button
|
||||
on:pointerdown={(e) => e.stopPropagation()}
|
||||
btnClasses={twMerge(css?.button?.class ?? '', 'wm-button')}
|
||||
btnClasses={twMerge(
|
||||
css?.button?.class ?? '',
|
||||
isMenuItem ? 'flex items-center justify-start' : '',
|
||||
isMenuItem ? '!border-0' : '',
|
||||
'wm-button'
|
||||
)}
|
||||
variant={isMenuItem ? 'border' : 'contained'}
|
||||
style={css?.button?.style}
|
||||
wrapperClasses={twMerge(
|
||||
css?.container?.class ?? '',
|
||||
resolvedConfig.fillContainer ? 'w-full h-full' : '',
|
||||
isMenuItem ? 'w-full' : '',
|
||||
'wm-button-container'
|
||||
)}
|
||||
wrapperStyle={css?.container?.style}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import { components, type ButtonComponent } from '../../editor/component'
|
||||
import type {
|
||||
AppViewerContext,
|
||||
BaseAppComponent,
|
||||
ComponentCustomCSS,
|
||||
RichConfigurations
|
||||
} from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { AlignWrapper } from '../helpers'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { loadIcon } from '../icon'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import Menu from '$lib/components/common/menu/MenuV2.svelte'
|
||||
import { AppButton } from '../buttons'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'menucomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let menuItems: (BaseAppComponent & ButtonComponent)[]
|
||||
|
||||
const resolvedConfig = initConfig(
|
||||
components['menucomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
initOutput($worldStore, id, {})
|
||||
|
||||
let beforeIconComponent: any
|
||||
let afterIconComponent: any
|
||||
let css = initCss($app.css?.menucomponent, customCss)
|
||||
|
||||
$: resolvedConfig.beforeIcon && handleBeforeIcon()
|
||||
$: resolvedConfig.afterIcon && handleAfterIcon()
|
||||
|
||||
async function handleBeforeIcon() {
|
||||
if (resolvedConfig.beforeIcon) {
|
||||
beforeIconComponent = await loadIcon(resolvedConfig.beforeIcon)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAfterIcon() {
|
||||
if (resolvedConfig.afterIcon) {
|
||||
afterIconComponent = await loadIcon(resolvedConfig.afterIcon)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
{#each Object.keys(components['menucomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#each Object.keys(css ?? {}) as key (key)}
|
||||
<ResolveStyle
|
||||
{id}
|
||||
{customCss}
|
||||
{key}
|
||||
bind:css={css[key]}
|
||||
componentStyle={$app.css?.menucomponent}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if render}
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<Menu placement="bottom-end" justifyEnd={false} on:close on:open>
|
||||
<div slot="trigger">
|
||||
<Button
|
||||
on:pointerdown={(e) => e.stopPropagation()}
|
||||
btnClasses={twMerge(
|
||||
css?.button?.class,
|
||||
'wm-button',
|
||||
'wm-download-button',
|
||||
resolvedConfig.fillContainer ? 'w-full h-full' : ''
|
||||
)}
|
||||
wrapperClasses={twMerge(
|
||||
'wm-button-container',
|
||||
'wm-download-button-container',
|
||||
resolvedConfig.fillContainer ? 'w-full h-full' : ''
|
||||
)}
|
||||
style={css?.button?.style}
|
||||
size={resolvedConfig.size}
|
||||
color={resolvedConfig.color}
|
||||
nonCaptureEvent
|
||||
>
|
||||
<span class="truncate inline-flex gap-2 items-center">
|
||||
{#if resolvedConfig.beforeIcon && beforeIconComponent}
|
||||
<svelte:component this={beforeIconComponent} size={14} />
|
||||
{/if}
|
||||
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
|
||||
<div>{resolvedConfig.label}</div>
|
||||
{/if}
|
||||
{#if resolvedConfig.afterIcon && afterIconComponent}
|
||||
<svelte:component this={afterIconComponent} size={14} />
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full p-1 gap-2">
|
||||
{#if menuItems.length > 0}
|
||||
{#each menuItems as actionButton, actionIndex (actionButton?.id)}
|
||||
{#if actionButton.type == 'buttoncomponent'}
|
||||
<AppButton
|
||||
extraKey={'idx' + actionIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
componentInput={actionButton.componentInput}
|
||||
noWFull={false}
|
||||
isMenuItem={true}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Menu>
|
||||
</AlignWrapper>
|
||||
{/if}
|
||||
@@ -183,6 +183,9 @@
|
||||
if (c.type === 'tablecomponent') {
|
||||
r.push(...c.actionButtons.map((x) => ({ input: x.componentInput, id: x.id })))
|
||||
}
|
||||
if (c.type === 'menucomponent') {
|
||||
r.push(...c.menuItems.map((x) => ({ input: x.componentInput, id: x.id })))
|
||||
}
|
||||
return r
|
||||
.filter((x) => x.input)
|
||||
.map(async (o) => {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
$: componentSettings = findComponentSettings($app, $selectedComponent?.[0])
|
||||
$: tableActionSettings = findTableActionSettings($app, $selectedComponent?.[0])
|
||||
$: menuItemsSettings = findMenuItemsSettings($app, $selectedComponent?.[0])
|
||||
|
||||
function findTableActionSettings(app: App, id: string | undefined) {
|
||||
return allItems(app.grid, app.subgrids)
|
||||
@@ -33,6 +34,21 @@
|
||||
.find((x) => x)
|
||||
}
|
||||
|
||||
function findMenuItemsSettings(app: App, id: string | undefined) {
|
||||
return allItems(app.grid, app.subgrids)
|
||||
.map((x) => {
|
||||
if (x?.data?.type === 'menucomponent') {
|
||||
if (x?.data?.menuItems) {
|
||||
const menuItem = x.data.menuItems.find((x) => x.id === id)
|
||||
if (menuItem) {
|
||||
return { item: { data: menuItem, id: menuItem.id }, parent: x.data.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.find((x) => x)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -65,6 +81,26 @@
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{:else if menuItemsSettings}
|
||||
{#key menuItemsSettings?.item?.id}
|
||||
<ComponentPanel
|
||||
noGrid
|
||||
bind:componentSettings={menuItemsSettings}
|
||||
duplicateMoveAllowed={false}
|
||||
onDelete={() => {
|
||||
if (menuItemsSettings) {
|
||||
const parent = findGridItem($app, menuItemsSettings.parent)
|
||||
if (!parent) return
|
||||
|
||||
if (parent.data.type === 'menucomponent') {
|
||||
parent.data.menuItems = parent.data.menuItems.filter(
|
||||
(x) => x.id !== menuItemsSettings?.item.id
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{:else if hiddenInlineScript}
|
||||
{@const id = BG_PREFIX + hiddenInlineScript.index}
|
||||
{#key id}
|
||||
|
||||
@@ -64,6 +64,8 @@ export function dfs(
|
||||
item.data.actionButtons.find((x) => id == x.id)
|
||||
) {
|
||||
return [item.id, id]
|
||||
} else if (item.data.type == 'menucomponent' && item.data.menuItems.find((x) => id == x.id)) {
|
||||
return [item.id, id]
|
||||
} else {
|
||||
for (let i = 0; i < (item.data.numberOfSubgrids ?? 0); i++) {
|
||||
const res = dfs(subgrids[`${item.id}-${i}`], id, subgrids)
|
||||
@@ -157,6 +159,9 @@ export function allsubIds(app: App, parentId: string): string[] {
|
||||
if (item.data.type === 'tablecomponent') {
|
||||
subIds.push(...item.data.actionButtons?.map((x) => x.id))
|
||||
}
|
||||
if (item.data.type === 'menucomponent') {
|
||||
subIds.push(...item.data.menuItems?.map((x) => x.id))
|
||||
}
|
||||
}
|
||||
return subIds
|
||||
}
|
||||
@@ -171,6 +176,8 @@ export function getNextGridItemId(app: App): string {
|
||||
const allIds = allItems(app.grid, app.subgrids).flatMap((x) => {
|
||||
if (x.data.type === 'tablecomponent') {
|
||||
return [x.id, ...x.data.actionButtons.map((x) => x.id)]
|
||||
} else if (x.data.type === 'menucomponent') {
|
||||
return [x.id, ...x.data.menuItems.map((x) => x.id)]
|
||||
} else {
|
||||
return [x.id]
|
||||
}
|
||||
@@ -302,6 +309,7 @@ export function appComponentFromType<T extends keyof typeof components>(
|
||||
customCss: ccomponents[type].customCss as any,
|
||||
recomputeIds: init.recomputeIds ? [] : undefined,
|
||||
actionButtons: init.actionButtons ? [] : undefined,
|
||||
menuItems: init.menuItems ? [] : undefined,
|
||||
numberOfSubgrids: init.numberOfSubgrids,
|
||||
horizontalAlignment: init.horizontalAlignment,
|
||||
verticalAlignment: init.verticalAlignment,
|
||||
@@ -377,6 +385,16 @@ export function copyComponent(
|
||||
id: x.id.replace(`${item.id}_`, `${id}_`)
|
||||
})) ?? []
|
||||
}
|
||||
} else if (item.data.type === 'menucomponent') {
|
||||
return {
|
||||
...item.data,
|
||||
id,
|
||||
menuItems:
|
||||
item.data.menuItems.map((x) => ({
|
||||
...x,
|
||||
id: x.id.replace(`${item.id}_`, `${id}_`)
|
||||
})) ?? []
|
||||
}
|
||||
} else {
|
||||
return { ...item.data, id }
|
||||
}
|
||||
@@ -409,6 +427,11 @@ export function getAllSubgridsAndComponentIds(
|
||||
if (component.type === 'tablecomponent') {
|
||||
components.push(...component.actionButtons?.map((x) => x.id))
|
||||
}
|
||||
|
||||
if (component.type === 'menucomponent') {
|
||||
components.push(...component.menuItems?.map((x) => x.id))
|
||||
}
|
||||
|
||||
if (app.subgrids && component.numberOfSubgrids) {
|
||||
for (let i = 0; i < component.numberOfSubgrids; i++) {
|
||||
const key = `${component.id}-${i}`
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
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'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -685,6 +686,16 @@
|
||||
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}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,8 @@ import {
|
||||
PanelTopInactive,
|
||||
ListIcon,
|
||||
Heading1,
|
||||
FileBarChart
|
||||
FileBarChart,
|
||||
Menu
|
||||
} from 'lucide-svelte'
|
||||
import type {
|
||||
Aligned,
|
||||
@@ -164,6 +165,9 @@ export type SelectStepComponent = BaseComponent<'selectstepcomponent'>
|
||||
|
||||
export type CarouselListComponent = BaseComponent<'carousellistcomponent'>
|
||||
export type StatisticCardComponent = BaseComponent<'statcomponent'>
|
||||
export type MenuComponent = BaseComponent<'menucomponent'> & {
|
||||
menuItems: (BaseAppComponent & ButtonComponent & GridItem)[]
|
||||
}
|
||||
|
||||
export type TypedComponent =
|
||||
| DisplayComponent
|
||||
@@ -226,6 +230,7 @@ export type TypedComponent =
|
||||
| PlotlyComponentV2
|
||||
| ChartJsComponentV2
|
||||
| StatisticCardComponent
|
||||
| MenuComponent
|
||||
|
||||
export type AppComponent = BaseAppComponent & TypedComponent
|
||||
|
||||
@@ -275,6 +280,7 @@ export interface InitialAppComponent extends Partial<Aligned> {
|
||||
numberOfSubgrids?: number
|
||||
recomputeIds?: boolean
|
||||
actionButtons?: boolean
|
||||
menuItems?: boolean
|
||||
tabs?: string[]
|
||||
panes?: number[]
|
||||
conditions?: AppInputSpec<'boolean', boolean>[]
|
||||
@@ -2790,6 +2796,56 @@ This is a paragraph.
|
||||
} as const
|
||||
}
|
||||
}
|
||||
},
|
||||
menucomponent: {
|
||||
name: 'Menu',
|
||||
icon: Menu,
|
||||
documentationLink: `${documentationBaseUrl}/menu`,
|
||||
dims: '1:1-1:2' as AppComponentDimensions,
|
||||
customCss: {
|
||||
button: { style: '', class: '' }
|
||||
},
|
||||
initialData: {
|
||||
...defaultAlignement,
|
||||
componentInput: undefined,
|
||||
configuration: {
|
||||
label: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: '' as string
|
||||
},
|
||||
color: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
selectOptions: selectOptions.buttonColorOptions,
|
||||
value: 'light'
|
||||
},
|
||||
size: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
|
||||
selectOptions: selectOptions.buttonSizeOptions,
|
||||
value: 'xs'
|
||||
},
|
||||
fillContainer: {
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
|
||||
value: false
|
||||
},
|
||||
beforeIcon: {
|
||||
type: 'static',
|
||||
value: 'Menu',
|
||||
fieldType: 'icon-select'
|
||||
},
|
||||
afterIcon: {
|
||||
type: 'static',
|
||||
value: undefined,
|
||||
fieldType: 'icon-select'
|
||||
}
|
||||
},
|
||||
menuItems: true
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ const display: ComponentSet = {
|
||||
'jobidlogcomponent',
|
||||
'jobidflowstatuscomponent',
|
||||
'customcomponent',
|
||||
'statcomponent'
|
||||
'statcomponent',
|
||||
'menucomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
|
||||
@@ -745,5 +745,8 @@ export const quickStyleProperties: Record<
|
||||
selectedTab: [typographyGrouping, sizeGrouping]
|
||||
},
|
||||
selectstepcomponent: {},
|
||||
statcomponent: {}
|
||||
statcomponent: {},
|
||||
menucomponent: {
|
||||
button: buttonDefaultProps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import OutputHeader from './components/OutputHeader.svelte'
|
||||
import TableActionsOutput from './components/TableActionsOutput.svelte'
|
||||
import { BG_PREFIX } from '../../utils'
|
||||
// @ts-ignore
|
||||
import MenuItemsOutput from './components/MenuItemsOutput.svelte'
|
||||
|
||||
export let gridItem: GridItem
|
||||
export let first: boolean = false
|
||||
@@ -43,4 +45,5 @@
|
||||
/>
|
||||
<SubGridOutput {name} {expanded} {subGrids} parentId={gridItem.id} />
|
||||
<TableActionsOutput {gridItem} />
|
||||
<MenuItemsOutput {gridItem} />
|
||||
</OutputHeader>
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { GridItem } from '$lib/components/apps/types'
|
||||
import TableActionOutput from './TableActionOutput.svelte'
|
||||
|
||||
export let gridItem: GridItem
|
||||
</script>
|
||||
|
||||
<div class="my-1">
|
||||
{#if gridItem.data.type === 'menucomponent' && gridItem.data.menuItems.length > 0}
|
||||
<div class="ml-2 border-l">
|
||||
{#each gridItem.data.menuItems as action, index}
|
||||
<TableActionOutput id={action.id} first={index === 0} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
+19
-2
@@ -90,6 +90,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (item?.data.type === 'menucomponent') {
|
||||
for (let c of item.data.menuItems) {
|
||||
let old = c.id
|
||||
c.id = c.id.replace(id + '_', newId + '_')
|
||||
propagateRename(old, c.id)
|
||||
}
|
||||
}
|
||||
|
||||
$app = $app
|
||||
$selectedComponent = [newId]
|
||||
|
||||
@@ -102,6 +110,13 @@
|
||||
renameComponent(from, to, c)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'menucomponent') {
|
||||
for (let c of data.menuItems) {
|
||||
renameComponent(from, to, c)
|
||||
}
|
||||
}
|
||||
|
||||
let componentInput = data.componentInput
|
||||
if (componentInput?.type == 'connected') {
|
||||
if (componentInput.connection?.componentId === from) {
|
||||
@@ -234,8 +249,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="border-b {open ? 'h-full' : 'h-0 overflow-hidden invisible'} {$connectingInput.hoveredComponent ===
|
||||
id && !$selectedComponent?.includes(id)
|
||||
class="border-b {open
|
||||
? 'h-full'
|
||||
: 'h-0 overflow-hidden invisible'} {$connectingInput.hoveredComponent === id &&
|
||||
!$selectedComponent?.includes(id)
|
||||
? ' bg-orange-100/40'
|
||||
: ''}"
|
||||
>
|
||||
|
||||
+13
@@ -31,3 +31,16 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if gridItem?.data?.type === 'menucomponent'}
|
||||
{#each gridItem.data.menuItems as actionButton, index (index)}
|
||||
{#if actionButton?.id === $selectedComponentInEditor || actionButton?.id + '_transformer' === $selectedComponentInEditor}
|
||||
<InlineScriptEditorPanel
|
||||
componentType={actionButton.type}
|
||||
id={actionButton.id}
|
||||
transformer={$selectedComponentInEditor?.endsWith('_transformer')}
|
||||
bind:componentInput={actionButton.componentInput}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -65,6 +65,21 @@ function processGridItemRunnable(gridItem: GridItem, list: AppScriptsList): AppS
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.type === 'menucomponent') {
|
||||
component.menuItems?.forEach((menuItem) => {
|
||||
if (menuItem.componentInput?.type !== 'runnable') {
|
||||
return
|
||||
}
|
||||
processRunnable(
|
||||
menuItem.componentInput.runnable,
|
||||
menuItem.componentInput.transformer,
|
||||
menuItem.id,
|
||||
list
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (componentInput?.type === 'runnable') {
|
||||
processRunnable(componentInput.runnable, componentInput.transformer, gridItem.id, list)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import ComponentControl from './ComponentControl.svelte'
|
||||
import GridAgGridLicenseKey from './GridAgGridLicenseKey.svelte'
|
||||
import ComponentPanelDataSource from './ComponentPanelDataSource.svelte'
|
||||
import MenuItems from './MenuItems.svelte'
|
||||
|
||||
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
||||
undefined
|
||||
@@ -342,6 +343,8 @@
|
||||
/>
|
||||
{:else if componentSettings.item.data.type === 'tablecomponent' && Array.isArray(componentSettings.item.data.actionButtons)}
|
||||
<TableActions id={component.id} bind:components={componentSettings.item.data.actionButtons} />
|
||||
{:else if componentSettings.item.data.type === 'menucomponent' && Array.isArray(componentSettings.item.data.menuItems)}
|
||||
<MenuItems id={component.id} bind:components={componentSettings.item.data.menuItems} />
|
||||
{/if}
|
||||
|
||||
{#if Object.values(initialConfiguration).length > 0}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { getNextId } from '$lib/components/flows/idUtils'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext, BaseAppComponent } from '../../types'
|
||||
import { appComponentFromType } from '../appUtils'
|
||||
import type { ButtonComponent } from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import { Plus, Trash } from 'lucide-svelte'
|
||||
|
||||
export let components: (BaseAppComponent & ButtonComponent)[]
|
||||
export let id: string
|
||||
|
||||
const { selectedComponent, app, errorByComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function addComponent() {
|
||||
const actionId = getNextId(components.map((x) => x.id.split('_')[1]))
|
||||
|
||||
const newComponent = {
|
||||
...appComponentFromType('buttoncomponent')(`${id}_${actionId}`),
|
||||
recomputeIds: []
|
||||
}
|
||||
components = [...components, newComponent]
|
||||
$app = $app
|
||||
}
|
||||
|
||||
function deleteComponent(cid: string) {
|
||||
components = components.filter((x) => x.id !== cid)
|
||||
|
||||
delete $errorByComponent[cid]
|
||||
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection title={`Menu items`}>
|
||||
{#if components.length == 0}
|
||||
<span class="text-xs text-tertiary">No action buttons</span>
|
||||
{/if}
|
||||
{#each components as component}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold gap-1 truncate py-1.5 px-2 cursor-pointer transition-all justify-between flex items-center border border-gray-3 rounded-md',
|
||||
'bg-surface border-gray-300 hover:bg-gray-100 focus:bg-gray-100 text-secondary',
|
||||
$selectedComponent?.includes(component.id) ? 'outline outline-blue-500 bg-red-400' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
$selectedComponent = [component.id]
|
||||
}}
|
||||
on:keypress
|
||||
>
|
||||
<Badge color="dark-indigo">
|
||||
{component.id}
|
||||
</Badge>
|
||||
|
||||
<div>
|
||||
{#if component.type == 'buttoncomponent'}
|
||||
Button
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
on:click={() => deleteComponent(component.id)}
|
||||
startIcon={{ icon: Trash }}
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="w-full flex gap-2">
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-tertiary"
|
||||
wrapperClasses="w-full"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => addComponent()}
|
||||
title="Add Button"
|
||||
startIcon={{ icon: Plus }}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
@@ -276,6 +276,16 @@ export function getAllScriptNames(app: App): string[] {
|
||||
})
|
||||
}
|
||||
|
||||
if (gridItem.data.type === 'menucomponent') {
|
||||
gridItem.data.menuItems.forEach((menuItem) => {
|
||||
if (menuItem.componentInput?.type === 'runnable') {
|
||||
if (menuItem.componentInput.runnable?.type === 'runnableByName') {
|
||||
acc.push(menuItem.componentInput.runnable.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user