Update routify structure to prevent remounting full page when changing URL params and update nav item wrapping

This commit is contained in:
Andrew Kingston 2022-04-25 19:33:43 +01:00
parent 300f1e8ab1
commit 1e59576a30
29 changed files with 166 additions and 115 deletions

View File

@ -39,6 +39,7 @@
class:spectrum-StatusLight--negative={negative} class:spectrum-StatusLight--negative={negative}
class:spectrum-StatusLight--disabled={disabled} class:spectrum-StatusLight--disabled={disabled}
class:spectrum-StatusLight--active={active} class:spectrum-StatusLight--active={active}
class:withText={!!$$slots.default}
> >
<slot /> <slot />
</div> </div>
@ -49,6 +50,10 @@
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
--spectrum-statuslight-info-text-gap: 4px;
}
.spectrum-StatusLight.withText::before {
margin-right: 12px;
} }
.custom::before { .custom::before {
background: var(--color) !important; background: var(--color) !important;

View File

@ -9,10 +9,13 @@ export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
export const themeStore = getThemeStore() export const themeStore = getThemeStore()
export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
})
export const currentAsset = derived(store, $store => { export const currentAsset = derived(store, $store => {
const type = $store.currentFrontEndType const type = $store.currentFrontEndType
if (type === FrontendTypes.SCREEN) { if (type === FrontendTypes.SCREEN) {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
} else if (type === FrontendTypes.LAYOUT) { } else if (type === FrontendTypes.LAYOUT) {
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId) return $store.layouts.find(layout => layout._id === $store.selectedLayoutId)
} }
@ -39,21 +42,10 @@ export const selectedComponentPath = derived(
} }
) )
export const currentAssetId = derived(store, $store => {
return $store.currentFrontEndType === FrontendTypes.SCREEN
? $store.selectedScreenId
: $store.selectedLayoutId
})
export const currentAssetName = derived(currentAsset, $currentAsset => { export const currentAssetName = derived(currentAsset, $currentAsset => {
return $currentAsset?.name return $currentAsset?.name
}) })
// leave this as before for consistency
export const allScreens = derived(store, $store => {
return $store.screens
})
export const mainLayout = derived(store, $store => { export const mainLayout = derived(store, $store => {
return $store.layouts?.find( return $store.layouts?.find(
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE

View File

@ -1,7 +1,6 @@
import { get, writable } from "svelte/store" import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { import {
allScreens,
currentAsset, currentAsset,
mainLayout, mainLayout,
selectedComponent, selectedComponent,
@ -148,7 +147,7 @@ export const getFrontendStore = () => {
screens: { screens: {
select: screenId => { select: screenId => {
store.update(state => { store.update(state => {
let screens = get(allScreens) let screens = state.screens
let screen = let screen =
screens.find(screen => screen._id === screenId) || screens[0] screens.find(screen => screen._id === screenId) || screens[0]
if (!screen) return state if (!screen) return state

View File

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { allScreens, store } from "builderStore" import { store } from "builderStore"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { import {
ActionMenu, ActionMenu,
@ -27,7 +27,7 @@
$: allowDeletion = !external || table?.created $: allowDeletion = !external || table?.created
function showDeleteModal() { function showDeleteModal() {
templateScreens = $allScreens.filter( templateScreens = $store.screens.filter(
screen => screen.autoTableId === table._id screen => screen.autoTableId === table._id
) )
willBeDeleted = ["All table data"].concat( willBeDeleted = ["All table data"].concat(

View File

@ -45,6 +45,7 @@
class="nav-item" class="nav-item"
class:border class:border
class:selected class:selected
class:withActions
style={`padding-left: ${14 + indentLevel * 14}px`} style={`padding-left: ${14 + indentLevel * 14}px`}
{draggable} {draggable}
on:dragend on:dragend
@ -80,7 +81,9 @@
</div> </div>
{/if} {/if}
{#if color} {#if color}
<div class="light">
<StatusLight size="L" {color} /> <StatusLight size="L" {color} />
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -91,7 +94,7 @@
color: var(--grey-7); color: var(--grey-7);
transition: background-color transition: background-color
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out; var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
padding: 0 var(--spacing-m) 0 var(--spacing-l); padding: 0 var(--spacing-l) 0;
height: 32px; height: 32px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -111,9 +114,8 @@
background-color: var(--grey-3); background-color: var(--grey-3);
} }
.nav-item:hover .actions { .nav-item:hover .actions {
visibility: visible; display: grid;
} }
.nav-item-content { .nav-item-content {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
@ -122,6 +124,7 @@
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
width: max-content; width: max-content;
overflow: hidden;
} }
.icon { .icon {
@ -157,19 +160,23 @@
} }
.actions { .actions {
visibility: hidden;
width: 20px;
height: 20px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
display: grid; display: none;
margin-left: var(--spacing-s);
place-items: center; place-items: center;
} }
.actions,
.light :global(.spectrum-StatusLight) {
width: 20px;
height: 20px;
margin-left: var(--spacing-s);
}
.iconText { .iconText {
margin-top: 1px; margin-top: 1px;
font-size: var(--spectrum-global-dimension-font-size-50); font-size: var(--spectrum-global-dimension-font-size-50);
flex: 0 0 34px; flex: 0 0 34px;
} }
.nav-item.withActions:hover .light {
display: none;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { store, currentAsset, allScreens } from "builderStore" import { store, selectedScreen, currentAsset } from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
@ -49,7 +49,7 @@
// Extract data to pass to the iframe // Extract data to pass to the iframe
$: { $: {
screen = $allScreens.find(x => x._id === $store.selectedScreenId) screen = $selectedScreen
layout = $store.layouts.find(layout => layout._id === screen?.layoutId) layout = $store.layouts.find(layout => layout._id === screen?.layoutId)
} }
$: selectedComponentId = $store.selectedComponentId ?? "" $: selectedComponentId = $store.selectedComponentId ?? ""

View File

@ -1,12 +1,7 @@
<script> <script>
import { onMount, setContext } from "svelte" import { onMount, setContext } from "svelte"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { import { store, selectedAccessRole, screenSearchString } from "builderStore"
store,
allScreens,
selectedAccessRole,
screenSearchString,
} from "builderStore"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte" import ComponentNavigationTree from "components/design/NavigationPanel/ComponentNavigationTree/index.svelte"
import Layout from "components/design/NavigationPanel/Layout.svelte" import Layout from "components/design/NavigationPanel/Layout.svelte"
@ -99,7 +94,7 @@
// Select a valid screen with this new role - otherwise we'll not be // Select a valid screen with this new role - otherwise we'll not be
// able to change role at all because ComponentNavigationTree will kick us // able to change role at all because ComponentNavigationTree will kick us
// back the current role again because the same screen ID is still selected // back the current role again because the same screen ID is still selected
const firstValidScreenId = $allScreens.find( const firstValidScreenId = $store.screens.find(
screen => screen.routing.roleId === role screen => screen.routing.roleId === role
)?._id )?._id
if (firstValidScreenId) { if (firstValidScreenId) {

View File

@ -31,11 +31,12 @@
align-items: stretch; align-items: stretch;
} }
.header { .header {
height: 55px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--spacing-m) var(--spacing-l); padding: 0 var(--spacing-l);
border-bottom: var(--border-light); border-bottom: var(--border-light);
gap: var(--spacing-l); gap: var(--spacing-l);
} }

View File

@ -8,7 +8,7 @@
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { allScreens, selectedAccessRole } from "builderStore" import { store, selectedAccessRole } from "builderStore"
export let componentInstance export let componentInstance
export let bindings export let bindings
@ -17,7 +17,7 @@
const routeTaken = url => { const routeTaken = url => {
const roleId = get(selectedAccessRole) || "BASIC" const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some( return get(store).screens.some(
screen => screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() && screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId screen.routing.roleId === roleId
@ -26,7 +26,7 @@
const roleTaken = roleId => { const roleTaken = roleId => {
const url = get(currentAsset)?.routing.route const url = get(currentAsset)?.routing.route
return get(allScreens).some( return get(store).screens.some(
screen => screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() && screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId screen.routing.roleId === roleId

View File

@ -0,0 +1,55 @@
<script>
import { Icon, Heading } from "@budibase/bbui"
export let title
export let icon
</script>
<div class="settings-panel">
{#if title}
<div class="header">
{#if icon}
<Icon name={icon} />
{/if}
<div class="title">
<Heading size="XS">{title || ""}</Heading>
</div>
</div>
{/if}
<slot />
</div>
<style>
.settings-panel {
width: 260px;
background: var(--background);
border-left: var(--border-light);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.header {
height: 55px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0 var(--spacing-l);
border-bottom: var(--border-light);
gap: var(--spacing-m);
}
.header :global(*) {
color: var(--spectrum-global-color-gray-700);
}
.title {
flex: 1 1 auto;
width: 0;
}
.title :global(h1) {
overflow: hidden;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -2,7 +2,7 @@ import { get } from "svelte/store"
import { isChangingPage } from "@roxi/routify" import { isChangingPage } from "@roxi/routify"
export const syncURLToState = options => { export const syncURLToState = options => {
const { keys, params, store, goto, redirect } = options || {} const { keys, params, store, goto, redirect, baseUrl = "." } = options || {}
if ( if (
!keys?.length || !keys?.length ||
!params?.subscribe || !params?.subscribe ||
@ -22,13 +22,15 @@ export const syncURLToState = options => {
let cachedGoto = get(goto) let cachedGoto = get(goto)
let cachedRedirect = get(redirect) let cachedRedirect = get(redirect)
let hydrated = false let hydrated = false
let debug = false
const log = (...params) => debug && console.log(...params)
// Navigate to a certain URL // Navigate to a certain URL
const gotoUrl = url => { const gotoUrl = url => {
if (get(isChangingPage) && hydrated) { if (get(isChangingPage) && hydrated) {
return return
} }
console.log("Navigating to", url) log("Navigating to", url)
cachedGoto(url) cachedGoto(url)
} }
@ -37,7 +39,7 @@ export const syncURLToState = options => {
if (get(isChangingPage) && hydrated) { if (get(isChangingPage) && hydrated) {
return return
} }
console.log("Redirecting to", url) log("Redirecting to", url)
cachedRedirect(url) cachedRedirect(url)
} }
@ -50,7 +52,7 @@ export const syncURLToState = options => {
const urlValue = params?.[key.url] const urlValue = params?.[key.url]
const stateValue = state?.[key.state] const stateValue = state?.[key.state]
if (urlValue && urlValue !== stateValue) { if (urlValue && urlValue !== stateValue) {
console.log( log(
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})` `state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
) )
stateUpdates.push(state => { stateUpdates.push(state => {
@ -58,7 +60,7 @@ export const syncURLToState = options => {
}) })
if (key.validate && key.fallbackUrl) { if (key.validate && key.fallbackUrl) {
if (!key.validate(urlValue)) { if (!key.validate(urlValue)) {
console.log("Invalid URL param!") log("Invalid URL param!")
redirectUrl(key.fallbackUrl) redirectUrl(key.fallbackUrl)
hydrated = true hydrated = true
return return
@ -77,7 +79,7 @@ export const syncURLToState = options => {
} }
// Apply the required state updates // Apply the required state updates
console.log("Performing", stateUpdates.length, "state updates") log("Performing", stateUpdates.length, "state updates")
store.update(state => { store.update(state => {
for (let update of stateUpdates) { for (let update of stateUpdates) {
update(state) update(state)
@ -89,7 +91,7 @@ export const syncURLToState = options => {
// Updates the URL with new state values // Updates the URL with new state values
const mapStateToUrl = state => { const mapStateToUrl = state => {
// Determine new URL while checking for changes // Determine new URL while checking for changes
let url = ".." let url = baseUrl
let needsUpdate = false let needsUpdate = false
for (let key of keys) { for (let key of keys) {
const urlValue = cachedParams?.[key.url] const urlValue = cachedParams?.[key.url]
@ -97,12 +99,12 @@ export const syncURLToState = options => {
url += `/${stateValue}` url += `/${stateValue}`
if (stateValue !== urlValue) { if (stateValue !== urlValue) {
needsUpdate = true needsUpdate = true
console.log( log(
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})` `url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
) )
if (key.validate && key.fallbackUrl) { if (key.validate && key.fallbackUrl) {
if (!key.validate(stateValue)) { if (!key.validate(stateValue)) {
console.log("Invalid state param!") log("Invalid state param!")
redirectUrl(key.fallbackUrl) redirectUrl(key.fallbackUrl)
return return
} }

View File

@ -1,5 +0,0 @@
<script>
import { redirect } from "@roxi/routify"
$redirect(`./screens`)
</script>

View File

@ -1,30 +1,8 @@
<script> <script>
import { IconSideNav, IconSideNavItem } from "@budibase/bbui" import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
import { params, goto, redirect, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { store, allScreens } from "builderStore"
import { syncURLToState } from "helpers/urlStateSync"
import { onDestroy } from "svelte"
// Keep URL and state in sync for selected screen ID
const unsync = syncURLToState({
keys: [
{
url: "screenId",
state: "selectedScreenId",
validate: screenId => $allScreens.some(x => x._id === screenId),
fallbackUrl: "../",
},
],
store,
params,
goto,
redirect,
})
onDestroy(unsync)
</script> </script>
<!-- routify:options index=1 -->
<div class="design"> <div class="design">
<div class="icon-nav"> <div class="icon-nav">
<IconSideNav> <IconSideNav>

View File

@ -1,19 +1,5 @@
<script> <script>
import { allScreens } from "builderStore"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
let loaded = false $redirect(`./screens`)
onMount(() => {
if ($allScreens?.length) {
$redirect(`./${$allScreens[0]._id}`)
} else {
loaded = true
}
})
</script> </script>
{#if loaded}
You need to create a screen!
{/if}

View File

@ -2,18 +2,26 @@
import { Search, Layout, Select } from "@budibase/bbui" import { Search, Layout, Select } from "@budibase/bbui"
import NavigationPanel from "components/design/NavigationPanel/NavigationPanel.svelte" import NavigationPanel from "components/design/NavigationPanel/NavigationPanel.svelte"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { store, allScreens } from "builderStore" import { store, selectedScreen } from "builderStore"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import ScreenDropdownMenu from "./_components/ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./_components/ScreenDropdownMenu.svelte"
import AppPanel from "components/design/AppPanel/AppPanel.svelte"
import { RoleColours } from "constants" import { RoleColours } from "constants"
import ScreenWizard from "./_components/ScreenWizard.svelte" import ScreenWizard from "./_components/ScreenWizard.svelte"
import { onDestroy } from "svelte"
import { syncURLToState } from "helpers/urlStateSync"
import { goto, params, redirect } from "@roxi/routify"
import AppPanel from "components/design/AppPanel/AppPanel.svelte"
import SettingsPanel from "components/design/SettingsPanel/SettingsPanel.svelte"
let searchString let searchString
let accessRole = "all" let accessRole = "all"
let showNewScreenModal let showNewScreenModal
$: filteredScreens = getFilteredScreens($allScreens, searchString, accessRole) $: filteredScreens = getFilteredScreens(
$store.screens,
searchString,
accessRole
)
const getFilteredScreens = (screens, search, role) => { const getFilteredScreens = (screens, search, role) => {
return screens return screens
@ -32,6 +40,24 @@
const getRoleColor = roleId => { const getRoleColor = roleId => {
return RoleColours[roleId] || "pink" return RoleColours[roleId] || "pink"
} }
// Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({
keys: [
{
url: "screenId",
state: "selectedScreenId",
validate: screenId => $store.screens.some(x => x._id === screenId),
fallbackUrl: "../",
},
],
store,
params,
goto,
redirect,
})
onDestroy(stopSyncing)
</script> </script>
<NavigationPanel <NavigationPanel
@ -64,17 +90,14 @@
> >
<ScreenDropdownMenu screenId={screen._id} /> <ScreenDropdownMenu screenId={screen._id} />
</NavItem> </NavItem>
<!--{#if selectedScreen?._id === screen.id}-->
<!-- <ComponentTree-->
<!-- level={1}-->
<!-- components={selectedScreen.props._children}-->
<!-- currentComponent={$selectedComponent}-->
<!-- {dragDropStore}-->
<!-- />-->
<!--{/if}-->
{/each} {/each}
</NavigationPanel> </NavigationPanel>
<ScreenWizard bind:showModal={showNewScreenModal} />
<AppPanel /> <AppPanel />
<SettingsPanel
title={$selectedScreen?.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
/>
<ScreenWizard bind:showModal={showNewScreenModal} />

View File

@ -1,7 +1,7 @@
<script> <script>
import { ModalContent, Input } from "@budibase/bbui" import { ModalContent, Input } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore" import { store, selectedAccessRole } from "builderStore"
import { get } from "svelte/store" import { get } from "svelte/store"
export let onConfirm export let onConfirm
@ -35,7 +35,7 @@
const routeExists = url => { const routeExists = url => {
const roleId = get(selectedAccessRole) || "BASIC" const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some( return get(store).screens.some(
screen => screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() && screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId screen.routing.roleId === roleId

View File

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { store, allScreens } from "builderStore" import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import {
ActionMenu, ActionMenu,
@ -20,7 +20,7 @@
let confirmDeleteDialog let confirmDeleteDialog
let screenDetailsModal let screenDetailsModal
$: screen = $allScreens.find(screen => screen._id === screenId) $: screen = $store.screens.find(screen => screen._id === screenId)
const duplicateScreen = () => { const duplicateScreen = () => {
screenDetailsModal.show() screenDetailsModal.show()

View File

@ -1,10 +1,5 @@
<script> <script>
import { import { store, currentAsset, selectedComponent } from "builderStore"
store,
currentAsset,
selectedComponent,
allScreens,
} from "builderStore"
import { Detail, Layout, Button, Icon } from "@budibase/bbui" import { Detail, Layout, Button, Icon } from "@budibase/bbui"
import CurrentItemPreview from "components/design/AppPreview" import CurrentItemPreview from "components/design/AppPreview"

View File

@ -0,0 +1,19 @@
<script>
import { store } from "builderStore"
import { onMount } from "svelte"
import { redirect } from "@roxi/routify"
let loaded = false
onMount(() => {
if ($store.screens?.length) {
$redirect(`./${$store.screens[0]._id}`)
} else {
loaded = true
}
})
</script>
{#if loaded}
You need to create a screen!
{/if}