@umodeapporg/ui 0.30.1 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/umode-ui.css +32 -0
- package/dist/components/UI/BulkActionBar.js +1 -1
- package/dist/components/UI/BulkActionBar.js.map +1 -1
- package/dist/components/UI/Flow/FlowAnnotationDraftPreview.d.ts +6 -0
- package/dist/components/UI/Flow/FlowAnnotationDraftPreview.js +65 -0
- package/dist/components/UI/Flow/FlowAnnotationDraftPreview.js.map +1 -0
- package/dist/components/UI/Flow/FlowAnnotationNode.d.ts +34 -0
- package/dist/components/UI/Flow/FlowAnnotationNode.js +231 -0
- package/dist/components/UI/Flow/FlowAnnotationNode.js.map +1 -0
- package/dist/components/UI/Flow/FlowAnnotationToolbar.d.ts +32 -0
- package/dist/components/UI/Flow/FlowAnnotationToolbar.js +298 -0
- package/dist/components/UI/Flow/FlowAnnotationToolbar.js.map +1 -0
- package/dist/components/UI/Flow/FlowCanvas.d.ts +2 -0
- package/dist/components/UI/Flow/FlowCanvas.js +278 -20
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -1
- package/dist/components/UI/Flow/FlowInspector.js +6 -5
- package/dist/components/UI/Flow/FlowInspector.js.map +1 -1
- package/dist/components/UI/Flow/FlowSelectionBar.d.ts +10 -0
- package/dist/components/UI/Flow/FlowSelectionBar.js +56 -0
- package/dist/components/UI/Flow/FlowSelectionBar.js.map +1 -0
- package/dist/components/UI/Flow/flowAnnotations.d.ts +38 -0
- package/dist/components/UI/Flow/flowAnnotations.js +123 -0
- package/dist/components/UI/Flow/flowAnnotations.js.map +1 -0
- package/dist/components/UI/Flow/flowEditorStore.d.ts +29 -5
- package/dist/components/UI/Flow/flowEditorStore.js +123 -23
- package/dist/components/UI/Flow/flowEditorStore.js.map +1 -1
- package/dist/components/UI/Flow/flowTypes.d.ts +30 -0
- package/dist/components/UI/Flow/index.d.ts +2 -1
- package/dist/components/UI/Flow/index.js +5 -1
- package/dist/components/UI/Flow/index.js.map +1 -1
- package/dist/components/UI/Flow/useFlowAnnotationDrawing.d.ts +25 -0
- package/dist/components/UI/Flow/useFlowAnnotationDrawing.js +117 -0
- package/dist/components/UI/Flow/useFlowAnnotationDrawing.js.map +1 -0
- package/dist/locales/en/ui.json.d.ts +41 -0
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +82 -0
- package/dist/locales/pt/ui.json.d.ts +41 -0
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
- package/src/assets/fonts/LICENSE-Virgil.txt +45 -0
- package/src/assets/fonts/virgil.woff2 +0 -0
- package/src/assets/styles/base.css +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { isFlowNodeRemovable } from './flowGraph';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const
|
|
1
|
+
{"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { selectFlowInspectedNodeId } from './flowEditorStore';\nimport { isFlowNodeRemovable } from './flowGraph';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNode = useFlowEditor((state) => {\n const inspectedNodeId = selectFlowInspectedNodeId(state);\n return inspectedNodeId ? state.graph.nodes.find((node) => node.nodeId === inspectedNodeId) : undefined;\n });\n\n const shellClass = mergeClasses(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n const canDuplicate = definition?.category !== 'trigger';\n const canRemove = isFlowNodeRemovable(selectedNode, registry);\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n aria-label={t('flow.inspector.close')}\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (canDuplicate || canRemove) && (\n <div className=\"flex gap-x-2\">\n {canDuplicate && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n className=\"normal-case\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n {canRemove && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n className=\"normal-case\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n )}\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface IFlowSelectionBarProps {
|
|
2
|
+
selectedCount: number;
|
|
3
|
+
canDelete: boolean;
|
|
4
|
+
onDuplicate: () => void;
|
|
5
|
+
onDelete: () => void;
|
|
6
|
+
onClear: () => void;
|
|
7
|
+
}
|
|
8
|
+
/** What can be done with several selected items at once: blocks and annotations alike. */
|
|
9
|
+
declare const FlowSelectionBar: React.FC<IFlowSelectionBarProps>;
|
|
10
|
+
export default FlowSelectionBar;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
4
|
+
import ControlledTooltip from "../ControlledTooltip.js";
|
|
5
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
6
|
+
const SelectionAction = ({ icon, label, onClick, disabled, danger }) => /* @__PURE__ */ jsxs(
|
|
7
|
+
ControlledButton,
|
|
8
|
+
{
|
|
9
|
+
defaultHtmlButton: true,
|
|
10
|
+
onClick,
|
|
11
|
+
disabled,
|
|
12
|
+
className: danger ? "interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-danger-emphasis hover:bg-danger-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40" : "interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40",
|
|
13
|
+
children: [
|
|
14
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { icon, size: 18, "aria-hidden": true }),
|
|
15
|
+
/* @__PURE__ */ jsx("span", { children: label })
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
const FlowSelectionBar = ({
|
|
20
|
+
selectedCount,
|
|
21
|
+
canDelete,
|
|
22
|
+
onDuplicate,
|
|
23
|
+
onDelete,
|
|
24
|
+
onClear
|
|
25
|
+
}) => {
|
|
26
|
+
const { t } = useTranslation("ui");
|
|
27
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-1 rounded-xl border border-border bg-surface p-1 shadow-md", children: [
|
|
28
|
+
/* @__PURE__ */ jsx("span", { className: "px-2 text-sm font-medium text-foreground", children: t("flow.selection.count", { count: selectedCount }) }),
|
|
29
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-5 w-px bg-border" }),
|
|
30
|
+
/* @__PURE__ */ jsx(SelectionAction, { icon: "content_copy", label: t("flow.selection.duplicate"), onClick: onDuplicate }),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
SelectionAction,
|
|
33
|
+
{
|
|
34
|
+
icon: "delete",
|
|
35
|
+
label: t("flow.selection.delete"),
|
|
36
|
+
onClick: onDelete,
|
|
37
|
+
disabled: !canDelete,
|
|
38
|
+
danger: true
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
/* @__PURE__ */ jsx(ControlledTooltip, { content: t("flow.selection.clear"), placement: "top", children: /* @__PURE__ */ jsx(
|
|
42
|
+
ControlledButton,
|
|
43
|
+
{
|
|
44
|
+
"aria-label": t("flow.selection.clear"),
|
|
45
|
+
defaultHtmlButton: true,
|
|
46
|
+
onClick: onClear,
|
|
47
|
+
className: "interactive flex size-8 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
48
|
+
children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close", size: 18, "aria-hidden": true })
|
|
49
|
+
}
|
|
50
|
+
) })
|
|
51
|
+
] });
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
FlowSelectionBar as default
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=FlowSelectionBar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowSelectionBar.js","sources":["../../../../src/components/UI/Flow/FlowSelectionBar.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\ninterface IFlowSelectionBarProps {\n selectedCount: number;\n canDelete: boolean;\n onDuplicate: () => void;\n onDelete: () => void;\n onClear: () => void;\n}\n\ninterface ISelectionActionProps {\n icon: SymbolCodepoints;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n danger?: boolean;\n}\n\nconst SelectionAction: React.FC<ISelectionActionProps> = ({ icon, label, onClick, disabled, danger }) => (\n <ControlledButton\n defaultHtmlButton\n onClick={onClick}\n disabled={disabled}\n className={\n danger\n ? 'interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-danger-emphasis hover:bg-danger-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40'\n : 'interactive flex h-8 items-center gap-x-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40'\n }\n >\n <MaterialSymbol icon={icon} size={18} aria-hidden />\n <span>{label}</span>\n </ControlledButton>\n);\n\n/** What can be done with several selected items at once: blocks and annotations alike. */\nconst FlowSelectionBar: React.FC<IFlowSelectionBarProps> = ({\n selectedCount,\n canDelete,\n onDuplicate,\n onDelete,\n onClear,\n}) => {\n const { t } = useTranslation('ui');\n\n return (\n <div className=\"flex items-center gap-x-1 rounded-xl border border-border bg-surface p-1 shadow-md\">\n <span className=\"px-2 text-sm font-medium text-foreground\">\n {t('flow.selection.count', { count: selectedCount })}\n </span>\n <span aria-hidden className=\"h-5 w-px bg-border\" />\n <SelectionAction icon=\"content_copy\" label={t('flow.selection.duplicate')} onClick={onDuplicate} />\n <SelectionAction\n icon=\"delete\"\n label={t('flow.selection.delete')}\n onClick={onDelete}\n disabled={!canDelete}\n danger\n />\n <ControlledTooltip content={t('flow.selection.clear')} placement=\"top\">\n <ControlledButton\n aria-label={t('flow.selection.clear')}\n defaultHtmlButton\n onClick={onClear}\n className=\"interactive flex size-8 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n};\n\nexport default FlowSelectionBar;\n"],"names":[],"mappings":";;;;;AAsBA,MAAM,kBAAmD,CAAC,EAAE,MAAM,OAAO,SAAS,UAAU,aAC1F;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,mBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA,WACE,SACI,yPACA;AAAA,IAGN,UAAA;AAAA,MAAA,oBAAC,gBAAA,EAAe,MAAY,MAAM,IAAI,eAAW,MAAC;AAAA,MAClD,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,IAAA;AAAA,EAAA;AACf;AAIF,MAAM,mBAAqD,CAAC;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,SACE,qBAAC,OAAA,EAAI,WAAU,sFACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,4CACb,UAAA,EAAE,wBAAwB,EAAE,OAAO,cAAA,CAAe,EAAA,CACrD;AAAA,IACA,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,sBAAqB;AAAA,IACjD,oBAAC,mBAAgB,MAAK,gBAAe,OAAO,EAAE,0BAA0B,GAAG,SAAS,aAAa;AAAA,IACjG;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,EAAE,uBAAuB;AAAA,QAChC,SAAS;AAAA,QACT,UAAU,CAAC;AAAA,QACX,QAAM;AAAA,MAAA;AAAA,IAAA;AAAA,wBAEP,mBAAA,EAAkB,SAAS,EAAE,sBAAsB,GAAG,WAAU,OAC/D,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,cAAY,EAAE,sBAAsB;AAAA,QACpC,mBAAiB;AAAA,QACjB,SAAS;AAAA,QACT,WAAU;AAAA,QAEV,8BAAC,gBAAA,EAAe,MAAK,SAAQ,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,MAAA;AAAA,IAAA,EACrD,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IFlowAnnotation, IFlowPosition, IFlowSize, TFlowAnnotationKind, TFlowAnnotationStroke, TFlowAnnotationTone } from './flowTypes';
|
|
2
|
+
export declare const FLOW_ANNOTATION_KINDS: Array<TFlowAnnotationKind>;
|
|
3
|
+
export declare const FLOW_ANNOTATION_TONES: Array<TFlowAnnotationTone>;
|
|
4
|
+
export declare const FLOW_ANNOTATION_STROKES: Array<TFlowAnnotationStroke>;
|
|
5
|
+
export declare const FLOW_ANNOTATION_STROKE_WIDTH: Record<TFlowAnnotationStroke, number>;
|
|
6
|
+
/** A box never collapses to nothing: a zero-wide arrow or a dot still has to be grabbed. */
|
|
7
|
+
export declare const FLOW_ANNOTATION_MIN_SIZE = 8;
|
|
8
|
+
/** Puts the first line of a new note on the point that was clicked, not below it. */
|
|
9
|
+
export declare const FLOW_TEXT_CARET_OFFSET = 14;
|
|
10
|
+
/** Keeps a long scribble from bloating the saved flow. */
|
|
11
|
+
export declare const FLOW_DRAWING_MAX_POINTS = 2000;
|
|
12
|
+
export declare const strokeWidthOf: (annotation: Pick<IFlowAnnotation, "stroke">) => number;
|
|
13
|
+
/** The number a new step takes: one past the highest on the canvas. */
|
|
14
|
+
export declare const nextFlowStepNumber: (annotations: Array<IFlowAnnotation>) => number;
|
|
15
|
+
/**
|
|
16
|
+
* A freehand stroke as a smooth path: each recorded point becomes the control of a curve through the
|
|
17
|
+
* midpoints, which takes the jitter of the pointer out without moving the stroke.
|
|
18
|
+
*/
|
|
19
|
+
export declare const smoothStrokePath: (points: Array<IFlowPosition>) => string;
|
|
20
|
+
/** Coerces what a host stored: an entry the canvas cannot draw is dropped, never rendered half-broken. */
|
|
21
|
+
export declare const normalizeFlowAnnotations: (annotations: unknown) => Array<IFlowAnnotation>;
|
|
22
|
+
/**
|
|
23
|
+
* Turns points in canvas coordinates into an annotation box: its position is the top-left corner
|
|
24
|
+
* (padded so the stroke is not clipped) and the points become relative to it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const boxAroundPoints: (points: Array<IFlowPosition>, padding: number) => {
|
|
27
|
+
position: IFlowPosition;
|
|
28
|
+
size: IFlowSize;
|
|
29
|
+
points: Array<IFlowPosition>;
|
|
30
|
+
};
|
|
31
|
+
/** A box spanning two corners dragged in any direction. */
|
|
32
|
+
export declare const boxBetween: (start: IFlowPosition, end: IFlowPosition) => {
|
|
33
|
+
position: IFlowPosition;
|
|
34
|
+
size: IFlowSize;
|
|
35
|
+
};
|
|
36
|
+
/** Scales a stroke with its box when the box is resized. */
|
|
37
|
+
export declare const scaleAnnotationPoints: (points: Array<IFlowPosition> | undefined, from: IFlowSize, to: IFlowSize) => Array<IFlowPosition> | undefined;
|
|
38
|
+
export declare const cloneFlowAnnotations: (annotations: Array<IFlowAnnotation>, offset: number) => Array<IFlowAnnotation>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { generateUUID } from "../../../helpers/utilityHelpers.js";
|
|
2
|
+
const FLOW_ANNOTATION_KINDS = [
|
|
3
|
+
"text",
|
|
4
|
+
"rectangle",
|
|
5
|
+
"ellipse",
|
|
6
|
+
"line",
|
|
7
|
+
"arrow",
|
|
8
|
+
"step",
|
|
9
|
+
"drawing"
|
|
10
|
+
];
|
|
11
|
+
const FLOW_ANNOTATION_TONES = [
|
|
12
|
+
"danger",
|
|
13
|
+
"warning",
|
|
14
|
+
"success",
|
|
15
|
+
"info",
|
|
16
|
+
"brand",
|
|
17
|
+
"neutral"
|
|
18
|
+
];
|
|
19
|
+
const FLOW_ANNOTATION_STROKES = ["thin", "medium", "thick"];
|
|
20
|
+
const FLOW_ANNOTATION_STROKE_WIDTH = {
|
|
21
|
+
thin: 2,
|
|
22
|
+
medium: 3.5,
|
|
23
|
+
thick: 6
|
|
24
|
+
};
|
|
25
|
+
const FLOW_ANNOTATION_MIN_SIZE = 8;
|
|
26
|
+
const FLOW_TEXT_CARET_OFFSET = 14;
|
|
27
|
+
const FLOW_DRAWING_MAX_POINTS = 2e3;
|
|
28
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
29
|
+
const isPoint = (value) => isFiniteNumber(value?.x) && isFiniteNumber(value?.y);
|
|
30
|
+
const strokeWidthOf = (annotation) => FLOW_ANNOTATION_STROKE_WIDTH[annotation.stroke ?? "medium"] ?? FLOW_ANNOTATION_STROKE_WIDTH.medium;
|
|
31
|
+
const nextFlowStepNumber = (annotations) => annotations.reduce((highest, annotation) => Math.max(highest, annotation.step ?? 0), 0) + 1;
|
|
32
|
+
const smoothStrokePath = (points) => {
|
|
33
|
+
const [first, ...rest] = points;
|
|
34
|
+
if (!first) return "";
|
|
35
|
+
if (rest.length === 0) return `M ${first.x} ${first.y} l 0 0`;
|
|
36
|
+
const curves = rest.map((point, index) => {
|
|
37
|
+
const previous = points[index] ?? first;
|
|
38
|
+
return `Q ${previous.x} ${previous.y} ${(previous.x + point.x) / 2} ${(previous.y + point.y) / 2}`;
|
|
39
|
+
});
|
|
40
|
+
const last = rest[rest.length - 1] ?? first;
|
|
41
|
+
return `M ${first.x} ${first.y} ${curves.join(" ")} L ${last.x} ${last.y}`;
|
|
42
|
+
};
|
|
43
|
+
const normalizeFlowAnnotations = (annotations) => {
|
|
44
|
+
if (!Array.isArray(annotations)) return [];
|
|
45
|
+
return annotations.flatMap((candidate) => {
|
|
46
|
+
const annotation = candidate;
|
|
47
|
+
if (!annotation || typeof annotation.annotationId !== "string" || !annotation.annotationId) return [];
|
|
48
|
+
if (!FLOW_ANNOTATION_KINDS.includes(annotation.kind)) return [];
|
|
49
|
+
if (!isPoint(annotation.position)) return [];
|
|
50
|
+
const width = isFiniteNumber(annotation.size?.width) ? annotation.size.width : FLOW_ANNOTATION_MIN_SIZE;
|
|
51
|
+
const height = isFiniteNumber(annotation.size?.height) ? annotation.size.height : FLOW_ANNOTATION_MIN_SIZE;
|
|
52
|
+
const points = Array.isArray(annotation.points) ? annotation.points.filter(isPoint) : void 0;
|
|
53
|
+
const needsTwoPoints = annotation.kind === "arrow" || annotation.kind === "line";
|
|
54
|
+
if (needsTwoPoints && (points?.length ?? 0) < 2 || annotation.kind === "drawing" && !points?.length) return [];
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
annotationId: annotation.annotationId,
|
|
58
|
+
kind: annotation.kind,
|
|
59
|
+
position: { x: annotation.position.x, y: annotation.position.y },
|
|
60
|
+
size: {
|
|
61
|
+
width: Math.max(width, FLOW_ANNOTATION_MIN_SIZE),
|
|
62
|
+
height: Math.max(height, FLOW_ANNOTATION_MIN_SIZE)
|
|
63
|
+
},
|
|
64
|
+
tone: FLOW_ANNOTATION_TONES.includes(annotation.tone) ? annotation.tone : "neutral",
|
|
65
|
+
...FLOW_ANNOTATION_STROKES.includes(annotation.stroke) ? { stroke: annotation.stroke } : {},
|
|
66
|
+
...typeof annotation.text === "string" ? { text: annotation.text } : {},
|
|
67
|
+
...isFiniteNumber(annotation.step) ? { step: annotation.step } : {},
|
|
68
|
+
...points ? { points } : {}
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const boxAroundPoints = (points, padding) => {
|
|
74
|
+
const xs = points.map((point) => point.x);
|
|
75
|
+
const ys = points.map((point) => point.y);
|
|
76
|
+
const left = Math.min(...xs) - padding;
|
|
77
|
+
const top = Math.min(...ys) - padding;
|
|
78
|
+
return {
|
|
79
|
+
position: { x: left, y: top },
|
|
80
|
+
size: {
|
|
81
|
+
width: Math.max(Math.max(...xs) - left + padding, FLOW_ANNOTATION_MIN_SIZE),
|
|
82
|
+
height: Math.max(Math.max(...ys) - top + padding, FLOW_ANNOTATION_MIN_SIZE)
|
|
83
|
+
},
|
|
84
|
+
points: points.map((point) => ({ x: point.x - left, y: point.y - top }))
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const boxBetween = (start, end) => ({
|
|
88
|
+
position: { x: Math.min(start.x, end.x), y: Math.min(start.y, end.y) },
|
|
89
|
+
size: {
|
|
90
|
+
width: Math.max(Math.abs(end.x - start.x), FLOW_ANNOTATION_MIN_SIZE),
|
|
91
|
+
height: Math.max(Math.abs(end.y - start.y), FLOW_ANNOTATION_MIN_SIZE)
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const scaleAnnotationPoints = (points, from, to) => {
|
|
95
|
+
if (!points) return void 0;
|
|
96
|
+
const scaleX = from.width > 0 ? to.width / from.width : 1;
|
|
97
|
+
const scaleY = from.height > 0 ? to.height / from.height : 1;
|
|
98
|
+
return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));
|
|
99
|
+
};
|
|
100
|
+
const cloneFlowAnnotations = (annotations, offset) => annotations.map((annotation) => ({
|
|
101
|
+
...annotation,
|
|
102
|
+
annotationId: generateUUID(),
|
|
103
|
+
position: { x: annotation.position.x + offset, y: annotation.position.y + offset },
|
|
104
|
+
points: annotation.points?.map((point) => ({ ...point }))
|
|
105
|
+
}));
|
|
106
|
+
export {
|
|
107
|
+
FLOW_ANNOTATION_KINDS,
|
|
108
|
+
FLOW_ANNOTATION_MIN_SIZE,
|
|
109
|
+
FLOW_ANNOTATION_STROKES,
|
|
110
|
+
FLOW_ANNOTATION_STROKE_WIDTH,
|
|
111
|
+
FLOW_ANNOTATION_TONES,
|
|
112
|
+
FLOW_DRAWING_MAX_POINTS,
|
|
113
|
+
FLOW_TEXT_CARET_OFFSET,
|
|
114
|
+
boxAroundPoints,
|
|
115
|
+
boxBetween,
|
|
116
|
+
cloneFlowAnnotations,
|
|
117
|
+
nextFlowStepNumber,
|
|
118
|
+
normalizeFlowAnnotations,
|
|
119
|
+
scaleAnnotationPoints,
|
|
120
|
+
smoothStrokePath,
|
|
121
|
+
strokeWidthOf
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=flowAnnotations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowAnnotations.js","sources":["../../../../src/components/UI/Flow/flowAnnotations.ts"],"sourcesContent":["import { generateUUID } from '@/helpers/utilityHelpers';\n\nimport type {\n IFlowAnnotation,\n IFlowPosition,\n IFlowSize,\n TFlowAnnotationKind,\n TFlowAnnotationStroke,\n TFlowAnnotationTone,\n} from './flowTypes';\n\nexport const FLOW_ANNOTATION_KINDS: Array<TFlowAnnotationKind> = [\n 'text',\n 'rectangle',\n 'ellipse',\n 'line',\n 'arrow',\n 'step',\n 'drawing',\n];\n\nexport const FLOW_ANNOTATION_TONES: Array<TFlowAnnotationTone> = [\n 'danger',\n 'warning',\n 'success',\n 'info',\n 'brand',\n 'neutral',\n];\n\nexport const FLOW_ANNOTATION_STROKES: Array<TFlowAnnotationStroke> = ['thin', 'medium', 'thick'];\n\nexport const FLOW_ANNOTATION_STROKE_WIDTH: Record<TFlowAnnotationStroke, number> = {\n thin: 2,\n medium: 3.5,\n thick: 6,\n};\n\n/** A box never collapses to nothing: a zero-wide arrow or a dot still has to be grabbed. */\nexport const FLOW_ANNOTATION_MIN_SIZE = 8;\n/** Puts the first line of a new note on the point that was clicked, not below it. */\nexport const FLOW_TEXT_CARET_OFFSET = 14;\n/** Keeps a long scribble from bloating the saved flow. */\nexport const FLOW_DRAWING_MAX_POINTS = 2000;\n\nconst isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);\n\nconst isPoint = (value: unknown): value is IFlowPosition =>\n isFiniteNumber((value as IFlowPosition | undefined)?.x) && isFiniteNumber((value as IFlowPosition | undefined)?.y);\n\nexport const strokeWidthOf = (annotation: Pick<IFlowAnnotation, 'stroke'>): number =>\n FLOW_ANNOTATION_STROKE_WIDTH[annotation.stroke ?? 'medium'] ?? FLOW_ANNOTATION_STROKE_WIDTH.medium;\n\n/** The number a new step takes: one past the highest on the canvas. */\nexport const nextFlowStepNumber = (annotations: Array<IFlowAnnotation>): number =>\n annotations.reduce((highest, annotation) => Math.max(highest, annotation.step ?? 0), 0) + 1;\n\n/**\n * A freehand stroke as a smooth path: each recorded point becomes the control of a curve through the\n * midpoints, which takes the jitter of the pointer out without moving the stroke.\n */\nexport const smoothStrokePath = (points: Array<IFlowPosition>): string => {\n const [first, ...rest] = points;\n if (!first) return '';\n if (rest.length === 0) return `M ${first.x} ${first.y} l 0 0`;\n const curves = rest.map((point, index) => {\n const previous = points[index] ?? first;\n return `Q ${previous.x} ${previous.y} ${(previous.x + point.x) / 2} ${(previous.y + point.y) / 2}`;\n });\n const last = rest[rest.length - 1] ?? first;\n return `M ${first.x} ${first.y} ${curves.join(' ')} L ${last.x} ${last.y}`;\n};\n\n/** Coerces what a host stored: an entry the canvas cannot draw is dropped, never rendered half-broken. */\nexport const normalizeFlowAnnotations = (annotations: unknown): Array<IFlowAnnotation> => {\n if (!Array.isArray(annotations)) return [];\n return annotations.flatMap((candidate): Array<IFlowAnnotation> => {\n const annotation = candidate as Partial<IFlowAnnotation> | null;\n if (!annotation || typeof annotation.annotationId !== 'string' || !annotation.annotationId) return [];\n if (!FLOW_ANNOTATION_KINDS.includes(annotation.kind as TFlowAnnotationKind)) return [];\n if (!isPoint(annotation.position)) return [];\n const width = isFiniteNumber(annotation.size?.width) ? annotation.size.width : FLOW_ANNOTATION_MIN_SIZE;\n const height = isFiniteNumber(annotation.size?.height) ? annotation.size.height : FLOW_ANNOTATION_MIN_SIZE;\n const points = Array.isArray(annotation.points) ? annotation.points.filter(isPoint) : undefined;\n const needsTwoPoints = annotation.kind === 'arrow' || annotation.kind === 'line';\n if ((needsTwoPoints && (points?.length ?? 0) < 2) || (annotation.kind === 'drawing' && !points?.length)) return [];\n return [\n {\n annotationId: annotation.annotationId,\n kind: annotation.kind as TFlowAnnotationKind,\n position: { x: annotation.position.x, y: annotation.position.y },\n size: {\n width: Math.max(width, FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(height, FLOW_ANNOTATION_MIN_SIZE),\n },\n tone: FLOW_ANNOTATION_TONES.includes(annotation.tone as TFlowAnnotationTone)\n ? (annotation.tone as TFlowAnnotationTone)\n : 'neutral',\n ...(FLOW_ANNOTATION_STROKES.includes(annotation.stroke as TFlowAnnotationStroke)\n ? { stroke: annotation.stroke }\n : {}),\n ...(typeof annotation.text === 'string' ? { text: annotation.text } : {}),\n ...(isFiniteNumber(annotation.step) ? { step: annotation.step } : {}),\n ...(points ? { points } : {}),\n },\n ];\n });\n};\n\n/**\n * Turns points in canvas coordinates into an annotation box: its position is the top-left corner\n * (padded so the stroke is not clipped) and the points become relative to it.\n */\nexport const boxAroundPoints = (\n points: Array<IFlowPosition>,\n padding: number,\n): { position: IFlowPosition; size: IFlowSize; points: Array<IFlowPosition> } => {\n const xs = points.map((point) => point.x);\n const ys = points.map((point) => point.y);\n const left = Math.min(...xs) - padding;\n const top = Math.min(...ys) - padding;\n return {\n position: { x: left, y: top },\n size: {\n width: Math.max(Math.max(...xs) - left + padding, FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(Math.max(...ys) - top + padding, FLOW_ANNOTATION_MIN_SIZE),\n },\n points: points.map((point) => ({ x: point.x - left, y: point.y - top })),\n };\n};\n\n/** A box spanning two corners dragged in any direction. */\nexport const boxBetween = (start: IFlowPosition, end: IFlowPosition): { position: IFlowPosition; size: IFlowSize } => ({\n position: { x: Math.min(start.x, end.x), y: Math.min(start.y, end.y) },\n size: {\n width: Math.max(Math.abs(end.x - start.x), FLOW_ANNOTATION_MIN_SIZE),\n height: Math.max(Math.abs(end.y - start.y), FLOW_ANNOTATION_MIN_SIZE),\n },\n});\n\n/** Scales a stroke with its box when the box is resized. */\nexport const scaleAnnotationPoints = (\n points: Array<IFlowPosition> | undefined,\n from: IFlowSize,\n to: IFlowSize,\n): Array<IFlowPosition> | undefined => {\n if (!points) return undefined;\n const scaleX = from.width > 0 ? to.width / from.width : 1;\n const scaleY = from.height > 0 ? to.height / from.height : 1;\n return points.map((point) => ({ x: point.x * scaleX, y: point.y * scaleY }));\n};\n\nexport const cloneFlowAnnotations = (annotations: Array<IFlowAnnotation>, offset: number): Array<IFlowAnnotation> =>\n annotations.map((annotation) => ({\n ...annotation,\n annotationId: generateUUID(),\n position: { x: annotation.position.x + offset, y: annotation.position.y + offset },\n points: annotation.points?.map((point) => ({ ...point })),\n }));\n"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { StoreApi } from 'zustand/vanilla';
|
|
2
2
|
import { IFlowBlockRegistry } from './flowBlockRegistry';
|
|
3
|
-
import { IFlowGraph, IFlowNode, IFlowPosition, TFlowPortId } from './flowTypes';
|
|
3
|
+
import { IFlowAnnotation, IFlowGraph, IFlowNode, IFlowPosition, IFlowSize, TFlowPortId } from './flowTypes';
|
|
4
|
+
export interface IFlowSelection {
|
|
5
|
+
nodeIds?: Array<string>;
|
|
6
|
+
edgeIds?: Array<string>;
|
|
7
|
+
annotationIds?: Array<string>;
|
|
8
|
+
}
|
|
9
|
+
export interface IFlowAnnotationGeometry {
|
|
10
|
+
position?: IFlowPosition;
|
|
11
|
+
size?: IFlowSize;
|
|
12
|
+
points?: Array<IFlowPosition>;
|
|
13
|
+
}
|
|
4
14
|
export interface IFlowEditorState {
|
|
5
15
|
graph: IFlowGraph;
|
|
6
16
|
selectedNodeIds: Array<string>;
|
|
7
17
|
selectedEdgeIds: Array<string>;
|
|
18
|
+
selectedAnnotationIds: Array<string>;
|
|
8
19
|
past: Array<IFlowGraph>;
|
|
9
20
|
future: Array<IFlowGraph>;
|
|
10
21
|
clipboard: Array<IFlowNode>;
|
|
22
|
+
annotationClipboard: Array<IFlowAnnotation>;
|
|
11
23
|
isDirty: boolean;
|
|
24
|
+
/** A marquee is being drawn: the selection is still changing, so nothing should react to it yet. */
|
|
25
|
+
isAreaSelecting: boolean;
|
|
12
26
|
replaceGraph: (graph: IFlowGraph, options?: {
|
|
13
27
|
keepHistory?: boolean;
|
|
14
28
|
}) => void;
|
|
@@ -27,15 +41,20 @@ export interface IFlowEditorState {
|
|
|
27
41
|
updateNodeConfig: (nodeId: string, config: unknown) => void;
|
|
28
42
|
updateNodeLabel: (nodeId: string, label: string) => void;
|
|
29
43
|
moveNodes: (positions: Record<string, IFlowPosition>) => void;
|
|
44
|
+
/** Adds an annotation and selects it, in one undo step. */
|
|
45
|
+
addAnnotation: (annotation: Omit<IFlowAnnotation, 'annotationId'>) => string;
|
|
46
|
+
/** Text and colour changes. Typing in the same note within a second is one undo step. */
|
|
47
|
+
updateAnnotations: (annotationIds: Array<string>, patch: Partial<Pick<IFlowAnnotation, 'text' | 'tone' | 'stroke'>>) => void;
|
|
48
|
+
/** Live geometry while dragging or resizing: no undo step, like `moveNodes`. */
|
|
49
|
+
setAnnotationGeometry: (geometry: Record<string, IFlowAnnotationGeometry>) => void;
|
|
50
|
+
removeAnnotations: (annotationIds: Array<string>) => void;
|
|
30
51
|
connect: (source: string, sourcePort: TFlowPortId, target: string) => boolean;
|
|
31
52
|
removeSelected: () => void;
|
|
32
53
|
duplicateSelected: () => void;
|
|
33
54
|
copySelected: () => void;
|
|
34
55
|
paste: () => void;
|
|
35
|
-
setSelection: (selection:
|
|
36
|
-
|
|
37
|
-
edgeIds?: Array<string>;
|
|
38
|
-
}) => void;
|
|
56
|
+
setSelection: (selection: IFlowSelection) => void;
|
|
57
|
+
setAreaSelecting: (active: boolean) => void;
|
|
39
58
|
undo: () => void;
|
|
40
59
|
redo: () => void;
|
|
41
60
|
/** The host may rebuild its registry (labels after a language switch); gestures always read the latest. */
|
|
@@ -47,3 +66,8 @@ export type TFlowEditorStore = StoreApi<IFlowEditorState>;
|
|
|
47
66
|
* is the source of truth; the canvas only renders it and reports gestures back.
|
|
48
67
|
*/
|
|
49
68
|
export declare const createFlowEditorStore: (initialGraph: IFlowGraph, registry: IFlowBlockRegistry) => TFlowEditorStore;
|
|
69
|
+
/**
|
|
70
|
+
* The block the side panel edits: exactly one block selected, nothing else, and no marquee still
|
|
71
|
+
* being drawn. `null` otherwise. Hosts that dock the panel themselves gate it on this.
|
|
72
|
+
*/
|
|
73
|
+
export declare const selectFlowInspectedNodeId: (state: IFlowEditorState) => string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createStore } from "zustand/vanilla";
|
|
2
2
|
import { generateUUID } from "../../../helpers/utilityHelpers.js";
|
|
3
|
+
import { cloneFlowAnnotations, normalizeFlowAnnotations } from "./flowAnnotations.js";
|
|
3
4
|
import { flowOutputsOf } from "./flowBlockRegistry.js";
|
|
4
5
|
import { isFlowNodeRemovable, canConnect, reachableFrom, FLOW_LEVEL_GAP, FLOW_SIBLING_GAP, layoutFlowByLevels } from "./flowGraph.js";
|
|
5
6
|
const HISTORY_LIMIT = 50;
|
|
@@ -9,7 +10,8 @@ const isPosition = (position) => typeof position?.x === "number" && typeof posit
|
|
|
9
10
|
const normalizeGraph = (graph, registry) => {
|
|
10
11
|
const normalized = {
|
|
11
12
|
nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],
|
|
12
|
-
edges: Array.isArray(graph?.edges) ? graph.edges : []
|
|
13
|
+
edges: Array.isArray(graph?.edges) ? graph.edges : [],
|
|
14
|
+
annotations: normalizeFlowAnnotations(graph?.annotations)
|
|
13
15
|
};
|
|
14
16
|
if (normalized.nodes.every((node) => isPosition(node.position))) return normalized;
|
|
15
17
|
const layout = layoutFlowByLevels(normalized, registry);
|
|
@@ -21,6 +23,7 @@ const normalizeGraph = (graph, registry) => {
|
|
|
21
23
|
};
|
|
22
24
|
};
|
|
23
25
|
const withoutNodes = (graph, nodeIds) => ({
|
|
26
|
+
...graph,
|
|
24
27
|
nodes: graph.nodes.filter((node) => !nodeIds.has(node.nodeId)),
|
|
25
28
|
edges: graph.edges.filter((edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target))
|
|
26
29
|
});
|
|
@@ -39,18 +42,21 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
39
42
|
let lastCoalesce = null;
|
|
40
43
|
const commit = (next, selection, coalesceKey) => {
|
|
41
44
|
const { graph, past } = get();
|
|
45
|
+
const nextGraph = next.annotations === void 0 ? { ...next, annotations: graph.annotations ?? [] } : next;
|
|
42
46
|
const now = Date.now();
|
|
43
47
|
const extendsLastStep = coalesceKey !== void 0 && lastCoalesce?.key === coalesceKey && now - lastCoalesce.at < COALESCE_WINDOW_MS;
|
|
44
48
|
lastCoalesce = coalesceKey === void 0 ? null : { key: coalesceKey, at: now };
|
|
45
49
|
set({
|
|
46
|
-
graph:
|
|
50
|
+
graph: nextGraph,
|
|
47
51
|
past: extendsLastStep ? past : [...past, graph].slice(-HISTORY_LIMIT),
|
|
48
52
|
future: [],
|
|
49
53
|
isDirty: true,
|
|
50
54
|
...selection?.nodeIds ? { selectedNodeIds: selection.nodeIds } : {},
|
|
51
|
-
...selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {}
|
|
55
|
+
...selection?.edgeIds ? { selectedEdgeIds: selection.edgeIds } : {},
|
|
56
|
+
...selection?.annotationIds ? { selectedAnnotationIds: selection.annotationIds } : {}
|
|
52
57
|
});
|
|
53
58
|
};
|
|
59
|
+
const annotationsOf = (graph) => graph.annotations ?? [];
|
|
54
60
|
const cloneNodes = (nodes, offset) => {
|
|
55
61
|
const instancesByType = /* @__PURE__ */ new Map();
|
|
56
62
|
get().graph.nodes.forEach((node) => instancesByType.set(node.type, (instancesByType.get(node.type) ?? 0) + 1));
|
|
@@ -72,10 +78,13 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
72
78
|
graph: normalizeGraph(initialGraph, registry),
|
|
73
79
|
selectedNodeIds: [],
|
|
74
80
|
selectedEdgeIds: [],
|
|
81
|
+
selectedAnnotationIds: [],
|
|
75
82
|
past: [],
|
|
76
83
|
future: [],
|
|
77
84
|
clipboard: [],
|
|
85
|
+
annotationClipboard: [],
|
|
78
86
|
isDirty: false,
|
|
87
|
+
isAreaSelecting: false,
|
|
79
88
|
replaceGraph: (graph, options) => {
|
|
80
89
|
set((state) => ({
|
|
81
90
|
graph: normalizeGraph(graph, currentRegistry),
|
|
@@ -83,6 +92,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
83
92
|
future: [],
|
|
84
93
|
selectedNodeIds: [],
|
|
85
94
|
selectedEdgeIds: [],
|
|
95
|
+
selectedAnnotationIds: [],
|
|
86
96
|
isDirty: Boolean(options?.keepHistory)
|
|
87
97
|
}));
|
|
88
98
|
},
|
|
@@ -217,6 +227,51 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
217
227
|
isDirty: true
|
|
218
228
|
});
|
|
219
229
|
},
|
|
230
|
+
addAnnotation: (annotation) => {
|
|
231
|
+
const { graph } = get();
|
|
232
|
+
const created = { ...annotation, annotationId: generateUUID() };
|
|
233
|
+
commit(
|
|
234
|
+
{ ...graph, annotations: [...annotationsOf(graph), created] },
|
|
235
|
+
{ nodeIds: [], edgeIds: [], annotationIds: [created.annotationId] }
|
|
236
|
+
);
|
|
237
|
+
return created.annotationId;
|
|
238
|
+
},
|
|
239
|
+
updateAnnotations: (annotationIds, patch) => {
|
|
240
|
+
const { graph } = get();
|
|
241
|
+
const targets = new Set(annotationIds);
|
|
242
|
+
if (targets.size === 0) return;
|
|
243
|
+
commit(
|
|
244
|
+
{
|
|
245
|
+
...graph,
|
|
246
|
+
annotations: annotationsOf(graph).map(
|
|
247
|
+
(annotation) => targets.has(annotation.annotationId) ? { ...annotation, ...patch } : annotation
|
|
248
|
+
)
|
|
249
|
+
},
|
|
250
|
+
void 0,
|
|
251
|
+
patch.text !== void 0 ? `annotation-text:${annotationIds.join(",")}` : void 0
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
setAnnotationGeometry: (geometry) => {
|
|
255
|
+
const { graph } = get();
|
|
256
|
+
set({
|
|
257
|
+
graph: {
|
|
258
|
+
...graph,
|
|
259
|
+
annotations: annotationsOf(graph).map(
|
|
260
|
+
(annotation) => geometry[annotation.annotationId] ? { ...annotation, ...geometry[annotation.annotationId] } : annotation
|
|
261
|
+
)
|
|
262
|
+
},
|
|
263
|
+
isDirty: true
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
removeAnnotations: (annotationIds) => {
|
|
267
|
+
const { graph, selectedAnnotationIds } = get();
|
|
268
|
+
const removed = new Set(annotationIds);
|
|
269
|
+
if (!annotationsOf(graph).some((annotation) => removed.has(annotation.annotationId))) return;
|
|
270
|
+
commit(
|
|
271
|
+
{ ...graph, annotations: annotationsOf(graph).filter((annotation) => !removed.has(annotation.annotationId)) },
|
|
272
|
+
{ annotationIds: selectedAnnotationIds.filter((id) => !removed.has(id)) }
|
|
273
|
+
);
|
|
274
|
+
},
|
|
220
275
|
connect: (source, sourcePort, target) => {
|
|
221
276
|
const { graph } = get();
|
|
222
277
|
if (!canConnect(graph, currentRegistry, { source, sourcePort, target })) return false;
|
|
@@ -225,46 +280,87 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
225
280
|
return true;
|
|
226
281
|
},
|
|
227
282
|
removeSelected: () => {
|
|
228
|
-
const { graph, selectedNodeIds, selectedEdgeIds } = get();
|
|
283
|
+
const { graph, selectedNodeIds, selectedEdgeIds, selectedAnnotationIds } = get();
|
|
229
284
|
const removableNodeIds = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId) && isFlowNodeRemovable(node, currentRegistry)).map((node) => node.nodeId);
|
|
230
|
-
|
|
231
|
-
const
|
|
285
|
+
const removedNodes = new Set(removableNodeIds);
|
|
286
|
+
const selectedNodes = new Set(selectedNodeIds);
|
|
287
|
+
const removedEdges = new Set(
|
|
288
|
+
graph.edges.filter((edge) => selectedEdgeIds.includes(edge.edgeId)).filter(
|
|
289
|
+
(edge) => removedNodes.has(edge.source) || removedNodes.has(edge.target) || !selectedNodes.has(edge.source) && !selectedNodes.has(edge.target)
|
|
290
|
+
).map((edge) => edge.edgeId)
|
|
291
|
+
);
|
|
292
|
+
const removedAnnotations = new Set(selectedAnnotationIds);
|
|
293
|
+
const annotations = annotationsOf(graph).filter(
|
|
294
|
+
(annotation) => !removedAnnotations.has(annotation.annotationId)
|
|
295
|
+
);
|
|
296
|
+
const removesAnnotations = annotations.length !== annotationsOf(graph).length;
|
|
297
|
+
if (removedNodes.size === 0 && removedEdges.size === 0 && !removesAnnotations) return;
|
|
232
298
|
const next = withoutNodes(
|
|
233
|
-
{ ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)) },
|
|
234
|
-
|
|
299
|
+
{ ...graph, edges: graph.edges.filter((edge) => !removedEdges.has(edge.edgeId)), annotations },
|
|
300
|
+
removedNodes
|
|
235
301
|
);
|
|
236
|
-
commit(next, { nodeIds: [], edgeIds: [] });
|
|
302
|
+
commit(next, { nodeIds: [], edgeIds: [], annotationIds: [] });
|
|
237
303
|
},
|
|
238
304
|
duplicateSelected: () => {
|
|
239
|
-
const { graph, selectedNodeIds } = get();
|
|
305
|
+
const { graph, selectedNodeIds, selectedAnnotationIds } = get();
|
|
240
306
|
const selected = graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId));
|
|
241
307
|
const copies = cloneNodes(selected, PASTE_OFFSET);
|
|
242
|
-
|
|
308
|
+
const annotationCopies = cloneFlowAnnotations(
|
|
309
|
+
annotationsOf(graph).filter((annotation) => selectedAnnotationIds.includes(annotation.annotationId)),
|
|
310
|
+
PASTE_OFFSET
|
|
311
|
+
);
|
|
312
|
+
if (copies.length === 0 && annotationCopies.length === 0) return;
|
|
243
313
|
commit(
|
|
244
|
-
{
|
|
245
|
-
|
|
314
|
+
{
|
|
315
|
+
...graph,
|
|
316
|
+
nodes: [...graph.nodes, ...copies],
|
|
317
|
+
annotations: [...annotationsOf(graph), ...annotationCopies]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
nodeIds: copies.map((node) => node.nodeId),
|
|
321
|
+
edgeIds: [],
|
|
322
|
+
annotationIds: annotationCopies.map((annotation) => annotation.annotationId)
|
|
323
|
+
}
|
|
246
324
|
);
|
|
247
325
|
},
|
|
248
326
|
copySelected: () => {
|
|
249
|
-
const { graph, selectedNodeIds } = get();
|
|
250
|
-
set({
|
|
327
|
+
const { graph, selectedNodeIds, selectedAnnotationIds } = get();
|
|
328
|
+
set({
|
|
329
|
+
clipboard: graph.nodes.filter((node) => selectedNodeIds.includes(node.nodeId)),
|
|
330
|
+
annotationClipboard: annotationsOf(graph).filter(
|
|
331
|
+
(annotation) => selectedAnnotationIds.includes(annotation.annotationId)
|
|
332
|
+
)
|
|
333
|
+
});
|
|
251
334
|
},
|
|
252
335
|
paste: () => {
|
|
253
|
-
const { graph, clipboard } = get();
|
|
336
|
+
const { graph, clipboard, annotationClipboard } = get();
|
|
254
337
|
const copies = cloneNodes(clipboard, PASTE_OFFSET);
|
|
255
|
-
|
|
338
|
+
const annotationCopies = cloneFlowAnnotations(annotationClipboard, PASTE_OFFSET);
|
|
339
|
+
if (copies.length === 0 && annotationCopies.length === 0) return;
|
|
256
340
|
commit(
|
|
257
|
-
{
|
|
258
|
-
|
|
341
|
+
{
|
|
342
|
+
...graph,
|
|
343
|
+
nodes: [...graph.nodes, ...copies],
|
|
344
|
+
annotations: [...annotationsOf(graph), ...annotationCopies]
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
nodeIds: copies.map((node) => node.nodeId),
|
|
348
|
+
edgeIds: [],
|
|
349
|
+
annotationIds: annotationCopies.map((annotation) => annotation.annotationId)
|
|
350
|
+
}
|
|
259
351
|
);
|
|
260
|
-
set({ clipboard: copies });
|
|
352
|
+
set({ clipboard: copies, annotationClipboard: annotationCopies });
|
|
261
353
|
},
|
|
262
|
-
setSelection: ({ nodeIds, edgeIds }) => {
|
|
354
|
+
setSelection: ({ nodeIds, edgeIds, annotationIds }) => {
|
|
263
355
|
set((state) => ({
|
|
264
356
|
selectedNodeIds: nodeIds ?? state.selectedNodeIds,
|
|
265
|
-
selectedEdgeIds: edgeIds ?? state.selectedEdgeIds
|
|
357
|
+
selectedEdgeIds: edgeIds ?? state.selectedEdgeIds,
|
|
358
|
+
selectedAnnotationIds: annotationIds ?? state.selectedAnnotationIds
|
|
266
359
|
}));
|
|
267
360
|
},
|
|
361
|
+
setAreaSelecting: (active) => {
|
|
362
|
+
if (get().isAreaSelecting !== active) set({ isAreaSelecting: active });
|
|
363
|
+
},
|
|
268
364
|
undo: () => {
|
|
269
365
|
const { graph, past, future } = get();
|
|
270
366
|
const previous = past[past.length - 1];
|
|
@@ -275,6 +371,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
275
371
|
future: [graph, ...future].slice(0, HISTORY_LIMIT),
|
|
276
372
|
selectedNodeIds: [],
|
|
277
373
|
selectedEdgeIds: [],
|
|
374
|
+
selectedAnnotationIds: [],
|
|
278
375
|
isDirty: true
|
|
279
376
|
});
|
|
280
377
|
},
|
|
@@ -288,6 +385,7 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
288
385
|
future: future.slice(1),
|
|
289
386
|
selectedNodeIds: [],
|
|
290
387
|
selectedEdgeIds: [],
|
|
388
|
+
selectedAnnotationIds: [],
|
|
291
389
|
isDirty: true
|
|
292
390
|
});
|
|
293
391
|
},
|
|
@@ -296,7 +394,9 @@ const createFlowEditorStore = (initialGraph, registry) => createStore()((set, ge
|
|
|
296
394
|
}
|
|
297
395
|
};
|
|
298
396
|
});
|
|
397
|
+
const selectFlowInspectedNodeId = (state) => state.selectedNodeIds.length === 1 && state.selectedAnnotationIds.length === 0 && !state.isAreaSelecting ? state.selectedNodeIds[0] ?? null : null;
|
|
299
398
|
export {
|
|
300
|
-
createFlowEditorStore
|
|
399
|
+
createFlowEditorStore,
|
|
400
|
+
selectFlowInspectedNodeId
|
|
301
401
|
};
|
|
302
402
|
//# sourceMappingURL=flowEditorStore.js.map
|