From a2b707f2cb0dacfd9adb6af8b6eec5a2abca2f48 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Thu, 4 Jul 2024 19:50:00 +0200 Subject: [PATCH] feat(frontend): app navbar (#3992) * feat(frontend): app navbar * feat(frontend): app navbar * fix(frontend): add icons + highlight + image * fix(frontend): improve style * fix(frontend): use a proper select component * fix(frontend): fix select * feat(frontend): Navbar component * feat(frontend): add path indicator + fix disabled navbar item * feat(frontend): add path indicator + fix disabled navbar item * feat(frontend): wip * feat(frontend): wip * feat(frontend): add local item * feat(frontend): introduced currentPath * feat(frontend): fix popups * feat(frontend): initial current path * feat(frontend): initial current path * feat(frontend): fix interactions * feat(frontend): improve code * feat(frontend): improve code * feat(frontend): wip * feat(frontend): open windmill apps in the same tab * feat(frontend): added support for the goto * feat(frontend): avoid loaded the app multiple times * feat(frontend): add support for oneOf * feat(frontend): done * feat(frontend): add missing tooltips * feat(frontend): improve alert message * feat(frontend): fix typo * feat(frontend): add missing reference to the ctx.query * fix(frontend): improve code * feat(frontend): navbar done * feat(frontend): fix navbar wizard wording * feat(frontend): only select the current app if the selected value is not defined + correctly clear --- .../apps/components/display/AppNavbar.svelte | 91 ++++++++ .../components/display/AppNavbarItem.svelte | 165 ++++++++++++++ .../display/ResolveNavbarItemPath.svelte | 28 +++ .../components/apps/editor/AppEditor.svelte | 6 +- .../components/apps/editor/AppPreview.svelte | 10 +- .../apps/editor/component/Component.svelte | 10 +- .../apps/editor/component/components.ts | 78 ++++++- .../components/apps/editor/component/sets.ts | 3 +- .../componentsPanel/quickStyleProperties.ts | 3 + .../contextPanel/ComponentOutput.svelte | 4 +- .../settingsPanel/ComponentPanel.svelte | 4 + .../editor/settingsPanel/GridNavbar.svelte | 211 ++++++++++++++++++ .../inputEditor/IconSelectInput.svelte | 25 ++- .../inputEditor/StaticInputEditor.svelte | 5 +- frontend/src/lib/components/apps/inputType.ts | 2 + frontend/src/lib/components/apps/types.ts | 2 + .../lib/components/common/popup/Popup.svelte | 7 +- .../lib/components/wizards/AppPicker.svelte | 79 +++++++ .../components/wizards/NavbarWizard.svelte | 136 +++++++++++ .../src/routes/(root)/(logged)/+page.svelte | 3 + .../(logged)/apps/get/[...path]/+page.svelte | 9 +- .../[workspace]/[...secret]/+page.svelte | 3 + 22 files changed, 860 insertions(+), 24 deletions(-) create mode 100644 frontend/src/lib/components/apps/components/display/AppNavbar.svelte create mode 100644 frontend/src/lib/components/apps/components/display/AppNavbarItem.svelte create mode 100644 frontend/src/lib/components/apps/components/display/ResolveNavbarItemPath.svelte create mode 100644 frontend/src/lib/components/apps/editor/settingsPanel/GridNavbar.svelte create mode 100644 frontend/src/lib/components/wizards/AppPicker.svelte create mode 100644 frontend/src/lib/components/wizards/NavbarWizard.svelte diff --git a/frontend/src/lib/components/apps/components/display/AppNavbar.svelte b/frontend/src/lib/components/apps/components/display/AppNavbar.svelte new file mode 100644 index 0000000000..832ccfbc85 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/AppNavbar.svelte @@ -0,0 +1,91 @@ + + +{#each Object.keys(components['navbarcomponent'].initialData.configuration) as key (key)} + +{/each} + +{#each Object.keys(css ?? {}) as key (key)} + +{/each} + + +{#if render} +
+ {#if resolvedConfig.logo?.selected === 'yes'} + {resolvedConfig.logo?.configuration?.yes?.altText} + {/if} +
+ {resolvedConfig?.title ?? 'No Title'} +
+
+ {#each navbarItems ?? [] as navbarItem, index (index)} + + {navbarItem.caption} + + + {/each} +
+
+{/if} diff --git a/frontend/src/lib/components/apps/components/display/AppNavbarItem.svelte b/frontend/src/lib/components/apps/components/display/AppNavbarItem.svelte new file mode 100644 index 0000000000..dea4ea1651 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/AppNavbarItem.svelte @@ -0,0 +1,165 @@ + + + + + + + + + + + + +{#if !resolvedHidden} +
+ +
+{/if} diff --git a/frontend/src/lib/components/apps/components/display/ResolveNavbarItemPath.svelte b/frontend/src/lib/components/apps/components/display/ResolveNavbarItemPath.svelte new file mode 100644 index 0000000000..7897656ef4 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/ResolveNavbarItemPath.svelte @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/lib/components/apps/editor/AppEditor.svelte b/frontend/src/lib/components/apps/editor/AppEditor.svelte index 7271fa7c71..5ba382f076 100644 --- a/frontend/src/lib/components/apps/editor/AppEditor.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditor.svelte @@ -55,6 +55,7 @@ import StylePanel from './settingsPanel/StylePanel.svelte' import type DiffDrawer from '$lib/components/DiffDrawer.svelte' import RunnableJobPanel from './RunnableJobPanel.svelte' + import { goto, replaceState } from '$app/navigation' export let app: App export let path: string @@ -155,7 +156,8 @@ darkMode, cssEditorOpen, previewTheme, - debuggingComponents: writable({}) + debuggingComponents: writable({}), + replaceStateFn: (path) => replaceState(path, $page.state) }) let scale = writable(100) @@ -551,6 +553,8 @@ isEditor {context} noBackend={false} + replaceStateFn={(path) => replaceState(path, $page.state)} + gotoFn={(path, opt) => goto(path, opt)} /> diff --git a/frontend/src/lib/components/apps/editor/AppPreview.svelte b/frontend/src/lib/components/apps/editor/AppPreview.svelte index c66be65ee5..8200acc3aa 100644 --- a/frontend/src/lib/components/apps/editor/AppPreview.svelte +++ b/frontend/src/lib/components/apps/editor/AppPreview.svelte @@ -37,6 +37,12 @@ export let noBackend: boolean = false export let isLocked = false export let hideRefreshBar = false + export let replaceStateFn: (path: string) => void = (path: string) => + window.history.replaceState(null, '', path) + export let gotoFn: (path: string, opt?: Record | undefined) => void = ( + path: string, + opt?: Record + ) => window.history.pushState(null, '', path) migrateApp(app) @@ -103,7 +109,9 @@ darkMode, cssEditorOpen: writable(false), previewTheme: writable(undefined), - debuggingComponents: writable({}) + debuggingComponents: writable({}), + replaceStateFn, + gotoFn }) let previousSelectedIds: string[] | undefined = undefined diff --git a/frontend/src/lib/components/apps/editor/component/Component.svelte b/frontend/src/lib/components/apps/editor/component/Component.svelte index 8f64fc9ede..ee11896b2d 100644 --- a/frontend/src/lib/components/apps/editor/component/Component.svelte +++ b/frontend/src/lib/components/apps/editor/component/Component.svelte @@ -75,6 +75,7 @@ import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte' import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte' import AppNumberInput from '../../components/inputs/AppNumberInput.svelte' + import AppNavbar from '../../components/display/AppNavbar.svelte' export let component: AppComponent export let selected: boolean @@ -823,6 +824,14 @@ verticalAlignment={component.verticalAlignment} {render} /> + {:else if component.type === 'navbarcomponent'} + {/if} @@ -843,4 +852,3 @@ class="absolute inset-0 center-center flex-col bg- border animate-skeleton" /> {/if} - diff --git a/frontend/src/lib/components/apps/editor/component/components.ts b/frontend/src/lib/components/apps/editor/component/components.ts index e84ad83847..edde1b0932 100644 --- a/frontend/src/lib/components/apps/editor/component/components.ts +++ b/frontend/src/lib/components/apps/editor/component/components.ts @@ -48,13 +48,15 @@ import { UploadCloud, AlertTriangle, Clock, - CalendarClock + CalendarClock, + AppWindow } from 'lucide-svelte' import type { Aligned, BaseAppComponent, ComponentCustomCSS, GridItem, + OneOfConfiguration, RichConfiguration, RichConfigurations, StaticRichConfigurations @@ -261,6 +263,19 @@ export type DecisionTreeComponent = BaseComponent<'decisiontreecomponent'> & { export type AlertComponent = BaseComponent<'alertcomponent'> +export type NavbarItem = { + path: OneOfConfiguration + label: RichConfiguration + caption?: string + disabled: RichConfiguration + hidden: RichConfiguration + icon?: string +} + +export type NavBarComponent = BaseComponent<'navbarcomponent'> & { + navbarItems: NavbarItem[] +} + export type TypedComponent = | DBExplorerComponent | DisplayComponent @@ -335,6 +350,7 @@ export type TypedComponent = | AggridInfiniteComponent | AggridInfiniteComponentEe | MultiSelectComponentV2 + | NavBarComponent export type AppComponent = BaseAppComponent & TypedComponent @@ -3835,6 +3851,66 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm' } } } + }, + navbarcomponent: { + name: 'Navbar', + icon: AppWindow, + documentationLink: `${documentationBaseUrl}/navbar`, + dims: '12:1-12:2' as AppComponentDimensions, + customCss: { + container: { class: '', style: '' }, + image: { class: '', style: '' } + }, + initialData: { + ...defaultAlignement, + componentInput: undefined, + configuration: { + title: { + type: 'static', + fieldType: 'text', + value: 'Title' + }, + borderColor: { + type: 'static', + value: '#555', + fieldType: 'color' + }, + logo: { + type: 'oneOf', + selected: 'no', + labels: { + yes: 'Use logo', + no: 'No logo' + }, + configuration: { + yes: { + source: { + type: 'static', + value: '/logo.svg', + fieldType: 'text', + fileUpload: { + accept: 'image/*', + convertTo: 'base64' + } + }, + sourceKind: { + fieldType: 'select', + type: 'static', + selectOptions: selectOptions.imageSourceKind, + value: 'url' as (typeof selectOptions.imageSourceKind)[number] + }, + altText: { + type: 'static', + value: '', + fieldType: 'text', + tooltip: "This text will appear if the image can't be loaded for any reason" + } + }, + no: {} + } + } as const + } + } } } 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 81dd4c0527..88a6966d19 100644 --- a/frontend/src/lib/components/apps/editor/component/sets.ts +++ b/frontend/src/lib/components/apps/editor/component/sets.ts @@ -19,7 +19,8 @@ const layout: ComponentSet = { 'modalcomponent', 'steppercomponent', 'carousellistcomponent', - 'decisiontreecomponent' + 'decisiontreecomponent', + 'navbarcomponent' ] } 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 071a87073c..cf01227014 100644 --- a/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts +++ b/frontend/src/lib/components/apps/editor/componentsPanel/quickStyleProperties.ts @@ -786,5 +786,8 @@ export const quickStyleProperties: Record< icon: containerDefaultProps, title: containerDefaultProps, description: containerDefaultProps + }, + navbarcomponent: { + container: containerDefaultProps } } diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte index 19723146d3..1ceb7c1162 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutput.svelte @@ -20,7 +20,7 @@ const name = getComponentNameById(gridItem.id) $: nameOverrides = - gridItem.data.type === 'decisiontreecomponent' + gridItem?.data?.type === 'decisiontreecomponent' ? gridItem.data.nodes.map((n, i) => `${n.label} (Tab index ${i})`) : undefined @@ -36,7 +36,7 @@ } } - $: subGrids = Array.from({ length: gridItem.data.numberOfSubgrids ?? 0 }).map( + $: subGrids = Array.from({ length: gridItem.data?.numberOfSubgrids ?? 0 }).map( (_, i) => `${gridItem.id}-${i}` ) diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index 07f699119e..f7d18b1f93 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -41,6 +41,7 @@ import Toggle from '$lib/components/Toggle.svelte' import ContextVariables from './ContextVariables.svelte' import EventHandlers from './EventHandlers.svelte' + import GridNavbar from './GridNavbar.svelte' export let componentSettings: { item: GridItem; parent: string | undefined } | undefined = undefined @@ -329,6 +330,9 @@ + {#if componentSettings.item.data.type === 'navbarcomponent'} + + {/if} {#if componentSettings.item.data.type === 'tabscomponent'} + import Button from '$lib/components/common/button/Button.svelte' + import CloseButton from '$lib/components/common/CloseButton.svelte' + import PanelSection from './common/PanelSection.svelte' + import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action' + import { generateRandomString } from '$lib/utils' + import { GripVertical, Plus, Settings } from 'lucide-svelte' + import type { NavbarItem } from '../component' + import NavbarWizard from '$lib/components/wizards/NavbarWizard.svelte' + + import Badge from '$lib/components/common/badge/Badge.svelte' + import ResolveConfig from '../../components/helpers/ResolveConfig.svelte' + import { getContext } from 'svelte' + import type { AppViewerContext } from '../../types' + import Tooltip from '$lib/components/Tooltip.svelte' + import type { StaticAppInput } from '../../inputType' + import ResolveNavbarItemPath from '../../components/display/ResolveNavbarItemPath.svelte' + + export let navbarItems: NavbarItem[] = [] + export let id: string + + const { appPath } = getContext('AppViewerContext') + + let items = navbarItems.map((tab, index) => { + return { value: tab, id: generateRandomString(), originalIndex: index } + }) + + $: navbarItems = items.map((item) => item.value) + + function addPath() { + const emptyAppPath: NavbarItem = { + disabled: { + type: 'static', + value: false, + fieldType: 'boolean' + }, + label: { + type: 'static', + value: undefined, + fieldType: 'text' + }, + + path: { + type: 'oneOf', + selected: 'app', + labels: { + href: 'Navigate to an external URL', + app: 'Navigate to an app' + }, + configuration: { + href: { + href: { + type: 'static', + value: undefined, + fieldType: 'text', + tooltip: + "The URL to navigate to when the item is clicked. Will be opened in a new tab. If you want to navigate to an other app, use the 'App' option." + } + }, + app: { + path: { + type: 'static', + value: '', + fieldType: 'app-path', + allowTypeChange: false, + tooltip: + 'The app to navigate to when the item is clicked. Will be opened in the same tab. If you want to navigate to an external URL, use the "Href" option.' + } as StaticAppInput, + queryParamsOrHash: { + type: 'static', + value: undefined, + fieldType: 'text', + tooltip: + 'Query parameters or hash to append to the URL. For example, `?key=value` or `#hash`.', + placeholder: '?key=value#hash' + } + } + } + } as const, + hidden: { + type: 'static', + value: false, + fieldType: 'boolean' + } + } + + items = [ + ...items, + { + value: emptyAppPath, + id: generateRandomString(), + originalIndex: items.length + } + ] + } + + function handleConsider(e: CustomEvent): void { + const { items: newItems } = e.detail + items = newItems + } + + function handleFinalize(e: CustomEvent) { + const { items: newItems } = e.detail + + items = newItems + } + + let resolvedPaths: string[] = [] + let resolvedLabels: string[] = [] + + + 0 ? `(${navbarItems.length})` : ''}`} +> + {#if !navbarItems || navbarItems.length == 0} + No items + {/if} +
+
+ {#each items as item, index (item.id)} + {#key item.id} +
+ + +
+
+ {resolvedLabels[item.originalIndex] ?? 'No label'} +
+ +
+ { + items = items.filter((_, i) => i !== index) + }} + /> +
+ + + + + + +
+ + +
+ +
+
+
+ + + + {#if resolvedPaths[item.originalIndex]} +
+ Path: {resolvedPaths[item.originalIndex]} + {#if appPath && resolvedPaths[item.originalIndex]?.includes(appPath)} + Current app + + + Clicking on those items will keep you in the current tab and change the output + of the component. + + + {/if} +
+ {:else} +
No app path or url selected
+ {/if} +
+ {/key} + {/each} +
+
+
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte index 7614d3e679..20704c9bd8 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte @@ -1,9 +1,10 @@ - + } + export let shouldUsePortal: boolean = true + + +
(componentInput.value = detail)} + {value} + on:change={({ detail }) => (value = detail)} on:focus={getData} class="!pr-6" /> @@ -84,7 +85,7 @@ }} class="w-full center-center flex-col font-normal p-1 hover:bg-gray-100 focus:bg-gray-100 rounded duration-200 dark:hover:bg-frost-900 dark:focus:bg-frost-900 - {label === componentInput.value ? 'text-blue-600 bg-blue-50 pointer-events-none' : ''}" + {label === value ? 'text-blue-600 bg-blue-50 pointer-events-none' : ''}" > | undefined export let fieldType: InputType | undefined = undefined @@ -69,7 +70,7 @@ {/if} {:else if fieldType === 'icon-select'} - + {:else if fieldType === 'tab-select'} {:else if fieldType === 'resource' && subFieldType && ['mysql', 'postgres', 'ms_sql_server', 'snowflake', 'bigquery'].includes(subFieldType)} @@ -313,6 +314,8 @@
+ {:else if fieldType === 'app-path'} + {:else}