@nnkogift/dhis2-form-utils-devtools 0.1.0-alpha.0

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/index.cjs ADDED
@@ -0,0 +1,1663 @@
1
+ 'use strict';
2
+
3
+ var ui = require('@dhis2/ui');
4
+ var dhis2FormUtilsHooks = require('@nnkogift/dhis2-form-utils-hooks');
5
+ var react = require('react');
6
+ var dhis2FormUtilsMetadata = require('@nnkogift/dhis2-form-utils-metadata');
7
+ var i18n = require('@dhis2/d2-i18n');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var react$1 = require('@xyflow/react');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var i18n__default = /*#__PURE__*/_interopDefault(i18n);
14
+
15
+ // src/RulesPanel.tsx
16
+
17
+ // src/buildGraph.ts
18
+ var nodeKey = (kind, id) => `${kind}:${id}`;
19
+ var edgeKey = (source, ruleId, target, effectType) => `${source}|${ruleId}|${target}|${effectType ?? ""}`;
20
+ function accumulateGraph(previous, entry, labelLookup) {
21
+ const nodes = new Map(previous.nodes.map((node) => [node.id, node]));
22
+ const edges = new Map(previous.edges.map((edge) => [edge.id, edge]));
23
+ const ensureNode = (kind, id, label) => {
24
+ const key = nodeKey(kind, id);
25
+ if (!nodes.has(key)) {
26
+ nodes.set(key, { id: key, kind, label });
27
+ }
28
+ return key;
29
+ };
30
+ for (const fieldId of entry.changedFields) {
31
+ ensureNode("field", fieldId, labelLookup?.resolveFieldName(fieldId) ?? fieldId);
32
+ }
33
+ for (const ruleResult of entry.ruleResults) {
34
+ const ruleLabel = labelLookup?.resolveRuleName(ruleResult.ruleId) ?? ruleResult.ruleId;
35
+ const ruleNodeId = ensureNode("rule", ruleResult.ruleId, ruleLabel);
36
+ for (const fieldId of entry.changedFields) {
37
+ const fieldNodeId = ensureNode(
38
+ "field",
39
+ fieldId,
40
+ labelLookup?.resolveFieldName(fieldId) ?? fieldId
41
+ );
42
+ const id = edgeKey(fieldNodeId, ruleResult.ruleId, ruleNodeId, "read");
43
+ const existing = edges.get(id);
44
+ edges.set(id, {
45
+ id,
46
+ source: fieldNodeId,
47
+ target: ruleNodeId,
48
+ effectType: "read",
49
+ fireCount: (existing?.fireCount ?? 0) + 1
50
+ });
51
+ }
52
+ for (const effect of ruleResult.effects) {
53
+ const targetKind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
54
+ const targetNodeId = ensureNode(
55
+ targetKind,
56
+ effect.targetId,
57
+ targetKind === "section" ? labelLookup?.resolveSectionName(effect.targetId) ?? effect.targetId : targetKind === "field" ? labelLookup?.resolveFieldName(effect.targetId) ?? effect.targetId : effect.targetId
58
+ );
59
+ const id = edgeKey(ruleNodeId, ruleResult.ruleId, targetNodeId, effect.type);
60
+ const existing = edges.get(id);
61
+ edges.set(id, {
62
+ id,
63
+ source: ruleNodeId,
64
+ target: targetNodeId,
65
+ effectType: effect.type,
66
+ fireCount: (existing?.fireCount ?? 0) + 1
67
+ });
68
+ }
69
+ }
70
+ return {
71
+ nodes: [...nodes.values()],
72
+ edges: [...edges.values()]
73
+ };
74
+ }
75
+ function buildGraphFromTrace(entries, labelLookup) {
76
+ return entries.reduce(
77
+ (graph, entry) => accumulateGraph(graph, entry, labelLookup),
78
+ { nodes: [], edges: [] }
79
+ );
80
+ }
81
+ var createLookup = (rules, fields, sections, stages) => ({
82
+ resolveRuleName: (id) => rules.get(id) ?? id,
83
+ resolveFieldName: (id) => fields.get(id) ?? id,
84
+ resolveSectionName: (id) => sections.get(id) ?? id,
85
+ resolveStageName: (id) => stages.get(id) ?? id
86
+ });
87
+ function buildEventLookup(source) {
88
+ const rules = /* @__PURE__ */ new Map();
89
+ const fields = /* @__PURE__ */ new Map();
90
+ const sections = /* @__PURE__ */ new Map();
91
+ const stages = /* @__PURE__ */ new Map();
92
+ for (const rule of source.metadata.programRules) {
93
+ if (!rule.id) {
94
+ continue;
95
+ }
96
+ rules.set(rule.id, rule.displayName ?? rule.id);
97
+ }
98
+ const stage = dhis2FormUtilsMetadata.selectProgramStage(
99
+ source.metadata,
100
+ source.programStageId
101
+ ) ?? {
102
+ id: source.programStageId,
103
+ programStageDataElements: [],
104
+ programStageSections: []
105
+ };
106
+ for (const psde of stage.programStageDataElements ?? []) {
107
+ const de = psde.dataElement;
108
+ if (!de?.id) {
109
+ continue;
110
+ }
111
+ fields.set(de.id, de.displayFormName ?? de.displayName ?? de.id);
112
+ }
113
+ for (const section of stage.programStageSections ?? []) {
114
+ sections.set(section.id, section.displayName ?? section.id);
115
+ }
116
+ for (const programStage of source.metadata.programStages) {
117
+ if (!programStage.id) {
118
+ continue;
119
+ }
120
+ stages.set(programStage.id, programStage.displayName ?? programStage.id);
121
+ }
122
+ return createLookup(rules, fields, sections, stages);
123
+ }
124
+ function buildTrackerLookup(source) {
125
+ const rules = /* @__PURE__ */ new Map();
126
+ const fields = /* @__PURE__ */ new Map();
127
+ const sections = /* @__PURE__ */ new Map();
128
+ const stages = /* @__PURE__ */ new Map();
129
+ for (const rule of source.metadata.programRules) {
130
+ if (!rule.id) {
131
+ continue;
132
+ }
133
+ rules.set(rule.id, rule.name ?? rule.id);
134
+ }
135
+ for (const ptea of source.metadata.programTrackedEntityAttributes) {
136
+ const tea = ptea.trackedEntityAttribute;
137
+ if (!tea.id) {
138
+ continue;
139
+ }
140
+ fields.set(tea.id, tea.formName ?? tea.displayName ?? tea.id);
141
+ }
142
+ for (const section of source.metadata.programSections ?? []) {
143
+ sections.set(section.id, section.displayName ?? section.id);
144
+ }
145
+ for (const programStage of source.programStages ?? []) {
146
+ if (!programStage.id) {
147
+ continue;
148
+ }
149
+ stages.set(programStage.id, programStage.displayName ?? programStage.id);
150
+ }
151
+ return createLookup(rules, fields, sections, stages);
152
+ }
153
+ function createLabelLookup(source) {
154
+ return source.formKind === "event" ? buildEventLookup(source) : buildTrackerLookup(source);
155
+ }
156
+ var EFFECT_VARIANT = {
157
+ HIDEFIELD: "hide",
158
+ HIDEOPTION: "hide",
159
+ HIDEOPTIONGROUP: "hide",
160
+ HIDESECTION: "hide",
161
+ HIDEPROGRAMSTAGE: "hide",
162
+ SHOWFIELD: "show",
163
+ SHOWOPTION: "show",
164
+ SHOWOPTIONGROUP: "show",
165
+ ASSIGN: "assign",
166
+ SETMANDATORYFIELD: "mandatory",
167
+ UNSETMANDATORYFIELD: "mandatory",
168
+ SHOWWARNING: "warning",
169
+ WARNINGONCOMPLETE: "warning",
170
+ SHOWERROR: "error",
171
+ ERRORONCOMPLETE: "error",
172
+ DISPLAYTEXT: "feedback",
173
+ DISPLAYKEYVALUEPAIR: "feedback",
174
+ SENDMESSAGE: "message",
175
+ SCHEDULEMESSAGE: "message",
176
+ read: "read"
177
+ };
178
+ var EFFECT_VISUALS = {
179
+ hide: {
180
+ tagClassName: "bg-dhis2-grey-200 text-dhis2-grey-900",
181
+ edgeStroke: "#494949",
182
+ shortLabel: "hide"
183
+ },
184
+ show: {
185
+ tagClassName: "bg-dhis2-green-100 text-dhis2-green-900",
186
+ edgeStroke: "#388e3c",
187
+ shortLabel: "show"
188
+ },
189
+ assign: {
190
+ tagClassName: "bg-dhis2-blue-100 text-dhis2-blue-900",
191
+ edgeStroke: "#1565c0",
192
+ shortLabel: "assign"
193
+ },
194
+ mandatory: {
195
+ tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
196
+ edgeStroke: "#e56408",
197
+ shortLabel: "required"
198
+ },
199
+ warning: {
200
+ tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
201
+ edgeStroke: "#ff8302",
202
+ shortLabel: "warn"
203
+ },
204
+ error: {
205
+ tagClassName: "bg-dhis2-red-100 text-dhis2-red-900",
206
+ edgeStroke: "#c62828",
207
+ shortLabel: "error"
208
+ },
209
+ feedback: {
210
+ tagClassName: "bg-dhis2-purple-100 text-dhis2-purple-900",
211
+ edgeStroke: "#9c27b0",
212
+ shortLabel: "feedback"
213
+ },
214
+ message: {
215
+ tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
216
+ edgeStroke: "#00796b",
217
+ shortLabel: "message"
218
+ },
219
+ read: {
220
+ tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
221
+ edgeStroke: "#00796b",
222
+ shortLabel: "read"
223
+ },
224
+ default: {
225
+ tagClassName: "bg-dhis2-grey-100 text-dhis2-grey-800",
226
+ edgeStroke: "#494949",
227
+ shortLabel: "effect"
228
+ }
229
+ };
230
+ var EFFECT_ICONS = {
231
+ hide: ui.IconViewOff16,
232
+ show: ui.IconView16,
233
+ assign: ui.IconEdit16,
234
+ mandatory: ui.IconStarFilled16,
235
+ warning: ui.IconWarningFilled16,
236
+ error: ui.IconErrorFilled16,
237
+ feedback: ui.IconMessages16,
238
+ message: ui.IconMail16,
239
+ read: ui.IconLink16,
240
+ default: ui.IconInfo16
241
+ };
242
+ function getEffectVariant(type) {
243
+ return EFFECT_VARIANT[type] ?? "default";
244
+ }
245
+ function getEffectVisual(type) {
246
+ const variant = getEffectVariant(type);
247
+ return { variant, ...EFFECT_VISUALS[variant] };
248
+ }
249
+ var TAG_LAYOUT_CLASS = "max-w-full break-words";
250
+ function tagPropsForVariant(variant) {
251
+ switch (variant) {
252
+ case "show":
253
+ return { positive: true, className: TAG_LAYOUT_CLASS };
254
+ case "error":
255
+ return { negative: true, className: TAG_LAYOUT_CLASS };
256
+ case "assign":
257
+ return { neutral: true, className: TAG_LAYOUT_CLASS };
258
+ case "hide":
259
+ case "mandatory":
260
+ case "warning":
261
+ case "feedback":
262
+ case "message":
263
+ case "read":
264
+ return {
265
+ className: `${TAG_LAYOUT_CLASS} ${EFFECT_VISUALS[variant].tagClassName}`
266
+ };
267
+ default:
268
+ return {
269
+ className: `${TAG_LAYOUT_CLASS} ${EFFECT_VISUALS.default.tagClassName}`
270
+ };
271
+ }
272
+ }
273
+ function getEffectTagRenderProps(type) {
274
+ return tagPropsForVariant(getEffectVariant(type));
275
+ }
276
+ function getEffectTagRenderPropsForVariant(variant) {
277
+ return tagPropsForVariant(variant);
278
+ }
279
+ function getEffectEdgeStroke(type, highlighted = true) {
280
+ const { edgeStroke } = getEffectVisual(type);
281
+ if (highlighted) {
282
+ return edgeStroke;
283
+ }
284
+ return "#bdbdbd";
285
+ }
286
+ function getEffectShortLabel(type) {
287
+ return getEffectVisual(type).shortLabel;
288
+ }
289
+ function translate(message, variables) {
290
+ return String(i18n__default.default.t(message, variables));
291
+ }
292
+ function EffectBadge({ type, children, className = "" }) {
293
+ const variant = getEffectVariant(type);
294
+ const Icon = EFFECT_ICONS[variant];
295
+ const tagProps = getEffectTagRenderProps(type);
296
+ const label = children ?? type;
297
+ return /* @__PURE__ */ jsxRuntime.jsx(
298
+ ui.Tag,
299
+ {
300
+ ...tagProps,
301
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Icon, { "aria-hidden": "true" }),
302
+ className: [tagProps.className, className].filter(Boolean).join(" "),
303
+ maxWidth: "100%",
304
+ children: label
305
+ }
306
+ );
307
+ }
308
+ var LEGEND_VARIANTS = [
309
+ { variant: "read", label: translate("Read") },
310
+ { variant: "hide", label: translate("Hide") },
311
+ { variant: "show", label: translate("Show") },
312
+ { variant: "assign", label: translate("Assign") },
313
+ { variant: "mandatory", label: translate("Required") },
314
+ { variant: "warning", label: translate("Warning") },
315
+ { variant: "error", label: translate("Error") },
316
+ { variant: "feedback", label: translate("Feedback") }
317
+ ];
318
+ function EffectLegend({ activeVariants }) {
319
+ const items = LEGEND_VARIANTS.filter((item) => activeVariants.has(item.variant));
320
+ if (!items.length) {
321
+ return null;
322
+ }
323
+ return /* @__PURE__ */ jsxRuntime.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) => {
324
+ const Icon = EFFECT_ICONS[item.variant];
325
+ const tagProps = getEffectTagRenderPropsForVariant(item.variant);
326
+ return /* @__PURE__ */ jsxRuntime.jsx(
327
+ ui.Tag,
328
+ {
329
+ ...tagProps,
330
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Icon, { "aria-hidden": "true" }),
331
+ maxWidth: "100%",
332
+ children: item.label
333
+ },
334
+ item.variant
335
+ );
336
+ }) });
337
+ }
338
+
339
+ // src/formatAgo.ts
340
+ function formatAgo(timestamp, now = Date.now()) {
341
+ const diffMs = Math.max(0, now - timestamp);
342
+ if (diffMs < 5e3) {
343
+ return translate("just now");
344
+ }
345
+ const seconds = Math.floor(diffMs / 1e3);
346
+ if (seconds < 60) {
347
+ return translate("{{seconds}}s ago", { seconds });
348
+ }
349
+ const minutes = Math.floor(seconds / 60);
350
+ if (minutes < 60) {
351
+ return translate("{{minutes}}m ago", { minutes });
352
+ }
353
+ const hours = Math.floor(minutes / 60);
354
+ if (hours < 24) {
355
+ return translate("{{hours}}h ago", { hours });
356
+ }
357
+ return new Date(timestamp).toLocaleString();
358
+ }
359
+
360
+ // src/formatRuleActionSummary.ts
361
+ function resolveFieldLabel(id, displayName, labelLookup) {
362
+ if (labelLookup) {
363
+ const resolved = labelLookup.resolveFieldName(id);
364
+ if (resolved !== id) {
365
+ return resolved;
366
+ }
367
+ }
368
+ return displayName ?? id;
369
+ }
370
+ function resolveSectionLabel(id, displayName, labelLookup) {
371
+ if (labelLookup) {
372
+ const resolved = labelLookup.resolveSectionName(id);
373
+ if (resolved !== id) {
374
+ return resolved;
375
+ }
376
+ }
377
+ return displayName ?? id;
378
+ }
379
+ function formatRuleActionSummary(action, labelLookup) {
380
+ const type = action.programRuleActionType ?? "UNKNOWN";
381
+ let targetLabel = "";
382
+ if (action.dataElement?.id) {
383
+ targetLabel = resolveFieldLabel(
384
+ action.dataElement.id,
385
+ action.dataElement.displayName,
386
+ labelLookup
387
+ );
388
+ } else if (action.trackedEntityAttribute?.id) {
389
+ targetLabel = resolveFieldLabel(
390
+ action.trackedEntityAttribute.id,
391
+ action.trackedEntityAttribute.displayName,
392
+ labelLookup
393
+ );
394
+ } else if (action.programStageSection?.id) {
395
+ targetLabel = resolveSectionLabel(
396
+ action.programStageSection.id,
397
+ action.programStageSection.displayName,
398
+ labelLookup
399
+ );
400
+ } else if (action.programSection?.id) {
401
+ targetLabel = resolveSectionLabel(
402
+ action.programSection.id,
403
+ action.programSection.displayName,
404
+ labelLookup
405
+ );
406
+ } else if (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR") {
407
+ targetLabel = action.location ?? "feedback";
408
+ }
409
+ let detail;
410
+ if (action.data) {
411
+ detail = action.data;
412
+ }
413
+ if (action.content && (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR")) {
414
+ detail = action.content;
415
+ }
416
+ return { type, targetLabel, detail };
417
+ }
418
+
419
+ // src/resolveProgramRulesList.ts
420
+ function sortByPriority(rules) {
421
+ return [...rules].sort((left, right) => (left.priority ?? 0) - (right.priority ?? 0));
422
+ }
423
+ function toCatalogRule(rule) {
424
+ return {
425
+ id: rule.id,
426
+ name: rule.name,
427
+ condition: rule.condition,
428
+ priority: rule.priority,
429
+ programRuleActions: [...rule.programRuleActions ?? []],
430
+ programStageId: rule.programStageId
431
+ };
432
+ }
433
+ function resolveProgramRulesList(metadata) {
434
+ if (metadata.formKind === "event") {
435
+ return sortByPriority(
436
+ metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
437
+ (rule) => toCatalogRule({
438
+ id: rule.id,
439
+ name: rule.displayName ?? rule.id,
440
+ condition: rule.condition,
441
+ priority: rule.priority,
442
+ programRuleActions: rule.programRuleActions,
443
+ programStageId: rule.programStage?.id ?? null
444
+ })
445
+ )
446
+ );
447
+ }
448
+ return sortByPriority(
449
+ metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
450
+ (rule) => toCatalogRule({
451
+ id: rule.id,
452
+ name: rule.name ?? rule.id,
453
+ condition: rule.condition,
454
+ priority: rule.priority,
455
+ programRuleActions: rule.programRuleActions,
456
+ programStageId: rule.programStage?.id ?? null
457
+ })
458
+ )
459
+ );
460
+ }
461
+
462
+ // src/constants.ts
463
+ var DEFAULT_TRACE_MAX_ENTRIES = 200;
464
+
465
+ // src/traceStore.ts
466
+ function createRuleTraceStore(maxEntries = DEFAULT_TRACE_MAX_ENTRIES) {
467
+ let entries = [];
468
+ const listeners = /* @__PURE__ */ new Set();
469
+ let disposeTraceSubscription = null;
470
+ const notify = () => {
471
+ for (const listener of listeners) {
472
+ listener();
473
+ }
474
+ };
475
+ return {
476
+ record(entry) {
477
+ entries = [...entries, entry].slice(-maxEntries);
478
+ notify();
479
+ },
480
+ getSnapshot() {
481
+ return entries;
482
+ },
483
+ subscribe(listener) {
484
+ listeners.add(listener);
485
+ return () => {
486
+ listeners.delete(listener);
487
+ };
488
+ },
489
+ dispose() {
490
+ disposeTraceSubscription?.();
491
+ disposeTraceSubscription = null;
492
+ listeners.clear();
493
+ },
494
+ bindTraceSubscription(unsubscribe) {
495
+ disposeTraceSubscription = unsubscribe;
496
+ }
497
+ };
498
+ }
499
+
500
+ // src/attach.ts
501
+ function attachRuleDevtools(formStore, options) {
502
+ const store = createRuleTraceStore(
503
+ DEFAULT_TRACE_MAX_ENTRIES
504
+ );
505
+ store.bindTraceSubscription(formStore.subscribeTrace(store.record));
506
+ return store;
507
+ }
508
+ var RuleTraceContext = react.createContext(null);
509
+ function RuleDevtoolsScope({ formStore, children }) {
510
+ const traceStore = react.useMemo(() => attachRuleDevtools(formStore), [formStore]);
511
+ react.useEffect(() => {
512
+ return () => {
513
+ traceStore.dispose();
514
+ };
515
+ }, [traceStore]);
516
+ return /* @__PURE__ */ jsxRuntime.jsx(RuleTraceContext.Provider, { value: traceStore, children });
517
+ }
518
+ function useRuleTraceStore() {
519
+ const store = react.useContext(RuleTraceContext);
520
+ if (!store) {
521
+ throw new Error("useRuleTraceStore must be used within RuleDevtoolsScope");
522
+ }
523
+ return store;
524
+ }
525
+
526
+ // src/graphLayout.ts
527
+ var ROLE_COLUMNS = {
528
+ source: 0,
529
+ rule: 280,
530
+ target: 560
531
+ };
532
+ var FEEDBACK_COLUMN = 720;
533
+ var LANE_HEIGHT = 104;
534
+ var STAGGER = 24;
535
+ function median(values) {
536
+ const sorted = [...values].sort((a, b) => a - b);
537
+ const mid = Math.floor(sorted.length / 2);
538
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
539
+ }
540
+ function staggerOffset(indexInBucket) {
541
+ if (indexInBucket === 0) {
542
+ return 0;
543
+ }
544
+ const magnitude = Math.ceil(indexInBucket / 2) * STAGGER;
545
+ return indexInBucket % 2 === 1 ? magnitude : -magnitude;
546
+ }
547
+ function columnFor(node) {
548
+ if (node.role === "target" && node.kind === "feedback") {
549
+ return FEEDBACK_COLUMN;
550
+ }
551
+ return ROLE_COLUMNS[node.role];
552
+ }
553
+ function prepareFlowGraph(graph) {
554
+ const readSources = /* @__PURE__ */ new Set();
555
+ const effectTargets = /* @__PURE__ */ new Set();
556
+ for (const edge of graph.edges) {
557
+ if (edge.effectType === "read") {
558
+ readSources.add(edge.source);
559
+ } else {
560
+ effectTargets.add(edge.target);
561
+ }
562
+ }
563
+ const flowNodes = [];
564
+ const endpointByGraphId = /* @__PURE__ */ new Map();
565
+ for (const node of graph.nodes) {
566
+ if (node.kind === "rule") {
567
+ flowNodes.push({
568
+ id: node.id,
569
+ graphNodeId: node.id,
570
+ kind: node.kind,
571
+ label: node.label,
572
+ role: "rule"
573
+ });
574
+ continue;
575
+ }
576
+ const isSource = readSources.has(node.id);
577
+ const isTarget = effectTargets.has(node.id);
578
+ const endpoints = {};
579
+ if (isSource) {
580
+ const flowId = isTarget ? `${node.id}@src` : node.id;
581
+ endpoints.source = flowId;
582
+ flowNodes.push({
583
+ id: flowId,
584
+ graphNodeId: node.id,
585
+ kind: node.kind,
586
+ label: node.label,
587
+ role: "source"
588
+ });
589
+ }
590
+ if (isTarget) {
591
+ const flowId = isSource ? `${node.id}@tgt` : node.id;
592
+ endpoints.target = flowId;
593
+ flowNodes.push({
594
+ id: flowId,
595
+ graphNodeId: node.id,
596
+ kind: node.kind,
597
+ label: node.label,
598
+ role: "target"
599
+ });
600
+ }
601
+ endpointByGraphId.set(node.id, endpoints);
602
+ }
603
+ const flowEdges = graph.edges.map((edge) => {
604
+ if (edge.effectType === "read") {
605
+ const endpoints2 = endpointByGraphId.get(edge.source);
606
+ return { ...edge, source: endpoints2?.source ?? edge.source };
607
+ }
608
+ const endpoints = endpointByGraphId.get(edge.target);
609
+ return { ...edge, target: endpoints?.target ?? edge.target };
610
+ });
611
+ return { nodes: flowNodes, edges: flowEdges };
612
+ }
613
+ function computeGraphLayout(flowNodes, flowEdges) {
614
+ const positions = /* @__PURE__ */ new Map();
615
+ const ruleLane = /* @__PURE__ */ new Map();
616
+ flowNodes.filter((node) => node.role === "rule").forEach((node, index) => {
617
+ ruleLane.set(node.id, index);
618
+ positions.set(node.id, { x: ROLE_COLUMNS.rule, y: index * LANE_HEIGHT });
619
+ });
620
+ const connectedRuleLanes = /* @__PURE__ */ new Map();
621
+ const addLane = (nodeId, lane) => {
622
+ const lanes = connectedRuleLanes.get(nodeId) ?? /* @__PURE__ */ new Set();
623
+ lanes.add(lane);
624
+ connectedRuleLanes.set(nodeId, lanes);
625
+ };
626
+ for (const edge of flowEdges) {
627
+ const sourceLane = ruleLane.get(edge.source);
628
+ const targetLane = ruleLane.get(edge.target);
629
+ if (targetLane !== void 0 && sourceLane === void 0) {
630
+ addLane(edge.source, targetLane);
631
+ }
632
+ if (sourceLane !== void 0 && targetLane === void 0) {
633
+ addLane(edge.target, sourceLane);
634
+ }
635
+ }
636
+ const bucketCounts = /* @__PURE__ */ new Map();
637
+ for (const node of flowNodes) {
638
+ if (node.role === "rule") {
639
+ continue;
640
+ }
641
+ const lanes = connectedRuleLanes.get(node.id);
642
+ const lane = lanes && lanes.size > 0 ? median([...lanes]) : 0;
643
+ const column = columnFor(node);
644
+ const bucketKey = `${String(column)}:${String(lane)}`;
645
+ const indexInBucket = bucketCounts.get(bucketKey) ?? 0;
646
+ bucketCounts.set(bucketKey, indexInBucket + 1);
647
+ positions.set(node.id, {
648
+ x: column,
649
+ y: lane * LANE_HEIGHT + staggerOffset(indexInBucket)
650
+ });
651
+ }
652
+ return positions;
653
+ }
654
+
655
+ // src/graphNodeStyles.ts
656
+ var GRAPH_NODE_CLASSES = {
657
+ field: "border-dhis2-blue-500",
658
+ rule: "border-dhis2-teal-600 bg-dhis2-teal-050",
659
+ section: "border-dhis2-grey-600",
660
+ feedback: "border-dhis2-purple-500"
661
+ };
662
+ var LEGEND_SWATCH_CLASSES = {
663
+ field: "bg-dhis2-blue-500",
664
+ rule: "bg-dhis2-teal-600",
665
+ section: "bg-dhis2-grey-600",
666
+ feedback: "bg-dhis2-purple-500"
667
+ };
668
+ function getGraphNodeClassName(kind, highlighted) {
669
+ 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"}`;
670
+ }
671
+ function getLegendSwatchClassName(kind) {
672
+ return `size-2.5 shrink-0 rounded-sm ${LEGEND_SWATCH_CLASSES[kind]}`;
673
+ }
674
+
675
+ // src/traceEntry.ts
676
+ function resolveGraphTraceEntry(entries, selectedEntryId) {
677
+ if (!entries.length) {
678
+ return null;
679
+ }
680
+ if (selectedEntryId) {
681
+ return entries.find((entry) => entry.id === selectedEntryId) ?? entries[entries.length - 1];
682
+ }
683
+ return entries[entries.length - 1];
684
+ }
685
+ function getActiveRuleIds(entries) {
686
+ const latest = resolveGraphTraceEntry(entries);
687
+ if (!latest) {
688
+ return /* @__PURE__ */ new Set();
689
+ }
690
+ return new Set(latest.ruleResults.map((result) => result.ruleId));
691
+ }
692
+ var KIND_LABELS = {
693
+ field: "Field",
694
+ rule: "Rule",
695
+ section: "Section",
696
+ feedback: "Feedback"
697
+ };
698
+ var READ_LABEL_EDGE_THRESHOLD = 4;
699
+ var DEFAULT_EDGE_OPTIONS = {
700
+ type: "ruleGraphEdge",
701
+ style: { stroke: getEffectEdgeStroke("default") },
702
+ markerEnd: {
703
+ type: react$1.MarkerType.ArrowClosed,
704
+ color: getEffectEdgeStroke("default")
705
+ }
706
+ };
707
+ function RuleGraphNode({ data }) {
708
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: getGraphNodeClassName(data.kind, data.highlighted), children: [
709
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left }),
710
+ /* @__PURE__ */ jsxRuntime.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] }),
711
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "break-words font-semibold leading-[1.3]", children: data.label }),
712
+ data.value !== void 0 ? /* @__PURE__ */ jsxRuntime.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,
713
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right })
714
+ ] });
715
+ }
716
+ var nodeTypes = {
717
+ ruleGraphNode: react.memo(RuleGraphNode)
718
+ };
719
+ function RuleGraphEdge({
720
+ id,
721
+ sourceX,
722
+ sourceY,
723
+ sourcePosition,
724
+ targetX,
725
+ targetY,
726
+ targetPosition,
727
+ markerEnd,
728
+ style,
729
+ data
730
+ }) {
731
+ const [hovered, setHovered] = react.useState(false);
732
+ const [edgePath, labelX, labelY] = react$1.getSmoothStepPath({
733
+ sourceX,
734
+ sourceY,
735
+ sourcePosition,
736
+ targetX,
737
+ targetY,
738
+ targetPosition,
739
+ offset: data?.offset ?? 20
740
+ });
741
+ const showLabel = data ? data.showLabel || hovered : false;
742
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
743
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.BaseEdge, { id, path: edgePath, markerEnd, style }),
744
+ /* @__PURE__ */ jsxRuntime.jsx(
745
+ "path",
746
+ {
747
+ d: edgePath,
748
+ fill: "none",
749
+ stroke: "transparent",
750
+ strokeWidth: 16,
751
+ onMouseEnter: () => {
752
+ setHovered(true);
753
+ },
754
+ onMouseLeave: () => {
755
+ setHovered(false);
756
+ }
757
+ }
758
+ ),
759
+ showLabel && data ? /* @__PURE__ */ jsxRuntime.jsx(react$1.EdgeLabelRenderer, { children: /* @__PURE__ */ jsxRuntime.jsx(
760
+ "div",
761
+ {
762
+ className: "nodrag nopan absolute rounded-sm px-dp4 text-[10px] font-semibold",
763
+ style: {
764
+ transform: `translate(-50%, -50%) translate(${String(labelX)}px, ${String(labelY)}px)`,
765
+ background: "rgba(255,255,255,0.92)",
766
+ color: data.stroke,
767
+ pointerEvents: "all"
768
+ },
769
+ onMouseEnter: () => {
770
+ setHovered(true);
771
+ },
772
+ onMouseLeave: () => {
773
+ setHovered(false);
774
+ },
775
+ children: data.label
776
+ }
777
+ ) }) : null
778
+ ] });
779
+ }
780
+ var edgeTypes = {
781
+ ruleGraphEdge: react.memo(RuleGraphEdge)
782
+ };
783
+ function effectNodeKey(effect) {
784
+ const kind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
785
+ return `${kind}:${effect.targetId}`;
786
+ }
787
+ function entryGraphKeys(entry) {
788
+ const keys = /* @__PURE__ */ new Set();
789
+ for (const fieldId of entry.changedFields) {
790
+ keys.add(`field:${fieldId}`);
791
+ }
792
+ for (const result of entry.ruleResults) {
793
+ keys.add(`rule:${result.ruleId}`);
794
+ for (const effect of result.effects) {
795
+ keys.add(effectNodeKey(effect));
796
+ }
797
+ }
798
+ return keys;
799
+ }
800
+ function ruleGraphKeys(entry, ruleId) {
801
+ const keys = /* @__PURE__ */ new Set([`rule:${ruleId}`]);
802
+ for (const result of entry.ruleResults) {
803
+ if (result.ruleId !== ruleId) {
804
+ continue;
805
+ }
806
+ for (const effect of result.effects) {
807
+ keys.add(effectNodeKey(effect));
808
+ }
809
+ }
810
+ return keys;
811
+ }
812
+ var formatDisplayValue = (value) => {
813
+ if (value === void 0 || value === null || value === "") {
814
+ return void 0;
815
+ }
816
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
817
+ return String(value);
818
+ }
819
+ return JSON.stringify(value);
820
+ };
821
+ function toFlowGraph(graph, fieldState, formValues, highlightedKeys) {
822
+ const { nodes: flowNodes, edges: flowEdges } = prepareFlowGraph(graph);
823
+ const positions = computeGraphLayout(flowNodes, flowEdges);
824
+ const graphNodeIdByFlowId = new Map(flowNodes.map((node) => [node.id, node.graphNodeId]));
825
+ const nodes = flowNodes.map((node) => {
826
+ const position = positions.get(node.id) ?? { x: 0, y: 0 };
827
+ const rawFieldId = node.kind === "field" ? node.graphNodeId.slice("field:".length) : void 0;
828
+ const assignedValue = rawFieldId && rawFieldId in fieldState ? fieldState[rawFieldId].assignedValue : void 0;
829
+ const formValue = rawFieldId ? formValues[rawFieldId] : void 0;
830
+ const displayValue = formatDisplayValue(assignedValue) ?? formatDisplayValue(formValue);
831
+ return {
832
+ id: node.id,
833
+ type: "ruleGraphNode",
834
+ position,
835
+ data: {
836
+ label: node.label,
837
+ kind: node.kind,
838
+ graphNodeId: node.graphNodeId,
839
+ value: displayValue,
840
+ highlighted: highlightedKeys ? highlightedKeys.has(node.graphNodeId) : true
841
+ }
842
+ };
843
+ });
844
+ const dense = flowEdges.length > READ_LABEL_EDGE_THRESHOLD;
845
+ const sourceFanOut = /* @__PURE__ */ new Map();
846
+ const edges = flowEdges.map((edge) => {
847
+ const sourceGraphId = graphNodeIdByFlowId.get(edge.source) ?? edge.source;
848
+ const targetGraphId = graphNodeIdByFlowId.get(edge.target) ?? edge.target;
849
+ const isHighlighted = highlightedKeys ? highlightedKeys.has(sourceGraphId) && highlightedKeys.has(targetGraphId) : true;
850
+ const effectType = edge.effectType ?? "default";
851
+ const isRead = effectType === "read";
852
+ const stroke = getEffectEdgeStroke(effectType, isHighlighted);
853
+ const label = getEffectShortLabel(effectType);
854
+ const showLabel = !(isRead && dense);
855
+ const fanIndex = sourceFanOut.get(edge.source) ?? 0;
856
+ sourceFanOut.set(edge.source, fanIndex + 1);
857
+ const offset = 20 + fanIndex * 14;
858
+ return {
859
+ id: edge.id,
860
+ source: edge.source,
861
+ target: edge.target,
862
+ type: "ruleGraphEdge",
863
+ animated: isHighlighted && !isRead,
864
+ zIndex: isHighlighted ? 1 : 0,
865
+ style: {
866
+ stroke,
867
+ strokeWidth: isRead ? 1 : Math.min(1 + edge.fireCount, 4),
868
+ opacity: isHighlighted ? isRead ? 0.5 : 1 : 0.35
869
+ },
870
+ markerEnd: {
871
+ type: react$1.MarkerType.ArrowClosed,
872
+ color: stroke
873
+ },
874
+ data: {
875
+ label,
876
+ stroke,
877
+ showLabel,
878
+ offset
879
+ }
880
+ };
881
+ });
882
+ return { nodes, edges };
883
+ }
884
+ function GraphToolbar({
885
+ nodeCount,
886
+ edgeCount,
887
+ headerActions,
888
+ activeEffectVariants
889
+ }) {
890
+ const items = [
891
+ { kind: "field", label: translate("Field") },
892
+ { kind: "rule", label: translate("Rule") },
893
+ { kind: "section", label: translate("Section") },
894
+ { kind: "feedback", label: translate("Feedback") }
895
+ ];
896
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white", children: [
897
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-dp8 px-dp12 py-dp8", children: [
898
+ /* @__PURE__ */ jsxRuntime.jsx(
899
+ "div",
900
+ {
901
+ className: "flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8",
902
+ "aria-hidden": "true",
903
+ children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
904
+ "span",
905
+ {
906
+ className: "inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800",
907
+ children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: getLegendSwatchClassName(item.kind) }),
909
+ item.label
910
+ ]
911
+ },
912
+ item.kind
913
+ ))
914
+ }
915
+ ),
916
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
917
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs tabular-nums text-dhis2-grey-600", children: translate("{{nodes}} nodes \xB7 {{edges}} edges", {
918
+ nodes: nodeCount,
919
+ edges: edgeCount
920
+ }) }),
921
+ headerActions
922
+ ] })
923
+ ] }),
924
+ /* @__PURE__ */ jsxRuntime.jsx(EffectLegend, { activeVariants: activeEffectVariants })
925
+ ] });
926
+ }
927
+ function FitViewOnChange({
928
+ layoutKey,
929
+ nodeCount,
930
+ edgeCount
931
+ }) {
932
+ const { fitView } = react$1.useReactFlow();
933
+ react.useEffect(() => {
934
+ const frameId = requestAnimationFrame(() => {
935
+ void fitView({ padding: 0.2, duration: 150 });
936
+ });
937
+ return () => {
938
+ cancelAnimationFrame(frameId);
939
+ };
940
+ }, [edgeCount, fitView, layoutKey, nodeCount]);
941
+ return null;
942
+ }
943
+ function mergeNodePositions(next, current) {
944
+ const currentById = new Map(current.map((node) => [node.id, node]));
945
+ return next.map((node) => {
946
+ const existing = currentById.get(node.id);
947
+ if (existing) {
948
+ return { ...node, position: existing.position };
949
+ }
950
+ return node;
951
+ });
952
+ }
953
+ function RuleGraphCanvas({
954
+ nodes: layoutNodes,
955
+ edges: layoutEdges,
956
+ layoutKey
957
+ }) {
958
+ const [nodes, setNodes, onNodesChange] = react$1.useNodesState(layoutNodes);
959
+ const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(layoutEdges);
960
+ react.useEffect(() => {
961
+ setNodes((current) => mergeNodePositions(layoutNodes, current));
962
+ }, [layoutNodes, setNodes]);
963
+ react.useEffect(() => {
964
+ setEdges(layoutEdges);
965
+ }, [layoutEdges, setEdges]);
966
+ return /* @__PURE__ */ jsxRuntime.jsxs(
967
+ react$1.ReactFlow,
968
+ {
969
+ nodes,
970
+ edges,
971
+ nodeTypes,
972
+ edgeTypes,
973
+ onNodesChange,
974
+ onEdgesChange,
975
+ nodesDraggable: true,
976
+ defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
977
+ elevateEdgesOnSelect: true,
978
+ fitView: true,
979
+ proOptions: { hideAttribution: true },
980
+ className: "h-full w-full",
981
+ children: [
982
+ /* @__PURE__ */ jsxRuntime.jsx(
983
+ FitViewOnChange,
984
+ {
985
+ layoutKey,
986
+ nodeCount: nodes.length,
987
+ edgeCount: edges.length
988
+ }
989
+ ),
990
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 16, size: 1 }),
991
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false })
992
+ ]
993
+ }
994
+ );
995
+ }
996
+ function RuleGraphView({
997
+ entries,
998
+ fieldState,
999
+ formValues,
1000
+ selectedEntryId,
1001
+ highlightRuleId,
1002
+ labelLookup,
1003
+ className,
1004
+ layoutKey,
1005
+ headerActions,
1006
+ minHeightClassName = "min-h-[360px]"
1007
+ }) {
1008
+ const graphEntry = react.useMemo(
1009
+ () => resolveGraphTraceEntry(entries, selectedEntryId),
1010
+ [entries, selectedEntryId]
1011
+ );
1012
+ const graph = react.useMemo(
1013
+ () => graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },
1014
+ [graphEntry, labelLookup]
1015
+ );
1016
+ const highlightedKeys = react.useMemo(() => {
1017
+ if (!graphEntry || !selectedEntryId && !highlightRuleId) {
1018
+ return null;
1019
+ }
1020
+ const keys = /* @__PURE__ */ new Set();
1021
+ if (selectedEntryId) {
1022
+ for (const key of entryGraphKeys(graphEntry)) {
1023
+ keys.add(key);
1024
+ }
1025
+ }
1026
+ if (highlightRuleId) {
1027
+ for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {
1028
+ keys.add(key);
1029
+ }
1030
+ }
1031
+ return keys;
1032
+ }, [graphEntry, highlightRuleId, selectedEntryId]);
1033
+ const { nodes, edges } = react.useMemo(
1034
+ () => toFlowGraph(graph, fieldState, formValues, highlightedKeys),
1035
+ [graph, fieldState, formValues, highlightedKeys]
1036
+ );
1037
+ const activeEffectVariants = react.useMemo(() => {
1038
+ const variants = /* @__PURE__ */ new Set();
1039
+ for (const edge of graph.edges) {
1040
+ if (edge.effectType) {
1041
+ variants.add(getEffectVariant(edge.effectType));
1042
+ }
1043
+ }
1044
+ return variants;
1045
+ }, [graph.edges]);
1046
+ if (!graph.nodes.length) {
1047
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.NoticeBox, { title: translate("No rule relationships yet"), children: translate(
1048
+ "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)."
1049
+ ) }) });
1050
+ }
1051
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1052
+ "div",
1053
+ {
1054
+ className: `flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ""}`,
1055
+ children: [
1056
+ /* @__PURE__ */ jsxRuntime.jsx(
1057
+ GraphToolbar,
1058
+ {
1059
+ nodeCount: graph.nodes.length,
1060
+ edgeCount: graph.edges.length,
1061
+ headerActions,
1062
+ activeEffectVariants
1063
+ }
1064
+ ),
1065
+ /* @__PURE__ */ jsxRuntime.jsx(
1066
+ "div",
1067
+ {
1068
+ className: `${minHeightClassName} flex-1 bg-white`,
1069
+ style: { width: "100%", height: "100%" },
1070
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(RuleGraphCanvas, { nodes, edges, layoutKey }) })
1071
+ }
1072
+ )
1073
+ ]
1074
+ }
1075
+ );
1076
+ }
1077
+ function RuleGraphModal({
1078
+ open,
1079
+ onClose,
1080
+ subtitle,
1081
+ layoutKey,
1082
+ ...graphProps
1083
+ }) {
1084
+ if (!open) {
1085
+ return null;
1086
+ }
1087
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Modal, { large: true, fluid: true, onClose, children: [
1088
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalTitle, { children: [
1089
+ translate("Rule dependency graph"),
1090
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-dp4 block text-sm font-normal text-dhis2-grey-700", children: subtitle }) : null
1091
+ ] }),
1092
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[min(85vh,900px)] w-full min-w-[min(1200px,92vw)]", children: /* @__PURE__ */ jsxRuntime.jsx(
1093
+ RuleGraphView,
1094
+ {
1095
+ ...graphProps,
1096
+ layoutKey: layoutKey ?? "modal",
1097
+ className: "h-full min-h-0 rounded-none border-0",
1098
+ minHeightClassName: "min-h-0 h-full"
1099
+ }
1100
+ ) }) })
1101
+ ] });
1102
+ }
1103
+ function resolveLabel(id, resolver) {
1104
+ const label = resolver?.(id) ?? id;
1105
+ return { label, showId: label !== id };
1106
+ }
1107
+ function resolveEffectTargetLabel(effect, labelLookup) {
1108
+ if (effect.type === "HIDESECTION") {
1109
+ return resolveLabel(
1110
+ effect.targetId,
1111
+ labelLookup ? (id) => labelLookup.resolveSectionName(id) : void 0
1112
+ );
1113
+ }
1114
+ if (effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR") {
1115
+ return { label: effect.targetId, showId: false };
1116
+ }
1117
+ return resolveLabel(
1118
+ effect.targetId,
1119
+ labelLookup ? (id) => labelLookup.resolveFieldName(id) : void 0
1120
+ );
1121
+ }
1122
+ function TraceTimeline({
1123
+ entries,
1124
+ selectedEntryId,
1125
+ highlightRuleId,
1126
+ onSelectEntry,
1127
+ onHighlightRule,
1128
+ labelLookup
1129
+ }) {
1130
+ const [expandedIds, setExpandedIds] = react.useState(() => /* @__PURE__ */ new Set());
1131
+ react.useEffect(() => {
1132
+ if (!entries.length) {
1133
+ setExpandedIds(/* @__PURE__ */ new Set());
1134
+ return;
1135
+ }
1136
+ const newestId = entries[entries.length - 1]?.id;
1137
+ if (!newestId) {
1138
+ return;
1139
+ }
1140
+ setExpandedIds((current) => {
1141
+ if (current.size > 0) {
1142
+ return current;
1143
+ }
1144
+ return /* @__PURE__ */ new Set([newestId]);
1145
+ });
1146
+ }, [entries]);
1147
+ if (!entries.length) {
1148
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.NoticeBox, { title: translate("No rules observed yet"), children: translate(
1149
+ "Interact with the form to record rule evaluations. Only rules that have fired at least once appear here."
1150
+ ) }) });
1151
+ }
1152
+ const reversed = [...entries].reverse();
1153
+ const toggleExpanded = (entryId) => {
1154
+ setExpandedIds((current) => {
1155
+ const next = new Set(current);
1156
+ if (next.has(entryId)) {
1157
+ next.delete(entryId);
1158
+ } else {
1159
+ next.add(entryId);
1160
+ }
1161
+ return next;
1162
+ });
1163
+ };
1164
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-dp12 p-0", children: reversed.map((entry) => {
1165
+ const isInitial = entry.changedFields.length === 0;
1166
+ const ruleCount = entry.ruleResults.length;
1167
+ const effectCount = entry.ruleResults.reduce(
1168
+ (total, result) => total + result.effects.length,
1169
+ 0
1170
+ );
1171
+ const isExpanded = expandedIds.has(entry.id);
1172
+ const isSelected = selectedEntryId === entry.id;
1173
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "m-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
1174
+ ui.Card,
1175
+ {
1176
+ 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%)]" : ""}`,
1177
+ children: [
1178
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-stretch gap-dp4 bg-dhis2-grey-050 py-dp4 pe-dp8 ps-dp4", children: [
1179
+ /* @__PURE__ */ jsxRuntime.jsx(
1180
+ "button",
1181
+ {
1182
+ type: "button",
1183
+ 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",
1184
+ "aria-expanded": isExpanded,
1185
+ "aria-label": isExpanded ? translate("Collapse evaluation details") : translate("Expand evaluation details"),
1186
+ onClick: () => {
1187
+ toggleExpanded(entry.id);
1188
+ },
1189
+ children: isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(ui.IconChevronDown16, {}) : /* @__PURE__ */ jsxRuntime.jsx(ui.IconChevronRight16, {})
1190
+ }
1191
+ ),
1192
+ /* @__PURE__ */ jsxRuntime.jsxs(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ 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",
1197
+ "aria-pressed": isSelected,
1198
+ onClick: () => {
1199
+ onSelectEntry(entry.id);
1200
+ },
1201
+ children: [
1202
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col items-start gap-0.5", children: [
1203
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold leading-[1.3] text-dhis2-grey-900 tabular-nums", children: formatAgo(entry.timestamp) }),
1204
+ !isExpanded && effectCount > 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs leading-[1.3] text-dhis2-grey-600", children: translate("{{count}} effects", {
1205
+ count: effectCount
1206
+ }) }) : null
1207
+ ] }),
1208
+ /* @__PURE__ */ jsxRuntime.jsx(
1209
+ ui.Tag,
1210
+ {
1211
+ className: isInitial ? "shrink-0" : void 0,
1212
+ neutral: !isInitial,
1213
+ children: isInitial ? translate("Initial") : translate("{{count}} rules", { count: ruleCount })
1214
+ }
1215
+ )
1216
+ ]
1217
+ }
1218
+ )
1219
+ ] }),
1220
+ isExpanded ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-dp16 border-t border-dhis2-grey-200 p-dp16", children: [
1221
+ isInitial ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: ruleCount ? translate("{{count}} effects on load", {
1222
+ count: effectCount
1223
+ }) : translate("No rules fired") }) : /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "flex flex-col gap-dp8", children: [
1224
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "m-0 text-[0.6875rem] font-bold uppercase leading-[1.3] tracking-wider text-dhis2-grey-600", children: translate("Changed fields") }),
1225
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-dp8", children: entry.changedFields.map((fieldId) => {
1226
+ const { label, showId } = resolveLabel(
1227
+ fieldId,
1228
+ labelLookup ? (id) => labelLookup.resolveFieldName(id) : void 0
1229
+ );
1230
+ return /* @__PURE__ */ jsxRuntime.jsx(
1231
+ "div",
1232
+ {
1233
+ className: "flex max-w-full flex-col gap-0.5",
1234
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1235
+ ui.Tag,
1236
+ {
1237
+ neutral: true,
1238
+ className: `max-w-full break-words text-xs ${showId ? "" : "font-mono break-all"}`,
1239
+ children: label
1240
+ }
1241
+ )
1242
+ },
1243
+ fieldId
1244
+ );
1245
+ }) })
1246
+ ] }),
1247
+ entry.ruleResults.length === 0 ? !isInitial ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("No rules fired") }) : null : /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "flex flex-col gap-dp8", children: [
1248
+ !isInitial ? /* @__PURE__ */ jsxRuntime.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,
1249
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-dp12", children: entry.ruleResults.map((result, index) => {
1250
+ const { label: displayName } = resolveLabel(
1251
+ result.ruleId,
1252
+ labelLookup ? (id) => labelLookup.resolveRuleName(id) : void 0
1253
+ );
1254
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1255
+ "div",
1256
+ {
1257
+ className: "rounded border border-dhis2-grey-200 bg-dhis2-grey-050 p-dp12",
1258
+ children: [
1259
+ index > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { margin: "12px 0" }) : null,
1260
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-dp12 flex flex-col items-start gap-dp4", children: /* @__PURE__ */ jsxRuntime.jsx(
1261
+ ui.Chip,
1262
+ {
1263
+ selected: highlightRuleId === result.ruleId,
1264
+ onClick: (_, event) => {
1265
+ event.stopPropagation();
1266
+ onHighlightRule(
1267
+ highlightRuleId === result.ruleId ? null : result.ruleId
1268
+ );
1269
+ },
1270
+ children: displayName
1271
+ }
1272
+ ) }),
1273
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-dp12 p-0", children: result.effects.map((effect) => {
1274
+ const {
1275
+ label: targetLabel,
1276
+ showId: showTargetId
1277
+ } = resolveEffectTargetLabel(
1278
+ effect,
1279
+ labelLookup
1280
+ );
1281
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1282
+ "li",
1283
+ {
1284
+ className: "flex flex-col gap-dp4",
1285
+ children: [
1286
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-baseline gap-dp8", children: [
1287
+ /* @__PURE__ */ jsxRuntime.jsx(
1288
+ EffectBadge,
1289
+ {
1290
+ type: effect.type
1291
+ }
1292
+ ),
1293
+ /* @__PURE__ */ jsxRuntime.jsx(
1294
+ "span",
1295
+ {
1296
+ className: `break-words text-[0.8125rem] leading-[1.45] text-dhis2-grey-800 ${showTargetId ? "" : "break-all font-mono"}`,
1297
+ children: targetLabel
1298
+ }
1299
+ )
1300
+ ] }),
1301
+ effect.data ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 ps-dp4 text-[0.8125rem] leading-[1.45] break-words text-dhis2-grey-600", children: effect.data }) : null
1302
+ ]
1303
+ },
1304
+ `${effect.type}-${effect.targetId}-${effect.data ?? ""}`
1305
+ );
1306
+ }) })
1307
+ ]
1308
+ },
1309
+ result.ruleId
1310
+ );
1311
+ }) })
1312
+ ] })
1313
+ ] }) : null
1314
+ ]
1315
+ }
1316
+ ) }, entry.id);
1317
+ }) });
1318
+ }
1319
+ function resolveScopeStageId(metadata) {
1320
+ return metadata.formKind === "event" ? metadata.programStageId : null;
1321
+ }
1322
+ function isRuleInScope(rule, scopeStageId) {
1323
+ return rule.programStageId === null || rule.programStageId === scopeStageId;
1324
+ }
1325
+ function resolveAccentClassName(inScope, firing, selected) {
1326
+ if (selected) {
1327
+ return "bg-dhis2-blue-600";
1328
+ }
1329
+ if (!inScope) {
1330
+ return "bg-transparent";
1331
+ }
1332
+ return firing ? "bg-dhis2-teal-600" : "bg-dhis2-grey-400";
1333
+ }
1334
+ function resolveCardStatus(inScope, firing) {
1335
+ if (!inScope) {
1336
+ return { label: translate("Out of scope"), className: "text-dhis2-grey-500" };
1337
+ }
1338
+ return firing ? { label: translate("Firing"), className: "text-dhis2-teal-700" } : { label: translate("Idle"), className: "text-dhis2-grey-600" };
1339
+ }
1340
+ function formatActionLabel(action) {
1341
+ if (action.targetLabel && action.detail) {
1342
+ return `${action.type} \xB7 ${action.targetLabel} = ${action.detail}`;
1343
+ }
1344
+ if (action.targetLabel) {
1345
+ return `${action.type} \xB7 ${action.targetLabel}`;
1346
+ }
1347
+ return action.type;
1348
+ }
1349
+ function countObservedRules(entries) {
1350
+ const ruleIds = /* @__PURE__ */ new Set();
1351
+ for (const entry of entries) {
1352
+ for (const result of entry.ruleResults) {
1353
+ ruleIds.add(result.ruleId);
1354
+ }
1355
+ }
1356
+ return ruleIds.size;
1357
+ }
1358
+ var PANEL_TAB_KEYS = ["rules", "trace", "graph"];
1359
+ function resolveTabLabel(key) {
1360
+ switch (key) {
1361
+ case "rules":
1362
+ return translate("Rules");
1363
+ case "trace":
1364
+ return translate("Trace");
1365
+ case "graph":
1366
+ return translate("Graph");
1367
+ }
1368
+ }
1369
+ function RulesPanel({ metadata, showConditions = true }) {
1370
+ const formStore = dhis2FormUtilsHooks.useFormStore();
1371
+ const { form } = dhis2FormUtilsHooks.useFormStateContext();
1372
+ const traceStore = useRuleTraceStore();
1373
+ const [tab, setTab] = react.useState("rules");
1374
+ const [selectedEntryId, setSelectedEntryId] = react.useState(null);
1375
+ const [highlightRuleId, setHighlightRuleId] = react.useState(null);
1376
+ const [graphModalOpen, setGraphModalOpen] = react.useState(false);
1377
+ const labelLookup = react.useMemo(() => createLabelLookup(metadata), [metadata]);
1378
+ const catalog = react.useMemo(() => resolveProgramRulesList(metadata), [metadata]);
1379
+ const scopeStageId = react.useMemo(() => resolveScopeStageId(metadata), [metadata]);
1380
+ const entries = react.useSyncExternalStore(
1381
+ react.useCallback((listener) => traceStore.subscribe(listener), [traceStore]),
1382
+ react.useCallback(() => traceStore.getSnapshot(), [traceStore]),
1383
+ react.useCallback(() => traceStore.getSnapshot(), [traceStore])
1384
+ );
1385
+ const fieldState = react.useSyncExternalStore(
1386
+ react.useCallback((listener) => formStore.fieldStore.subscribeAll(listener), [formStore]),
1387
+ react.useCallback(() => formStore.fieldStore.getSnapshot(), [formStore]),
1388
+ react.useCallback(() => formStore.fieldStore.getSnapshot(), [formStore])
1389
+ );
1390
+ const activeRuleIds = react.useMemo(() => getActiveRuleIds(entries), [entries]);
1391
+ const scopedRules = react.useMemo(
1392
+ () => catalog.filter((rule) => isRuleInScope(rule, scopeStageId)),
1393
+ [catalog, scopeStageId]
1394
+ );
1395
+ const firingCount = react.useMemo(
1396
+ () => scopedRules.filter((rule) => activeRuleIds.has(rule.id)).length,
1397
+ [scopedRules, activeRuleIds]
1398
+ );
1399
+ const selectedEntry = react.useMemo(
1400
+ () => entries.find((entry) => entry.id === selectedEntryId) ?? null,
1401
+ [entries, selectedEntryId]
1402
+ );
1403
+ const observedRuleCount = react.useMemo(() => countObservedRules(entries), [entries]);
1404
+ const highlightedRuleName = react.useMemo(() => {
1405
+ if (!highlightRuleId) {
1406
+ return null;
1407
+ }
1408
+ return labelLookup.resolveRuleName(highlightRuleId);
1409
+ }, [highlightRuleId, labelLookup]);
1410
+ const graphHasNodes = react.useMemo(() => {
1411
+ const entry = resolveGraphTraceEntry(entries, selectedEntryId);
1412
+ return entry ? buildGraphFromTrace([entry], labelLookup).nodes.length > 0 : false;
1413
+ }, [entries, labelLookup, selectedEntryId]);
1414
+ const graphSubtitle = react.useMemo(() => {
1415
+ if (selectedEntry && highlightedRuleName) {
1416
+ return translate("Highlighting evaluation {{time}} \xB7 Rule: {{name}}", {
1417
+ time: formatAgo(selectedEntry.timestamp),
1418
+ name: highlightedRuleName
1419
+ });
1420
+ }
1421
+ if (selectedEntry) {
1422
+ return translate("Highlighting evaluation {{time}}", {
1423
+ time: formatAgo(selectedEntry.timestamp)
1424
+ });
1425
+ }
1426
+ if (highlightedRuleName) {
1427
+ return translate("Rule: {{name}}", { name: highlightedRuleName });
1428
+ }
1429
+ return null;
1430
+ }, [highlightedRuleName, selectedEntry]);
1431
+ const graphProps = {
1432
+ entries,
1433
+ fieldState,
1434
+ formValues: form.getValues(),
1435
+ selectedEntryId,
1436
+ highlightRuleId,
1437
+ labelLookup
1438
+ };
1439
+ const expandGraphButton = /* @__PURE__ */ jsxRuntime.jsx(
1440
+ "button",
1441
+ {
1442
+ type: "button",
1443
+ 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",
1444
+ "aria-label": translate("Open graph in full screen"),
1445
+ onClick: () => {
1446
+ setGraphModalOpen(true);
1447
+ },
1448
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconFullscreen16, {})
1449
+ }
1450
+ );
1451
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1452
+ "aside",
1453
+ {
1454
+ 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",
1455
+ "aria-label": translate("Rules"),
1456
+ children: [
1457
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white px-dp16 pt-dp12", children: [
1458
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-dp12 pb-[10px]", children: [
1459
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-dp8", children: [
1460
+ /* @__PURE__ */ jsxRuntime.jsx(
1461
+ "span",
1462
+ {
1463
+ className: "size-2 shrink-0 rounded-full bg-dhis2-teal-600",
1464
+ "aria-hidden": "true"
1465
+ }
1466
+ ),
1467
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 text-base font-bold leading-[1.35] text-dhis2-grey-900", children: translate("Rules") })
1468
+ ] }),
1469
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs text-dhis2-grey-600", children: translate("{{scoped}} in scope \xB7 {{firing}} firing", {
1470
+ scoped: scopedRules.length,
1471
+ firing: firingCount
1472
+ }) })
1473
+ ] }),
1474
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex", children: PANEL_TAB_KEYS.map((key) => /* @__PURE__ */ jsxRuntime.jsx(
1475
+ "button",
1476
+ {
1477
+ type: "button",
1478
+ 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"}`,
1479
+ onClick: () => {
1480
+ setTab(key);
1481
+ },
1482
+ children: resolveTabLabel(key)
1483
+ },
1484
+ key
1485
+ )) })
1486
+ ] }),
1487
+ tab !== "rules" && (selectedEntry || highlightedRuleName) ? /* @__PURE__ */ jsxRuntime.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: [
1488
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
1489
+ selectedEntry ? translate("Highlighting evaluation {{time}}", {
1490
+ time: formatAgo(selectedEntry.timestamp)
1491
+ }) : null,
1492
+ selectedEntry && highlightedRuleName ? " \xB7 " : null,
1493
+ highlightedRuleName ? translate("Rule: {{name}}", { name: highlightedRuleName }) : null
1494
+ ] }),
1495
+ /* @__PURE__ */ jsxRuntime.jsx(
1496
+ ui.Button,
1497
+ {
1498
+ small: true,
1499
+ onClick: () => {
1500
+ setSelectedEntryId(null);
1501
+ setHighlightRuleId(null);
1502
+ },
1503
+ children: translate("Clear")
1504
+ }
1505
+ )
1506
+ ] }) : null,
1507
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto p-dp16", children: tab === "rules" ? /* @__PURE__ */ jsxRuntime.jsx(
1508
+ RulesTab,
1509
+ {
1510
+ catalog,
1511
+ scopeStageId,
1512
+ activeRuleIds,
1513
+ selectedRuleId: highlightRuleId,
1514
+ showConditions,
1515
+ labelLookup,
1516
+ onSelectRule: (ruleId) => {
1517
+ setHighlightRuleId(ruleId);
1518
+ setTab("graph");
1519
+ }
1520
+ }
1521
+ ) : tab === "trace" ? /* @__PURE__ */ jsxRuntime.jsx(
1522
+ TraceTimeline,
1523
+ {
1524
+ entries,
1525
+ selectedEntryId,
1526
+ highlightRuleId,
1527
+ onSelectEntry: (entryId) => {
1528
+ setSelectedEntryId((current) => current === entryId ? null : entryId);
1529
+ },
1530
+ onHighlightRule: (ruleId) => {
1531
+ setHighlightRuleId(ruleId);
1532
+ if (ruleId) {
1533
+ setTab("graph");
1534
+ }
1535
+ },
1536
+ labelLookup
1537
+ }
1538
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1539
+ RuleGraphView,
1540
+ {
1541
+ ...graphProps,
1542
+ headerActions: graphHasNodes ? expandGraphButton : void 0
1543
+ }
1544
+ ) }),
1545
+ entries.length > 0 && tab !== "rules" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sr-only", "aria-live": "polite", children: translate("{{evaluations}} evaluations \xB7 {{rules}} rules observed", {
1546
+ evaluations: entries.length,
1547
+ rules: observedRuleCount
1548
+ }) }) : null,
1549
+ /* @__PURE__ */ jsxRuntime.jsx(
1550
+ RuleGraphModal,
1551
+ {
1552
+ ...graphProps,
1553
+ open: graphModalOpen,
1554
+ onClose: () => {
1555
+ setGraphModalOpen(false);
1556
+ },
1557
+ subtitle: graphSubtitle,
1558
+ layoutKey: graphModalOpen ? "open" : "closed"
1559
+ }
1560
+ )
1561
+ ]
1562
+ }
1563
+ );
1564
+ }
1565
+ function RulesTab({
1566
+ catalog,
1567
+ scopeStageId,
1568
+ activeRuleIds,
1569
+ selectedRuleId,
1570
+ showConditions,
1571
+ labelLookup,
1572
+ onSelectRule
1573
+ }) {
1574
+ if (!catalog.length) {
1575
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("This program has no rules.") });
1576
+ }
1577
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-[10px] p-0", children: catalog.map((rule) => {
1578
+ const inScope = isRuleInScope(rule, scopeStageId);
1579
+ const firing = activeRuleIds.has(rule.id);
1580
+ const isSelected = selectedRuleId === rule.id;
1581
+ const status = resolveCardStatus(inScope, firing);
1582
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "m-0 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
1583
+ "article",
1584
+ {
1585
+ role: "button",
1586
+ tabIndex: 0,
1587
+ onClick: () => {
1588
+ onSelectRule(rule.id);
1589
+ },
1590
+ onKeyDown: (event) => {
1591
+ if (event.key === "Enter" || event.key === " ") {
1592
+ event.preventDefault();
1593
+ onSelectRule(rule.id);
1594
+ }
1595
+ },
1596
+ 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%)]",
1597
+ children: [
1598
+ /* @__PURE__ */ jsxRuntime.jsx(
1599
+ "span",
1600
+ {
1601
+ className: `absolute inset-y-0 start-0 w-[3px] rounded-s-[5px] ${resolveAccentClassName(inScope, firing, isSelected)}`,
1602
+ "aria-hidden": "true"
1603
+ }
1604
+ ),
1605
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-dp8", children: [
1606
+ /* @__PURE__ */ jsxRuntime.jsx(
1607
+ "h3",
1608
+ {
1609
+ className: `m-0 min-w-0 flex-1 text-sm font-semibold leading-[1.4] ${inScope ? "text-dhis2-grey-900" : "text-dhis2-grey-600"}`,
1610
+ children: rule.name
1611
+ }
1612
+ ),
1613
+ /* @__PURE__ */ jsxRuntime.jsx(
1614
+ "span",
1615
+ {
1616
+ className: `shrink-0 text-[11px] font-semibold ${status.className}`,
1617
+ children: status.label
1618
+ }
1619
+ )
1620
+ ] }),
1621
+ rule.programRuleActions.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-[6px]", children: rule.programRuleActions.map((action, index) => {
1622
+ const summary = formatRuleActionSummary(
1623
+ action,
1624
+ labelLookup
1625
+ );
1626
+ return /* @__PURE__ */ jsxRuntime.jsx(
1627
+ EffectBadge,
1628
+ {
1629
+ type: summary.type,
1630
+ children: formatActionLabel(summary)
1631
+ },
1632
+ `${rule.id}-action-${String(index)}`
1633
+ );
1634
+ }) }) : null,
1635
+ showConditions && rule.condition ? /* @__PURE__ */ jsxRuntime.jsx(
1636
+ "p",
1637
+ {
1638
+ className: "m-0 break-words font-mono text-[11px] leading-[1.5] text-dhis2-grey-700",
1639
+ title: rule.condition,
1640
+ children: rule.condition
1641
+ }
1642
+ ) : null,
1643
+ !inScope && rule.programStageId ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-dhis2-grey-600", children: translate("Applies to {{stage}}", {
1644
+ stage: labelLookup.resolveStageName(rule.programStageId)
1645
+ }) }) : null
1646
+ ]
1647
+ }
1648
+ ) }, rule.id);
1649
+ }) });
1650
+ }
1651
+
1652
+ exports.EFFECT_ICONS = EFFECT_ICONS;
1653
+ exports.RuleDevtoolsScope = RuleDevtoolsScope;
1654
+ exports.RulesPanel = RulesPanel;
1655
+ exports.createLabelLookup = createLabelLookup;
1656
+ exports.getEffectEdgeStroke = getEffectEdgeStroke;
1657
+ exports.getEffectShortLabel = getEffectShortLabel;
1658
+ exports.getEffectTagRenderProps = getEffectTagRenderProps;
1659
+ exports.getEffectTagRenderPropsForVariant = getEffectTagRenderPropsForVariant;
1660
+ exports.getEffectVariant = getEffectVariant;
1661
+ exports.getEffectVisual = getEffectVisual;
1662
+ //# sourceMappingURL=index.cjs.map
1663
+ //# sourceMappingURL=index.cjs.map