Add client support for nav item updates
This commit is contained in:
parent
917a387468
commit
284f7fe3cc
|
@ -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 {
|
||||
|
@ -18,6 +17,7 @@
|
|||
} = sdk
|
||||
const component = getContext("component")
|
||||
const context = getContext("context")
|
||||
const navStateStore = writable({})
|
||||
|
||||
// Legacy props which must remain unchanged for backwards compatibility
|
||||
export let title
|
||||
|
@ -65,7 +65,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
|
||||
|
@ -103,26 +103,50 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getValidLinks = (allLinks, userRoleHierarchy) => {
|
||||
// Strip links missing required info
|
||||
let validLinks = (allLinks || []).filter(link => link.text)
|
||||
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
|
||||
}
|
||||
|
||||
// Filter to only links allowed by the current role
|
||||
return validLinks.filter(link => {
|
||||
const role = link.roleId || Constants.Roles.BASIC
|
||||
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 => {
|
||||
return url.startsWith("/")
|
||||
return url?.startsWith("/")
|
||||
}
|
||||
|
||||
const ensureExternal = url => {
|
||||
return !url.startsWith("http") ? `http://${url}` : url
|
||||
if (!url?.length) {
|
||||
return url
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
mobileOpen = false
|
||||
return !url.startsWith("http") ? `http://${url}` : url
|
||||
}
|
||||
|
||||
const navigateToPortal = () => {
|
||||
|
@ -197,7 +221,7 @@
|
|||
>
|
||||
<div class="nav nav--{typeClass} size--{navWidthClass}">
|
||||
<div class="nav-header">
|
||||
{#if validLinks?.length}
|
||||
{#if enrichedNavItems.length}
|
||||
<div class="burger">
|
||||
<Icon
|
||||
hoverable
|
||||
|
@ -246,28 +270,19 @@
|
|||
class:visible={mobileOpen}
|
||||
on:click={() => (mobileOpen = false)}
|
||||
/>
|
||||
{#if validLinks?.length}
|
||||
{#if enrichedNavItems.length}
|
||||
<div class="links" class:visible={mobileOpen}>
|
||||
{#each validLinks as { text, url }}
|
||||
{#if isInternal(url)}
|
||||
<a
|
||||
class={FieldTypes.LINK}
|
||||
href={url}
|
||||
use:linkable
|
||||
on:click={close}
|
||||
use:active={url}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
class={FieldTypes.LINK}
|
||||
href={ensureExternal(url)}
|
||||
on:click={close}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
{/if}
|
||||
{#each enrichedNavItems as navItem}
|
||||
<NavItem
|
||||
type={navItem.type}
|
||||
text={navItem.text}
|
||||
url={navItem.url}
|
||||
subLinks={navItem.subLinks}
|
||||
internalLink={navItem.internalLink}
|
||||
on:clickLink={() => (mobileOpen = false)}
|
||||
leftNav={navigation === "Left"}
|
||||
{navStateStore}
|
||||
/>
|
||||
{/each}
|
||||
<div class="close">
|
||||
<Icon
|
||||
|
@ -505,21 +520,6 @@
|
|||
gap: 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 {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue