diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 98593f71bd..c38cf1d9b1 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -4,6 +4,7 @@ import "@spectrum-css/menu/dist/index-vars.css" import { createEventDispatcher } from "svelte" import clickOutside from "../../Actions/click_outside" + import Popover from "../../Popover/Popover.svelte" export let value = null export let id = null @@ -15,8 +16,10 @@ export let getOptionValue = option => option const dispatch = createEventDispatcher() + let open = false let focus = false + let anchor const selectOption = value => { dispatch("change", value) @@ -35,11 +38,11 @@ } -
(open = true)} + on:click={() => (open = !open)} > - {#if open} -
{ - open = false - }} - > -
    - {#if options && Array.isArray(options)} - {#each options as option} -
  • onPick(getOptionValue(option))} - > - {getOptionLabel(option)} - -
  • - {/each} - {/if} -
-
- {/if}
+ + (open = false)} + useAnchorWidth +> +
(open = false)}> +
    + {#if options && Array.isArray(options)} + {#each options as option} +
  • onPick(getOptionValue(option))} + > + {getOptionLabel(option)} + +
  • + {/each} + {/if} +
+
+
+ diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 8b7a641563..f6bffbbf10 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -45,7 +45,6 @@ const dispatch = createEventDispatcher() let button - let popover let component $: sortedOptions = getSortedOptions(options, getOptionLabel, sort) @@ -146,11 +145,11 @@ + (open = false)} useAnchorWidth={!autoWidth} @@ -266,16 +265,6 @@ width: 100%; box-shadow: none; } - - .subtitle-text { - font-size: 12px; - line-height: 15px; - font-weight: 500; - color: var(--spectrum-global-color-gray-600); - display: block; - margin-top: var(--spacing-s); - } - .spectrum-Picker-label.auto-width { margin-right: var(--spacing-xs); } @@ -356,11 +345,9 @@ .option-extra.icon.field-icon { display: flex; } - .option-tag { margin: 0 var(--spacing-m) 0 var(--spacing-m); } - .option-tag :global(.spectrum-Tags-item > .spectrum-Icon) { margin-top: 2px; } @@ -374,4 +361,13 @@ .loading--withAutocomplete { top: calc(34px + var(--spacing-m)); } + + .subtitle-text { + font-size: 12px; + line-height: 15px; + font-weight: 500; + color: var(--spectrum-global-color-gray-600); + display: block; + margin-top: var(--spacing-s); + } diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 1391348f43..c51af48300 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -99,10 +99,10 @@ on:keydown={handleEscape} class="spectrum-Popover is-open" class:customZindex - class:hide-popover={open && !showPopover} + class:hidden={!showPopover} role="presentation" style="height: {customHeight}; --customZindex: {customZindex};" - transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }} + transition:fly|local={{ y: -20, duration: animate ? 260 : 0 }} on:mouseenter on:mouseleave > @@ -112,16 +112,17 @@ {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksDrawer.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksDrawer.svelte deleted file mode 100644 index 3779568d93..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksDrawer.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - - - -
- - {#if links?.length} - - {/if} -
- -
-
-
-
- - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksEditor.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksEditor.svelte deleted file mode 100644 index c664f50f10..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/LinksEditor.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Configure the links in your navigation bar. - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItem.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItem.svelte new file mode 100644 index 0000000000..3cea3f7367 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItem.svelte @@ -0,0 +1,55 @@ + + +
+
+ +
{text}
+
+
+ { + e.stopPropagation() + removeNavItem(item.id) + }} + /> +
+
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItemConfiguration.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItemConfiguration.svelte new file mode 100644 index 0000000000..dffa4bf3ff --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/NavItemConfiguration.svelte @@ -0,0 +1,118 @@ + + + + + + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/SubLinksDrawer.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/SubLinksDrawer.svelte new file mode 100644 index 0000000000..b1958e4d3d --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/SubLinksDrawer.svelte @@ -0,0 +1,142 @@ + + + + + +
+ + {#if subLinks?.length} + + {/if} +
+ + Add link + +
+
+
+
+
+ +
+ {buttonText} +
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte index 4db218f60b..a0e0f1d93a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte @@ -1,62 +1,61 @@ -
-
- General -
-
- - Show nav on this screen -
-
+ + + {#if $selectedScreen?.showNavigation} -
-
-
- Customize -
-
- - These settings apply to all screens -
- -
-
- -
- - update("navigation", "Top")} + + +
+ update("navigation", position)} + value={$nav.navigation} + props={{ + options: positionOptions, + }} + /> + {#if $nav.navigation === "Top"} + update("sticky", sticky)} /> - update("navigation", "Left")} - /> - - - {#if $navigationStore.navigation === "Top"} -
- -
- update("sticky", e.detail)} - /> -
- -
- update("title", e.detail)} - updateOnChange={false} + {#if !$nav.hideTitle} + update("title", title)} + {bindings} + props={{ + updateOnChange: false, + }} /> - -
- -
- nav.syncAppNavigation({ textAlign: align })} + value={$nav.textAlign} + props={{ + options: alignmentOptions, + }} /> {/if} -
- -
- update("navBackground", e.detail)} + update("navBackground", color)} + value={$nav.navBackground || DefaultAppTheme.navBackground} + props={{ + spectrumTheme: $themeStore.theme, + }} /> -
- -
- update("navTextColor", e.detail)} + update("navTextColor", color)} + value={$nav.navTextColor || DefaultAppTheme.navTextColor} + props={{ + spectrumTheme: $themeStore.theme, + }} />
-
+ -
-
-
- Logo -
-
-
- -
- update("hideLogo", !e.detail)} + +
+ update("hideLogo", !show)} /> - {#if !$navigationStore.hideLogo} -
- -
- update("logoUrl", e.detail)} - updateOnChange={false} + {#if !$nav.hideLogo} + update("logoUrl", url)} + {bindings} + props={{ + updateOnChange: false, + }} /> -
- -
- update("logoLinkUrl", e.detail)} - options={screenRouteOptions} + update("logoLinkUrl", url)} + {bindings} + props={{ + appendBindingsAsOptions: false, + options: screenRouteOptions, + }} /> -
- -
- update("openLogoLinkInNewTab", !!e.detail)} + update("openLogoLinkInNewTab", show)} /> {/if}
-
+ {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte index b49e38d9cd..68d74218c8 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_components/NewScreen/CreateScreenModal.svelte @@ -79,7 +79,8 @@ // for autoscreens, so it's always safe to do this. await navigationStore.saveLink( screen.routing.route, - capitalise(screen.routing.route.split("/")[1]) + capitalise(screen.routing.route.split("/")[1]), + screenAccessRole ) } diff --git a/packages/builder/src/stores/builder/navigation.js b/packages/builder/src/stores/builder/navigation.js index 0cc4923c56..86e484b0a6 100644 --- a/packages/builder/src/stores/builder/navigation.js +++ b/packages/builder/src/stores/builder/navigation.js @@ -42,7 +42,7 @@ export class NavigationStore extends BudiStore { this.syncAppNavigation(app.navigation) } - async saveLink(url, title) { + async saveLink(url, title, roleId) { const navigation = get(this.store) let links = [...(navigation?.links ?? [])] @@ -54,6 +54,8 @@ export class NavigationStore extends BudiStore { links.push({ text: title, url, + type: "link", + roleId, }) await this.save({ ...navigation, @@ -67,11 +69,20 @@ export class NavigationStore extends BudiStore { if (!links?.length) { return } - - // Filter out the URLs to delete urls = Array.isArray(urls) ? urls : [urls] + + // Filter out top level links pointing to these URLs links = links.filter(link => !urls.includes(link.url)) + // Filter out nested links pointing to these URLs + links.forEach(link => { + if (link.type === "sublinks" && link.subLinks?.length) { + link.subLinks = link.subLinks.filter( + subLink => !urls.includes(subLink.url) + ) + } + }) + await this.save({ ...navigation, links, diff --git a/packages/builder/src/stores/builder/tests/navigation.test.js b/packages/builder/src/stores/builder/tests/navigation.test.js index 19a3361721..365b7f497b 100644 --- a/packages/builder/src/stores/builder/tests/navigation.test.js +++ b/packages/builder/src/stores/builder/tests/navigation.test.js @@ -50,10 +50,18 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, { url: "/test", text: "Test", + type: "sublinks", + subLinks: [ + { + text: "Foo", + url: "/bar", + }, + ], }, ] @@ -66,7 +74,7 @@ describe("Navigation store", () => { .spyOn(ctx.test.navigationStore, "save") .mockImplementation(() => {}) - await ctx.test.navigationStore.saveLink("/test-url", "Testing") + await ctx.test.navigationStore.saveLink("/test-url", "Testing", "BASIC") expect(saveSpy).toBeCalledWith({ ...INITIAL_NAVIGATION_STATE, @@ -75,6 +83,8 @@ describe("Navigation store", () => { { url: "/test-url", text: "Testing", + type: "link", + roleId: "BASIC", }, ], }) @@ -87,6 +97,7 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, ], })) @@ -94,7 +105,7 @@ describe("Navigation store", () => { .spyOn(ctx.test.navigationStore, "save") .mockImplementation(() => {}) - await ctx.test.navigationStore.saveLink("/home", "Home") + await ctx.test.navigationStore.saveLink("/home", "Home", "BASIC") expect(saveSpy).not.toHaveBeenCalled() }) @@ -106,14 +117,23 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, { url: "/test", text: "Test", + type: "link", }, { url: "/last", text: "Last Link", + type: "sublinks", + subLinks: [ + { + text: "Foo", + url: "/home", + }, + ], }, ], })) @@ -130,6 +150,8 @@ describe("Navigation store", () => { { text: "Last Link", url: "/last", + type: "sublinks", + subLinks: [], }, ], }) @@ -140,14 +162,17 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, { url: "/test", text: "Test", + type: "link", }, { url: "/last", text: "Last Link", + type: "link", }, ] @@ -168,10 +193,12 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, { url: "/last", text: "Last Link", + type: "link", }, ], }) @@ -180,10 +207,7 @@ describe("Navigation store", () => { it("Should ignore a request to delete if there are no links", async ctx => { const saveSpy = vi.spyOn(ctx.test.navigationStore, "save") - await ctx.test.navigationStore.deleteLink({ - url: "/some-link", - text: "Some Link", - }) + await ctx.test.navigationStore.deleteLink("/some-link") expect(saveSpy).not.toBeCalled() }) @@ -201,10 +225,18 @@ describe("Navigation store", () => { { url: "/home", text: "Home", + type: "link", }, { url: "/last", text: "Last Link", + type: "sublinks", + subLinks: [ + { + text: "Foo", + url: "/bar", + }, + ], }, ], })) @@ -217,6 +249,7 @@ describe("Navigation store", () => { { url: "/new-link", text: "New Link", + type: "link", }, ], } diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index 0d6d7cd7d5..8508e943ff 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -2,9 +2,8 @@ import { getContext, setContext } from "svelte" import { writable } from "svelte/store" import { Heading, Icon, clickOutside } from "@budibase/bbui" - import { FieldTypes } from "constants" import { Constants } from "@budibase/frontend-core" - import active from "svelte-spa-router/active" + import NavItem from "./NavItem.svelte" const sdk = getContext("sdk") const { @@ -16,6 +15,7 @@ appStore, } = sdk const context = getContext("context") + const navStateStore = writable({}) // Legacy props which must remain unchanged for backwards compatibility export let title @@ -63,7 +63,7 @@ }) setContext("layout", store) - $: validLinks = getValidLinks(links, $roleStore) + $: enrichedNavItems = enrichNavItems(links, $roleStore) $: typeClass = NavigationClasses[navigation] || NavigationClasses.None $: navWidthClass = WidthClasses[navWidth || width] || WidthClasses.Large $: pageWidthClass = WidthClasses[pageWidth || width] || WidthClasses.Large @@ -101,28 +101,57 @@ } } - const getValidLinks = (allLinks, userRoleHierarchy) => { - // Strip links missing required info - let validLinks = (allLinks || []).filter(link => link.text && link.url) - // Filter to only links allowed by the current role - return validLinks.filter(link => { - const role = link.roleId || Constants.Roles.BASIC - return userRoleHierarchy?.find(roleId => roleId === role) - }) + const enrichNavItem = navItem => { + const internalLink = isInternal(navItem.url) + return { + ...navItem, + internalLink, + url: internalLink ? navItem.url : ensureExternal(navItem.url), + } + } + + const enrichNavItems = (navItems, userRoleHierarchy) => { + if (!navItems?.length) { + return [] + } + return navItems + .filter(navItem => { + // Strip nav items without text + if (!navItem.text) { + return false + } + + // Strip out links without URLs + if (navItem.type !== "sublinks" && !navItem.url) { + return false + } + + // Filter to only links allowed by the current role + const role = navItem.roleId || Constants.Roles.BASIC + return userRoleHierarchy?.find(roleId => roleId === role) + }) + .map(navItem => { + const enrichedNavItem = enrichNavItem(navItem) + if (navItem.type === "sublinks" && navItem.subLinks?.length) { + enrichedNavItem.subLinks = navItem.subLinks + .filter(subLink => subLink.text && subLink.url) + .map(enrichNavItem) + } + return enrichedNavItem + }) } const isInternal = url => { - return url.startsWith("/") + return url?.startsWith("/") } const ensureExternal = url => { + if (!url?.length) { + return url + } return !url.startsWith("http") ? `http://${url}` : url } - const close = () => { - mobileOpen = false - } - const navigateToPortal = () => { if ($builderStore.inBuilder) return window.location.href = "/builder/apps" @@ -194,7 +223,7 @@ >