@nnkogift/dhis2-form-utils-devtools 0.1.0-alpha.5 → 0.1.0-alpha.6

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