Skip to content

Commit 21210d8

Browse files
authored
[WC-3345] fix: custom chart store (#2165)
2 parents b257ffe + 29e39c7 commit 21210d8

28 files changed

Lines changed: 465 additions & 841 deletions

packages/pluggableWidgets/chart-playground-web/package.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,10 @@
4040
"verify": "rui-verify-package-format"
4141
},
4242
"dependencies": {
43-
"@codemirror/lang-json": "^6.0.2",
44-
"@codemirror/lint": "^6.8.5",
4543
"@mendix/shared-charts": "workspace:*",
4644
"@mendix/widget-plugin-component-kit": "workspace:*",
4745
"@mendix/widget-plugin-hooks": "workspace:*",
48-
"@mendix/widget-plugin-platform": "workspace:*",
49-
"@uiw/codemirror-theme-github": "^4.23.13",
50-
"@uiw/react-codemirror": "^4.23.13"
46+
"@mendix/widget-plugin-platform": "workspace:*"
5147
},
5248
"devDependencies": {
5349
"@mendix/automation-utils": "workspace:*",
Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,19 @@
1-
import { json, jsonParseLinter } from "@codemirror/lang-json";
2-
import { linter, lintGutter } from "@codemirror/lint";
3-
import { githubLight } from "@uiw/codemirror-theme-github";
4-
import CodeMirror, { type Extension } from "@uiw/react-codemirror";
5-
import { ReactElement, useEffect, useMemo, useRef, useState } from "react";
6-
7-
export type EditorChangeHandler = (value: string) => void;
1+
import { ReactElement } from "react";
82

93
export interface CodeEditorProps {
10-
defaultValue: string;
11-
onChange?: EditorChangeHandler;
4+
value: string;
5+
onChange?: (value: string) => void;
126
readOnly?: boolean;
137
height?: string;
148
}
159

1610
export function CodeEditor(props: CodeEditorProps): ReactElement {
17-
const [value, onChange] = useEditorState({ initState: props.defaultValue, onChange: props.onChange });
18-
const extensions = useMemo<Extension[]>(
19-
() => [
20-
json(),
21-
linter(jsonParseLinter(), {
22-
// default is 750ms
23-
delay: 300
24-
}),
25-
lintGutter()
26-
],
27-
[]
28-
);
2911
return (
30-
<CodeMirror
31-
height={props.height}
32-
value={value}
33-
onChange={onChange}
34-
theme={githubLight}
12+
<textarea
13+
value={props.value}
14+
onChange={e => props.onChange?.(e.target.value)}
15+
style={{ height: props.height ?? "200px", width: "100%", fontFamily: "monospace" }}
3516
readOnly={props.readOnly}
36-
extensions={extensions}
3717
/>
3818
);
3919
}
40-
41-
function useEditorState(params: { initState: string; onChange?: EditorChangeHandler }): [string, EditorChangeHandler] {
42-
const listener = useRef(params.onChange);
43-
const [value, setValue] = useState(params.initState);
44-
const { current: onValueChange } = useRef<EditorChangeHandler>(value => {
45-
setValue(value);
46-
listener.current?.(value);
47-
});
48-
useEffect(() => {
49-
listener.current = params.onChange;
50-
});
51-
return [value, onValueChange];
52-
}

packages/pluggableWidgets/chart-playground-web/src/components/ComposedEditor.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
2-
import { useOnClickOutside } from "@mendix/widget-plugin-hooks/useOnClickOutside";
31
import classNames from "classnames";
42
import { Fragment, ReactElement, ReactNode, RefObject, useCallback, useRef, useState } from "react";
3+
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
4+
import { useOnClickOutside } from "@mendix/widget-plugin-hooks/useOnClickOutside";
55
import "../ui/Playground.scss";
6+
import { CodeEditor } from "./CodeEditor";
67
import { Select, SelectOption, Sidebar, SidebarHeader, SidebarHeaderTools, SidebarPanel } from "./Sidebar";
7-
import { CodeEditor, EditorChangeHandler } from "./CodeEditor";
88

99
interface WrapperProps {
1010
renderPanels: ReactNode;
@@ -71,8 +71,8 @@ const SidebarContentTooltip = (): ReactElement => {
7171
};
7272

7373
export interface ComposedEditorProps {
74-
defaultEditorValue: string;
75-
onEditorChange: EditorChangeHandler;
74+
value: string;
75+
onEditorChange: (value: string) => void;
7676
modelerCode: string;
7777
onViewSelectChange: (value: string) => void;
7878
viewSelectValue: string;
@@ -138,11 +138,7 @@ export function ComposedEditor(props: ComposedEditorProps): ReactElement {
138138
heading={topPanelHeader}
139139
>
140140
<TabGuard>
141-
<CodeEditor
142-
defaultValue={props.defaultEditorValue}
143-
onChange={props.onEditorChange}
144-
height="var(--editor-h)"
145-
/>
141+
<CodeEditor value={props.value} onChange={props.onEditorChange} height="var(--editor-h)" />
146142
</TabGuard>
147143
</SidebarPanel>
148144
<SidebarPanel
@@ -154,7 +150,7 @@ export function ComposedEditor(props: ComposedEditorProps): ReactElement {
154150
<CodeEditor
155151
key={props.modelerCode}
156152
readOnly
157-
defaultValue={props.modelerCode}
153+
value={props.modelerCode}
158154
height="var(--static-settings-h)"
159155
/>
160156
</SidebarPanel>
Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
1-
import { PlaygroundData, usePlaygroundContext } from "@mendix/shared-charts/main";
2-
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
31
import { ReactElement } from "react";
4-
import { useComposedEditorController } from "../helpers/useComposedEditorController";
2+
import { PlaygroundDataV1, PlaygroundDataV2, usePlaygroundContext } from "@mendix/shared-charts/main";
3+
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
54
import { ComposedEditor } from "./ComposedEditor";
5+
import { useComposedEditorController } from "../helpers/useComposedEditorController";
66
import "../ui/Playground.scss";
7+
import { useV2EditorController } from "../helpers/useV2EditorController";
78

8-
function Editor({ data }: { data: PlaygroundData }): ReactElement {
9+
function EditorGen1({ data }: { data: PlaygroundDataV1 }): ReactElement {
910
const props = useComposedEditorController(data);
1011

1112
return <ComposedEditor {...props} />;
1213
}
1314

15+
function EditorGen2({ data }: { data: PlaygroundDataV2 }): ReactElement {
16+
const props = useV2EditorController(data);
17+
18+
return <ComposedEditor {...props} />;
19+
}
20+
1421
export function Playground(): ReactElement {
1522
const ctx = usePlaygroundContext();
1623

1724
if ("error" in ctx) {
1825
return <Alert bootstrapStyle="danger">{ctx.error.message}</Alert>;
1926
}
2027

21-
return <Editor data={ctx.data} />;
28+
const { data } = ctx;
29+
30+
if (Object.hasOwn(data, "type") && (data as PlaygroundDataV2).type === "editor.data.v2") {
31+
return <EditorGen2 data={data as PlaygroundDataV2} />;
32+
}
33+
34+
return <EditorGen1 data={data as PlaygroundDataV1} />;
2235
}

packages/pluggableWidgets/chart-playground-web/src/helpers/useComposedEditorController.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { fallback, PlaygroundData } from "@mendix/shared-charts/main";
2-
import { EditorChangeHandler } from "../components/CodeEditor";
3-
import { useMemo, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useState } from "react";
2+
import { fallback, PlaygroundDataV1 } from "@mendix/shared-charts/main";
43
import { ComposedEditorProps } from "../components/ComposedEditor";
54
import { SelectOption } from "../components/Sidebar";
65

@@ -14,13 +13,13 @@ const irrelevantSeriesKeys = ["x", "y", "z", "customSeriesOptions", "dataSourceI
1413

1514
type ConfigKey = "layout" | "config" | number;
1615

17-
function getEditorCode({ store }: PlaygroundData, key: ConfigKey): string {
16+
function getEditorCode({ store }: PlaygroundDataV1, key: ConfigKey): string {
1817
let value = typeof key === "number" ? store.state.data.at(key) : store.state[key];
1918
value = value ?? '{ "error": "value is unavailable" }';
2019
return value;
2120
}
2221

23-
function getModelerCode(data: PlaygroundData, key: ConfigKey): Partial<Data> | Partial<Layout> | Partial<Config> {
22+
function getModelerCode(data: PlaygroundDataV1, key: ConfigKey): Partial<Data> | Partial<Layout> | Partial<Config> {
2423
if (key === "layout") {
2524
return data.layoutOptions;
2625
}
@@ -31,8 +30,15 @@ function getModelerCode(data: PlaygroundData, key: ConfigKey): Partial<Data> | P
3130
const entries = Object.entries(data.plotData.at(key) ?? {}).filter(([key]) => !irrelevantSeriesKeys.includes(key));
3231
return Object.fromEntries(entries) as Partial<Data>;
3332
}
33+
function prettifyJson(json: string): string {
34+
try {
35+
return JSON.stringify(JSON.parse(json), null, 2);
36+
} catch {
37+
return '{ "error": "invalid JSON" }';
38+
}
39+
}
3440

35-
export function useComposedEditorController(data: PlaygroundData): ComposedEditorProps {
41+
export function useComposedEditorController(data: PlaygroundDataV1): ComposedEditorProps {
3642
const [key, setKey] = useState<ConfigKey>("layout");
3743

3844
const onViewSelectChange = (value: string): void => {
@@ -56,20 +62,34 @@ export function useComposedEditorController(data: PlaygroundData): ComposedEdito
5662
];
5763
}, [data.plotData]);
5864

59-
const onEditorChange: EditorChangeHandler = (json): void => {
60-
json = fallback(json);
61-
try {
62-
JSON.parse(json);
63-
data.store.set(key, json);
64-
// eslint-disable-next-line no-empty
65-
} catch {}
66-
};
65+
const store = data.store;
66+
const code = prettifyJson(getEditorCode(data, key));
67+
const [input, setInput] = useState(() => code);
68+
const onEditorChange = useCallback(
69+
(value: string): void => {
70+
setInput(value);
71+
try {
72+
const json = fallback(value);
73+
JSON.parse(value);
74+
store.set(key, json);
75+
// eslint-disable-next-line no-empty
76+
} catch {}
77+
},
78+
[store, key]
79+
);
80+
81+
useEffect(
82+
() =>
83+
// eslint-disable-next-line react-hooks/set-state-in-effect
84+
setInput(code),
85+
[code]
86+
);
6787

6888
return {
6989
viewSelectValue: key.toString(),
7090
viewSelectOptions: options,
7191
onViewSelectChange,
72-
defaultEditorValue: getEditorCode(data, key),
92+
value: input,
7393
modelerCode: useMemo(() => JSON.stringify(getModelerCode(data, key), null, 2), [data, key]),
7494
onEditorChange
7595
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { observable, reaction, runInAction } from "mobx";
2+
import { useCallback, useEffect, useMemo, useState } from "react";
3+
import { PlaygroundDataV2 } from "@mendix/shared-charts/main";
4+
import { ComposedEditorProps } from "../components/ComposedEditor";
5+
import { SelectOption } from "../components/Sidebar";
6+
7+
type ConfigKey = "layout" | "config" | number;
8+
9+
const irrelevantSeriesKeys = ["x", "y", "z", "customSeriesOptions", "dataSourceItems"];
10+
11+
function getEditorCode(store: PlaygroundDataV2["store"], key: ConfigKey): string {
12+
if (key === "layout") {
13+
return store.layoutJson ?? '{ "error": "value is unavailable" }';
14+
}
15+
if (key === "config") {
16+
return store.configJson ?? '{ "error": "value is unavailable" }';
17+
}
18+
return store.dataJson.at(key) ?? '{ "error": "value is unavailable" }';
19+
}
20+
21+
function getModelerCode(data: PlaygroundDataV2, key: ConfigKey): object {
22+
if (key === "layout") {
23+
return data.layoutOptions;
24+
}
25+
if (key === "config") {
26+
return data.configOptions;
27+
}
28+
const entries = Object.entries(data.plotData.at(key) ?? {}).filter(([k]) => !irrelevantSeriesKeys.includes(k));
29+
return Object.fromEntries(entries);
30+
}
31+
32+
function prettifyJson(json: string): string {
33+
try {
34+
return JSON.stringify(JSON.parse(json), null, 2);
35+
} catch {
36+
return '{ "error": "invalid JSON" }';
37+
}
38+
}
39+
40+
export function useV2EditorController(context: PlaygroundDataV2): ComposedEditorProps {
41+
const [key, setKey] = useState<ConfigKey>("layout");
42+
const keyBox = useState(() => observable.box<ConfigKey>(key))[0];
43+
44+
const onViewSelectChange = (value: string): void => {
45+
let newKey: ConfigKey;
46+
if (value === "layout" || value === "config") {
47+
newKey = value;
48+
} else {
49+
const n = parseInt(value, 10);
50+
newKey = isNaN(n) ? "layout" : n;
51+
}
52+
setKey(newKey);
53+
runInAction(() => keyBox.set(newKey));
54+
};
55+
56+
const store = context.store;
57+
58+
const options: SelectOption[] = useMemo(() => {
59+
return [
60+
{ name: "Layout", value: "layout", isDefaultSelected: true },
61+
...store.data.map((trace, index) => ({
62+
name: (trace.name as string) || `trace ${index}`,
63+
value: index,
64+
isDefaultSelected: false
65+
})),
66+
{ name: "Configuration", value: "config", isDefaultSelected: false }
67+
];
68+
}, [store.data]);
69+
70+
const code = prettifyJson(getEditorCode(store, key));
71+
const [input, setInput] = useState(() => code);
72+
const onEditorChange = useCallback(
73+
(value: string): void => {
74+
setInput(value);
75+
try {
76+
// Parse string before sending to store
77+
const obj = JSON.parse(value);
78+
if (key === "layout") {
79+
store.setLayout(obj);
80+
} else if (key === "config") {
81+
store.setConfig(obj);
82+
} else {
83+
store.setDataAt(key, value);
84+
}
85+
// eslint-disable-next-line no-empty
86+
} catch {}
87+
},
88+
[store, key]
89+
);
90+
91+
useEffect(
92+
() =>
93+
reaction(
94+
() => getEditorCode(store, keyBox.get()),
95+
code => setInput(prettifyJson(code))
96+
),
97+
[store, keyBox]
98+
);
99+
100+
return {
101+
viewSelectValue: key.toString(),
102+
viewSelectOptions: options,
103+
onViewSelectChange,
104+
value: input,
105+
modelerCode: useMemo(() => JSON.stringify(getModelerCode(context, key), null, 2), [context, key]),
106+
onEditorChange
107+
};
108+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module "*.scss";

packages/pluggableWidgets/charts-web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- We disabled code-mirror from playground widget for compatibility. Only basic editor is available now. Charts widget not affected. This might impact developer experience when using playground.
12+
913
## [6.3.0] Charts - 2026-02-17
1014

1115
### [6.3.0] CustomChart

packages/pluggableWidgets/custom-chart-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@mendix/widget-plugin-platform": "workspace:*",
5151
"classnames": "^2.5.1",
5252
"deepmerge": "^4.3.1",
53+
"mobx-react-lite": "4.0.7",
5354
"plotly.js-dist-min": "^3.0.0"
5455
},
5556
"devDependencies": {

0 commit comments

Comments
 (0)