Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,10 @@
"verify": "rui-verify-package-format"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lint": "^6.8.5",
"@mendix/shared-charts": "workspace:*",
"@mendix/widget-plugin-component-kit": "workspace:*",
"@mendix/widget-plugin-hooks": "workspace:*",
"@mendix/widget-plugin-platform": "workspace:*",
"@uiw/codemirror-theme-github": "^4.23.13",
"@uiw/react-codemirror": "^4.23.13"
"@mendix/widget-plugin-platform": "workspace:*"
},
"devDependencies": {
"@mendix/automation-utils": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,19 @@
import { json, jsonParseLinter } from "@codemirror/lang-json";
import { linter, lintGutter } from "@codemirror/lint";
import { githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type Extension } from "@uiw/react-codemirror";
import { ReactElement, useEffect, useMemo, useRef, useState } from "react";

export type EditorChangeHandler = (value: string) => void;
import { ReactElement } from "react";

export interface CodeEditorProps {
defaultValue: string;
onChange?: EditorChangeHandler;
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
height?: string;
}

export function CodeEditor(props: CodeEditorProps): ReactElement {
const [value, onChange] = useEditorState({ initState: props.defaultValue, onChange: props.onChange });
const extensions = useMemo<Extension[]>(
() => [
json(),
linter(jsonParseLinter(), {
// default is 750ms
delay: 300
}),
lintGutter()
],
[]
);
return (
<CodeMirror
height={props.height}
value={value}
onChange={onChange}
theme={githubLight}
<textarea
value={props.value}
onChange={e => props.onChange?.(e.target.value)}
style={{ height: props.height ?? "200px", width: "100%", fontFamily: "monospace" }}
readOnly={props.readOnly}
extensions={extensions}
/>
);
}

function useEditorState(params: { initState: string; onChange?: EditorChangeHandler }): [string, EditorChangeHandler] {
const listener = useRef(params.onChange);
const [value, setValue] = useState(params.initState);
const { current: onValueChange } = useRef<EditorChangeHandler>(value => {
setValue(value);
listener.current?.(value);
});
useEffect(() => {
listener.current = params.onChange;
});
return [value, onValueChange];
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { useOnClickOutside } from "@mendix/widget-plugin-hooks/useOnClickOutside";
import classNames from "classnames";
import { Fragment, ReactElement, ReactNode, RefObject, useCallback, useRef, useState } from "react";
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { useOnClickOutside } from "@mendix/widget-plugin-hooks/useOnClickOutside";
import "../ui/Playground.scss";
import { CodeEditor } from "./CodeEditor";
import { Select, SelectOption, Sidebar, SidebarHeader, SidebarHeaderTools, SidebarPanel } from "./Sidebar";
import { CodeEditor, EditorChangeHandler } from "./CodeEditor";

interface WrapperProps {
renderPanels: ReactNode;
Expand Down Expand Up @@ -71,8 +71,8 @@ const SidebarContentTooltip = (): ReactElement => {
};

export interface ComposedEditorProps {
defaultEditorValue: string;
onEditorChange: EditorChangeHandler;
value: string;
onEditorChange: (value: string) => void;
modelerCode: string;
onViewSelectChange: (value: string) => void;
viewSelectValue: string;
Expand Down Expand Up @@ -138,11 +138,7 @@ export function ComposedEditor(props: ComposedEditorProps): ReactElement {
heading={topPanelHeader}
>
<TabGuard>
<CodeEditor
defaultValue={props.defaultEditorValue}
onChange={props.onEditorChange}
height="var(--editor-h)"
/>
<CodeEditor value={props.value} onChange={props.onEditorChange} height="var(--editor-h)" />
</TabGuard>
</SidebarPanel>
<SidebarPanel
Expand All @@ -154,7 +150,7 @@ export function ComposedEditor(props: ComposedEditorProps): ReactElement {
<CodeEditor
key={props.modelerCode}
readOnly
defaultValue={props.modelerCode}
value={props.modelerCode}
height="var(--static-settings-h)"
/>
</SidebarPanel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { PlaygroundData, usePlaygroundContext } from "@mendix/shared-charts/main";
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { ReactElement } from "react";
import { useComposedEditorController } from "../helpers/useComposedEditorController";
import { PlaygroundDataV1, PlaygroundDataV2, usePlaygroundContext } from "@mendix/shared-charts/main";
import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { ComposedEditor } from "./ComposedEditor";
import { useComposedEditorController } from "../helpers/useComposedEditorController";
import "../ui/Playground.scss";
import { useV2EditorController } from "../helpers/useV2EdtiorController";

function Editor({ data }: { data: PlaygroundData }): ReactElement {
function EditorGen1({ data }: { data: PlaygroundDataV1 }): ReactElement {
const props = useComposedEditorController(data);

return <ComposedEditor {...props} />;
}

function EditorGen2({ data }: { data: PlaygroundDataV2 }): ReactElement {
const props = useV2EditorController(data);

return <ComposedEditor {...props} />;
}

export function Playground(): ReactElement {
const ctx = usePlaygroundContext();

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

return <Editor data={ctx.data} />;
const { data } = ctx;

if (Object.hasOwn(data, "type") && (data as PlaygroundDataV2).type === "editor.data.v2") {
return <EditorGen2 data={data as PlaygroundDataV2} />;
}

return <EditorGen1 data={data as PlaygroundDataV1} />;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fallback, PlaygroundData } from "@mendix/shared-charts/main";
import { EditorChangeHandler } from "../components/CodeEditor";
import { useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { fallback, PlaygroundDataV1 } from "@mendix/shared-charts/main";
import { ComposedEditorProps } from "../components/ComposedEditor";
import { SelectOption } from "../components/Sidebar";

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

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

function getEditorCode({ store }: PlaygroundData, key: ConfigKey): string {
function getEditorCode({ store }: PlaygroundDataV1, key: ConfigKey): string {
let value = typeof key === "number" ? store.state.data.at(key) : store.state[key];
value = value ?? '{ "error": "value is unavailable" }';
return value;
}

function getModelerCode(data: PlaygroundData, key: ConfigKey): Partial<Data> | Partial<Layout> | Partial<Config> {
function getModelerCode(data: PlaygroundDataV1, key: ConfigKey): Partial<Data> | Partial<Layout> | Partial<Config> {
if (key === "layout") {
return data.layoutOptions;
}
Expand All @@ -31,8 +30,15 @@ function getModelerCode(data: PlaygroundData, key: ConfigKey): Partial<Data> | P
const entries = Object.entries(data.plotData.at(key) ?? {}).filter(([key]) => !irrelevantSeriesKeys.includes(key));
return Object.fromEntries(entries) as Partial<Data>;
}
function prettifyJson(json: string): string {
try {
return JSON.stringify(JSON.parse(json), null, 2);
} catch {
return '{ "error": "invalid JSON" }';
}
}

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

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

const onEditorChange: EditorChangeHandler = (json): void => {
json = fallback(json);
try {
JSON.parse(json);
data.store.set(key, json);
// eslint-disable-next-line no-empty
} catch {}
};
const store = data.store;
const code = prettifyJson(getEditorCode(data, key));
const [input, setInput] = useState(() => code);
const onEditorChange = useCallback(
(value: string): void => {
setInput(value);
try {
const json = fallback(value);
JSON.parse(value);
store.set(key, json);
// eslint-disable-next-line no-empty
} catch {}
},
[store, key]
);

useEffect(
() =>
// eslint-disable-next-line react-hooks/set-state-in-effect
setInput(code),
[code]
);

return {
viewSelectValue: key.toString(),
viewSelectOptions: options,
onViewSelectChange,
defaultEditorValue: getEditorCode(data, key),
value: input,
modelerCode: useMemo(() => JSON.stringify(getModelerCode(data, key), null, 2), [data, key]),
onEditorChange
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { observable, reaction, runInAction } from "mobx";
import { useCallback, useEffect, useMemo, useState } from "react";
import { PlaygroundDataV2 } from "@mendix/shared-charts/main";
import { ComposedEditorProps } from "../components/ComposedEditor";
import { SelectOption } from "../components/Sidebar";

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

const irrelevantSeriesKeys = ["x", "y", "z", "customSeriesOptions", "dataSourceItems"];

function getEditorCode(store: PlaygroundDataV2["store"], key: ConfigKey): string {
if (key === "layout") {
return store.layoutJson ?? '{ "error": "value is unavailable" }';
}
if (key === "config") {
return store.configJson ?? '{ "error": "value is unavailable" }';
}
return store.dataJson.at(key) ?? '{ "error": "value is unavailable" }';
}

function getModelerCode(data: PlaygroundDataV2, key: ConfigKey): object {
if (key === "layout") {
return data.layoutOptions;
}
if (key === "config") {
return data.configOptions;
}
const entries = Object.entries(data.plotData.at(key) ?? {}).filter(([k]) => !irrelevantSeriesKeys.includes(k));
return Object.fromEntries(entries);
}

function prettifyJson(json: string): string {
try {
return JSON.stringify(JSON.parse(json), null, 2);
} catch {
return '{ "error": "invalid JSON" }';
}
}

export function useV2EditorController(context: PlaygroundDataV2): ComposedEditorProps {
const [key, setKey] = useState<ConfigKey>("layout");
const keyBox = useState(() => observable.box<ConfigKey>(key))[0];

const onViewSelectChange = (value: string): void => {
let newKey: ConfigKey;
if (value === "layout" || value === "config") {
newKey = value;
} else {
const n = parseInt(value, 10);
newKey = isNaN(n) ? "layout" : n;
}
setKey(newKey);
runInAction(() => keyBox.set(newKey));
};

const store = context.store;

const options: SelectOption[] = useMemo(() => {
return [
{ name: "Layout", value: "layout", isDefaultSelected: true },
...store.data.map((trace, index) => ({
name: (trace.name as string) || `trace ${index}`,
value: index,
isDefaultSelected: false
})),
{ name: "Configuration", value: "config", isDefaultSelected: false }
];
}, [store.data]);

const code = prettifyJson(getEditorCode(store, key));
const [input, setInput] = useState(() => code);
const onEditorChange = useCallback(
(value: string): void => {
setInput(value);
try {
// Parse string before sending to store
const obj = JSON.parse(value);
if (key === "layout") {
store.setLayout(obj);
} else if (key === "config") {
store.setConfig(obj);
} else {
store.setDataAt(key, value);
}
// eslint-disable-next-line no-empty
} catch {}
},
[store, key]
);

useEffect(
() =>
reaction(
() => getEditorCode(store, keyBox.get()),
code => setInput(prettifyJson(code))
),
[store, keyBox]
);

return {
viewSelectValue: key.toString(),
viewSelectOptions: options,
onViewSelectChange,
value: input,
modelerCode: useMemo(() => JSON.stringify(getModelerCode(context, key), null, 2), [context, key]),
onEditorChange
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.scss";
1 change: 1 addition & 0 deletions packages/pluggableWidgets/custom-chart-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@mendix/widget-plugin-platform": "workspace:*",
"classnames": "^2.5.1",
"deepmerge": "^4.3.1",
"mobx-react-lite": "4.0.7",
"plotly.js-dist-min": "^3.0.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { constructWrapperStyle, getPlaygroundContext } from "@mendix/shared-charts/main";
import { Fragment, ReactElement } from "react";
import { constructWrapperStyle, getPlaygroundContext } from "@mendix/shared-charts/main";
import { CustomChartContainerProps } from "../typings/CustomChartProps";
import { useCustomChart } from "./hooks/useCustomChart";
import "./ui/CustomChart.scss";
import { observer } from "mobx-react-lite";

const PlaygroundContext = getPlaygroundContext();

export default function CustomChart(props: CustomChartContainerProps): ReactElement {
const Container = observer(function CustomChart(props: CustomChartContainerProps): ReactElement {
const { playgroundData, ref } = useCustomChart(props);
const wrapperStyle = constructWrapperStyle(props);

Expand All @@ -16,4 +17,8 @@ export default function CustomChart(props: CustomChartContainerProps): ReactElem
<div ref={ref} className="widget-custom-chart" style={wrapperStyle} tabIndex={props.tabIndex} />
</Fragment>
);
});

export default function CustomChart(props: CustomChartContainerProps): ReactElement {
return <Container {...props} />;
}
Loading
Loading