From 49948df9cdb7c908a37bcb46e3d3ea83ce682003 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 14 Sep 2023 15:31:51 +0100 Subject: [PATCH] Support both global and local state simultaneously --- .../client/src/components/app/Repeater.svelte | 4 +- .../src/components/context/Provider.svelte | 14 +++- packages/client/src/constants.js | 5 ++ packages/client/src/sdk.js | 3 +- packages/client/src/stores/context.js | 81 +++++++++++++++---- 5 files changed, 85 insertions(+), 22 deletions(-) diff --git a/packages/client/src/components/app/Repeater.svelte b/packages/client/src/components/app/Repeater.svelte index 39e11d12cf..16e1a5363e 100644 --- a/packages/client/src/components/app/Repeater.svelte +++ b/packages/client/src/components/app/Repeater.svelte @@ -10,7 +10,7 @@ export let vAlign export let gap - const { Provider } = getContext("sdk") + const { Provider, ContextScopes } = getContext("sdk") const component = getContext("component") $: rows = dataProvider?.rows ?? [] @@ -22,7 +22,7 @@ {:else if rows.length > 0} {#each rows as row, index} - + {/each} diff --git a/packages/client/src/components/context/Provider.svelte b/packages/client/src/components/context/Provider.svelte index b3e9d1e9fc..45ba4deed4 100644 --- a/packages/client/src/components/context/Provider.svelte +++ b/packages/client/src/components/context/Provider.svelte @@ -3,15 +3,23 @@ import { dataSourceStore, createContextStore } from "stores" import { ActionTypes } from "constants" import { generate } from "shortid" + import { ContextScopes } from "constants" export let data export let actions export let key + export let scope = ContextScopes.Global - const context = getContext("context") + let context = getContext("context") const component = getContext("component") const providerKey = key || $component.id + // Create a new layer of context if we are only locally scoped + if (scope === ContextScopes.Local) { + context = createContextStore(context) + setContext("context", context) + } + // Generate a permanent unique ID for this component and use it to register // any datasource actions const instanceId = generate() @@ -26,7 +34,7 @@ const provideData = newData => { const dataKey = JSON.stringify(newData) if (dataKey !== lastDataKey) { - context.actions.provideData(providerKey, newData) + context.actions.provideData(providerKey, newData, scope) lastDataKey = dataKey } } @@ -36,7 +44,7 @@ if (actionsKey !== lastActionsKey) { lastActionsKey = actionsKey newActions?.forEach(({ type, callback, metadata }) => { - context.actions.provideAction(providerKey, type, callback) + context.actions.provideAction(providerKey, type, callback, scope) // Register any "refresh datasource" actions with a singleton store // so we can easily refresh data at all levels for any datasource diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js index 37a45fbe5d..dfa7985599 100644 --- a/packages/client/src/constants.js +++ b/packages/client/src/constants.js @@ -32,5 +32,10 @@ export const ActionTypes = { ScrollTo: "ScrollTo", } +export const ContextScopes = { + Local: "local", + Global: "global", +} + export const DNDPlaceholderID = "dnd-placeholder" export const ScreenslotType = "screenslot" diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 237334ca57..6ea1d1b46d 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -20,7 +20,7 @@ import { getAction } from "utils/getAction" import Provider from "components/context/Provider.svelte" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" -import { ActionTypes } from "./constants" +import { ActionTypes, ContextScopes } from "./constants" import { fetchDatasourceSchema } from "./utils/schema.js" import { getAPIKey } from "./utils/api.js" @@ -44,6 +44,7 @@ export default { getAction, fetchDatasourceSchema, Provider, + ContextScopes, ActionTypes, getAPIKey, Block, diff --git a/packages/client/src/stores/context.js b/packages/client/src/stores/context.js index 0363fbf7a6..e54c773591 100644 --- a/packages/client/src/stores/context.js +++ b/packages/client/src/stores/context.js @@ -1,30 +1,79 @@ -import { writable } from "svelte/store" +import { writable, derived } from "svelte/store" +import { ContextScopes } from "constants" -export const createContextStore = () => { +export const createContextStore = parentContext => { const context = writable({}) let observers = [] - // Adds a data context layer to the tree - const provideData = (providerId, data) => { + // Derive the total context state at this point in the tree + const contexts = parentContext ? [parentContext, context] : [context] + const totalContext = derived(contexts, $contexts => { + return $contexts.reduce((total, context) => ({ ...total, ...context }), {}) + }) + + // Subscribe to updates in the parent context, so that we can proxy on any + // change messages to our own subscribers + if (parentContext) { + parentContext.actions.observeChanges(key => { + broadcastChange(key) + }) + } + + // Provide some data in context + const provideData = (providerId, data, scope = ContextScopes.Global) => { if (!providerId || data === undefined) { return } - context.update(state => { - state[providerId] = data - return state - }) - broadcastChange(providerId) + + // Proxy message up the chain if we have a parent and are providing global + // context + if (scope === ContextScopes.Global && parentContext) { + parentContext.actions.provideData(providerId, data, scope) + } + + // Otherwise this is either the context root, or we're providing a local + // context override, so we need to update the local context instead + else { + context.update(state => { + state[providerId] = data + return state + }) + broadcastChange(providerId) + } } - // Adds an action context layer to the tree - const provideAction = (providerId, actionType, callback) => { + // Provides some action in context + const provideAction = ( + providerId, + actionType, + callback, + scope = ContextScopes.Global + ) => { if (!providerId || !actionType) { return } - context.update(state => { - state[`${providerId}_${actionType}`] = callback - return state - }) + + // Proxy message up the chain if we have a parent and are providing global + // context + if (scope === ContextScopes.Global && parentContext) { + parentContext.actions.provideAction( + providerId, + actionType, + callback, + scope + ) + } + + // Otherwise this is either the context root, or we're providing a local + // context override, so we need to update the local context instead + else { + const key = `${providerId}_${actionType}` + context.update(state => { + state[key] = callback + return state + }) + broadcastChange(key) + } } const observeChanges = callback => { @@ -39,7 +88,7 @@ export const createContextStore = () => { } return { - subscribe: context.subscribe, + subscribe: totalContext.subscribe, actions: { provideData, provideAction,