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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var react = require('react');
6
6
  var dhis2FormUtilsMetadata = require('@nnkogift/dhis2-form-utils-metadata');
7
7
  var i18n = require('@dhis2/d2-i18n');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
+ var appRuntime = require('@dhis2/app-runtime');
9
10
  var react$1 = require('@xyflow/react');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -523,6 +524,463 @@ function useRuleTraceStore() {
523
524
  return store;
524
525
  }
525
526
 
527
+ // src/programRuleDetailQuery.ts
528
+ var PROGRAM_RULE_ACTION_DETAIL_FIELDS = [
529
+ "id",
530
+ "programRuleActionType",
531
+ "priority",
532
+ "content",
533
+ "data",
534
+ "location",
535
+ "dataElement[id,displayName]",
536
+ "trackedEntityAttribute[id,displayName]",
537
+ "option[id,displayName]",
538
+ "optionGroup[id,displayName]",
539
+ "programStageSection[id,displayName]",
540
+ "programStage[id,displayName]",
541
+ "programSection[id,displayName]"
542
+ ].join(",");
543
+ var PROGRAM_RULE_DETAIL_FIELDS = [
544
+ "id",
545
+ "name",
546
+ "displayName",
547
+ "code",
548
+ "description",
549
+ "condition",
550
+ "priority",
551
+ "lastUpdated",
552
+ "lastUpdatedBy[displayName]",
553
+ "program[id,displayName]",
554
+ "programStage[id,displayName]",
555
+ `programRuleActions[${PROGRAM_RULE_ACTION_DETAIL_FIELDS}]`
556
+ ].join(",");
557
+ var programRuleDetailQuery = {
558
+ programRule: {
559
+ resource: "programRules",
560
+ id: (variables) => variables.ruleId,
561
+ params: {
562
+ fields: PROGRAM_RULE_DETAIL_FIELDS
563
+ }
564
+ }
565
+ };
566
+
567
+ // src/useProgramRuleDetail.ts
568
+ function isCurrentDetail(ruleId, detail) {
569
+ return ruleId != null && detail?.id === ruleId;
570
+ }
571
+ function resolveDetailState(ruleId, detail, hasError) {
572
+ const isCurrent = isCurrentDetail(ruleId, detail);
573
+ return {
574
+ detail: isCurrent ? detail : void 0,
575
+ loading: ruleId != null && !isCurrent && !hasError
576
+ };
577
+ }
578
+ function useProgramRuleDetail(ruleId) {
579
+ const { data, error, refetch } = appRuntime.useDataQuery(
580
+ programRuleDetailQuery,
581
+ { lazy: true }
582
+ );
583
+ react.useEffect(() => {
584
+ if (ruleId) {
585
+ void refetch({ ruleId });
586
+ }
587
+ }, [ruleId, refetch]);
588
+ return { ...resolveDetailState(ruleId, data?.programRule, Boolean(error)), error };
589
+ }
590
+ var EM_DASH = "\u2014";
591
+ function formatTimestamp(value) {
592
+ if (!value) {
593
+ return null;
594
+ }
595
+ const date = new Date(value);
596
+ if (Number.isNaN(date.getTime())) {
597
+ return null;
598
+ }
599
+ const day = String(date.getDate());
600
+ const month = date.toLocaleString("en-GB", { month: "short" });
601
+ const year = String(date.getFullYear());
602
+ const hours = String(date.getHours()).padStart(2, "0");
603
+ const minutes = String(date.getMinutes()).padStart(2, "0");
604
+ return `${day} ${month} ${year} ${hours}:${minutes}`;
605
+ }
606
+ function resolveStatusChip(status) {
607
+ switch (status) {
608
+ case "firing":
609
+ return {
610
+ label: translate("Firing"),
611
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
612
+ };
613
+ case "idle":
614
+ return { label: translate("Idle"), className: "bg-dhis2-grey-200 text-dhis2-grey-700" };
615
+ case "out-of-scope":
616
+ return {
617
+ label: translate("Out of scope"),
618
+ className: "bg-dhis2-grey-200 text-dhis2-grey-600"
619
+ };
620
+ }
621
+ }
622
+ var VARIABLE_PATTERNS = [
623
+ {
624
+ pattern: /#\{[^}]*\}/g,
625
+ kind: "Data element",
626
+ className: "bg-dhis2-blue-100 text-dhis2-blue-900"
627
+ },
628
+ {
629
+ pattern: /A\{[^}]*\}/g,
630
+ kind: "Tracked entity attribute",
631
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
632
+ },
633
+ {
634
+ pattern: /V\{[^}]*\}/g,
635
+ kind: "Environment variable",
636
+ className: "bg-dhis2-grey-200 text-dhis2-grey-900"
637
+ },
638
+ {
639
+ pattern: /d2:\w+(?=\()/g,
640
+ kind: "Function",
641
+ className: "bg-dhis2-yellow-100 text-dhis2-yellow-900"
642
+ }
643
+ ];
644
+ var RESOLVABLE_VARIABLE_KINDS = /* @__PURE__ */ new Set([
645
+ "Data element",
646
+ "Tracked entity attribute"
647
+ ]);
648
+ function resolveVariableDisplayName(name, programRuleVariables) {
649
+ const variable = programRuleVariables.find((candidate) => candidate.name === name);
650
+ return variable?.dataElement?.displayName ?? variable?.trackedEntityAttribute?.displayName;
651
+ }
652
+ function resolveVariableToken(token, kind, programRuleVariables) {
653
+ if (kind === "Function") {
654
+ return `${token}()`;
655
+ }
656
+ if (!RESOLVABLE_VARIABLE_KINDS.has(kind)) {
657
+ return token;
658
+ }
659
+ return resolveVariableDisplayName(token.slice(2, -1), programRuleVariables) ?? token;
660
+ }
661
+ function matchDistinctTokens(condition, pattern, seen) {
662
+ const matches = condition.match(pattern) ?? [];
663
+ const distinct = matches.filter((token) => !seen.has(token));
664
+ for (const token of distinct) {
665
+ seen.add(token);
666
+ }
667
+ return distinct;
668
+ }
669
+ function parseConditionVariables(condition, programRuleVariables) {
670
+ if (!condition) {
671
+ return [];
672
+ }
673
+ const seen = /* @__PURE__ */ new Set();
674
+ return VARIABLE_PATTERNS.flatMap(
675
+ ({ pattern, kind, className }) => matchDistinctTokens(condition, pattern, seen).map((token) => ({
676
+ token,
677
+ kind,
678
+ label: resolveVariableToken(token, kind, programRuleVariables),
679
+ className
680
+ }))
681
+ );
682
+ }
683
+ var ACTION_TARGET_RESOLVERS = [
684
+ { label: "Data element", getRef: (action) => action.dataElement },
685
+ { label: "Tracked entity attribute", getRef: (action) => action.trackedEntityAttribute },
686
+ { label: "Program stage section", getRef: (action) => action.programStageSection },
687
+ { label: "Program stage", getRef: (action) => action.programStage },
688
+ { label: "Option", getRef: (action) => action.option },
689
+ { label: "Option group", getRef: (action) => action.optionGroup }
690
+ ];
691
+ function formatActionTargetValue(ref) {
692
+ return `${ref.displayName ?? ref.id} \xB7 ${ref.id}`;
693
+ }
694
+ function resolveActionTarget(action) {
695
+ for (const { label, getRef } of ACTION_TARGET_RESOLVERS) {
696
+ const ref = getRef(action);
697
+ if (ref?.id) {
698
+ return { label: translate(label), value: formatActionTargetValue(ref) };
699
+ }
700
+ }
701
+ return null;
702
+ }
703
+ function BasicDetailCell({ label, value, mono }) {
704
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
705
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-xs text-dhis2-grey-600", children: label }),
706
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: `m-0 mt-[2px] text-sm text-dhis2-grey-900 ${mono ? "font-mono" : ""}`, children: value })
707
+ ] });
708
+ }
709
+ var FEEDBACK_ACTION_TYPES = /* @__PURE__ */ new Set(["DISPLAYTEXT", "DISPLAYKEYVALUEPAIR"]);
710
+ function resolveDataRow(action) {
711
+ return action.data ? { label: translate("Data (expression)"), value: action.data, mono: true } : null;
712
+ }
713
+ function resolveContentRow(action) {
714
+ const showContent = Boolean(action.content) && FEEDBACK_ACTION_TYPES.has(action.programRuleActionType);
715
+ return showContent ? { label: translate("Content (static text)"), value: action.content ?? "" } : null;
716
+ }
717
+ function resolveLocationRow(action) {
718
+ return action.location ? { label: translate("Location"), value: action.location, mono: true } : null;
719
+ }
720
+ function resolveActionRows(action) {
721
+ const rows = [
722
+ resolveActionTarget(action),
723
+ resolveDataRow(action),
724
+ resolveContentRow(action),
725
+ resolveLocationRow(action)
726
+ ];
727
+ return rows.filter((row) => row !== null);
728
+ }
729
+ function ActionCard({ action, index }) {
730
+ const type = action.programRuleActionType;
731
+ const visual = getEffectVisual(type);
732
+ const Icon = EFFECT_ICONS[visual.variant];
733
+ const rows = resolveActionRows(action);
734
+ return /* @__PURE__ */ jsxRuntime.jsxs(
735
+ "div",
736
+ {
737
+ 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%)]",
738
+ style: { borderInlineStartWidth: 3, borderInlineStartColor: visual.edgeStroke },
739
+ children: [
740
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-dp12 flex items-center justify-between gap-dp8", children: [
741
+ /* @__PURE__ */ jsxRuntime.jsxs(
742
+ "span",
743
+ {
744
+ className: `inline-flex items-center gap-[4px] rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${visual.tagClassName}`,
745
+ children: [
746
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, {}),
747
+ type
748
+ ]
749
+ }
750
+ ),
751
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] uppercase tracking-wide text-dhis2-grey-600", children: translate("Action {{n}}", { n: index + 1 }) })
752
+ ] }),
753
+ /* @__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: [
754
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-dhis2-grey-600", children: row.label }),
755
+ /* @__PURE__ */ jsxRuntime.jsx(
756
+ "span",
757
+ {
758
+ className: `min-w-0 break-words text-sm ${row.mono ? "font-mono text-[12px]" : ""}`,
759
+ children: row.value
760
+ }
761
+ )
762
+ ] }, row.label)) })
763
+ ]
764
+ }
765
+ );
766
+ }
767
+ function orDash(value) {
768
+ return value ?? EM_DASH;
769
+ }
770
+ function resolveDetailDisplayName(rule) {
771
+ return rule?.displayName;
772
+ }
773
+ function resolveDetailName(rule) {
774
+ return rule?.name;
775
+ }
776
+ function resolveRuleDisplayName(rule, fallback) {
777
+ const displayName = resolveDetailDisplayName(rule) ?? resolveDetailName(rule);
778
+ return displayName ?? fallback;
779
+ }
780
+ function resolveRuleStageName(rule) {
781
+ return rule?.programStage?.displayName;
782
+ }
783
+ function resolveStageName(rule, programStageName) {
784
+ if (programStageName === null) {
785
+ return translate("All stages (registration)");
786
+ }
787
+ return orDash(resolveRuleStageName(rule) ?? programStageName);
788
+ }
789
+ function resolveLastUpdatedByName(rule) {
790
+ return rule?.lastUpdatedBy?.displayName;
791
+ }
792
+ function resolveLastUpdatedValue(rule) {
793
+ const label = formatTimestamp(rule?.lastUpdated);
794
+ if (!label) {
795
+ return EM_DASH;
796
+ }
797
+ const by = resolveLastUpdatedByName(rule);
798
+ return by ? `${label} \xB7 ${by}` : label;
799
+ }
800
+ function resolvePriorityCellValue(rule) {
801
+ return rule?.priority != null ? String(rule.priority) : EM_DASH;
802
+ }
803
+ function resolveActionsCellValue(rule) {
804
+ return translate("{{n}} action(s)", { n: rule?.programRuleActions?.length ?? 0 });
805
+ }
806
+ function resolveBasicDetailCells(rule, ruleName, programStageName) {
807
+ return [
808
+ { label: translate("Name"), value: resolveRuleDisplayName(rule, ruleName) },
809
+ { label: translate("Code"), value: orDash(rule?.code), mono: true },
810
+ { label: translate("Identifier"), value: orDash(rule?.id), mono: true },
811
+ { label: translate("Program"), value: orDash(rule?.program?.displayName) },
812
+ { label: translate("Program stage"), value: resolveStageName(rule, programStageName) },
813
+ { label: translate("Priority"), value: resolvePriorityCellValue(rule) },
814
+ { label: translate("Actions"), value: resolveActionsCellValue(rule) },
815
+ { label: translate("Last updated"), value: resolveLastUpdatedValue(rule) }
816
+ ];
817
+ }
818
+ function BasicDetails({
819
+ rule,
820
+ ruleName,
821
+ programStageName
822
+ }) {
823
+ const cells = resolveBasicDetailCells(rule, ruleName, programStageName);
824
+ 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)) });
825
+ }
826
+ var SECTION_HEADING_CLASS = "m-0 mb-dp8 text-[13px] font-bold uppercase tracking-wide text-dhis2-grey-700";
827
+ function RuleDetailsHeader({
828
+ title,
829
+ chip,
830
+ description
831
+ }) {
832
+ 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: [
833
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-[11px] font-bold uppercase tracking-[.09em] text-dhis2-grey-600", children: translate("Program rule") }),
834
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-[6px] flex flex-wrap items-center gap-dp8", children: [
835
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "m-0 text-xl font-medium leading-[1.3] text-dhis2-grey-900", children: title }),
836
+ /* @__PURE__ */ jsxRuntime.jsx(
837
+ "span",
838
+ {
839
+ className: `rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${chip.className}`,
840
+ children: chip.label
841
+ }
842
+ )
843
+ ] }),
844
+ description ? /* @__PURE__ */ jsxRuntime.jsx(
845
+ "p",
846
+ {
847
+ className: "m-0 mt-[6px] text-sm text-dhis2-grey-700",
848
+ style: { textWrap: "pretty" },
849
+ children: description
850
+ }
851
+ ) : null
852
+ ] });
853
+ }
854
+ function ConditionSection({
855
+ condition,
856
+ variables
857
+ }) {
858
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
859
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Condition") }),
860
+ /* @__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 }),
861
+ variables.length ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-dp12", children: [
862
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 mb-dp8 text-xs text-dhis2-grey-600", children: translate("Variables referenced") }),
863
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-dp8", children: variables.map((variable) => /* @__PURE__ */ jsxRuntime.jsxs(
864
+ "span",
865
+ {
866
+ className: `inline-flex items-center gap-[4px] rounded-full px-dp8 py-[2px] text-xs ${variable.className}`,
867
+ children: [
868
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-medium", children: variable.label }),
869
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "opacity-70", children: translate(variable.kind) })
870
+ ]
871
+ },
872
+ variable.token
873
+ )) })
874
+ ] }) : null
875
+ ] });
876
+ }
877
+ function ActionsSection({ actions }) {
878
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
879
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Actions") }),
880
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-dp12", children: actions.map((action, index) => /* @__PURE__ */ jsxRuntime.jsx(ActionCard, { action, index }, action.id)) })
881
+ ] });
882
+ }
883
+ function RuleDetailsContent({
884
+ detail,
885
+ ruleName,
886
+ programStageName,
887
+ variables
888
+ }) {
889
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-dp24 pt-dp16", children: [
890
+ /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
891
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Basic details") }),
892
+ /* @__PURE__ */ jsxRuntime.jsx(
893
+ BasicDetails,
894
+ {
895
+ rule: detail,
896
+ ruleName,
897
+ programStageName
898
+ }
899
+ )
900
+ ] }),
901
+ /* @__PURE__ */ jsxRuntime.jsx(ConditionSection, { condition: detail.condition, variables }),
902
+ /* @__PURE__ */ jsxRuntime.jsx(ActionsSection, { actions: detail.programRuleActions ?? [] })
903
+ ] });
904
+ }
905
+ function RuleDetailsBody({
906
+ detail,
907
+ loading,
908
+ error,
909
+ ruleName,
910
+ programStageName,
911
+ variables
912
+ }) {
913
+ if (loading) {
914
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[280px] items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.CircularLoader, { small: true }) });
915
+ }
916
+ if (error) {
917
+ 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 }) });
918
+ }
919
+ if (!detail) {
920
+ return null;
921
+ }
922
+ return /* @__PURE__ */ jsxRuntime.jsx(
923
+ RuleDetailsContent,
924
+ {
925
+ detail,
926
+ ruleName,
927
+ programStageName,
928
+ variables
929
+ }
930
+ );
931
+ }
932
+ function RuleDetailsFooter({ onClose }) {
933
+ return /* @__PURE__ */ jsxRuntime.jsxs(
934
+ "div",
935
+ {
936
+ style: { order: 3 },
937
+ 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]",
938
+ children: [
939
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm text-dhis2-grey-600", children: translate("Read-only view. Edit rules in the Maintenance app.") }),
940
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ButtonStrip, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { secondary: true, onClick: onClose, children: translate("Close") }) })
941
+ ]
942
+ }
943
+ );
944
+ }
945
+ function resolveActiveRuleId(open, ruleId) {
946
+ return open ? ruleId : null;
947
+ }
948
+ function shouldRenderModal(open, ruleId) {
949
+ return open && ruleId != null;
950
+ }
951
+ function RuleDetailsModal({
952
+ open,
953
+ onClose,
954
+ ruleId,
955
+ ruleName,
956
+ status,
957
+ programStageName,
958
+ programRuleVariables
959
+ }) {
960
+ const { detail, loading, error } = useProgramRuleDetail(resolveActiveRuleId(open, ruleId));
961
+ if (!shouldRenderModal(open, ruleId)) {
962
+ return null;
963
+ }
964
+ const chip = resolveStatusChip(status);
965
+ const variables = parseConditionVariables(detail?.condition, programRuleVariables);
966
+ const title = resolveRuleDisplayName(detail, ruleName);
967
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Modal, { position: "middle", onClose, children: [
968
+ /* @__PURE__ */ jsxRuntime.jsx(RuleDetailsHeader, { title, chip, description: detail?.description }),
969
+ /* @__PURE__ */ jsxRuntime.jsx(ui.ModalContent, { children: /* @__PURE__ */ jsxRuntime.jsx(
970
+ RuleDetailsBody,
971
+ {
972
+ detail,
973
+ loading,
974
+ error,
975
+ ruleName,
976
+ programStageName,
977
+ variables
978
+ }
979
+ ) }),
980
+ /* @__PURE__ */ jsxRuntime.jsx(RuleDetailsFooter, { onClose })
981
+ ] });
982
+ }
983
+
526
984
  // src/graphLayout.ts
527
985
  var ROLE_COLUMNS = {
528
986
  source: 0,
@@ -1316,6 +1774,52 @@ function TraceTimeline({
1316
1774
  ) }, entry.id);
1317
1775
  }) });
1318
1776
  }
1777
+ var REORDER_DURATION_MS = 220;
1778
+ var REORDER_EASING = "ease-out";
1779
+ function prefersReducedMotion() {
1780
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1781
+ }
1782
+ function useFlipReorder(orderedIds, containerRef) {
1783
+ const positionsRef = react.useRef(/* @__PURE__ */ new Map());
1784
+ react.useLayoutEffect(() => {
1785
+ const container = containerRef.current;
1786
+ if (!container) {
1787
+ return;
1788
+ }
1789
+ const previousPositions = positionsRef.current;
1790
+ const items = container.querySelectorAll("[data-rule-id]");
1791
+ if (!prefersReducedMotion()) {
1792
+ items.forEach((item) => {
1793
+ const id = item.dataset.ruleId;
1794
+ if (!id) {
1795
+ return;
1796
+ }
1797
+ const previousRect = previousPositions.get(id);
1798
+ if (!previousRect) {
1799
+ return;
1800
+ }
1801
+ const currentRect = item.getBoundingClientRect();
1802
+ const deltaY = previousRect.top - currentRect.top;
1803
+ if (deltaY === 0) {
1804
+ return;
1805
+ }
1806
+ item.style.transition = "none";
1807
+ item.style.transform = `translateY(${String(deltaY)}px)`;
1808
+ item.getBoundingClientRect();
1809
+ item.style.transition = `transform ${String(REORDER_DURATION_MS)}ms ${REORDER_EASING}`;
1810
+ item.style.transform = "";
1811
+ });
1812
+ }
1813
+ const nextPositions = /* @__PURE__ */ new Map();
1814
+ items.forEach((item) => {
1815
+ const id = item.dataset.ruleId;
1816
+ if (id) {
1817
+ nextPositions.set(id, item.getBoundingClientRect());
1818
+ }
1819
+ });
1820
+ positionsRef.current = nextPositions;
1821
+ }, [orderedIds.join("|")]);
1822
+ }
1319
1823
  function resolveScopeStageId(metadata) {
1320
1824
  return metadata.formKind === "event" ? metadata.programStageId : null;
1321
1825
  }
@@ -1337,6 +1841,12 @@ function resolveCardStatus(inScope, firing) {
1337
1841
  }
1338
1842
  return firing ? { label: translate("Firing"), className: "text-dhis2-teal-700" } : { label: translate("Idle"), className: "text-dhis2-grey-600" };
1339
1843
  }
1844
+ function resolveDetailsStatus(inScope, firing) {
1845
+ if (!inScope) {
1846
+ return "out-of-scope";
1847
+ }
1848
+ return firing ? "firing" : "idle";
1849
+ }
1340
1850
  function formatActionLabel(action) {
1341
1851
  if (action.targetLabel && action.detail) {
1342
1852
  return `${action.type} \xB7 ${action.targetLabel} = ${action.detail}`;
@@ -1379,6 +1889,7 @@ function RulesPanel({ metadata, showConditions = true }) {
1379
1889
  const [highlightRuleId, setHighlightRuleId] = react.useState(null);
1380
1890
  const [graphModalOpen, setGraphModalOpen] = react.useState(false);
1381
1891
  const [scopeFilter, setScopeFilter] = react.useState("scoped");
1892
+ const [detailsRuleId, setDetailsRuleId] = react.useState(null);
1382
1893
  const labelLookup = react.useMemo(() => createLabelLookup(metadata), [metadata]);
1383
1894
  const catalog = react.useMemo(() => resolveProgramRulesList(metadata), [metadata]);
1384
1895
  const scopeStageId = react.useMemo(() => resolveScopeStageId(metadata), [metadata]);
@@ -1433,6 +1944,15 @@ function RulesPanel({ metadata, showConditions = true }) {
1433
1944
  }
1434
1945
  return null;
1435
1946
  }, [highlightedRuleName, selectedEntry]);
1947
+ const detailsRule = react.useMemo(
1948
+ () => catalog.find((rule) => rule.id === detailsRuleId) ?? null,
1949
+ [catalog, detailsRuleId]
1950
+ );
1951
+ const detailsStatus = detailsRule ? resolveDetailsStatus(
1952
+ isRuleInScope(detailsRule, scopeStageId),
1953
+ activeRuleIds.has(detailsRule.id)
1954
+ ) : "idle";
1955
+ const detailsProgramStageName = detailsRule ? detailsRule.programStageId === null ? null : labelLookup.resolveStageName(detailsRule.programStageId) : void 0;
1436
1956
  const graphProps = {
1437
1957
  entries,
1438
1958
  fieldState,
@@ -1534,6 +2054,9 @@ function RulesPanel({ metadata, showConditions = true }) {
1534
2054
  onSelectRule: (ruleId) => {
1535
2055
  setHighlightRuleId(ruleId);
1536
2056
  setTab("graph");
2057
+ },
2058
+ onOpenDetails: (ruleId) => {
2059
+ setDetailsRuleId(ruleId);
1537
2060
  }
1538
2061
  }
1539
2062
  ) : tab === "trace" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -1575,11 +2098,33 @@ function RulesPanel({ metadata, showConditions = true }) {
1575
2098
  subtitle: graphSubtitle,
1576
2099
  layoutKey: graphModalOpen ? "open" : "closed"
1577
2100
  }
2101
+ ),
2102
+ /* @__PURE__ */ jsxRuntime.jsx(
2103
+ RuleDetailsModal,
2104
+ {
2105
+ open: detailsRuleId != null,
2106
+ onClose: () => {
2107
+ setDetailsRuleId(null);
2108
+ },
2109
+ ruleId: detailsRuleId,
2110
+ ruleName: detailsRule?.name ?? "",
2111
+ status: detailsStatus,
2112
+ programStageName: detailsProgramStageName,
2113
+ programRuleVariables: metadata.metadata.programRuleVariables
2114
+ }
1578
2115
  )
1579
2116
  ]
1580
2117
  }
1581
2118
  );
1582
2119
  }
2120
+ function sortRulesFiringFirst(visibleRules, activeRuleIds) {
2121
+ const firing = [];
2122
+ const idle = [];
2123
+ for (const rule of visibleRules) {
2124
+ (activeRuleIds.has(rule.id) ? firing : idle).push(rule);
2125
+ }
2126
+ return [...firing, ...idle];
2127
+ }
1583
2128
  function RulesTab({
1584
2129
  catalog,
1585
2130
  scopeStageId,
@@ -1588,21 +2133,31 @@ function RulesTab({
1588
2133
  selectedRuleId,
1589
2134
  showConditions,
1590
2135
  labelLookup,
1591
- onSelectRule
2136
+ onSelectRule,
2137
+ onOpenDetails
1592
2138
  }) {
2139
+ const listRef = react.useRef(null);
2140
+ const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
2141
+ const sortedRules = react.useMemo(
2142
+ () => sortRulesFiringFirst(visibleRules, activeRuleIds),
2143
+ [visibleRules, activeRuleIds]
2144
+ );
2145
+ useFlipReorder(
2146
+ react.useMemo(() => sortedRules.map((rule) => rule.id), [sortedRules]),
2147
+ listRef
2148
+ );
1593
2149
  if (!catalog.length) {
1594
2150
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("This program has no rules.") });
1595
2151
  }
1596
- const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
1597
- if (!visibleRules.length) {
2152
+ if (!sortedRules.length) {
1598
2153
  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
2154
  }
1600
- return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 flex list-none flex-col gap-[10px] p-0", children: visibleRules.map((rule) => {
2155
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { ref: listRef, className: "m-0 flex list-none flex-col gap-[10px] p-0", children: sortedRules.map((rule) => {
1601
2156
  const inScope = isRuleInScope(rule, scopeStageId);
1602
2157
  const firing = activeRuleIds.has(rule.id);
1603
2158
  const isSelected = selectedRuleId === rule.id;
1604
2159
  const status = resolveCardStatus(inScope, firing);
1605
- return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "m-0 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
2160
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { "data-rule-id": rule.id, className: "m-0 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
1606
2161
  "article",
1607
2162
  {
1608
2163
  role: "button",
@@ -1633,13 +2188,29 @@ function RulesTab({
1633
2188
  children: rule.name
1634
2189
  }
1635
2190
  ),
1636
- /* @__PURE__ */ jsxRuntime.jsx(
1637
- "span",
1638
- {
1639
- className: `shrink-0 text-[11px] font-semibold ${status.className}`,
1640
- children: status.label
1641
- }
1642
- )
2191
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-[6px]", children: [
2192
+ /* @__PURE__ */ jsxRuntime.jsx(
2193
+ "span",
2194
+ {
2195
+ className: `text-[11px] font-semibold ${status.className}`,
2196
+ children: status.label
2197
+ }
2198
+ ),
2199
+ /* @__PURE__ */ jsxRuntime.jsx(
2200
+ "button",
2201
+ {
2202
+ type: "button",
2203
+ title: translate("Program rule details"),
2204
+ "aria-label": translate("Program rule details"),
2205
+ onClick: (event) => {
2206
+ event.stopPropagation();
2207
+ onOpenDetails(rule.id);
2208
+ },
2209
+ 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",
2210
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconInfo16, {})
2211
+ }
2212
+ )
2213
+ ] })
1643
2214
  ] }),
1644
2215
  rule.programRuleActions.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-[6px]", children: rule.programRuleActions.map((action, index) => {
1645
2216
  const summary = formatRuleActionSummary(
@@ -1673,6 +2244,7 @@ function RulesTab({
1673
2244
  }
1674
2245
 
1675
2246
  exports.EFFECT_ICONS = EFFECT_ICONS;
2247
+ exports.RuleDetailsModal = RuleDetailsModal;
1676
2248
  exports.RuleDevtoolsScope = RuleDevtoolsScope;
1677
2249
  exports.RulesPanel = RulesPanel;
1678
2250
  exports.createLabelLookup = createLabelLookup;