Improve urlStateSync utility, improve routing structure, improve component tree

This commit is contained in:
Andrew Kingston 2022-04-28 12:05:34 +01:00
parent 1d9b053efc
commit f50ba7ab4f
52 changed files with 145 additions and 207 deletions

View File

@ -90,7 +90,7 @@
"@babel/preset-env": "^7.13.12", "@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10", "@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.0", "@roxi/routify": "2.18.5",
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19", "@sveltejs/vite-plugin-svelte": "1.0.0-next.19",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0", "@testing-library/svelte": "^3.0.0",

View File

@ -2,13 +2,26 @@ 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, baseUrl = "." } = options || {} const {
urlParam,
stateKey,
validate,
baseUrl = "..",
fallbackUrl,
store,
routify,
} = options || {}
if ( if (
!keys?.length || !urlParam ||
!params?.subscribe || !stateKey ||
!baseUrl ||
!urlParam ||
!store?.subscribe || !store?.subscribe ||
!goto?.subscribe || !routify ||
!redirect?.subscribe !routify.params?.subscribe ||
!routify.goto?.subscribe ||
!routify.redirect?.subscribe ||
!routify.page?.subscribe
) { ) {
console.warn("syncURLToState invoked with missing parameters") console.warn("syncURLToState invoked with missing parameters")
return return
@ -18,96 +31,70 @@ export const syncURLToState = options => {
// to just subscribe and cache the latest versions. // to just subscribe and cache the latest versions.
// We can grab their initial values as this is during component // We can grab their initial values as this is during component
// initialisation. // initialisation.
let cachedParams = get(params) let cachedParams = get(routify.params)
let cachedGoto = get(goto) let cachedGoto = get(routify.goto)
let cachedRedirect = get(redirect) let cachedRedirect = get(routify.redirect)
let hydrated = false let cachedPage = get(routify.page)
let previousParamsHash = null
let debug = false let debug = false
const log = (...params) => debug && console.log(...params) const log = (...params) => debug && console.log(...params)
// Navigate to a certain URL // Navigate to a certain URL
const gotoUrl = url => { const gotoUrl = (url, params) => {
if (get(isChangingPage) && hydrated) { log("Navigating to", url, "with params", params)
return cachedGoto(url, params)
}
log("Navigating to", url)
cachedGoto(url)
} }
// Redirect to a certain URL // Redirect to a certain URL
const redirectUrl = url => { const redirectUrl = url => {
if (get(isChangingPage) && hydrated) {
return
}
log("Redirecting to", url) log("Redirecting to", url)
cachedRedirect(url) cachedRedirect(url)
} }
// Updates state with new URL params // Updates state with new URL params
const mapUrlToState = params => { const mapUrlToState = params => {
// Determine any required state updates // Check if we have new URL params
let stateUpdates = [] const paramsHash = JSON.stringify(params)
const state = get(store) const newParams = paramsHash !== previousParamsHash
for (let key of keys) { previousParamsHash = paramsHash
const urlValue = params?.[key.url] const urlValue = params?.[urlParam]
const stateValue = state?.[key.state] const stateValue = get(store)?.[stateKey]
if (urlValue && urlValue !== stateValue) { if (!newParams || !urlValue) {
log(
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
)
stateUpdates.push(state => {
state[key.state] = urlValue
})
if (key.validate && key.fallbackUrl) {
if (!key.validate(urlValue)) {
log("Invalid URL param!")
redirectUrl(key.fallbackUrl)
hydrated = true
return
}
}
}
}
// Mark our initial hydration as completed
hydrated = true
// Avoid updating the store at all if not necessary to prevent a wasted
// store invalidation
if (!stateUpdates.length) {
return return
} }
// Apply the required state updates // Check if new value is valid
log("Performing", stateUpdates.length, "state updates") if (validate && fallbackUrl) {
store.update(state => { if (!validate(urlValue)) {
for (let update of stateUpdates) { log("Invalid URL param!")
update(state) redirectUrl(fallbackUrl)
return
} }
return state }
})
// Only update state if we have a new value
if (urlValue !== stateValue) {
log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`)
store.update(state => {
state[stateKey] = urlValue
return state
})
}
} }
// 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
let url = baseUrl
let needsUpdate = false let needsUpdate = false
for (let key of keys) { const urlValue = cachedParams?.[urlParam]
const urlValue = cachedParams?.[key.url] const stateValue = state?.[stateKey]
const stateValue = state?.[key.state] if (stateValue !== urlValue) {
url += `/${stateValue}` needsUpdate = true
if (stateValue !== urlValue) { log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
needsUpdate = true if (validate && fallbackUrl) {
log( if (!validate(stateValue)) {
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})` log("Invalid state param!")
) redirectUrl(fallbackUrl)
if (key.validate && key.fallbackUrl) { return
if (!key.validate(stateValue)) {
log("Invalid state param!")
redirectUrl(key.fallbackUrl)
return
}
} }
} }
} }
@ -120,7 +107,11 @@ export const syncURLToState = options => {
// Navigate to the new URL // Navigate to the new URL
if (!get(isChangingPage)) { if (!get(isChangingPage)) {
gotoUrl(url) const newUrlParams = {
...cachedParams,
[urlParam]: stateValue,
}
gotoUrl(cachedPage.path, newUrlParams)
} }
} }
@ -128,18 +119,21 @@ export const syncURLToState = options => {
mapUrlToState(cachedParams) mapUrlToState(cachedParams)
// Subscribe to URL changes and cache them // Subscribe to URL changes and cache them
const unsubscribeParams = params.subscribe($urlParams => { const unsubscribeParams = routify.params.subscribe($urlParams => {
cachedParams = $urlParams cachedParams = $urlParams
mapUrlToState($urlParams) mapUrlToState($urlParams)
}) })
// Subscribe to routify store changes and cache them // Subscribe to routify store changes and cache them
const unsubscribeGoto = goto.subscribe($goto => { const unsubscribeGoto = routify.goto.subscribe($goto => {
cachedGoto = $goto cachedGoto = $goto
}) })
const unsubscribeRedirect = redirect.subscribe($redirect => { const unsubscribeRedirect = routify.redirect.subscribe($redirect => {
cachedRedirect = $redirect cachedRedirect = $redirect
}) })
const unsubscribePage = routify.page.subscribe($page => {
cachedPage = $page
})
// Subscribe to store changes and keep URL up to date // Subscribe to store changes and keep URL up to date
const unsubscribeStore = store.subscribe(mapStateToUrl) const unsubscribeStore = store.subscribe(mapStateToUrl)
@ -149,6 +143,7 @@ export const syncURLToState = options => {
unsubscribeParams() unsubscribeParams()
unsubscribeGoto() unsubscribeGoto()
unsubscribeRedirect() unsubscribeRedirect()
unsubscribePage()
unsubscribeStore() unsubscribeStore()
} }
} }

View File

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

View File

@ -11,9 +11,6 @@
$: roleColor = getRoleColor(roleId) $: roleColor = getRoleColor(roleId)
$: roleName = $roles.find(x => x._id === roleId)?.name || "Unknown" $: roleName = $roles.find(x => x._id === roleId)?.name || "Unknown"
// Needs to be absolute as we embed this component from multiple different URLs
$: newComponentUrl = `/builder/app/${$store.appId}/design/components/${$selectedScreen?._id}/new`
const getRoleColor = roleId => { const getRoleColor = roleId => {
return RoleColours[roleId] || "#ffa500" return RoleColours[roleId] || "#ffa500"
} }
@ -42,7 +39,7 @@
<Button <Button
cta cta
icon="Add" icon="Add"
on:click={() => $goto(`./components/${$selectedScreen._id}/new`)} on:click={() => $goto(`../${$selectedScreen._id}/components/new`)}
> >
Component Component
</Button> </Button>

View File

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

View File

@ -1,7 +1,23 @@
<script> <script>
import { IconSideNav, IconSideNavItem } from "@budibase/bbui" import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
import { goto, isActive } from "@roxi/routify" import * as routify from "@roxi/routify"
import AppPanel from "./_components/AppPanel.svelte" import AppPanel from "./_components/AppPanel.svelte"
import { syncURLToState } from "helpers/urlStateSync"
import { store, selectedScreen } from "builderStore"
import { onDestroy } from "svelte"
const { isActive, goto } = routify
// Keep URL and state in sync for selected screen ID
const stopSyncing = syncURLToState({
urlParam: "screenId",
stateKey: "selectedScreenId",
validate: id => $store.screens.some(screen => screen._id === id),
fallbackUrl: "../../",
store,
routify,
})
onDestroy(stopSyncing)
</script> </script>
<div class="design"> <div class="design">
@ -41,8 +57,10 @@
</div> </div>
<div class="content"> <div class="content">
<slot /> {#if $selectedScreen}
<AppPanel /> <slot />
<AppPanel />
{/if}
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@
let newOffsets = {} let newOffsets = {}
// Calculate left offset // Calculate left offset
const offsetX = bounds.left + bounds.width + scrollLeft - 40 const offsetX = bounds.left + bounds.width + scrollLeft - 58
if (offsetX > sidebarWidth) { if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth newOffsets.left = offsetX - sidebarWidth
} else { } else {
@ -91,8 +91,7 @@
<style> <style>
.nav-items-container { .nav-items-container {
margin: 0 calc(-1 * var(--spacing-l)); padding: var(--spacing-xl) 0;
padding: var(--spacing-xl) var(--spacing-l);
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; overflow: auto;
height: 0; height: 0;

View File

@ -1,27 +1,18 @@
<script> <script>
import { syncURLToState } from "helpers/urlStateSync" import { syncURLToState } from "helpers/urlStateSync"
import { store, selectedScreen } from "builderStore" import { store, selectedScreen } from "builderStore"
import { goto, params, redirect } from "@roxi/routify" import * as routify from "@roxi/routify"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
// Keep URL and state in sync for selected component ID // Keep URL and state in sync for selected component ID
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
keys: [ urlParam: "componentId",
{ stateKey: "selectedComponentId",
url: "componentId", validate: id => !!findComponent($selectedScreen.props, id),
state: "selectedComponentId", fallbackUrl: "../",
validate: componentId => {
return !!findComponent($selectedScreen.props, componentId)
},
fallbackUrl: "../",
},
],
store, store,
params, routify,
goto,
redirect,
baseUrl: "..",
}) })
onDestroy(stopSyncing) onDestroy(stopSyncing)

View File

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

View File

@ -0,0 +1,7 @@
<script>
import ScreenNavigationPanel from "./_components/ScreenNavigationPanel.svelte"
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
</script>
<ScreenNavigationPanel />
<ScreenSettingsPanel />

View File

@ -1,27 +0,0 @@
<script>
import { store } from "builderStore"
import { onDestroy } from "svelte"
import { syncURLToState } from "helpers/urlStateSync"
import { goto, params, redirect } from "@roxi/routify"
// 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,
baseUrl: "..",
})
onDestroy(stopSyncing)
</script>
<slot />

View File

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

View File

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

View File

@ -1,29 +0,0 @@
<script>
import { store } from "builderStore"
import { onDestroy } from "svelte"
import { syncURLToState } from "helpers/urlStateSync"
import { goto, params, redirect } from "@roxi/routify"
import ScreenNavigationPanel from "./_components/ScreenNavigationPanel.svelte"
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
// 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>
<ScreenNavigationPanel />
<ScreenSettingsPanel />

View File

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

View File

@ -2,5 +2,3 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
$goto("./design") $goto("./design")
</script> </script>
<!-- routify:options index=false -->

View File

@ -1223,10 +1223,10 @@
estree-walker "^2.0.1" estree-walker "^2.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@roxi/routify@2.18.0": "@roxi/routify@2.18.5":
version "2.18.0" version "2.18.5"
resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2" resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.5.tgz#abd2b7cadeed008ab20d40a68323719d9c254552"
integrity sha512-MVB50HN+VQWLzfjLplcBjsSBvwOiExKOmht2DuWR3WQ60JxQi9pSejkB06tFVkFKNXz2X5iYtKDqKBTdae/gRg== integrity sha512-xNG84JOSUtCyOV0WeQwPcj+HbA4nxtXCqvt9JXQZm13pdJZqY18WV2C7ayReKGtRAa3ogQyDo8k/C8f1MixA1w==
dependencies: dependencies:
"@roxi/ssr" "^0.2.1" "@roxi/ssr" "^0.2.1"
"@types/node" ">=4.2.0 < 13" "@types/node" ">=4.2.0 < 13"