From 7754286d47e092fa0921e8d71aa1cab46390fe74 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 25 Oct 2024 08:49:54 +0200 Subject: [PATCH 01/38] new FrameworkOptions, HostStore, configureFramework() action --- dashi/src/demo/App.tsx | 4 +- dashi/src/lib/actions/configureFramework.ts | 11 ++++++ ...temStore.ts => initializeContributions.ts} | 14 +++++-- dashi/src/lib/index.ts | 12 ++++-- dashi/src/lib/types/state/store.ts | 37 ++++++++++++------- dashi/src/lib/utils/configureLogging.ts | 6 ++- 6 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 dashi/src/lib/actions/configureFramework.ts rename dashi/src/lib/actions/{initSystemStore.ts => initializeContributions.ts} (71%) diff --git a/dashi/src/demo/App.tsx b/dashi/src/demo/App.tsx index 8836424a..b143befd 100644 --- a/dashi/src/demo/App.tsx +++ b/dashi/src/demo/App.tsx @@ -1,14 +1,14 @@ import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"; import Typography from "@mui/material/Typography"; -import { configureLogging, initSystemStore } from "@/lib"; +import { configureLogging, initializeContributions } from "@/lib"; import ExtensionsInfo from "./components/ExtensionInfo"; import PanelsControl from "./components/PanelsControl"; import PanelsRow from "./components/PanelsRow"; configureLogging(); -initSystemStore(); +initializeContributions(); // MUI's default font family const fontFamily = "Roboto, Arial, sans-serif"; diff --git a/dashi/src/lib/actions/configureFramework.ts b/dashi/src/lib/actions/configureFramework.ts new file mode 100644 index 00000000..9af72cce --- /dev/null +++ b/dashi/src/lib/actions/configureFramework.ts @@ -0,0 +1,11 @@ +import { store } from "@/lib/store"; +import type { FrameworkOptions } from "@/lib/types/state/store"; +import { configureLogging } from "@/lib/utils/configureLogging"; + +export function configureFramework(options?: FrameworkOptions) { + const { loggingOptions, ...storeOptions } = options || {}; + if (loggingOptions) { + configureLogging(loggingOptions); + } + store.setState(storeOptions); +} diff --git a/dashi/src/lib/actions/initSystemStore.ts b/dashi/src/lib/actions/initializeContributions.ts similarity index 71% rename from dashi/src/lib/actions/initSystemStore.ts rename to dashi/src/lib/actions/initializeContributions.ts index 6c484b95..4949c512 100644 --- a/dashi/src/lib/actions/initSystemStore.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -1,14 +1,20 @@ import { store } from "@/lib/store"; import { fetchApiResult } from "@/lib/utils/fetchApiResult"; -import { type ApiOptions, fetchContributions } from "@/lib/api"; +import { fetchContributions } from "@/lib/api"; import { type Contribution } from "@/lib/types/model/contribution"; import { type ContributionState } from "@/lib/types/state/contribution"; import { type ContribPoint } from "@/lib/types/model/extension"; +import type { FrameworkOptions } from "@/lib/types/state/store"; +import { configureFramework } from "@/lib"; -export function initSystemStore(apiOptions?: ApiOptions) { - store.setState({ apiOptions, contributionsResult: { status: "pending" } }); +export function initializeContributions(options?: FrameworkOptions) { + if (options) { + configureFramework(options); + } + const apiOptions = store.getState().apiOptions; + store.setState({ contributionsResult: { status: "pending" } }); fetchApiResult(fetchContributions, apiOptions).then((contributionsResult) => { - // TODO: assert Boolean(contributionsResult.data) + // TODO: validate contributionsResult and contributionsResult.data const { extensions, contributions: contributionModelsRecord } = contributionsResult.data!; const contributionStatesRecord: Record = diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index 68f0f1ed..0d6fdec0 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -1,15 +1,19 @@ +// Types export { type Contribution } from "@/lib/types/model/contribution"; export { type ContributionState } from "@/lib/types/state/contribution"; export { type PropertyChangeEvent, type PropertyChangeHandler, } from "@/lib/types/model/event"; -export { DashiComponent } from "@/lib/components/DashiComponent"; -export { initSystemStore } from "@/lib/actions/initSystemStore"; +// Actions (store changes) +export { initializeContributions } from "@/lib/actions/initializeContributions"; +export { configureFramework } from "@/lib/actions/configureFramework"; export { applyPropertyChange } from "@/lib/actions/applyPropertyChange"; export { setComponentVisibility } from "@/lib/actions/setComponentVisibility"; export { updateContributionState } from "@/lib/actions/updateContributionState"; -export { configureLogging } from "@/lib/utils/configureLogging"; +// React Components +export { DashiComponent } from "@/lib/components/DashiComponent"; +// React Hooks export { useStore, useExtensions, @@ -17,3 +21,5 @@ export { useContributionModelsRecord, useContributionStatesRecord, } from "@/lib/hooks"; +// Utilities +export { configureLogging } from "@/lib/utils/configureLogging"; diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 2cff38bd..51cc80a6 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -1,21 +1,32 @@ -import { - type ContribPoint, - type Contributions, - type Extension, +import type { + ContribPoint, + Contributions, + Extension, } from "@/lib/types/model/extension"; -import { type Contribution } from "@/lib/types/model/contribution"; -import { type ApiResult } from "@/lib/utils/fetchApiResult"; -import { type ContributionState } from "@/lib/types/state/contribution"; -import { type ApiOptions } from "@/lib/api"; +import type { Contribution } from "@/lib/types/model/contribution"; +import type { ApiResult } from "@/lib/utils/fetchApiResult"; +import type { ContributionState } from "@/lib/types/state/contribution"; +import type { ApiOptions } from "@/lib/api"; -export interface StoreState { +export interface HostStore = object> { + getState: () => T; + setState: (state: Partial) => void; +} + +export interface FrameworkOptions { + /** The host applications state management store. */ + hostStore?: HostStore; + /** API options to configure backend. */ apiOptions?: ApiOptions; - // API call result GET /contributions +} + +export interface StoreState extends FrameworkOptions { + /** API call result from `GET /contributions`. */ contributionsResult: ApiResult; - // All extensions + /** All extensions */ extensions: Extension[]; - // A record that maps contribPoint --> Contribution[] + /** A record that maps contribPoint --> Contribution[].*/ contributionModelsRecord: Record; - // A record that maps contribPoint --> ContributionState[] + /** A record that maps contribPoint --> ContributionState[].*/ contributionStatesRecord: Record; } diff --git a/dashi/src/lib/utils/configureLogging.ts b/dashi/src/lib/utils/configureLogging.ts index f8c5b012..618e0622 100644 --- a/dashi/src/lib/utils/configureLogging.ts +++ b/dashi/src/lib/utils/configureLogging.ts @@ -9,7 +9,11 @@ const pathStyle = "color:light-dark(darkgrey, lightgray)"; let unsubscribe: (() => void) | undefined = undefined; -export function configureLogging(options?: { enabled?: boolean }) { +export interface LoggingOptions { + enabled?: boolean; +} + +export function configureLogging(options?: LoggingOptions) { if (unsubscribe) { unsubscribe(); unsubscribe = undefined; From 9fcbe0aee500dc6835be7f6973e455e50f8431d8 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 25 Oct 2024 09:05:18 +0200 Subject: [PATCH 02/38] new FrameworkOptions.configuration --- dashi/src/lib/actions/applyPropertyChange.ts | 42 ++++++++++--------- dashi/src/lib/actions/configureFramework.ts | 12 +++--- .../lib/actions/initializeContributions.ts | 2 +- .../src/lib/actions/setComponentVisibility.ts | 4 +- dashi/src/lib/store.ts | 1 + dashi/src/lib/types/state/store.ts | 13 ++++-- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 3bea08d8..627526e1 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -25,7 +25,7 @@ export function applyPropertyChange( contribIndex: number, contribEvent: PropertyChangeEvent, ) { - const { apiOptions, contributionModelsRecord, contributionStatesRecord } = + const { configuration, contributionModelsRecord, contributionStatesRecord } = store.getState(); const contributionModels = contributionModelsRecord[contribPoint]; const contributionStates = contributionStatesRecord[contribPoint]; @@ -61,25 +61,27 @@ export function applyPropertyChange( ...callbackRef, }), ); - fetchApiResult(fetchStateChangeRequests, callbackRequests, apiOptions).then( - (changeRequestsResult) => { - const secondaryChangeRequests = changeRequestsResult.data; - if (secondaryChangeRequests) { - applyStateChangeRequests( - [primaryChangeRequest].concat(secondaryChangeRequests), - ); - } else { - // Note, we do not even apply the primaryChangeRequest - // in order to avoid an inconsistent state. - console.error( - "callback failed:", - changeRequestsResult.error, - "for call requests:", - callbackRefs, - ); - } - }, - ); + fetchApiResult( + fetchStateChangeRequests, + callbackRequests, + configuration.api, + ).then((changeRequestsResult) => { + const secondaryChangeRequests = changeRequestsResult.data; + if (secondaryChangeRequests) { + applyStateChangeRequests( + [primaryChangeRequest].concat(secondaryChangeRequests), + ); + } else { + // Note, we do not even apply the primaryChangeRequest + // in order to avoid an inconsistent state. + console.error( + "callback failed:", + changeRequestsResult.error, + "for call requests:", + callbackRefs, + ); + } + }); } } diff --git a/dashi/src/lib/actions/configureFramework.ts b/dashi/src/lib/actions/configureFramework.ts index 9af72cce..5e6176bf 100644 --- a/dashi/src/lib/actions/configureFramework.ts +++ b/dashi/src/lib/actions/configureFramework.ts @@ -2,10 +2,12 @@ import { store } from "@/lib/store"; import type { FrameworkOptions } from "@/lib/types/state/store"; import { configureLogging } from "@/lib/utils/configureLogging"; -export function configureFramework(options?: FrameworkOptions) { - const { loggingOptions, ...storeOptions } = options || {}; - if (loggingOptions) { - configureLogging(loggingOptions); +export function configureFramework(options: FrameworkOptions) { + if (options.logging) { + configureLogging(options.logging); } - store.setState(storeOptions); + const { configuration } = store.getState(); + store.setState({ + configuration: { ...configuration, ...options }, + }); } diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index 4949c512..46ee9944 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -11,7 +11,7 @@ export function initializeContributions(options?: FrameworkOptions) { if (options) { configureFramework(options); } - const apiOptions = store.getState().apiOptions; + const apiOptions = store.getState().configuration.api; store.setState({ contributionsResult: { status: "pending" } }); fetchApiResult(fetchContributions, apiOptions).then((contributionsResult) => { // TODO: validate contributionsResult and contributionsResult.data diff --git a/dashi/src/lib/actions/setComponentVisibility.ts b/dashi/src/lib/actions/setComponentVisibility.ts index 459cc55a..7b873fac 100644 --- a/dashi/src/lib/actions/setComponentVisibility.ts +++ b/dashi/src/lib/actions/setComponentVisibility.ts @@ -10,7 +10,7 @@ export function setComponentVisibility( panelIndex: number, visible: boolean, ) { - const { apiOptions, contributionStatesRecord } = store.getState(); + const { configuration, contributionStatesRecord } = store.getState(); const contributionStates = contributionStatesRecord[contribPoint]; const contributionState = contributionStates[panelIndex]; if (contributionState.visible === visible) { @@ -30,7 +30,7 @@ export function setComponentVisibility( contribPoint, panelIndex, inputValues, - apiOptions, + configuration.api, ).then((componentModelResult) => { const componentState = componentModelResult?.data; updateContributionState(contribPoint, panelIndex, { diff --git a/dashi/src/lib/store.ts b/dashi/src/lib/store.ts index 706f9ef1..5fc9a3e3 100644 --- a/dashi/src/lib/store.ts +++ b/dashi/src/lib/store.ts @@ -3,6 +3,7 @@ import { create } from "zustand"; import { type StoreState } from "@/lib/types/state/store"; export const store = create(() => ({ + configuration: {}, contributionsResult: {}, extensions: [], contributionModelsRecord: {}, diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 51cc80a6..619990ff 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -7,8 +7,11 @@ import type { Contribution } from "@/lib/types/model/contribution"; import type { ApiResult } from "@/lib/utils/fetchApiResult"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ApiOptions } from "@/lib/api"; +import type { LoggingOptions } from "@/lib/utils/configureLogging"; -export interface HostStore = object> { +export interface HostStore< + T extends Record = Record, +> { getState: () => T; setState: (state: Partial) => void; } @@ -17,10 +20,14 @@ export interface FrameworkOptions { /** The host applications state management store. */ hostStore?: HostStore; /** API options to configure backend. */ - apiOptions?: ApiOptions; + api?: ApiOptions; + /** Logging options. */ + logging?: LoggingOptions; } -export interface StoreState extends FrameworkOptions { +export interface StoreState { + /** Framework configuration */ + configuration: FrameworkOptions; /** API call result from `GET /contributions`. */ contributionsResult: ApiResult; /** All extensions */ From 8f14be5d466b3457e779ed673d806c74efbd0eb1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 25 Oct 2024 12:53:55 +0200 Subject: [PATCH 03/38] now get input values from input kinds "AppState" and "State" --- .../lib/actions/applyPropertyChange.test.ts | 27 ++++++++++ dashi/src/lib/actions/applyPropertyChange.ts | 53 ++++++++++++++++--- dashi/src/lib/types/model/callback.ts | 15 +++++- dashi/src/lib/types/state/store.ts | 2 +- dashi/src/lib/utils/isSubscriptable.ts | 7 +++ 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 dashi/src/lib/utils/isSubscriptable.ts diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index 79938dae..ce705ec6 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -11,6 +11,7 @@ import { applyComponentStateChange, applyContributionChangeRequests, getComponentStateValue, + getInputValue, } from "./applyPropertyChange"; const componentState: ComponentState = { @@ -142,3 +143,29 @@ describe("Test that getComponentStateValue()", () => { ).toEqual(13); }); }); + +describe("Test that getInputValue()", () => { + it("works with input.id and input.property", () => { + const state = { x: { y: 26 } }; + expect( + getInputValue({ kind: "State", id: "x", property: "y" }, state), + ).toEqual(26); + }); + + it("works with arrays indexes", () => { + const state = { x: [4, 5, 6] }; + expect( + getInputValue({ kind: "State", id: "x", property: "1" }, state), + ).toEqual(5); + }); + + it("works without input.property", () => { + const state = { x: [4, 5, 6] }; + expect(getInputValue({ kind: "State", id: "x" }, state)).toEqual([4, 5, 6]); + }); + + it("works without input.id and input.property", () => { + const state = { x: [4, 5, 6] }; + expect(getInputValue({ kind: "State" }, state)).toBe(state); + }); +}); diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 627526e1..1f90dfc1 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -19,6 +19,7 @@ import { type Contribution } from "@/lib/types/model/contribution"; import { type ContributionState } from "@/lib/types/state/contribution"; import { updateArray } from "@/lib/utils/updateArray"; import { isContainerState } from "@/lib/utils/isContainerState"; +import { isSubscriptable } from "@/lib/utils/isSubscriptable"; export function applyPropertyChange( contribPoint: ContribPoint, @@ -35,6 +36,7 @@ export function applyPropertyChange( contributionModel, contributionState, contribEvent, + store.getState().configuration.hostStore?.getState, ); // The primary state change request corresponds // to the original property change event. @@ -89,6 +91,7 @@ function generateCallbackRefs( contributionModel: Contribution, contributionState: ContributionState, contribEvent: PropertyChangeEvent, + getHostState?: () => unknown, ): CallbackRef[] { const callbackRefs: CallbackRef[] = []; // Prepare calling all callbacks of the contribution @@ -98,6 +101,7 @@ function generateCallbackRefs( contributionState, contribEvent, callback, + getHostState, ); if (inputValues) { callbackRefs.push({ callbackIndex, inputValues }); @@ -106,10 +110,12 @@ function generateCallbackRefs( return callbackRefs; } +// we export for testing only export function getInputValues( contributionState: ContributionState, contribEvent: PropertyChangeEvent, callback: Callback, + getHostState?: () => unknown, ): unknown[] | undefined { // No inputs defined if (!callback.inputs || !callback.inputs.length) { @@ -142,9 +148,23 @@ export function getInputValues( input, ); } + } else if (input.kind === "State") { + // Note, it is actually not ok to pass contributionState here directly. + // We may use sub-state of contributionState later that holds + // the extra state required here. + inputValue = getInputValue(input, contributionState); + } else if (input.kind === "AppState") { + if (getHostState) { + inputValue = getInputValue(input, getHostState()); + } else { + console.warn( + "missing configuration 'hostState'," + + " which is need to resolve inputs of kind 'AppState'", + input, + ); + } } else { - // TODO: Get value from another kind of input. - console.warn(`input kind not supported yet:`, input); + console.warn(`unknown input kind:`, input); } if (inputValue === undefined) { // We use null, because undefined is not JSON-serializable. @@ -155,9 +175,7 @@ export function getInputValues( }); } -export function applyStateChangeRequests( - stateChangeRequests: StateChangeRequest[], -) { +function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { const { contributionStatesRecord } = store.getState(); const contributionStatesRecordNew = applyContributionChangeRequests( contributionStatesRecord, @@ -171,6 +189,7 @@ export function applyStateChangeRequests( // TODO: Set value of another kind of output. } +// we export for testing only export function applyContributionChangeRequests( contributionStatesRecord: Record, stateChangeRequests: StateChangeRequest[], @@ -209,16 +228,18 @@ export function applyContributionChangeRequests( return contributionStatesRecord; } +// we export for testing only export function applyComponentStateChange( componentState: ComponentState, stateChange: StateChange, ): ComponentState { if (componentState.id === stateChange.id) { + const property = stateChange.property || "value"; const oldValue = (componentState as unknown as Record)[ - stateChange.property + property ]; if (oldValue !== stateChange.value) { - return { ...componentState, [stateChange.property]: stateChange.value }; + return { ...componentState, [property]: stateChange.value }; } } else if (isContainerState(componentState)) { const containerStateOld: ContainerState = componentState; @@ -243,11 +264,12 @@ export function applyComponentStateChange( const noValue = {}; +// we export for testing only export function getComponentStateValue( componentState: ComponentState, input: Input, ): unknown { - if (componentState.id === input.id) { + if (componentState.id === input.id && input.property) { return (componentState as unknown as Record)[ input.property ]; @@ -262,3 +284,18 @@ export function getComponentStateValue( } return noValue; } + +// we export for testing only +export function getInputValue( + input: Input, + state: unknown, +): unknown | undefined { + let inputValue: unknown = state; + if (input.id && isSubscriptable(inputValue)) { + inputValue = inputValue[input.id]; + } + if (input.property && isSubscriptable(inputValue)) { + inputValue = inputValue[input.property]; + } + return inputValue; +} diff --git a/dashi/src/lib/types/model/callback.ts b/dashi/src/lib/types/model/callback.ts index b5fc1fbd..f9bea9b7 100644 --- a/dashi/src/lib/types/model/callback.ts +++ b/dashi/src/lib/types/model/callback.ts @@ -19,9 +19,20 @@ export interface CbParameter { export type InputOutputKind = "AppState" | "State" | "Component"; export interface InputOutput { + /** The kind of input or output. */ kind: InputOutputKind; - id: string; - property: string; + /** + * The identifier of a subcomponent. + * `id` is not needed if kind == "AppState" | "State". + */ + id?: string; + // Note, we may allow `property` to be a constant + // expression of the form: name {"." name | index} + /** + * The property of an object or array index. + * `property` may not be needed if `id` is sufficient. + */ + property?: string; } export interface Input extends InputOutput {} diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 619990ff..32eb7487 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -10,7 +10,7 @@ import type { ApiOptions } from "@/lib/api"; import type { LoggingOptions } from "@/lib/utils/configureLogging"; export interface HostStore< - T extends Record = Record, + T extends Record = Record, > { getState: () => T; setState: (state: Partial) => void; diff --git a/dashi/src/lib/utils/isSubscriptable.ts b/dashi/src/lib/utils/isSubscriptable.ts new file mode 100644 index 00000000..8eb79110 --- /dev/null +++ b/dashi/src/lib/utils/isSubscriptable.ts @@ -0,0 +1,7 @@ +export function isSubscriptable( + value: unknown, +): value is { [key: string]: unknown } { + return ( + (typeof value === "object" && value !== null) || typeof value === "function" + ); +} From f8cbd173b9b73f5510b5c1cc71100b9e7ebaedb3 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 25 Oct 2024 14:53:46 +0200 Subject: [PATCH 04/38] type-safety for the hostStore --- dashi/src/demo/App.tsx | 14 +++++++++++--- dashi/src/lib/actions/configureFramework.ts | 2 +- dashi/src/lib/actions/initializeContributions.ts | 2 +- dashi/src/lib/types/state/store.ts | 16 +++++----------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/dashi/src/demo/App.tsx b/dashi/src/demo/App.tsx index b143befd..ca274250 100644 --- a/dashi/src/demo/App.tsx +++ b/dashi/src/demo/App.tsx @@ -1,14 +1,22 @@ import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"; import Typography from "@mui/material/Typography"; -import { configureLogging, initializeContributions } from "@/lib"; +import { initializeContributions } from "@/lib"; import ExtensionsInfo from "./components/ExtensionInfo"; import PanelsControl from "./components/PanelsControl"; import PanelsRow from "./components/PanelsRow"; +import { create } from "zustand/react"; -configureLogging(); +interface AppState { + datasetIndex: number; +} + +const appStore = create(() => ({ datasetIndex: 0 })); -initializeContributions(); +initializeContributions({ + hostStore: appStore, + logging: { enabled: true }, +}); // MUI's default font family const fontFamily = "Roboto, Arial, sans-serif"; diff --git a/dashi/src/lib/actions/configureFramework.ts b/dashi/src/lib/actions/configureFramework.ts index 5e6176bf..acfc7279 100644 --- a/dashi/src/lib/actions/configureFramework.ts +++ b/dashi/src/lib/actions/configureFramework.ts @@ -2,7 +2,7 @@ import { store } from "@/lib/store"; import type { FrameworkOptions } from "@/lib/types/state/store"; import { configureLogging } from "@/lib/utils/configureLogging"; -export function configureFramework(options: FrameworkOptions) { +export function configureFramework(options: FrameworkOptions) { if (options.logging) { configureLogging(options.logging); } diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index 46ee9944..f9a16343 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -7,7 +7,7 @@ import { type ContribPoint } from "@/lib/types/model/extension"; import type { FrameworkOptions } from "@/lib/types/state/store"; import { configureFramework } from "@/lib"; -export function initializeContributions(options?: FrameworkOptions) { +export function initializeContributions(options?: FrameworkOptions) { if (options) { configureFramework(options); } diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 32eb7487..c68ad50a 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -8,26 +8,20 @@ import type { ApiResult } from "@/lib/utils/fetchApiResult"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ApiOptions } from "@/lib/api"; import type { LoggingOptions } from "@/lib/utils/configureLogging"; +import type { StoreApi } from "zustand"; -export interface HostStore< - T extends Record = Record, -> { - getState: () => T; - setState: (state: Partial) => void; -} - -export interface FrameworkOptions { +export interface FrameworkOptions { /** The host applications state management store. */ - hostStore?: HostStore; + hostStore?: StoreApi; /** API options to configure backend. */ api?: ApiOptions; /** Logging options. */ logging?: LoggingOptions; } -export interface StoreState { +export interface StoreState { /** Framework configuration */ - configuration: FrameworkOptions; + configuration: FrameworkOptions; /** API call result from `GET /contributions`. */ contributionsResult: ApiResult; /** All extensions */ From 1682ea92009c57bff6fbed8ccece20845be40e6c Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 25 Oct 2024 17:03:56 +0200 Subject: [PATCH 05/38] Adjust demo for application state changes --- dashi/src/demo/App.tsx | 10 +- dashi/src/demo/components/ControlBar.tsx | 33 ++++++ dashi/src/demo/store.ts | 28 +++++ .../lib/actions/applyPropertyChange.test.ts | 68 ------------ dashi/src/lib/actions/applyPropertyChange.ts | 101 ++++-------------- dashi/src/lib/actions/common.test.ts | 89 +++++++++++++++ dashi/src/lib/actions/common.ts | 94 ++++++++++++++++ .../src/lib/actions/setComponentVisibility.ts | 21 ++-- dashipy/dashipy/callback.py | 30 +++--- dashipy/dashipy/contribution.py | 1 - dashipy/dashipy/demo/context.py | 27 +++-- dashipy/dashipy/demo/contribs/panel.py | 1 + dashipy/my_extension/my_panel_1.py | 42 ++++---- dashipy/my_extension/my_panel_2.py | 79 ++++++++------ 14 files changed, 383 insertions(+), 241 deletions(-) create mode 100644 dashi/src/demo/components/ControlBar.tsx create mode 100644 dashi/src/demo/store.ts create mode 100644 dashi/src/lib/actions/common.test.ts create mode 100644 dashi/src/lib/actions/common.ts diff --git a/dashi/src/demo/App.tsx b/dashi/src/demo/App.tsx index ca274250..1d2b5c6a 100644 --- a/dashi/src/demo/App.tsx +++ b/dashi/src/demo/App.tsx @@ -3,15 +3,10 @@ import Typography from "@mui/material/Typography"; import { initializeContributions } from "@/lib"; import ExtensionsInfo from "./components/ExtensionInfo"; +import ControlBar from "@/demo/components/ControlBar"; import PanelsControl from "./components/PanelsControl"; import PanelsRow from "./components/PanelsRow"; -import { create } from "zustand/react"; - -interface AppState { - datasetIndex: number; -} - -const appStore = create(() => ({ datasetIndex: 0 })); +import { appStore } from "@/demo/store"; initializeContributions({ hostStore: appStore, @@ -40,6 +35,7 @@ function App() { Dashi Demo + diff --git a/dashi/src/demo/components/ControlBar.tsx b/dashi/src/demo/components/ControlBar.tsx new file mode 100644 index 00000000..4cf010ac --- /dev/null +++ b/dashi/src/demo/components/ControlBar.tsx @@ -0,0 +1,33 @@ +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; + +import { useAppStore } from "@/demo/store"; + +function ControlBar() { + const { datasets, selectedDatasetId, setSelectedDatasetId } = useAppStore(); + + return ( +
+ + Dataset (App State) + + +
+ ); +} + +export default ControlBar; diff --git a/dashi/src/demo/store.ts b/dashi/src/demo/store.ts new file mode 100644 index 00000000..cd17f757 --- /dev/null +++ b/dashi/src/demo/store.ts @@ -0,0 +1,28 @@ +import { create } from "zustand/react"; + +export interface Dataset { + id: string; + title: string; +} + +export interface AppState { + datasets: Dataset[]; + selectedDatasetId: string | null; + setSelectedDatasetId(setSelectedDatasetId: string | null): void; +} + +export const appStore = create((set, get) => ({ + // TODO: get from demo server + datasets: [ + { id: "ds0", title: "Dataset #1" }, + { id: "ds1", title: "Dataset #2" }, + ], + selectedDatasetId: "ds0", + setSelectedDatasetId: (selectedDatasetId: string | null) => { + if (selectedDatasetId !== get().selectedDatasetId) { + set({ selectedDatasetId }); + } + }, +})); + +export const useAppStore = appStore; diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index ce705ec6..f7e38125 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -10,8 +10,6 @@ import { type ContributionState } from "@/lib/types/state/contribution"; import { applyComponentStateChange, applyContributionChangeRequests, - getComponentStateValue, - getInputValue, } from "./applyPropertyChange"; const componentState: ComponentState = { @@ -103,69 +101,3 @@ describe("Test that applyComponentStateChange()", () => { expect(newState).toBe(componentState); }); }); - -describe("Test that getComponentStateValue()", () => { - it("works on 1st level", () => { - expect( - getComponentStateValue(componentState, { - kind: "Component", - id: "b1", - property: "value", - }), - ).toBeUndefined(); - }); - - it("works on 2nd level", () => { - expect( - getComponentStateValue(componentState, { - kind: "Component", - id: "p1", - property: "figure", - }), - ).toEqual(null); - }); - - it("works on 3rd level", () => { - expect( - getComponentStateValue(componentState, { - kind: "Component", - id: "cb1", - property: "value", - }), - ).toEqual(true); - - expect( - getComponentStateValue(componentState, { - kind: "Component", - id: "dd1", - property: "value", - }), - ).toEqual(13); - }); -}); - -describe("Test that getInputValue()", () => { - it("works with input.id and input.property", () => { - const state = { x: { y: 26 } }; - expect( - getInputValue({ kind: "State", id: "x", property: "y" }, state), - ).toEqual(26); - }); - - it("works with arrays indexes", () => { - const state = { x: [4, 5, 6] }; - expect( - getInputValue({ kind: "State", id: "x", property: "1" }, state), - ).toEqual(5); - }); - - it("works without input.property", () => { - const state = { x: [4, 5, 6] }; - expect(getInputValue({ kind: "State", id: "x" }, state)).toEqual([4, 5, 6]); - }); - - it("works without input.id and input.property", () => { - const state = { x: [4, 5, 6] }; - expect(getInputValue({ kind: "State" }, state)).toBe(state); - }); -}); diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 1f90dfc1..ba49860e 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -10,7 +10,6 @@ import { type Callback, type CallbackRef, type CallbackRequest, - type Input, type StateChange, type StateChangeRequest, } from "@/lib/types/model/callback"; @@ -19,7 +18,7 @@ import { type Contribution } from "@/lib/types/model/contribution"; import { type ContributionState } from "@/lib/types/state/contribution"; import { updateArray } from "@/lib/utils/updateArray"; import { isContainerState } from "@/lib/utils/isContainerState"; -import { isSubscriptable } from "@/lib/utils/isSubscriptable"; +import { getInputValues } from "@/lib/actions/common"; export function applyPropertyChange( contribPoint: ContribPoint, @@ -97,7 +96,7 @@ function generateCallbackRefs( // Prepare calling all callbacks of the contribution // that are triggered by the property change (contributionModel.callbacks || []).forEach((callback, callbackIndex) => { - const inputValues = getInputValues( + const inputValues = getCallbackInputValues( contributionState, contribEvent, callback, @@ -111,14 +110,14 @@ function generateCallbackRefs( } // we export for testing only -export function getInputValues( +export function getCallbackInputValues( contributionState: ContributionState, contribEvent: PropertyChangeEvent, callback: Callback, getHostState?: () => unknown, ): unknown[] | undefined { - // No inputs defined if (!callback.inputs || !callback.inputs.length) { + // No inputs defined return undefined; } @@ -129,50 +128,24 @@ export function getInputValues( input.id === contribEvent.componentId && input.property === contribEvent.propertyName, ); - // No trigger index found --> this callback is not applicable if (triggerIndex < 0) { + // No trigger index found --> this callback is not applicable return undefined; } - // Compute input values - return callback.inputs.map((input, inputIndex) => { - let inputValue: unknown = undefined; - if (!input.kind || input.kind === "Component") { - if (inputIndex === triggerIndex) { - // Return the property value of the trigger event - inputValue = contribEvent.propertyValue; - } else if (contributionState.componentState) { - // Return value of a property of some component in the tree - inputValue = getComponentStateValue( - contributionState.componentState, - input, - ); - } - } else if (input.kind === "State") { - // Note, it is actually not ok to pass contributionState here directly. - // We may use sub-state of contributionState later that holds - // the extra state required here. - inputValue = getInputValue(input, contributionState); - } else if (input.kind === "AppState") { - if (getHostState) { - inputValue = getInputValue(input, getHostState()); - } else { - console.warn( - "missing configuration 'hostState'," + - " which is need to resolve inputs of kind 'AppState'", - input, - ); - } - } else { - console.warn(`unknown input kind:`, input); - } - if (inputValue === undefined) { - // We use null, because undefined is not JSON-serializable. - inputValue = null; - console.warn(`value is undefined for input`, input); - } - return inputValue; - }); + const inputValues = getInputValues( + callback.inputs, + contributionState, + getHostState, + ); + + if (inputValues[triggerIndex] === contribEvent.propertyValue) { + // No change --> this callback is not applicable + return undefined; + } + + inputValues[triggerIndex] = contribEvent.propertyValue; + return inputValues; } function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { @@ -261,41 +234,3 @@ export function applyComponentStateChange( } return componentState; } - -const noValue = {}; - -// we export for testing only -export function getComponentStateValue( - componentState: ComponentState, - input: Input, -): unknown { - if (componentState.id === input.id && input.property) { - return (componentState as unknown as Record)[ - input.property - ]; - } else if (isContainerState(componentState)) { - for (let i = 0; i < componentState.components.length; i++) { - const item = componentState.components[i]; - const itemValue = getComponentStateValue(item, input); - if (itemValue !== noValue) { - return itemValue; - } - } - } - return noValue; -} - -// we export for testing only -export function getInputValue( - input: Input, - state: unknown, -): unknown | undefined { - let inputValue: unknown = state; - if (input.id && isSubscriptable(inputValue)) { - inputValue = inputValue[input.id]; - } - if (input.property && isSubscriptable(inputValue)) { - inputValue = inputValue[input.property]; - } - return inputValue; -} diff --git a/dashi/src/lib/actions/common.test.ts b/dashi/src/lib/actions/common.test.ts new file mode 100644 index 00000000..9e1b9770 --- /dev/null +++ b/dashi/src/lib/actions/common.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect } from "vitest"; + +import type { ComponentState, PlotState } from "@/lib/types/state/component"; +import { getComponentStateValue, getInputValueFromState } from "./common"; + +const componentState: ComponentState = { + type: "Box", + id: "b1", + components: [ + { type: "Plot", id: "p1", chart: null } as PlotState, + { + type: "Box", + id: "b2", + components: [ + { type: "Checkbox", id: "cb1", value: true }, + { type: "Dropdown", id: "dd1", value: 13 }, + ], + }, + ], + value: 14, +}; + +describe("Test that getComponentStateValue()", () => { + it("works on 1st level", () => { + expect( + getComponentStateValue(componentState, { + kind: "Component", + id: "b1", + property: "value", + }), + ).toEqual(14); + }); + + it("works on 2nd level", () => { + expect( + getComponentStateValue(componentState, { + kind: "Component", + id: "p1", + property: "chart", + }), + ).toEqual(null); + }); + + it("works on 3rd level", () => { + expect( + getComponentStateValue(componentState, { + kind: "Component", + id: "cb1", + property: "value", + }), + ).toEqual(true); + + expect( + getComponentStateValue(componentState, { + kind: "Component", + id: "dd1", + property: "value", + }), + ).toEqual(13); + }); +}); + +describe("Test that getInputValueFromState()", () => { + it("works with input.id and input.property", () => { + const state = { x: { y: 26 } }; + expect( + getInputValueFromState({ kind: "State", id: "x", property: "y" }, state), + ).toEqual(26); + }); + + it("works with arrays indexes", () => { + const state = { x: [4, 5, 6] }; + expect( + getInputValueFromState({ kind: "State", id: "x", property: "1" }, state), + ).toEqual(5); + }); + + it("works without input.property", () => { + const state = { x: [4, 5, 6] }; + expect(getInputValueFromState({ kind: "State", id: "x" }, state)).toEqual([ + 4, 5, 6, + ]); + }); + + it("works without input.id and input.property", () => { + const state = { x: [4, 5, 6] }; + expect(getInputValueFromState({ kind: "State" }, state)).toBe(state); + }); +}); diff --git a/dashi/src/lib/actions/common.ts b/dashi/src/lib/actions/common.ts new file mode 100644 index 00000000..43b1c69a --- /dev/null +++ b/dashi/src/lib/actions/common.ts @@ -0,0 +1,94 @@ +import type { Input } from "@/lib/types/model/callback.js"; +import type { ContributionState } from "@/lib/types/state/contribution.js"; +import type { ComponentState } from "@/lib/types/state/component.js"; +import { isSubscriptable } from "@/lib/utils/isSubscriptable.js"; +import { isContainerState } from "@/lib/utils/isContainerState.js"; + +export function getInputValues( + inputs: Input[], + contributionState: ContributionState, + getHostState?: () => unknown, +): unknown[] { + return inputs.map((input) => + getInputValue(input, contributionState, getHostState), + ); +} + +export function getInputValue( + input: Input, + contributionState: ContributionState, + getHostState?: () => unknown, +): unknown { + let inputValue: unknown = undefined; + + if (!input.kind || input.kind === "Component") { + if (contributionState.componentState) { + // Return value of a property of some component in the tree + inputValue = getComponentStateValue( + contributionState.componentState, + input, + ); + } + } else if (input.kind === "State") { + // Note, it is actually not ok to pass contributionState here directly. + // We may use a sub-state of contributionState later that holds + // the extra state required here. + inputValue = getInputValueFromState(input, contributionState); + } else if (input.kind === "AppState") { + if (getHostState) { + inputValue = getInputValueFromState(input, getHostState()); + } else { + console.warn( + "missing configuration 'hostState'," + + " which is need to resolve inputs of kind 'AppState'", + input, + ); + } + } else { + console.warn(`unknown input kind:`, input); + } + if (inputValue === undefined) { + // We use null, because undefined is not JSON-serializable. + inputValue = null; + console.warn(`value is undefined for input`, input); + } + return inputValue; +} + +const noValue = {}; + +// we export for testing only +export function getComponentStateValue( + componentState: ComponentState, + input: Input, +): unknown { + if (componentState.id === input.id && input.property) { + return (componentState as unknown as Record)[ + input.property + ]; + } else if (isContainerState(componentState)) { + for (let i = 0; i < componentState.components.length; i++) { + const item = componentState.components[i]; + const itemValue = getComponentStateValue(item, input); + if (itemValue !== noValue) { + return itemValue; + } + } + } + return noValue; +} + +// we export for testing only +export function getInputValueFromState( + input: Input, + state: unknown, +): unknown | undefined { + let inputValue: unknown = state; + if (input.id && isSubscriptable(inputValue)) { + inputValue = inputValue[input.id]; + } + if (input.property && isSubscriptable(inputValue)) { + inputValue = inputValue[input.property]; + } + return inputValue; +} diff --git a/dashi/src/lib/actions/setComponentVisibility.ts b/dashi/src/lib/actions/setComponentVisibility.ts index 7b873fac..d00ec02c 100644 --- a/dashi/src/lib/actions/setComponentVisibility.ts +++ b/dashi/src/lib/actions/setComponentVisibility.ts @@ -1,8 +1,8 @@ import { store } from "@/lib/store"; import { fetchInitialComponentState } from "@/lib/api"; import { type ContribPoint } from "@/lib/types/model/extension"; -import { type Input } from "@/lib/types/model/callback"; import { fetchApiResult } from "@/lib/utils/fetchApiResult"; +import { getInputValues } from "@/lib/actions/common"; import { updateContributionState } from "./updateContributionState"; export function setComponentVisibility( @@ -45,20 +45,19 @@ function getLayoutInputValues( contribPoint: ContribPoint, contribIndex: number, ): unknown[] { - const { contributionModelsRecord } = store.getState(); + const { configuration, contributionModelsRecord, contributionStatesRecord } = + store.getState(); const contributionModels = contributionModelsRecord[contribPoint]; + const contributionStates = contributionStatesRecord[contribPoint]; const contributionModel = contributionModels[contribIndex]; + const contributionState = contributionStates[contribIndex]; const inputs = contributionModel.layout!.inputs; if (inputs && inputs.length > 0) { - return inputs.map((input: Input) => { - if (!input.kind || input.kind === "Component") { - console.warn(`input kind not supported in layout:`, input); - } else { - // TODO: Get value from another kind of input. - console.warn(`input kind not supported yet:`, input); - } - return null; - }); + return getInputValues( + inputs, + contributionState, + configuration.hostStore?.getState, + ); } else { return []; } diff --git a/dashipy/dashipy/callback.py b/dashipy/dashipy/callback.py index fedfdc12..a4d67c4e 100644 --- a/dashipy/dashipy/callback.py +++ b/dashipy/dashipy/callback.py @@ -3,7 +3,6 @@ from abc import ABC from typing import Callable, Any, Literal -from .component import Component ComponentKind = Literal["Component"] AppStateKind = Literal["AppState"] @@ -15,18 +14,23 @@ class InputOutput(ABC): # noinspection PyShadowingBuiltins def __init__( self, - id: str, + id: str | None = None, property: str | None = None, kind: InputOutputKind | None = None, ): - property = "value" if property is None else property kind = "Component" if kind is None else kind - assert id is None or (isinstance(id, str) and id != "") - assert isinstance(property, str) and property != "" assert kind in ("AppState", "State", "Component") + if kind == "Component": + assert id is not None and isinstance(id, str) and id != "" + property = "value" if property is None else property + assert isinstance(property, str) and property != "" + else: + assert id is None or (isinstance(id, str) and id != "") + assert property is None or (isinstance(property, str) and property != "") + + self.kind = kind self.id = id self.property = property - self.kind = kind def to_dict(self) -> dict[str, Any]: return { @@ -155,12 +159,17 @@ def make_function_args( num_values = len(values) delta = num_inputs - num_values if delta != 0: - raise TypeError( - f"too {'few' if delta < 0 else 'many'} input values" + message = ( + f"too {'few' if delta > 0 else 'many'} input values" f" given for function {self.function.__qualname__!r}:" f" expected {num_inputs}," f" but got {num_values}" ) + if delta > 0: + values = (*values, *(delta * (None,))) + print(f"WARNING: {message}") # TODO use logging + else: + raise TypeError(message) param_names = self.param_names[1:] args = [context] @@ -204,10 +213,7 @@ def _parameter_to_dict(parameter: inspect.Parameter) -> dict[str, Any]: "list[Component]": "Component[]", } -_object_types = { - "Component": "Component", - "Chart": "Chart" -} +_object_types = {"Component": "Component", "Chart": "Chart"} def _annotation_to_str(annotation: Any) -> str | list[str]: diff --git a/dashipy/dashipy/contribution.py b/dashipy/dashipy/contribution.py index 43a20cee..f13a7a14 100644 --- a/dashipy/dashipy/contribution.py +++ b/dashipy/dashipy/contribution.py @@ -51,4 +51,3 @@ def decorator(function: Callable) -> Callable: def __str__(self): return self.name - diff --git a/dashipy/dashipy/demo/context.py b/dashipy/dashipy/demo/context.py index 883ea580..13355365 100644 --- a/dashipy/dashipy/demo/context.py +++ b/dashipy/dashipy/demo/context.py @@ -1,16 +1,23 @@ -from typing import Union import pandas as pd class Context: def __init__(self): - self.datasets= { - 0: pd.DataFrame({ - 'a': ['A', 'B', 'C', 'D', 'E'], - 'b': [28, 55, 43, 91, 81] - }), - 1: pd.DataFrame({ - 'a': ['V', 'W', 'X', 'Y', 'Z'], - 'b': [99, 1, 7, 43, 49] - }) + self.datasets = { + "ds0": pd.DataFrame( + { + "x": ["A", "B", "C", "D", "E"], + "a": [28, 55, 43, 91, 81], + "b": [50, 32, 56, 44, 8], + "c": [50, 40, 30, 20, 10], + } + ), + "ds1": pd.DataFrame( + { + "x": ["V", "W", "X", "Y", "Z"], + "u": [99, 1, 7, 43, 49], + "v": [23, 35, 45, 39, 18], + "w": [10, 0, 30, 35, 40], + } + ), } diff --git a/dashipy/dashipy/demo/contribs/panel.py b/dashipy/dashipy/demo/contribs/panel.py index 92c104b9..64019f96 100644 --- a/dashipy/dashipy/demo/contribs/panel.py +++ b/dashipy/dashipy/demo/contribs/panel.py @@ -3,5 +3,6 @@ class Panel(Contribution): """Panel contribution""" + def __init__(self, name: str, title: str | None = None): super().__init__(name, title=title) diff --git a/dashipy/my_extension/my_panel_1.py b/dashipy/my_extension/my_panel_1.py index 552855be..3993e063 100644 --- a/dashipy/my_extension/my_panel_1.py +++ b/dashipy/my_extension/my_panel_1.py @@ -1,7 +1,7 @@ import altair as alt -from dashipy import (Component, Input, Output) -from dashipy.components import (Plot, Box, Dropdown) +from dashipy import Component, Input, Output +from dashipy.components import Plot, Box, Dropdown from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -48,7 +48,10 @@ def render_panel(ctx: Context) -> Component: Output("plot", "chart"), ) def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: - dataset = ctx.datasets[selected_dataset] + dataset_key = tuple(ctx.datasets.keys())[selected_dataset] + dataset = ctx.datasets[dataset_key] + + variable_name = "a" if selected_dataset == 0 else "u" # Create a slider corner_slider = alt.binding_range(min=0, max=50, step=1) @@ -56,22 +59,25 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: corner_var = alt.param(bind=corner_slider, value=0, name="cornerRadius") # Create another parameter to handle the click events and send the data as # specified in the fields - click_param = alt.selection_point(on="click", name="onClick", - fields=["a", "b"]) + click_param = alt.selection_point( + on="click", name="onClick", fields=["x", variable_name] + ) # Create a chart type using mark_* where * could be any kind of chart # supported by Vega. We can add properties and parameters as shown below. - chart = alt.Chart(dataset).mark_bar(cornerRadius=corner_var).encode( - x=alt.X('a:N', title='a'), - y=alt.Y('b:Q', title='b'), - tooltip=[ - alt.Tooltip('a:N'), - alt.Tooltip('b:Q'), - ], - color='b:Q', - ).properties( - width=300, - height=300, - title="Vega charts" - ).add_params(corner_var, click_param) + chart = ( + alt.Chart(dataset) + .mark_bar(cornerRadius=corner_var) + .encode( + x=alt.X("x:N", title="x"), + y=alt.Y(f"{variable_name}:Q", title=variable_name), + tooltip=[ + alt.Tooltip("x:N"), + alt.Tooltip(f"{variable_name}:Q"), + ], + color=f"{variable_name}:Q", + ) + .properties(width=290, height=300, title="Vega charts") + .add_params(corner_var, click_param) + ) return chart diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index 95c6b30c..ab94f50a 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -1,7 +1,7 @@ import altair as alt -from dashipy import (Component, Input, Output) -from dashipy.components import (Plot, Box, Dropdown) +from dashipy import Component, Input, Output +from dashipy.components import Plot, Box, Dropdown from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -9,17 +9,30 @@ panel = Panel(__name__, title="Panel B") -@panel.layout() -def render_panel(ctx: Context) -> Component: - selected_dataset: int = 0 +@panel.layout(Input(kind="AppState", property="selectedDatasetId")) +def render_panel( + ctx: Context, + selected_dataset_id: str = None, +) -> Component: + + dataset = ctx.datasets.get(selected_dataset_id) + if dataset is not None: + variable_names = [v for v in dataset.keys() if v != "x"] + selected_variable_name = variable_names[0] + else: + variable_names = [] + selected_variable_name = None + plot = Plot( - id="plot", chart=make_figure(ctx, selected_dataset), style={"flexGrow": 1} + id="plot", + chart=make_figure(ctx, selected_dataset_id, selected_variable_name), + style={"flexGrow": 1}, ) dropdown = Dropdown( - id="selected_dataset", - value=selected_dataset, - label="Dataset", - options=[(f"DS #{i + 1}", i) for i in range(len(ctx.datasets))], + id="selected_variable_name", + value=selected_variable_name, + label="Variable", + options=[(variable_name, variable_name) for variable_name in variable_names], style={"flexGrow": 0, "minWidth": 120}, ) control_group = Box( @@ -44,32 +57,36 @@ def render_panel(ctx: Context) -> Component: @panel.callback( - Input("selected_dataset"), + Input(kind="AppState", property="selectedDatasetId"), + Input("selected_variable_name"), Output("plot", "chart"), ) -def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: - dataset = ctx.datasets[selected_dataset] - slider = alt.binding_range(min=0, max=100, step=1, name='Cutoff ') - selector = alt.param(name='SelectorName', value=50, bind=slider) +def make_figure( + ctx: Context, selected_dataset_id: str = None, selected_variable_name: str = None +) -> alt.Chart: + dataset = ctx.datasets.get(selected_dataset_id) + + slider = alt.binding_range(min=0, max=100, step=1, name="Cutoff ") + selector = alt.param(name="SelectorName", value=50, bind=slider) # Almost same as the chart in Panel 1, but here we use the Shorthand # notation for setting x,y and the tooltip, although they both give the # same output. We also call interactive() on this chart object which allows # to zoom in and out as well as move the chart around. - chart = alt.Chart(dataset).mark_bar().encode( - x='a:N', - y='b:Q', - tooltip=['a:N','b:Q'], - color = alt.condition( - 'datum.b < SelectorName', - alt.value('green'), - alt.value('yellow') + chart = ( + alt.Chart(dataset) + .mark_bar() + .encode( + x="x:N", + y=f"{selected_variable_name}:Q", + tooltip=["x:N", f"{selected_variable_name}:Q"], + color=alt.condition( + f"datum.{selected_variable_name} < SelectorName", + alt.value("green"), + alt.value("yellow"), + ), ) - ).properties( - width=300, - height=300, - title="Vega charts using Shorthand syntax" - ).add_params( - selector - ).interactive() + .properties(width=300, height=300, title="Vega charts using Shorthand syntax") + .add_params(selector) + .interactive() + ) return chart - From 656c0e8fa986ae6d1f7f6cd5e5917678c3ce35b7 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 29 Oct 2024 19:37:05 +0100 Subject: [PATCH 06/38] integrated "ContributionModel" into "ContributionState" --- dashi/src/demo/actions/hidePanel.ts | 5 +- dashi/src/demo/actions/showPanel.ts | 5 +- dashi/src/demo/components/Panel.tsx | 19 ++--- dashi/src/demo/components/PanelsControl.tsx | 11 +-- dashi/src/demo/components/PanelsRow.tsx | 34 +++----- dashi/src/demo/hooks.ts | 7 ++ dashi/src/demo/types.ts | 4 + .../lib/actions/applyPropertyChange.test.ts | 8 +- .../lib/actions/initializeContributions.ts | 5 +- .../src/lib/actions/setComponentVisibility.ts | 64 -------------- .../lib/actions/updateContributionState.ts | 83 +++++++++++++++++-- dashi/src/lib/index.ts | 2 - dashi/src/lib/types/model/contribution.ts | 7 +- dashi/src/lib/types/state/contribution.ts | 13 +-- dashipy/dashipy/contribution.py | 8 +- 15 files changed, 138 insertions(+), 137 deletions(-) create mode 100644 dashi/src/demo/hooks.ts create mode 100644 dashi/src/demo/types.ts delete mode 100644 dashi/src/lib/actions/setComponentVisibility.ts diff --git a/dashi/src/demo/actions/hidePanel.ts b/dashi/src/demo/actions/hidePanel.ts index 08740b05..ffb11113 100644 --- a/dashi/src/demo/actions/hidePanel.ts +++ b/dashi/src/demo/actions/hidePanel.ts @@ -1,5 +1,6 @@ -import { setComponentVisibility } from "@/lib"; +import { updateContributionState } from "@/lib"; +import type { PanelState } from "@/demo/types"; export function hidePanel(panelIndex: number) { - setComponentVisibility("panels", panelIndex, false); + updateContributionState("panels", panelIndex, { visible: false }); } diff --git a/dashi/src/demo/actions/showPanel.ts b/dashi/src/demo/actions/showPanel.ts index d17074e0..39190ce2 100644 --- a/dashi/src/demo/actions/showPanel.ts +++ b/dashi/src/demo/actions/showPanel.ts @@ -1,5 +1,6 @@ -import { setComponentVisibility } from "@/lib"; +import { updateContributionState } from "@/lib"; +import type { PanelState } from "@/demo/types"; export function showPanel(panelIndex: number) { - setComponentVisibility("panels", panelIndex, true); + updateContributionState("panels", panelIndex, { visible: true }); } diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 804c554d..ce07258e 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -1,12 +1,12 @@ -import { type CSSProperties, type ReactElement } from "react"; +import type { CSSProperties, ReactElement } from "react"; import CircularProgress from "@mui/material/CircularProgress"; import { - type Contribution, - type ContributionState, type PropertyChangeHandler, + type ContributionState, DashiComponent, } from "@/lib"; +import type { PanelState } from "@/demo/types"; const panelContainerStyle: CSSProperties = { display: "flex", @@ -33,13 +33,12 @@ const panelContentStyle: CSSProperties = { }; interface PanelProps { - panelModel: Contribution; - panelState: ContributionState; + panelState: ContributionState; onPropertyChange: PropertyChangeHandler; } -function Panel({ panelModel, panelState, onPropertyChange }: PanelProps) { - if (!panelState.visible) { +function Panel({ panelState, onPropertyChange }: PanelProps) { + if (!panelState.state.visible) { return null; } const componentState = panelState.componentState; @@ -52,20 +51,20 @@ function Panel({ panelModel, panelState, onPropertyChange }: PanelProps) { } else if (componentModelResult.error) { panelElement = ( - Error loading {panelModel.name}: {componentModelResult.error.message} + Error loading {panelState.name}: {componentModelResult.error.message} ); } else if (componentModelResult.status === "pending") { panelElement = ( Loading{" "} - {panelModel.name}... + {panelState.name}... ); } return (
-
{panelState.title}
+
{panelState.state.title}
{panelElement}
); diff --git a/dashi/src/demo/components/PanelsControl.tsx b/dashi/src/demo/components/PanelsControl.tsx index 6bd67347..9214b7de 100644 --- a/dashi/src/demo/components/PanelsControl.tsx +++ b/dashi/src/demo/components/PanelsControl.tsx @@ -2,15 +2,12 @@ import Checkbox from "@mui/material/Checkbox"; import FormControlLabel from "@mui/material/FormControlLabel"; import FormGroup from "@mui/material/FormGroup"; -import { useContributionStatesRecord } from "@/lib"; import { hidePanel } from "@/demo/actions/hidePanel"; import { showPanel } from "@/demo/actions/showPanel"; - -const contribPoint = "panels"; +import { usePanelStates } from "@/demo/hooks"; function PanelsControl() { - const contributionStatesRecord = useContributionStatesRecord(); - const panelStates = contributionStatesRecord[contribPoint]; + const panelStates = usePanelStates(); if (!panelStates) { // Ok, not ready yet return null; @@ -23,12 +20,12 @@ function PanelsControl() { return ( { if (e.currentTarget.checked) { diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index d0e0c2f1..a6153893 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -1,44 +1,30 @@ -import React from "react"; +import type { JSX } from "react"; -import { - type PropertyChangeEvent, - applyPropertyChange, - useContributionModelsRecord, - useContributionStatesRecord, -} from "@/lib"; +import { type PropertyChangeEvent, applyPropertyChange } from "@/lib"; +import { usePanelStates } from "@/demo/hooks"; import Panel from "./Panel"; -const contribPoint = "panels"; - function PanelsRow() { - const contributionModelsRecord = useContributionModelsRecord(); - const contributionStatesRecord = useContributionStatesRecord(); - const panelModels = contributionModelsRecord[contribPoint]; - const panelStates = contributionStatesRecord[contribPoint]; - if (!panelModels || !panelStates) { + const panelStates = usePanelStates(); + if (!panelStates) { // Ok, not ready yet return null; } - // TODO: assert panelModels.length === panelStates.length - if (panelModels.length != panelStates?.length) { - throw Error("internal state error"); - } - const handlePropertyChange = ( + const handlePanelPropertyChange = ( panelIndex: number, panelEvent: PropertyChangeEvent, ) => { - applyPropertyChange(contribPoint, panelIndex, panelEvent); + applyPropertyChange("panels", panelIndex, panelEvent); }; - const visiblePanels: React.JSX.Element[] = []; + const visiblePanels: JSX.Element[] = []; panelStates.forEach((panelState, panelIndex) => { - if (panelState.visible) { + if (panelState.state.visible) { visiblePanels.push( handlePropertyChange(panelIndex, e)} + onPropertyChange={(e) => handlePanelPropertyChange(panelIndex, e)} />, ); } diff --git a/dashi/src/demo/hooks.ts b/dashi/src/demo/hooks.ts new file mode 100644 index 00000000..4a5b8c74 --- /dev/null +++ b/dashi/src/demo/hooks.ts @@ -0,0 +1,7 @@ +import { type ContributionState, useContributionStatesRecord } from "@/lib"; +import type { PanelState } from "@/demo/types"; + +export function usePanelStates() { + const contributionStatesRecord = useContributionStatesRecord(); + return contributionStatesRecord["panels"] as ContributionState[]; +} diff --git a/dashi/src/demo/types.ts b/dashi/src/demo/types.ts new file mode 100644 index 00000000..93b07386 --- /dev/null +++ b/dashi/src/demo/types.ts @@ -0,0 +1,4 @@ +export interface PanelState { + title: string; + visible: boolean; +} diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index f7e38125..905f3c0f 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -31,7 +31,13 @@ const componentState: ComponentState = { describe("Test that applyContributionChangeRequests()", () => { const contributionStatesRecord: Record = { panels: [ - { componentStateResult: { status: "ok" }, visible: true, componentState }, + { + name: "", + extension: "", + componentStateResult: { status: "ok" }, + state: { visible: true }, + componentState, + }, ], }; diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index f9a16343..bdd6688b 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -25,8 +25,8 @@ export function initializeContributions(options?: FrameworkOptions) { contributionModelsRecord[contribPoint]; contributionStatesRecord[contribPoint] = contributionModels.map( (contribution) => ({ - title: contribution.title, - visible: contribution.visible, + ...contribution, + state: { ...contribution.initialState }, componentStateResult: {}, }), ); @@ -35,7 +35,6 @@ export function initializeContributions(options?: FrameworkOptions) { store.setState({ contributionsResult, extensions, - contributionModelsRecord, contributionStatesRecord, }); }); diff --git a/dashi/src/lib/actions/setComponentVisibility.ts b/dashi/src/lib/actions/setComponentVisibility.ts deleted file mode 100644 index d00ec02c..00000000 --- a/dashi/src/lib/actions/setComponentVisibility.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { store } from "@/lib/store"; -import { fetchInitialComponentState } from "@/lib/api"; -import { type ContribPoint } from "@/lib/types/model/extension"; -import { fetchApiResult } from "@/lib/utils/fetchApiResult"; -import { getInputValues } from "@/lib/actions/common"; -import { updateContributionState } from "./updateContributionState"; - -export function setComponentVisibility( - contribPoint: ContribPoint, - panelIndex: number, - visible: boolean, -) { - const { configuration, contributionStatesRecord } = store.getState(); - const contributionStates = contributionStatesRecord[contribPoint]; - const contributionState = contributionStates[panelIndex]; - if (contributionState.visible === visible) { - return; // nothing to do - } - if (contributionState.componentStateResult.status) { - updateContributionState(contribPoint, panelIndex, { visible }); - } else { - // No status yet, so we must load the component - updateContributionState(contribPoint, panelIndex, { - visible, - componentStateResult: { status: "pending" }, - }); - const inputValues = getLayoutInputValues(contribPoint, panelIndex); - fetchApiResult( - fetchInitialComponentState, - contribPoint, - panelIndex, - inputValues, - configuration.api, - ).then((componentModelResult) => { - const componentState = componentModelResult?.data; - updateContributionState(contribPoint, panelIndex, { - componentStateResult: componentModelResult, - componentState, - }); - }); - } -} - -function getLayoutInputValues( - contribPoint: ContribPoint, - contribIndex: number, -): unknown[] { - const { configuration, contributionModelsRecord, contributionStatesRecord } = - store.getState(); - const contributionModels = contributionModelsRecord[contribPoint]; - const contributionStates = contributionStatesRecord[contribPoint]; - const contributionModel = contributionModels[contribIndex]; - const contributionState = contributionStates[contribIndex]; - const inputs = contributionModel.layout!.inputs; - if (inputs && inputs.length > 0) { - return getInputValues( - inputs, - contributionState, - configuration.hostStore?.getState, - ); - } else { - return []; - } -} diff --git a/dashi/src/lib/actions/updateContributionState.ts b/dashi/src/lib/actions/updateContributionState.ts index ee762547..659d7849 100644 --- a/dashi/src/lib/actions/updateContributionState.ts +++ b/dashi/src/lib/actions/updateContributionState.ts @@ -1,22 +1,91 @@ import { store } from "@/lib/store"; +import { fetchInitialComponentState } from "@/lib/api"; +import { fetchApiResult } from "@/lib/utils/fetchApiResult"; +import { getInputValues } from "@/lib/actions/common"; import { updateArray } from "@/lib/utils/updateArray"; -import { type ContributionState } from "@/lib/types/state/contribution"; -import { type ContribPoint } from "@/lib/types/model/extension"; +import type { ContribPoint } from "@/lib/types/model/extension"; +import type { ContributionState } from "@/lib/types/state/contribution"; -export function updateContributionState( +export function updateContributionState( contribPoint: ContribPoint, - panelIndex: number, - panelState: Partial, + contribIndex: number, + contribState: Partial, + requireComponent: boolean = true, +) { + const { configuration, contributionStatesRecord } = store.getState(); + const contributionStates = contributionStatesRecord[contribPoint]; + const contributionState = contributionStates[contribIndex]; + if (contributionState.state === contribState) { + return; // nothing to do + } + const componentStatus = contributionState.componentStateResult.status; + if (!requireComponent || componentStatus) { + _updateContributionState(contribPoint, contribIndex, { + state: contribState, + }); + } else if (!componentStatus) { + // No status yet, so we must load the component + _updateContributionState(contribPoint, contribIndex, { + state: contribState, + componentStateResult: { status: "pending" }, + }); + const inputValues = getLayoutInputValues(contribPoint, contribIndex); + fetchApiResult( + fetchInitialComponentState, + contribPoint, + contribIndex, + inputValues, + configuration.api, + ).then((componentModelResult) => { + const componentState = componentModelResult?.data; + _updateContributionState(contribPoint, contribIndex, { + componentStateResult: componentModelResult, + componentState, + }); + }); + } +} + +function getLayoutInputValues( + contribPoint: ContribPoint, + contribIndex: number, +): unknown[] { + const { configuration, contributionStatesRecord } = store.getState(); + const contributionStates = contributionStatesRecord[contribPoint]; + const contributionState = contributionStates[contribIndex]; + const inputs = contributionState.layout!.inputs; + if (inputs && inputs.length > 0) { + return getInputValues( + inputs, + contributionState, + configuration.hostStore?.getState, + ); + } else { + return []; + } +} + +function _updateContributionState( + contribPoint: ContribPoint, + contribIndex: number, + contribState: Partial, ) { const { contributionStatesRecord } = store.getState(); const contribStates = contributionStatesRecord[contribPoint]!; + const contribStateOld = contribStates[contribIndex]; + const contribStateNew = contribState.state + ? { + ...contribState, + state: { ...contribStateOld.state, ...contribState.state }, + } + : contribState; store.setState({ contributionStatesRecord: { ...contributionStatesRecord, [contribPoint]: updateArray( contribStates, - panelIndex, - panelState, + contribIndex, + contribStateNew, ), }, }); diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index 0d6fdec0..b0a65437 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -9,7 +9,6 @@ export { export { initializeContributions } from "@/lib/actions/initializeContributions"; export { configureFramework } from "@/lib/actions/configureFramework"; export { applyPropertyChange } from "@/lib/actions/applyPropertyChange"; -export { setComponentVisibility } from "@/lib/actions/setComponentVisibility"; export { updateContributionState } from "@/lib/actions/updateContributionState"; // React Components export { DashiComponent } from "@/lib/components/DashiComponent"; @@ -18,7 +17,6 @@ export { useStore, useExtensions, useContributionsResult, - useContributionModelsRecord, useContributionStatesRecord, } from "@/lib/hooks"; // Utilities diff --git a/dashi/src/lib/types/model/contribution.ts b/dashi/src/lib/types/model/contribution.ts index 78f8b416..ef10bf60 100644 --- a/dashi/src/lib/types/model/contribution.ts +++ b/dashi/src/lib/types/model/contribution.ts @@ -1,12 +1,9 @@ import { type Callback } from "./callback"; -export interface Contribution { +export interface Contribution { name: string; extension: string; layout?: Callback; callbacks?: Callback[]; - // The following properties will become the - // initial contribution state - title?: string; - visible?: boolean; + initialState?: S; } diff --git a/dashi/src/lib/types/state/contribution.ts b/dashi/src/lib/types/state/contribution.ts index f19bbcaa..04baa45b 100644 --- a/dashi/src/lib/types/state/contribution.ts +++ b/dashi/src/lib/types/state/contribution.ts @@ -1,10 +1,11 @@ -import { type ApiResult } from "@/lib/utils/fetchApiResult"; -import { type ComponentState } from "./component"; +import type { ApiResult } from "@/lib/utils/fetchApiResult"; +import type { Contribution } from "@/lib/types/model/contribution"; +import type { ComponentState } from "./component"; -export interface ContributionState { - title?: string; - visible?: boolean; +export interface ContributionState + extends Contribution { + state: S; componentStateResult: ApiResult; - // componentStateResult.data + // The value of componentStateResult.data, once it is available componentState?: ComponentState; } diff --git a/dashipy/dashipy/contribution.py b/dashipy/dashipy/contribution.py index f13a7a14..79a797bd 100644 --- a/dashipy/dashipy/contribution.py +++ b/dashipy/dashipy/contribution.py @@ -6,17 +6,17 @@ class Contribution(ABC): # noinspection PyShadowingBuiltins - def __init__(self, name: str, title: str | None = None): + def __init__(self, name: str, **initial_state): self.name = name - self.title = title + self.initial_state = initial_state self.extension: str | None = None self.layout_callback: Callback | None = None self.callbacks: list[Callback] = [] def to_dict(self) -> dict[str, Any]: d = dict(name=self.name) - if self.title is not None: - d.update(title=self.title) + if self.initial_state is not None: + d.update(initialState=self.initial_state) if self.extension is not None: d.update(extension=self.extension) if self.layout_callback is not None: From 7e01da44f22ffcb6cf9da4d04292a0687f42cb89 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 29 Oct 2024 19:38:23 +0100 Subject: [PATCH 07/38] integrated "ContributionModel" into "ContributionState" --- dashi/src/lib/actions/applyPropertyChange.ts | 10 ++-------- dashi/src/lib/hooks.ts | 2 -- dashi/src/lib/types/state/store.ts | 3 --- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index ba49860e..dcee65a9 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -14,7 +14,6 @@ import { type StateChangeRequest, } from "@/lib/types/model/callback"; import { type PropertyChangeEvent } from "@/lib/types/model/event"; -import { type Contribution } from "@/lib/types/model/contribution"; import { type ContributionState } from "@/lib/types/state/contribution"; import { updateArray } from "@/lib/utils/updateArray"; import { isContainerState } from "@/lib/utils/isContainerState"; @@ -25,14 +24,10 @@ export function applyPropertyChange( contribIndex: number, contribEvent: PropertyChangeEvent, ) { - const { configuration, contributionModelsRecord, contributionStatesRecord } = - store.getState(); - const contributionModels = contributionModelsRecord[contribPoint]; + const { configuration, contributionStatesRecord } = store.getState(); const contributionStates = contributionStatesRecord[contribPoint]; - const contributionModel = contributionModels[contribIndex]; const contributionState = contributionStates[contribIndex]; const callbackRefs = generateCallbackRefs( - contributionModel, contributionState, contribEvent, store.getState().configuration.hostStore?.getState, @@ -87,7 +82,6 @@ export function applyPropertyChange( } function generateCallbackRefs( - contributionModel: Contribution, contributionState: ContributionState, contribEvent: PropertyChangeEvent, getHostState?: () => unknown, @@ -95,7 +89,7 @@ function generateCallbackRefs( const callbackRefs: CallbackRef[] = []; // Prepare calling all callbacks of the contribution // that are triggered by the property change - (contributionModel.callbacks || []).forEach((callback, callbackIndex) => { + (contributionState.callbacks || []).forEach((callback, callbackIndex) => { const inputValues = getCallbackInputValues( contributionState, contribEvent, diff --git a/dashi/src/lib/hooks.ts b/dashi/src/lib/hooks.ts index 6f05cfc3..c08d9540 100644 --- a/dashi/src/lib/hooks.ts +++ b/dashi/src/lib/hooks.ts @@ -6,5 +6,3 @@ export const useContributionsResult = () => useStore((state) => state.contributionsResult); export const useContributionStatesRecord = () => useStore((state) => state.contributionStatesRecord); -export const useContributionModelsRecord = () => - useStore((state) => state.contributionModelsRecord); diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index c68ad50a..1fc6542f 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -3,7 +3,6 @@ import type { Contributions, Extension, } from "@/lib/types/model/extension"; -import type { Contribution } from "@/lib/types/model/contribution"; import type { ApiResult } from "@/lib/utils/fetchApiResult"; import type { ContributionState } from "@/lib/types/state/contribution"; import type { ApiOptions } from "@/lib/api"; @@ -26,8 +25,6 @@ export interface StoreState { contributionsResult: ApiResult; /** All extensions */ extensions: Extension[]; - /** A record that maps contribPoint --> Contribution[].*/ - contributionModelsRecord: Record; /** A record that maps contribPoint --> ContributionState[].*/ contributionStatesRecord: Record; } From f38e95869d17dad79876a15ae5cd3fd56e436d7d Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 29 Oct 2024 19:46:57 +0100 Subject: [PATCH 08/38] integrated "ContributionModel" into "ContributionState" --- dashi/src/lib/store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dashi/src/lib/store.ts b/dashi/src/lib/store.ts index 5fc9a3e3..ac5d0ff6 100644 --- a/dashi/src/lib/store.ts +++ b/dashi/src/lib/store.ts @@ -6,6 +6,5 @@ export const store = create(() => ({ configuration: {}, contributionsResult: {}, extensions: [], - contributionModelsRecord: {}, contributionStatesRecord: {}, })); From 0f9203e4cf8590a6c743ef511528277cbdf4260f Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 30 Oct 2024 10:09:13 +0100 Subject: [PATCH 09/38] renamed "contributionStatesRecord" --> "contributionsRecord" --- dashi/src/demo/hooks.ts | 4 +- .../lib/actions/applyPropertyChange.test.ts | 10 +-- dashi/src/lib/actions/applyPropertyChange.ts | 77 +++++++++---------- .../lib/actions/initializeContributions.ts | 24 +++--- .../lib/actions/updateContributionState.ts | 16 ++-- dashi/src/lib/hooks.ts | 4 +- dashi/src/lib/index.ts | 2 +- dashi/src/lib/store.ts | 4 +- dashi/src/lib/types/state/store.ts | 2 +- 9 files changed, 71 insertions(+), 72 deletions(-) diff --git a/dashi/src/demo/hooks.ts b/dashi/src/demo/hooks.ts index 4a5b8c74..4f9bb735 100644 --- a/dashi/src/demo/hooks.ts +++ b/dashi/src/demo/hooks.ts @@ -1,7 +1,7 @@ -import { type ContributionState, useContributionStatesRecord } from "@/lib"; +import { type ContributionState, useContributionsRecord } from "@/lib"; import type { PanelState } from "@/demo/types"; export function usePanelStates() { - const contributionStatesRecord = useContributionStatesRecord(); + const contributionStatesRecord = useContributionsRecord(); return contributionStatesRecord["panels"] as ContributionState[]; } diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index 905f3c0f..8b544615 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -29,7 +29,7 @@ const componentState: ComponentState = { }; describe("Test that applyContributionChangeRequests()", () => { - const contributionStatesRecord: Record = { + const contributionsRecord: Record = { panels: [ { name: "", @@ -68,20 +68,20 @@ describe("Test that applyContributionChangeRequests()", () => { }; it("changes state if values are different", () => { - const newState = applyContributionChangeRequests(contributionStatesRecord, [ + const newState = applyContributionChangeRequests(contributionsRecord, [ stateChangeRequest1, ]); - expect(newState).not.toBe(contributionStatesRecord); + expect(newState).not.toBe(contributionsRecord); expect( newState["panels"][0].componentState!.components![1].components![1].value, ).toEqual(14); }); it("doesn't change the state if value stays the same", () => { - const newState = applyContributionChangeRequests(contributionStatesRecord, [ + const newState = applyContributionChangeRequests(contributionsRecord, [ stateChangeRequest2, ]); - expect(newState).toBe(contributionStatesRecord); + expect(newState).toBe(contributionsRecord); }); }); diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index dcee65a9..7b4335de 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -24,11 +24,11 @@ export function applyPropertyChange( contribIndex: number, contribEvent: PropertyChangeEvent, ) { - const { configuration, contributionStatesRecord } = store.getState(); - const contributionStates = contributionStatesRecord[contribPoint]; - const contributionState = contributionStates[contribIndex]; + const { configuration, contributionsRecord } = store.getState(); + const contributions = contributionsRecord[contribPoint]; + const contribution = contributions[contribIndex]; const callbackRefs = generateCallbackRefs( - contributionState, + contribution, contribEvent, store.getState().configuration.hostStore?.getState, ); @@ -82,16 +82,16 @@ export function applyPropertyChange( } function generateCallbackRefs( - contributionState: ContributionState, + contribution: ContributionState, contribEvent: PropertyChangeEvent, getHostState?: () => unknown, ): CallbackRef[] { const callbackRefs: CallbackRef[] = []; // Prepare calling all callbacks of the contribution // that are triggered by the property change - (contributionState.callbacks || []).forEach((callback, callbackIndex) => { + (contribution.callbacks || []).forEach((callback, callbackIndex) => { const inputValues = getCallbackInputValues( - contributionState, + contribution, contribEvent, callback, getHostState, @@ -105,7 +105,7 @@ function generateCallbackRefs( // we export for testing only export function getCallbackInputValues( - contributionState: ContributionState, + contribution: ContributionState, contribEvent: PropertyChangeEvent, callback: Callback, getHostState?: () => unknown, @@ -129,7 +129,7 @@ export function getCallbackInputValues( const inputValues = getInputValues( callback.inputs, - contributionState, + contribution, getHostState, ); @@ -143,14 +143,14 @@ export function getCallbackInputValues( } function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { - const { contributionStatesRecord } = store.getState(); - const contributionStatesRecordNew = applyContributionChangeRequests( - contributionStatesRecord, + const { contributionsRecord } = store.getState(); + const contributionsRecordNew = applyContributionChangeRequests( + contributionsRecord, stateChangeRequests, ); - if (contributionStatesRecordNew !== contributionStatesRecord) { + if (contributionsRecordNew !== contributionsRecord) { store.setState({ - contributionStatesRecord: contributionStatesRecordNew, + contributionsRecord: contributionsRecordNew, }); } // TODO: Set value of another kind of output. @@ -158,14 +158,13 @@ function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { // we export for testing only export function applyContributionChangeRequests( - contributionStatesRecord: Record, + contributionsRecord: Record, stateChangeRequests: StateChangeRequest[], ): Record { stateChangeRequests.forEach( ({ contribPoint, contribIndex, stateChanges }) => { - const contributionState = - contributionStatesRecord[contribPoint][contribIndex]; - const componentStateOld = contributionState.componentState; + const contribution = contributionsRecord[contribPoint][contribIndex]; + const componentStateOld = contribution.componentState; let componentState = componentStateOld; if (componentState) { stateChanges @@ -180,51 +179,51 @@ export function applyContributionChangeRequests( ); }); if (componentState !== componentStateOld) { - contributionStatesRecord = { - ...contributionStatesRecord, + contributionsRecord = { + ...contributionsRecord, [contribPoint]: updateArray( - contributionStatesRecord[contribPoint], + contributionsRecord[contribPoint], contribIndex, - { ...contributionState, componentState }, + { ...contribution, componentState }, ), }; } } }, ); - return contributionStatesRecord; + return contributionsRecord; } // we export for testing only export function applyComponentStateChange( - componentState: ComponentState, + component: ComponentState, stateChange: StateChange, ): ComponentState { - if (componentState.id === stateChange.id) { + if (component.id === stateChange.id) { const property = stateChange.property || "value"; - const oldValue = (componentState as unknown as Record)[ + const oldValue = (component as unknown as Record)[ property ]; if (oldValue !== stateChange.value) { - return { ...componentState, [property]: stateChange.value }; + return { ...component, [property]: stateChange.value }; } - } else if (isContainerState(componentState)) { - const containerStateOld: ContainerState = componentState; - let containerStateNew: ContainerState = containerStateOld; - for (let i = 0; i < containerStateOld.components.length; i++) { - const itemOld = containerStateOld.components[i]; + } else if (isContainerState(component)) { + const containerOld: ContainerState = component; + let containerNew: ContainerState = containerOld; + for (let i = 0; i < containerOld.components.length; i++) { + const itemOld = containerOld.components[i]; const itemNew = applyComponentStateChange(itemOld, stateChange); if (itemNew !== itemOld) { - if (containerStateNew === containerStateOld) { - containerStateNew = { - ...containerStateOld, - components: [...containerStateOld.components], + if (containerNew === containerOld) { + containerNew = { + ...containerOld, + components: [...containerOld.components], }; } - containerStateNew.components[i] = itemNew; + containerNew.components[i] = itemNew; } } - return containerStateNew; + return containerNew; } - return componentState; + return component; } diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index bdd6688b..57ea44a8 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -15,27 +15,27 @@ export function initializeContributions(options?: FrameworkOptions) { store.setState({ contributionsResult: { status: "pending" } }); fetchApiResult(fetchContributions, apiOptions).then((contributionsResult) => { // TODO: validate contributionsResult and contributionsResult.data - const { extensions, contributions: contributionModelsRecord } = + const { extensions, contributions: rawContributionsRecord } = contributionsResult.data!; - const contributionStatesRecord: Record = - {}; - Object.getOwnPropertyNames(contributionModelsRecord).forEach( + const contributionsRecord: Record = {}; + Object.getOwnPropertyNames(rawContributionsRecord).forEach( (contribPoint: ContribPoint) => { - const contributionModels: Contribution[] = - contributionModelsRecord[contribPoint]; - contributionStatesRecord[contribPoint] = contributionModels.map( - (contribution) => ({ - ...contribution, - state: { ...contribution.initialState }, + const rawContributions: Contribution[] = + rawContributionsRecord[contribPoint]; + contributionsRecord[contribPoint] = rawContributions.map( + // Contribution --> ContributionState + (rawContribution) => ({ + ...rawContribution, + state: { ...rawContribution.initialState }, componentStateResult: {}, }), ); }, ); store.setState({ - contributionsResult, extensions, - contributionStatesRecord, + contributionsResult, + contributionsRecord, }); }); } diff --git a/dashi/src/lib/actions/updateContributionState.ts b/dashi/src/lib/actions/updateContributionState.ts index 659d7849..1ee85b6e 100644 --- a/dashi/src/lib/actions/updateContributionState.ts +++ b/dashi/src/lib/actions/updateContributionState.ts @@ -12,8 +12,8 @@ export function updateContributionState( contribState: Partial, requireComponent: boolean = true, ) { - const { configuration, contributionStatesRecord } = store.getState(); - const contributionStates = contributionStatesRecord[contribPoint]; + const { configuration, contributionsRecord } = store.getState(); + const contributionStates = contributionsRecord[contribPoint]; const contributionState = contributionStates[contribIndex]; if (contributionState.state === contribState) { return; // nothing to do @@ -50,8 +50,8 @@ function getLayoutInputValues( contribPoint: ContribPoint, contribIndex: number, ): unknown[] { - const { configuration, contributionStatesRecord } = store.getState(); - const contributionStates = contributionStatesRecord[contribPoint]; + const { configuration, contributionsRecord } = store.getState(); + const contributionStates = contributionsRecord[contribPoint]; const contributionState = contributionStates[contribIndex]; const inputs = contributionState.layout!.inputs; if (inputs && inputs.length > 0) { @@ -70,8 +70,8 @@ function _updateContributionState( contribIndex: number, contribState: Partial, ) { - const { contributionStatesRecord } = store.getState(); - const contribStates = contributionStatesRecord[contribPoint]!; + const { contributionsRecord } = store.getState(); + const contribStates = contributionsRecord[contribPoint]!; const contribStateOld = contribStates[contribIndex]; const contribStateNew = contribState.state ? { @@ -80,8 +80,8 @@ function _updateContributionState( } : contribState; store.setState({ - contributionStatesRecord: { - ...contributionStatesRecord, + contributionsRecord: { + ...contributionsRecord, [contribPoint]: updateArray( contribStates, contribIndex, diff --git a/dashi/src/lib/hooks.ts b/dashi/src/lib/hooks.ts index c08d9540..caf5c9ac 100644 --- a/dashi/src/lib/hooks.ts +++ b/dashi/src/lib/hooks.ts @@ -4,5 +4,5 @@ export const useStore = store; export const useExtensions = () => useStore((state) => state.extensions); export const useContributionsResult = () => useStore((state) => state.contributionsResult); -export const useContributionStatesRecord = () => - useStore((state) => state.contributionStatesRecord); +export const useContributionsRecord = () => + useStore((state) => state.contributionsRecord); diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index b0a65437..daf3f573 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -17,7 +17,7 @@ export { useStore, useExtensions, useContributionsResult, - useContributionStatesRecord, + useContributionsRecord, } from "@/lib/hooks"; // Utilities export { configureLogging } from "@/lib/utils/configureLogging"; diff --git a/dashi/src/lib/store.ts b/dashi/src/lib/store.ts index ac5d0ff6..ea89e7a1 100644 --- a/dashi/src/lib/store.ts +++ b/dashi/src/lib/store.ts @@ -4,7 +4,7 @@ import { type StoreState } from "@/lib/types/state/store"; export const store = create(() => ({ configuration: {}, - contributionsResult: {}, extensions: [], - contributionStatesRecord: {}, + contributionsResult: {}, + contributionsRecord: {}, })); diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 1fc6542f..873e5d13 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -26,5 +26,5 @@ export interface StoreState { /** All extensions */ extensions: Extension[]; /** A record that maps contribPoint --> ContributionState[].*/ - contributionStatesRecord: Record; + contributionsRecord: Record; } From f016e3d84b514498e5b42b1253ac1937b1d40934 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 30 Oct 2024 10:25:29 +0100 Subject: [PATCH 10/38] renamed "componentStateResult" --> "componentResult" --- dashi/src/demo/components/Panel.tsx | 28 ++++++++++--------- dashi/src/demo/components/PanelsRow.tsx | 2 +- .../lib/actions/applyPropertyChange.test.ts | 2 +- .../lib/actions/initializeContributions.ts | 2 +- .../lib/actions/updateContributionState.ts | 11 ++++---- dashi/src/lib/types/state/contribution.ts | 3 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index ce07258e..93bbf707 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -32,39 +32,41 @@ const panelContentStyle: CSSProperties = { padding: 2, }; -interface PanelProps { - panelState: ContributionState; +interface PanelProps extends ContributionState { onPropertyChange: PropertyChangeHandler; } -function Panel({ panelState, onPropertyChange }: PanelProps) { - if (!panelState.state.visible) { +function Panel({ + name, + state, + componentState, + componentResult, + onPropertyChange, +}: PanelProps) { + if (!state.visible) { return null; } - const componentState = panelState.componentState; let panelElement: ReactElement | null = null; - const componentModelResult = panelState.componentStateResult; - if (componentModelResult.data && componentState) { + if (componentState) { panelElement = ( ); - } else if (componentModelResult.error) { + } else if (componentResult.error) { panelElement = ( - Error loading {panelState.name}: {componentModelResult.error.message} + Error loading {name}: {componentResult.error.message} ); - } else if (componentModelResult.status === "pending") { + } else if (componentResult.status === "pending") { panelElement = ( - Loading{" "} - {panelState.name}... + Loading {name}... ); } return (
-
{panelState.state.title}
+
{state.title}
{panelElement}
); diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index a6153893..ebcb11b4 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -23,7 +23,7 @@ function PanelsRow() { visiblePanels.push( handlePanelPropertyChange(panelIndex, e)} />, ); diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index 8b544615..70cb6eea 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -34,7 +34,7 @@ describe("Test that applyContributionChangeRequests()", () => { { name: "", extension: "", - componentStateResult: { status: "ok" }, + componentResult: { status: "ok" }, state: { visible: true }, componentState, }, diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index 57ea44a8..d246386b 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -27,7 +27,7 @@ export function initializeContributions(options?: FrameworkOptions) { (rawContribution) => ({ ...rawContribution, state: { ...rawContribution.initialState }, - componentStateResult: {}, + componentResult: {}, }), ); }, diff --git a/dashi/src/lib/actions/updateContributionState.ts b/dashi/src/lib/actions/updateContributionState.ts index 1ee85b6e..4cd8ff72 100644 --- a/dashi/src/lib/actions/updateContributionState.ts +++ b/dashi/src/lib/actions/updateContributionState.ts @@ -18,7 +18,7 @@ export function updateContributionState( if (contributionState.state === contribState) { return; // nothing to do } - const componentStatus = contributionState.componentStateResult.status; + const componentStatus = contributionState.componentResult.status; if (!requireComponent || componentStatus) { _updateContributionState(contribPoint, contribIndex, { state: contribState, @@ -27,7 +27,7 @@ export function updateContributionState( // No status yet, so we must load the component _updateContributionState(contribPoint, contribIndex, { state: contribState, - componentStateResult: { status: "pending" }, + componentResult: { status: "pending" }, }); const inputValues = getLayoutInputValues(contribPoint, contribIndex); fetchApiResult( @@ -36,11 +36,10 @@ export function updateContributionState( contribIndex, inputValues, configuration.api, - ).then((componentModelResult) => { - const componentState = componentModelResult?.data; + ).then((componentResult) => { _updateContributionState(contribPoint, contribIndex, { - componentStateResult: componentModelResult, - componentState, + componentResult, + componentState: componentResult.data, }); }); } diff --git a/dashi/src/lib/types/state/contribution.ts b/dashi/src/lib/types/state/contribution.ts index 04baa45b..c91d20c2 100644 --- a/dashi/src/lib/types/state/contribution.ts +++ b/dashi/src/lib/types/state/contribution.ts @@ -5,7 +5,6 @@ import type { ComponentState } from "./component"; export interface ContributionState extends Contribution { state: S; - componentStateResult: ApiResult; - // The value of componentStateResult.data, once it is available + componentResult: ApiResult; componentState?: ComponentState; } From cf791fcf5e3916b4026f3049c3d4c29816b2781c Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 31 Oct 2024 09:19:48 +0100 Subject: [PATCH 11/38] renamed "componentState" --> "component" --- dashi/src/demo/components/Panel.tsx | 6 +++--- dashi/src/lib/actions/applyPropertyChange.test.ts | 14 +++++++------- dashi/src/lib/actions/applyPropertyChange.ts | 15 ++++++--------- dashi/src/lib/actions/common.ts | 7 ++----- dashi/src/lib/actions/updateContributionState.ts | 2 +- dashi/src/lib/types/state/contribution.ts | 12 +++++++++++- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 93bbf707..10e70e33 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -39,7 +39,7 @@ interface PanelProps extends ContributionState { function Panel({ name, state, - componentState, + component, componentResult, onPropertyChange, }: PanelProps) { @@ -47,9 +47,9 @@ function Panel({ return null; } let panelElement: ReactElement | null = null; - if (componentState) { + if (component) { panelElement = ( - + ); } else if (componentResult.error) { panelElement = ( diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyPropertyChange.test.ts index 70cb6eea..73f2f5d4 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyPropertyChange.test.ts @@ -12,7 +12,7 @@ import { applyContributionChangeRequests, } from "./applyPropertyChange"; -const componentState: ComponentState = { +const componentTree: ComponentState = { type: "Box", id: "b1", components: [ @@ -36,7 +36,7 @@ describe("Test that applyContributionChangeRequests()", () => { extension: "", componentResult: { status: "ok" }, state: { visible: true }, - componentState, + component: componentTree, }, ], }; @@ -73,7 +73,7 @@ describe("Test that applyContributionChangeRequests()", () => { ]); expect(newState).not.toBe(contributionsRecord); expect( - newState["panels"][0].componentState!.components![1].components![1].value, + newState["panels"][0].component!.components![1].components![1].value, ).toEqual(14); }); @@ -87,23 +87,23 @@ describe("Test that applyContributionChangeRequests()", () => { describe("Test that applyComponentStateChange()", () => { it("changes state if values are different", () => { - const newState = applyComponentStateChange(componentState, { + const newState = applyComponentStateChange(componentTree, { kind: "Component", id: "cb1", property: "value", value: false, }); - expect(newState).not.toBe(componentState); + expect(newState).not.toBe(componentTree); expect(newState.components![1].components![0].value).toEqual(false); }); it("doesn't change the state if value stays the same", () => { - const newState = applyComponentStateChange(componentState, { + const newState = applyComponentStateChange(componentTree, { kind: "Component", id: "cb1", property: "value", value: true, }); - expect(newState).toBe(componentState); + expect(newState).toBe(componentTree); }); }); diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 7b4335de..7c5c2086 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -164,27 +164,24 @@ export function applyContributionChangeRequests( stateChangeRequests.forEach( ({ contribPoint, contribIndex, stateChanges }) => { const contribution = contributionsRecord[contribPoint][contribIndex]; - const componentStateOld = contribution.componentState; - let componentState = componentStateOld; - if (componentState) { + const componentOld = contribution.component; + let component = componentOld; + if (component) { stateChanges .filter( (stateChange) => !stateChange.kind || stateChange.kind === "Component", ) .forEach((stateChange) => { - componentState = applyComponentStateChange( - componentState!, - stateChange, - ); + component = applyComponentStateChange(component!, stateChange); }); - if (componentState !== componentStateOld) { + if (component !== componentOld) { contributionsRecord = { ...contributionsRecord, [contribPoint]: updateArray( contributionsRecord[contribPoint], contribIndex, - { ...contribution, componentState }, + { ...contribution, component }, ), }; } diff --git a/dashi/src/lib/actions/common.ts b/dashi/src/lib/actions/common.ts index 43b1c69a..eff89b86 100644 --- a/dashi/src/lib/actions/common.ts +++ b/dashi/src/lib/actions/common.ts @@ -22,12 +22,9 @@ export function getInputValue( let inputValue: unknown = undefined; if (!input.kind || input.kind === "Component") { - if (contributionState.componentState) { + if (contributionState.component) { // Return value of a property of some component in the tree - inputValue = getComponentStateValue( - contributionState.componentState, - input, - ); + inputValue = getComponentStateValue(contributionState.component, input); } } else if (input.kind === "State") { // Note, it is actually not ok to pass contributionState here directly. diff --git a/dashi/src/lib/actions/updateContributionState.ts b/dashi/src/lib/actions/updateContributionState.ts index 4cd8ff72..2d82e77d 100644 --- a/dashi/src/lib/actions/updateContributionState.ts +++ b/dashi/src/lib/actions/updateContributionState.ts @@ -39,7 +39,7 @@ export function updateContributionState( ).then((componentResult) => { _updateContributionState(contribPoint, contribIndex, { componentResult, - componentState: componentResult.data, + component: componentResult.data, }); }); } diff --git a/dashi/src/lib/types/state/contribution.ts b/dashi/src/lib/types/state/contribution.ts index c91d20c2..aaafd46e 100644 --- a/dashi/src/lib/types/state/contribution.ts +++ b/dashi/src/lib/types/state/contribution.ts @@ -4,7 +4,17 @@ import type { ComponentState } from "./component"; export interface ContributionState extends Contribution { + /** + * Contribution-private state properties. + */ state: S; + /** + * The result of loading the initial component tree. + */ componentResult: ApiResult; - componentState?: ComponentState; + /** + * The root node of the component tree. + * The value is initialized from `componentResult.data` + */ + component?: ComponentState; } From 5d8202ca4a4a40c5415214054cfdb05238ffec8d Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 31 Oct 2024 15:49:05 +0100 Subject: [PATCH 12/38] supporting both output kinds "State" and "AppState" now --- dashi/src/lib/actions/applyPropertyChange.ts | 108 ++++++++++++++----- dashi/src/lib/types/state/store.ts | 4 +- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 7c5c2086..5f6164f3 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -153,7 +153,44 @@ function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { contributionsRecord: contributionsRecordNew, }); } - // TODO: Set value of another kind of output. + applyHostStateChanges(stateChangeRequests); +} + +function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) { + const hostStore = store.getState().configuration.hostStore; + const getHostState = hostStore?.getState; + const setHostState = hostStore?.setState; + if (getHostState && setHostState) { + const hostStateOld = getHostState(); + let hostState = hostStateOld; + stateChangeRequests.forEach((stateChangeRequest) => { + hostState = applyStateChanges( + hostState, + stateChangeRequest.stateChanges, + "AppState", + ); + }); + if (hostState !== hostStateOld) { + setHostState(hostState); + } + } +} + +function applyComponentStateChanges( + componentOld: ComponentState | undefined, + stateChanges: StateChange[], +) { + let component = componentOld; + if (component) { + stateChanges + .filter( + (stateChange) => !stateChange.kind || stateChange.kind === "Component", + ) + .forEach((stateChange) => { + component = applyComponentStateChange(component!, stateChange); + }); + } + return component; } // we export for testing only @@ -164,30 +201,31 @@ export function applyContributionChangeRequests( stateChangeRequests.forEach( ({ contribPoint, contribIndex, stateChanges }) => { const contribution = contributionsRecord[contribPoint][contribIndex]; - const componentOld = contribution.component; - let component = componentOld; - if (component) { - stateChanges - .filter( - (stateChange) => - !stateChange.kind || stateChange.kind === "Component", - ) - .forEach((stateChange) => { - component = applyComponentStateChange(component!, stateChange); - }); - if (component !== componentOld) { - contributionsRecord = { - ...contributionsRecord, - [contribPoint]: updateArray( - contributionsRecord[contribPoint], - contribIndex, - { ...contribution, component }, - ), - }; - } + const state = applyStateChanges( + contribution.state, + stateChanges, + "State", + ); + const component = applyComponentStateChanges( + contribution.component, + stateChanges, + ); + if ( + state !== contribution.state || + component !== contribution.component + ) { + contributionsRecord = { + ...contributionsRecord, + [contribPoint]: updateArray( + contributionsRecord[contribPoint], + contribIndex, + { ...contribution, state, component }, + ), + }; } }, ); + return contributionsRecord; } @@ -198,11 +236,10 @@ export function applyComponentStateChange( ): ComponentState { if (component.id === stateChange.id) { const property = stateChange.property || "value"; - const oldValue = (component as unknown as Record)[ - property - ]; - if (oldValue !== stateChange.value) { - return { ...component, [property]: stateChange.value }; + const valueOld = (component as object)[property]; + const valueNew = stateChange.value; + if (valueOld !== valueNew) { + return { ...component, [property]: valueNew }; } } else if (isContainerState(component)) { const containerOld: ContainerState = component; @@ -224,3 +261,20 @@ export function applyComponentStateChange( } return component; } + +// we export for testing only +export function applyStateChanges( + state: S | undefined, + stateChanges: StateChange[], + kind: "State" | "AppState", +): S | undefined { + stateChanges.forEach((stateChange) => { + if ( + stateChange.kind === kind && + (!state || state[stateChange.property] !== stateChange.value) + ) { + state = { ...state, [stateChange.property]: stateChange.value }; + } + }); + return state; +} diff --git a/dashi/src/lib/types/state/store.ts b/dashi/src/lib/types/state/store.ts index 873e5d13..688b1995 100644 --- a/dashi/src/lib/types/state/store.ts +++ b/dashi/src/lib/types/state/store.ts @@ -9,7 +9,7 @@ import type { ApiOptions } from "@/lib/api"; import type { LoggingOptions } from "@/lib/utils/configureLogging"; import type { StoreApi } from "zustand"; -export interface FrameworkOptions { +export interface FrameworkOptions { /** The host applications state management store. */ hostStore?: StoreApi; /** API options to configure backend. */ @@ -18,7 +18,7 @@ export interface FrameworkOptions { logging?: LoggingOptions; } -export interface StoreState { +export interface StoreState { /** Framework configuration */ configuration: FrameworkOptions; /** API call result from `GET /contributions`. */ From 7905ecda3ac3a29b1cfd4e0d2fd8595e96f8b1b3 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 08:20:19 +0100 Subject: [PATCH 13/38] about to implement reaction to host state changes --- dashi/src/lib/actions/configureFramework.ts | 8 +- .../src/lib/actions/handleHostStoreChange.ts | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 dashi/src/lib/actions/handleHostStoreChange.ts diff --git a/dashi/src/lib/actions/configureFramework.ts b/dashi/src/lib/actions/configureFramework.ts index acfc7279..c5cbd801 100644 --- a/dashi/src/lib/actions/configureFramework.ts +++ b/dashi/src/lib/actions/configureFramework.ts @@ -1,12 +1,18 @@ import { store } from "@/lib/store"; import type { FrameworkOptions } from "@/lib/types/state/store"; import { configureLogging } from "@/lib/utils/configureLogging"; +import { handleHostStoreChange } from "./handleHostStoreChange"; -export function configureFramework(options: FrameworkOptions) { +export function configureFramework( + options: FrameworkOptions, +) { if (options.logging) { configureLogging(options.logging); } const { configuration } = store.getState(); + if (configuration.hostStore) { + configuration.hostStore.subscribe(handleHostStoreChange); + } store.setState({ configuration: { ...configuration, ...options }, }); diff --git a/dashi/src/lib/actions/handleHostStoreChange.ts b/dashi/src/lib/actions/handleHostStoreChange.ts new file mode 100644 index 00000000..ddbe5f7a --- /dev/null +++ b/dashi/src/lib/actions/handleHostStoreChange.ts @@ -0,0 +1,73 @@ +import { store } from "@/lib/store"; +import type { ContribRef, Input } from "@/lib/types/model/callback"; + +interface InputRef extends ContribRef { + // The callback index of the contribution + callbackIndex: number; + // The index of an input + inputIndex: number; + // The property path + propertyPath: string[]; +} + +// TODO: use memo +function getHostStoreInputRefs(): InputRef[] { + const { contributionsRecord } = store.getState(); + const appStateRefs: InputRef[] = []; + Object.getOwnPropertyNames(contributionsRecord).forEach((contribPoint) => { + const contributions = contributionsRecord[contribPoint]; + contributions.forEach((contribution, contribIndex) => { + (contribution.callbacks || []).forEach( + (callback, callbackIndex) => + (callback.inputs || []).forEach((input, inputIndex) => { + if (input.kind === "AppState" && input.property) { + appStateRefs.push({ + contribPoint, + contribIndex, + callbackIndex, + inputIndex, + propertyPath: input.property.split("."), + }); + } + }), + [] as Input[], + ); + }); + }); + return appStateRefs; +} + +export function handleHostStoreChange( + currState: S, + prevState: S, +) { + const effectiveInputRefs = getHostStoreInputRefs().filter((inputRef) => + isEffectiveInputRef(inputRef, currState, prevState), + ); + if (effectiveInputRefs.length === 0) { + return; + } +} + +function isEffectiveInputRef( + inputRef: InputRef, + currState: S, + prevState: S, +): boolean { + const propertyPath = inputRef.propertyPath; + const currValue = get(currState, propertyPath); + const prevValue = get(prevState, propertyPath); + return !Object.is(currValue, prevValue); +} + +function get(obj: object, path: (string | number)[]): unknown { + let value: unknown = obj; + for (let key of path) { + if (typeof value === "object") { + value = (value as object)[key]; + } else { + return undefined; + } + } + return value; +} From 20cd6672948722b8bf9c40a9a9ddcc1a388c3193 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 12:49:33 +0100 Subject: [PATCH 14/38] Fixed typing --- dashi/src/lib/actions/applyPropertyChange.ts | 14 +++++++++----- dashi/src/lib/actions/common.test.ts | 13 ++++--------- dashi/src/lib/actions/common.ts | 14 +++++++++++++- .../src/lib/actions/handleHostStoreChange.ts | 19 ++++--------------- .../lib/actions/initializeContributions.ts | 4 +++- dashi/src/lib/types/model/callback.ts | 3 +-- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyPropertyChange.ts index 5f6164f3..56082f34 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyPropertyChange.ts @@ -162,7 +162,7 @@ function applyHostStateChanges(stateChangeRequests: StateChangeRequest[]) { const setHostState = hostStore?.setState; if (getHostState && setHostState) { const hostStateOld = getHostState(); - let hostState = hostStateOld; + let hostState: object | undefined = hostStateOld; stateChangeRequests.forEach((stateChangeRequest) => { hostState = applyStateChanges( hostState, @@ -235,8 +235,10 @@ export function applyComponentStateChange( stateChange: StateChange, ): ComponentState { if (component.id === stateChange.id) { - const property = stateChange.property || "value"; - const valueOld = (component as object)[property]; + const property = stateChange.property; + const valueOld = (component as unknown as Record)[ + property + ]; const valueNew = stateChange.value; if (valueOld !== valueNew) { return { ...component, [property]: valueNew }; @@ -271,9 +273,11 @@ export function applyStateChanges( stateChanges.forEach((stateChange) => { if ( stateChange.kind === kind && - (!state || state[stateChange.property] !== stateChange.value) + (!state || + (state as unknown as Record)[stateChange.property] !== + stateChange.value) ) { - state = { ...state, [stateChange.property]: stateChange.value }; + state = { ...state, [stateChange.property]: stateChange.value } as S; } }); return state; diff --git a/dashi/src/lib/actions/common.test.ts b/dashi/src/lib/actions/common.test.ts index 9e1b9770..8362a83e 100644 --- a/dashi/src/lib/actions/common.test.ts +++ b/dashi/src/lib/actions/common.test.ts @@ -75,15 +75,10 @@ describe("Test that getInputValueFromState()", () => { ).toEqual(5); }); - it("works without input.property", () => { + it("works without input.id", () => { const state = { x: [4, 5, 6] }; - expect(getInputValueFromState({ kind: "State", id: "x" }, state)).toEqual([ - 4, 5, 6, - ]); - }); - - it("works without input.id and input.property", () => { - const state = { x: [4, 5, 6] }; - expect(getInputValueFromState({ kind: "State" }, state)).toBe(state); + expect( + getInputValueFromState({ kind: "State", property: "x" }, state), + ).toEqual([4, 5, 6]); }); }); diff --git a/dashi/src/lib/actions/common.ts b/dashi/src/lib/actions/common.ts index eff89b86..8205c6c1 100644 --- a/dashi/src/lib/actions/common.ts +++ b/dashi/src/lib/actions/common.ts @@ -84,8 +84,20 @@ export function getInputValueFromState( if (input.id && isSubscriptable(inputValue)) { inputValue = inputValue[input.id]; } - if (input.property && isSubscriptable(inputValue)) { + if (isSubscriptable(inputValue)) { inputValue = inputValue[input.property]; } return inputValue; } + +export function getValue(obj: object, path: (string | number)[]): unknown { + let value: unknown = obj; + for (const key of path) { + if (typeof value === "object") { + value = (value as unknown as Record)[key]; + } else { + return undefined; + } + } + return value; +} diff --git a/dashi/src/lib/actions/handleHostStoreChange.ts b/dashi/src/lib/actions/handleHostStoreChange.ts index ddbe5f7a..7c67800e 100644 --- a/dashi/src/lib/actions/handleHostStoreChange.ts +++ b/dashi/src/lib/actions/handleHostStoreChange.ts @@ -1,5 +1,6 @@ import { store } from "@/lib/store"; import type { ContribRef, Input } from "@/lib/types/model/callback"; +import { getValue } from "@/lib/actions/common"; interface InputRef extends ContribRef { // The callback index of the contribution @@ -20,7 +21,7 @@ function getHostStoreInputRefs(): InputRef[] { (contribution.callbacks || []).forEach( (callback, callbackIndex) => (callback.inputs || []).forEach((input, inputIndex) => { - if (input.kind === "AppState" && input.property) { + if (input.kind === "AppState") { appStateRefs.push({ contribPoint, contribIndex, @@ -55,19 +56,7 @@ function isEffectiveInputRef( prevState: S, ): boolean { const propertyPath = inputRef.propertyPath; - const currValue = get(currState, propertyPath); - const prevValue = get(prevState, propertyPath); + const currValue = getValue(currState, propertyPath); + const prevValue = getValue(prevState, propertyPath); return !Object.is(currValue, prevValue); } - -function get(obj: object, path: (string | number)[]): unknown { - let value: unknown = obj; - for (let key of path) { - if (typeof value === "object") { - value = (value as object)[key]; - } else { - return undefined; - } - } - return value; -} diff --git a/dashi/src/lib/actions/initializeContributions.ts b/dashi/src/lib/actions/initializeContributions.ts index d246386b..5b39889c 100644 --- a/dashi/src/lib/actions/initializeContributions.ts +++ b/dashi/src/lib/actions/initializeContributions.ts @@ -7,7 +7,9 @@ import { type ContribPoint } from "@/lib/types/model/extension"; import type { FrameworkOptions } from "@/lib/types/state/store"; import { configureFramework } from "@/lib"; -export function initializeContributions(options?: FrameworkOptions) { +export function initializeContributions( + options?: FrameworkOptions, +) { if (options) { configureFramework(options); } diff --git a/dashi/src/lib/types/model/callback.ts b/dashi/src/lib/types/model/callback.ts index f9bea9b7..5590c8b8 100644 --- a/dashi/src/lib/types/model/callback.ts +++ b/dashi/src/lib/types/model/callback.ts @@ -30,9 +30,8 @@ export interface InputOutput { // expression of the form: name {"." name | index} /** * The property of an object or array index. - * `property` may not be needed if `id` is sufficient. */ - property?: string; + property: string; } export interface Input extends InputOutput {} From 668b48d465df13b250a66af61202cbf5d465838e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 14:18:53 +0100 Subject: [PATCH 15/38] implemented handleHostStoreChange(); renamed applyPropertyChange() --> handleComponentChange() --- dashi/src/demo/components/PanelsRow.tsx | 8 +- ...st.ts => applyStateChangeRequests.test.ts} | 2 +- ...yChange.ts => applyStateChangeRequests.ts} | 150 ++---------------- dashi/src/lib/actions/common.ts | 16 +- .../src/lib/actions/handleComponentChange.ts | 134 ++++++++++++++++ .../src/lib/actions/handleHostStoreChange.ts | 71 +++++++-- dashi/src/lib/api.ts | 2 +- dashi/src/lib/index.ts | 2 +- 8 files changed, 217 insertions(+), 168 deletions(-) rename dashi/src/lib/actions/{applyPropertyChange.test.ts => applyStateChangeRequests.test.ts} (98%) rename dashi/src/lib/actions/{applyPropertyChange.ts => applyStateChangeRequests.ts} (51%) create mode 100644 dashi/src/lib/actions/handleComponentChange.ts diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index ebcb11b4..38be3150 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -1,6 +1,6 @@ import type { JSX } from "react"; -import { type PropertyChangeEvent, applyPropertyChange } from "@/lib"; +import { type PropertyChangeEvent, handleComponentChange } from "@/lib"; import { usePanelStates } from "@/demo/hooks"; import Panel from "./Panel"; @@ -11,11 +11,11 @@ function PanelsRow() { return null; } - const handlePanelPropertyChange = ( + const handlePanelChange = ( panelIndex: number, panelEvent: PropertyChangeEvent, ) => { - applyPropertyChange("panels", panelIndex, panelEvent); + handleComponentChange("panels", panelIndex, panelEvent); }; const visiblePanels: JSX.Element[] = []; panelStates.forEach((panelState, panelIndex) => { @@ -24,7 +24,7 @@ function PanelsRow() { handlePanelPropertyChange(panelIndex, e)} + onPropertyChange={(e) => handlePanelChange(panelIndex, e)} />, ); } diff --git a/dashi/src/lib/actions/applyPropertyChange.test.ts b/dashi/src/lib/actions/applyStateChangeRequests.test.ts similarity index 98% rename from dashi/src/lib/actions/applyPropertyChange.test.ts rename to dashi/src/lib/actions/applyStateChangeRequests.test.ts index 73f2f5d4..ee6c822d 100644 --- a/dashi/src/lib/actions/applyPropertyChange.test.ts +++ b/dashi/src/lib/actions/applyStateChangeRequests.test.ts @@ -10,7 +10,7 @@ import { type ContributionState } from "@/lib/types/state/contribution"; import { applyComponentStateChange, applyContributionChangeRequests, -} from "./applyPropertyChange"; +} from "./applyStateChangeRequests"; const componentTree: ComponentState = { type: "Box", diff --git a/dashi/src/lib/actions/applyPropertyChange.ts b/dashi/src/lib/actions/applyStateChangeRequests.ts similarity index 51% rename from dashi/src/lib/actions/applyPropertyChange.ts rename to dashi/src/lib/actions/applyStateChangeRequests.ts index 56082f34..45b1eb72 100644 --- a/dashi/src/lib/actions/applyPropertyChange.ts +++ b/dashi/src/lib/actions/applyStateChangeRequests.ts @@ -1,148 +1,20 @@ +import type { + StateChange, + StateChangeRequest, +} from "@/lib/types/model/callback"; import { store } from "@/lib/store"; -import { fetchStateChangeRequests } from "@/lib/api"; -import { fetchApiResult } from "@/lib/utils/fetchApiResult"; -import { - type ComponentState, - type ContainerState, +import type { + ComponentState, + ContainerState, } from "@/lib/types/state/component"; -import { type ContribPoint } from "@/lib/types/model/extension"; -import { - type Callback, - type CallbackRef, - type CallbackRequest, - type StateChange, - type StateChangeRequest, -} from "@/lib/types/model/callback"; -import { type PropertyChangeEvent } from "@/lib/types/model/event"; -import { type ContributionState } from "@/lib/types/state/contribution"; +import type { ContribPoint } from "@/lib/types/model/extension"; +import type { ContributionState } from "@/lib"; import { updateArray } from "@/lib/utils/updateArray"; import { isContainerState } from "@/lib/utils/isContainerState"; -import { getInputValues } from "@/lib/actions/common"; -export function applyPropertyChange( - contribPoint: ContribPoint, - contribIndex: number, - contribEvent: PropertyChangeEvent, +export function applyStateChangeRequests( + stateChangeRequests: StateChangeRequest[], ) { - const { configuration, contributionsRecord } = store.getState(); - const contributions = contributionsRecord[contribPoint]; - const contribution = contributions[contribIndex]; - const callbackRefs = generateCallbackRefs( - contribution, - contribEvent, - store.getState().configuration.hostStore?.getState, - ); - // The primary state change request corresponds - // to the original property change event. - const primaryChangeRequest: StateChangeRequest = { - contribPoint, - contribIndex, - stateChanges: [ - { - kind: "Component", - id: contribEvent.componentId, - property: contribEvent.propertyName, - value: contribEvent.propertyValue, - }, - ], - }; - // console.debug("callRequests", callRequests); - if (callbackRefs.length == 0) { - applyStateChangeRequests([primaryChangeRequest]); - } else { - const callbackRequests: CallbackRequest[] = callbackRefs.map( - (callbackRef) => ({ - contribPoint, - contribIndex, - ...callbackRef, - }), - ); - fetchApiResult( - fetchStateChangeRequests, - callbackRequests, - configuration.api, - ).then((changeRequestsResult) => { - const secondaryChangeRequests = changeRequestsResult.data; - if (secondaryChangeRequests) { - applyStateChangeRequests( - [primaryChangeRequest].concat(secondaryChangeRequests), - ); - } else { - // Note, we do not even apply the primaryChangeRequest - // in order to avoid an inconsistent state. - console.error( - "callback failed:", - changeRequestsResult.error, - "for call requests:", - callbackRefs, - ); - } - }); - } -} - -function generateCallbackRefs( - contribution: ContributionState, - contribEvent: PropertyChangeEvent, - getHostState?: () => unknown, -): CallbackRef[] { - const callbackRefs: CallbackRef[] = []; - // Prepare calling all callbacks of the contribution - // that are triggered by the property change - (contribution.callbacks || []).forEach((callback, callbackIndex) => { - const inputValues = getCallbackInputValues( - contribution, - contribEvent, - callback, - getHostState, - ); - if (inputValues) { - callbackRefs.push({ callbackIndex, inputValues }); - } - }); - return callbackRefs; -} - -// we export for testing only -export function getCallbackInputValues( - contribution: ContributionState, - contribEvent: PropertyChangeEvent, - callback: Callback, - getHostState?: () => unknown, -): unknown[] | undefined { - if (!callback.inputs || !callback.inputs.length) { - // No inputs defined - return undefined; - } - - // Find the index of input that is a trigger - const triggerIndex: number = callback.inputs.findIndex( - (input) => - (!input.kind || input.kind === "Component") && - input.id === contribEvent.componentId && - input.property === contribEvent.propertyName, - ); - if (triggerIndex < 0) { - // No trigger index found --> this callback is not applicable - return undefined; - } - - const inputValues = getInputValues( - callback.inputs, - contribution, - getHostState, - ); - - if (inputValues[triggerIndex] === contribEvent.propertyValue) { - // No change --> this callback is not applicable - return undefined; - } - - inputValues[triggerIndex] = contribEvent.propertyValue; - return inputValues; -} - -function applyStateChangeRequests(stateChangeRequests: StateChangeRequest[]) { const { contributionsRecord } = store.getState(); const contributionsRecordNew = applyContributionChangeRequests( contributionsRecord, diff --git a/dashi/src/lib/actions/common.ts b/dashi/src/lib/actions/common.ts index 8205c6c1..789ec9af 100644 --- a/dashi/src/lib/actions/common.ts +++ b/dashi/src/lib/actions/common.ts @@ -4,20 +4,20 @@ import type { ComponentState } from "@/lib/types/state/component.js"; import { isSubscriptable } from "@/lib/utils/isSubscriptable.js"; import { isContainerState } from "@/lib/utils/isContainerState.js"; -export function getInputValues( +export function getInputValues( inputs: Input[], contributionState: ContributionState, - getHostState?: () => unknown, + hostState?: S | undefined, ): unknown[] { return inputs.map((input) => - getInputValue(input, contributionState, getHostState), + getInputValue(input, contributionState, hostState), ); } -export function getInputValue( +export function getInputValue( input: Input, contributionState: ContributionState, - getHostState?: () => unknown, + hostState?: S | undefined, ): unknown { let inputValue: unknown = undefined; @@ -32,11 +32,11 @@ export function getInputValue( // the extra state required here. inputValue = getInputValueFromState(input, contributionState); } else if (input.kind === "AppState") { - if (getHostState) { - inputValue = getInputValueFromState(input, getHostState()); + if (hostState) { + inputValue = getInputValueFromState(input, hostState); } else { console.warn( - "missing configuration 'hostState'," + + "missing 'hostState'," + " which is need to resolve inputs of kind 'AppState'", input, ); diff --git a/dashi/src/lib/actions/handleComponentChange.ts b/dashi/src/lib/actions/handleComponentChange.ts new file mode 100644 index 00000000..93b161f6 --- /dev/null +++ b/dashi/src/lib/actions/handleComponentChange.ts @@ -0,0 +1,134 @@ +import { store } from "@/lib/store"; +import { fetchStateChangeRequests } from "@/lib/api"; +import { fetchApiResult } from "@/lib/utils/fetchApiResult"; +import { type ContribPoint } from "@/lib/types/model/extension"; +import { + type Callback, + type CallbackRef, + type CallbackRequest, + type StateChangeRequest, +} from "@/lib/types/model/callback"; +import { type PropertyChangeEvent } from "@/lib/types/model/event"; +import { type ContributionState } from "@/lib/types/state/contribution"; +import { getInputValues } from "@/lib/actions/common"; +import { applyStateChangeRequests } from "@/lib/actions/applyStateChangeRequests"; + +export function handleComponentChange( + contribPoint: ContribPoint, + contribIndex: number, + contribEvent: PropertyChangeEvent, +) { + const { configuration, contributionsRecord } = store.getState(); + const { hostStore } = configuration; + const contributions = contributionsRecord[contribPoint]; + const contribution = contributions[contribIndex]; + const callbackRefs = generateCallbackRefs( + contribution, + contribEvent, + hostStore ? hostStore.getState() : undefined, + ); + // The primary state change request corresponds + // to the original property change event. + const primaryChangeRequest: StateChangeRequest = { + contribPoint, + contribIndex, + stateChanges: [ + { + kind: "Component", + id: contribEvent.componentId, + property: contribEvent.propertyName, + value: contribEvent.propertyValue, + }, + ], + }; + // console.debug("callRequests", callRequests); + if (callbackRefs.length == 0) { + applyStateChangeRequests([primaryChangeRequest]); + } else { + const callbackRequests: CallbackRequest[] = callbackRefs.map( + (callbackRef) => ({ + contribPoint, + contribIndex, + ...callbackRef, + }), + ); + fetchApiResult( + fetchStateChangeRequests, + callbackRequests, + configuration.api, + ).then((changeRequestsResult) => { + const secondaryChangeRequests = changeRequestsResult.data; + if (secondaryChangeRequests) { + applyStateChangeRequests( + [primaryChangeRequest].concat(secondaryChangeRequests), + ); + } else { + // Note, we do not even apply the primaryChangeRequest + // in order to avoid an inconsistent state. + console.error( + "callback failed:", + changeRequestsResult.error, + "for call requests:", + callbackRequests, + ); + } + }); + } +} + +function generateCallbackRefs( + contribution: ContributionState, + contribEvent: PropertyChangeEvent, + hostState?: S | undefined, +): CallbackRef[] { + const callbackRefs: CallbackRef[] = []; + // Prepare calling all callbacks of the contribution + // that are triggered by the property change + (contribution.callbacks || []).forEach((callback, callbackIndex) => { + const inputValues = getCallbackInputValues( + contribution, + contribEvent, + callback, + hostState, + ); + if (inputValues) { + callbackRefs.push({ callbackIndex, inputValues }); + } + }); + return callbackRefs; +} + +// we export for testing only +export function getCallbackInputValues( + contribution: ContributionState, + contribEvent: PropertyChangeEvent, + callback: Callback, + hostState?: S | undefined, +): unknown[] | undefined { + if (!callback.inputs || !callback.inputs.length) { + // No inputs defined + return undefined; + } + + // Find the index of input that is a trigger + const triggerIndex: number = callback.inputs.findIndex( + (input) => + (!input.kind || input.kind === "Component") && + input.id === contribEvent.componentId && + input.property === contribEvent.propertyName, + ); + if (triggerIndex < 0) { + // No trigger index found --> this callback is not applicable + return undefined; + } + + const inputValues = getInputValues(callback.inputs, contribution, hostState); + + if (inputValues[triggerIndex] === contribEvent.propertyValue) { + // No change --> this callback is not applicable + return undefined; + } + + inputValues[triggerIndex] = contribEvent.propertyValue; + return inputValues; +} diff --git a/dashi/src/lib/actions/handleHostStoreChange.ts b/dashi/src/lib/actions/handleHostStoreChange.ts index 7c67800e..621607cb 100644 --- a/dashi/src/lib/actions/handleHostStoreChange.ts +++ b/dashi/src/lib/actions/handleHostStoreChange.ts @@ -1,6 +1,13 @@ import { store } from "@/lib/store"; -import type { ContribRef, Input } from "@/lib/types/model/callback"; -import { getValue } from "@/lib/actions/common"; +import type { + CallbackRequest, + ContribRef, + Input, +} from "@/lib/types/model/callback"; +import { getInputValues, getValue } from "@/lib/actions/common"; +import { fetchApiResult } from "@/lib/utils/fetchApiResult"; +import { fetchStateChangeRequests } from "@/lib/api"; +import { applyStateChangeRequests } from "@/lib/actions/applyStateChangeRequests"; interface InputRef extends ContribRef { // The callback index of the contribution @@ -11,6 +18,54 @@ interface InputRef extends ContribRef { propertyPath: string[]; } +export function handleHostStoreChange( + currState: S, + prevState: S, +) { + const effectiveInputRefs = getHostStoreInputRefs().filter((inputRef) => + isEffectiveInputRef(inputRef, currState, prevState), + ); + if (effectiveInputRefs.length === 0) { + return; + } + const { configuration, contributionsRecord } = store.getState(); + const { hostStore } = configuration; + const callbackRequests: CallbackRequest[] = effectiveInputRefs.map( + (inputRef) => { + const contribution = + contributionsRecord[inputRef.contribPoint][inputRef.contribIndex]; + const callback = contribution.callbacks + ? contribution.callbacks[inputRef.callbackIndex] + : undefined; + const inputValues = + callback && callback.inputs + ? getInputValues( + callback.inputs, + contribution, + hostStore ? hostStore.getState() : undefined, + ) + : []; + return { ...inputRef, inputValues }; + }, + ); + fetchApiResult( + fetchStateChangeRequests, + callbackRequests, + configuration.api, + ).then((changeRequestsResult) => { + if (changeRequestsResult.data) { + applyStateChangeRequests(changeRequestsResult.data); + } else { + console.error( + "callback failed:", + changeRequestsResult.error, + "for call requests:", + callbackRequests, + ); + } + }); +} + // TODO: use memo function getHostStoreInputRefs(): InputRef[] { const { contributionsRecord } = store.getState(); @@ -38,18 +93,6 @@ function getHostStoreInputRefs(): InputRef[] { return appStateRefs; } -export function handleHostStoreChange( - currState: S, - prevState: S, -) { - const effectiveInputRefs = getHostStoreInputRefs().filter((inputRef) => - isEffectiveInputRef(inputRef, currState, prevState), - ); - if (effectiveInputRefs.length === 0) { - return; - } -} - function isEffectiveInputRef( inputRef: InputRef, currState: S, diff --git a/dashi/src/lib/api.ts b/dashi/src/lib/api.ts index 9124997c..37d4d3ca 100644 --- a/dashi/src/lib/api.ts +++ b/dashi/src/lib/api.ts @@ -40,7 +40,7 @@ export async function fetchStateChangeRequests( options?: ApiOptions, ): Promise { return callApi(makeUrl("callback", options), { - body: JSON.stringify({ callbackRequests: callbackRequests }), + body: JSON.stringify({ callbackRequests }), method: "post", }); } diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index daf3f573..af9d5ea5 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -8,7 +8,7 @@ export { // Actions (store changes) export { initializeContributions } from "@/lib/actions/initializeContributions"; export { configureFramework } from "@/lib/actions/configureFramework"; -export { applyPropertyChange } from "@/lib/actions/applyPropertyChange"; +export { handleComponentChange } from "@/lib/actions/handleComponentChange"; export { updateContributionState } from "@/lib/actions/updateContributionState"; // React Components export { DashiComponent } from "@/lib/components/DashiComponent"; From 1804154af50bff2c11d7c17e6490b84dd54951be Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 14:28:12 +0100 Subject: [PATCH 16/38] renamed PropertyChangeEvent --> ComponentChangeEvent; renamed PropertyChangeHandler --> ComponentChangeHandler; --- dashi/src/demo/components/Panel.tsx | 4 ++-- dashi/src/demo/components/PanelsRow.tsx | 4 ++-- dashi/src/lib/actions/handleComponentChange.ts | 8 ++++---- dashi/src/lib/components/DashiBox.tsx | 4 ++-- dashi/src/lib/components/DashiButton.tsx | 4 ++-- dashi/src/lib/components/DashiCheckbox.tsx | 4 ++-- dashi/src/lib/components/DashiChildren.tsx | 4 ++-- dashi/src/lib/components/DashiComponent.tsx | 4 ++-- dashi/src/lib/components/DashiDropdown.tsx | 4 ++-- dashi/src/lib/components/DashiPlot.tsx | 4 ++-- dashi/src/lib/index.ts | 4 ++-- dashi/src/lib/types/model/event.ts | 4 ++-- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 10e70e33..a4e1de4b 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -2,7 +2,7 @@ import type { CSSProperties, ReactElement } from "react"; import CircularProgress from "@mui/material/CircularProgress"; import { - type PropertyChangeHandler, + type ComponentChangeHandler, type ContributionState, DashiComponent, } from "@/lib"; @@ -33,7 +33,7 @@ const panelContentStyle: CSSProperties = { }; interface PanelProps extends ContributionState { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } function Panel({ diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index 38be3150..0395203e 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -1,6 +1,6 @@ import type { JSX } from "react"; -import { type PropertyChangeEvent, handleComponentChange } from "@/lib"; +import { type ComponentChangeEvent, handleComponentChange } from "@/lib"; import { usePanelStates } from "@/demo/hooks"; import Panel from "./Panel"; @@ -13,7 +13,7 @@ function PanelsRow() { const handlePanelChange = ( panelIndex: number, - panelEvent: PropertyChangeEvent, + panelEvent: ComponentChangeEvent, ) => { handleComponentChange("panels", panelIndex, panelEvent); }; diff --git a/dashi/src/lib/actions/handleComponentChange.ts b/dashi/src/lib/actions/handleComponentChange.ts index 93b161f6..26b551b1 100644 --- a/dashi/src/lib/actions/handleComponentChange.ts +++ b/dashi/src/lib/actions/handleComponentChange.ts @@ -8,7 +8,7 @@ import { type CallbackRequest, type StateChangeRequest, } from "@/lib/types/model/callback"; -import { type PropertyChangeEvent } from "@/lib/types/model/event"; +import { type ComponentChangeEvent } from "@/lib/types/model/event"; import { type ContributionState } from "@/lib/types/state/contribution"; import { getInputValues } from "@/lib/actions/common"; import { applyStateChangeRequests } from "@/lib/actions/applyStateChangeRequests"; @@ -16,7 +16,7 @@ import { applyStateChangeRequests } from "@/lib/actions/applyStateChangeRequests export function handleComponentChange( contribPoint: ContribPoint, contribIndex: number, - contribEvent: PropertyChangeEvent, + contribEvent: ComponentChangeEvent, ) { const { configuration, contributionsRecord } = store.getState(); const { hostStore } = configuration; @@ -78,7 +78,7 @@ export function handleComponentChange( function generateCallbackRefs( contribution: ContributionState, - contribEvent: PropertyChangeEvent, + contribEvent: ComponentChangeEvent, hostState?: S | undefined, ): CallbackRef[] { const callbackRefs: CallbackRef[] = []; @@ -101,7 +101,7 @@ function generateCallbackRefs( // we export for testing only export function getCallbackInputValues( contribution: ContributionState, - contribEvent: PropertyChangeEvent, + contribEvent: ComponentChangeEvent, callback: Callback, hostState?: S | undefined, ): unknown[] | undefined { diff --git a/dashi/src/lib/components/DashiBox.tsx b/dashi/src/lib/components/DashiBox.tsx index c0444c34..62f6b8a7 100644 --- a/dashi/src/lib/components/DashiBox.tsx +++ b/dashi/src/lib/components/DashiBox.tsx @@ -1,11 +1,11 @@ import Box from "@mui/material/Box"; import { type BoxState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; import { DashiChildren } from "./DashiChildren"; export interface DashiBoxProps extends Omit { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiBox({ diff --git a/dashi/src/lib/components/DashiButton.tsx b/dashi/src/lib/components/DashiButton.tsx index 13b98c55..f18b2678 100644 --- a/dashi/src/lib/components/DashiButton.tsx +++ b/dashi/src/lib/components/DashiButton.tsx @@ -2,10 +2,10 @@ import { type MouseEvent } from "react"; import Button from "@mui/material/Button"; import { type ButtonState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiButtonProps extends Omit { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiButton({ diff --git a/dashi/src/lib/components/DashiCheckbox.tsx b/dashi/src/lib/components/DashiCheckbox.tsx index dd8683ee..a35c46f4 100644 --- a/dashi/src/lib/components/DashiCheckbox.tsx +++ b/dashi/src/lib/components/DashiCheckbox.tsx @@ -4,10 +4,10 @@ import FormControl from "@mui/material/FormControl"; import FormControlLabel from "@mui/material/FormControlLabel"; import { type CheckboxState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiCheckboxProps extends Omit { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiCheckbox({ diff --git a/dashi/src/lib/components/DashiChildren.tsx b/dashi/src/lib/components/DashiChildren.tsx index 84008fd7..5ec98abd 100644 --- a/dashi/src/lib/components/DashiChildren.tsx +++ b/dashi/src/lib/components/DashiChildren.tsx @@ -1,10 +1,10 @@ -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; import { type ComponentState } from "@/lib/types/state/component"; import { DashiComponent } from "./DashiComponent"; export interface DashiChildrenProps { components?: ComponentState[]; - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiChildren({ diff --git a/dashi/src/lib/components/DashiComponent.tsx b/dashi/src/lib/components/DashiComponent.tsx index 63398988..6fc701b9 100644 --- a/dashi/src/lib/components/DashiComponent.tsx +++ b/dashi/src/lib/components/DashiComponent.tsx @@ -1,12 +1,12 @@ import { type ComponentState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; import { DashiPlot, type DashiPlotProps } from "./DashiPlot"; import { DashiButton, type DashiButtonProps } from "./DashiButton"; import { DashiBox, type DashiBoxProps } from "./DashiBox"; import { DashiDropdown, type DashiDropdownProps } from "./DashiDropdown"; export interface DashiComponentProps extends ComponentState { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiComponent({ type, ...props }: DashiComponentProps) { diff --git a/dashi/src/lib/components/DashiDropdown.tsx b/dashi/src/lib/components/DashiDropdown.tsx index 8a3de05c..83ab7c1c 100644 --- a/dashi/src/lib/components/DashiDropdown.tsx +++ b/dashi/src/lib/components/DashiDropdown.tsx @@ -4,10 +4,10 @@ import MenuItem from "@mui/material/MenuItem"; import Select, { type SelectChangeEvent } from "@mui/material/Select"; import { type DropdownState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiDropdownProps extends Omit { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiDropdown({ diff --git a/dashi/src/lib/components/DashiPlot.tsx b/dashi/src/lib/components/DashiPlot.tsx index c4af48b3..53c03117 100644 --- a/dashi/src/lib/components/DashiPlot.tsx +++ b/dashi/src/lib/components/DashiPlot.tsx @@ -1,10 +1,10 @@ import { VegaLite } from "react-vega"; import { type PlotState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiPlotProps extends Omit { - onPropertyChange: PropertyChangeHandler; + onPropertyChange: ComponentChangeHandler; } export function DashiPlot({ diff --git a/dashi/src/lib/index.ts b/dashi/src/lib/index.ts index af9d5ea5..87b42967 100644 --- a/dashi/src/lib/index.ts +++ b/dashi/src/lib/index.ts @@ -2,8 +2,8 @@ export { type Contribution } from "@/lib/types/model/contribution"; export { type ContributionState } from "@/lib/types/state/contribution"; export { - type PropertyChangeEvent, - type PropertyChangeHandler, + type ComponentChangeEvent, + type ComponentChangeHandler, } from "@/lib/types/model/event"; // Actions (store changes) export { initializeContributions } from "@/lib/actions/initializeContributions"; diff --git a/dashi/src/lib/types/model/event.ts b/dashi/src/lib/types/model/event.ts index 39866284..310292e8 100644 --- a/dashi/src/lib/types/model/event.ts +++ b/dashi/src/lib/types/model/event.ts @@ -1,10 +1,10 @@ import { type ComponentType } from "@/lib/types/state/component"; -export interface PropertyChangeEvent { +export interface ComponentChangeEvent { componentType: ComponentType; componentId: string; propertyName: string; propertyValue: unknown; } -export type PropertyChangeHandler = (event: PropertyChangeEvent) => void; +export type ComponentChangeHandler = (event: ComponentChangeEvent) => void; From 48f7663b0df82bc22772595ae43f880e05725240 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 14:44:40 +0100 Subject: [PATCH 17/38] renamed onPropertyChange --> onChange --- dashi/src/demo/components/Panel.tsx | 8 +++----- dashi/src/demo/components/PanelsRow.tsx | 2 +- dashi/src/lib/components/DashiBox.tsx | 14 +++----------- dashi/src/lib/components/DashiButton.tsx | 6 +++--- dashi/src/lib/components/DashiChildren.tsx | 15 +++------------ dashi/src/lib/components/DashiComponent.tsx | 2 +- dashi/src/lib/components/DashiDropdown.tsx | 6 +++--- dashi/src/lib/components/DashiPlot.tsx | 11 +++-------- 8 files changed, 20 insertions(+), 44 deletions(-) diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index a4e1de4b..9ed56db7 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -33,7 +33,7 @@ const panelContentStyle: CSSProperties = { }; interface PanelProps extends ContributionState { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } function Panel({ @@ -41,16 +41,14 @@ function Panel({ state, component, componentResult, - onPropertyChange, + onChange, }: PanelProps) { if (!state.visible) { return null; } let panelElement: ReactElement | null = null; if (component) { - panelElement = ( - - ); + panelElement = ; } else if (componentResult.error) { panelElement = ( diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index 0395203e..cf0bd986 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -24,7 +24,7 @@ function PanelsRow() { handlePanelChange(panelIndex, e)} + onChange={(e) => handlePanelChange(panelIndex, e)} />, ); } diff --git a/dashi/src/lib/components/DashiBox.tsx b/dashi/src/lib/components/DashiBox.tsx index 62f6b8a7..253b1284 100644 --- a/dashi/src/lib/components/DashiBox.tsx +++ b/dashi/src/lib/components/DashiBox.tsx @@ -5,21 +5,13 @@ import { type ComponentChangeHandler } from "@/lib/types/model/event"; import { DashiChildren } from "./DashiChildren"; export interface DashiBoxProps extends Omit { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } -export function DashiBox({ - id, - style, - components, - onPropertyChange, -}: DashiBoxProps) { +export function DashiBox({ id, style, components, onChange }: DashiBoxProps) { return ( - + ); } diff --git a/dashi/src/lib/components/DashiButton.tsx b/dashi/src/lib/components/DashiButton.tsx index f18b2678..2097fd9a 100644 --- a/dashi/src/lib/components/DashiButton.tsx +++ b/dashi/src/lib/components/DashiButton.tsx @@ -5,7 +5,7 @@ import { type ButtonState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiButtonProps extends Omit { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } export function DashiButton({ @@ -14,11 +14,11 @@ export function DashiButton({ style, text, disabled, - onPropertyChange, + onChange, }: DashiButtonProps) { const handleClick = (event: MouseEvent) => { if (id) { - onPropertyChange({ + onChange({ componentType: "Button", componentId: id, // Compat with plotly/dash diff --git a/dashi/src/lib/components/DashiChildren.tsx b/dashi/src/lib/components/DashiChildren.tsx index 5ec98abd..4e46098c 100644 --- a/dashi/src/lib/components/DashiChildren.tsx +++ b/dashi/src/lib/components/DashiChildren.tsx @@ -4,13 +4,10 @@ import { DashiComponent } from "./DashiComponent"; export interface DashiChildrenProps { components?: ComponentState[]; - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } -export function DashiChildren({ - components, - onPropertyChange, -}: DashiChildrenProps) { +export function DashiChildren({ components, onChange }: DashiChildrenProps) { if (!components || components.length === 0) { return null; } @@ -18,13 +15,7 @@ export function DashiChildren({ <> {components.map((component, index) => { const key = component.id || index; - return ( - - ); + return ; })} ); diff --git a/dashi/src/lib/components/DashiComponent.tsx b/dashi/src/lib/components/DashiComponent.tsx index 6fc701b9..af57aad2 100644 --- a/dashi/src/lib/components/DashiComponent.tsx +++ b/dashi/src/lib/components/DashiComponent.tsx @@ -6,7 +6,7 @@ import { DashiBox, type DashiBoxProps } from "./DashiBox"; import { DashiDropdown, type DashiDropdownProps } from "./DashiDropdown"; export interface DashiComponentProps extends ComponentState { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } export function DashiComponent({ type, ...props }: DashiComponentProps) { diff --git a/dashi/src/lib/components/DashiDropdown.tsx b/dashi/src/lib/components/DashiDropdown.tsx index 83ab7c1c..c961ca61 100644 --- a/dashi/src/lib/components/DashiDropdown.tsx +++ b/dashi/src/lib/components/DashiDropdown.tsx @@ -7,7 +7,7 @@ import { type DropdownState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiDropdownProps extends Omit { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } export function DashiDropdown({ @@ -18,7 +18,7 @@ export function DashiDropdown({ disabled, style, label, - onPropertyChange, + onChange, }: DashiDropdownProps) { const handleChange = (event: SelectChangeEvent) => { if (!id) { @@ -29,7 +29,7 @@ export function DashiDropdown({ newValue = Number.parseInt(newValue); } - return onPropertyChange({ + return onChange({ componentType: "Dropdown", componentId: id, propertyName: "value", diff --git a/dashi/src/lib/components/DashiPlot.tsx b/dashi/src/lib/components/DashiPlot.tsx index 53c03117..f65eef97 100644 --- a/dashi/src/lib/components/DashiPlot.tsx +++ b/dashi/src/lib/components/DashiPlot.tsx @@ -4,22 +4,17 @@ import { type PlotState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/model/event"; export interface DashiPlotProps extends Omit { - onPropertyChange: ComponentChangeHandler; + onChange: ComponentChangeHandler; } -export function DashiPlot({ - id, - style, - chart, - onPropertyChange, -}: DashiPlotProps) { +export function DashiPlot({ id, style, chart, onChange }: DashiPlotProps) { if (!chart) { return
; } const { datasets, ...spec } = chart; const handleSignal = (_signalName: string, value: unknown) => { if (id) { - return onPropertyChange({ + return onChange({ componentType: "Plot", componentId: id, propertyName: "points", From ddf34f9c067ebf7b5ec218b3dfeae13a195b44f1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 4 Nov 2024 14:58:09 +0100 Subject: [PATCH 18/38] removed Dashi prefix from component names --- dashi/src/demo/components/Panel.tsx | 4 +-- dashi/src/lib/components/Box.tsx | 17 ++++++++++ .../{DashiButton.tsx => Button.tsx} | 12 +++---- .../{DashiCheckbox.tsx => Checkbox.tsx} | 26 ++++++++-------- dashi/src/lib/components/Component.tsx | 31 +++++++++++++++++++ ...ashiChildren.tsx => ComponentChildren.tsx} | 11 ++++--- dashi/src/lib/components/DashiBox.tsx | 17 ---------- dashi/src/lib/components/DashiComponent.tsx | 28 ----------------- .../{DashiDropdown.tsx => Dropdown.tsx} | 6 ++-- .../components/{DashiPlot.tsx => Plot.tsx} | 4 +-- dashi/src/lib/index.ts | 2 +- 11 files changed, 82 insertions(+), 76 deletions(-) create mode 100644 dashi/src/lib/components/Box.tsx rename dashi/src/lib/components/{DashiButton.tsx => Button.tsx} (79%) rename dashi/src/lib/components/{DashiCheckbox.tsx => Checkbox.tsx} (58%) create mode 100644 dashi/src/lib/components/Component.tsx rename dashi/src/lib/components/{DashiChildren.tsx => ComponentChildren.tsx} (62%) delete mode 100644 dashi/src/lib/components/DashiBox.tsx delete mode 100644 dashi/src/lib/components/DashiComponent.tsx rename dashi/src/lib/components/{DashiDropdown.tsx => Dropdown.tsx} (91%) rename dashi/src/lib/components/{DashiPlot.tsx => Plot.tsx} (83%) diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 9ed56db7..7e85d6eb 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -4,7 +4,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import { type ComponentChangeHandler, type ContributionState, - DashiComponent, + Component, } from "@/lib"; import type { PanelState } from "@/demo/types"; @@ -48,7 +48,7 @@ function Panel({ } let panelElement: ReactElement | null = null; if (component) { - panelElement = ; + panelElement = ; } else if (componentResult.error) { panelElement = ( diff --git a/dashi/src/lib/components/Box.tsx b/dashi/src/lib/components/Box.tsx new file mode 100644 index 00000000..54354dce --- /dev/null +++ b/dashi/src/lib/components/Box.tsx @@ -0,0 +1,17 @@ +import MuiBox from "@mui/material/Box"; + +import { type BoxState } from "@/lib/types/state/component"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; +import { ComponentChildren } from "./ComponentChildren"; + +export interface BoxProps extends Omit { + onChange: ComponentChangeHandler; +} + +export function Box({ id, style, components, onChange }: BoxProps) { + return ( + + + + ); +} diff --git a/dashi/src/lib/components/DashiButton.tsx b/dashi/src/lib/components/Button.tsx similarity index 79% rename from dashi/src/lib/components/DashiButton.tsx rename to dashi/src/lib/components/Button.tsx index 2097fd9a..9a11f9d5 100644 --- a/dashi/src/lib/components/DashiButton.tsx +++ b/dashi/src/lib/components/Button.tsx @@ -1,21 +1,21 @@ import { type MouseEvent } from "react"; -import Button from "@mui/material/Button"; +import MuiButton from "@mui/material/Button"; import { type ButtonState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/model/event"; -export interface DashiButtonProps extends Omit { +export interface ButtonProps extends Omit { onChange: ComponentChangeHandler; } -export function DashiButton({ +export function Button({ id, name, style, text, disabled, onChange, -}: DashiButtonProps) { +}: ButtonProps) { const handleClick = (event: MouseEvent) => { if (id) { onChange({ @@ -29,7 +29,7 @@ export function DashiButton({ } }; return ( - + ); } diff --git a/dashi/src/lib/components/DashiCheckbox.tsx b/dashi/src/lib/components/Checkbox.tsx similarity index 58% rename from dashi/src/lib/components/DashiCheckbox.tsx rename to dashi/src/lib/components/Checkbox.tsx index a35c46f4..dd20897a 100644 --- a/dashi/src/lib/components/DashiCheckbox.tsx +++ b/dashi/src/lib/components/Checkbox.tsx @@ -1,27 +1,27 @@ import { type ChangeEvent } from "react"; -import Checkbox from "@mui/material/Checkbox"; -import FormControl from "@mui/material/FormControl"; -import FormControlLabel from "@mui/material/FormControlLabel"; +import MuiCheckbox from "@mui/material/Checkbox"; +import MuiFormControl from "@mui/material/FormControl"; +import MuiFormControlLabel from "@mui/material/FormControlLabel"; import { type CheckboxState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/model/event"; -export interface DashiCheckboxProps extends Omit { - onPropertyChange: ComponentChangeHandler; +export interface CheckboxProps extends Omit { + onChange: ComponentChangeHandler; } -export function DashiCheckbox({ +export function Checkbox({ id, name, value, disabled, style, label, - onPropertyChange, -}: DashiCheckboxProps) { + onChange, +}: CheckboxProps) { const handleChange = (event: ChangeEvent) => { if (id) { - return onPropertyChange({ + return onChange({ componentType: "Checkbox", componentId: id, propertyName: "value", @@ -30,11 +30,11 @@ export function DashiCheckbox({ } }; return ( - - + } /> - + ); } diff --git a/dashi/src/lib/components/Component.tsx b/dashi/src/lib/components/Component.tsx new file mode 100644 index 00000000..618374b1 --- /dev/null +++ b/dashi/src/lib/components/Component.tsx @@ -0,0 +1,31 @@ +import { type ComponentState } from "@/lib/types/state/component"; +import { type ComponentChangeHandler } from "@/lib/types/model/event"; +import { Button, type ButtonProps } from "./Button"; +import { Box, type BoxProps } from "./Box"; +import { Checkbox, type CheckboxProps } from "./Checkbox"; +import { Dropdown, type DropdownProps } from "./Dropdown"; +import { Plot, type PlotProps } from "./Plot"; + +export interface ComponentProps extends ComponentState { + onChange: ComponentChangeHandler; +} + +export function Component({ type, ...props }: ComponentProps) { + // TODO: allow for registering components via their types + // and make following code generic. + // + // const DashiComp = Registry.getComponent(type); + // return return ; + // + if (type === "Plot") { + return ; + } else if (type === "Dropdown") { + return ; + } else if (type === "Button") { + return