-
- 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 @@
>