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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1364 -651
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.css +1 -1
  58. package/dist/index.d.cts +12 -63
  59. package/dist/index.d.ts +12 -63
  60. package/dist/index.js +5 -1669
  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 +27 -4
package/dist/index.cjs CHANGED
@@ -1,22 +1,34 @@
1
1
  'use strict';
2
2
 
3
3
  var ui = require('@dhis2/ui');
4
- var dhis2FormUtilsHooks = require('@nnkogift/dhis2-form-utils-hooks');
5
- var react = require('react');
6
- var dhis2FormUtilsMetadata = require('@nnkogift/dhis2-form-utils-metadata');
7
4
  var i18n = require('@dhis2/d2-i18n');
8
5
  var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('react');
9
7
  var react$1 = require('@xyflow/react');
8
+ var dhis2FormUtilsHooks = require('@nnkogift/dhis2-form-utils-hooks');
9
+ var dhis2FormUtilsMetadata = require('@nnkogift/dhis2-form-utils-metadata');
10
+ var appRuntime = require('@dhis2/app-runtime');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
13
 
13
14
  var i18n__default = /*#__PURE__*/_interopDefault(i18n);
14
15
 
15
- // src/RulesPanel.tsx
16
+ var __defProp = Object.defineProperty;
17
+ var __getOwnPropNames = Object.getOwnPropertyNames;
18
+ var __esm = (fn, res, err) => function __init() {
19
+ if (err) throw err[0];
20
+ try {
21
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
22
+ } catch (e) {
23
+ throw err = [e], e;
24
+ }
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
16
30
 
17
31
  // src/buildGraph.ts
18
- var nodeKey = (kind, id) => `${kind}:${id}`;
19
- var edgeKey = (source, ruleId, target, effectType) => `${source}|${ruleId}|${target}|${effectType ?? ""}`;
20
32
  function accumulateGraph(previous, entry, labelLookup) {
21
33
  const nodes = new Map(previous.nodes.map((node) => [node.id, node]));
22
34
  const edges = new Map(previous.edges.map((edge) => [edge.id, edge]));
@@ -78,167 +90,13 @@ function buildGraphFromTrace(entries, labelLookup) {
78
90
  { nodes: [], edges: [] }
79
91
  );
80
92
  }
81
- var createLookup = (rules, fields, sections, stages) => ({
82
- resolveRuleName: (id) => rules.get(id) ?? id,
83
- resolveFieldName: (id) => fields.get(id) ?? id,
84
- resolveSectionName: (id) => sections.get(id) ?? id,
85
- resolveStageName: (id) => stages.get(id) ?? id
86
- });
87
- function buildEventLookup(source) {
88
- const rules = /* @__PURE__ */ new Map();
89
- const fields = /* @__PURE__ */ new Map();
90
- const sections = /* @__PURE__ */ new Map();
91
- const stages = /* @__PURE__ */ new Map();
92
- for (const rule of source.metadata.programRules) {
93
- if (!rule.id) {
94
- continue;
95
- }
96
- rules.set(rule.id, rule.displayName ?? rule.id);
97
- }
98
- const stage = dhis2FormUtilsMetadata.selectProgramStage(
99
- source.metadata,
100
- source.programStageId
101
- ) ?? {
102
- id: source.programStageId,
103
- programStageDataElements: [],
104
- programStageSections: []
105
- };
106
- for (const psde of stage.programStageDataElements ?? []) {
107
- const de = psde.dataElement;
108
- if (!de?.id) {
109
- continue;
110
- }
111
- fields.set(de.id, de.displayFormName ?? de.displayName ?? de.id);
112
- }
113
- for (const section of stage.programStageSections ?? []) {
114
- sections.set(section.id, section.displayName ?? section.id);
115
- }
116
- for (const programStage of source.metadata.programStages) {
117
- if (!programStage.id) {
118
- continue;
119
- }
120
- stages.set(programStage.id, programStage.displayName ?? programStage.id);
121
- }
122
- return createLookup(rules, fields, sections, stages);
123
- }
124
- function buildTrackerLookup(source) {
125
- const rules = /* @__PURE__ */ new Map();
126
- const fields = /* @__PURE__ */ new Map();
127
- const sections = /* @__PURE__ */ new Map();
128
- const stages = /* @__PURE__ */ new Map();
129
- for (const rule of source.metadata.programRules) {
130
- if (!rule.id) {
131
- continue;
132
- }
133
- rules.set(rule.id, rule.name ?? rule.id);
134
- }
135
- for (const ptea of source.metadata.programTrackedEntityAttributes) {
136
- const tea = ptea.trackedEntityAttribute;
137
- if (!tea.id) {
138
- continue;
139
- }
140
- fields.set(tea.id, tea.formName ?? tea.displayName ?? tea.id);
141
- }
142
- for (const section of source.metadata.programSections ?? []) {
143
- sections.set(section.id, section.displayName ?? section.id);
144
- }
145
- for (const programStage of source.programStages ?? []) {
146
- if (!programStage.id) {
147
- continue;
148
- }
149
- stages.set(programStage.id, programStage.displayName ?? programStage.id);
150
- }
151
- return createLookup(rules, fields, sections, stages);
152
- }
153
- function createLabelLookup(source) {
154
- return source.formKind === "event" ? buildEventLookup(source) : buildTrackerLookup(source);
155
- }
156
- var EFFECT_VARIANT = {
157
- HIDEFIELD: "hide",
158
- HIDEOPTION: "hide",
159
- HIDEOPTIONGROUP: "hide",
160
- HIDESECTION: "hide",
161
- HIDEPROGRAMSTAGE: "hide",
162
- SHOWFIELD: "show",
163
- SHOWOPTION: "show",
164
- SHOWOPTIONGROUP: "show",
165
- ASSIGN: "assign",
166
- SETMANDATORYFIELD: "mandatory",
167
- UNSETMANDATORYFIELD: "mandatory",
168
- SHOWWARNING: "warning",
169
- WARNINGONCOMPLETE: "warning",
170
- SHOWERROR: "error",
171
- ERRORONCOMPLETE: "error",
172
- DISPLAYTEXT: "feedback",
173
- DISPLAYKEYVALUEPAIR: "feedback",
174
- SENDMESSAGE: "message",
175
- SCHEDULEMESSAGE: "message",
176
- read: "read"
177
- };
178
- var EFFECT_VISUALS = {
179
- hide: {
180
- tagClassName: "bg-dhis2-grey-200 text-dhis2-grey-900",
181
- edgeStroke: "#494949",
182
- shortLabel: "hide"
183
- },
184
- show: {
185
- tagClassName: "bg-dhis2-green-100 text-dhis2-green-900",
186
- edgeStroke: "#388e3c",
187
- shortLabel: "show"
188
- },
189
- assign: {
190
- tagClassName: "bg-dhis2-blue-100 text-dhis2-blue-900",
191
- edgeStroke: "#1565c0",
192
- shortLabel: "assign"
193
- },
194
- mandatory: {
195
- tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
196
- edgeStroke: "#e56408",
197
- shortLabel: "required"
198
- },
199
- warning: {
200
- tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
201
- edgeStroke: "#ff8302",
202
- shortLabel: "warn"
203
- },
204
- error: {
205
- tagClassName: "bg-dhis2-red-100 text-dhis2-red-900",
206
- edgeStroke: "#c62828",
207
- shortLabel: "error"
208
- },
209
- feedback: {
210
- tagClassName: "bg-dhis2-purple-100 text-dhis2-purple-900",
211
- edgeStroke: "#9c27b0",
212
- shortLabel: "feedback"
213
- },
214
- message: {
215
- tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
216
- edgeStroke: "#00796b",
217
- shortLabel: "message"
218
- },
219
- read: {
220
- tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
221
- edgeStroke: "#00796b",
222
- shortLabel: "read"
223
- },
224
- default: {
225
- tagClassName: "bg-dhis2-grey-100 text-dhis2-grey-800",
226
- edgeStroke: "#494949",
227
- shortLabel: "effect"
93
+ var nodeKey, edgeKey;
94
+ var init_buildGraph = __esm({
95
+ "src/buildGraph.ts"() {
96
+ nodeKey = (kind, id) => `${kind}:${id}`;
97
+ edgeKey = (source, ruleId, target, effectType) => `${source}|${ruleId}|${target}|${effectType ?? ""}`;
228
98
  }
229
- };
230
- var EFFECT_ICONS = {
231
- hide: ui.IconViewOff16,
232
- show: ui.IconView16,
233
- assign: ui.IconEdit16,
234
- mandatory: ui.IconStarFilled16,
235
- warning: ui.IconWarningFilled16,
236
- error: ui.IconErrorFilled16,
237
- feedback: ui.IconMessages16,
238
- message: ui.IconMail16,
239
- read: ui.IconLink16,
240
- default: ui.IconInfo16
241
- };
99
+ });
242
100
  function getEffectVariant(type) {
243
101
  return EFFECT_VARIANT[type] ?? "default";
244
102
  }
@@ -246,7 +104,6 @@ function getEffectVisual(type) {
246
104
  const variant = getEffectVariant(type);
247
105
  return { variant, ...EFFECT_VISUALS[variant] };
248
106
  }
249
- var TAG_LAYOUT_CLASS = "max-w-full break-words";
250
107
  function tagPropsForVariant(variant) {
251
108
  switch (variant) {
252
109
  case "show":
@@ -286,12 +143,108 @@ function getEffectEdgeStroke(type, highlighted = true) {
286
143
  function getEffectShortLabel(type) {
287
144
  return getEffectVisual(type).shortLabel;
288
145
  }
146
+ var EFFECT_VARIANT, EFFECT_VISUALS; exports.EFFECT_ICONS = void 0; var TAG_LAYOUT_CLASS;
147
+ var init_effectStyles = __esm({
148
+ "src/effectStyles.ts"() {
149
+ EFFECT_VARIANT = {
150
+ HIDEFIELD: "hide",
151
+ HIDEOPTION: "hide",
152
+ HIDEOPTIONGROUP: "hide",
153
+ HIDESECTION: "hide",
154
+ HIDEPROGRAMSTAGE: "hide",
155
+ SHOWFIELD: "show",
156
+ SHOWOPTION: "show",
157
+ SHOWOPTIONGROUP: "show",
158
+ ASSIGN: "assign",
159
+ SETMANDATORYFIELD: "mandatory",
160
+ UNSETMANDATORYFIELD: "mandatory",
161
+ SHOWWARNING: "warning",
162
+ WARNINGONCOMPLETE: "warning",
163
+ SHOWERROR: "error",
164
+ ERRORONCOMPLETE: "error",
165
+ DISPLAYTEXT: "feedback",
166
+ DISPLAYKEYVALUEPAIR: "feedback",
167
+ SENDMESSAGE: "message",
168
+ SCHEDULEMESSAGE: "message",
169
+ read: "read"
170
+ };
171
+ EFFECT_VISUALS = {
172
+ hide: {
173
+ tagClassName: "bg-dhis2-grey-200 text-dhis2-grey-900",
174
+ edgeStroke: "#494949",
175
+ shortLabel: "hide"
176
+ },
177
+ show: {
178
+ tagClassName: "bg-dhis2-green-100 text-dhis2-green-900",
179
+ edgeStroke: "#388e3c",
180
+ shortLabel: "show"
181
+ },
182
+ assign: {
183
+ tagClassName: "bg-dhis2-blue-100 text-dhis2-blue-900",
184
+ edgeStroke: "#1565c0",
185
+ shortLabel: "assign"
186
+ },
187
+ mandatory: {
188
+ tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
189
+ edgeStroke: "#e56408",
190
+ shortLabel: "required"
191
+ },
192
+ warning: {
193
+ tagClassName: "bg-dhis2-yellow-100 text-dhis2-yellow-900",
194
+ edgeStroke: "#ff8302",
195
+ shortLabel: "warn"
196
+ },
197
+ error: {
198
+ tagClassName: "bg-dhis2-red-100 text-dhis2-red-900",
199
+ edgeStroke: "#c62828",
200
+ shortLabel: "error"
201
+ },
202
+ feedback: {
203
+ tagClassName: "bg-dhis2-purple-100 text-dhis2-purple-900",
204
+ edgeStroke: "#9c27b0",
205
+ shortLabel: "feedback"
206
+ },
207
+ message: {
208
+ tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
209
+ edgeStroke: "#00796b",
210
+ shortLabel: "message"
211
+ },
212
+ read: {
213
+ tagClassName: "bg-dhis2-teal-100 text-dhis2-teal-900",
214
+ edgeStroke: "#00796b",
215
+ shortLabel: "read"
216
+ },
217
+ default: {
218
+ tagClassName: "bg-dhis2-grey-100 text-dhis2-grey-800",
219
+ edgeStroke: "#494949",
220
+ shortLabel: "effect"
221
+ }
222
+ };
223
+ exports.EFFECT_ICONS = {
224
+ hide: ui.IconViewOff16,
225
+ show: ui.IconView16,
226
+ assign: ui.IconEdit16,
227
+ mandatory: ui.IconStarFilled16,
228
+ warning: ui.IconWarningFilled16,
229
+ error: ui.IconErrorFilled16,
230
+ feedback: ui.IconMessages16,
231
+ message: ui.IconMail16,
232
+ read: ui.IconLink16,
233
+ default: ui.IconInfo16
234
+ };
235
+ TAG_LAYOUT_CLASS = "max-w-full break-words";
236
+ }
237
+ });
289
238
  function translate(message, variables) {
290
239
  return String(i18n__default.default.t(message, variables));
291
240
  }
241
+ var init_i18n = __esm({
242
+ "src/i18n.ts"() {
243
+ }
244
+ });
292
245
  function EffectBadge({ type, children, className = "" }) {
293
246
  const variant = getEffectVariant(type);
294
- const Icon = EFFECT_ICONS[variant];
247
+ const Icon = exports.EFFECT_ICONS[variant];
295
248
  const tagProps = getEffectTagRenderProps(type);
296
249
  const label = children ?? type;
297
250
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -305,23 +258,13 @@ function EffectBadge({ type, children, className = "" }) {
305
258
  }
306
259
  );
307
260
  }
308
- var LEGEND_VARIANTS = [
309
- { variant: "read", label: translate("Read") },
310
- { variant: "hide", label: translate("Hide") },
311
- { variant: "show", label: translate("Show") },
312
- { variant: "assign", label: translate("Assign") },
313
- { variant: "mandatory", label: translate("Required") },
314
- { variant: "warning", label: translate("Warning") },
315
- { variant: "error", label: translate("Error") },
316
- { variant: "feedback", label: translate("Feedback") }
317
- ];
318
261
  function EffectLegend({ activeVariants }) {
319
262
  const items = LEGEND_VARIANTS.filter((item) => activeVariants.has(item.variant));
320
263
  if (!items.length) {
321
264
  return null;
322
265
  }
323
266
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-dp8 border-t border-dhis2-grey-200 px-dp12 py-dp8", children: items.map((item) => {
324
- const Icon = EFFECT_ICONS[item.variant];
267
+ const Icon = exports.EFFECT_ICONS[item.variant];
325
268
  const tagProps = getEffectTagRenderPropsForVariant(item.variant);
326
269
  return /* @__PURE__ */ jsxRuntime.jsx(
327
270
  ui.Tag,
@@ -335,230 +278,74 @@ function EffectLegend({ activeVariants }) {
335
278
  );
336
279
  }) });
337
280
  }
338
-
339
- // src/formatAgo.ts
340
- function formatAgo(timestamp, now = Date.now()) {
341
- const diffMs = Math.max(0, now - timestamp);
342
- if (diffMs < 5e3) {
343
- return translate("just now");
344
- }
345
- const seconds = Math.floor(diffMs / 1e3);
346
- if (seconds < 60) {
347
- return translate("{{seconds}}s ago", { seconds });
281
+ var LEGEND_VARIANTS;
282
+ var init_EffectBadge = __esm({
283
+ "src/EffectBadge.tsx"() {
284
+ init_effectStyles();
285
+ init_i18n();
286
+ LEGEND_VARIANTS = [
287
+ { variant: "read", label: translate("Read") },
288
+ { variant: "hide", label: translate("Hide") },
289
+ { variant: "show", label: translate("Show") },
290
+ { variant: "assign", label: translate("Assign") },
291
+ { variant: "mandatory", label: translate("Required") },
292
+ { variant: "warning", label: translate("Warning") },
293
+ { variant: "error", label: translate("Error") },
294
+ { variant: "feedback", label: translate("Feedback") }
295
+ ];
348
296
  }
349
- const minutes = Math.floor(seconds / 60);
350
- if (minutes < 60) {
351
- return translate("{{minutes}}m ago", { minutes });
297
+ });
298
+
299
+ // src/traceEntry.ts
300
+ function resolveGraphTraceEntry(entries, selectedEntryId) {
301
+ if (!entries.length) {
302
+ return null;
352
303
  }
353
- const hours = Math.floor(minutes / 60);
354
- if (hours < 24) {
355
- return translate("{{hours}}h ago", { hours });
304
+ if (selectedEntryId) {
305
+ return entries.find((entry) => entry.id === selectedEntryId) ?? entries[entries.length - 1];
356
306
  }
357
- return new Date(timestamp).toLocaleString();
307
+ return entries[entries.length - 1];
358
308
  }
359
-
360
- // src/formatRuleActionSummary.ts
361
- function resolveFieldLabel(id, displayName, labelLookup) {
362
- if (labelLookup) {
363
- const resolved = labelLookup.resolveFieldName(id);
364
- if (resolved !== id) {
365
- return resolved;
366
- }
309
+ function getActiveRuleIds(entries) {
310
+ const latest = resolveGraphTraceEntry(entries);
311
+ if (!latest) {
312
+ return /* @__PURE__ */ new Set();
367
313
  }
368
- return displayName ?? id;
314
+ return new Set(latest.ruleResults.map((result) => result.ruleId));
369
315
  }
370
- function resolveSectionLabel(id, displayName, labelLookup) {
371
- if (labelLookup) {
372
- const resolved = labelLookup.resolveSectionName(id);
373
- if (resolved !== id) {
374
- return resolved;
375
- }
316
+ var init_traceEntry = __esm({
317
+ "src/traceEntry.ts"() {
376
318
  }
377
- return displayName ?? id;
319
+ });
320
+
321
+ // src/graphLayout.ts
322
+ function median(values) {
323
+ const sorted = [...values].sort((a, b) => a - b);
324
+ const mid = Math.floor(sorted.length / 2);
325
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
378
326
  }
379
- function formatRuleActionSummary(action, labelLookup) {
380
- const type = action.programRuleActionType ?? "UNKNOWN";
381
- let targetLabel = "";
382
- if (action.dataElement?.id) {
383
- targetLabel = resolveFieldLabel(
384
- action.dataElement.id,
385
- action.dataElement.displayName,
386
- labelLookup
387
- );
388
- } else if (action.trackedEntityAttribute?.id) {
389
- targetLabel = resolveFieldLabel(
390
- action.trackedEntityAttribute.id,
391
- action.trackedEntityAttribute.displayName,
392
- labelLookup
393
- );
394
- } else if (action.programStageSection?.id) {
395
- targetLabel = resolveSectionLabel(
396
- action.programStageSection.id,
397
- action.programStageSection.displayName,
398
- labelLookup
399
- );
400
- } else if (action.programSection?.id) {
401
- targetLabel = resolveSectionLabel(
402
- action.programSection.id,
403
- action.programSection.displayName,
404
- labelLookup
405
- );
406
- } else if (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR") {
407
- targetLabel = action.location ?? "feedback";
408
- }
409
- let detail;
410
- if (action.data) {
411
- detail = action.data;
412
- }
413
- if (action.content && (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR")) {
414
- detail = action.content;
327
+ function staggerOffset(indexInBucket) {
328
+ if (indexInBucket === 0) {
329
+ return 0;
415
330
  }
416
- return { type, targetLabel, detail };
417
- }
418
-
419
- // src/resolveProgramRulesList.ts
420
- function sortByPriority(rules) {
421
- return [...rules].sort((left, right) => (left.priority ?? 0) - (right.priority ?? 0));
331
+ const magnitude = Math.ceil(indexInBucket / 2) * STAGGER;
332
+ return indexInBucket % 2 === 1 ? magnitude : -magnitude;
422
333
  }
423
- function toCatalogRule(rule) {
424
- return {
425
- id: rule.id,
426
- name: rule.name,
427
- condition: rule.condition,
428
- priority: rule.priority,
429
- programRuleActions: [...rule.programRuleActions ?? []],
430
- programStageId: rule.programStageId
431
- };
334
+ function columnFor(node) {
335
+ if (node.role === "target" && node.kind === "feedback") {
336
+ return FEEDBACK_COLUMN;
337
+ }
338
+ return ROLE_COLUMNS[node.role];
432
339
  }
433
- function resolveProgramRulesList(metadata) {
434
- if (metadata.formKind === "event") {
435
- return sortByPriority(
436
- metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
437
- (rule) => toCatalogRule({
438
- id: rule.id,
439
- name: rule.displayName ?? rule.id,
440
- condition: rule.condition,
441
- priority: rule.priority,
442
- programRuleActions: rule.programRuleActions,
443
- programStageId: rule.programStage?.id ?? null
444
- })
445
- )
446
- );
447
- }
448
- return sortByPriority(
449
- metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
450
- (rule) => toCatalogRule({
451
- id: rule.id,
452
- name: rule.name ?? rule.id,
453
- condition: rule.condition,
454
- priority: rule.priority,
455
- programRuleActions: rule.programRuleActions,
456
- programStageId: rule.programStage?.id ?? null
457
- })
458
- )
459
- );
460
- }
461
-
462
- // src/constants.ts
463
- var DEFAULT_TRACE_MAX_ENTRIES = 200;
464
-
465
- // src/traceStore.ts
466
- function createRuleTraceStore(maxEntries = DEFAULT_TRACE_MAX_ENTRIES) {
467
- let entries = [];
468
- const listeners = /* @__PURE__ */ new Set();
469
- let disposeTraceSubscription = null;
470
- const notify = () => {
471
- for (const listener of listeners) {
472
- listener();
473
- }
474
- };
475
- return {
476
- record(entry) {
477
- entries = [...entries, entry].slice(-maxEntries);
478
- notify();
479
- },
480
- getSnapshot() {
481
- return entries;
482
- },
483
- subscribe(listener) {
484
- listeners.add(listener);
485
- return () => {
486
- listeners.delete(listener);
487
- };
488
- },
489
- dispose() {
490
- disposeTraceSubscription?.();
491
- disposeTraceSubscription = null;
492
- listeners.clear();
493
- },
494
- bindTraceSubscription(unsubscribe) {
495
- disposeTraceSubscription = unsubscribe;
496
- }
497
- };
498
- }
499
-
500
- // src/attach.ts
501
- function attachRuleDevtools(formStore, options) {
502
- const store = createRuleTraceStore(
503
- DEFAULT_TRACE_MAX_ENTRIES
504
- );
505
- store.bindTraceSubscription(formStore.subscribeTrace(store.record));
506
- return store;
507
- }
508
- var RuleTraceContext = react.createContext(null);
509
- function RuleDevtoolsScope({ formStore, children }) {
510
- const traceStore = react.useMemo(() => attachRuleDevtools(formStore), [formStore]);
511
- react.useEffect(() => {
512
- return () => {
513
- traceStore.dispose();
514
- };
515
- }, [traceStore]);
516
- return /* @__PURE__ */ jsxRuntime.jsx(RuleTraceContext.Provider, { value: traceStore, children });
517
- }
518
- function useRuleTraceStore() {
519
- const store = react.useContext(RuleTraceContext);
520
- if (!store) {
521
- throw new Error("useRuleTraceStore must be used within RuleDevtoolsScope");
522
- }
523
- return store;
524
- }
525
-
526
- // src/graphLayout.ts
527
- var ROLE_COLUMNS = {
528
- source: 0,
529
- rule: 280,
530
- target: 560
531
- };
532
- var FEEDBACK_COLUMN = 720;
533
- var LANE_HEIGHT = 104;
534
- var STAGGER = 24;
535
- function median(values) {
536
- const sorted = [...values].sort((a, b) => a - b);
537
- const mid = Math.floor(sorted.length / 2);
538
- return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
539
- }
540
- function staggerOffset(indexInBucket) {
541
- if (indexInBucket === 0) {
542
- return 0;
543
- }
544
- const magnitude = Math.ceil(indexInBucket / 2) * STAGGER;
545
- return indexInBucket % 2 === 1 ? magnitude : -magnitude;
546
- }
547
- function columnFor(node) {
548
- if (node.role === "target" && node.kind === "feedback") {
549
- return FEEDBACK_COLUMN;
550
- }
551
- return ROLE_COLUMNS[node.role];
552
- }
553
- function prepareFlowGraph(graph) {
554
- const readSources = /* @__PURE__ */ new Set();
555
- const effectTargets = /* @__PURE__ */ new Set();
556
- for (const edge of graph.edges) {
557
- if (edge.effectType === "read") {
558
- readSources.add(edge.source);
559
- } else {
560
- effectTargets.add(edge.target);
561
- }
340
+ function prepareFlowGraph(graph) {
341
+ const readSources = /* @__PURE__ */ new Set();
342
+ const effectTargets = /* @__PURE__ */ new Set();
343
+ for (const edge of graph.edges) {
344
+ if (edge.effectType === "read") {
345
+ readSources.add(edge.source);
346
+ } else {
347
+ effectTargets.add(edge.target);
348
+ }
562
349
  }
563
350
  const flowNodes = [];
564
351
  const endpointByGraphId = /* @__PURE__ */ new Map();
@@ -651,59 +438,57 @@ function computeGraphLayout(flowNodes, flowEdges) {
651
438
  }
652
439
  return positions;
653
440
  }
441
+ var ROLE_COLUMNS, FEEDBACK_COLUMN, LANE_HEIGHT, STAGGER;
442
+ var init_graphLayout = __esm({
443
+ "src/graphLayout.ts"() {
444
+ ROLE_COLUMNS = {
445
+ source: 0,
446
+ rule: 280,
447
+ target: 560
448
+ };
449
+ FEEDBACK_COLUMN = 720;
450
+ LANE_HEIGHT = 104;
451
+ STAGGER = 24;
452
+ ({
453
+ field: ROLE_COLUMNS.source,
454
+ rule: ROLE_COLUMNS.rule,
455
+ section: ROLE_COLUMNS.target,
456
+ feedback: FEEDBACK_COLUMN
457
+ });
458
+ }
459
+ });
654
460
 
655
461
  // src/graphNodeStyles.ts
656
- var GRAPH_NODE_CLASSES = {
657
- field: "border-dhis2-blue-500",
658
- rule: "border-dhis2-teal-600 bg-dhis2-teal-050",
659
- section: "border-dhis2-grey-600",
660
- feedback: "border-dhis2-purple-500"
661
- };
662
- var LEGEND_SWATCH_CLASSES = {
663
- field: "bg-dhis2-blue-500",
664
- rule: "bg-dhis2-teal-600",
665
- section: "bg-dhis2-grey-600",
666
- feedback: "bg-dhis2-purple-500"
667
- };
668
462
  function getGraphNodeClassName(kind, highlighted) {
669
463
  return `p-dp8 rounded border bg-white min-w-28 max-w-[180px] text-xs shadow-[0_1px_2px_rgb(0_0_0/6%)] transition-opacity duration-150 ease-out cursor-grab active:cursor-grabbing ${GRAPH_NODE_CLASSES[kind]} ${highlighted ? "" : "opacity-35"}`;
670
464
  }
671
465
  function getLegendSwatchClassName(kind) {
672
466
  return `size-2.5 shrink-0 rounded-sm ${LEGEND_SWATCH_CLASSES[kind]}`;
673
467
  }
674
-
675
- // src/traceEntry.ts
676
- function resolveGraphTraceEntry(entries, selectedEntryId) {
677
- if (!entries.length) {
678
- return null;
679
- }
680
- if (selectedEntryId) {
681
- return entries.find((entry) => entry.id === selectedEntryId) ?? entries[entries.length - 1];
682
- }
683
- return entries[entries.length - 1];
684
- }
685
- function getActiveRuleIds(entries) {
686
- const latest = resolveGraphTraceEntry(entries);
687
- if (!latest) {
688
- return /* @__PURE__ */ new Set();
689
- }
690
- return new Set(latest.ruleResults.map((result) => result.ruleId));
691
- }
692
- var KIND_LABELS = {
693
- field: "Field",
694
- rule: "Rule",
695
- section: "Section",
696
- feedback: "Feedback"
697
- };
698
- var READ_LABEL_EDGE_THRESHOLD = 4;
699
- var DEFAULT_EDGE_OPTIONS = {
700
- type: "ruleGraphEdge",
701
- style: { stroke: getEffectEdgeStroke("default") },
702
- markerEnd: {
703
- type: react$1.MarkerType.ArrowClosed,
704
- color: getEffectEdgeStroke("default")
468
+ var GRAPH_NODE_CLASSES, LEGEND_SWATCH_CLASSES;
469
+ var init_graphNodeStyles = __esm({
470
+ "src/graphNodeStyles.ts"() {
471
+ GRAPH_NODE_CLASSES = {
472
+ field: "border-dhis2-blue-500",
473
+ rule: "border-dhis2-teal-600 bg-dhis2-teal-050",
474
+ section: "border-dhis2-grey-600",
475
+ feedback: "border-dhis2-purple-500"
476
+ };
477
+ LEGEND_SWATCH_CLASSES = {
478
+ field: "bg-dhis2-blue-500",
479
+ rule: "bg-dhis2-teal-600",
480
+ section: "bg-dhis2-grey-600",
481
+ feedback: "bg-dhis2-purple-500"
482
+ };
705
483
  }
706
- };
484
+ });
485
+
486
+ // src/RuleGraphView.tsx
487
+ var RuleGraphView_exports = {};
488
+ __export(RuleGraphView_exports, {
489
+ RuleGraphView: () => RuleGraphView,
490
+ toFlowGraph: () => toFlowGraph
491
+ });
707
492
  function RuleGraphNode({ data }) {
708
493
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: getGraphNodeClassName(data.kind, data.highlighted), children: [
709
494
  /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left }),
@@ -713,9 +498,6 @@ function RuleGraphNode({ data }) {
713
498
  /* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right })
714
499
  ] });
715
500
  }
716
- var nodeTypes = {
717
- ruleGraphNode: react.memo(RuleGraphNode)
718
- };
719
501
  function RuleGraphEdge({
720
502
  id,
721
503
  sourceX,
@@ -777,9 +559,6 @@ function RuleGraphEdge({
777
559
  ) }) : null
778
560
  ] });
779
561
  }
780
- var edgeTypes = {
781
- ruleGraphEdge: react.memo(RuleGraphEdge)
782
- };
783
562
  function effectNodeKey(effect) {
784
563
  const kind = effect.type === "HIDESECTION" ? "section" : effect.type === "DISPLAYTEXT" || effect.type === "DISPLAYKEYVALUEPAIR" ? "feedback" : "field";
785
564
  return `${kind}:${effect.targetId}`;
@@ -809,15 +588,6 @@ function ruleGraphKeys(entry, ruleId) {
809
588
  }
810
589
  return keys;
811
590
  }
812
- var formatDisplayValue = (value) => {
813
- if (value === void 0 || value === null || value === "") {
814
- return void 0;
815
- }
816
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
817
- return String(value);
818
- }
819
- return JSON.stringify(value);
820
- };
821
591
  function toFlowGraph(graph, fieldState, formValues, highlightedKeys) {
822
592
  const { nodes: flowNodes, edges: flowEdges } = prepareFlowGraph(graph);
823
593
  const positions = computeGraphLayout(flowNodes, flowEdges);
@@ -896,210 +666,1015 @@ function GraphToolbar({
896
666
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 border-b border-dhis2-grey-200 bg-white", children: [
897
667
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-dp8 px-dp12 py-dp8", children: [
898
668
  /* @__PURE__ */ jsxRuntime.jsx(
899
- "div",
669
+ "div",
670
+ {
671
+ className: "flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8",
672
+ "aria-hidden": "true",
673
+ children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
674
+ "span",
675
+ {
676
+ className: "inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800",
677
+ children: [
678
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: getLegendSwatchClassName(item.kind) }),
679
+ item.label
680
+ ]
681
+ },
682
+ item.kind
683
+ ))
684
+ }
685
+ ),
686
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
687
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs tabular-nums text-dhis2-grey-600", children: translate("{{nodes}} nodes \xB7 {{edges}} edges", {
688
+ nodes: nodeCount,
689
+ edges: edgeCount
690
+ }) }),
691
+ headerActions
692
+ ] })
693
+ ] }),
694
+ /* @__PURE__ */ jsxRuntime.jsx(EffectLegend, { activeVariants: activeEffectVariants })
695
+ ] });
696
+ }
697
+ function FitViewOnChange({
698
+ layoutKey,
699
+ nodeCount,
700
+ edgeCount
701
+ }) {
702
+ const { fitView } = react$1.useReactFlow();
703
+ react.useEffect(() => {
704
+ const frameId = requestAnimationFrame(() => {
705
+ void fitView({ padding: 0.2, duration: 150 });
706
+ });
707
+ return () => {
708
+ cancelAnimationFrame(frameId);
709
+ };
710
+ }, [edgeCount, fitView, layoutKey, nodeCount]);
711
+ return null;
712
+ }
713
+ function mergeNodePositions(next, current) {
714
+ const currentById = new Map(current.map((node) => [node.id, node]));
715
+ return next.map((node) => {
716
+ const existing = currentById.get(node.id);
717
+ if (existing) {
718
+ return { ...node, position: existing.position };
719
+ }
720
+ return node;
721
+ });
722
+ }
723
+ function RuleGraphCanvas({
724
+ nodes: layoutNodes,
725
+ edges: layoutEdges,
726
+ layoutKey
727
+ }) {
728
+ const [nodes, setNodes, onNodesChange] = react$1.useNodesState(layoutNodes);
729
+ const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(layoutEdges);
730
+ react.useEffect(() => {
731
+ setNodes((current) => mergeNodePositions(layoutNodes, current));
732
+ }, [layoutNodes, setNodes]);
733
+ react.useEffect(() => {
734
+ setEdges(layoutEdges);
735
+ }, [layoutEdges, setEdges]);
736
+ return /* @__PURE__ */ jsxRuntime.jsxs(
737
+ react$1.ReactFlow,
738
+ {
739
+ nodes,
740
+ edges,
741
+ nodeTypes,
742
+ edgeTypes,
743
+ onNodesChange,
744
+ onEdgesChange,
745
+ nodesDraggable: true,
746
+ defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
747
+ elevateEdgesOnSelect: true,
748
+ fitView: true,
749
+ proOptions: { hideAttribution: true },
750
+ className: "h-full w-full",
751
+ children: [
752
+ /* @__PURE__ */ jsxRuntime.jsx(
753
+ FitViewOnChange,
754
+ {
755
+ layoutKey,
756
+ nodeCount: nodes.length,
757
+ edgeCount: edges.length
758
+ }
759
+ ),
760
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 16, size: 1 }),
761
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false })
762
+ ]
763
+ }
764
+ );
765
+ }
766
+ function RuleGraphView({
767
+ entries,
768
+ fieldState,
769
+ formValues,
770
+ selectedEntryId,
771
+ highlightRuleId,
772
+ labelLookup,
773
+ className,
774
+ layoutKey,
775
+ headerActions,
776
+ minHeightClassName = "min-h-[360px]"
777
+ }) {
778
+ const graphEntry = react.useMemo(
779
+ () => resolveGraphTraceEntry(entries, selectedEntryId),
780
+ [entries, selectedEntryId]
781
+ );
782
+ const graph = react.useMemo(
783
+ () => graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },
784
+ [graphEntry, labelLookup]
785
+ );
786
+ const highlightedKeys = react.useMemo(() => {
787
+ if (!graphEntry || !selectedEntryId && !highlightRuleId) {
788
+ return null;
789
+ }
790
+ const keys = /* @__PURE__ */ new Set();
791
+ if (selectedEntryId) {
792
+ for (const key of entryGraphKeys(graphEntry)) {
793
+ keys.add(key);
794
+ }
795
+ }
796
+ if (highlightRuleId) {
797
+ for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {
798
+ keys.add(key);
799
+ }
800
+ }
801
+ return keys;
802
+ }, [graphEntry, highlightRuleId, selectedEntryId]);
803
+ const { nodes, edges } = react.useMemo(
804
+ () => toFlowGraph(graph, fieldState, formValues, highlightedKeys),
805
+ [graph, fieldState, formValues, highlightedKeys]
806
+ );
807
+ const activeEffectVariants = react.useMemo(() => {
808
+ const variants = /* @__PURE__ */ new Set();
809
+ for (const edge of graph.edges) {
810
+ if (edge.effectType) {
811
+ variants.add(getEffectVariant(edge.effectType));
812
+ }
813
+ }
814
+ return variants;
815
+ }, [graph.edges]);
816
+ if (!graph.nodes.length) {
817
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.NoticeBox, { title: translate("No rule relationships yet"), children: translate(
818
+ "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)."
819
+ ) }) });
820
+ }
821
+ return /* @__PURE__ */ jsxRuntime.jsxs(
822
+ "div",
823
+ {
824
+ className: `flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ""}`,
825
+ children: [
826
+ /* @__PURE__ */ jsxRuntime.jsx(
827
+ GraphToolbar,
828
+ {
829
+ nodeCount: graph.nodes.length,
830
+ edgeCount: graph.edges.length,
831
+ headerActions,
832
+ activeEffectVariants
833
+ }
834
+ ),
835
+ /* @__PURE__ */ jsxRuntime.jsx(
836
+ "div",
837
+ {
838
+ className: `${minHeightClassName} flex-1 bg-white`,
839
+ style: { width: "100%", height: "100%" },
840
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(RuleGraphCanvas, { nodes, edges, layoutKey }) })
841
+ }
842
+ )
843
+ ]
844
+ }
845
+ );
846
+ }
847
+ var KIND_LABELS, READ_LABEL_EDGE_THRESHOLD, DEFAULT_EDGE_OPTIONS, nodeTypes, edgeTypes, formatDisplayValue;
848
+ var init_RuleGraphView = __esm({
849
+ "src/RuleGraphView.tsx"() {
850
+ init_buildGraph();
851
+ init_graphLayout();
852
+ init_EffectBadge();
853
+ init_effectStyles();
854
+ init_graphNodeStyles();
855
+ init_i18n();
856
+ init_traceEntry();
857
+ KIND_LABELS = {
858
+ field: "Field",
859
+ rule: "Rule",
860
+ section: "Section",
861
+ feedback: "Feedback"
862
+ };
863
+ READ_LABEL_EDGE_THRESHOLD = 4;
864
+ DEFAULT_EDGE_OPTIONS = {
865
+ type: "ruleGraphEdge",
866
+ style: { stroke: getEffectEdgeStroke("default") },
867
+ markerEnd: {
868
+ type: react$1.MarkerType.ArrowClosed,
869
+ color: getEffectEdgeStroke("default")
870
+ }
871
+ };
872
+ nodeTypes = {
873
+ ruleGraphNode: react.memo(RuleGraphNode)
874
+ };
875
+ edgeTypes = {
876
+ ruleGraphEdge: react.memo(RuleGraphEdge)
877
+ };
878
+ formatDisplayValue = (value) => {
879
+ if (value === void 0 || value === null || value === "") {
880
+ return void 0;
881
+ }
882
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
883
+ return String(value);
884
+ }
885
+ return JSON.stringify(value);
886
+ };
887
+ }
888
+ });
889
+
890
+ // src/RuleGraphModal.tsx
891
+ var RuleGraphModal_exports = {};
892
+ __export(RuleGraphModal_exports, {
893
+ RuleGraphModal: () => RuleGraphModal
894
+ });
895
+ function RuleGraphModal({
896
+ open,
897
+ onClose,
898
+ subtitle,
899
+ layoutKey,
900
+ ...graphProps
901
+ }) {
902
+ if (!open) {
903
+ return null;
904
+ }
905
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Modal, { large: true, fluid: true, onClose, children: [
906
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalTitle, { children: [
907
+ translate("Rule dependency graph"),
908
+ subtitle ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-dp4 block text-sm font-normal text-dhis2-grey-700", children: subtitle }) : null
909
+ ] }),
910
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[min(85vh,900px)] w-full min-w-[min(1200px,92vw)]", children: /* @__PURE__ */ jsxRuntime.jsx(
911
+ RuleGraphView,
912
+ {
913
+ ...graphProps,
914
+ layoutKey: layoutKey ?? "modal",
915
+ className: "h-full min-h-0 rounded-none border-0",
916
+ minHeightClassName: "min-h-0 h-full"
917
+ }
918
+ ) }) })
919
+ ] });
920
+ }
921
+ var init_RuleGraphModal = __esm({
922
+ "src/RuleGraphModal.tsx"() {
923
+ init_i18n();
924
+ init_RuleGraphView();
925
+ }
926
+ });
927
+
928
+ // src/RulesPanel.tsx
929
+ init_buildGraph();
930
+ var createLookup = (rules, fields, sections, stages) => ({
931
+ resolveRuleName: (id) => rules.get(id) ?? id,
932
+ resolveFieldName: (id) => fields.get(id) ?? id,
933
+ resolveSectionName: (id) => sections.get(id) ?? id,
934
+ resolveStageName: (id) => stages.get(id) ?? id
935
+ });
936
+ function buildEventLookup(source) {
937
+ const rules = /* @__PURE__ */ new Map();
938
+ const fields = /* @__PURE__ */ new Map();
939
+ const sections = /* @__PURE__ */ new Map();
940
+ const stages = /* @__PURE__ */ new Map();
941
+ for (const rule of source.metadata.programRules) {
942
+ if (!rule.id) {
943
+ continue;
944
+ }
945
+ rules.set(rule.id, rule.displayName ?? rule.id);
946
+ }
947
+ const stage = dhis2FormUtilsMetadata.selectProgramStage(
948
+ source.metadata,
949
+ source.programStageId
950
+ ) ?? {
951
+ id: source.programStageId,
952
+ programStageDataElements: [],
953
+ programStageSections: []
954
+ };
955
+ for (const psde of stage.programStageDataElements ?? []) {
956
+ const de = psde.dataElement;
957
+ if (!de?.id) {
958
+ continue;
959
+ }
960
+ fields.set(de.id, de.displayFormName ?? de.displayName ?? de.id);
961
+ }
962
+ for (const section of stage.programStageSections ?? []) {
963
+ sections.set(section.id, section.displayName ?? section.id);
964
+ }
965
+ for (const programStage of source.metadata.programStages) {
966
+ if (!programStage.id) {
967
+ continue;
968
+ }
969
+ stages.set(programStage.id, programStage.displayName ?? programStage.id);
970
+ }
971
+ return createLookup(rules, fields, sections, stages);
972
+ }
973
+ function buildTrackerLookup(source) {
974
+ const rules = /* @__PURE__ */ new Map();
975
+ const fields = /* @__PURE__ */ new Map();
976
+ const sections = /* @__PURE__ */ new Map();
977
+ const stages = /* @__PURE__ */ new Map();
978
+ for (const rule of source.metadata.programRules) {
979
+ if (!rule.id) {
980
+ continue;
981
+ }
982
+ rules.set(rule.id, rule.name ?? rule.id);
983
+ }
984
+ for (const ptea of source.metadata.programTrackedEntityAttributes) {
985
+ const tea = ptea.trackedEntityAttribute;
986
+ if (!tea.id) {
987
+ continue;
988
+ }
989
+ fields.set(tea.id, tea.formName ?? tea.displayName ?? tea.id);
990
+ }
991
+ for (const section of source.metadata.programSections ?? []) {
992
+ sections.set(section.id, section.displayName ?? section.id);
993
+ }
994
+ for (const programStage of source.programStages ?? []) {
995
+ if (!programStage.id) {
996
+ continue;
997
+ }
998
+ stages.set(programStage.id, programStage.displayName ?? programStage.id);
999
+ }
1000
+ return createLookup(rules, fields, sections, stages);
1001
+ }
1002
+ function createLabelLookup(source) {
1003
+ return source.formKind === "event" ? buildEventLookup(source) : buildTrackerLookup(source);
1004
+ }
1005
+
1006
+ // src/RulesPanel.tsx
1007
+ init_EffectBadge();
1008
+
1009
+ // src/formatAgo.ts
1010
+ init_i18n();
1011
+ function formatAgo(timestamp, now = Date.now()) {
1012
+ const diffMs = Math.max(0, now - timestamp);
1013
+ if (diffMs < 5e3) {
1014
+ return translate("just now");
1015
+ }
1016
+ const seconds = Math.floor(diffMs / 1e3);
1017
+ if (seconds < 60) {
1018
+ return translate("{{seconds}}s ago", { seconds });
1019
+ }
1020
+ const minutes = Math.floor(seconds / 60);
1021
+ if (minutes < 60) {
1022
+ return translate("{{minutes}}m ago", { minutes });
1023
+ }
1024
+ const hours = Math.floor(minutes / 60);
1025
+ if (hours < 24) {
1026
+ return translate("{{hours}}h ago", { hours });
1027
+ }
1028
+ return new Date(timestamp).toLocaleString();
1029
+ }
1030
+
1031
+ // src/formatRuleActionSummary.ts
1032
+ function resolveFieldLabel(id, displayName, labelLookup) {
1033
+ if (labelLookup) {
1034
+ const resolved = labelLookup.resolveFieldName(id);
1035
+ if (resolved !== id) {
1036
+ return resolved;
1037
+ }
1038
+ }
1039
+ return displayName ?? id;
1040
+ }
1041
+ function resolveSectionLabel(id, displayName, labelLookup) {
1042
+ if (labelLookup) {
1043
+ const resolved = labelLookup.resolveSectionName(id);
1044
+ if (resolved !== id) {
1045
+ return resolved;
1046
+ }
1047
+ }
1048
+ return displayName ?? id;
1049
+ }
1050
+ function formatRuleActionSummary(action, labelLookup) {
1051
+ const type = action.programRuleActionType ?? "UNKNOWN";
1052
+ let targetLabel = "";
1053
+ if (action.dataElement?.id) {
1054
+ targetLabel = resolveFieldLabel(
1055
+ action.dataElement.id,
1056
+ action.dataElement.displayName,
1057
+ labelLookup
1058
+ );
1059
+ } else if (action.trackedEntityAttribute?.id) {
1060
+ targetLabel = resolveFieldLabel(
1061
+ action.trackedEntityAttribute.id,
1062
+ action.trackedEntityAttribute.displayName,
1063
+ labelLookup
1064
+ );
1065
+ } else if (action.programStageSection?.id) {
1066
+ targetLabel = resolveSectionLabel(
1067
+ action.programStageSection.id,
1068
+ action.programStageSection.displayName,
1069
+ labelLookup
1070
+ );
1071
+ } else if (action.programSection?.id) {
1072
+ targetLabel = resolveSectionLabel(
1073
+ action.programSection.id,
1074
+ action.programSection.displayName,
1075
+ labelLookup
1076
+ );
1077
+ } else if (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR") {
1078
+ targetLabel = action.location ?? "feedback";
1079
+ }
1080
+ let detail;
1081
+ if (action.data) {
1082
+ detail = action.data;
1083
+ }
1084
+ if (action.content && (type === "DISPLAYTEXT" || type === "DISPLAYKEYVALUEPAIR")) {
1085
+ detail = action.content;
1086
+ }
1087
+ return { type, targetLabel, detail };
1088
+ }
1089
+
1090
+ // src/RulesPanel.tsx
1091
+ init_i18n();
1092
+
1093
+ // src/resolveProgramRulesList.ts
1094
+ function sortByPriority(rules) {
1095
+ return [...rules].sort((left, right) => (left.priority ?? 0) - (right.priority ?? 0));
1096
+ }
1097
+ function toCatalogRule(rule) {
1098
+ return {
1099
+ id: rule.id,
1100
+ name: rule.name,
1101
+ condition: rule.condition,
1102
+ priority: rule.priority,
1103
+ programRuleActions: [...rule.programRuleActions ?? []],
1104
+ programStageId: rule.programStageId
1105
+ };
1106
+ }
1107
+ function resolveProgramRulesList(metadata) {
1108
+ if (metadata.formKind === "event") {
1109
+ return sortByPriority(
1110
+ metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
1111
+ (rule) => toCatalogRule({
1112
+ id: rule.id,
1113
+ name: rule.displayName ?? rule.id,
1114
+ condition: rule.condition,
1115
+ priority: rule.priority,
1116
+ programRuleActions: rule.programRuleActions,
1117
+ programStageId: rule.programStage?.id ?? null
1118
+ })
1119
+ )
1120
+ );
1121
+ }
1122
+ return sortByPriority(
1123
+ metadata.metadata.programRules.filter((rule) => Boolean(rule.id)).map(
1124
+ (rule) => toCatalogRule({
1125
+ id: rule.id,
1126
+ name: rule.name ?? rule.id,
1127
+ condition: rule.condition,
1128
+ priority: rule.priority,
1129
+ programRuleActions: rule.programRuleActions,
1130
+ programStageId: rule.programStage?.id ?? null
1131
+ })
1132
+ )
1133
+ );
1134
+ }
1135
+
1136
+ // src/constants.ts
1137
+ var DEFAULT_TRACE_MAX_ENTRIES = 200;
1138
+
1139
+ // src/traceStore.ts
1140
+ function createRuleTraceStore(maxEntries = DEFAULT_TRACE_MAX_ENTRIES) {
1141
+ let entries = [];
1142
+ const listeners = /* @__PURE__ */ new Set();
1143
+ let disposeTraceSubscription = null;
1144
+ const notify = () => {
1145
+ for (const listener of listeners) {
1146
+ listener();
1147
+ }
1148
+ };
1149
+ return {
1150
+ record(entry) {
1151
+ entries = [...entries, entry].slice(-maxEntries);
1152
+ notify();
1153
+ },
1154
+ getSnapshot() {
1155
+ return entries;
1156
+ },
1157
+ subscribe(listener) {
1158
+ listeners.add(listener);
1159
+ return () => {
1160
+ listeners.delete(listener);
1161
+ };
1162
+ },
1163
+ dispose() {
1164
+ disposeTraceSubscription?.();
1165
+ disposeTraceSubscription = null;
1166
+ listeners.clear();
1167
+ },
1168
+ bindTraceSubscription(unsubscribe) {
1169
+ disposeTraceSubscription = unsubscribe;
1170
+ }
1171
+ };
1172
+ }
1173
+
1174
+ // src/attach.ts
1175
+ function attachRuleDevtools(formStore, options) {
1176
+ const store = createRuleTraceStore(
1177
+ DEFAULT_TRACE_MAX_ENTRIES
1178
+ );
1179
+ store.bindTraceSubscription(formStore.subscribeTrace(store.record));
1180
+ return store;
1181
+ }
1182
+ var RuleTraceContext = react.createContext(null);
1183
+ function RuleDevtoolsScope({ formStore, children }) {
1184
+ const traceStore = react.useMemo(() => attachRuleDevtools(formStore), [formStore]);
1185
+ react.useEffect(() => {
1186
+ return () => {
1187
+ traceStore.dispose();
1188
+ };
1189
+ }, [traceStore]);
1190
+ return /* @__PURE__ */ jsxRuntime.jsx(RuleTraceContext.Provider, { value: traceStore, children });
1191
+ }
1192
+ function useRuleTraceStore() {
1193
+ const store = react.useContext(RuleTraceContext);
1194
+ if (!store) {
1195
+ throw new Error("useRuleTraceStore must be used within RuleDevtoolsScope");
1196
+ }
1197
+ return store;
1198
+ }
1199
+
1200
+ // src/RuleDetailsModal.tsx
1201
+ init_effectStyles();
1202
+ init_i18n();
1203
+
1204
+ // src/programRuleDetailQuery.ts
1205
+ var PROGRAM_RULE_ACTION_DETAIL_FIELDS = [
1206
+ "id",
1207
+ "programRuleActionType",
1208
+ "priority",
1209
+ "content",
1210
+ "data",
1211
+ "location",
1212
+ "dataElement[id,displayName]",
1213
+ "trackedEntityAttribute[id,displayName]",
1214
+ "option[id,displayName]",
1215
+ "optionGroup[id,displayName]",
1216
+ "programStageSection[id,displayName]",
1217
+ "programStage[id,displayName]",
1218
+ "programSection[id,displayName]"
1219
+ ].join(",");
1220
+ var PROGRAM_RULE_DETAIL_FIELDS = [
1221
+ "id",
1222
+ "name",
1223
+ "displayName",
1224
+ "code",
1225
+ "description",
1226
+ "condition",
1227
+ "priority",
1228
+ "lastUpdated",
1229
+ "lastUpdatedBy[displayName]",
1230
+ "program[id,displayName]",
1231
+ "programStage[id,displayName]",
1232
+ `programRuleActions[${PROGRAM_RULE_ACTION_DETAIL_FIELDS}]`
1233
+ ].join(",");
1234
+ var programRuleDetailQuery = {
1235
+ programRule: {
1236
+ resource: "programRules",
1237
+ id: (variables) => variables.ruleId,
1238
+ params: {
1239
+ fields: PROGRAM_RULE_DETAIL_FIELDS
1240
+ }
1241
+ }
1242
+ };
1243
+
1244
+ // src/useProgramRuleDetail.ts
1245
+ function isCurrentDetail(ruleId, detail) {
1246
+ return ruleId != null && detail?.id === ruleId;
1247
+ }
1248
+ function resolveDetailState(ruleId, detail, hasError) {
1249
+ const isCurrent = isCurrentDetail(ruleId, detail);
1250
+ return {
1251
+ detail: isCurrent ? detail : void 0,
1252
+ loading: ruleId != null && !isCurrent && !hasError
1253
+ };
1254
+ }
1255
+ function useProgramRuleDetail(ruleId) {
1256
+ const { data, error, refetch } = appRuntime.useDataQuery(
1257
+ programRuleDetailQuery,
1258
+ { lazy: true }
1259
+ );
1260
+ react.useEffect(() => {
1261
+ if (ruleId) {
1262
+ void refetch({ ruleId });
1263
+ }
1264
+ }, [ruleId, refetch]);
1265
+ return { ...resolveDetailState(ruleId, data?.programRule, Boolean(error)), error };
1266
+ }
1267
+ var EM_DASH = "\u2014";
1268
+ function formatTimestamp(value) {
1269
+ if (!value) {
1270
+ return null;
1271
+ }
1272
+ const date = new Date(value);
1273
+ if (Number.isNaN(date.getTime())) {
1274
+ return null;
1275
+ }
1276
+ const day = String(date.getDate());
1277
+ const month = date.toLocaleString("en-GB", { month: "short" });
1278
+ const year = String(date.getFullYear());
1279
+ const hours = String(date.getHours()).padStart(2, "0");
1280
+ const minutes = String(date.getMinutes()).padStart(2, "0");
1281
+ return `${day} ${month} ${year} ${hours}:${minutes}`;
1282
+ }
1283
+ function resolveStatusChip(status) {
1284
+ switch (status) {
1285
+ case "firing":
1286
+ return {
1287
+ label: translate("Firing"),
1288
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
1289
+ };
1290
+ case "idle":
1291
+ return { label: translate("Idle"), className: "bg-dhis2-grey-200 text-dhis2-grey-700" };
1292
+ case "out-of-scope":
1293
+ return {
1294
+ label: translate("Out of scope"),
1295
+ className: "bg-dhis2-grey-200 text-dhis2-grey-600"
1296
+ };
1297
+ }
1298
+ }
1299
+ var VARIABLE_PATTERNS = [
1300
+ {
1301
+ pattern: /#\{[^}]*\}/g,
1302
+ kind: "Data element",
1303
+ className: "bg-dhis2-blue-100 text-dhis2-blue-900"
1304
+ },
1305
+ {
1306
+ pattern: /A\{[^}]*\}/g,
1307
+ kind: "Tracked entity attribute",
1308
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
1309
+ },
1310
+ {
1311
+ pattern: /V\{[^}]*\}/g,
1312
+ kind: "Environment variable",
1313
+ className: "bg-dhis2-grey-200 text-dhis2-grey-900"
1314
+ },
1315
+ {
1316
+ pattern: /d2:\w+(?=\()/g,
1317
+ kind: "Function",
1318
+ className: "bg-dhis2-yellow-100 text-dhis2-yellow-900"
1319
+ }
1320
+ ];
1321
+ var RESOLVABLE_VARIABLE_KINDS = /* @__PURE__ */ new Set([
1322
+ "Data element",
1323
+ "Tracked entity attribute"
1324
+ ]);
1325
+ function resolveVariableDisplayName(name, programRuleVariables) {
1326
+ const variable = programRuleVariables.find((candidate) => candidate.name === name);
1327
+ return variable?.dataElement?.displayName ?? variable?.trackedEntityAttribute?.displayName;
1328
+ }
1329
+ function resolveVariableToken(token, kind, programRuleVariables) {
1330
+ if (kind === "Function") {
1331
+ return `${token}()`;
1332
+ }
1333
+ if (!RESOLVABLE_VARIABLE_KINDS.has(kind)) {
1334
+ return token;
1335
+ }
1336
+ return resolveVariableDisplayName(token.slice(2, -1), programRuleVariables) ?? token;
1337
+ }
1338
+ function matchDistinctTokens(condition, pattern, seen) {
1339
+ const matches = condition.match(pattern) ?? [];
1340
+ const distinct = matches.filter((token) => !seen.has(token));
1341
+ for (const token of distinct) {
1342
+ seen.add(token);
1343
+ }
1344
+ return distinct;
1345
+ }
1346
+ function parseConditionVariables(condition, programRuleVariables) {
1347
+ if (!condition) {
1348
+ return [];
1349
+ }
1350
+ const seen = /* @__PURE__ */ new Set();
1351
+ return VARIABLE_PATTERNS.flatMap(
1352
+ ({ pattern, kind, className }) => matchDistinctTokens(condition, pattern, seen).map((token) => ({
1353
+ token,
1354
+ kind,
1355
+ label: resolveVariableToken(token, kind, programRuleVariables),
1356
+ className
1357
+ }))
1358
+ );
1359
+ }
1360
+ var ACTION_TARGET_RESOLVERS = [
1361
+ { label: "Data element", getRef: (action) => action.dataElement },
1362
+ { label: "Tracked entity attribute", getRef: (action) => action.trackedEntityAttribute },
1363
+ { label: "Program stage section", getRef: (action) => action.programStageSection },
1364
+ { label: "Program stage", getRef: (action) => action.programStage },
1365
+ { label: "Option", getRef: (action) => action.option },
1366
+ { label: "Option group", getRef: (action) => action.optionGroup }
1367
+ ];
1368
+ function formatActionTargetValue(ref) {
1369
+ return `${ref.displayName ?? ref.id} \xB7 ${ref.id}`;
1370
+ }
1371
+ function resolveActionTarget(action) {
1372
+ for (const { label, getRef } of ACTION_TARGET_RESOLVERS) {
1373
+ const ref = getRef(action);
1374
+ if (ref?.id) {
1375
+ return { label: translate(label), value: formatActionTargetValue(ref) };
1376
+ }
1377
+ }
1378
+ return null;
1379
+ }
1380
+ function BasicDetailCell({ label, value, mono }) {
1381
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
1382
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-xs text-dhis2-grey-600", children: label }),
1383
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: `m-0 mt-[2px] text-sm text-dhis2-grey-900 ${mono ? "font-mono" : ""}`, children: value })
1384
+ ] });
1385
+ }
1386
+ var FEEDBACK_ACTION_TYPES = /* @__PURE__ */ new Set(["DISPLAYTEXT", "DISPLAYKEYVALUEPAIR"]);
1387
+ function resolveDataRow(action) {
1388
+ return action.data ? { label: translate("Data (expression)"), value: action.data, mono: true } : null;
1389
+ }
1390
+ function resolveContentRow(action) {
1391
+ const showContent = Boolean(action.content) && FEEDBACK_ACTION_TYPES.has(action.programRuleActionType);
1392
+ return showContent ? { label: translate("Content (static text)"), value: action.content ?? "" } : null;
1393
+ }
1394
+ function resolveLocationRow(action) {
1395
+ return action.location ? { label: translate("Location"), value: action.location, mono: true } : null;
1396
+ }
1397
+ function resolveActionRows(action) {
1398
+ const rows = [
1399
+ resolveActionTarget(action),
1400
+ resolveDataRow(action),
1401
+ resolveContentRow(action),
1402
+ resolveLocationRow(action)
1403
+ ];
1404
+ return rows.filter((row) => row !== null);
1405
+ }
1406
+ function ActionCard({ action, index }) {
1407
+ const type = action.programRuleActionType;
1408
+ const visual = getEffectVisual(type);
1409
+ const Icon = exports.EFFECT_ICONS[visual.variant];
1410
+ const rows = resolveActionRows(action);
1411
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1412
+ "div",
1413
+ {
1414
+ 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%)]",
1415
+ style: { borderInlineStartWidth: 3, borderInlineStartColor: visual.edgeStroke },
1416
+ children: [
1417
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-dp12 flex items-center justify-between gap-dp8", children: [
1418
+ /* @__PURE__ */ jsxRuntime.jsxs(
1419
+ "span",
1420
+ {
1421
+ className: `inline-flex items-center gap-[4px] rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${visual.tagClassName}`,
1422
+ children: [
1423
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, {}),
1424
+ type
1425
+ ]
1426
+ }
1427
+ ),
1428
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] uppercase tracking-wide text-dhis2-grey-600", children: translate("Action {{n}}", { n: index + 1 }) })
1429
+ ] }),
1430
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-dp10", children: rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[172px_minmax(0,1fr)] gap-dp10", children: [
1431
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-dhis2-grey-600", children: row.label }),
1432
+ /* @__PURE__ */ jsxRuntime.jsx(
1433
+ "span",
1434
+ {
1435
+ className: `min-w-0 break-words text-sm ${row.mono ? "font-mono text-[12px]" : ""}`,
1436
+ children: row.value
1437
+ }
1438
+ )
1439
+ ] }, row.label)) })
1440
+ ]
1441
+ }
1442
+ );
1443
+ }
1444
+ function orDash(value) {
1445
+ return value ?? EM_DASH;
1446
+ }
1447
+ function resolveDetailDisplayName(rule) {
1448
+ return rule?.displayName;
1449
+ }
1450
+ function resolveDetailName(rule) {
1451
+ return rule?.name;
1452
+ }
1453
+ function resolveRuleDisplayName(rule, fallback) {
1454
+ const displayName = resolveDetailDisplayName(rule) ?? resolveDetailName(rule);
1455
+ return displayName ?? fallback;
1456
+ }
1457
+ function resolveRuleStageName(rule) {
1458
+ return rule?.programStage?.displayName;
1459
+ }
1460
+ function resolveStageName(rule, programStageName) {
1461
+ if (programStageName === null) {
1462
+ return translate("All stages (registration)");
1463
+ }
1464
+ return orDash(resolveRuleStageName(rule) ?? programStageName);
1465
+ }
1466
+ function resolveLastUpdatedByName(rule) {
1467
+ return rule?.lastUpdatedBy?.displayName;
1468
+ }
1469
+ function resolveLastUpdatedValue(rule) {
1470
+ const label = formatTimestamp(rule?.lastUpdated);
1471
+ if (!label) {
1472
+ return EM_DASH;
1473
+ }
1474
+ const by = resolveLastUpdatedByName(rule);
1475
+ return by ? `${label} \xB7 ${by}` : label;
1476
+ }
1477
+ function resolvePriorityCellValue(rule) {
1478
+ return rule?.priority != null ? String(rule.priority) : EM_DASH;
1479
+ }
1480
+ function resolveActionsCellValue(rule) {
1481
+ return translate("{{n}} action(s)", { n: rule?.programRuleActions?.length ?? 0 });
1482
+ }
1483
+ function resolveBasicDetailCells(rule, ruleName, programStageName) {
1484
+ return [
1485
+ { label: translate("Name"), value: resolveRuleDisplayName(rule, ruleName) },
1486
+ { label: translate("Code"), value: orDash(rule?.code), mono: true },
1487
+ { label: translate("Identifier"), value: orDash(rule?.id), mono: true },
1488
+ { label: translate("Program"), value: orDash(rule?.program?.displayName) },
1489
+ { label: translate("Program stage"), value: resolveStageName(rule, programStageName) },
1490
+ { label: translate("Priority"), value: resolvePriorityCellValue(rule) },
1491
+ { label: translate("Actions"), value: resolveActionsCellValue(rule) },
1492
+ { label: translate("Last updated"), value: resolveLastUpdatedValue(rule) }
1493
+ ];
1494
+ }
1495
+ function BasicDetails({
1496
+ rule,
1497
+ ruleName,
1498
+ programStageName
1499
+ }) {
1500
+ const cells = resolveBasicDetailCells(rule, ruleName, programStageName);
1501
+ return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(BasicDetailCell, { ...cell }, cell.label)) });
1502
+ }
1503
+ var SECTION_HEADING_CLASS = "m-0 mb-dp8 text-[13px] font-bold uppercase tracking-wide text-dhis2-grey-700";
1504
+ function RuleDetailsHeader({
1505
+ title,
1506
+ chip,
1507
+ description
1508
+ }) {
1509
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "-mx-dp24 -mt-dp24 mb-0 border-b border-dhis2-grey-300 px-dp24 pb-dp16 pe-[44px] pt-[20px]", children: [
1510
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-[11px] font-bold uppercase tracking-[.09em] text-dhis2-grey-600", children: translate("Program rule") }),
1511
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-[6px] flex flex-wrap items-center gap-dp8", children: [
1512
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 text-xl font-medium leading-[1.3] text-dhis2-grey-900", children: title }),
1513
+ /* @__PURE__ */ jsxRuntime.jsx(
1514
+ "span",
900
1515
  {
901
- className: "flex min-w-0 flex-wrap items-center gap-x-dp12 gap-y-dp8",
902
- "aria-hidden": "true",
903
- children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
904
- "span",
905
- {
906
- className: "inline-flex items-center gap-dp8 text-xs text-dhis2-grey-800",
907
- children: [
908
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: getLegendSwatchClassName(item.kind) }),
909
- item.label
910
- ]
911
- },
912
- item.kind
913
- ))
1516
+ className: `rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${chip.className}`,
1517
+ children: chip.label
914
1518
  }
915
- ),
916
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-dp8", children: [
917
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs tabular-nums text-dhis2-grey-600", children: translate("{{nodes}} nodes \xB7 {{edges}} edges", {
918
- nodes: nodeCount,
919
- edges: edgeCount
920
- }) }),
921
- headerActions
922
- ] })
1519
+ )
923
1520
  ] }),
924
- /* @__PURE__ */ jsxRuntime.jsx(EffectLegend, { activeVariants: activeEffectVariants })
1521
+ description ? /* @__PURE__ */ jsxRuntime.jsx(
1522
+ "p",
1523
+ {
1524
+ className: "m-0 mt-[6px] text-sm text-dhis2-grey-700",
1525
+ style: { textWrap: "pretty" },
1526
+ children: description
1527
+ }
1528
+ ) : null
925
1529
  ] });
926
1530
  }
927
- function FitViewOnChange({
928
- layoutKey,
929
- nodeCount,
930
- edgeCount
1531
+ function ConditionSection({
1532
+ condition,
1533
+ variables
931
1534
  }) {
932
- const { fitView } = react$1.useReactFlow();
933
- react.useEffect(() => {
934
- const frameId = requestAnimationFrame(() => {
935
- void fitView({ padding: 0.2, duration: 150 });
936
- });
937
- return () => {
938
- cancelAnimationFrame(frameId);
939
- };
940
- }, [edgeCount, fitView, layoutKey, nodeCount]);
941
- return null;
1535
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
1536
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Condition") }),
1537
+ /* @__PURE__ */ jsxRuntime.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 }),
1538
+ variables.length ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-dp12", children: [
1539
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 mb-dp8 text-xs text-dhis2-grey-600", children: translate("Variables referenced") }),
1540
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-dp8", children: variables.map((variable) => /* @__PURE__ */ jsxRuntime.jsxs(
1541
+ "span",
1542
+ {
1543
+ className: `inline-flex items-center gap-[4px] rounded-full px-dp8 py-[2px] text-xs ${variable.className}`,
1544
+ children: [
1545
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-medium", children: variable.label }),
1546
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-70", children: translate(variable.kind) })
1547
+ ]
1548
+ },
1549
+ variable.token
1550
+ )) })
1551
+ ] }) : null
1552
+ ] });
942
1553
  }
943
- function mergeNodePositions(next, current) {
944
- const currentById = new Map(current.map((node) => [node.id, node]));
945
- return next.map((node) => {
946
- const existing = currentById.get(node.id);
947
- if (existing) {
948
- return { ...node, position: existing.position };
949
- }
950
- return node;
951
- });
1554
+ function ActionsSection({ actions }) {
1555
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
1556
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Actions") }),
1557
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-dp12", children: actions.map((action, index) => /* @__PURE__ */ jsxRuntime.jsx(ActionCard, { action, index }, action.id)) })
1558
+ ] });
952
1559
  }
953
- function RuleGraphCanvas({
954
- nodes: layoutNodes,
955
- edges: layoutEdges,
956
- layoutKey
1560
+ function RuleDetailsContent({
1561
+ detail,
1562
+ ruleName,
1563
+ programStageName,
1564
+ variables
957
1565
  }) {
958
- const [nodes, setNodes, onNodesChange] = react$1.useNodesState(layoutNodes);
959
- const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(layoutEdges);
960
- react.useEffect(() => {
961
- setNodes((current) => mergeNodePositions(layoutNodes, current));
962
- }, [layoutNodes, setNodes]);
963
- react.useEffect(() => {
964
- setEdges(layoutEdges);
965
- }, [layoutEdges, setEdges]);
966
- return /* @__PURE__ */ jsxRuntime.jsxs(
967
- react$1.ReactFlow,
968
- {
969
- nodes,
970
- edges,
971
- nodeTypes,
972
- edgeTypes,
973
- onNodesChange,
974
- onEdgesChange,
975
- nodesDraggable: true,
976
- defaultEdgeOptions: DEFAULT_EDGE_OPTIONS,
977
- elevateEdgesOnSelect: true,
978
- fitView: true,
979
- proOptions: { hideAttribution: true },
980
- className: "h-full w-full",
981
- children: [
982
- /* @__PURE__ */ jsxRuntime.jsx(
983
- FitViewOnChange,
984
- {
985
- layoutKey,
986
- nodeCount: nodes.length,
987
- edgeCount: edges.length
988
- }
989
- ),
990
- /* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 16, size: 1 }),
991
- /* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false })
992
- ]
993
- }
994
- );
1566
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-dp24 pt-dp16", children: [
1567
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
1568
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Basic details") }),
1569
+ /* @__PURE__ */ jsxRuntime.jsx(
1570
+ BasicDetails,
1571
+ {
1572
+ rule: detail,
1573
+ ruleName,
1574
+ programStageName
1575
+ }
1576
+ )
1577
+ ] }),
1578
+ /* @__PURE__ */ jsxRuntime.jsx(ConditionSection, { condition: detail.condition, variables }),
1579
+ /* @__PURE__ */ jsxRuntime.jsx(ActionsSection, { actions: detail.programRuleActions ?? [] })
1580
+ ] });
995
1581
  }
996
- function RuleGraphView({
997
- entries,
998
- fieldState,
999
- formValues,
1000
- selectedEntryId,
1001
- highlightRuleId,
1002
- labelLookup,
1003
- className,
1004
- layoutKey,
1005
- headerActions,
1006
- minHeightClassName = "min-h-[360px]"
1582
+ function RuleDetailsBody({
1583
+ detail,
1584
+ loading,
1585
+ error,
1586
+ ruleName,
1587
+ programStageName,
1588
+ variables
1007
1589
  }) {
1008
- const graphEntry = react.useMemo(
1009
- () => resolveGraphTraceEntry(entries, selectedEntryId),
1010
- [entries, selectedEntryId]
1011
- );
1012
- const graph = react.useMemo(
1013
- () => graphEntry ? buildGraphFromTrace([graphEntry], labelLookup) : { nodes: [], edges: [] },
1014
- [graphEntry, labelLookup]
1015
- );
1016
- const highlightedKeys = react.useMemo(() => {
1017
- if (!graphEntry || !selectedEntryId && !highlightRuleId) {
1018
- return null;
1019
- }
1020
- const keys = /* @__PURE__ */ new Set();
1021
- if (selectedEntryId) {
1022
- for (const key of entryGraphKeys(graphEntry)) {
1023
- keys.add(key);
1024
- }
1025
- }
1026
- if (highlightRuleId) {
1027
- for (const key of ruleGraphKeys(graphEntry, highlightRuleId)) {
1028
- keys.add(key);
1029
- }
1590
+ if (loading) {
1591
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[280px] items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.CircularLoader, { small: true }) });
1592
+ }
1593
+ if (error) {
1594
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm text-dhis2-red-700", children: translate("Could not load this rule: {{message}}", { message: error.message }) });
1595
+ }
1596
+ if (!detail) {
1597
+ return null;
1598
+ }
1599
+ return /* @__PURE__ */ jsxRuntime.jsx(
1600
+ RuleDetailsContent,
1601
+ {
1602
+ detail,
1603
+ ruleName,
1604
+ programStageName,
1605
+ variables
1030
1606
  }
1031
- return keys;
1032
- }, [graphEntry, highlightRuleId, selectedEntryId]);
1033
- const { nodes, edges } = react.useMemo(
1034
- () => toFlowGraph(graph, fieldState, formValues, highlightedKeys),
1035
- [graph, fieldState, formValues, highlightedKeys]
1036
1607
  );
1037
- const activeEffectVariants = react.useMemo(() => {
1038
- const variants = /* @__PURE__ */ new Set();
1039
- for (const edge of graph.edges) {
1040
- if (edge.effectType) {
1041
- variants.add(getEffectVariant(edge.effectType));
1042
- }
1043
- }
1044
- return variants;
1045
- }, [graph.edges]);
1046
- if (!graph.nodes.length) {
1047
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-dp8", children: /* @__PURE__ */ jsxRuntime.jsx(ui.NoticeBox, { title: translate("No rule relationships yet"), children: translate(
1048
- "Interact with the form to build the dependency graph. Only rules active in the latest evaluation are shown. Select a trace entry to inspect a past evaluation. Connections flow Field \u2192 Rule \u2192 Target (read / effect)."
1049
- ) }) });
1050
- }
1608
+ }
1609
+ function RuleDetailsFooter({ onClose }) {
1051
1610
  return /* @__PURE__ */ jsxRuntime.jsxs(
1052
1611
  "div",
1053
1612
  {
1054
- className: `flex h-full min-h-[480px] flex-1 flex-col overflow-hidden rounded-md border border-dhis2-grey-200 bg-white ${className ?? ""}`,
1613
+ style: { order: 3 },
1614
+ 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]",
1055
1615
  children: [
1056
- /* @__PURE__ */ jsxRuntime.jsx(
1057
- GraphToolbar,
1058
- {
1059
- nodeCount: graph.nodes.length,
1060
- edgeCount: graph.edges.length,
1061
- headerActions,
1062
- activeEffectVariants
1063
- }
1064
- ),
1065
- /* @__PURE__ */ jsxRuntime.jsx(
1066
- "div",
1067
- {
1068
- className: `${minHeightClassName} flex-1 bg-white`,
1069
- style: { width: "100%", height: "100%" },
1070
- children: /* @__PURE__ */ jsxRuntime.jsx(react$1.ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(RuleGraphCanvas, { nodes, edges, layoutKey }) })
1071
- }
1072
- )
1616
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm text-dhis2-grey-600", children: translate("Read-only view. Edit rules in the Maintenance app.") }),
1617
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ButtonStrip, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { secondary: true, onClick: onClose, children: translate("Close") }) })
1073
1618
  ]
1074
1619
  }
1075
1620
  );
1076
1621
  }
1077
- function RuleGraphModal({
1622
+ function resolveActiveRuleId(open, ruleId) {
1623
+ return open ? ruleId : null;
1624
+ }
1625
+ function shouldRenderModal(open, ruleId) {
1626
+ return open && ruleId != null;
1627
+ }
1628
+ function RuleDetailsModal({
1078
1629
  open,
1079
1630
  onClose,
1080
- subtitle,
1081
- layoutKey,
1082
- ...graphProps
1631
+ ruleId,
1632
+ ruleName,
1633
+ status,
1634
+ programStageName,
1635
+ programRuleVariables
1083
1636
  }) {
1084
- if (!open) {
1637
+ const { detail, loading, error } = useProgramRuleDetail(resolveActiveRuleId(open, ruleId));
1638
+ if (!shouldRenderModal(open, ruleId)) {
1085
1639
  return null;
1086
1640
  }
1087
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Modal, { large: true, fluid: true, onClose, children: [
1088
- /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalTitle, { children: [
1089
- translate("Rule dependency graph"),
1090
- subtitle ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-dp4 block text-sm font-normal text-dhis2-grey-700", children: subtitle }) : null
1091
- ] }),
1092
- /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { className: "p-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[min(85vh,900px)] w-full min-w-[min(1200px,92vw)]", children: /* @__PURE__ */ jsxRuntime.jsx(
1093
- RuleGraphView,
1641
+ const chip = resolveStatusChip(status);
1642
+ const variables = parseConditionVariables(detail?.condition, programRuleVariables);
1643
+ const title = resolveRuleDisplayName(detail, ruleName);
1644
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Modal, { position: "middle", onClose, children: [
1645
+ /* @__PURE__ */ jsxRuntime.jsx(RuleDetailsHeader, { title, chip, description: detail?.description }),
1646
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { children: /* @__PURE__ */ jsxRuntime.jsx(
1647
+ RuleDetailsBody,
1094
1648
  {
1095
- ...graphProps,
1096
- layoutKey: layoutKey ?? "modal",
1097
- className: "h-full min-h-0 rounded-none border-0",
1098
- minHeightClassName: "min-h-0 h-full"
1649
+ detail,
1650
+ loading,
1651
+ error,
1652
+ ruleName,
1653
+ programStageName,
1654
+ variables
1099
1655
  }
1100
- ) }) })
1656
+ ) }),
1657
+ /* @__PURE__ */ jsxRuntime.jsx(RuleDetailsFooter, { onClose })
1101
1658
  ] });
1102
1659
  }
1660
+
1661
+ // src/RulesPanelGraphFallback.tsx
1662
+ init_i18n();
1663
+ function RulesPanelGraphFallback() {
1664
+ return /* @__PURE__ */ jsxRuntime.jsx(
1665
+ "div",
1666
+ {
1667
+ className: "flex min-h-[200px] items-center justify-center",
1668
+ role: "status",
1669
+ "aria-label": translate("Loading graph"),
1670
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.CircularLoader, { small: true })
1671
+ }
1672
+ );
1673
+ }
1674
+
1675
+ // src/TraceTimeline.tsx
1676
+ init_EffectBadge();
1677
+ init_i18n();
1103
1678
  function resolveLabel(id, resolver) {
1104
1679
  const label = resolver?.(id) ?? id;
1105
1680
  return { label, showId: label !== id };
@@ -1316,11 +1891,69 @@ function TraceTimeline({
1316
1891
  ) }, entry.id);
1317
1892
  }) });
1318
1893
  }
1319
- function resolveScopeStageId(metadata) {
1894
+
1895
+ // src/RulesPanel.tsx
1896
+ init_traceEntry();
1897
+ var REORDER_DURATION_MS = 220;
1898
+ var REORDER_EASING = "ease-out";
1899
+ function prefersReducedMotion() {
1900
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1901
+ }
1902
+ function useFlipReorder(orderedIds, containerRef) {
1903
+ const positionsRef = react.useRef(/* @__PURE__ */ new Map());
1904
+ react.useLayoutEffect(() => {
1905
+ const container = containerRef.current;
1906
+ if (!container) {
1907
+ return;
1908
+ }
1909
+ const previousPositions = positionsRef.current;
1910
+ const items = container.querySelectorAll("[data-rule-id]");
1911
+ if (!prefersReducedMotion()) {
1912
+ items.forEach((item) => {
1913
+ const id = item.dataset.ruleId;
1914
+ if (!id) {
1915
+ return;
1916
+ }
1917
+ const previousRect = previousPositions.get(id);
1918
+ if (!previousRect) {
1919
+ return;
1920
+ }
1921
+ const currentRect = item.getBoundingClientRect();
1922
+ const deltaY = previousRect.top - currentRect.top;
1923
+ if (deltaY === 0) {
1924
+ return;
1925
+ }
1926
+ item.style.transition = "none";
1927
+ item.style.transform = `translateY(${String(deltaY)}px)`;
1928
+ item.getBoundingClientRect();
1929
+ item.style.transition = `transform ${String(REORDER_DURATION_MS)}ms ${REORDER_EASING}`;
1930
+ item.style.transform = "";
1931
+ });
1932
+ }
1933
+ const nextPositions = /* @__PURE__ */ new Map();
1934
+ items.forEach((item) => {
1935
+ const id = item.dataset.ruleId;
1936
+ if (id) {
1937
+ nextPositions.set(id, item.getBoundingClientRect());
1938
+ }
1939
+ });
1940
+ positionsRef.current = nextPositions;
1941
+ }, [orderedIds.join("|")]);
1942
+ }
1943
+ var LazyRuleGraphView = react.lazy(
1944
+ () => Promise.resolve().then(() => (init_RuleGraphView(), RuleGraphView_exports)).then((module) => ({ default: module.RuleGraphView }))
1945
+ );
1946
+ var LazyRuleGraphModal = react.lazy(
1947
+ () => Promise.resolve().then(() => (init_RuleGraphModal(), RuleGraphModal_exports)).then((module) => ({ default: module.RuleGraphModal }))
1948
+ );
1949
+ function resolveScopeStageId(metadata, activeProgramStageId) {
1950
+ if (activeProgramStageId !== void 0) {
1951
+ return activeProgramStageId;
1952
+ }
1320
1953
  return metadata.formKind === "event" ? metadata.programStageId : null;
1321
1954
  }
1322
1955
  function isRuleInScope(rule, scopeStageId) {
1323
- return rule.programStageId === null || rule.programStageId === scopeStageId;
1956
+ return rule.programStageId === scopeStageId;
1324
1957
  }
1325
1958
  function resolveAccentClassName(inScope, firing, selected) {
1326
1959
  if (selected) {
@@ -1337,6 +1970,17 @@ function resolveCardStatus(inScope, firing) {
1337
1970
  }
1338
1971
  return firing ? { label: translate("Firing"), className: "text-dhis2-teal-700" } : { label: translate("Idle"), className: "text-dhis2-grey-600" };
1339
1972
  }
1973
+ function resolveDetailsStatus(inScope, firing) {
1974
+ if (!inScope) {
1975
+ return "out-of-scope";
1976
+ }
1977
+ return firing ? "firing" : "idle";
1978
+ }
1979
+ function resolveAppliesToCaption(rule, labelLookup) {
1980
+ return rule.programStageId === null ? translate("Applies to registration") : translate("Applies to {{stage}}", {
1981
+ stage: labelLookup.resolveStageName(rule.programStageId)
1982
+ });
1983
+ }
1340
1984
  function formatActionLabel(action) {
1341
1985
  if (action.targetLabel && action.detail) {
1342
1986
  return `${action.type} \xB7 ${action.targetLabel} = ${action.detail}`;
@@ -1370,7 +2014,11 @@ function resolveTabLabel(key) {
1370
2014
  return translate("Graph");
1371
2015
  }
1372
2016
  }
1373
- function RulesPanel({ metadata, showConditions = true }) {
2017
+ function RulesPanel({
2018
+ metadata,
2019
+ showConditions = true,
2020
+ activeProgramStageId
2021
+ }) {
1374
2022
  const formStore = dhis2FormUtilsHooks.useFormStore();
1375
2023
  const { form } = dhis2FormUtilsHooks.useFormStateContext();
1376
2024
  const traceStore = useRuleTraceStore();
@@ -1379,9 +2027,13 @@ function RulesPanel({ metadata, showConditions = true }) {
1379
2027
  const [highlightRuleId, setHighlightRuleId] = react.useState(null);
1380
2028
  const [graphModalOpen, setGraphModalOpen] = react.useState(false);
1381
2029
  const [scopeFilter, setScopeFilter] = react.useState("scoped");
2030
+ const [detailsRuleId, setDetailsRuleId] = react.useState(null);
1382
2031
  const labelLookup = react.useMemo(() => createLabelLookup(metadata), [metadata]);
1383
2032
  const catalog = react.useMemo(() => resolveProgramRulesList(metadata), [metadata]);
1384
- const scopeStageId = react.useMemo(() => resolveScopeStageId(metadata), [metadata]);
2033
+ const scopeStageId = react.useMemo(
2034
+ () => resolveScopeStageId(metadata, activeProgramStageId),
2035
+ [metadata, activeProgramStageId]
2036
+ );
1385
2037
  const entries = react.useSyncExternalStore(
1386
2038
  react.useCallback((listener) => traceStore.subscribe(listener), [traceStore]),
1387
2039
  react.useCallback(() => traceStore.getSnapshot(), [traceStore]),
@@ -1433,6 +2085,15 @@ function RulesPanel({ metadata, showConditions = true }) {
1433
2085
  }
1434
2086
  return null;
1435
2087
  }, [highlightedRuleName, selectedEntry]);
2088
+ const detailsRule = react.useMemo(
2089
+ () => catalog.find((rule) => rule.id === detailsRuleId) ?? null,
2090
+ [catalog, detailsRuleId]
2091
+ );
2092
+ const detailsStatus = detailsRule ? resolveDetailsStatus(
2093
+ isRuleInScope(detailsRule, scopeStageId),
2094
+ activeRuleIds.has(detailsRule.id)
2095
+ ) : "idle";
2096
+ const detailsProgramStageName = detailsRule ? detailsRule.programStageId === null ? null : labelLookup.resolveStageName(detailsRule.programStageId) : void 0;
1436
2097
  const graphProps = {
1437
2098
  entries,
1438
2099
  fieldState,
@@ -1534,6 +2195,9 @@ function RulesPanel({ metadata, showConditions = true }) {
1534
2195
  onSelectRule: (ruleId) => {
1535
2196
  setHighlightRuleId(ruleId);
1536
2197
  setTab("graph");
2198
+ },
2199
+ onOpenDetails: (ruleId) => {
2200
+ setDetailsRuleId(ruleId);
1537
2201
  }
1538
2202
  }
1539
2203
  ) : tab === "trace" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -1553,19 +2217,19 @@ function RulesPanel({ metadata, showConditions = true }) {
1553
2217
  },
1554
2218
  labelLookup
1555
2219
  }
1556
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1557
- RuleGraphView,
2220
+ ) : /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(RulesPanelGraphFallback, {}), children: /* @__PURE__ */ jsxRuntime.jsx(
2221
+ LazyRuleGraphView,
1558
2222
  {
1559
2223
  ...graphProps,
1560
2224
  headerActions: graphHasNodes ? expandGraphButton : void 0
1561
2225
  }
1562
- ) }),
2226
+ ) }) }),
1563
2227
  entries.length > 0 && tab !== "rules" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sr-only", "aria-live": "polite", children: translate("{{evaluations}} evaluations \xB7 {{rules}} rules observed", {
1564
2228
  evaluations: entries.length,
1565
2229
  rules: observedRuleCount
1566
2230
  }) }) : null,
1567
- /* @__PURE__ */ jsxRuntime.jsx(
1568
- RuleGraphModal,
2231
+ graphModalOpen ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(
2232
+ LazyRuleGraphModal,
1569
2233
  {
1570
2234
  ...graphProps,
1571
2235
  open: graphModalOpen,
@@ -1573,13 +2237,35 @@ function RulesPanel({ metadata, showConditions = true }) {
1573
2237
  setGraphModalOpen(false);
1574
2238
  },
1575
2239
  subtitle: graphSubtitle,
1576
- layoutKey: graphModalOpen ? "open" : "closed"
2240
+ layoutKey: "open"
2241
+ }
2242
+ ) }) : null,
2243
+ /* @__PURE__ */ jsxRuntime.jsx(
2244
+ RuleDetailsModal,
2245
+ {
2246
+ open: detailsRuleId != null,
2247
+ onClose: () => {
2248
+ setDetailsRuleId(null);
2249
+ },
2250
+ ruleId: detailsRuleId,
2251
+ ruleName: detailsRule?.name ?? "",
2252
+ status: detailsStatus,
2253
+ programStageName: detailsProgramStageName,
2254
+ programRuleVariables: metadata.metadata.programRuleVariables
1577
2255
  }
1578
2256
  )
1579
2257
  ]
1580
2258
  }
1581
2259
  );
1582
2260
  }
2261
+ function sortRulesFiringFirst(visibleRules, activeRuleIds) {
2262
+ const firing = [];
2263
+ const idle = [];
2264
+ for (const rule of visibleRules) {
2265
+ (activeRuleIds.has(rule.id) ? firing : idle).push(rule);
2266
+ }
2267
+ return [...firing, ...idle];
2268
+ }
1583
2269
  function RulesTab({
1584
2270
  catalog,
1585
2271
  scopeStageId,
@@ -1588,21 +2274,31 @@ function RulesTab({
1588
2274
  selectedRuleId,
1589
2275
  showConditions,
1590
2276
  labelLookup,
1591
- onSelectRule
2277
+ onSelectRule,
2278
+ onOpenDetails
1592
2279
  }) {
2280
+ const listRef = react.useRef(null);
2281
+ const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
2282
+ const sortedRules = react.useMemo(
2283
+ () => sortRulesFiringFirst(visibleRules, activeRuleIds),
2284
+ [visibleRules, activeRuleIds]
2285
+ );
2286
+ useFlipReorder(
2287
+ react.useMemo(() => sortedRules.map((rule) => rule.id), [sortedRules]),
2288
+ listRef
2289
+ );
1593
2290
  if (!catalog.length) {
1594
2291
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("This program has no rules.") });
1595
2292
  }
1596
- const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
1597
- if (!visibleRules.length) {
2293
+ if (!sortedRules.length) {
1598
2294
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("No rules are in scope for this stage.") });
1599
2295
  }
1600
- return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-[10px] p-0", children: visibleRules.map((rule) => {
2296
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { ref: listRef, className: "m-0 flex list-none flex-col gap-[10px] p-0", children: sortedRules.map((rule) => {
1601
2297
  const inScope = isRuleInScope(rule, scopeStageId);
1602
2298
  const firing = activeRuleIds.has(rule.id);
1603
2299
  const isSelected = selectedRuleId === rule.id;
1604
2300
  const status = resolveCardStatus(inScope, firing);
1605
- return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "m-0 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
2301
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { "data-rule-id": rule.id, className: "m-0 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
1606
2302
  "article",
1607
2303
  {
1608
2304
  role: "button",
@@ -1633,13 +2329,29 @@ function RulesTab({
1633
2329
  children: rule.name
1634
2330
  }
1635
2331
  ),
1636
- /* @__PURE__ */ jsxRuntime.jsx(
1637
- "span",
1638
- {
1639
- className: `shrink-0 text-[11px] font-semibold ${status.className}`,
1640
- children: status.label
1641
- }
1642
- )
2332
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[6px]", children: [
2333
+ /* @__PURE__ */ jsxRuntime.jsx(
2334
+ "span",
2335
+ {
2336
+ className: `text-[11px] font-semibold ${status.className}`,
2337
+ children: status.label
2338
+ }
2339
+ ),
2340
+ /* @__PURE__ */ jsxRuntime.jsx(
2341
+ "button",
2342
+ {
2343
+ type: "button",
2344
+ title: translate("Program rule details"),
2345
+ "aria-label": translate("Program rule details"),
2346
+ onClick: (event) => {
2347
+ event.stopPropagation();
2348
+ onOpenDetails(rule.id);
2349
+ },
2350
+ 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",
2351
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconInfo16, {})
2352
+ }
2353
+ )
2354
+ ] })
1643
2355
  ] }),
1644
2356
  rule.programRuleActions.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-[6px]", children: rule.programRuleActions.map((action, index) => {
1645
2357
  const summary = formatRuleActionSummary(
@@ -1663,16 +2375,17 @@ function RulesTab({
1663
2375
  children: rule.condition
1664
2376
  }
1665
2377
  ) : null,
1666
- !inScope && rule.programStageId ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-dhis2-grey-600", children: translate("Applies to {{stage}}", {
1667
- stage: labelLookup.resolveStageName(rule.programStageId)
1668
- }) }) : null
2378
+ !inScope ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-dhis2-grey-600", children: resolveAppliesToCaption(rule, labelLookup) }) : null
1669
2379
  ]
1670
2380
  }
1671
2381
  ) }, rule.id);
1672
2382
  }) });
1673
2383
  }
1674
2384
 
1675
- exports.EFFECT_ICONS = EFFECT_ICONS;
2385
+ // src/index.ts
2386
+ init_effectStyles();
2387
+
2388
+ exports.RuleDetailsModal = RuleDetailsModal;
1676
2389
  exports.RuleDevtoolsScope = RuleDevtoolsScope;
1677
2390
  exports.RulesPanel = RulesPanel;
1678
2391
  exports.createLabelLookup = createLabelLookup;