diff --git a/frontend/src/lib/components/DropdownV2.svelte b/frontend/src/lib/components/DropdownV2.svelte index 599a677a20..ccfb33ceff 100644 --- a/frontend/src/lib/components/DropdownV2.svelte +++ b/frontend/src/lib/components/DropdownV2.svelte @@ -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 @@ } - +
{#if $$slots.buttonReplacement} diff --git a/frontend/src/lib/components/apps/components/buttons/AppButton.svelte b/frontend/src/lib/components/apps/components/buttons/AppButton.svelte index 9138c0a384..d7255415a0 100644 --- a/frontend/src/lib/components/apps/components/buttons/AppButton.svelte +++ b/frontend/src/lib/components/apps/components/buttons/AppButton.svelte @@ -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} +
+ +
+ {#if menuItems.length > 0} + {#each menuItems as actionButton, actionIndex (actionButton?.id)} + {#if actionButton.type == 'buttoncomponent'} + + {/if} + {/each} + {/if} +
+
+ +{/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 9dd76e2177..68a2848230 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -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) => { diff --git a/frontend/src/lib/components/apps/editor/SettingsPanel.svelte b/frontend/src/lib/components/apps/editor/SettingsPanel.svelte index 4ee72d0a18..b1cfa1a438 100644 --- a/frontend/src/lib/components/apps/editor/SettingsPanel.svelte +++ b/frontend/src/lib/components/apps/editor/SettingsPanel.svelte @@ -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() @@ -65,6 +81,26 @@ }} /> {/key} +{:else if menuItemsSettings} + {#key menuItemsSettings?.item?.id} + { + 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} diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index d071a278f8..7a4fd3b097 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -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( 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}` diff --git a/frontend/src/lib/components/apps/editor/component/Component.svelte b/frontend/src/lib/components/apps/editor/component/Component.svelte index db5f1904df..ce4b3fe95b 100644 --- a/frontend/src/lib/components/apps/editor/component/Component.svelte +++ b/frontend/src/lib/components/apps/editor/component/Component.svelte @@ -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'} + {/if} diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index 77f1263fe2..d72a8c625c 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -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 { 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 diff --git a/frontend/src/lib/components/apps/editor/component/sets.ts b/frontend/src/lib/components/apps/editor/component/sets.ts index 6af2bd520c..919170b0d3 100644 --- a/frontend/src/lib/components/apps/editor/component/sets.ts +++ b/frontend/src/lib/components/apps/editor/component/sets.ts @@ -65,7 +65,8 @@ const display: ComponentSet = { 'jobidlogcomponent', 'jobidflowstatuscomponent', 'customcomponent', - 'statcomponent' + 'statcomponent', + 'menucomponent' ] } as const diff --git a/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts b/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts index 9c4649e091..16364cd87d 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts +++ b/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts @@ -745,5 +745,8 @@ export const quickStyleProperties: Record< selectedTab: [typographyGrouping, sizeGrouping] }, selectstepcomponent: {}, - statcomponent: {} + statcomponent: {}, + menucomponent: { + button: buttonDefaultProps + } } diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte index ace45dc6ef..91e5424340 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte @@ -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 @@ /> + diff --git a/frontend/src/lib/components/apps/editor/contextPanel/components/MenuItemsOutput.svelte b/frontend/src/lib/components/apps/editor/contextPanel/components/MenuItemsOutput.svelte new file mode 100644 index 0000000000..58a0fd0461 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/contextPanel/components/MenuItemsOutput.svelte @@ -0,0 +1,16 @@ + + +
+ {#if gridItem.data.type === 'menucomponent' && gridItem.data.menuItems.length > 0} +
+ {#each gridItem.data.menuItems as action, index} + + {/each} +
+ {/if} +
diff --git a/frontend/src/lib/components/apps/editor/contextPanel/components/OutputHeader.svelte b/frontend/src/lib/components/apps/editor/contextPanel/components/OutputHeader.svelte index 5cda0c55a6..a88e03d81c 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/components/OutputHeader.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/components/OutputHeader.svelte @@ -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 @@
diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelWithTable.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelWithTable.svelte index c2d7114499..0791c619a0 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelWithTable.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelWithTable.svelte @@ -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} + + {/if} + {/each} +{/if} diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts index 54cf0b8554..cb52429441 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts @@ -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) } diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index abe9855297..c7a5c11c94 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -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)} + {:else if componentSettings.item.data.type === 'menucomponent' && Array.isArray(componentSettings.item.data.menuItems)} + {/if} {#if Object.values(initialConfiguration).length > 0} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/MenuItems.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/MenuItems.svelte new file mode 100644 index 0000000000..584cc5d2aa --- /dev/null +++ b/frontend/src/lib/components/apps/editor/settingsPanel/MenuItems.svelte @@ -0,0 +1,91 @@ + + + + {#if components.length == 0} + No action buttons + {/if} + {#each components as component} + +
{ + $selectedComponent = [component.id] + }} + on:keypress + > + + {component.id} + + +
+ {#if component.type == 'buttoncomponent'} + Button + {/if} +
+
+
+
+ {/each} +
+ +
+
diff --git a/frontend/src/lib/components/apps/utils.ts b/frontend/src/lib/components/apps/utils.ts index b7eee7c3f7..71933f9817 100644 --- a/frontend/src/lib/components/apps/utils.ts +++ b/frontend/src/lib/components/apps/utils.ts @@ -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[])