Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions chartlets.js/packages/lib/src/actions/configureFramework.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { ComponentType, FC } from "react";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { configureFramework, resolvePlugin } from "./configureFramework";
import { store } from "@/store";
import { registry } from "@/components/registry";
import type { HostStore } from "@/types/state/host";
import type { Plugin } from "@/types/state/plugin";
import type { ComponentProps } from "@/components/Component";

function getComponents(): [string, ComponentType<ComponentProps>][] {
interface DivProps extends ComponentProps {
text: string;
}
const Div: FC<DivProps> = ({ text }) => <div>{text}</div>;
return [
["A", Div as FC<ComponentProps>],
["B", Div as FC<ComponentProps>],
];
}

describe("configureFramework", () => {
it("should accept no arg", () => {
configureFramework();
expect(store.getState().configuration).toEqual({});
});

it("should accept empty arg", () => {
configureFramework({});
expect(store.getState().configuration).toEqual({});
});

it("should enable logging", () => {
configureFramework({
logging: {
enabled: true,
},
});
expect(store.getState().configuration).toEqual({
logging: { enabled: true },
});
});

it("should subscribe to host store", () => {
const listeners = [];
const hostStore: HostStore = {
get: (_key: string) => null,
subscribe: (l: () => void) => {
listeners.push(l);
},
};
configureFramework({
hostStore,
});
expect(listeners.length).toBe(1);
});

it("should install plugins", () => {
expect(registry.types.length).toBe(0);
configureFramework({
plugins: [{ components: getComponents() }],
});
expect(registry.types.length).toBe(2);
});
});

describe("resolvePlugin", () => {
beforeEach(() => {
registry.clear();
});

afterEach(() => {
registry.clear();
});

it("should resolve a object", async () => {
const pluginObj: Plugin = { components: getComponents() };
expect(registry.types.length).toBe(0);
const result = await resolvePlugin(pluginObj);
expect(result).toBe(pluginObj);
expect(registry.types.length).toBe(2);
});

it("should resolve a function", async () => {
const pluginObj = { components: getComponents() };
const pluginFunction = () => pluginObj;
expect(registry.types.length).toBe(0);
const result = await resolvePlugin(pluginFunction);
expect(result).toBe(pluginObj);
expect(registry.types.length).toBe(2);
});

it("should resolve a promise", async () => {
const pluginObj = { components: getComponents() };
const pluginPromise = Promise.resolve(pluginObj);
expect(registry.types.length).toBe(0);
const result = await resolvePlugin(pluginPromise);
expect(result).toBe(pluginObj);
expect(registry.types.length).toBe(2);
});

it("should resolve undefined", async () => {
expect(registry.types.length).toBe(0);
const result = await resolvePlugin(undefined as unknown as Plugin);
expect(result).toBe(undefined);
expect(registry.types.length).toBe(0);
});
});
16 changes: 10 additions & 6 deletions chartlets.js/packages/lib/src/actions/configureFramework.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { store } from "@/store";
import type { FrameworkOptions } from "@/types/state/options";
import type {
ComponentRegistration,
FrameworkOptions,
Plugin,
PluginLike,
} from "@/types/state/options";
} from "@/types/state/plugin";
import { registry } from "@/components/registry";
import { isPromise } from "@/utils/isPromise";
import { isFunction } from "@/utils/isFunction";
import { isObject } from "@/utils/isObject";
import { handleHostStoreChange } from "./handleHostStoreChange";
import { configureLogging } from "./helpers/configureLogging";

export function configureFramework(options: FrameworkOptions) {
export function configureFramework(options?: FrameworkOptions) {
options = options || {};
if (options.logging) {
configureLogging(options.logging);
}
Expand All @@ -26,16 +28,18 @@ export function configureFramework(options: FrameworkOptions) {
}
}

function resolvePlugin(plugin: PluginLike) {
export function resolvePlugin(plugin: PluginLike): Promise<Plugin | undefined> {
if (isPromise<PluginLike>(plugin)) {
plugin.then(resolvePlugin);
return plugin.then(resolvePlugin);
} else if (isFunction(plugin)) {
resolvePlugin(plugin());
return resolvePlugin(plugin());
} else if (isObject(plugin) && plugin.components) {
(plugin.components as ComponentRegistration[]).forEach(
([name, component]) => {
registry.register(name, component);
},
);
return Promise.resolve(plugin as Plugin);
}
return Promise.resolve(undefined);
}
14 changes: 9 additions & 5 deletions chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { store } from "@/store";
import type {
CallbackRef,
CallbackRequest,
Expand All @@ -10,7 +9,8 @@ import { getInputValues } from "@/actions/helpers/getInputValues";
import { formatObjPath } from "@/utils/objPath";
import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks";
import type { ContributionState } from "@/types/state/contribution";
import type { HostStore } from "@/types/state/options";
import type { HostStore } from "@/types/state/host";
import { store } from "@/store";

/**
* A reference to a property of an input of a callback of a contribution.
Expand All @@ -23,12 +23,16 @@ export interface PropertyRef extends ContribRef, CallbackRef, InputRef {
export function handleHostStoreChange() {
const { extensions, configuration, contributionsRecord } = store.getState();
const { hostStore } = configuration;
if (!hostStore || extensions.length === 0) {
// Exit if no host store configured or
// there are no extensions (yet)
if (!hostStore) {
// Exit if no host store configured.
// Actually, we should not come here.
return;
}
synchronizeThemeMode(hostStore);
if (extensions.length === 0) {
// Exit if there are no extensions (yet)
return;
}
const propertyRefs = getHostStorePropertyRefs();
if (!propertyRefs || propertyRefs.length === 0) {
// Exit if there are is nothing to be changed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, beforeEach } from "vitest";
import { store } from "@/store";
import { handleHostStoreChange } from "./handleHostStoreChange";

describe("handleHostStoreChange", () => {
let listeners: (() => void)[] = [];
let hostState: Record<string, unknown> = {};
const hostStore = {
get: (key: string) => hostState[key],
set: (key: string, value: unknown) => {
hostState = { ...hostState, [key]: value };
listeners.forEach((l) => void l());
},
subscribe: (_l: () => void) => {
listeners.push(_l);
},
};

beforeEach(() => {
listeners = [];
hostState = {};
});

it("should do nothing without host store", () => {
store.setState({ configuration: {} });
const oldState = store.getState();
handleHostStoreChange();
const newState = store.getState();
expect(newState).toBe(oldState);
expect(newState).toEqual(oldState);
});

it("should synchronize theme mode", () => {
store.setState({ configuration: { hostStore } });
expect(store.getState().themeMode).toBeUndefined();
hostStore.set("themeMode", "light");
handleHostStoreChange();
expect(store.getState().themeMode).toEqual("light");
});

it("should generate callback requests", () => {
const extensions = [{ name: "e0", version: "0", contributes: ["panels"] }];
store.setState({
configuration: { hostStore },
extensions,
contributionsResult: {
status: "ok",
data: {
extensions,
contributions: {
panels: [
{
name: "p0",
extension: "e0",
layout: {
function: {
name: "layout",
parameters: [],
return: {},
},
inputs: [],
outputs: [],
},
callbacks: [
{
function: {
name: "callback",
parameters: [],
return: {},
},
inputs: [{ id: "@app", property: "variableName" }],
outputs: [{ id: "select", property: "value" }],
},
],
initialState: {},
},
],
},
},
},
});
hostStore.set("variableName", "CHL");
handleHostStoreChange();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import {
normalizeObjPath,
setValue,
} from "@/utils/objPath";
import {
isMutableHostStore,
type MutableHostStore,
} from "@/types/state/options";
import { isMutableHostStore, type MutableHostStore } from "@/types/state/host";
import {
isHostChannel,
isComponentChannel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@/types/state/component";
import { formatObjPath, getValue, type ObjPathLike } from "@/utils/objPath";
import { isObject } from "@/utils/isObject";
import type { HostStore } from "@/types/state/options";
import type { HostStore } from "@/types/state/host";

export function getInputValues(
inputs: Input[],
Expand Down
10 changes: 3 additions & 7 deletions chartlets.js/packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export {
} from "@/hooks";

// Application interface
export type {
FrameworkOptions,
HostStore,
MutableHostStore,
Plugin,
PluginLike,
} from "@/types/state/options";
export type { HostStore, MutableHostStore } from "@/types/state/host";
export type { Plugin, PluginLike } from "@/types/state/plugin";
export type { FrameworkOptions } from "@/types/state/options";
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type MutableHostStore,
isHostStore,
isMutableHostStore,
} from "./options";
} from "./host";

const hostStore: HostStore = {
get: (name: string) => name,
Expand Down
60 changes: 60 additions & 0 deletions chartlets.js/packages/lib/src/types/state/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { isObject } from "@/utils/isObject";
import { isFunction } from "@/utils/isFunction";

/**
* The host store represents an interface to the state of
* the application that is using Chartlets.
*/
export interface HostStore {
/**
* Let Chartlets listen to changes in the host store that may
* cause different values to be returned from the `get()` method.
*
* @param listener A listener that is called when the
* host store changes
*/
subscribe: (listener: () => void) => void;

/**
* Get a property value from the host state.
*
* @param property The property name.
* @returns The property value.
*/
get: (property: string) => unknown;

/**
* **UNSTABLE API**
*
* Set a property value in the host state.
*
* @param property The property name.
* @param value The new property value.
*/
set?: (property: string, value: unknown) => void;
}

/**
* A mutable host store implements the `set()` method.
*/
export interface MutableHostStore extends HostStore {
/**
* **UNSTABLE API**
*
* Set a property value in the host state.
*
* @param property The property name.
* @param value The new property value.
*/
set: (property: string, value: unknown) => void;
}

export function isHostStore(value: unknown): value is HostStore {
return (
isObject(value) && isFunction(value.get) && isFunction(value.subscribe)
);
}

export function isMutableHostStore(value: unknown): value is MutableHostStore {
return isHostStore(value) && isFunction(value.set);
}
Loading
Loading