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 2736954438
commit 1fb1e70b28
52 changed files with 145 additions and 207 deletions

View File

@ -90,7 +90,7 @@
"@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10",
"@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",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0",

View File

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

View File

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

View File

@ -1,7 +1,23 @@
<script>
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 { 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>
<div class="design">
@ -41,8 +57,10 @@
</div>
<div class="content">
{#if $selectedScreen}
<slot />
<AppPanel />
{/if}
</div>
</div>

View File

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

View File

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

View File

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