Add validation and fallback URLs to URL/state sync utility
This commit is contained in:
parent
a2bb2aa631
commit
300f1e8ab1
|
@ -1,22 +1,45 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
import { isChangingPage } from "@roxi/routify"
|
||||||
|
|
||||||
export const syncURLToState = options => {
|
export const syncURLToState = options => {
|
||||||
const { keys, params, store, goto } = options || {}
|
const { keys, params, store, goto, redirect } = options || {}
|
||||||
if (
|
if (
|
||||||
!keys?.length ||
|
!keys?.length ||
|
||||||
!params?.subscribe ||
|
!params?.subscribe ||
|
||||||
!store?.subscribe ||
|
!store?.subscribe ||
|
||||||
!goto?.subscribe
|
!goto?.subscribe ||
|
||||||
|
!redirect?.subscribe
|
||||||
) {
|
) {
|
||||||
|
console.warn("syncURLToState invoked with missing parameters")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't dynamically fetch the value of routify stores so we need to
|
// We can't dynamically fetch the value of stateful routify stores so we need
|
||||||
// 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(params)
|
||||||
let cachedGoto = get(goto)
|
let cachedGoto = get(goto)
|
||||||
|
let cachedRedirect = get(redirect)
|
||||||
|
let hydrated = false
|
||||||
|
|
||||||
|
// Navigate to a certain URL
|
||||||
|
const gotoUrl = url => {
|
||||||
|
if (get(isChangingPage) && hydrated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Navigating to", url)
|
||||||
|
cachedGoto(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to a certain URL
|
||||||
|
const redirectUrl = url => {
|
||||||
|
if (get(isChangingPage) && hydrated) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Redirecting to", url)
|
||||||
|
cachedRedirect(url)
|
||||||
|
}
|
||||||
|
|
||||||
// Updates state with new URL params
|
// Updates state with new URL params
|
||||||
const mapUrlToState = params => {
|
const mapUrlToState = params => {
|
||||||
|
@ -26,16 +49,27 @@ export const syncURLToState = options => {
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
const urlValue = params?.[key.url]
|
const urlValue = params?.[key.url]
|
||||||
const stateValue = state?.[key.state]
|
const stateValue = state?.[key.state]
|
||||||
if (urlValue !== stateValue) {
|
if (urlValue && urlValue !== stateValue) {
|
||||||
console.log(
|
console.log(
|
||||||
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
|
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
|
||||||
)
|
)
|
||||||
stateUpdates.push(state => {
|
stateUpdates.push(state => {
|
||||||
state[key.state] = urlValue
|
state[key.state] = urlValue
|
||||||
})
|
})
|
||||||
|
if (key.validate && key.fallbackUrl) {
|
||||||
|
if (!key.validate(urlValue)) {
|
||||||
|
console.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
|
// Avoid updating the store at all if not necessary to prevent a wasted
|
||||||
// store invalidation
|
// store invalidation
|
||||||
if (!stateUpdates.length) {
|
if (!stateUpdates.length) {
|
||||||
|
@ -62,10 +96,17 @@ export const syncURLToState = options => {
|
||||||
const stateValue = state?.[key.state]
|
const stateValue = state?.[key.state]
|
||||||
url += `/${stateValue}`
|
url += `/${stateValue}`
|
||||||
if (stateValue !== urlValue) {
|
if (stateValue !== urlValue) {
|
||||||
|
needsUpdate = true
|
||||||
console.log(
|
console.log(
|
||||||
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
|
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
|
||||||
)
|
)
|
||||||
needsUpdate = true
|
if (key.validate && key.fallbackUrl) {
|
||||||
|
if (!key.validate(stateValue)) {
|
||||||
|
console.log("Invalid state param!")
|
||||||
|
redirectUrl(key.fallbackUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,8 +117,9 @@ export const syncURLToState = options => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the new URL
|
// Navigate to the new URL
|
||||||
console.log("Navigating to", url)
|
if (!get(isChangingPage)) {
|
||||||
cachedGoto(url)
|
gotoUrl(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initially hydrate state from URL
|
// Initially hydrate state from URL
|
||||||
|
@ -89,10 +131,13 @@ export const syncURLToState = options => {
|
||||||
mapUrlToState($urlParams)
|
mapUrlToState($urlParams)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to goto changes and cache them
|
// Subscribe to routify store changes and cache them
|
||||||
const unsubscribeGoto = goto.subscribe($goto => {
|
const unsubscribeGoto = goto.subscribe($goto => {
|
||||||
cachedGoto = $goto
|
cachedGoto = $goto
|
||||||
})
|
})
|
||||||
|
const unsubscribeRedirect = redirect.subscribe($redirect => {
|
||||||
|
cachedRedirect = $redirect
|
||||||
|
})
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -101,6 +146,7 @@ export const syncURLToState = options => {
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeParams()
|
unsubscribeParams()
|
||||||
unsubscribeGoto()
|
unsubscribeGoto()
|
||||||
|
unsubscribeRedirect()
|
||||||
unsubscribeStore()
|
unsubscribeStore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
||||||
import { params, goto, isActive } from "@roxi/routify"
|
import { params, goto, redirect, isActive } from "@roxi/routify"
|
||||||
import { store } from "builderStore"
|
import { store, allScreens } from "builderStore"
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
import { onDestroy } from "svelte"
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
|
// Keep URL and state in sync for selected screen ID
|
||||||
const unsync = syncURLToState({
|
const unsync = syncURLToState({
|
||||||
keys: [
|
keys: [
|
||||||
{
|
{
|
||||||
url: "screenId",
|
url: "screenId",
|
||||||
state: "selectedScreenId",
|
state: "selectedScreenId",
|
||||||
|
validate: screenId => $allScreens.some(x => x._id === screenId),
|
||||||
|
fallbackUrl: "../",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
store,
|
store,
|
||||||
params,
|
params,
|
||||||
goto,
|
goto,
|
||||||
|
redirect,
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(unsync)
|
onDestroy(unsync)
|
||||||
|
|
Loading…
Reference in New Issue