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