@nnkogift/dhis2-form-utils-devtools 0.1.0-alpha.4 → 0.1.0-alpha.6
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/EffectBadge.d.ts +17 -0
- package/dist/EffectBadge.js +55 -0
- package/dist/EffectBadge.js.map +1 -0
- package/dist/RuleDetailsModal.d.ts +30 -0
- package/dist/RuleDetailsModal.js +403 -0
- package/dist/RuleDetailsModal.js.map +1 -0
- package/dist/RuleDevtoolsScope.d.ts +14 -0
- package/dist/RuleDevtoolsScope.js +25 -0
- package/dist/RuleDevtoolsScope.js.map +1 -0
- package/dist/RuleGraphModal.d.ts +17 -0
- package/dist/RuleGraphModal.js +35 -0
- package/dist/RuleGraphModal.js.map +1 -0
- package/dist/RuleGraphView.d.ts +43 -0
- package/dist/RuleGraphView.js +401 -0
- package/dist/RuleGraphView.js.map +1 -0
- package/dist/RulesPanel.d.ts +19 -0
- package/dist/RulesPanel.js +463 -0
- package/dist/RulesPanel.js.map +1 -0
- package/dist/RulesPanelGraphFallback.d.ts +5 -0
- package/dist/RulesPanelGraphFallback.js +19 -0
- package/dist/RulesPanelGraphFallback.js.map +1 -0
- package/dist/TraceTimeline.d.ts +16 -0
- package/dist/TraceTimeline.js +227 -0
- package/dist/TraceTimeline.js.map +1 -0
- package/dist/attach.d.ts +12 -0
- package/dist/attach.js +14 -0
- package/dist/attach.js.map +1 -0
- package/dist/buildGraph.d.ts +25 -0
- package/dist/buildGraph.js +67 -0
- package/dist/buildGraph.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/createLabelLookup.d.ts +25 -0
- package/dist/createLabelLookup.js +81 -0
- package/dist/createLabelLookup.js.map +1 -0
- package/dist/effectStyles.d.ts +28 -0
- package/dist/effectStyles.js +142 -0
- package/dist/effectStyles.js.map +1 -0
- package/dist/formatAgo.d.ts +3 -0
- package/dist/formatAgo.js +25 -0
- package/dist/formatAgo.js.map +1 -0
- package/dist/formatRuleActionSummary.d.ts +33 -0
- package/dist/formatRuleActionSummary.js +61 -0
- package/dist/formatRuleActionSummary.js.map +1 -0
- package/dist/graphLayout.d.ts +48 -0
- package/dist/graphLayout.js +137 -0
- package/dist/graphLayout.js.map +1 -0
- package/dist/graphNodeStyles.d.ts +9 -0
- package/dist/graphNodeStyles.js +22 -0
- package/dist/graphNodeStyles.js.map +1 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +9 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.cjs +1364 -651
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +12 -63
- package/dist/index.d.ts +12 -63
- package/dist/index.js +5 -1669
- package/dist/index.js.map +1 -1
- package/dist/programRuleDetailQuery.d.ts +70 -0
- package/dist/programRuleDetailQuery.js +42 -0
- package/dist/programRuleDetailQuery.js.map +1 -0
- package/dist/resolveProgramRulesList.d.ts +21 -0
- package/dist/resolveProgramRulesList.js +45 -0
- package/dist/resolveProgramRulesList.js.map +1 -0
- package/dist/traceEntry.d.ts +12 -0
- package/dist/traceEntry.js +20 -0
- package/dist/traceEntry.js.map +1 -0
- package/dist/traceStore.d.ts +12 -0
- package/dist/traceStore.js +39 -0
- package/dist/traceStore.js.map +1 -0
- package/dist/useFlipReorder.d.ts +8 -0
- package/dist/useFlipReorder.js +52 -0
- package/dist/useFlipReorder.js.map +1 -0
- package/dist/useProgramRuleDetail.d.ts +17 -0
- package/dist/useProgramRuleDetail.js +30 -0
- package/dist/useProgramRuleDetail.js.map +1 -0
- package/package.json +27 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { RuleGraphViewProps } from './RuleGraphView.js';
|
|
3
|
+
import 'react';
|
|
4
|
+
import '@xyflow/react';
|
|
5
|
+
import '@nnkogift/dhis2-form-utils-hooks';
|
|
6
|
+
import './buildGraph.js';
|
|
7
|
+
import './createLabelLookup.js';
|
|
8
|
+
import '@nnkogift/dhis2-form-utils-metadata';
|
|
9
|
+
|
|
10
|
+
type RuleGraphModalProps = Omit<RuleGraphViewProps, 'className' | 'minHeightClassName' | 'headerActions'> & {
|
|
11
|
+
open: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
subtitle?: string | null;
|
|
14
|
+
};
|
|
15
|
+
declare function RuleGraphModal({ open, onClose, subtitle, layoutKey, ...graphProps }: RuleGraphModalProps): react_jsx_runtime.JSX.Element | null;
|
|
16
|
+
|
|
17
|
+
export { RuleGraphModal, type RuleGraphModalProps };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { Modal, ModalTitle, ModalContent } from '@dhis2/ui';
|
|
3
|
+
import { translate } from './i18n.js';
|
|
4
|
+
import { RuleGraphView } from './RuleGraphView.js';
|
|
5
|
+
|
|
6
|
+
function RuleGraphModal({
|
|
7
|
+
open,
|
|
8
|
+
onClose,
|
|
9
|
+
subtitle,
|
|
10
|
+
layoutKey,
|
|
11
|
+
...graphProps
|
|
12
|
+
}) {
|
|
13
|
+
if (!open) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return /* @__PURE__ */ jsxs(Modal, { large: true, fluid: true, onClose, children: [
|
|
17
|
+
/* @__PURE__ */ jsxs(ModalTitle, { children: [
|
|
18
|
+
translate("Rule dependency graph"),
|
|
19
|
+
subtitle ? /* @__PURE__ */ jsx("span", { className: "mt-dp4 block text-sm font-normal text-dhis2-grey-700", children: subtitle }) : null
|
|
20
|
+
] }),
|
|
21
|
+
/* @__PURE__ */ jsx(ModalContent, { className: "p-0", children: /* @__PURE__ */ jsx("div", { className: "h-[min(85vh,900px)] w-full min-w-[min(1200px,92vw)]", children: /* @__PURE__ */ jsx(
|
|
22
|
+
RuleGraphView,
|
|
23
|
+
{
|
|
24
|
+
...graphProps,
|
|
25
|
+
layoutKey: layoutKey ?? "modal",
|
|
26
|
+
className: "h-full min-h-0 rounded-none border-0",
|
|
27
|
+
minHeightClassName: "min-h-0 h-full"
|
|
28
|
+
}
|
|
29
|
+
) }) })
|
|
30
|
+
] });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { RuleGraphModal };
|
|
34
|
+
//# sourceMappingURL=RuleGraphModal.js.map
|
|
35
|
+
//# sourceMappingURL=RuleGraphModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/RuleGraphModal.tsx"],"names":[],"mappings":";;;;;AAaO,SAAS,cAAA,CAAe;AAAA,EAC3B,IAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,GAAG;AACP,CAAA,EAAwB;AACpB,EAAA,IAAI,CAAC,IAAA,EAAM;AACP,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,4BACK,KAAA,EAAA,EAAM,KAAA,EAAK,IAAA,EAAC,KAAA,EAAK,MAAC,OAAA,EACf,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,UAAA,EAAA,EACI,QAAA,EAAA;AAAA,MAAA,SAAA,CAAU,uBAAuB,CAAA;AAAA,MACjC,2BACG,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,sDAAA,EACX,oBACL,CAAA,GACA;AAAA,KAAA,EACR,CAAA;AAAA,wBACC,YAAA,EAAA,EAAa,SAAA,EAAU,OACpB,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,WAAU,qDAAA,EACX,QAAA,kBAAA,GAAA;AAAA,MAAC,aAAA;AAAA,MAAA;AAAA,QACI,GAAG,UAAA;AAAA,QACJ,WAAW,SAAA,IAAa,OAAA;AAAA,QACxB,SAAA,EAAU,sCAAA;AAAA,QACV,kBAAA,EAAmB;AAAA;AAAA,OAE3B,CAAA,EACJ;AAAA,GAAA,EACJ,CAAA;AAER","file":"RuleGraphModal.js","sourcesContent":["import { Modal, ModalContent, ModalTitle } from '@dhis2/ui';\nimport { translate } from './i18n.js';\nimport { RuleGraphView, type RuleGraphViewProps } from './RuleGraphView.js';\n\nexport type RuleGraphModalProps = Omit<\n RuleGraphViewProps,\n 'className' | 'minHeightClassName' | 'headerActions'\n> & {\n open: boolean;\n onClose: () => void;\n subtitle?: string | null;\n};\n\nexport function RuleGraphModal({\n open,\n onClose,\n subtitle,\n layoutKey,\n ...graphProps\n}: RuleGraphModalProps) {\n if (!open) {\n return null;\n }\n\n return (\n <Modal large fluid onClose={onClose}>\n <ModalTitle>\n {translate('Rule dependency graph')}\n {subtitle ? (\n <span className=\"mt-dp4 block text-sm font-normal text-dhis2-grey-700\">\n {subtitle}\n </span>\n ) : null}\n </ModalTitle>\n <ModalContent className=\"p-0\">\n <div className=\"h-[min(85vh,900px)] w-full min-w-[min(1200px,92vw)]\">\n <RuleGraphView\n {...graphProps}\n layoutKey={layoutKey ?? 'modal'}\n className=\"h-full min-h-0 rounded-none border-0\"\n minHeightClassName=\"min-h-0 h-full\"\n />\n </div>\n </ModalContent>\n </Modal>\n );\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Node, Edge } from '@xyflow/react';
|
|
4
|
+
import { RuleTraceEntry, FormStore } from '@nnkogift/dhis2-form-utils-hooks';
|
|
5
|
+
import { RuleDependencyGraph, GraphNode } from './buildGraph.js';
|
|
6
|
+
import { DevtoolsLabelLookup } from './createLabelLookup.js';
|
|
7
|
+
import '@nnkogift/dhis2-form-utils-metadata';
|
|
8
|
+
|
|
9
|
+
type FieldStateMap = ReturnType<FormStore['fieldStore']['getSnapshot']>;
|
|
10
|
+
type RuleNodeData = {
|
|
11
|
+
label: string;
|
|
12
|
+
kind: GraphNode['kind'];
|
|
13
|
+
graphNodeId: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
highlighted: boolean;
|
|
16
|
+
};
|
|
17
|
+
type RuleGraphViewProps = {
|
|
18
|
+
entries: readonly RuleTraceEntry[];
|
|
19
|
+
fieldState: FieldStateMap;
|
|
20
|
+
formValues: Record<string, unknown>;
|
|
21
|
+
selectedEntryId: string | null;
|
|
22
|
+
highlightRuleId: string | null;
|
|
23
|
+
labelLookup?: DevtoolsLabelLookup;
|
|
24
|
+
className?: string;
|
|
25
|
+
layoutKey?: string | number;
|
|
26
|
+
headerActions?: ReactNode;
|
|
27
|
+
minHeightClassName?: string;
|
|
28
|
+
};
|
|
29
|
+
type RuleGraphEdgeData = {
|
|
30
|
+
label: string;
|
|
31
|
+
stroke: string;
|
|
32
|
+
/** When false, the label is hidden on the canvas and shown only on hover. */
|
|
33
|
+
showLabel: boolean;
|
|
34
|
+
/** Per-source fan-out distance so parallel edges don't overlap. */
|
|
35
|
+
offset: number;
|
|
36
|
+
};
|
|
37
|
+
declare function toFlowGraph(graph: RuleDependencyGraph, fieldState: FieldStateMap, formValues: Record<string, unknown>, highlightedKeys: Set<string> | null): {
|
|
38
|
+
nodes: Node<RuleNodeData>[];
|
|
39
|
+
edges: Edge<RuleGraphEdgeData>[];
|
|
40
|
+
};
|
|
41
|
+
declare function RuleGraphView({ entries, fieldState, formValues, selectedEntryId, highlightRuleId, labelLookup, className, layoutKey, headerActions, minHeightClassName, }: RuleGraphViewProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
|
|
43
|
+
export { RuleGraphView, type RuleGraphViewProps, toFlowGraph };
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { memo, useState, useMemo, useEffect } from 'react';
|
|
3
|
+
import { MarkerType, Handle, Position, getSmoothStepPath, BaseEdge, EdgeLabelRenderer, ReactFlowProvider, useNodesState, useEdgesState, ReactFlow, Background, Controls, useReactFlow } from '@xyflow/react';
|
|
4
|
+
import { NoticeBox } from '@dhis2/ui';
|
|
5
|
+
import { buildGraphFromTrace } from './buildGraph.js';
|
|
6
|
+
import { prepareFlowGraph, computeGraphLayout } from './graphLayout.js';
|
|
7
|
+
import { EffectLegend } from './EffectBadge.js';
|
|
8
|
+
import { getEffectEdgeStroke, getEffectShortLabel, getEffectVariant } from './effectStyles.js';
|
|
9
|
+
import { getGraphNodeClassName, getLegendSwatchClassName } from './graphNodeStyles.js';
|
|
10
|
+
import { translate } from './i18n.js';
|
|
11
|
+
import { resolveGraphTraceEntry } from './traceEntry.js';
|
|
12
|
+
|
|
13
|
+
const KIND_LABELS = {
|
|
14
|
+
field: "Field",
|
|
15
|
+
rule: "Rule",
|
|
16
|
+
section: "Section",
|
|
17
|
+
feedback: "Feedback"
|
|
18
|
+
};
|
|
19
|
+
const READ_LABEL_EDGE_THRESHOLD = 4;
|
|
20
|
+
const DEFAULT_EDGE_OPTIONS = {
|
|
21
|
+
type: "ruleGraphEdge",
|
|
22
|
+
style: { stroke: getEffectEdgeStroke("default") },
|
|
23
|
+
markerEnd: {
|
|
24
|
+
type: MarkerType.ArrowClosed,
|
|
25
|
+
color: getEffectEdgeStroke("default")
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function RuleGraphNode({ data }) {
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: getGraphNodeClassName(data.kind, data.highlighted), children: [
|
|
30
|
+
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Left }),
|
|
31
|
+
/* @__PURE__ */ jsx("span", { className: "mb-dp4 block text-[0.625rem] font-semibold uppercase leading-none tracking-wide text-dhis2-grey-600", children: KIND_LABELS[data.kind] }),
|
|
32
|
+
/* @__PURE__ */ jsx("div", { className: "break-words font-semibold leading-[1.3]", children: data.label }),
|
|
33
|
+
data.value !== void 0 ? /* @__PURE__ */ jsx("div", { className: "mt-dp4 border-t border-dhis2-grey-200 pt-dp4 font-mono text-[0.6875rem] break-all text-dhis2-grey-700", children: data.value }) : null,
|
|
34
|
+
/* @__PURE__ */ jsx(Handle, { type: "source", position: Position.Right })
|
|
35
|
+
] });
|
|
36
|
+
}
|
|
37
|
+
const nodeTypes = {
|
|
38
|
+
ruleGraphNode: memo(RuleGraphNode)
|
|
39
|
+
};
|
|
40
|
+
function RuleGraphEdge({
|
|
41
|
+
id,
|
|
42
|
+
sourceX,
|
|
43
|
+
sourceY,
|
|
44
|
+
sourcePosition,
|
|
45
|
+
targetX,
|
|
46
|
+
targetY,
|
|
47
|
+
targetPosition,
|
|
48
|
+
markerEnd,
|
|
49
|
+
style,
|
|
50
|
+
data
|
|
51
|
+
}) {
|
|
52
|
+
const [hovered, setHovered] = useState(false);
|
|
53
|
+
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
|
54
|
+
sourceX,
|
|
55
|
+
sourceY,
|
|
56
|
+
sourcePosition,
|
|
57
|
+
targetX,
|
|
58
|
+
targetY,
|
|
59
|
+
targetPosition,
|
|
60
|
+
offset: data?.offset ?? 20
|
|
61
|
+
});
|
|
62
|
+
const showLabel = data ? data.showLabel || hovered : false;
|
|
63
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
64
|
+
/* @__PURE__ */ jsx(BaseEdge, { id, path: edgePath, markerEnd, style }),
|
|
65
|
+
/* @__PURE__ */ jsx(
|
|
66
|
+
"path",
|
|
67
|
+
{
|
|
68
|
+
d: edgePath,
|
|
69
|
+
fill: "none",
|
|
70
|
+
stroke: "transparent",
|
|
71
|
+
strokeWidth: 16,
|
|
72
|
+
onMouseEnter: () => {
|
|
73
|
+
setHovered(true);
|
|
74
|
+
},
|
|
75
|
+
onMouseLeave: () => {
|
|
76
|
+
setHovered(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
showLabel && data ? /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx(
|
|
81
|
+
"div",
|
|
82
|
+
{
|
|
83
|
+
className: "nodrag nopan absolute rounded-sm px-dp4 text-[10px] font-semibold",
|
|
84
|
+
style: {
|
|
85
|
+
transform: `translate(-50%, -50%) translate(${String(labelX)}px, ${String(labelY)}px)`,
|
|
86
|
+
background: "rgba(255,255,255,0.92)",
|
|
87
|
+
color: data.stroke,
|
|
88
|
+
pointerEvents: "all"
|
|
89
|
+
},
|
|
90
|
+
onMouseEnter: () => {
|
|
91
|
+
setHovered(true);
|
|
92
|
+
},
|
|
93
|
+
onMouseLeave: () => {
|
|
94
|
+
setHovered(false);
|
|
95
|
+
},
|
|
96
|
+
children: data.label
|
|
97
|
+
}
|
|
98
|
+
) }) : null
|
|
99
|
+
] });
|
|
100
|
+
}
|
|
101
|
+
const edgeTypes = {
|
|
102
|
+
ruleGraphEdge: memo(RuleGraphEdge)
|
|
103
|
+
};
|
|
104
|
+
function effectNodeKey(effect) {
|
|
105
|
+
const kind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
|
|
106
|
+
return `${kind}:${effect.targetId}`;
|
|
107
|
+
}
|
|
108
|
+
function entryGraphKeys(entry) {
|
|
109
|
+
const keys = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const fieldId of entry.changedFields) {
|
|
111
|
+
keys.add(`field:${fieldId}`);
|
|
112
|
+
}
|
|
113
|
+
for (const result of entry.ruleResults) {
|
|
114
|
+
keys.add(`rule:${result.ruleId}`);
|
|
115
|
+
for (const effect of result.effects) {
|
|
116
|
+
keys.add(effectNodeKey(effect));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return keys;
|
|
120
|
+
}
|
|
121
|
+
function ruleGraphKeys(entry, ruleId) {
|
|
122
|
+
const keys = /* @__PURE__ */ new Set([`rule:${ruleId}`]);
|
|
123
|
+
for (const result of entry.ruleResults) {
|
|
124
|
+
if (result.ruleId !== ruleId) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const effect of result.effects) {
|
|
128
|
+
keys.add(effectNodeKey(effect));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return keys;
|
|
132
|
+
}
|
|
133
|
+
const formatDisplayValue = (value) => {
|
|
134
|
+
if (value === void 0 || value === null || value === "") {
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(value);
|
|
141
|
+
};
|
|
142
|
+
function toFlowGraph(graph, fieldState, formValues, highlightedKeys) {
|
|
143
|
+
const { nodes: flowNodes, edges: flowEdges } = prepareFlowGraph(graph);
|
|
144
|
+
const positions = computeGraphLayout(flowNodes, flowEdges);
|
|
145
|
+
const graphNodeIdByFlowId = new Map(flowNodes.map((node) => [node.id, node.graphNodeId]));
|
|
146
|
+
const nodes = flowNodes.map((node) => {
|
|
147
|
+
const position = positions.get(node.id) ?? { x: 0, y: 0 };
|
|
148
|
+
const rawFieldId = node.kind === "field" ? node.graphNodeId.slice("field:".length) : void 0;
|
|
149
|
+
const assignedValue = rawFieldId && rawFieldId in fieldState ? fieldState[rawFieldId].assignedValue : void 0;
|
|
150
|
+
const formValue = rawFieldId ? formValues[rawFieldId] : void 0;
|
|
151
|
+
const displayValue = formatDisplayValue(assignedValue) ?? formatDisplayValue(formValue);
|
|
152
|
+
return {
|
|
153
|
+
id: node.id,
|
|
154
|
+
type: "ruleGraphNode",
|
|
155
|
+
position,
|
|
156
|
+
data: {
|
|
157
|
+
label: node.label,
|
|
158
|
+
kind: node.kind,
|
|
159
|
+
graphNodeId: node.graphNodeId,
|
|
160
|
+
value: displayValue,
|
|
161
|
+
highlighted: highlightedKeys ? highlightedKeys.has(node.graphNodeId) : true
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const dense = flowEdges.length > READ_LABEL_EDGE_THRESHOLD;
|
|
166
|
+
const sourceFanOut = /* @__PURE__ */ new Map();
|
|
167
|
+
const edges = flowEdges.map((edge) => {
|
|
168
|
+
const sourceGraphId = graphNodeIdByFlowId.get(edge.source) ?? edge.source;
|
|
169
|
+
const targetGraphId = graphNodeIdByFlowId.get(edge.target) ?? edge.target;
|
|
170
|
+
const isHighlighted = highlightedKeys ? highlightedKeys.has(sourceGraphId) && highlightedKeys.has(targetGraphId) : true;
|
|
171
|
+
const effectType = edge.effectType ?? "default";
|
|
172
|
+
const isRead = effectType === "read";
|
|
173
|
+
const stroke = getEffectEdgeStroke(effectType, isHighlighted);
|
|
174
|
+
const label = getEffectShortLabel(effectType);
|
|
175
|
+
const showLabel = !(isRead && dense);
|
|
176
|
+
const fanIndex = sourceFanOut.get(edge.source) ?? 0;
|
|
177
|
+
sourceFanOut.set(edge.source, fanIndex + 1);
|
|
178
|
+
const offset = 20 + fanIndex * 14;
|
|
179
|
+
return {
|
|
180
|
+
id: edge.id,
|
|
181
|
+
source: edge.source,
|
|
182
|
+
target: edge.target,
|
|
183
|
+
type: "ruleGraphEdge",
|
|
184
|
+
animated: isHighlighted && !isRead,
|
|
185
|
+
zIndex: isHighlighted ? 1 : 0,
|
|
186
|
+
style: {
|
|
187
|
+
stroke,
|
|
188
|
+
strokeWidth: isRead ? 1 : Math.min(1 + edge.fireCount, 4),
|
|
189
|
+
opacity: isHighlighted ? isRead ? 0.5 : 1 : 0.35
|
|
190
|
+
},
|
|
191
|
+
markerEnd: {
|
|
192
|
+
type: MarkerType.ArrowClosed,
|
|
193
|
+
color: stroke
|
|
194
|
+
},
|
|
195
|
+
data: {
|
|
196
|
+
label,
|
|
197
|
+
stroke,
|
|
198
|
+
showLabel,
|
|
199
|
+
offset
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
return { nodes, edges };
|
|
204
|
+
}
|
|
205
|
+
function GraphToolbar({
|
|
206
|
+
nodeCount,
|
|
207
|
+
edgeCount,
|
|
208
|
+
headerActions,
|
|
209
|
+
activeEffectVariants
|
|
210
|
+
}) {
|
|
211
|
+
const items = [
|
|
212
|
+
{ kind: "field", label: translate("Field") },
|
|
213
|
+
{ kind: "rule", label: translate("Rule") },
|
|
214
|
+
{ kind: "section", label: translate("Section") },
|
|
215
|
+
{ kind: "feedback", label: translate("Feedback") }
|
|
216
|
+
];
|
|
217
|
+
return /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white", children: [
|
|
218
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-dp8 px-dp12 py-dp8", children: [
|
|
219
|
+
/* @__PURE__ */ jsx(
|
|
220
|
+
"div",
|
|
221
|
+
{
|
|
222
|
+
className: "flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8",
|
|
223
|
+
"aria-hidden": "true",
|
|
224
|
+
children: items.map((item) => /* @__PURE__ */ jsxs(
|
|
225
|
+
"span",
|
|
226
|
+
{
|
|
227
|
+
className: "inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800",
|
|
228
|
+
children: [
|
|
229
|
+
/* @__PURE__ */ jsx("span", { className: getLegendSwatchClassName(item.kind) }),
|
|
230
|
+
item.label
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
item.kind
|
|
234
|
+
))
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
|
|
238
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-dhis2-grey-600", children: translate("{{nodes}} nodes \xB7 {{edges}} edges", {
|
|
239
|
+
nodes: nodeCount,
|
|
240
|
+
edges: edgeCount
|
|
241
|
+
}) }),
|
|
242
|
+
headerActions
|
|
243
|
+
] })
|
|
244
|
+
] }),
|
|
245
|
+
/* @__PURE__ */ jsx(EffectLegend, { activeVariants: activeEffectVariants })
|
|
246
|
+
] });
|
|
247
|
+
}
|
|
248
|
+
function FitViewOnChange({
|
|
249
|
+
layoutKey,
|
|
250
|
+
nodeCount,
|
|
251
|
+
edgeCount
|
|
252
|
+
}) {
|
|
253
|
+
const { fitView } = useReactFlow();
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const frameId = requestAnimationFrame(() => {
|
|
256
|
+
void fitView({ padding: 0.2, duration: 150 });
|
|
257
|
+
});
|
|
258
|
+
return () => {
|
|
259
|
+
cancelAnimationFrame(frameId);
|
|
260
|
+
};
|
|
261
|
+
}, [edgeCount, fitView, layoutKey, nodeCount]);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
function mergeNodePositions(next, current) {
|
|
265
|
+
const currentById = new Map(current.map((node) => [node.id, node]));
|
|
266
|
+
return next.map((node) => {
|
|
267
|
+
const existing = currentById.get(node.id);
|
|
268
|
+
if (existing) {
|
|
269
|
+
return { ...node, position: existing.position };
|
|
270
|
+
}
|
|
271
|
+
return node;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function RuleGraphCanvas({
|
|
275
|
+
nodes: layoutNodes,
|
|
276
|
+
edges: layoutEdges,
|
|
277
|
+
layoutKey
|
|
278
|
+
}) {
|
|
279
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
|
280
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
setNodes((current) => mergeNodePositions(layoutNodes, current));
|
|
283
|
+
}, [layoutNodes, setNodes]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
setEdges(layoutEdges);
|
|
286
|
+
}, [layoutEdges, setEdges]);
|
|
287
|
+
return /* @__PURE__ */ jsxs(
|
|
288
|
+
ReactFlow,
|
|
289
|
+
{
|
|
290
|
+
nodes,
|
|
291
|
+
edges,
|
|
292
|
+
nodeTypes,
|
|
293
|
+
edgeTypes,
|
|
294
|
+
onNodesChange,
|
|
295
|
+
onEdgesChange,
|
|
296
|
+
nodesDraggable: true,
|
|
297
|
+
defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
|
|
298
|
+
elevateEdgesOnSelect: true,
|
|
299
|
+
fitView: true,
|
|
300
|
+
proOptions: { hideAttribution: true },
|
|
301
|
+
className: "h-full w-full",
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ jsx(
|
|
304
|
+
FitViewOnChange,
|
|
305
|
+
{
|
|
306
|
+
layoutKey,
|
|
307
|
+
nodeCount: nodes.length,
|
|
308
|
+
edgeCount: edges.length
|
|
309
|
+
}
|
|
310
|
+
),
|
|
311
|
+
/* @__PURE__ */ jsx(Background, { gap: 16, size: 1 }),
|
|
312
|
+
/* @__PURE__ */ jsx(Controls, { showInteractive: false })
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
function RuleGraphView({
|
|
318
|
+
entries,
|
|
319
|
+
fieldState,
|
|
320
|
+
formValues,
|
|
321
|
+
selectedEntryId,
|
|
322
|
+
highlightRuleId,
|
|
323
|
+
labelLookup,
|
|
324
|
+
className,
|
|
325
|
+
layoutKey,
|
|
326
|
+
headerActions,
|
|
327
|
+
minHeightClassName = "min-h-[360px]"
|
|
328
|
+
}) {
|
|
329
|
+
const graphEntry = useMemo(
|
|
330
|
+
() => resolveGraphTraceEntry(entries, selectedEntryId),
|
|
331
|
+
[entries, selectedEntryId]
|
|
332
|
+
);
|
|
333
|
+
const graph = useMemo(
|
|
334
|
+
() => graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },
|
|
335
|
+
[graphEntry, labelLookup]
|
|
336
|
+
);
|
|
337
|
+
const highlightedKeys = useMemo(() => {
|
|
338
|
+
if (!graphEntry || !selectedEntryId && !highlightRuleId) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const keys = /* @__PURE__ */ new Set();
|
|
342
|
+
if (selectedEntryId) {
|
|
343
|
+
for (const key of entryGraphKeys(graphEntry)) {
|
|
344
|
+
keys.add(key);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (highlightRuleId) {
|
|
348
|
+
for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {
|
|
349
|
+
keys.add(key);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return keys;
|
|
353
|
+
}, [graphEntry, highlightRuleId, selectedEntryId]);
|
|
354
|
+
const { nodes, edges } = useMemo(
|
|
355
|
+
() => toFlowGraph(graph, fieldState, formValues, highlightedKeys),
|
|
356
|
+
[graph, fieldState, formValues, highlightedKeys]
|
|
357
|
+
);
|
|
358
|
+
const activeEffectVariants = useMemo(() => {
|
|
359
|
+
const variants = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const edge of graph.edges) {
|
|
361
|
+
if (edge.effectType) {
|
|
362
|
+
variants.add(getEffectVariant(edge.effectType));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return variants;
|
|
366
|
+
}, [graph.edges]);
|
|
367
|
+
if (!graph.nodes.length) {
|
|
368
|
+
return /* @__PURE__ */ jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsx(NoticeBox, { title: translate("No rule relationships yet"), children: translate(
|
|
369
|
+
"Interact with the form to build the dependency graph. Only rules active in the latest evaluation are shown. Select a trace entry to inspect a past evaluation. Connections flow Field \u2192 Rule \u2192 Target (read / effect)."
|
|
370
|
+
) }) });
|
|
371
|
+
}
|
|
372
|
+
return /* @__PURE__ */ jsxs(
|
|
373
|
+
"div",
|
|
374
|
+
{
|
|
375
|
+
className: `flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ""}`,
|
|
376
|
+
children: [
|
|
377
|
+
/* @__PURE__ */ jsx(
|
|
378
|
+
GraphToolbar,
|
|
379
|
+
{
|
|
380
|
+
nodeCount: graph.nodes.length,
|
|
381
|
+
edgeCount: graph.edges.length,
|
|
382
|
+
headerActions,
|
|
383
|
+
activeEffectVariants
|
|
384
|
+
}
|
|
385
|
+
),
|
|
386
|
+
/* @__PURE__ */ jsx(
|
|
387
|
+
"div",
|
|
388
|
+
{
|
|
389
|
+
className: `${minHeightClassName} flex-1 bg-white`,
|
|
390
|
+
style: { width: "100%", height: "100%" },
|
|
391
|
+
children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(RuleGraphCanvas, { nodes, edges, layoutKey }) })
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export { RuleGraphView, toFlowGraph };
|
|
400
|
+
//# sourceMappingURL=RuleGraphView.js.map
|
|
401
|
+
//# sourceMappingURL=RuleGraphView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/RuleGraphView.tsx"],"names":[],"mappings":"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","file":"RuleGraphView.js","sourcesContent":["import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';\nimport {\n Background,\n BaseEdge,\n Controls,\n EdgeLabelRenderer,\n type Edge,\n type EdgeProps,\n getSmoothStepPath,\n Handle,\n MarkerType,\n type Node,\n type NodeProps,\n Position,\n ReactFlow,\n ReactFlowProvider,\n useEdgesState,\n useNodesState,\n useReactFlow,\n} from '@xyflow/react';\nimport { NoticeBox } from '@dhis2/ui';\nimport type { RuleTraceEntry } from '@nnkogift/dhis2-form-utils-hooks';\nimport type { FormStore } from '@nnkogift/dhis2-form-utils-hooks';\nimport { buildGraphFromTrace, type GraphNode, type RuleDependencyGraph } from './buildGraph.js';\nimport { computeGraphLayout, prepareFlowGraph } from './graphLayout.js';\nimport type { DevtoolsLabelLookup } from './createLabelLookup.js';\nimport { EffectLegend } from './EffectBadge.js';\nimport {\n getEffectEdgeStroke,\n getEffectShortLabel,\n getEffectVariant,\n type EffectVisualVariant,\n} from './effectStyles.js';\nimport { getGraphNodeClassName, getLegendSwatchClassName } from './graphNodeStyles.js';\nimport { translate } from './i18n.js';\nimport { resolveGraphTraceEntry } from './traceEntry.js';\n\ntype FieldStateMap = ReturnType<FormStore['fieldStore']['getSnapshot']>;\n\ntype RuleNodeData = {\n label: string;\n kind: GraphNode['kind'];\n graphNodeId: string;\n value?: string;\n highlighted: boolean;\n};\n\nexport type RuleGraphViewProps = {\n entries: readonly RuleTraceEntry[];\n fieldState: FieldStateMap;\n formValues: Record<string, unknown>;\n selectedEntryId: string | null;\n highlightRuleId: string | null;\n labelLookup?: DevtoolsLabelLookup;\n className?: string;\n layoutKey?: string | number;\n headerActions?: ReactNode;\n minHeightClassName?: string;\n};\n\nconst KIND_LABELS: Record<GraphNode['kind'], string> = {\n field: 'Field',\n rule: 'Rule',\n section: 'Section',\n feedback: 'Feedback',\n};\n\n/**\n * Above this edge count, low-signal `read` labels are dropped from the canvas\n * and shown on hover instead. They stay documented in the toolbar legend.\n */\nconst READ_LABEL_EDGE_THRESHOLD = 4;\n\nconst DEFAULT_EDGE_OPTIONS = {\n type: 'ruleGraphEdge' as const,\n style: { stroke: getEffectEdgeStroke('default') },\n markerEnd: {\n type: MarkerType.ArrowClosed,\n color: getEffectEdgeStroke('default'),\n },\n};\n\nfunction RuleGraphNode({ data }: NodeProps<Node<RuleNodeData>>) {\n return (\n <div className={getGraphNodeClassName(data.kind, data.highlighted)}>\n <Handle type=\"target\" position={Position.Left} />\n <span className=\"mb-dp4 block text-[0.625rem] font-semibold uppercase leading-none tracking-wide text-dhis2-grey-600\">\n {KIND_LABELS[data.kind]}\n </span>\n <div className=\"break-words font-semibold leading-[1.3]\">{data.label}</div>\n {data.value !== undefined ? (\n <div className=\"mt-dp4 border-t border-dhis2-grey-200 pt-dp4 font-mono text-[0.6875rem] break-all text-dhis2-grey-700\">\n {data.value}\n </div>\n ) : null}\n <Handle type=\"source\" position={Position.Right} />\n </div>\n );\n}\n\nconst nodeTypes = {\n ruleGraphNode: memo(RuleGraphNode),\n};\n\ntype RuleGraphEdgeData = {\n label: string;\n stroke: string;\n /** When false, the label is hidden on the canvas and shown only on hover. */\n showLabel: boolean;\n /** Per-source fan-out distance so parallel edges don't overlap. */\n offset: number;\n};\n\nfunction RuleGraphEdge({\n id,\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n markerEnd,\n style,\n data,\n}: EdgeProps<Edge<RuleGraphEdgeData>>) {\n const [hovered, setHovered] = useState(false);\n const [edgePath, labelX, labelY] = getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n offset: data?.offset ?? 20,\n });\n\n const showLabel = data ? data.showLabel || hovered : false;\n\n return (\n <>\n <BaseEdge id={id} path={edgePath} markerEnd={markerEnd} style={style} />\n <path\n d={edgePath}\n fill=\"none\"\n stroke=\"transparent\"\n strokeWidth={16}\n onMouseEnter={() => {\n setHovered(true);\n }}\n onMouseLeave={() => {\n setHovered(false);\n }}\n />\n {showLabel && data ? (\n <EdgeLabelRenderer>\n <div\n className=\"nodrag nopan absolute rounded-sm px-dp4 text-[10px] font-semibold\"\n style={{\n transform: `translate(-50%, -50%) translate(${String(labelX)}px, ${String(labelY)}px)`,\n background: 'rgba(255,255,255,0.92)',\n color: data.stroke,\n pointerEvents: 'all',\n }}\n onMouseEnter={() => {\n setHovered(true);\n }}\n onMouseLeave={() => {\n setHovered(false);\n }}\n >\n {data.label}\n </div>\n </EdgeLabelRenderer>\n ) : null}\n </>\n );\n}\n\nconst edgeTypes = {\n ruleGraphEdge: memo(RuleGraphEdge),\n};\n\nfunction effectNodeKey(effect: RuleTraceEntry['ruleResults'][number]['effects'][number]): string {\n const kind =\n effect.type === 'HIDESECTION'\n ? 'section'\n : effect.type === 'DISPLAYTEXT' || effect.type === 'DISPLAYKEYVALUEPAIR'\n ? 'feedback'\n : 'field';\n return `${kind}:${effect.targetId}`;\n}\n\nfunction entryGraphKeys(entry: RuleTraceEntry): Set<string> {\n const keys = new Set<string>();\n\n for (const fieldId of entry.changedFields) {\n keys.add(`field:${fieldId}`);\n }\n\n for (const result of entry.ruleResults) {\n keys.add(`rule:${result.ruleId}`);\n for (const effect of result.effects) {\n keys.add(effectNodeKey(effect));\n }\n }\n\n return keys;\n}\n\nfunction ruleGraphKeys(entry: RuleTraceEntry, ruleId: string): Set<string> {\n const keys = new Set<string>([`rule:${ruleId}`]);\n\n for (const result of entry.ruleResults) {\n if (result.ruleId !== ruleId) {\n continue;\n }\n for (const effect of result.effects) {\n keys.add(effectNodeKey(effect));\n }\n }\n\n return keys;\n}\n\nconst formatDisplayValue = (value: unknown): string | undefined => {\n if (value === undefined || value === null || value === '') {\n return undefined;\n }\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n return JSON.stringify(value);\n};\n\nexport function toFlowGraph(\n graph: RuleDependencyGraph,\n fieldState: FieldStateMap,\n formValues: Record<string, unknown>,\n highlightedKeys: Set<string> | null\n): { nodes: Node<RuleNodeData>[]; edges: Edge<RuleGraphEdgeData>[] } {\n const { nodes: flowNodes, edges: flowEdges } = prepareFlowGraph(graph);\n const positions = computeGraphLayout(flowNodes, flowEdges);\n const graphNodeIdByFlowId = new Map(flowNodes.map((node) => [node.id, node.graphNodeId]));\n\n const nodes: Node<RuleNodeData>[] = flowNodes.map((node) => {\n const position = positions.get(node.id) ?? { x: 0, y: 0 };\n const rawFieldId =\n node.kind === 'field' ? node.graphNodeId.slice('field:'.length) : undefined;\n const assignedValue =\n rawFieldId && rawFieldId in fieldState\n ? fieldState[rawFieldId].assignedValue\n : undefined;\n const formValue = rawFieldId ? formValues[rawFieldId] : undefined;\n const displayValue = formatDisplayValue(assignedValue) ?? formatDisplayValue(formValue);\n\n return {\n id: node.id,\n type: 'ruleGraphNode',\n position,\n data: {\n label: node.label,\n kind: node.kind,\n graphNodeId: node.graphNodeId,\n value: displayValue,\n highlighted: highlightedKeys ? highlightedKeys.has(node.graphNodeId) : true,\n },\n };\n });\n\n const dense = flowEdges.length > READ_LABEL_EDGE_THRESHOLD;\n const sourceFanOut = new Map<string, number>();\n\n const edges: Edge<RuleGraphEdgeData>[] = flowEdges.map((edge) => {\n const sourceGraphId = graphNodeIdByFlowId.get(edge.source) ?? edge.source;\n const targetGraphId = graphNodeIdByFlowId.get(edge.target) ?? edge.target;\n const isHighlighted = highlightedKeys\n ? highlightedKeys.has(sourceGraphId) && highlightedKeys.has(targetGraphId)\n : true;\n const effectType = edge.effectType ?? 'default';\n const isRead = effectType === 'read';\n const stroke = getEffectEdgeStroke(effectType, isHighlighted);\n const label = getEffectShortLabel(effectType);\n const showLabel = !(isRead && dense);\n\n const fanIndex = sourceFanOut.get(edge.source) ?? 0;\n sourceFanOut.set(edge.source, fanIndex + 1);\n const offset = 20 + fanIndex * 14;\n\n return {\n id: edge.id,\n source: edge.source,\n target: edge.target,\n type: 'ruleGraphEdge',\n animated: isHighlighted && !isRead,\n zIndex: isHighlighted ? 1 : 0,\n style: {\n stroke,\n strokeWidth: isRead ? 1 : Math.min(1 + edge.fireCount, 4),\n opacity: isHighlighted ? (isRead ? 0.5 : 1) : 0.35,\n },\n markerEnd: {\n type: MarkerType.ArrowClosed,\n color: stroke,\n },\n data: {\n label,\n stroke,\n showLabel,\n offset,\n },\n };\n });\n\n return { nodes, edges };\n}\n\ntype GraphToolbarProps = {\n nodeCount: number;\n edgeCount: number;\n headerActions?: ReactNode;\n activeEffectVariants: ReadonlySet<EffectVisualVariant>;\n};\n\nfunction GraphToolbar({\n nodeCount,\n edgeCount,\n headerActions,\n activeEffectVariants,\n}: GraphToolbarProps) {\n const items: Array<{ kind: GraphNode['kind']; label: string }> = [\n { kind: 'field', label: translate('Field') },\n { kind: 'rule', label: translate('Rule') },\n { kind: 'section', label: translate('Section') },\n { kind: 'feedback', label: translate('Feedback') },\n ];\n\n return (\n <div className=\"shrink-0 border-b border-dhis2-grey-200 bg-white\">\n <div className=\"flex flex-wrap items-center justify-between gap-dp8 px-dp12 py-dp8\">\n <div\n className=\"flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8\"\n aria-hidden=\"true\"\n >\n {items.map((item) => (\n <span\n key={item.kind}\n className=\"inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800\"\n >\n <span className={getLegendSwatchClassName(item.kind)} />\n {item.label}\n </span>\n ))}\n </div>\n <div className=\"flex shrink-0 items-center gap-dp8\">\n <span className=\"text-xs tabular-nums text-dhis2-grey-600\">\n {translate('{{nodes}} nodes · {{edges}} edges', {\n nodes: nodeCount,\n edges: edgeCount,\n })}\n </span>\n {headerActions}\n </div>\n </div>\n <EffectLegend activeVariants={activeEffectVariants} />\n </div>\n );\n}\n\ntype RuleGraphCanvasProps = {\n nodes: Node<RuleNodeData>[];\n edges: Edge<RuleGraphEdgeData>[];\n layoutKey?: string | number;\n};\n\nfunction FitViewOnChange({\n layoutKey,\n nodeCount,\n edgeCount,\n}: {\n layoutKey?: string | number;\n nodeCount: number;\n edgeCount: number;\n}) {\n const { fitView } = useReactFlow();\n\n useEffect(() => {\n const frameId = requestAnimationFrame(() => {\n void fitView({ padding: 0.2, duration: 150 });\n });\n return () => {\n cancelAnimationFrame(frameId);\n };\n }, [edgeCount, fitView, layoutKey, nodeCount]);\n\n return null;\n}\n\nfunction mergeNodePositions(\n next: Node<RuleNodeData>[],\n current: Node<RuleNodeData>[]\n): Node<RuleNodeData>[] {\n const currentById = new Map(current.map((node) => [node.id, node]));\n return next.map((node) => {\n const existing = currentById.get(node.id);\n if (existing) {\n return { ...node, position: existing.position };\n }\n return node;\n });\n}\n\nfunction RuleGraphCanvas({\n nodes: layoutNodes,\n edges: layoutEdges,\n layoutKey,\n}: RuleGraphCanvasProps) {\n const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);\n const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);\n\n useEffect(() => {\n setNodes((current) => mergeNodePositions(layoutNodes, current));\n }, [layoutNodes, setNodes]);\n\n useEffect(() => {\n setEdges(layoutEdges);\n }, [layoutEdges, setEdges]);\n\n return (\n <ReactFlow\n nodes={nodes}\n edges={edges}\n nodeTypes={nodeTypes}\n edgeTypes={edgeTypes}\n onNodesChange={onNodesChange}\n onEdgesChange={onEdgesChange}\n nodesDraggable\n defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}\n elevateEdgesOnSelect\n fitView\n proOptions={{ hideAttribution: true }}\n className=\"h-full w-full\"\n >\n <FitViewOnChange\n layoutKey={layoutKey}\n nodeCount={nodes.length}\n edgeCount={edges.length}\n />\n <Background gap={16} size={1} />\n <Controls showInteractive={false} />\n </ReactFlow>\n );\n}\n\nexport function RuleGraphView({\n entries,\n fieldState,\n formValues,\n selectedEntryId,\n highlightRuleId,\n labelLookup,\n className,\n layoutKey,\n headerActions,\n minHeightClassName = 'min-h-[360px]',\n}: RuleGraphViewProps) {\n const graphEntry = useMemo(\n () => resolveGraphTraceEntry(entries, selectedEntryId),\n [entries, selectedEntryId]\n );\n\n const graph = useMemo(\n () =>\n graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },\n [graphEntry, labelLookup]\n );\n\n const highlightedKeys = useMemo(() => {\n if (!graphEntry || (!selectedEntryId && !highlightRuleId)) {\n return null;\n }\n\n const keys = new Set<string>();\n\n if (selectedEntryId) {\n for (const key of entryGraphKeys(graphEntry)) {\n keys.add(key);\n }\n }\n\n if (highlightRuleId) {\n for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {\n keys.add(key);\n }\n }\n\n return keys;\n }, [graphEntry, highlightRuleId, selectedEntryId]);\n\n const { nodes, edges } = useMemo(\n () => toFlowGraph(graph, fieldState, formValues, highlightedKeys),\n [graph, fieldState, formValues, highlightedKeys]\n );\n\n const activeEffectVariants = useMemo(() => {\n const variants = new Set<EffectVisualVariant>();\n for (const edge of graph.edges) {\n if (edge.effectType) {\n variants.add(getEffectVariant(edge.effectType));\n }\n }\n return variants;\n }, [graph.edges]);\n\n if (!graph.nodes.length) {\n return (\n <div className=\"p-dp8\">\n <NoticeBox title={translate('No rule relationships yet')}>\n {translate(\n 'Interact with the form to build the dependency graph. Only rules active in the latest evaluation are shown. Select a trace entry to inspect a past evaluation. Connections flow Field → Rule → Target (read / effect).'\n )}\n </NoticeBox>\n </div>\n );\n }\n\n return (\n <div\n className={`flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ''}`}\n >\n <GraphToolbar\n nodeCount={graph.nodes.length}\n edgeCount={graph.edges.length}\n headerActions={headerActions}\n activeEffectVariants={activeEffectVariants}\n />\n <div\n className={`${minHeightClassName} flex-1 bg-white`}\n style={{ width: '100%', height: '100%' }}\n >\n <ReactFlowProvider>\n <RuleGraphCanvas nodes={nodes} edges={edges} layoutKey={layoutKey} />\n </ReactFlowProvider>\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { RuleDevtoolsMetadata } from './createLabelLookup.js';
|
|
3
|
+
import '@nnkogift/dhis2-form-utils-metadata';
|
|
4
|
+
|
|
5
|
+
type RulesPanelProps = {
|
|
6
|
+
metadata: RuleDevtoolsMetadata;
|
|
7
|
+
/** Show each rule's raw condition expression in the Rules tab. Defaults to true. */
|
|
8
|
+
showConditions?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The program stage currently visible in the host app, overriding the metadata-derived
|
|
11
|
+
* default. Pass `null` for a registration/enrollment view (no stage visible), or a stage
|
|
12
|
+
* UID for the stage currently being viewed. Omit to fall back to `metadata.programStageId`
|
|
13
|
+
* (event forms) or `null` (tracker forms).
|
|
14
|
+
*/
|
|
15
|
+
activeProgramStageId?: string | null;
|
|
16
|
+
};
|
|
17
|
+
declare function RulesPanel({ metadata, showConditions, activeProgramStageId, }: RulesPanelProps): react_jsx_runtime.JSX.Element;
|
|
18
|
+
|
|
19
|
+
export { RuleDevtoolsMetadata, RulesPanel, type RulesPanelProps };
|