Improve urlStateSync utility, improve routing structure, improve component tree
This commit is contained in:
parent
2736954438
commit
1fb1e70b28
|
@ -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",
|
||||
|
|
|
@ -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,96 +31,70 @@ 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)) {
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Apply the required state updates
|
||||
log("Performing", stateUpdates.length, "state updates")
|
||||
store.update(state => {
|
||||
for (let update of stateUpdates) {
|
||||
update(state)
|
||||
// Check if new value is valid
|
||||
if (validate && fallbackUrl) {
|
||||
if (!validate(urlValue)) {
|
||||
log("Invalid URL param!")
|
||||
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
|
||||
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}`
|
||||
if (stateValue !== urlValue) {
|
||||
needsUpdate = true
|
||||
log(
|
||||
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
|
||||
)
|
||||
if (key.validate && key.fallbackUrl) {
|
||||
if (!key.validate(stateValue)) {
|
||||
log("Invalid state param!")
|
||||
redirectUrl(key.fallbackUrl)
|
||||
return
|
||||
}
|
||||
const urlValue = cachedParams?.[urlParam]
|
||||
const stateValue = state?.[stateKey]
|
||||
if (stateValue !== urlValue) {
|
||||
needsUpdate = true
|
||||
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
|
||||
if (validate && fallbackUrl) {
|
||||
if (!validate(stateValue)) {
|
||||
log("Invalid state param!")
|
||||
redirectUrl(fallbackUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
$goto("../design")
|
||||
</script>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../screens")
|
||||
</script>
|
|
@ -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">
|
||||
<slot />
|
||||
<AppPanel />
|
||||
{#if $selectedScreen}
|
||||
<slot />
|
||||
<AppPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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;
|
|
@ -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)
|
||||
},
|
||||
fallbackUrl: "../",
|
||||
},
|
||||
],
|
||||
urlParam: "componentId",
|
||||
stateKey: "selectedComponentId",
|
||||
validate: id => !!findComponent($selectedScreen.props, id),
|
||||
fallbackUrl: "../",
|
||||
store,
|
||||
params,
|
||||
goto,
|
||||
redirect,
|
||||
baseUrl: "..",
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("./screens")
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
import ScreenNavigationPanel from "./_components/ScreenNavigationPanel.svelte"
|
||||
import ScreenSettingsPanel from "./_components/ScreenSettingsPanel.svelte"
|
||||
</script>
|
||||
|
||||
<ScreenNavigationPanel />
|
||||
<ScreenSettingsPanel />
|
|
@ -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 />
|
|
@ -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}
|
|
@ -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}
|
||||
|
|
|
@ -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 />
|
|
@ -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}
|
|
@ -2,5 +2,3 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
$goto("./design")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=false -->
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue