@nnkogift/dhis2-form-utils-devtools 0.1.0-alpha.5 → 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 +1230 -1089
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +12 -90
- package/dist/index.d.ts +12 -90
- package/dist/index.js +5 -2240
- 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 +24 -4
package/dist/index.js
CHANGED
|
@@ -1,2242 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
-
import { useDataQuery } from '@dhis2/app-runtime';
|
|
8
|
-
import { MarkerType, Handle, Position, getSmoothStepPath, BaseEdge, EdgeLabelRenderer, ReactFlowProvider, useNodesState, useEdgesState, ReactFlow, Background, Controls, useReactFlow } from '@xyflow/react';
|
|
9
|
-
|
|
10
|
-
// src/RulesPanel.tsx
|
|
11
|
-
|
|
12
|
-
// src/buildGraph.ts
|
|
13
|
-
var nodeKey = (kind, id) => `${kind}:${id}`;
|
|
14
|
-
var edgeKey = (source, ruleId, target, effectType) => `${source}|${ruleId}|${target}|${effectType ?? ""}`;
|
|
15
|
-
function accumulateGraph(previous, entry, labelLookup) {
|
|
16
|
-
const nodes = new Map(previous.nodes.map((node) => [node.id, node]));
|
|
17
|
-
const edges = new Map(previous.edges.map((edge) => [edge.id, edge]));
|
|
18
|
-
const ensureNode = (kind, id, label) => {
|
|
19
|
-
const key = nodeKey(kind, id);
|
|
20
|
-
if (!nodes.has(key)) {
|
|
21
|
-
nodes.set(key, { id: key, kind, label });
|
|
22
|
-
}
|
|
23
|
-
return key;
|
|
24
|
-
};
|
|
25
|
-
for (const fieldId of entry.changedFields) {
|
|
26
|
-
ensureNode("field", fieldId, labelLookup?.resolveFieldName(fieldId) ?? fieldId);
|
|
27
|
-
}
|
|
28
|
-
for (const ruleResult of entry.ruleResults) {
|
|
29
|
-
const ruleLabel = labelLookup?.resolveRuleName(ruleResult.ruleId) ?? ruleResult.ruleId;
|
|
30
|
-
const ruleNodeId = ensureNode("rule", ruleResult.ruleId, ruleLabel);
|
|
31
|
-
for (const fieldId of entry.changedFields) {
|
|
32
|
-
const fieldNodeId = ensureNode(
|
|
33
|
-
"field",
|
|
34
|
-
fieldId,
|
|
35
|
-
labelLookup?.resolveFieldName(fieldId) ?? fieldId
|
|
36
|
-
);
|
|
37
|
-
const id = edgeKey(fieldNodeId, ruleResult.ruleId, ruleNodeId, "read");
|
|
38
|
-
const existing = edges.get(id);
|
|
39
|
-
edges.set(id, {
|
|
40
|
-
id,
|
|
41
|
-
source: fieldNodeId,
|
|
42
|
-
target: ruleNodeId,
|
|
43
|
-
effectType: "read",
|
|
44
|
-
fireCount: (existing?.fireCount ?? 0) + 1
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
for (const effect of ruleResult.effects) {
|
|
48
|
-
const targetKind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
|
|
49
|
-
const targetNodeId = ensureNode(
|
|
50
|
-
targetKind,
|
|
51
|
-
effect.targetId,
|
|
52
|
-
targetKind === "section" ? labelLookup?.resolveSectionName(effect.targetId) ?? effect.targetId : targetKind === "field" ? labelLookup?.resolveFieldName(effect.targetId) ?? effect.targetId : effect.targetId
|
|
53
|
-
);
|
|
54
|
-
const id = edgeKey(ruleNodeId, ruleResult.ruleId, targetNodeId, effect.type);
|
|
55
|
-
const existing = edges.get(id);
|
|
56
|
-
edges.set(id, {
|
|
57
|
-
id,
|
|
58
|
-
source: ruleNodeId,
|
|
59
|
-
target: targetNodeId,
|
|
60
|
-
effectType: effect.type,
|
|
61
|
-
fireCount: (existing?.fireCount ?? 0) + 1
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
nodes: [...nodes.values()],
|
|
67
|
-
edges: [...edges.values()]
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function buildGraphFromTrace(entries, labelLookup) {
|
|
71
|
-
return entries.reduce(
|
|
72
|
-
(graph, entry) => accumulateGraph(graph, entry, labelLookup),
|
|
73
|
-
{ nodes: [], edges: [] }
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
var createLookup = (rules, fields, sections, stages) => ({
|
|
77
|
-
resolveRuleName: (id) => rules.get(id) ?? id,
|
|
78
|
-
resolveFieldName: (id) => fields.get(id) ?? id,
|
|
79
|
-
resolveSectionName: (id) => sections.get(id) ?? id,
|
|
80
|
-
resolveStageName: (id) => stages.get(id) ?? id
|
|
81
|
-
});
|
|
82
|
-
function buildEventLookup(source) {
|
|
83
|
-
const rules = /* @__PURE__ */ new Map();
|
|
84
|
-
const fields = /* @__PURE__ */ new Map();
|
|
85
|
-
const sections = /* @__PURE__ */ new Map();
|
|
86
|
-
const stages = /* @__PURE__ */ new Map();
|
|
87
|
-
for (const rule of source.metadata.programRules) {
|
|
88
|
-
if (!rule.id) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
rules.set(rule.id, rule.displayName ?? rule.id);
|
|
92
|
-
}
|
|
93
|
-
const stage = selectProgramStage(
|
|
94
|
-
source.metadata,
|
|
95
|
-
source.programStageId
|
|
96
|
-
) ?? {
|
|
97
|
-
id: source.programStageId,
|
|
98
|
-
programStageDataElements: [],
|
|
99
|
-
programStageSections: []
|
|
100
|
-
};
|
|
101
|
-
for (const psde of stage.programStageDataElements ?? []) {
|
|
102
|
-
const de = psde.dataElement;
|
|
103
|
-
if (!de?.id) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
fields.set(de.id, de.displayFormName ?? de.displayName ?? de.id);
|
|
107
|
-
}
|
|
108
|
-
for (const section of stage.programStageSections ?? []) {
|
|
109
|
-
sections.set(section.id, section.displayName ?? section.id);
|
|
110
|
-
}
|
|
111
|
-
for (const programStage of source.metadata.programStages) {
|
|
112
|
-
if (!programStage.id) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
stages.set(programStage.id, programStage.displayName ?? programStage.id);
|
|
116
|
-
}
|
|
117
|
-
return createLookup(rules, fields, sections, stages);
|
|
118
|
-
}
|
|
119
|
-
function buildTrackerLookup(source) {
|
|
120
|
-
const rules = /* @__PURE__ */ new Map();
|
|
121
|
-
const fields = /* @__PURE__ */ new Map();
|
|
122
|
-
const sections = /* @__PURE__ */ new Map();
|
|
123
|
-
const stages = /* @__PURE__ */ new Map();
|
|
124
|
-
for (const rule of source.metadata.programRules) {
|
|
125
|
-
if (!rule.id) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
rules.set(rule.id, rule.name ?? rule.id);
|
|
129
|
-
}
|
|
130
|
-
for (const ptea of source.metadata.programTrackedEntityAttributes) {
|
|
131
|
-
const tea = ptea.trackedEntityAttribute;
|
|
132
|
-
if (!tea.id) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
fields.set(tea.id, tea.formName ?? tea.displayName ?? tea.id);
|
|
136
|
-
}
|
|
137
|
-
for (const section of source.metadata.programSections ?? []) {
|
|
138
|
-
sections.set(section.id, section.displayName ?? section.id);
|
|
139
|
-
}
|
|
140
|
-
for (const programStage of source.programStages ?? []) {
|
|
141
|
-
if (!programStage.id) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
stages.set(programStage.id, programStage.displayName ?? programStage.id);
|
|
145
|
-
}
|
|
146
|
-
return createLookup(rules, fields, sections, stages);
|
|
147
|
-
}
|
|
148
|
-
function createLabelLookup(source) {
|
|
149
|
-
return source.formKind === "event" ? buildEventLookup(source) : buildTrackerLookup(source);
|
|
150
|
-
}
|
|
151
|
-
var EFFECT_VARIANT = {
|
|
152
|
-
HIDEFIELD: "hide",
|
|
153
|
-
HIDEOPTION: "hide",
|
|
154
|
-
HIDEOPTIONGROUP: "hide",
|
|
155
|
-
HIDESECTION: "hide",
|
|
156
|
-
HIDEPROGRAMSTAGE: "hide",
|
|
157
|
-
SHOWFIELD: "show",
|
|
158
|
-
SHOWOPTION: "show",
|
|
159
|
-
SHOWOPTIONGROUP: "show",
|
|
160
|
-
ASSIGN: "assign",
|
|
161
|
-
SETMANDATORYFIELD: "mandatory",
|
|
162
|
-
UNSETMANDATORYFIELD: "mandatory",
|
|
163
|
-
SHOWWARNING: "warning",
|
|
164
|
-
WARNINGONCOMPLETE: "warning",
|
|
165
|
-
SHOWERROR: "error",
|
|
166
|
-
ERRORONCOMPLETE: "error",
|
|
167
|
-
DISPLAYTEXT: "feedback",
|
|
168
|
-
DISPLAYKEYVALUEPAIR: "feedback",
|
|
169
|
-
SENDMESSAGE: "message",
|
|
170
|
-
SCHEDULEMESSAGE: "message",
|
|
171
|
-
read: "read"
|
|
172
|
-
};
|
|
173
|
-
var EFFECT_VISUALS = {
|
|
174
|
-
hide: {
|
|
175
|
-
tagClassName: "bg-dhis2-grey-200 text-dhis2-grey-900",
|
|
176
|
-
edgeStroke: "#494949",
|
|
177
|
-
shortLabel: "hide"
|
|
178
|
-
},
|
|
179
|
-
show: {
|
|
180
|
-
tagClassName: "bg-dhis2-green-100 text-dhis2-green-900",
|
|
181
|
-
edgeStroke: "#388e3c",
|
|
182
|
-
shortLabel: "show"
|
|
183
|
-
},
|
|
184
|
-
assign: {
|
|
185
|
-
tagClassName: "bg-dhis2-blue-100 text-dhis2-blue-900",
|
|
186
|
-
edgeStroke: "#1565c0",
|
|
187
|
-
shortLabel: "assign"
|
|
188
|
-
},
|
|
189
|
-
mandatory: {
|
|
190
|
-
tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
|
|
191
|
-
edgeStroke: "#e56408",
|
|
192
|
-
shortLabel: "required"
|
|
193
|
-
},
|
|
194
|
-
warning: {
|
|
195
|
-
tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
|
|
196
|
-
edgeStroke: "#ff8302",
|
|
197
|
-
shortLabel: "warn"
|
|
198
|
-
},
|
|
199
|
-
error: {
|
|
200
|
-
tagClassName: "bg-dhis2-red-100 text-dhis2-red-900",
|
|
201
|
-
edgeStroke: "#c62828",
|
|
202
|
-
shortLabel: "error"
|
|
203
|
-
},
|
|
204
|
-
feedback: {
|
|
205
|
-
tagClassName: "bg-dhis2-purple-100 text-dhis2-purple-900",
|
|
206
|
-
edgeStroke: "#9c27b0",
|
|
207
|
-
shortLabel: "feedback"
|
|
208
|
-
},
|
|
209
|
-
message: {
|
|
210
|
-
tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
|
|
211
|
-
edgeStroke: "#00796b",
|
|
212
|
-
shortLabel: "message"
|
|
213
|
-
},
|
|
214
|
-
read: {
|
|
215
|
-
tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
|
|
216
|
-
edgeStroke: "#00796b",
|
|
217
|
-
shortLabel: "read"
|
|
218
|
-
},
|
|
219
|
-
default: {
|
|
220
|
-
tagClassName: "bg-dhis2-grey-100 text-dhis2-grey-800",
|
|
221
|
-
edgeStroke: "#494949",
|
|
222
|
-
shortLabel: "effect"
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
var EFFECT_ICONS = {
|
|
226
|
-
hide: IconViewOff16,
|
|
227
|
-
show: IconView16,
|
|
228
|
-
assign: IconEdit16,
|
|
229
|
-
mandatory: IconStarFilled16,
|
|
230
|
-
warning: IconWarningFilled16,
|
|
231
|
-
error: IconErrorFilled16,
|
|
232
|
-
feedback: IconMessages16,
|
|
233
|
-
message: IconMail16,
|
|
234
|
-
read: IconLink16,
|
|
235
|
-
default: IconInfo16
|
|
236
|
-
};
|
|
237
|
-
function getEffectVariant(type) {
|
|
238
|
-
return EFFECT_VARIANT[type] ?? "default";
|
|
239
|
-
}
|
|
240
|
-
function getEffectVisual(type) {
|
|
241
|
-
const variant = getEffectVariant(type);
|
|
242
|
-
return { variant, ...EFFECT_VISUALS[variant] };
|
|
243
|
-
}
|
|
244
|
-
var TAG_LAYOUT_CLASS = "max-w-full break-words";
|
|
245
|
-
function tagPropsForVariant(variant) {
|
|
246
|
-
switch (variant) {
|
|
247
|
-
case "show":
|
|
248
|
-
return { positive: true, className: TAG_LAYOUT_CLASS };
|
|
249
|
-
case "error":
|
|
250
|
-
return { negative: true, className: TAG_LAYOUT_CLASS };
|
|
251
|
-
case "assign":
|
|
252
|
-
return { neutral: true, className: TAG_LAYOUT_CLASS };
|
|
253
|
-
case "hide":
|
|
254
|
-
case "mandatory":
|
|
255
|
-
case "warning":
|
|
256
|
-
case "feedback":
|
|
257
|
-
case "message":
|
|
258
|
-
case "read":
|
|
259
|
-
return {
|
|
260
|
-
className: `${TAG_LAYOUT_CLASS} ${EFFECT_VISUALS[variant].tagClassName}`
|
|
261
|
-
};
|
|
262
|
-
default:
|
|
263
|
-
return {
|
|
264
|
-
className: `${TAG_LAYOUT_CLASS} ${EFFECT_VISUALS.default.tagClassName}`
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
function getEffectTagRenderProps(type) {
|
|
269
|
-
return tagPropsForVariant(getEffectVariant(type));
|
|
270
|
-
}
|
|
271
|
-
function getEffectTagRenderPropsForVariant(variant) {
|
|
272
|
-
return tagPropsForVariant(variant);
|
|
273
|
-
}
|
|
274
|
-
function getEffectEdgeStroke(type, highlighted = true) {
|
|
275
|
-
const { edgeStroke } = getEffectVisual(type);
|
|
276
|
-
if (highlighted) {
|
|
277
|
-
return edgeStroke;
|
|
278
|
-
}
|
|
279
|
-
return "#bdbdbd";
|
|
280
|
-
}
|
|
281
|
-
function getEffectShortLabel(type) {
|
|
282
|
-
return getEffectVisual(type).shortLabel;
|
|
283
|
-
}
|
|
284
|
-
function translate(message, variables) {
|
|
285
|
-
return String(i18n.t(message, variables));
|
|
286
|
-
}
|
|
287
|
-
function EffectBadge({ type, children, className = "" }) {
|
|
288
|
-
const variant = getEffectVariant(type);
|
|
289
|
-
const Icon = EFFECT_ICONS[variant];
|
|
290
|
-
const tagProps = getEffectTagRenderProps(type);
|
|
291
|
-
const label = children ?? type;
|
|
292
|
-
return /* @__PURE__ */ jsx(
|
|
293
|
-
Tag,
|
|
294
|
-
{
|
|
295
|
-
...tagProps,
|
|
296
|
-
icon: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" }),
|
|
297
|
-
className: [tagProps.className, className].filter(Boolean).join(" "),
|
|
298
|
-
maxWidth: "100%",
|
|
299
|
-
children: label
|
|
300
|
-
}
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
var LEGEND_VARIANTS = [
|
|
304
|
-
{ variant: "read", label: translate("Read") },
|
|
305
|
-
{ variant: "hide", label: translate("Hide") },
|
|
306
|
-
{ variant: "show", label: translate("Show") },
|
|
307
|
-
{ variant: "assign", label: translate("Assign") },
|
|
308
|
-
{ variant: "mandatory", label: translate("Required") },
|
|
309
|
-
{ variant: "warning", label: translate("Warning") },
|
|
310
|
-
{ variant: "error", label: translate("Error") },
|
|
311
|
-
{ variant: "feedback", label: translate("Feedback") }
|
|
312
|
-
];
|
|
313
|
-
function EffectLegend({ activeVariants }) {
|
|
314
|
-
const items = LEGEND_VARIANTS.filter((item) => activeVariants.has(item.variant));
|
|
315
|
-
if (!items.length) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
return /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-dp8 border-t border-dhis2-grey-200 px-dp12 py-dp8", children: items.map((item) => {
|
|
319
|
-
const Icon = EFFECT_ICONS[item.variant];
|
|
320
|
-
const tagProps = getEffectTagRenderPropsForVariant(item.variant);
|
|
321
|
-
return /* @__PURE__ */ jsx(
|
|
322
|
-
Tag,
|
|
323
|
-
{
|
|
324
|
-
...tagProps,
|
|
325
|
-
icon: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" }),
|
|
326
|
-
maxWidth: "100%",
|
|
327
|
-
children: item.label
|
|
328
|
-
},
|
|
329
|
-
item.variant
|
|
330
|
-
);
|
|
331
|
-
}) });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// src/formatAgo.ts
|
|
335
|
-
function formatAgo(timestamp, now = Date.now()) {
|
|
336
|
-
const diffMs = Math.max(0, now - timestamp);
|
|
337
|
-
if (diffMs < 5e3) {
|
|
338
|
-
return translate("just now");
|
|
339
|
-
}
|
|
340
|
-
const seconds = Math.floor(diffMs / 1e3);
|
|
341
|
-
if (seconds < 60) {
|
|
342
|
-
return translate("{{seconds}}s ago", { seconds });
|
|
343
|
-
}
|
|
344
|
-
const minutes = Math.floor(seconds / 60);
|
|
345
|
-
if (minutes < 60) {
|
|
346
|
-
return translate("{{minutes}}m ago", { minutes });
|
|
347
|
-
}
|
|
348
|
-
const hours = Math.floor(minutes / 60);
|
|
349
|
-
if (hours < 24) {
|
|
350
|
-
return translate("{{hours}}h ago", { hours });
|
|
351
|
-
}
|
|
352
|
-
return new Date(timestamp).toLocaleString();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// src/formatRuleActionSummary.ts
|
|
356
|
-
function resolveFieldLabel(id, displayName, labelLookup) {
|
|
357
|
-
if (labelLookup) {
|
|
358
|
-
const resolved = labelLookup.resolveFieldName(id);
|
|
359
|
-
if (resolved !== id) {
|
|
360
|
-
return resolved;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return displayName ?? id;
|
|
364
|
-
}
|
|
365
|
-
function resolveSectionLabel(id, displayName, labelLookup) {
|
|
366
|
-
if (labelLookup) {
|
|
367
|
-
const resolved = labelLookup.resolveSectionName(id);
|
|
368
|
-
if (resolved !== id) {
|
|
369
|
-
return resolved;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return displayName ?? id;
|
|
373
|
-
}
|
|
374
|
-
function formatRuleActionSummary(action, labelLookup) {
|
|
375
|
-
const type = action.programRuleActionType ?? "UNKNOWN";
|
|
376
|
-
let targetLabel = "";
|
|
377
|
-
if (action.dataElement?.id) {
|
|
378
|
-
targetLabel = resolveFieldLabel(
|
|
379
|
-
action.dataElement.id,
|
|
380
|
-
action.dataElement.displayName,
|
|
381
|
-
labelLookup
|
|
382
|
-
);
|
|
383
|
-
} else if (action.trackedEntityAttribute?.id) {
|
|
384
|
-
targetLabel = resolveFieldLabel(
|
|
385
|
-
action.trackedEntityAttribute.id,
|
|
386
|
-
action.trackedEntityAttribute.displayName,
|
|
387
|
-
labelLookup
|
|
388
|
-
);
|
|
389
|
-
} else if (action.programStageSection?.id) {
|
|
390
|
-
targetLabel = resolveSectionLabel(
|
|
391
|
-
action.programStageSection.id,
|
|
392
|
-
action.programStageSection.displayName,
|
|
393
|
-
labelLookup
|
|
394
|
-
);
|
|
395
|
-
} else if (action.programSection?.id) {
|
|
396
|
-
targetLabel = resolveSectionLabel(
|
|
397
|
-
action.programSection.id,
|
|
398
|
-
action.programSection.displayName,
|
|
399
|
-
labelLookup
|
|
400
|
-
);
|
|
401
|
-
} else if (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR") {
|
|
402
|
-
targetLabel = action.location ?? "feedback";
|
|
403
|
-
}
|
|
404
|
-
let detail;
|
|
405
|
-
if (action.data) {
|
|
406
|
-
detail = action.data;
|
|
407
|
-
}
|
|
408
|
-
if (action.content && (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR")) {
|
|
409
|
-
detail = action.content;
|
|
410
|
-
}
|
|
411
|
-
return { type, targetLabel, detail };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// src/resolveProgramRulesList.ts
|
|
415
|
-
function sortByPriority(rules) {
|
|
416
|
-
return [...rules].sort((left, right) => (left.priority ?? 0) - (right.priority ?? 0));
|
|
417
|
-
}
|
|
418
|
-
function toCatalogRule(rule) {
|
|
419
|
-
return {
|
|
420
|
-
id: rule.id,
|
|
421
|
-
name: rule.name,
|
|
422
|
-
condition: rule.condition,
|
|
423
|
-
priority: rule.priority,
|
|
424
|
-
programRuleActions: [...rule.programRuleActions ?? []],
|
|
425
|
-
programStageId: rule.programStageId
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function resolveProgramRulesList(metadata) {
|
|
429
|
-
if (metadata.formKind === "event") {
|
|
430
|
-
return sortByPriority(
|
|
431
|
-
metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
|
|
432
|
-
(rule) => toCatalogRule({
|
|
433
|
-
id: rule.id,
|
|
434
|
-
name: rule.displayName ?? rule.id,
|
|
435
|
-
condition: rule.condition,
|
|
436
|
-
priority: rule.priority,
|
|
437
|
-
programRuleActions: rule.programRuleActions,
|
|
438
|
-
programStageId: rule.programStage?.id ?? null
|
|
439
|
-
})
|
|
440
|
-
)
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
return sortByPriority(
|
|
444
|
-
metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
|
|
445
|
-
(rule) => toCatalogRule({
|
|
446
|
-
id: rule.id,
|
|
447
|
-
name: rule.name ?? rule.id,
|
|
448
|
-
condition: rule.condition,
|
|
449
|
-
priority: rule.priority,
|
|
450
|
-
programRuleActions: rule.programRuleActions,
|
|
451
|
-
programStageId: rule.programStage?.id ?? null
|
|
452
|
-
})
|
|
453
|
-
)
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// src/constants.ts
|
|
458
|
-
var DEFAULT_TRACE_MAX_ENTRIES = 200;
|
|
459
|
-
|
|
460
|
-
// src/traceStore.ts
|
|
461
|
-
function createRuleTraceStore(maxEntries = DEFAULT_TRACE_MAX_ENTRIES) {
|
|
462
|
-
let entries = [];
|
|
463
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
464
|
-
let disposeTraceSubscription = null;
|
|
465
|
-
const notify = () => {
|
|
466
|
-
for (const listener of listeners) {
|
|
467
|
-
listener();
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
return {
|
|
471
|
-
record(entry) {
|
|
472
|
-
entries = [...entries, entry].slice(-maxEntries);
|
|
473
|
-
notify();
|
|
474
|
-
},
|
|
475
|
-
getSnapshot() {
|
|
476
|
-
return entries;
|
|
477
|
-
},
|
|
478
|
-
subscribe(listener) {
|
|
479
|
-
listeners.add(listener);
|
|
480
|
-
return () => {
|
|
481
|
-
listeners.delete(listener);
|
|
482
|
-
};
|
|
483
|
-
},
|
|
484
|
-
dispose() {
|
|
485
|
-
disposeTraceSubscription?.();
|
|
486
|
-
disposeTraceSubscription = null;
|
|
487
|
-
listeners.clear();
|
|
488
|
-
},
|
|
489
|
-
bindTraceSubscription(unsubscribe) {
|
|
490
|
-
disposeTraceSubscription = unsubscribe;
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// src/attach.ts
|
|
496
|
-
function attachRuleDevtools(formStore, options) {
|
|
497
|
-
const store = createRuleTraceStore(
|
|
498
|
-
DEFAULT_TRACE_MAX_ENTRIES
|
|
499
|
-
);
|
|
500
|
-
store.bindTraceSubscription(formStore.subscribeTrace(store.record));
|
|
501
|
-
return store;
|
|
502
|
-
}
|
|
503
|
-
var RuleTraceContext = createContext(null);
|
|
504
|
-
function RuleDevtoolsScope({ formStore, children }) {
|
|
505
|
-
const traceStore = useMemo(() => attachRuleDevtools(formStore), [formStore]);
|
|
506
|
-
useEffect(() => {
|
|
507
|
-
return () => {
|
|
508
|
-
traceStore.dispose();
|
|
509
|
-
};
|
|
510
|
-
}, [traceStore]);
|
|
511
|
-
return /* @__PURE__ */ jsx(RuleTraceContext.Provider, { value: traceStore, children });
|
|
512
|
-
}
|
|
513
|
-
function useRuleTraceStore() {
|
|
514
|
-
const store = useContext(RuleTraceContext);
|
|
515
|
-
if (!store) {
|
|
516
|
-
throw new Error("useRuleTraceStore must be used within RuleDevtoolsScope");
|
|
517
|
-
}
|
|
518
|
-
return store;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// src/programRuleDetailQuery.ts
|
|
522
|
-
var PROGRAM_RULE_ACTION_DETAIL_FIELDS = [
|
|
523
|
-
"id",
|
|
524
|
-
"programRuleActionType",
|
|
525
|
-
"priority",
|
|
526
|
-
"content",
|
|
527
|
-
"data",
|
|
528
|
-
"location",
|
|
529
|
-
"dataElement[id,displayName]",
|
|
530
|
-
"trackedEntityAttribute[id,displayName]",
|
|
531
|
-
"option[id,displayName]",
|
|
532
|
-
"optionGroup[id,displayName]",
|
|
533
|
-
"programStageSection[id,displayName]",
|
|
534
|
-
"programStage[id,displayName]",
|
|
535
|
-
"programSection[id,displayName]"
|
|
536
|
-
].join(",");
|
|
537
|
-
var PROGRAM_RULE_DETAIL_FIELDS = [
|
|
538
|
-
"id",
|
|
539
|
-
"name",
|
|
540
|
-
"displayName",
|
|
541
|
-
"code",
|
|
542
|
-
"description",
|
|
543
|
-
"condition",
|
|
544
|
-
"priority",
|
|
545
|
-
"lastUpdated",
|
|
546
|
-
"lastUpdatedBy[displayName]",
|
|
547
|
-
"program[id,displayName]",
|
|
548
|
-
"programStage[id,displayName]",
|
|
549
|
-
`programRuleActions[${PROGRAM_RULE_ACTION_DETAIL_FIELDS}]`
|
|
550
|
-
].join(",");
|
|
551
|
-
var programRuleDetailQuery = {
|
|
552
|
-
programRule: {
|
|
553
|
-
resource: "programRules",
|
|
554
|
-
id: (variables) => variables.ruleId,
|
|
555
|
-
params: {
|
|
556
|
-
fields: PROGRAM_RULE_DETAIL_FIELDS
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
// src/useProgramRuleDetail.ts
|
|
562
|
-
function isCurrentDetail(ruleId, detail) {
|
|
563
|
-
return ruleId != null && detail?.id === ruleId;
|
|
564
|
-
}
|
|
565
|
-
function resolveDetailState(ruleId, detail, hasError) {
|
|
566
|
-
const isCurrent = isCurrentDetail(ruleId, detail);
|
|
567
|
-
return {
|
|
568
|
-
detail: isCurrent ? detail : void 0,
|
|
569
|
-
loading: ruleId != null && !isCurrent && !hasError
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
function useProgramRuleDetail(ruleId) {
|
|
573
|
-
const { data, error, refetch } = useDataQuery(
|
|
574
|
-
programRuleDetailQuery,
|
|
575
|
-
{ lazy: true }
|
|
576
|
-
);
|
|
577
|
-
useEffect(() => {
|
|
578
|
-
if (ruleId) {
|
|
579
|
-
void refetch({ ruleId });
|
|
580
|
-
}
|
|
581
|
-
}, [ruleId, refetch]);
|
|
582
|
-
return { ...resolveDetailState(ruleId, data?.programRule, Boolean(error)), error };
|
|
583
|
-
}
|
|
584
|
-
var EM_DASH = "\u2014";
|
|
585
|
-
function formatTimestamp(value) {
|
|
586
|
-
if (!value) {
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
const date = new Date(value);
|
|
590
|
-
if (Number.isNaN(date.getTime())) {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
const day = String(date.getDate());
|
|
594
|
-
const month = date.toLocaleString("en-GB", { month: "short" });
|
|
595
|
-
const year = String(date.getFullYear());
|
|
596
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
597
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
598
|
-
return `${day} ${month} ${year} ${hours}:${minutes}`;
|
|
599
|
-
}
|
|
600
|
-
function resolveStatusChip(status) {
|
|
601
|
-
switch (status) {
|
|
602
|
-
case "firing":
|
|
603
|
-
return {
|
|
604
|
-
label: translate("Firing"),
|
|
605
|
-
className: "bg-dhis2-teal-100 text-dhis2-teal-900"
|
|
606
|
-
};
|
|
607
|
-
case "idle":
|
|
608
|
-
return { label: translate("Idle"), className: "bg-dhis2-grey-200 text-dhis2-grey-700" };
|
|
609
|
-
case "out-of-scope":
|
|
610
|
-
return {
|
|
611
|
-
label: translate("Out of scope"),
|
|
612
|
-
className: "bg-dhis2-grey-200 text-dhis2-grey-600"
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
var VARIABLE_PATTERNS = [
|
|
617
|
-
{
|
|
618
|
-
pattern: /#\{[^}]*\}/g,
|
|
619
|
-
kind: "Data element",
|
|
620
|
-
className: "bg-dhis2-blue-100 text-dhis2-blue-900"
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
pattern: /A\{[^}]*\}/g,
|
|
624
|
-
kind: "Tracked entity attribute",
|
|
625
|
-
className: "bg-dhis2-teal-100 text-dhis2-teal-900"
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
pattern: /V\{[^}]*\}/g,
|
|
629
|
-
kind: "Environment variable",
|
|
630
|
-
className: "bg-dhis2-grey-200 text-dhis2-grey-900"
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
pattern: /d2:\w+(?=\()/g,
|
|
634
|
-
kind: "Function",
|
|
635
|
-
className: "bg-dhis2-yellow-100 text-dhis2-yellow-900"
|
|
636
|
-
}
|
|
637
|
-
];
|
|
638
|
-
var RESOLVABLE_VARIABLE_KINDS = /* @__PURE__ */ new Set([
|
|
639
|
-
"Data element",
|
|
640
|
-
"Tracked entity attribute"
|
|
641
|
-
]);
|
|
642
|
-
function resolveVariableDisplayName(name, programRuleVariables) {
|
|
643
|
-
const variable = programRuleVariables.find((candidate) => candidate.name === name);
|
|
644
|
-
return variable?.dataElement?.displayName ?? variable?.trackedEntityAttribute?.displayName;
|
|
645
|
-
}
|
|
646
|
-
function resolveVariableToken(token, kind, programRuleVariables) {
|
|
647
|
-
if (kind === "Function") {
|
|
648
|
-
return `${token}()`;
|
|
649
|
-
}
|
|
650
|
-
if (!RESOLVABLE_VARIABLE_KINDS.has(kind)) {
|
|
651
|
-
return token;
|
|
652
|
-
}
|
|
653
|
-
return resolveVariableDisplayName(token.slice(2, -1), programRuleVariables) ?? token;
|
|
654
|
-
}
|
|
655
|
-
function matchDistinctTokens(condition, pattern, seen) {
|
|
656
|
-
const matches = condition.match(pattern) ?? [];
|
|
657
|
-
const distinct = matches.filter((token) => !seen.has(token));
|
|
658
|
-
for (const token of distinct) {
|
|
659
|
-
seen.add(token);
|
|
660
|
-
}
|
|
661
|
-
return distinct;
|
|
662
|
-
}
|
|
663
|
-
function parseConditionVariables(condition, programRuleVariables) {
|
|
664
|
-
if (!condition) {
|
|
665
|
-
return [];
|
|
666
|
-
}
|
|
667
|
-
const seen = /* @__PURE__ */ new Set();
|
|
668
|
-
return VARIABLE_PATTERNS.flatMap(
|
|
669
|
-
({ pattern, kind, className }) => matchDistinctTokens(condition, pattern, seen).map((token) => ({
|
|
670
|
-
token,
|
|
671
|
-
kind,
|
|
672
|
-
label: resolveVariableToken(token, kind, programRuleVariables),
|
|
673
|
-
className
|
|
674
|
-
}))
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
var ACTION_TARGET_RESOLVERS = [
|
|
678
|
-
{ label: "Data element", getRef: (action) => action.dataElement },
|
|
679
|
-
{ label: "Tracked entity attribute", getRef: (action) => action.trackedEntityAttribute },
|
|
680
|
-
{ label: "Program stage section", getRef: (action) => action.programStageSection },
|
|
681
|
-
{ label: "Program stage", getRef: (action) => action.programStage },
|
|
682
|
-
{ label: "Option", getRef: (action) => action.option },
|
|
683
|
-
{ label: "Option group", getRef: (action) => action.optionGroup }
|
|
684
|
-
];
|
|
685
|
-
function formatActionTargetValue(ref) {
|
|
686
|
-
return `${ref.displayName ?? ref.id} \xB7 ${ref.id}`;
|
|
687
|
-
}
|
|
688
|
-
function resolveActionTarget(action) {
|
|
689
|
-
for (const { label, getRef } of ACTION_TARGET_RESOLVERS) {
|
|
690
|
-
const ref = getRef(action);
|
|
691
|
-
if (ref?.id) {
|
|
692
|
-
return { label: translate(label), value: formatActionTargetValue(ref) };
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
function BasicDetailCell({ label, value, mono }) {
|
|
698
|
-
return /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
699
|
-
/* @__PURE__ */ jsx("p", { className: "m-0 text-xs text-dhis2-grey-600", children: label }),
|
|
700
|
-
/* @__PURE__ */ jsx("p", { className: `m-0 mt-[2px] text-sm text-dhis2-grey-900 ${mono ? "font-mono" : ""}`, children: value })
|
|
701
|
-
] });
|
|
702
|
-
}
|
|
703
|
-
var FEEDBACK_ACTION_TYPES = /* @__PURE__ */ new Set(["DISPLAYTEXT", "DISPLAYKEYVALUEPAIR"]);
|
|
704
|
-
function resolveDataRow(action) {
|
|
705
|
-
return action.data ? { label: translate("Data (expression)"), value: action.data, mono: true } : null;
|
|
706
|
-
}
|
|
707
|
-
function resolveContentRow(action) {
|
|
708
|
-
const showContent = Boolean(action.content) && FEEDBACK_ACTION_TYPES.has(action.programRuleActionType);
|
|
709
|
-
return showContent ? { label: translate("Content (static text)"), value: action.content ?? "" } : null;
|
|
710
|
-
}
|
|
711
|
-
function resolveLocationRow(action) {
|
|
712
|
-
return action.location ? { label: translate("Location"), value: action.location, mono: true } : null;
|
|
713
|
-
}
|
|
714
|
-
function resolveActionRows(action) {
|
|
715
|
-
const rows = [
|
|
716
|
-
resolveActionTarget(action),
|
|
717
|
-
resolveDataRow(action),
|
|
718
|
-
resolveContentRow(action),
|
|
719
|
-
resolveLocationRow(action)
|
|
720
|
-
];
|
|
721
|
-
return rows.filter((row) => row !== null);
|
|
722
|
-
}
|
|
723
|
-
function ActionCard({ action, index }) {
|
|
724
|
-
const type = action.programRuleActionType;
|
|
725
|
-
const visual = getEffectVisual(type);
|
|
726
|
-
const Icon = EFFECT_ICONS[visual.variant];
|
|
727
|
-
const rows = resolveActionRows(action);
|
|
728
|
-
return /* @__PURE__ */ jsxs(
|
|
729
|
-
"div",
|
|
730
|
-
{
|
|
731
|
-
className: "relative rounded-[3px] border border-dhis2-grey-300 bg-white py-dp12 pe-dp16 ps-dp16 shadow-[0_1px_2px_rgb(0_0_0/4%)]",
|
|
732
|
-
style: { borderInlineStartWidth: 3, borderInlineStartColor: visual.edgeStroke },
|
|
733
|
-
children: [
|
|
734
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-dp12 flex items-center justify-between gap-dp8", children: [
|
|
735
|
-
/* @__PURE__ */ jsxs(
|
|
736
|
-
"span",
|
|
737
|
-
{
|
|
738
|
-
className: `inline-flex items-center gap-[4px] rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${visual.tagClassName}`,
|
|
739
|
-
children: [
|
|
740
|
-
/* @__PURE__ */ jsx(Icon, {}),
|
|
741
|
-
type
|
|
742
|
-
]
|
|
743
|
-
}
|
|
744
|
-
),
|
|
745
|
-
/* @__PURE__ */ jsx("span", { className: "text-[11px] uppercase tracking-wide text-dhis2-grey-600", children: translate("Action {{n}}", { n: index + 1 }) })
|
|
746
|
-
] }),
|
|
747
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-dp10", children: rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[172px_minmax(0,1fr)] gap-dp10", children: [
|
|
748
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-dhis2-grey-600", children: row.label }),
|
|
749
|
-
/* @__PURE__ */ jsx(
|
|
750
|
-
"span",
|
|
751
|
-
{
|
|
752
|
-
className: `min-w-0 break-words text-sm ${row.mono ? "font-mono text-[12px]" : ""}`,
|
|
753
|
-
children: row.value
|
|
754
|
-
}
|
|
755
|
-
)
|
|
756
|
-
] }, row.label)) })
|
|
757
|
-
]
|
|
758
|
-
}
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
function orDash(value) {
|
|
762
|
-
return value ?? EM_DASH;
|
|
763
|
-
}
|
|
764
|
-
function resolveDetailDisplayName(rule) {
|
|
765
|
-
return rule?.displayName;
|
|
766
|
-
}
|
|
767
|
-
function resolveDetailName(rule) {
|
|
768
|
-
return rule?.name;
|
|
769
|
-
}
|
|
770
|
-
function resolveRuleDisplayName(rule, fallback) {
|
|
771
|
-
const displayName = resolveDetailDisplayName(rule) ?? resolveDetailName(rule);
|
|
772
|
-
return displayName ?? fallback;
|
|
773
|
-
}
|
|
774
|
-
function resolveRuleStageName(rule) {
|
|
775
|
-
return rule?.programStage?.displayName;
|
|
776
|
-
}
|
|
777
|
-
function resolveStageName(rule, programStageName) {
|
|
778
|
-
if (programStageName === null) {
|
|
779
|
-
return translate("All stages (registration)");
|
|
780
|
-
}
|
|
781
|
-
return orDash(resolveRuleStageName(rule) ?? programStageName);
|
|
782
|
-
}
|
|
783
|
-
function resolveLastUpdatedByName(rule) {
|
|
784
|
-
return rule?.lastUpdatedBy?.displayName;
|
|
785
|
-
}
|
|
786
|
-
function resolveLastUpdatedValue(rule) {
|
|
787
|
-
const label = formatTimestamp(rule?.lastUpdated);
|
|
788
|
-
if (!label) {
|
|
789
|
-
return EM_DASH;
|
|
790
|
-
}
|
|
791
|
-
const by = resolveLastUpdatedByName(rule);
|
|
792
|
-
return by ? `${label} \xB7 ${by}` : label;
|
|
793
|
-
}
|
|
794
|
-
function resolvePriorityCellValue(rule) {
|
|
795
|
-
return rule?.priority != null ? String(rule.priority) : EM_DASH;
|
|
796
|
-
}
|
|
797
|
-
function resolveActionsCellValue(rule) {
|
|
798
|
-
return translate("{{n}} action(s)", { n: rule?.programRuleActions?.length ?? 0 });
|
|
799
|
-
}
|
|
800
|
-
function resolveBasicDetailCells(rule, ruleName, programStageName) {
|
|
801
|
-
return [
|
|
802
|
-
{ label: translate("Name"), value: resolveRuleDisplayName(rule, ruleName) },
|
|
803
|
-
{ label: translate("Code"), value: orDash(rule?.code), mono: true },
|
|
804
|
-
{ label: translate("Identifier"), value: orDash(rule?.id), mono: true },
|
|
805
|
-
{ label: translate("Program"), value: orDash(rule?.program?.displayName) },
|
|
806
|
-
{ label: translate("Program stage"), value: resolveStageName(rule, programStageName) },
|
|
807
|
-
{ label: translate("Priority"), value: resolvePriorityCellValue(rule) },
|
|
808
|
-
{ label: translate("Actions"), value: resolveActionsCellValue(rule) },
|
|
809
|
-
{ label: translate("Last updated"), value: resolveLastUpdatedValue(rule) }
|
|
810
|
-
];
|
|
811
|
-
}
|
|
812
|
-
function BasicDetails({
|
|
813
|
-
rule,
|
|
814
|
-
ruleName,
|
|
815
|
-
programStageName
|
|
816
|
-
}) {
|
|
817
|
-
const cells = resolveBasicDetailCells(rule, ruleName, programStageName);
|
|
818
|
-
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-x-dp24 gap-y-dp12 rounded-[3px] border border-dhis2-grey-300 bg-dhis2-grey-050 p-dp16", children: cells.map((cell) => /* @__PURE__ */ jsx(BasicDetailCell, { ...cell }, cell.label)) });
|
|
819
|
-
}
|
|
820
|
-
var SECTION_HEADING_CLASS = "m-0 mb-dp8 text-[13px] font-bold uppercase tracking-wide text-dhis2-grey-700";
|
|
821
|
-
function RuleDetailsHeader({
|
|
822
|
-
title,
|
|
823
|
-
chip,
|
|
824
|
-
description
|
|
825
|
-
}) {
|
|
826
|
-
return /* @__PURE__ */ jsxs("div", { className: "-mx-dp24 -mt-dp24 mb-0 border-b border-dhis2-grey-300 px-dp24 pb-dp16 pe-[44px] pt-[20px]", children: [
|
|
827
|
-
/* @__PURE__ */ jsx("p", { className: "m-0 text-[11px] font-bold uppercase tracking-[.09em] text-dhis2-grey-600", children: translate("Program rule") }),
|
|
828
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-[6px] flex flex-wrap items-center gap-dp8", children: [
|
|
829
|
-
/* @__PURE__ */ jsx("h2", { className: "m-0 text-xl font-medium leading-[1.3] text-dhis2-grey-900", children: title }),
|
|
830
|
-
/* @__PURE__ */ jsx(
|
|
831
|
-
"span",
|
|
832
|
-
{
|
|
833
|
-
className: `rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${chip.className}`,
|
|
834
|
-
children: chip.label
|
|
835
|
-
}
|
|
836
|
-
)
|
|
837
|
-
] }),
|
|
838
|
-
description ? /* @__PURE__ */ jsx(
|
|
839
|
-
"p",
|
|
840
|
-
{
|
|
841
|
-
className: "m-0 mt-[6px] text-sm text-dhis2-grey-700",
|
|
842
|
-
style: { textWrap: "pretty" },
|
|
843
|
-
children: description
|
|
844
|
-
}
|
|
845
|
-
) : null
|
|
846
|
-
] });
|
|
847
|
-
}
|
|
848
|
-
function ConditionSection({
|
|
849
|
-
condition,
|
|
850
|
-
variables
|
|
851
|
-
}) {
|
|
852
|
-
return /* @__PURE__ */ jsxs("section", { children: [
|
|
853
|
-
/* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Condition") }),
|
|
854
|
-
/* @__PURE__ */ jsx("pre", { className: "m-0 whitespace-pre-wrap break-words rounded-[3px] border border-dhis2-grey-300 bg-dhis2-grey-200 px-dp16 py-[14px] font-mono text-[13px] leading-[1.6] text-dhis2-grey-900", children: condition ?? EM_DASH }),
|
|
855
|
-
variables.length ? /* @__PURE__ */ jsxs("div", { className: "mt-dp12", children: [
|
|
856
|
-
/* @__PURE__ */ jsx("p", { className: "m-0 mb-dp8 text-xs text-dhis2-grey-600", children: translate("Variables referenced") }),
|
|
857
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-dp8", children: variables.map((variable) => /* @__PURE__ */ jsxs(
|
|
858
|
-
"span",
|
|
859
|
-
{
|
|
860
|
-
className: `inline-flex items-center gap-[4px] rounded-full px-dp8 py-[2px] text-xs ${variable.className}`,
|
|
861
|
-
children: [
|
|
862
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: variable.label }),
|
|
863
|
-
/* @__PURE__ */ jsx("span", { className: "opacity-70", children: translate(variable.kind) })
|
|
864
|
-
]
|
|
865
|
-
},
|
|
866
|
-
variable.token
|
|
867
|
-
)) })
|
|
868
|
-
] }) : null
|
|
869
|
-
] });
|
|
870
|
-
}
|
|
871
|
-
function ActionsSection({ actions }) {
|
|
872
|
-
return /* @__PURE__ */ jsxs("section", { children: [
|
|
873
|
-
/* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Actions") }),
|
|
874
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-dp12", children: actions.map((action, index) => /* @__PURE__ */ jsx(ActionCard, { action, index }, action.id)) })
|
|
875
|
-
] });
|
|
876
|
-
}
|
|
877
|
-
function RuleDetailsContent({
|
|
878
|
-
detail,
|
|
879
|
-
ruleName,
|
|
880
|
-
programStageName,
|
|
881
|
-
variables
|
|
882
|
-
}) {
|
|
883
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-dp24 pt-dp16", children: [
|
|
884
|
-
/* @__PURE__ */ jsxs("section", { children: [
|
|
885
|
-
/* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Basic details") }),
|
|
886
|
-
/* @__PURE__ */ jsx(
|
|
887
|
-
BasicDetails,
|
|
888
|
-
{
|
|
889
|
-
rule: detail,
|
|
890
|
-
ruleName,
|
|
891
|
-
programStageName
|
|
892
|
-
}
|
|
893
|
-
)
|
|
894
|
-
] }),
|
|
895
|
-
/* @__PURE__ */ jsx(ConditionSection, { condition: detail.condition, variables }),
|
|
896
|
-
/* @__PURE__ */ jsx(ActionsSection, { actions: detail.programRuleActions ?? [] })
|
|
897
|
-
] });
|
|
898
|
-
}
|
|
899
|
-
function RuleDetailsBody({
|
|
900
|
-
detail,
|
|
901
|
-
loading,
|
|
902
|
-
error,
|
|
903
|
-
ruleName,
|
|
904
|
-
programStageName,
|
|
905
|
-
variables
|
|
906
|
-
}) {
|
|
907
|
-
if (loading) {
|
|
908
|
-
return /* @__PURE__ */ jsx("div", { className: "flex min-h-[280px] items-center justify-center", children: /* @__PURE__ */ jsx(CircularLoader, { small: true }) });
|
|
909
|
-
}
|
|
910
|
-
if (error) {
|
|
911
|
-
return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-dhis2-red-700", children: translate("Could not load this rule: {{message}}", { message: error.message }) });
|
|
912
|
-
}
|
|
913
|
-
if (!detail) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
return /* @__PURE__ */ jsx(
|
|
917
|
-
RuleDetailsContent,
|
|
918
|
-
{
|
|
919
|
-
detail,
|
|
920
|
-
ruleName,
|
|
921
|
-
programStageName,
|
|
922
|
-
variables
|
|
923
|
-
}
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
function RuleDetailsFooter({ onClose }) {
|
|
927
|
-
return /* @__PURE__ */ jsxs(
|
|
928
|
-
"div",
|
|
929
|
-
{
|
|
930
|
-
style: { order: 3 },
|
|
931
|
-
className: "-mx-dp24 -mb-dp24 mt-dp16 flex items-center justify-between gap-dp12 border-t border-dhis2-grey-300 bg-dhis2-grey-050 px-dp24 py-[14px]",
|
|
932
|
-
children: [
|
|
933
|
-
/* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-dhis2-grey-600", children: translate("Read-only view. Edit rules in the Maintenance app.") }),
|
|
934
|
-
/* @__PURE__ */ jsx(ButtonStrip, { children: /* @__PURE__ */ jsx(Button, { secondary: true, onClick: onClose, children: translate("Close") }) })
|
|
935
|
-
]
|
|
936
|
-
}
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
function resolveActiveRuleId(open, ruleId) {
|
|
940
|
-
return open ? ruleId : null;
|
|
941
|
-
}
|
|
942
|
-
function shouldRenderModal(open, ruleId) {
|
|
943
|
-
return open && ruleId != null;
|
|
944
|
-
}
|
|
945
|
-
function RuleDetailsModal({
|
|
946
|
-
open,
|
|
947
|
-
onClose,
|
|
948
|
-
ruleId,
|
|
949
|
-
ruleName,
|
|
950
|
-
status,
|
|
951
|
-
programStageName,
|
|
952
|
-
programRuleVariables
|
|
953
|
-
}) {
|
|
954
|
-
const { detail, loading, error } = useProgramRuleDetail(resolveActiveRuleId(open, ruleId));
|
|
955
|
-
if (!shouldRenderModal(open, ruleId)) {
|
|
956
|
-
return null;
|
|
957
|
-
}
|
|
958
|
-
const chip = resolveStatusChip(status);
|
|
959
|
-
const variables = parseConditionVariables(detail?.condition, programRuleVariables);
|
|
960
|
-
const title = resolveRuleDisplayName(detail, ruleName);
|
|
961
|
-
return /* @__PURE__ */ jsxs(Modal, { position: "middle", onClose, children: [
|
|
962
|
-
/* @__PURE__ */ jsx(RuleDetailsHeader, { title, chip, description: detail?.description }),
|
|
963
|
-
/* @__PURE__ */ jsx(ModalContent, { children: /* @__PURE__ */ jsx(
|
|
964
|
-
RuleDetailsBody,
|
|
965
|
-
{
|
|
966
|
-
detail,
|
|
967
|
-
loading,
|
|
968
|
-
error,
|
|
969
|
-
ruleName,
|
|
970
|
-
programStageName,
|
|
971
|
-
variables
|
|
972
|
-
}
|
|
973
|
-
) }),
|
|
974
|
-
/* @__PURE__ */ jsx(RuleDetailsFooter, { onClose })
|
|
975
|
-
] });
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// src/graphLayout.ts
|
|
979
|
-
var ROLE_COLUMNS = {
|
|
980
|
-
source: 0,
|
|
981
|
-
rule: 280,
|
|
982
|
-
target: 560
|
|
983
|
-
};
|
|
984
|
-
var FEEDBACK_COLUMN = 720;
|
|
985
|
-
var LANE_HEIGHT = 104;
|
|
986
|
-
var STAGGER = 24;
|
|
987
|
-
function median(values) {
|
|
988
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
989
|
-
const mid = Math.floor(sorted.length / 2);
|
|
990
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
991
|
-
}
|
|
992
|
-
function staggerOffset(indexInBucket) {
|
|
993
|
-
if (indexInBucket === 0) {
|
|
994
|
-
return 0;
|
|
995
|
-
}
|
|
996
|
-
const magnitude = Math.ceil(indexInBucket / 2) * STAGGER;
|
|
997
|
-
return indexInBucket % 2 === 1 ? magnitude : -magnitude;
|
|
998
|
-
}
|
|
999
|
-
function columnFor(node) {
|
|
1000
|
-
if (node.role === "target" && node.kind === "feedback") {
|
|
1001
|
-
return FEEDBACK_COLUMN;
|
|
1002
|
-
}
|
|
1003
|
-
return ROLE_COLUMNS[node.role];
|
|
1004
|
-
}
|
|
1005
|
-
function prepareFlowGraph(graph) {
|
|
1006
|
-
const readSources = /* @__PURE__ */ new Set();
|
|
1007
|
-
const effectTargets = /* @__PURE__ */ new Set();
|
|
1008
|
-
for (const edge of graph.edges) {
|
|
1009
|
-
if (edge.effectType === "read") {
|
|
1010
|
-
readSources.add(edge.source);
|
|
1011
|
-
} else {
|
|
1012
|
-
effectTargets.add(edge.target);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
const flowNodes = [];
|
|
1016
|
-
const endpointByGraphId = /* @__PURE__ */ new Map();
|
|
1017
|
-
for (const node of graph.nodes) {
|
|
1018
|
-
if (node.kind === "rule") {
|
|
1019
|
-
flowNodes.push({
|
|
1020
|
-
id: node.id,
|
|
1021
|
-
graphNodeId: node.id,
|
|
1022
|
-
kind: node.kind,
|
|
1023
|
-
label: node.label,
|
|
1024
|
-
role: "rule"
|
|
1025
|
-
});
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
const isSource = readSources.has(node.id);
|
|
1029
|
-
const isTarget = effectTargets.has(node.id);
|
|
1030
|
-
const endpoints = {};
|
|
1031
|
-
if (isSource) {
|
|
1032
|
-
const flowId = isTarget ? `${node.id}@src` : node.id;
|
|
1033
|
-
endpoints.source = flowId;
|
|
1034
|
-
flowNodes.push({
|
|
1035
|
-
id: flowId,
|
|
1036
|
-
graphNodeId: node.id,
|
|
1037
|
-
kind: node.kind,
|
|
1038
|
-
label: node.label,
|
|
1039
|
-
role: "source"
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
if (isTarget) {
|
|
1043
|
-
const flowId = isSource ? `${node.id}@tgt` : node.id;
|
|
1044
|
-
endpoints.target = flowId;
|
|
1045
|
-
flowNodes.push({
|
|
1046
|
-
id: flowId,
|
|
1047
|
-
graphNodeId: node.id,
|
|
1048
|
-
kind: node.kind,
|
|
1049
|
-
label: node.label,
|
|
1050
|
-
role: "target"
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
endpointByGraphId.set(node.id, endpoints);
|
|
1054
|
-
}
|
|
1055
|
-
const flowEdges = graph.edges.map((edge) => {
|
|
1056
|
-
if (edge.effectType === "read") {
|
|
1057
|
-
const endpoints2 = endpointByGraphId.get(edge.source);
|
|
1058
|
-
return { ...edge, source: endpoints2?.source ?? edge.source };
|
|
1059
|
-
}
|
|
1060
|
-
const endpoints = endpointByGraphId.get(edge.target);
|
|
1061
|
-
return { ...edge, target: endpoints?.target ?? edge.target };
|
|
1062
|
-
});
|
|
1063
|
-
return { nodes: flowNodes, edges: flowEdges };
|
|
1064
|
-
}
|
|
1065
|
-
function computeGraphLayout(flowNodes, flowEdges) {
|
|
1066
|
-
const positions = /* @__PURE__ */ new Map();
|
|
1067
|
-
const ruleLane = /* @__PURE__ */ new Map();
|
|
1068
|
-
flowNodes.filter((node) => node.role === "rule").forEach((node, index) => {
|
|
1069
|
-
ruleLane.set(node.id, index);
|
|
1070
|
-
positions.set(node.id, { x: ROLE_COLUMNS.rule, y: index * LANE_HEIGHT });
|
|
1071
|
-
});
|
|
1072
|
-
const connectedRuleLanes = /* @__PURE__ */ new Map();
|
|
1073
|
-
const addLane = (nodeId, lane) => {
|
|
1074
|
-
const lanes = connectedRuleLanes.get(nodeId) ?? /* @__PURE__ */ new Set();
|
|
1075
|
-
lanes.add(lane);
|
|
1076
|
-
connectedRuleLanes.set(nodeId, lanes);
|
|
1077
|
-
};
|
|
1078
|
-
for (const edge of flowEdges) {
|
|
1079
|
-
const sourceLane = ruleLane.get(edge.source);
|
|
1080
|
-
const targetLane = ruleLane.get(edge.target);
|
|
1081
|
-
if (targetLane !== void 0 && sourceLane === void 0) {
|
|
1082
|
-
addLane(edge.source, targetLane);
|
|
1083
|
-
}
|
|
1084
|
-
if (sourceLane !== void 0 && targetLane === void 0) {
|
|
1085
|
-
addLane(edge.target, sourceLane);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
const bucketCounts = /* @__PURE__ */ new Map();
|
|
1089
|
-
for (const node of flowNodes) {
|
|
1090
|
-
if (node.role === "rule") {
|
|
1091
|
-
continue;
|
|
1092
|
-
}
|
|
1093
|
-
const lanes = connectedRuleLanes.get(node.id);
|
|
1094
|
-
const lane = lanes && lanes.size > 0 ? median([...lanes]) : 0;
|
|
1095
|
-
const column = columnFor(node);
|
|
1096
|
-
const bucketKey = `${String(column)}:${String(lane)}`;
|
|
1097
|
-
const indexInBucket = bucketCounts.get(bucketKey) ?? 0;
|
|
1098
|
-
bucketCounts.set(bucketKey, indexInBucket + 1);
|
|
1099
|
-
positions.set(node.id, {
|
|
1100
|
-
x: column,
|
|
1101
|
-
y: lane * LANE_HEIGHT + staggerOffset(indexInBucket)
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
return positions;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
// src/graphNodeStyles.ts
|
|
1108
|
-
var GRAPH_NODE_CLASSES = {
|
|
1109
|
-
field: "border-dhis2-blue-500",
|
|
1110
|
-
rule: "border-dhis2-teal-600 bg-dhis2-teal-050",
|
|
1111
|
-
section: "border-dhis2-grey-600",
|
|
1112
|
-
feedback: "border-dhis2-purple-500"
|
|
1113
|
-
};
|
|
1114
|
-
var LEGEND_SWATCH_CLASSES = {
|
|
1115
|
-
field: "bg-dhis2-blue-500",
|
|
1116
|
-
rule: "bg-dhis2-teal-600",
|
|
1117
|
-
section: "bg-dhis2-grey-600",
|
|
1118
|
-
feedback: "bg-dhis2-purple-500"
|
|
1119
|
-
};
|
|
1120
|
-
function getGraphNodeClassName(kind, highlighted) {
|
|
1121
|
-
return `p-dp8 rounded border bg-white min-w-28 max-w-[180px] text-xs shadow-[0_1px_2px_rgb(0_0_0/6%)] transition-opacity duration-150 ease-out cursor-grab active:cursor-grabbing ${GRAPH_NODE_CLASSES[kind]} ${highlighted ? "" : "opacity-35"}`;
|
|
1122
|
-
}
|
|
1123
|
-
function getLegendSwatchClassName(kind) {
|
|
1124
|
-
return `size-2.5 shrink-0 rounded-sm ${LEGEND_SWATCH_CLASSES[kind]}`;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// src/traceEntry.ts
|
|
1128
|
-
function resolveGraphTraceEntry(entries, selectedEntryId) {
|
|
1129
|
-
if (!entries.length) {
|
|
1130
|
-
return null;
|
|
1131
|
-
}
|
|
1132
|
-
if (selectedEntryId) {
|
|
1133
|
-
return entries.find((entry) => entry.id === selectedEntryId) ?? entries[entries.length - 1];
|
|
1134
|
-
}
|
|
1135
|
-
return entries[entries.length - 1];
|
|
1136
|
-
}
|
|
1137
|
-
function getActiveRuleIds(entries) {
|
|
1138
|
-
const latest = resolveGraphTraceEntry(entries);
|
|
1139
|
-
if (!latest) {
|
|
1140
|
-
return /* @__PURE__ */ new Set();
|
|
1141
|
-
}
|
|
1142
|
-
return new Set(latest.ruleResults.map((result) => result.ruleId));
|
|
1143
|
-
}
|
|
1144
|
-
var KIND_LABELS = {
|
|
1145
|
-
field: "Field",
|
|
1146
|
-
rule: "Rule",
|
|
1147
|
-
section: "Section",
|
|
1148
|
-
feedback: "Feedback"
|
|
1149
|
-
};
|
|
1150
|
-
var READ_LABEL_EDGE_THRESHOLD = 4;
|
|
1151
|
-
var DEFAULT_EDGE_OPTIONS = {
|
|
1152
|
-
type: "ruleGraphEdge",
|
|
1153
|
-
style: { stroke: getEffectEdgeStroke("default") },
|
|
1154
|
-
markerEnd: {
|
|
1155
|
-
type: MarkerType.ArrowClosed,
|
|
1156
|
-
color: getEffectEdgeStroke("default")
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
function RuleGraphNode({ data }) {
|
|
1160
|
-
return /* @__PURE__ */ jsxs("div", { className: getGraphNodeClassName(data.kind, data.highlighted), children: [
|
|
1161
|
-
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Left }),
|
|
1162
|
-
/* @__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] }),
|
|
1163
|
-
/* @__PURE__ */ jsx("div", { className: "break-words font-semibold leading-[1.3]", children: data.label }),
|
|
1164
|
-
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,
|
|
1165
|
-
/* @__PURE__ */ jsx(Handle, { type: "source", position: Position.Right })
|
|
1166
|
-
] });
|
|
1167
|
-
}
|
|
1168
|
-
var nodeTypes = {
|
|
1169
|
-
ruleGraphNode: memo(RuleGraphNode)
|
|
1170
|
-
};
|
|
1171
|
-
function RuleGraphEdge({
|
|
1172
|
-
id,
|
|
1173
|
-
sourceX,
|
|
1174
|
-
sourceY,
|
|
1175
|
-
sourcePosition,
|
|
1176
|
-
targetX,
|
|
1177
|
-
targetY,
|
|
1178
|
-
targetPosition,
|
|
1179
|
-
markerEnd,
|
|
1180
|
-
style,
|
|
1181
|
-
data
|
|
1182
|
-
}) {
|
|
1183
|
-
const [hovered, setHovered] = useState(false);
|
|
1184
|
-
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
|
1185
|
-
sourceX,
|
|
1186
|
-
sourceY,
|
|
1187
|
-
sourcePosition,
|
|
1188
|
-
targetX,
|
|
1189
|
-
targetY,
|
|
1190
|
-
targetPosition,
|
|
1191
|
-
offset: data?.offset ?? 20
|
|
1192
|
-
});
|
|
1193
|
-
const showLabel = data ? data.showLabel || hovered : false;
|
|
1194
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1195
|
-
/* @__PURE__ */ jsx(BaseEdge, { id, path: edgePath, markerEnd, style }),
|
|
1196
|
-
/* @__PURE__ */ jsx(
|
|
1197
|
-
"path",
|
|
1198
|
-
{
|
|
1199
|
-
d: edgePath,
|
|
1200
|
-
fill: "none",
|
|
1201
|
-
stroke: "transparent",
|
|
1202
|
-
strokeWidth: 16,
|
|
1203
|
-
onMouseEnter: () => {
|
|
1204
|
-
setHovered(true);
|
|
1205
|
-
},
|
|
1206
|
-
onMouseLeave: () => {
|
|
1207
|
-
setHovered(false);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
),
|
|
1211
|
-
showLabel && data ? /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx(
|
|
1212
|
-
"div",
|
|
1213
|
-
{
|
|
1214
|
-
className: "nodrag nopan absolute rounded-sm px-dp4 text-[10px] font-semibold",
|
|
1215
|
-
style: {
|
|
1216
|
-
transform: `translate(-50%, -50%) translate(${String(labelX)}px, ${String(labelY)}px)`,
|
|
1217
|
-
background: "rgba(255,255,255,0.92)",
|
|
1218
|
-
color: data.stroke,
|
|
1219
|
-
pointerEvents: "all"
|
|
1220
|
-
},
|
|
1221
|
-
onMouseEnter: () => {
|
|
1222
|
-
setHovered(true);
|
|
1223
|
-
},
|
|
1224
|
-
onMouseLeave: () => {
|
|
1225
|
-
setHovered(false);
|
|
1226
|
-
},
|
|
1227
|
-
children: data.label
|
|
1228
|
-
}
|
|
1229
|
-
) }) : null
|
|
1230
|
-
] });
|
|
1231
|
-
}
|
|
1232
|
-
var edgeTypes = {
|
|
1233
|
-
ruleGraphEdge: memo(RuleGraphEdge)
|
|
1234
|
-
};
|
|
1235
|
-
function effectNodeKey(effect) {
|
|
1236
|
-
const kind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
|
|
1237
|
-
return `${kind}:${effect.targetId}`;
|
|
1238
|
-
}
|
|
1239
|
-
function entryGraphKeys(entry) {
|
|
1240
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1241
|
-
for (const fieldId of entry.changedFields) {
|
|
1242
|
-
keys.add(`field:${fieldId}`);
|
|
1243
|
-
}
|
|
1244
|
-
for (const result of entry.ruleResults) {
|
|
1245
|
-
keys.add(`rule:${result.ruleId}`);
|
|
1246
|
-
for (const effect of result.effects) {
|
|
1247
|
-
keys.add(effectNodeKey(effect));
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
return keys;
|
|
1251
|
-
}
|
|
1252
|
-
function ruleGraphKeys(entry, ruleId) {
|
|
1253
|
-
const keys = /* @__PURE__ */ new Set([`rule:${ruleId}`]);
|
|
1254
|
-
for (const result of entry.ruleResults) {
|
|
1255
|
-
if (result.ruleId !== ruleId) {
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
for (const effect of result.effects) {
|
|
1259
|
-
keys.add(effectNodeKey(effect));
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
return keys;
|
|
1263
|
-
}
|
|
1264
|
-
var formatDisplayValue = (value) => {
|
|
1265
|
-
if (value === void 0 || value === null || value === "") {
|
|
1266
|
-
return void 0;
|
|
1267
|
-
}
|
|
1268
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1269
|
-
return String(value);
|
|
1270
|
-
}
|
|
1271
|
-
return JSON.stringify(value);
|
|
1272
|
-
};
|
|
1273
|
-
function toFlowGraph(graph, fieldState, formValues, highlightedKeys) {
|
|
1274
|
-
const { nodes: flowNodes, edges: flowEdges } = prepareFlowGraph(graph);
|
|
1275
|
-
const positions = computeGraphLayout(flowNodes, flowEdges);
|
|
1276
|
-
const graphNodeIdByFlowId = new Map(flowNodes.map((node) => [node.id, node.graphNodeId]));
|
|
1277
|
-
const nodes = flowNodes.map((node) => {
|
|
1278
|
-
const position = positions.get(node.id) ?? { x: 0, y: 0 };
|
|
1279
|
-
const rawFieldId = node.kind === "field" ? node.graphNodeId.slice("field:".length) : void 0;
|
|
1280
|
-
const assignedValue = rawFieldId && rawFieldId in fieldState ? fieldState[rawFieldId].assignedValue : void 0;
|
|
1281
|
-
const formValue = rawFieldId ? formValues[rawFieldId] : void 0;
|
|
1282
|
-
const displayValue = formatDisplayValue(assignedValue) ?? formatDisplayValue(formValue);
|
|
1283
|
-
return {
|
|
1284
|
-
id: node.id,
|
|
1285
|
-
type: "ruleGraphNode",
|
|
1286
|
-
position,
|
|
1287
|
-
data: {
|
|
1288
|
-
label: node.label,
|
|
1289
|
-
kind: node.kind,
|
|
1290
|
-
graphNodeId: node.graphNodeId,
|
|
1291
|
-
value: displayValue,
|
|
1292
|
-
highlighted: highlightedKeys ? highlightedKeys.has(node.graphNodeId) : true
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
1295
|
-
});
|
|
1296
|
-
const dense = flowEdges.length > READ_LABEL_EDGE_THRESHOLD;
|
|
1297
|
-
const sourceFanOut = /* @__PURE__ */ new Map();
|
|
1298
|
-
const edges = flowEdges.map((edge) => {
|
|
1299
|
-
const sourceGraphId = graphNodeIdByFlowId.get(edge.source) ?? edge.source;
|
|
1300
|
-
const targetGraphId = graphNodeIdByFlowId.get(edge.target) ?? edge.target;
|
|
1301
|
-
const isHighlighted = highlightedKeys ? highlightedKeys.has(sourceGraphId) && highlightedKeys.has(targetGraphId) : true;
|
|
1302
|
-
const effectType = edge.effectType ?? "default";
|
|
1303
|
-
const isRead = effectType === "read";
|
|
1304
|
-
const stroke = getEffectEdgeStroke(effectType, isHighlighted);
|
|
1305
|
-
const label = getEffectShortLabel(effectType);
|
|
1306
|
-
const showLabel = !(isRead && dense);
|
|
1307
|
-
const fanIndex = sourceFanOut.get(edge.source) ?? 0;
|
|
1308
|
-
sourceFanOut.set(edge.source, fanIndex + 1);
|
|
1309
|
-
const offset = 20 + fanIndex * 14;
|
|
1310
|
-
return {
|
|
1311
|
-
id: edge.id,
|
|
1312
|
-
source: edge.source,
|
|
1313
|
-
target: edge.target,
|
|
1314
|
-
type: "ruleGraphEdge",
|
|
1315
|
-
animated: isHighlighted && !isRead,
|
|
1316
|
-
zIndex: isHighlighted ? 1 : 0,
|
|
1317
|
-
style: {
|
|
1318
|
-
stroke,
|
|
1319
|
-
strokeWidth: isRead ? 1 : Math.min(1 + edge.fireCount, 4),
|
|
1320
|
-
opacity: isHighlighted ? isRead ? 0.5 : 1 : 0.35
|
|
1321
|
-
},
|
|
1322
|
-
markerEnd: {
|
|
1323
|
-
type: MarkerType.ArrowClosed,
|
|
1324
|
-
color: stroke
|
|
1325
|
-
},
|
|
1326
|
-
data: {
|
|
1327
|
-
label,
|
|
1328
|
-
stroke,
|
|
1329
|
-
showLabel,
|
|
1330
|
-
offset
|
|
1331
|
-
}
|
|
1332
|
-
};
|
|
1333
|
-
});
|
|
1334
|
-
return { nodes, edges };
|
|
1335
|
-
}
|
|
1336
|
-
function GraphToolbar({
|
|
1337
|
-
nodeCount,
|
|
1338
|
-
edgeCount,
|
|
1339
|
-
headerActions,
|
|
1340
|
-
activeEffectVariants
|
|
1341
|
-
}) {
|
|
1342
|
-
const items = [
|
|
1343
|
-
{ kind: "field", label: translate("Field") },
|
|
1344
|
-
{ kind: "rule", label: translate("Rule") },
|
|
1345
|
-
{ kind: "section", label: translate("Section") },
|
|
1346
|
-
{ kind: "feedback", label: translate("Feedback") }
|
|
1347
|
-
];
|
|
1348
|
-
return /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white", children: [
|
|
1349
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-dp8 px-dp12 py-dp8", children: [
|
|
1350
|
-
/* @__PURE__ */ jsx(
|
|
1351
|
-
"div",
|
|
1352
|
-
{
|
|
1353
|
-
className: "flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8",
|
|
1354
|
-
"aria-hidden": "true",
|
|
1355
|
-
children: items.map((item) => /* @__PURE__ */ jsxs(
|
|
1356
|
-
"span",
|
|
1357
|
-
{
|
|
1358
|
-
className: "inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800",
|
|
1359
|
-
children: [
|
|
1360
|
-
/* @__PURE__ */ jsx("span", { className: getLegendSwatchClassName(item.kind) }),
|
|
1361
|
-
item.label
|
|
1362
|
-
]
|
|
1363
|
-
},
|
|
1364
|
-
item.kind
|
|
1365
|
-
))
|
|
1366
|
-
}
|
|
1367
|
-
),
|
|
1368
|
-
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
|
|
1369
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs tabular-nums text-dhis2-grey-600", children: translate("{{nodes}} nodes \xB7 {{edges}} edges", {
|
|
1370
|
-
nodes: nodeCount,
|
|
1371
|
-
edges: edgeCount
|
|
1372
|
-
}) }),
|
|
1373
|
-
headerActions
|
|
1374
|
-
] })
|
|
1375
|
-
] }),
|
|
1376
|
-
/* @__PURE__ */ jsx(EffectLegend, { activeVariants: activeEffectVariants })
|
|
1377
|
-
] });
|
|
1378
|
-
}
|
|
1379
|
-
function FitViewOnChange({
|
|
1380
|
-
layoutKey,
|
|
1381
|
-
nodeCount,
|
|
1382
|
-
edgeCount
|
|
1383
|
-
}) {
|
|
1384
|
-
const { fitView } = useReactFlow();
|
|
1385
|
-
useEffect(() => {
|
|
1386
|
-
const frameId = requestAnimationFrame(() => {
|
|
1387
|
-
void fitView({ padding: 0.2, duration: 150 });
|
|
1388
|
-
});
|
|
1389
|
-
return () => {
|
|
1390
|
-
cancelAnimationFrame(frameId);
|
|
1391
|
-
};
|
|
1392
|
-
}, [edgeCount, fitView, layoutKey, nodeCount]);
|
|
1393
|
-
return null;
|
|
1394
|
-
}
|
|
1395
|
-
function mergeNodePositions(next, current) {
|
|
1396
|
-
const currentById = new Map(current.map((node) => [node.id, node]));
|
|
1397
|
-
return next.map((node) => {
|
|
1398
|
-
const existing = currentById.get(node.id);
|
|
1399
|
-
if (existing) {
|
|
1400
|
-
return { ...node, position: existing.position };
|
|
1401
|
-
}
|
|
1402
|
-
return node;
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
function RuleGraphCanvas({
|
|
1406
|
-
nodes: layoutNodes,
|
|
1407
|
-
edges: layoutEdges,
|
|
1408
|
-
layoutKey
|
|
1409
|
-
}) {
|
|
1410
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
|
1411
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);
|
|
1412
|
-
useEffect(() => {
|
|
1413
|
-
setNodes((current) => mergeNodePositions(layoutNodes, current));
|
|
1414
|
-
}, [layoutNodes, setNodes]);
|
|
1415
|
-
useEffect(() => {
|
|
1416
|
-
setEdges(layoutEdges);
|
|
1417
|
-
}, [layoutEdges, setEdges]);
|
|
1418
|
-
return /* @__PURE__ */ jsxs(
|
|
1419
|
-
ReactFlow,
|
|
1420
|
-
{
|
|
1421
|
-
nodes,
|
|
1422
|
-
edges,
|
|
1423
|
-
nodeTypes,
|
|
1424
|
-
edgeTypes,
|
|
1425
|
-
onNodesChange,
|
|
1426
|
-
onEdgesChange,
|
|
1427
|
-
nodesDraggable: true,
|
|
1428
|
-
defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
|
|
1429
|
-
elevateEdgesOnSelect: true,
|
|
1430
|
-
fitView: true,
|
|
1431
|
-
proOptions: { hideAttribution: true },
|
|
1432
|
-
className: "h-full w-full",
|
|
1433
|
-
children: [
|
|
1434
|
-
/* @__PURE__ */ jsx(
|
|
1435
|
-
FitViewOnChange,
|
|
1436
|
-
{
|
|
1437
|
-
layoutKey,
|
|
1438
|
-
nodeCount: nodes.length,
|
|
1439
|
-
edgeCount: edges.length
|
|
1440
|
-
}
|
|
1441
|
-
),
|
|
1442
|
-
/* @__PURE__ */ jsx(Background, { gap: 16, size: 1 }),
|
|
1443
|
-
/* @__PURE__ */ jsx(Controls, { showInteractive: false })
|
|
1444
|
-
]
|
|
1445
|
-
}
|
|
1446
|
-
);
|
|
1447
|
-
}
|
|
1448
|
-
function RuleGraphView({
|
|
1449
|
-
entries,
|
|
1450
|
-
fieldState,
|
|
1451
|
-
formValues,
|
|
1452
|
-
selectedEntryId,
|
|
1453
|
-
highlightRuleId,
|
|
1454
|
-
labelLookup,
|
|
1455
|
-
className,
|
|
1456
|
-
layoutKey,
|
|
1457
|
-
headerActions,
|
|
1458
|
-
minHeightClassName = "min-h-[360px]"
|
|
1459
|
-
}) {
|
|
1460
|
-
const graphEntry = useMemo(
|
|
1461
|
-
() => resolveGraphTraceEntry(entries, selectedEntryId),
|
|
1462
|
-
[entries, selectedEntryId]
|
|
1463
|
-
);
|
|
1464
|
-
const graph = useMemo(
|
|
1465
|
-
() => graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },
|
|
1466
|
-
[graphEntry, labelLookup]
|
|
1467
|
-
);
|
|
1468
|
-
const highlightedKeys = useMemo(() => {
|
|
1469
|
-
if (!graphEntry || !selectedEntryId && !highlightRuleId) {
|
|
1470
|
-
return null;
|
|
1471
|
-
}
|
|
1472
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1473
|
-
if (selectedEntryId) {
|
|
1474
|
-
for (const key of entryGraphKeys(graphEntry)) {
|
|
1475
|
-
keys.add(key);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
if (highlightRuleId) {
|
|
1479
|
-
for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {
|
|
1480
|
-
keys.add(key);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
return keys;
|
|
1484
|
-
}, [graphEntry, highlightRuleId, selectedEntryId]);
|
|
1485
|
-
const { nodes, edges } = useMemo(
|
|
1486
|
-
() => toFlowGraph(graph, fieldState, formValues, highlightedKeys),
|
|
1487
|
-
[graph, fieldState, formValues, highlightedKeys]
|
|
1488
|
-
);
|
|
1489
|
-
const activeEffectVariants = useMemo(() => {
|
|
1490
|
-
const variants = /* @__PURE__ */ new Set();
|
|
1491
|
-
for (const edge of graph.edges) {
|
|
1492
|
-
if (edge.effectType) {
|
|
1493
|
-
variants.add(getEffectVariant(edge.effectType));
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
return variants;
|
|
1497
|
-
}, [graph.edges]);
|
|
1498
|
-
if (!graph.nodes.length) {
|
|
1499
|
-
return /* @__PURE__ */ jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsx(NoticeBox, { title: translate("No rule relationships yet"), children: translate(
|
|
1500
|
-
"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)."
|
|
1501
|
-
) }) });
|
|
1502
|
-
}
|
|
1503
|
-
return /* @__PURE__ */ jsxs(
|
|
1504
|
-
"div",
|
|
1505
|
-
{
|
|
1506
|
-
className: `flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ""}`,
|
|
1507
|
-
children: [
|
|
1508
|
-
/* @__PURE__ */ jsx(
|
|
1509
|
-
GraphToolbar,
|
|
1510
|
-
{
|
|
1511
|
-
nodeCount: graph.nodes.length,
|
|
1512
|
-
edgeCount: graph.edges.length,
|
|
1513
|
-
headerActions,
|
|
1514
|
-
activeEffectVariants
|
|
1515
|
-
}
|
|
1516
|
-
),
|
|
1517
|
-
/* @__PURE__ */ jsx(
|
|
1518
|
-
"div",
|
|
1519
|
-
{
|
|
1520
|
-
className: `${minHeightClassName} flex-1 bg-white`,
|
|
1521
|
-
style: { width: "100%", height: "100%" },
|
|
1522
|
-
children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(RuleGraphCanvas, { nodes, edges, layoutKey }) })
|
|
1523
|
-
}
|
|
1524
|
-
)
|
|
1525
|
-
]
|
|
1526
|
-
}
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
function RuleGraphModal({
|
|
1530
|
-
open,
|
|
1531
|
-
onClose,
|
|
1532
|
-
subtitle,
|
|
1533
|
-
layoutKey,
|
|
1534
|
-
...graphProps
|
|
1535
|
-
}) {
|
|
1536
|
-
if (!open) {
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
return /* @__PURE__ */ jsxs(Modal, { large: true, fluid: true, onClose, children: [
|
|
1540
|
-
/* @__PURE__ */ jsxs(ModalTitle, { children: [
|
|
1541
|
-
translate("Rule dependency graph"),
|
|
1542
|
-
subtitle ? /* @__PURE__ */ jsx("span", { className: "mt-dp4 block text-sm font-normal text-dhis2-grey-700", children: subtitle }) : null
|
|
1543
|
-
] }),
|
|
1544
|
-
/* @__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(
|
|
1545
|
-
RuleGraphView,
|
|
1546
|
-
{
|
|
1547
|
-
...graphProps,
|
|
1548
|
-
layoutKey: layoutKey ?? "modal",
|
|
1549
|
-
className: "h-full min-h-0 rounded-none border-0",
|
|
1550
|
-
minHeightClassName: "min-h-0 h-full"
|
|
1551
|
-
}
|
|
1552
|
-
) }) })
|
|
1553
|
-
] });
|
|
1554
|
-
}
|
|
1555
|
-
function resolveLabel(id, resolver) {
|
|
1556
|
-
const label = resolver?.(id) ?? id;
|
|
1557
|
-
return { label, showId: label !== id };
|
|
1558
|
-
}
|
|
1559
|
-
function resolveEffectTargetLabel(effect, labelLookup) {
|
|
1560
|
-
if (effect.type === "HIDESECTION") {
|
|
1561
|
-
return resolveLabel(
|
|
1562
|
-
effect.targetId,
|
|
1563
|
-
labelLookup ? (id) => labelLookup.resolveSectionName(id) : void 0
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
1566
|
-
if (effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR") {
|
|
1567
|
-
return { label: effect.targetId, showId: false };
|
|
1568
|
-
}
|
|
1569
|
-
return resolveLabel(
|
|
1570
|
-
effect.targetId,
|
|
1571
|
-
labelLookup ? (id) => labelLookup.resolveFieldName(id) : void 0
|
|
1572
|
-
);
|
|
1573
|
-
}
|
|
1574
|
-
function TraceTimeline({
|
|
1575
|
-
entries,
|
|
1576
|
-
selectedEntryId,
|
|
1577
|
-
highlightRuleId,
|
|
1578
|
-
onSelectEntry,
|
|
1579
|
-
onHighlightRule,
|
|
1580
|
-
labelLookup
|
|
1581
|
-
}) {
|
|
1582
|
-
const [expandedIds, setExpandedIds] = useState(() => /* @__PURE__ */ new Set());
|
|
1583
|
-
useEffect(() => {
|
|
1584
|
-
if (!entries.length) {
|
|
1585
|
-
setExpandedIds(/* @__PURE__ */ new Set());
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
const newestId = entries[entries.length - 1]?.id;
|
|
1589
|
-
if (!newestId) {
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
setExpandedIds((current) => {
|
|
1593
|
-
if (current.size > 0) {
|
|
1594
|
-
return current;
|
|
1595
|
-
}
|
|
1596
|
-
return /* @__PURE__ */ new Set([newestId]);
|
|
1597
|
-
});
|
|
1598
|
-
}, [entries]);
|
|
1599
|
-
if (!entries.length) {
|
|
1600
|
-
return /* @__PURE__ */ jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsx(NoticeBox, { title: translate("No rules observed yet"), children: translate(
|
|
1601
|
-
"Interact with the form to record rule evaluations. Only rules that have fired at least once appear here."
|
|
1602
|
-
) }) });
|
|
1603
|
-
}
|
|
1604
|
-
const reversed = [...entries].reverse();
|
|
1605
|
-
const toggleExpanded = (entryId) => {
|
|
1606
|
-
setExpandedIds((current) => {
|
|
1607
|
-
const next = new Set(current);
|
|
1608
|
-
if (next.has(entryId)) {
|
|
1609
|
-
next.delete(entryId);
|
|
1610
|
-
} else {
|
|
1611
|
-
next.add(entryId);
|
|
1612
|
-
}
|
|
1613
|
-
return next;
|
|
1614
|
-
});
|
|
1615
|
-
};
|
|
1616
|
-
return /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-dp12 p-0", children: reversed.map((entry) => {
|
|
1617
|
-
const isInitial = entry.changedFields.length === 0;
|
|
1618
|
-
const ruleCount = entry.ruleResults.length;
|
|
1619
|
-
const effectCount = entry.ruleResults.reduce(
|
|
1620
|
-
(total, result) => total + result.effects.length,
|
|
1621
|
-
0
|
|
1622
|
-
);
|
|
1623
|
-
const isExpanded = expandedIds.has(entry.id);
|
|
1624
|
-
const isSelected = selectedEntryId === entry.id;
|
|
1625
|
-
return /* @__PURE__ */ jsx("li", { className: "m-0", children: /* @__PURE__ */ jsxs(
|
|
1626
|
-
Card,
|
|
1627
|
-
{
|
|
1628
|
-
className: `overflow-hidden rounded-md border border-dhis2-grey-300 bg-white shadow-[0_1px_2px_rgb(0_0_0/4%)] transition-[border-color,box-shadow] duration-150 ease-out ${isSelected ? "border-dhis2-teal-600 shadow-[0_0_0_1px_var(--color-dhis2-teal-600),0_2px_8px_rgb(0_137_123/12%)]" : ""}`,
|
|
1629
|
-
children: [
|
|
1630
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-dp4 bg-dhis2-grey-050 py-dp4 pe-dp8 ps-dp4", children: [
|
|
1631
|
-
/* @__PURE__ */ jsx(
|
|
1632
|
-
"button",
|
|
1633
|
-
{
|
|
1634
|
-
type: "button",
|
|
1635
|
-
className: "inline-flex size-10 min-h-10 min-w-10 shrink-0 cursor-pointer items-center justify-center self-center rounded border-none bg-transparent text-dhis2-grey-700 hover:bg-dhis2-grey-200 focus-visible:outline-2 focus-visible:outline-dhis2-teal-600 focus-visible:outline-offset-1",
|
|
1636
|
-
"aria-expanded": isExpanded,
|
|
1637
|
-
"aria-label": isExpanded ? translate("Collapse evaluation details") : translate("Expand evaluation details"),
|
|
1638
|
-
onClick: () => {
|
|
1639
|
-
toggleExpanded(entry.id);
|
|
1640
|
-
},
|
|
1641
|
-
children: isExpanded ? /* @__PURE__ */ jsx(IconChevronDown16, {}) : /* @__PURE__ */ jsx(IconChevronRight16, {})
|
|
1642
|
-
}
|
|
1643
|
-
),
|
|
1644
|
-
/* @__PURE__ */ jsxs(
|
|
1645
|
-
"button",
|
|
1646
|
-
{
|
|
1647
|
-
type: "button",
|
|
1648
|
-
className: "flex min-h-11 min-w-0 flex-1 cursor-pointer items-center justify-between gap-dp12 rounded border-none bg-transparent py-dp8 pe-dp8 ps-dp4 text-start font-[inherit] text-inherit hover:bg-white/80 focus-visible:outline-2 focus-visible:outline-dhis2-teal-600 focus-visible:outline-offset-1",
|
|
1649
|
-
"aria-pressed": isSelected,
|
|
1650
|
-
onClick: () => {
|
|
1651
|
-
onSelectEntry(entry.id);
|
|
1652
|
-
},
|
|
1653
|
-
children: [
|
|
1654
|
-
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col items-start gap-0.5", children: [
|
|
1655
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold leading-[1.3] text-dhis2-grey-900 tabular-nums", children: formatAgo(entry.timestamp) }),
|
|
1656
|
-
!isExpanded && effectCount > 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs leading-[1.3] text-dhis2-grey-600", children: translate("{{count}} effects", {
|
|
1657
|
-
count: effectCount
|
|
1658
|
-
}) }) : null
|
|
1659
|
-
] }),
|
|
1660
|
-
/* @__PURE__ */ jsx(
|
|
1661
|
-
Tag,
|
|
1662
|
-
{
|
|
1663
|
-
className: isInitial ? "shrink-0" : void 0,
|
|
1664
|
-
neutral: !isInitial,
|
|
1665
|
-
children: isInitial ? translate("Initial") : translate("{{count}} rules", { count: ruleCount })
|
|
1666
|
-
}
|
|
1667
|
-
)
|
|
1668
|
-
]
|
|
1669
|
-
}
|
|
1670
|
-
)
|
|
1671
|
-
] }),
|
|
1672
|
-
isExpanded ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-dp16 border-t border-dhis2-grey-200 p-dp16", children: [
|
|
1673
|
-
isInitial ? /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: ruleCount ? translate("{{count}} effects on load", {
|
|
1674
|
-
count: effectCount
|
|
1675
|
-
}) : translate("No rules fired") }) : /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-dp8", children: [
|
|
1676
|
-
/* @__PURE__ */ jsx("h3", { className: "m-0 text-[0.6875rem] font-bold uppercase leading-[1.3] tracking-wider text-dhis2-grey-600", children: translate("Changed fields") }),
|
|
1677
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-dp8", children: entry.changedFields.map((fieldId) => {
|
|
1678
|
-
const { label, showId } = resolveLabel(
|
|
1679
|
-
fieldId,
|
|
1680
|
-
labelLookup ? (id) => labelLookup.resolveFieldName(id) : void 0
|
|
1681
|
-
);
|
|
1682
|
-
return /* @__PURE__ */ jsx(
|
|
1683
|
-
"div",
|
|
1684
|
-
{
|
|
1685
|
-
className: "flex max-w-full flex-col gap-0.5",
|
|
1686
|
-
children: /* @__PURE__ */ jsx(
|
|
1687
|
-
Tag,
|
|
1688
|
-
{
|
|
1689
|
-
neutral: true,
|
|
1690
|
-
className: `max-w-full break-words text-xs ${showId ? "" : "font-mono break-all"}`,
|
|
1691
|
-
children: label
|
|
1692
|
-
}
|
|
1693
|
-
)
|
|
1694
|
-
},
|
|
1695
|
-
fieldId
|
|
1696
|
-
);
|
|
1697
|
-
}) })
|
|
1698
|
-
] }),
|
|
1699
|
-
entry.ruleResults.length === 0 ? !isInitial ? /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("No rules fired") }) : null : /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-dp8", children: [
|
|
1700
|
-
!isInitial ? /* @__PURE__ */ jsx("h3", { className: "m-0 text-[0.6875rem] font-bold uppercase leading-[1.3] tracking-wider text-dhis2-grey-600", children: translate("Rules fired") }) : null,
|
|
1701
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-dp12", children: entry.ruleResults.map((result, index) => {
|
|
1702
|
-
const { label: displayName } = resolveLabel(
|
|
1703
|
-
result.ruleId,
|
|
1704
|
-
labelLookup ? (id) => labelLookup.resolveRuleName(id) : void 0
|
|
1705
|
-
);
|
|
1706
|
-
return /* @__PURE__ */ jsxs(
|
|
1707
|
-
"div",
|
|
1708
|
-
{
|
|
1709
|
-
className: "rounded border border-dhis2-grey-200 bg-dhis2-grey-050 p-dp12",
|
|
1710
|
-
children: [
|
|
1711
|
-
index > 0 ? /* @__PURE__ */ jsx(Divider, { margin: "12px 0" }) : null,
|
|
1712
|
-
/* @__PURE__ */ jsx("div", { className: "mb-dp12 flex flex-col items-start gap-dp4", children: /* @__PURE__ */ jsx(
|
|
1713
|
-
Chip,
|
|
1714
|
-
{
|
|
1715
|
-
selected: highlightRuleId === result.ruleId,
|
|
1716
|
-
onClick: (_, event) => {
|
|
1717
|
-
event.stopPropagation();
|
|
1718
|
-
onHighlightRule(
|
|
1719
|
-
highlightRuleId === result.ruleId ? null : result.ruleId
|
|
1720
|
-
);
|
|
1721
|
-
},
|
|
1722
|
-
children: displayName
|
|
1723
|
-
}
|
|
1724
|
-
) }),
|
|
1725
|
-
/* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-dp12 p-0", children: result.effects.map((effect) => {
|
|
1726
|
-
const {
|
|
1727
|
-
label: targetLabel,
|
|
1728
|
-
showId: showTargetId
|
|
1729
|
-
} = resolveEffectTargetLabel(
|
|
1730
|
-
effect,
|
|
1731
|
-
labelLookup
|
|
1732
|
-
);
|
|
1733
|
-
return /* @__PURE__ */ jsxs(
|
|
1734
|
-
"li",
|
|
1735
|
-
{
|
|
1736
|
-
className: "flex flex-col gap-dp4",
|
|
1737
|
-
children: [
|
|
1738
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-dp8", children: [
|
|
1739
|
-
/* @__PURE__ */ jsx(
|
|
1740
|
-
EffectBadge,
|
|
1741
|
-
{
|
|
1742
|
-
type: effect.type
|
|
1743
|
-
}
|
|
1744
|
-
),
|
|
1745
|
-
/* @__PURE__ */ jsx(
|
|
1746
|
-
"span",
|
|
1747
|
-
{
|
|
1748
|
-
className: `break-words text-[0.8125rem] leading-[1.45] text-dhis2-grey-800 ${showTargetId ? "" : "break-all font-mono"}`,
|
|
1749
|
-
children: targetLabel
|
|
1750
|
-
}
|
|
1751
|
-
)
|
|
1752
|
-
] }),
|
|
1753
|
-
effect.data ? /* @__PURE__ */ jsx("p", { className: "m-0 ps-dp4 text-[0.8125rem] leading-[1.45] break-words text-dhis2-grey-600", children: effect.data }) : null
|
|
1754
|
-
]
|
|
1755
|
-
},
|
|
1756
|
-
`${effect.type}-${effect.targetId}-${effect.data ?? ""}`
|
|
1757
|
-
);
|
|
1758
|
-
}) })
|
|
1759
|
-
]
|
|
1760
|
-
},
|
|
1761
|
-
result.ruleId
|
|
1762
|
-
);
|
|
1763
|
-
}) })
|
|
1764
|
-
] })
|
|
1765
|
-
] }) : null
|
|
1766
|
-
]
|
|
1767
|
-
}
|
|
1768
|
-
) }, entry.id);
|
|
1769
|
-
}) });
|
|
1770
|
-
}
|
|
1771
|
-
var REORDER_DURATION_MS = 220;
|
|
1772
|
-
var REORDER_EASING = "ease-out";
|
|
1773
|
-
function prefersReducedMotion() {
|
|
1774
|
-
return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1775
|
-
}
|
|
1776
|
-
function useFlipReorder(orderedIds, containerRef) {
|
|
1777
|
-
const positionsRef = useRef(/* @__PURE__ */ new Map());
|
|
1778
|
-
useLayoutEffect(() => {
|
|
1779
|
-
const container = containerRef.current;
|
|
1780
|
-
if (!container) {
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
const previousPositions = positionsRef.current;
|
|
1784
|
-
const items = container.querySelectorAll("[data-rule-id]");
|
|
1785
|
-
if (!prefersReducedMotion()) {
|
|
1786
|
-
items.forEach((item) => {
|
|
1787
|
-
const id = item.dataset.ruleId;
|
|
1788
|
-
if (!id) {
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
const previousRect = previousPositions.get(id);
|
|
1792
|
-
if (!previousRect) {
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
const currentRect = item.getBoundingClientRect();
|
|
1796
|
-
const deltaY = previousRect.top - currentRect.top;
|
|
1797
|
-
if (deltaY === 0) {
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
item.style.transition = "none";
|
|
1801
|
-
item.style.transform = `translateY(${String(deltaY)}px)`;
|
|
1802
|
-
item.getBoundingClientRect();
|
|
1803
|
-
item.style.transition = `transform ${String(REORDER_DURATION_MS)}ms ${REORDER_EASING}`;
|
|
1804
|
-
item.style.transform = "";
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
const nextPositions = /* @__PURE__ */ new Map();
|
|
1808
|
-
items.forEach((item) => {
|
|
1809
|
-
const id = item.dataset.ruleId;
|
|
1810
|
-
if (id) {
|
|
1811
|
-
nextPositions.set(id, item.getBoundingClientRect());
|
|
1812
|
-
}
|
|
1813
|
-
});
|
|
1814
|
-
positionsRef.current = nextPositions;
|
|
1815
|
-
}, [orderedIds.join("|")]);
|
|
1816
|
-
}
|
|
1817
|
-
function resolveScopeStageId(metadata) {
|
|
1818
|
-
return metadata.formKind === "event" ? metadata.programStageId : null;
|
|
1819
|
-
}
|
|
1820
|
-
function isRuleInScope(rule, scopeStageId) {
|
|
1821
|
-
return rule.programStageId === null || rule.programStageId === scopeStageId;
|
|
1822
|
-
}
|
|
1823
|
-
function resolveAccentClassName(inScope, firing, selected) {
|
|
1824
|
-
if (selected) {
|
|
1825
|
-
return "bg-dhis2-blue-600";
|
|
1826
|
-
}
|
|
1827
|
-
if (!inScope) {
|
|
1828
|
-
return "bg-transparent";
|
|
1829
|
-
}
|
|
1830
|
-
return firing ? "bg-dhis2-teal-600" : "bg-dhis2-grey-400";
|
|
1831
|
-
}
|
|
1832
|
-
function resolveCardStatus(inScope, firing) {
|
|
1833
|
-
if (!inScope) {
|
|
1834
|
-
return { label: translate("Out of scope"), className: "text-dhis2-grey-500" };
|
|
1835
|
-
}
|
|
1836
|
-
return firing ? { label: translate("Firing"), className: "text-dhis2-teal-700" } : { label: translate("Idle"), className: "text-dhis2-grey-600" };
|
|
1837
|
-
}
|
|
1838
|
-
function resolveDetailsStatus(inScope, firing) {
|
|
1839
|
-
if (!inScope) {
|
|
1840
|
-
return "out-of-scope";
|
|
1841
|
-
}
|
|
1842
|
-
return firing ? "firing" : "idle";
|
|
1843
|
-
}
|
|
1844
|
-
function formatActionLabel(action) {
|
|
1845
|
-
if (action.targetLabel && action.detail) {
|
|
1846
|
-
return `${action.type} \xB7 ${action.targetLabel} = ${action.detail}`;
|
|
1847
|
-
}
|
|
1848
|
-
if (action.targetLabel) {
|
|
1849
|
-
return `${action.type} \xB7 ${action.targetLabel}`;
|
|
1850
|
-
}
|
|
1851
|
-
return action.type;
|
|
1852
|
-
}
|
|
1853
|
-
function countObservedRules(entries) {
|
|
1854
|
-
const ruleIds = /* @__PURE__ */ new Set();
|
|
1855
|
-
for (const entry of entries) {
|
|
1856
|
-
for (const result of entry.ruleResults) {
|
|
1857
|
-
ruleIds.add(result.ruleId);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return ruleIds.size;
|
|
1861
|
-
}
|
|
1862
|
-
var PANEL_TAB_KEYS = ["rules", "trace", "graph"];
|
|
1863
|
-
var SCOPE_FILTER_OPTIONS = [
|
|
1864
|
-
{ label: translate("In scope"), value: "scoped" },
|
|
1865
|
-
{ label: translate("All"), value: "all" }
|
|
1866
|
-
];
|
|
1867
|
-
function resolveTabLabel(key) {
|
|
1868
|
-
switch (key) {
|
|
1869
|
-
case "rules":
|
|
1870
|
-
return translate("Rules");
|
|
1871
|
-
case "trace":
|
|
1872
|
-
return translate("Trace");
|
|
1873
|
-
case "graph":
|
|
1874
|
-
return translate("Graph");
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
function RulesPanel({ metadata, showConditions = true }) {
|
|
1878
|
-
const formStore = useFormStore();
|
|
1879
|
-
const { form } = useFormStateContext();
|
|
1880
|
-
const traceStore = useRuleTraceStore();
|
|
1881
|
-
const [tab, setTab] = useState("rules");
|
|
1882
|
-
const [selectedEntryId, setSelectedEntryId] = useState(null);
|
|
1883
|
-
const [highlightRuleId, setHighlightRuleId] = useState(null);
|
|
1884
|
-
const [graphModalOpen, setGraphModalOpen] = useState(false);
|
|
1885
|
-
const [scopeFilter, setScopeFilter] = useState("scoped");
|
|
1886
|
-
const [detailsRuleId, setDetailsRuleId] = useState(null);
|
|
1887
|
-
const labelLookup = useMemo(() => createLabelLookup(metadata), [metadata]);
|
|
1888
|
-
const catalog = useMemo(() => resolveProgramRulesList(metadata), [metadata]);
|
|
1889
|
-
const scopeStageId = useMemo(() => resolveScopeStageId(metadata), [metadata]);
|
|
1890
|
-
const entries = useSyncExternalStore(
|
|
1891
|
-
useCallback((listener) => traceStore.subscribe(listener), [traceStore]),
|
|
1892
|
-
useCallback(() => traceStore.getSnapshot(), [traceStore]),
|
|
1893
|
-
useCallback(() => traceStore.getSnapshot(), [traceStore])
|
|
1894
|
-
);
|
|
1895
|
-
const fieldState = useSyncExternalStore(
|
|
1896
|
-
useCallback((listener) => formStore.fieldStore.subscribeAll(listener), [formStore]),
|
|
1897
|
-
useCallback(() => formStore.fieldStore.getSnapshot(), [formStore]),
|
|
1898
|
-
useCallback(() => formStore.fieldStore.getSnapshot(), [formStore])
|
|
1899
|
-
);
|
|
1900
|
-
const activeRuleIds = useMemo(() => getActiveRuleIds(entries), [entries]);
|
|
1901
|
-
const scopedRules = useMemo(
|
|
1902
|
-
() => catalog.filter((rule) => isRuleInScope(rule, scopeStageId)),
|
|
1903
|
-
[catalog, scopeStageId]
|
|
1904
|
-
);
|
|
1905
|
-
const firingCount = useMemo(
|
|
1906
|
-
() => scopedRules.filter((rule) => activeRuleIds.has(rule.id)).length,
|
|
1907
|
-
[scopedRules, activeRuleIds]
|
|
1908
|
-
);
|
|
1909
|
-
const selectedEntry = useMemo(
|
|
1910
|
-
() => entries.find((entry) => entry.id === selectedEntryId) ?? null,
|
|
1911
|
-
[entries, selectedEntryId]
|
|
1912
|
-
);
|
|
1913
|
-
const observedRuleCount = useMemo(() => countObservedRules(entries), [entries]);
|
|
1914
|
-
const highlightedRuleName = useMemo(() => {
|
|
1915
|
-
if (!highlightRuleId) {
|
|
1916
|
-
return null;
|
|
1917
|
-
}
|
|
1918
|
-
return labelLookup.resolveRuleName(highlightRuleId);
|
|
1919
|
-
}, [highlightRuleId, labelLookup]);
|
|
1920
|
-
const graphHasNodes = useMemo(() => {
|
|
1921
|
-
const entry = resolveGraphTraceEntry(entries, selectedEntryId);
|
|
1922
|
-
return entry ? buildGraphFromTrace([entry], labelLookup).nodes.length > 0 : false;
|
|
1923
|
-
}, [entries, labelLookup, selectedEntryId]);
|
|
1924
|
-
const graphSubtitle = useMemo(() => {
|
|
1925
|
-
if (selectedEntry && highlightedRuleName) {
|
|
1926
|
-
return translate("Highlighting evaluation {{time}} \xB7 Rule: {{name}}", {
|
|
1927
|
-
time: formatAgo(selectedEntry.timestamp),
|
|
1928
|
-
name: highlightedRuleName
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
if (selectedEntry) {
|
|
1932
|
-
return translate("Highlighting evaluation {{time}}", {
|
|
1933
|
-
time: formatAgo(selectedEntry.timestamp)
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
if (highlightedRuleName) {
|
|
1937
|
-
return translate("Rule: {{name}}", { name: highlightedRuleName });
|
|
1938
|
-
}
|
|
1939
|
-
return null;
|
|
1940
|
-
}, [highlightedRuleName, selectedEntry]);
|
|
1941
|
-
const detailsRule = useMemo(
|
|
1942
|
-
() => catalog.find((rule) => rule.id === detailsRuleId) ?? null,
|
|
1943
|
-
[catalog, detailsRuleId]
|
|
1944
|
-
);
|
|
1945
|
-
const detailsStatus = detailsRule ? resolveDetailsStatus(
|
|
1946
|
-
isRuleInScope(detailsRule, scopeStageId),
|
|
1947
|
-
activeRuleIds.has(detailsRule.id)
|
|
1948
|
-
) : "idle";
|
|
1949
|
-
const detailsProgramStageName = detailsRule ? detailsRule.programStageId === null ? null : labelLookup.resolveStageName(detailsRule.programStageId) : void 0;
|
|
1950
|
-
const graphProps = {
|
|
1951
|
-
entries,
|
|
1952
|
-
fieldState,
|
|
1953
|
-
formValues: form.getValues(),
|
|
1954
|
-
selectedEntryId,
|
|
1955
|
-
highlightRuleId,
|
|
1956
|
-
labelLookup
|
|
1957
|
-
};
|
|
1958
|
-
const expandGraphButton = /* @__PURE__ */ jsx(
|
|
1959
|
-
"button",
|
|
1960
|
-
{
|
|
1961
|
-
type: "button",
|
|
1962
|
-
className: "inline-flex size-11 min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded border border-dhis2-grey-300 bg-white text-dhis2-grey-800 hover:bg-dhis2-grey-100 focus-visible:outline-2 focus-visible:outline-dhis2-teal-600 focus-visible:outline-offset-1",
|
|
1963
|
-
"aria-label": translate("Open graph in full screen"),
|
|
1964
|
-
onClick: () => {
|
|
1965
|
-
setGraphModalOpen(true);
|
|
1966
|
-
},
|
|
1967
|
-
children: /* @__PURE__ */ jsx(IconFullscreen16, {})
|
|
1968
|
-
}
|
|
1969
|
-
);
|
|
1970
|
-
return /* @__PURE__ */ jsxs(
|
|
1971
|
-
"aside",
|
|
1972
|
-
{
|
|
1973
|
-
className: "flex h-full w-[404px] min-w-[320px] max-w-[min(92vw,480px)] flex-col border-s border-dhis2-grey-400 bg-dhis2-grey-100",
|
|
1974
|
-
"aria-label": translate("Rules"),
|
|
1975
|
-
children: [
|
|
1976
|
-
/* @__PURE__ */ jsxs("header", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white px-dp16 pt-dp12", children: [
|
|
1977
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-dp12 pb-[10px]", children: [
|
|
1978
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-dp8", children: [
|
|
1979
|
-
/* @__PURE__ */ jsx(
|
|
1980
|
-
"span",
|
|
1981
|
-
{
|
|
1982
|
-
className: "size-2 shrink-0 rounded-full bg-dhis2-teal-600",
|
|
1983
|
-
"aria-hidden": "true"
|
|
1984
|
-
}
|
|
1985
|
-
),
|
|
1986
|
-
/* @__PURE__ */ jsx("h2", { className: "m-0 text-base font-bold leading-[1.35] text-dhis2-grey-900", children: translate("Rules") })
|
|
1987
|
-
] }),
|
|
1988
|
-
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
|
|
1989
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-dhis2-grey-600", children: translate("{{scoped}} in scope \xB7 {{firing}} firing", {
|
|
1990
|
-
scoped: scopedRules.length,
|
|
1991
|
-
firing: firingCount
|
|
1992
|
-
}) }),
|
|
1993
|
-
/* @__PURE__ */ jsx(
|
|
1994
|
-
SegmentedControl,
|
|
1995
|
-
{
|
|
1996
|
-
options: SCOPE_FILTER_OPTIONS,
|
|
1997
|
-
selected: scopeFilter,
|
|
1998
|
-
onChange: ({ value }) => {
|
|
1999
|
-
setScopeFilter(value);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
)
|
|
2003
|
-
] })
|
|
2004
|
-
] }),
|
|
2005
|
-
/* @__PURE__ */ jsx("div", { className: "flex", children: PANEL_TAB_KEYS.map((key) => /* @__PURE__ */ jsx(
|
|
2006
|
-
"button",
|
|
2007
|
-
{
|
|
2008
|
-
type: "button",
|
|
2009
|
-
className: `cursor-pointer border-0 border-b-[3px] bg-transparent px-dp12 py-dp8 text-sm font-medium ${tab === key ? "border-b-dhis2-blue-600 text-dhis2-blue-600" : "border-b-transparent text-dhis2-grey-700"}`,
|
|
2010
|
-
onClick: () => {
|
|
2011
|
-
setTab(key);
|
|
2012
|
-
},
|
|
2013
|
-
children: resolveTabLabel(key)
|
|
2014
|
-
},
|
|
2015
|
-
key
|
|
2016
|
-
)) })
|
|
2017
|
-
] }),
|
|
2018
|
-
tab !== "rules" && (selectedEntry || highlightedRuleName) ? /* @__PURE__ */ jsxs("div", { className: "mx-dp16 mt-dp12 flex shrink-0 items-center justify-between gap-dp12 rounded border border-dhis2-teal-400 bg-dhis2-teal-050 p-dp12 text-[0.8125rem] leading-[1.45] text-dhis2-grey-900", children: [
|
|
2019
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
|
|
2020
|
-
selectedEntry ? translate("Highlighting evaluation {{time}}", {
|
|
2021
|
-
time: formatAgo(selectedEntry.timestamp)
|
|
2022
|
-
}) : null,
|
|
2023
|
-
selectedEntry && highlightedRuleName ? " \xB7 " : null,
|
|
2024
|
-
highlightedRuleName ? translate("Rule: {{name}}", { name: highlightedRuleName }) : null
|
|
2025
|
-
] }),
|
|
2026
|
-
/* @__PURE__ */ jsx(
|
|
2027
|
-
Button,
|
|
2028
|
-
{
|
|
2029
|
-
small: true,
|
|
2030
|
-
onClick: () => {
|
|
2031
|
-
setSelectedEntryId(null);
|
|
2032
|
-
setHighlightRuleId(null);
|
|
2033
|
-
},
|
|
2034
|
-
children: translate("Clear")
|
|
2035
|
-
}
|
|
2036
|
-
)
|
|
2037
|
-
] }) : null,
|
|
2038
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto p-dp16", children: tab === "rules" ? /* @__PURE__ */ jsx(
|
|
2039
|
-
RulesTab,
|
|
2040
|
-
{
|
|
2041
|
-
catalog,
|
|
2042
|
-
scopeStageId,
|
|
2043
|
-
scopeFilter,
|
|
2044
|
-
activeRuleIds,
|
|
2045
|
-
selectedRuleId: highlightRuleId,
|
|
2046
|
-
showConditions,
|
|
2047
|
-
labelLookup,
|
|
2048
|
-
onSelectRule: (ruleId) => {
|
|
2049
|
-
setHighlightRuleId(ruleId);
|
|
2050
|
-
setTab("graph");
|
|
2051
|
-
},
|
|
2052
|
-
onOpenDetails: (ruleId) => {
|
|
2053
|
-
setDetailsRuleId(ruleId);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
) : tab === "trace" ? /* @__PURE__ */ jsx(
|
|
2057
|
-
TraceTimeline,
|
|
2058
|
-
{
|
|
2059
|
-
entries,
|
|
2060
|
-
selectedEntryId,
|
|
2061
|
-
highlightRuleId,
|
|
2062
|
-
onSelectEntry: (entryId) => {
|
|
2063
|
-
setSelectedEntryId((current) => current === entryId ? null : entryId);
|
|
2064
|
-
},
|
|
2065
|
-
onHighlightRule: (ruleId) => {
|
|
2066
|
-
setHighlightRuleId(ruleId);
|
|
2067
|
-
if (ruleId) {
|
|
2068
|
-
setTab("graph");
|
|
2069
|
-
}
|
|
2070
|
-
},
|
|
2071
|
-
labelLookup
|
|
2072
|
-
}
|
|
2073
|
-
) : /* @__PURE__ */ jsx(
|
|
2074
|
-
RuleGraphView,
|
|
2075
|
-
{
|
|
2076
|
-
...graphProps,
|
|
2077
|
-
headerActions: graphHasNodes ? expandGraphButton : void 0
|
|
2078
|
-
}
|
|
2079
|
-
) }),
|
|
2080
|
-
entries.length > 0 && tab !== "rules" ? /* @__PURE__ */ jsx("p", { className: "sr-only", "aria-live": "polite", children: translate("{{evaluations}} evaluations \xB7 {{rules}} rules observed", {
|
|
2081
|
-
evaluations: entries.length,
|
|
2082
|
-
rules: observedRuleCount
|
|
2083
|
-
}) }) : null,
|
|
2084
|
-
/* @__PURE__ */ jsx(
|
|
2085
|
-
RuleGraphModal,
|
|
2086
|
-
{
|
|
2087
|
-
...graphProps,
|
|
2088
|
-
open: graphModalOpen,
|
|
2089
|
-
onClose: () => {
|
|
2090
|
-
setGraphModalOpen(false);
|
|
2091
|
-
},
|
|
2092
|
-
subtitle: graphSubtitle,
|
|
2093
|
-
layoutKey: graphModalOpen ? "open" : "closed"
|
|
2094
|
-
}
|
|
2095
|
-
),
|
|
2096
|
-
/* @__PURE__ */ jsx(
|
|
2097
|
-
RuleDetailsModal,
|
|
2098
|
-
{
|
|
2099
|
-
open: detailsRuleId != null,
|
|
2100
|
-
onClose: () => {
|
|
2101
|
-
setDetailsRuleId(null);
|
|
2102
|
-
},
|
|
2103
|
-
ruleId: detailsRuleId,
|
|
2104
|
-
ruleName: detailsRule?.name ?? "",
|
|
2105
|
-
status: detailsStatus,
|
|
2106
|
-
programStageName: detailsProgramStageName,
|
|
2107
|
-
programRuleVariables: metadata.metadata.programRuleVariables
|
|
2108
|
-
}
|
|
2109
|
-
)
|
|
2110
|
-
]
|
|
2111
|
-
}
|
|
2112
|
-
);
|
|
2113
|
-
}
|
|
2114
|
-
function sortRulesFiringFirst(visibleRules, activeRuleIds) {
|
|
2115
|
-
const firing = [];
|
|
2116
|
-
const idle = [];
|
|
2117
|
-
for (const rule of visibleRules) {
|
|
2118
|
-
(activeRuleIds.has(rule.id) ? firing : idle).push(rule);
|
|
2119
|
-
}
|
|
2120
|
-
return [...firing, ...idle];
|
|
2121
|
-
}
|
|
2122
|
-
function RulesTab({
|
|
2123
|
-
catalog,
|
|
2124
|
-
scopeStageId,
|
|
2125
|
-
scopeFilter,
|
|
2126
|
-
activeRuleIds,
|
|
2127
|
-
selectedRuleId,
|
|
2128
|
-
showConditions,
|
|
2129
|
-
labelLookup,
|
|
2130
|
-
onSelectRule,
|
|
2131
|
-
onOpenDetails
|
|
2132
|
-
}) {
|
|
2133
|
-
const listRef = useRef(null);
|
|
2134
|
-
const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
|
|
2135
|
-
const sortedRules = useMemo(
|
|
2136
|
-
() => sortRulesFiringFirst(visibleRules, activeRuleIds),
|
|
2137
|
-
[visibleRules, activeRuleIds]
|
|
2138
|
-
);
|
|
2139
|
-
useFlipReorder(
|
|
2140
|
-
useMemo(() => sortedRules.map((rule) => rule.id), [sortedRules]),
|
|
2141
|
-
listRef
|
|
2142
|
-
);
|
|
2143
|
-
if (!catalog.length) {
|
|
2144
|
-
return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("This program has no rules.") });
|
|
2145
|
-
}
|
|
2146
|
-
if (!sortedRules.length) {
|
|
2147
|
-
return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("No rules are in scope for this stage.") });
|
|
2148
|
-
}
|
|
2149
|
-
return /* @__PURE__ */ jsx("ul", { ref: listRef, className: "m-0 flex list-none flex-col gap-[10px] p-0", children: sortedRules.map((rule) => {
|
|
2150
|
-
const inScope = isRuleInScope(rule, scopeStageId);
|
|
2151
|
-
const firing = activeRuleIds.has(rule.id);
|
|
2152
|
-
const isSelected = selectedRuleId === rule.id;
|
|
2153
|
-
const status = resolveCardStatus(inScope, firing);
|
|
2154
|
-
return /* @__PURE__ */ jsx("li", { "data-rule-id": rule.id, className: "m-0 shrink-0", children: /* @__PURE__ */ jsxs(
|
|
2155
|
-
"article",
|
|
2156
|
-
{
|
|
2157
|
-
role: "button",
|
|
2158
|
-
tabIndex: 0,
|
|
2159
|
-
onClick: () => {
|
|
2160
|
-
onSelectRule(rule.id);
|
|
2161
|
-
},
|
|
2162
|
-
onKeyDown: (event) => {
|
|
2163
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
2164
|
-
event.preventDefault();
|
|
2165
|
-
onSelectRule(rule.id);
|
|
2166
|
-
}
|
|
2167
|
-
},
|
|
2168
|
-
className: "relative flex cursor-pointer flex-col gap-dp8 rounded-[5px] border border-dhis2-grey-300 bg-white py-[11px] pe-[12px] ps-[14px] shadow-[0_1px_2px_rgb(0_0_0/4%)]",
|
|
2169
|
-
children: [
|
|
2170
|
-
/* @__PURE__ */ jsx(
|
|
2171
|
-
"span",
|
|
2172
|
-
{
|
|
2173
|
-
className: `absolute inset-y-0 start-0 w-[3px] rounded-s-[5px] ${resolveAccentClassName(inScope, firing, isSelected)}`,
|
|
2174
|
-
"aria-hidden": "true"
|
|
2175
|
-
}
|
|
2176
|
-
),
|
|
2177
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-dp8", children: [
|
|
2178
|
-
/* @__PURE__ */ jsx(
|
|
2179
|
-
"h3",
|
|
2180
|
-
{
|
|
2181
|
-
className: `m-0 min-w-0 flex-1 text-sm font-semibold leading-[1.4] ${inScope ? "text-dhis2-grey-900" : "text-dhis2-grey-600"}`,
|
|
2182
|
-
children: rule.name
|
|
2183
|
-
}
|
|
2184
|
-
),
|
|
2185
|
-
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-[6px]", children: [
|
|
2186
|
-
/* @__PURE__ */ jsx(
|
|
2187
|
-
"span",
|
|
2188
|
-
{
|
|
2189
|
-
className: `text-[11px] font-semibold ${status.className}`,
|
|
2190
|
-
children: status.label
|
|
2191
|
-
}
|
|
2192
|
-
),
|
|
2193
|
-
/* @__PURE__ */ jsx(
|
|
2194
|
-
"button",
|
|
2195
|
-
{
|
|
2196
|
-
type: "button",
|
|
2197
|
-
title: translate("Program rule details"),
|
|
2198
|
-
"aria-label": translate("Program rule details"),
|
|
2199
|
-
onClick: (event) => {
|
|
2200
|
-
event.stopPropagation();
|
|
2201
|
-
onOpenDetails(rule.id);
|
|
2202
|
-
},
|
|
2203
|
-
className: "inline-flex size-[22px] shrink-0 cursor-pointer items-center justify-center rounded-[3px] border-0 bg-transparent text-dhis2-grey-600 hover:bg-dhis2-grey-200 hover:text-dhis2-blue-600",
|
|
2204
|
-
children: /* @__PURE__ */ jsx(IconInfo16, {})
|
|
2205
|
-
}
|
|
2206
|
-
)
|
|
2207
|
-
] })
|
|
2208
|
-
] }),
|
|
2209
|
-
rule.programRuleActions.length ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-[6px]", children: rule.programRuleActions.map((action, index) => {
|
|
2210
|
-
const summary = formatRuleActionSummary(
|
|
2211
|
-
action,
|
|
2212
|
-
labelLookup
|
|
2213
|
-
);
|
|
2214
|
-
return /* @__PURE__ */ jsx(
|
|
2215
|
-
EffectBadge,
|
|
2216
|
-
{
|
|
2217
|
-
type: summary.type,
|
|
2218
|
-
children: formatActionLabel(summary)
|
|
2219
|
-
},
|
|
2220
|
-
`${rule.id}-action-${String(index)}`
|
|
2221
|
-
);
|
|
2222
|
-
}) }) : null,
|
|
2223
|
-
showConditions && rule.condition ? /* @__PURE__ */ jsx(
|
|
2224
|
-
"p",
|
|
2225
|
-
{
|
|
2226
|
-
className: "m-0 break-words font-mono text-[11px] leading-[1.5] text-dhis2-grey-700",
|
|
2227
|
-
title: rule.condition,
|
|
2228
|
-
children: rule.condition
|
|
2229
|
-
}
|
|
2230
|
-
) : null,
|
|
2231
|
-
!inScope && rule.programStageId ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-dhis2-grey-600", children: translate("Applies to {{stage}}", {
|
|
2232
|
-
stage: labelLookup.resolveStageName(rule.programStageId)
|
|
2233
|
-
}) }) : null
|
|
2234
|
-
]
|
|
2235
|
-
}
|
|
2236
|
-
) }, rule.id);
|
|
2237
|
-
}) });
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
export { EFFECT_ICONS, RuleDetailsModal, RuleDevtoolsScope, RulesPanel, createLabelLookup, getEffectEdgeStroke, getEffectShortLabel, getEffectTagRenderProps, getEffectTagRenderPropsForVariant, getEffectVariant, getEffectVisual };
|
|
1
|
+
export { RulesPanel } from './RulesPanel.js';
|
|
2
|
+
export { RuleDetailsModal } from './RuleDetailsModal.js';
|
|
3
|
+
export { RuleDevtoolsScope } from './RuleDevtoolsScope.js';
|
|
4
|
+
export { EFFECT_ICONS, getEffectEdgeStroke, getEffectShortLabel, getEffectTagRenderProps, getEffectTagRenderPropsForVariant, getEffectVariant, getEffectVisual } from './effectStyles.js';
|
|
5
|
+
export { createLabelLookup } from './createLabelLookup.js';
|
|
2241
6
|
//# sourceMappingURL=index.js.map
|
|
2242
7
|
//# sourceMappingURL=index.js.map
|