Add client support for nav item updates

This commit is contained in:
Andrew Kingston 2024-03-28 17:58:32 +00:00
parent 917a387468
commit 284f7fe3cc
2 changed files with 217 additions and 53 deletions

View File

@ -2,9 +2,8 @@
import { getContext, setContext } from "svelte" import { getContext, setContext } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { Heading, Icon, clickOutside } from "@budibase/bbui" import { Heading, Icon, clickOutside } from "@budibase/bbui"
import { FieldTypes } from "constants"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import active from "svelte-spa-router/active" import NavItem from "./NavItem.svelte"
const sdk = getContext("sdk") const sdk = getContext("sdk")
const { const {
@ -18,6 +17,7 @@
} = sdk } = sdk
const component = getContext("component") const component = getContext("component")
const context = getContext("context") const context = getContext("context")
const navStateStore = writable({})
// Legacy props which must remain unchanged for backwards compatibility // Legacy props which must remain unchanged for backwards compatibility
export let title export let title
@ -65,7 +65,7 @@
}) })
setContext("layout", store) setContext("layout", store)
$: validLinks = getValidLinks(links, $roleStore) $: enrichedNavItems = enrichNavItems(links, $roleStore)
$: typeClass = NavigationClasses[navigation] || NavigationClasses.None $: typeClass = NavigationClasses[navigation] || NavigationClasses.None
$: navWidthClass = WidthClasses[navWidth || width] || WidthClasses.Large $: navWidthClass = WidthClasses[navWidth || width] || WidthClasses.Large
$: pageWidthClass = WidthClasses[pageWidth || width] || WidthClasses.Large $: pageWidthClass = WidthClasses[pageWidth || width] || WidthClasses.Large
@ -103,28 +103,52 @@
} }
} }
const getValidLinks = (allLinks, userRoleHierarchy) => { const enrichNavItem = navItem => {
// Strip links missing required info const internalLink = isInternal(navItem.url)
let validLinks = (allLinks || []).filter(link => link.text) return {
// Filter to only links allowed by the current role ...navItem,
return validLinks.filter(link => { internalLink,
const role = link.roleId || Constants.Roles.BASIC url: internalLink ? navItem.url : ensureExternal(navItem.url),
return userRoleHierarchy?.find(roleId => roleId === role) }
}) }
const enrichNavItems = (navItems, userRoleHierarchy) => {
if (!navItems?.length) {
return []
}
return navItems
.filter(navitem => {
// Strip nav items without text
if (!navitem.text) {
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)
.map(enrichNavItem)
}
return enrichedNavItem
})
} }
const isInternal = url => { const isInternal = url => {
return url.startsWith("/") return url?.startsWith("/")
} }
const ensureExternal = url => { const ensureExternal = url => {
if (!url?.length) {
return url
}
return !url.startsWith("http") ? `http://${url}` : url return !url.startsWith("http") ? `http://${url}` : url
} }
const close = () => {
mobileOpen = false
}
const navigateToPortal = () => { const navigateToPortal = () => {
if ($builderStore.inBuilder) return if ($builderStore.inBuilder) return
window.location.href = "/builder/apps" window.location.href = "/builder/apps"
@ -197,7 +221,7 @@
> >
<div class="nav nav--{typeClass} size--{navWidthClass}"> <div class="nav nav--{typeClass} size--{navWidthClass}">
<div class="nav-header"> <div class="nav-header">
{#if validLinks?.length} {#if enrichedNavItems.length}
<div class="burger"> <div class="burger">
<Icon <Icon
hoverable hoverable
@ -246,28 +270,19 @@
class:visible={mobileOpen} class:visible={mobileOpen}
on:click={() => (mobileOpen = false)} on:click={() => (mobileOpen = false)}
/> />
{#if validLinks?.length} {#if enrichedNavItems.length}
<div class="links" class:visible={mobileOpen}> <div class="links" class:visible={mobileOpen}>
{#each validLinks as { text, url }} {#each enrichedNavItems as navItem}
{#if isInternal(url)} <NavItem
<a type={navItem.type}
class={FieldTypes.LINK} text={navItem.text}
href={url} url={navItem.url}
use:linkable subLinks={navItem.subLinks}
on:click={close} internalLink={navItem.internalLink}
use:active={url} on:clickLink={() => (mobileOpen = false)}
> leftNav={navigation === "Left"}
{text} {navStateStore}
</a> />
{:else}
<a
class={FieldTypes.LINK}
href={ensureExternal(url)}
on:click={close}
>
{text}
</a>
{/if}
{/each} {/each}
<div class="close"> <div class="close">
<Icon <Icon
@ -505,21 +520,6 @@
gap: var(--spacing-xl); gap: var(--spacing-xl);
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
.link {
opacity: 0.75;
color: var(--navTextColor);
font-size: var(--spectrum-global-dimension-font-size-200);
font-weight: 600;
transition: color 130ms ease-out;
}
.link.active {
opacity: 1;
}
.link:hover {
opacity: 1;
text-decoration: underline;
text-underline-position: under;
}
.close { .close {
display: none; display: none;
position: absolute; position: absolute;

View File

@ -0,0 +1,164 @@
<script>
import { createEventDispatcher, getContext } from "svelte"
import active from "svelte-spa-router/active"
import { Icon } from "@budibase/bbui"
export let type
export let url
export let text
export let subLinks
export let internalLink
export let leftNav = false
export let navStateStore
const dispatch = createEventDispatcher()
const sdk = getContext("sdk")
const { linkable } = sdk
$: expanded = !!$navStateStore[text]
$: icon = !leftNav || expanded ? "ChevronDown" : "ChevronRight"
const onClickLink = () => {
dispatch("clickLink")
}
const onClickDropdown = () => {
if (!leftNav) {
return
}
navStateStore.update(state => ({
...state,
[text]: !state[text],
}))
}
</script>
{#if !type || type === "link"}
{#if internalLink}
<!--
It's stupid that we have to add class:active={false} here, but if we don't
then svelte will strip out the CSS selector and active links won't be
styled
-->
<a
href={url}
on:click={onClickLink}
use:active={url}
use:linkable
class:active={false}
>
{text}
</a>
{:else}
<a href={url} on:click={onClickLink}>
{text}
</a>
{/if}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="dropdown" class:left={leftNav} class:expanded>
<div class="text" on:click={onClickDropdown}>
{text}
<Icon name={icon} />
</div>
<div class="sublinks-wrapper">
<div class="sublinks">
{#each subLinks as subLink}
{#if subLink.internalLink}
<a
href={subLink.url}
on:click={onClickLink}
use:active={subLink.url}
use:linkable
>
{subLink.text}
</a>
{:else}
<a href={subLink.url} on:click={onClickLink}>
{subLink.text}
</a>
{/if}
{/each}
</div>
</div>
</div>
{/if}
<style>
/* Generic styles */
a,
.text {
opacity: 0.75;
color: var(--navTextColor);
font-size: var(--spectrum-global-dimension-font-size-200);
transition: opacity 130ms ease-out;
font-weight: 600;
}
a.active {
opacity: 1;
}
a:hover,
.dropdown:not(.left.expanded):hover .text,
.text:hover {
cursor: pointer;
opacity: 1;
}
/* Top dropdowns */
.dropdown {
position: relative;
}
.text {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
user-select: none;
}
.sublinks-wrapper {
position: absolute;
top: 100%;
display: none;
padding-top: var(--spacing-s);
}
.dropdown:hover .sublinks-wrapper {
display: block;
}
.sublinks {
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: flex-start;
background: var(--spectrum-global-color-gray-50);
border-radius: 6px;
border: 1px solid var(--spectrum-global-color-gray-300);
min-width: 150px;
max-width: 250px;
padding: 10px 0;
overflow: hidden;
}
.sublinks a {
padding: 6px var(--spacing-l);
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
/* Left dropdowns */
.dropdown.left .sublinks-wrapper {
display: none;
}
.dropdown.left,
.dropdown.left.expanded .sublinks-wrapper,
.dropdown.dropdown.left.expanded .sublinks {
display: contents;
}
.dropdown.left a {
padding-top: 0;
padding-bottom: 0;
}
</style>