Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow custom tabs #56

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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 @@ -7,6 +7,7 @@ import type { StoreApi, UseBoundStore } from 'zustand';
import { create } from 'zustand';

import { mapToGraphEdge, mapToGraphEdges, mapToGraphNode, mapToGraphNodes } from '../dg-util';
import { BASE_TABS, type Tab } from '../graph/common-tab';
import type { useGraphClipboard } from '../hooks/use-graph-clipboard';
import type { CustomNodeSpecification } from '../nodes/custom-node/index';
import { NodeKind, type NodeSpecification } from '../nodes/specifications/specification-types';
Expand Down Expand Up @@ -63,6 +64,7 @@ export type DecisionGraphStoreType = {
activeTab: string;

customNodes: CustomNodeSpecification<object, string>[];
customTabs: Tab[];

panels?: PanelType[];
activePanel?: string;
Expand Down Expand Up @@ -141,6 +143,7 @@ export const DecisionGraphProvider: React.FC<React.PropsWithChildren<DecisionGra
configurable: true,
components: [],
customNodes: [],
customTabs: [...BASE_TABS],
activePanel: undefined,
panels: [],
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type DecisionGraphEmptyType = {

components?: DecisionGraphStoreType['state']['components'];
customNodes?: DecisionGraphStoreType['state']['customNodes'];
customTabs?: DecisionGraphStoreType['state']['customTabs'];

defaultActivePanel?: string;
panels?: DecisionGraphStoreType['state']['panels'];
Expand All @@ -41,6 +42,7 @@ export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
onChange,
components,
customNodes,
customTabs,
defaultActivePanel,
panels,
simulate,
Expand All @@ -50,8 +52,9 @@ export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
const mountedRef = useRef(false);
const graphActions = useDecisionGraphActions();
const { stateStore, listenerStore } = useDecisionGraphRaw();
const { decisionGraph } = useDecisionGraphState(({ decisionGraph }) => ({
const { decisionGraph, tabs } = useDecisionGraphState(({ decisionGraph, customTabs }) => ({
decisionGraph,
tabs: customTabs,
}));

const innerChange = useDebouncedCallback((graph: DecisionGraphType) => {
Expand All @@ -65,6 +68,7 @@ export const DecisionGraphEmpty: React.FC<DecisionGraphEmptyType> = ({
configurable,
components: Array.isArray(components) ? components : [],
customNodes: Array.isArray(customNodes) ? customNodes : [],
customTabs: Array.isArray(customTabs) ? [...tabs, ...customTabs] : tabs,
panels,
});
}, [id, disabled, configurable, components, customNodes, panels]);
Expand Down
26 changes: 12 additions & 14 deletions packages/jdm-editor/src/components/decision-graph/dg-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import type { GraphRef } from './graph/graph';
import { Graph } from './graph/graph';
import { GraphAside, type GraphAsideProps } from './graph/graph-aside';
import { GraphTabs } from './graph/graph-tabs';
import { TabDecisionTable } from './graph/tab-decision-table';
import { TabExpression } from './graph/tab-expression';
import { TabFunction } from './graph/tab-function';

export type DecisionGraphWrapperProps = {
reactFlowProOptions?: ProOptions;
Expand Down Expand Up @@ -48,15 +45,18 @@ export const DecisionGraphWrapper = React.memo(
);

const TabContents: React.FC = React.memo(() => {
const { openNodes, activeNodeId } = useDecisionGraphState(({ decisionGraph, openTabs, activeTab }) => {
const activeNodeId = (decisionGraph?.nodes ?? []).find((node) => node.id === activeTab)?.id;
const openNodes = (decisionGraph?.nodes ?? []).filter((node) => openTabs.includes(node.id));
const { openNodes, activeNodeId, tabs } = useDecisionGraphState(
({ decisionGraph, openTabs, activeTab, customTabs }) => {
const activeNodeId = (decisionGraph?.nodes ?? []).find((node) => node.id === activeTab)?.id;
const openNodes = (decisionGraph?.nodes ?? []).filter((node) => openTabs.includes(node.id));

return {
openNodes: openNodes.map(({ id, type }) => ({ id, type })),
activeNodeId,
};
});
return {
openNodes: openNodes.map(({ id, type }) => ({ id, type })),
activeNodeId,
tabs: customTabs,
};
},
);

const containerRef = useRef<HTMLDivElement>(null);
const dndManager = useMemo(() => {
Expand All @@ -69,9 +69,7 @@ const TabContents: React.FC = React.memo(() => {
<div style={{ display: 'contents' }} ref={containerRef}>
{openNodes.map((node) => (
<div key={node?.id} className={clsx(['tab-content', activeNodeId === node?.id && 'active'])}>
{node?.type === 'decisionTableNode' && <TabDecisionTable id={node.id} manager={dndManager} />}
{node?.type === 'expressionNode' && <TabExpression id={node.id} manager={dndManager} />}
{node?.type === 'functionNode' && <TabFunction id={node.id} />}
{tabs.find((tab) => tab.type === node?.type)?.tab({ id: node.id, manager: dndManager })}
</div>
))}
</div>
Expand Down
75 changes: 73 additions & 2 deletions packages/jdm-editor/src/components/decision-graph/dg.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ApartmentOutlined, ApiOutlined, LeftOutlined, PlayCircleOutlined, RightOutlined } from '@ant-design/icons';
import type { Meta, StoryObj } from '@storybook/react';
import { Select } from 'antd';
import { Button, Select } from 'antd';
import json5 from 'json5';
import React, { useRef, useState } from 'react';

import type { PanelType } from './context/dg-store.context';
import { type PanelType, useDecisionGraphActions } from './context/dg-store.context';
import { DecisionGraph } from './dg';
import { GraphSimulator } from './dg-simulator';
import { defaultGraph, defaultGraphCustomNode, defaultGraphUnknownNode } from './dg.stories-values';
import type { Tab } from './graph/common-tab';
import type { GraphRef } from './graph/graph';
import { createJdmNode } from './nodes/custom-node';
import { GraphNode } from './nodes/graph-node';
Expand Down Expand Up @@ -103,6 +104,76 @@ export const Extended: Story = {
},
};

const customTabsComponents: NodeSpecification[] = [
{
type: 'decisionNode',
displayName: 'Decision',
shortDescription: 'Execute decisions',
icon: <ApartmentOutlined />,
generateNode: () => ({ name: 'myDecision' }),
renderNode: ({ specification, id, selected, data }) => {
const graphActions = useDecisionGraphActions();
return (
<GraphNode
id={id}
specification={specification}
name={data.name}
isSelected={selected}
actions={[
<Button key='edit-table' type='link' onClick={() => graphActions.openTab(id)}>
Edit Table
</Button>,
]}
></GraphNode>
);
},
},
];

const customTabs: Tab[] = [
{
type: 'decisionNode',
tab: ({ id }) => {
const graphActions = useDecisionGraphActions();

return (
<input
onChange={(e) =>
graphActions.updateNode(id, (draft) => {
draft.content = e.target.value;
return draft;
})
}
></input>
);
},
},
];

export const CustomTab: Story = {
render: (args) => {
const ref = useRef<GraphRef>(null);
const [value, setValue] = useState<any>();

return (
<div
style={{
height: '100%',
}}
>
<DecisionGraph
{...args}
ref={ref}
value={value}
onChange={(val) => setValue(val)}
components={customTabsComponents}
customTabs={customTabs}
/>
</div>
);
},
};

const customNodes = [
createJdmNode({
kind: 'pingNode',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { DragDropManager } from 'dnd-core';
import { type ReactNode } from 'react';

import { TabDecisionTable } from './tab-decision-table';
import { TabExpression } from './tab-expression';
import { TabFunction } from './tab-function';

export type Tab = { type: string; tab: (p: CommonTabProps) => ReactNode };

export type CommonTabProps = {
id: string;
manager?: DragDropManager;
};

export const BASE_TABS: Tab[] = [
{ type: 'decisionTableNode', tab: (p) => <TabDecisionTable {...p} /> },
{ type: 'expressionNode', tab: (p) => <TabExpression {...p} /> },
{ type: 'functionNode', tab: (p) => <TabFunction {...p} /> },
] as const;
Loading