@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.js CHANGED
@@ -1,9 +1,10 @@
1
- import { IconInfo16, IconLink16, IconMail16, IconMessages16, IconErrorFilled16, IconWarningFilled16, IconStarFilled16, IconEdit16, IconView16, IconViewOff16, IconFullscreen16, SegmentedControl, Button, NoticeBox, Card, IconChevronDown16, IconChevronRight16, Tag, Divider, Chip, Modal, ModalTitle, ModalContent } from '@dhis2/ui';
1
+ import { IconInfo16, IconLink16, IconMail16, IconMessages16, IconErrorFilled16, IconWarningFilled16, IconStarFilled16, IconEdit16, IconView16, IconViewOff16, Modal, ModalContent, IconFullscreen16, SegmentedControl, Button, CircularLoader, ButtonStrip, NoticeBox, Card, IconChevronDown16, IconChevronRight16, Tag, Divider, Chip, ModalTitle } from '@dhis2/ui';
2
2
  import { useFormStore, useFormStateContext } from '@nnkogift/dhis2-form-utils-hooks';
3
- import { createContext, memo, useState, useMemo, useEffect, useSyncExternalStore, useCallback, useContext } from 'react';
3
+ import { createContext, memo, useState, useMemo, useEffect, useSyncExternalStore, useCallback, useRef, useContext, useLayoutEffect } from 'react';
4
4
  import { selectProgramStage } from '@nnkogift/dhis2-form-utils-metadata';
5
5
  import i18n from '@dhis2/d2-i18n';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+ import { useDataQuery } from '@dhis2/app-runtime';
7
8
  import { MarkerType, Handle, Position, getSmoothStepPath, BaseEdge, EdgeLabelRenderer, ReactFlowProvider, useNodesState, useEdgesState, ReactFlow, Background, Controls, useReactFlow } from '@xyflow/react';
8
9
 
9
10
  // src/RulesPanel.tsx
@@ -517,6 +518,463 @@ function useRuleTraceStore() {
517
518
  return store;
518
519
  }
519
520
 
521
+ // src/programRuleDetailQuery.ts
522
+ var PROGRAM_RULE_ACTION_DETAIL_FIELDS = [
523
+ "id",
524
+ "programRuleActionType",
525
+ "priority",
526
+ "content",
527
+ "data",
528
+ "location",
529
+ "dataElement[id,displayName]",
530
+ "trackedEntityAttribute[id,displayName]",
531
+ "option[id,displayName]",
532
+ "optionGroup[id,displayName]",
533
+ "programStageSection[id,displayName]",
534
+ "programStage[id,displayName]",
535
+ "programSection[id,displayName]"
536
+ ].join(",");
537
+ var PROGRAM_RULE_DETAIL_FIELDS = [
538
+ "id",
539
+ "name",
540
+ "displayName",
541
+ "code",
542
+ "description",
543
+ "condition",
544
+ "priority",
545
+ "lastUpdated",
546
+ "lastUpdatedBy[displayName]",
547
+ "program[id,displayName]",
548
+ "programStage[id,displayName]",
549
+ `programRuleActions[${PROGRAM_RULE_ACTION_DETAIL_FIELDS}]`
550
+ ].join(",");
551
+ var programRuleDetailQuery = {
552
+ programRule: {
553
+ resource: "programRules",
554
+ id: (variables) => variables.ruleId,
555
+ params: {
556
+ fields: PROGRAM_RULE_DETAIL_FIELDS
557
+ }
558
+ }
559
+ };
560
+
561
+ // src/useProgramRuleDetail.ts
562
+ function isCurrentDetail(ruleId, detail) {
563
+ return ruleId != null && detail?.id === ruleId;
564
+ }
565
+ function resolveDetailState(ruleId, detail, hasError) {
566
+ const isCurrent = isCurrentDetail(ruleId, detail);
567
+ return {
568
+ detail: isCurrent ? detail : void 0,
569
+ loading: ruleId != null && !isCurrent && !hasError
570
+ };
571
+ }
572
+ function useProgramRuleDetail(ruleId) {
573
+ const { data, error, refetch } = useDataQuery(
574
+ programRuleDetailQuery,
575
+ { lazy: true }
576
+ );
577
+ useEffect(() => {
578
+ if (ruleId) {
579
+ void refetch({ ruleId });
580
+ }
581
+ }, [ruleId, refetch]);
582
+ return { ...resolveDetailState(ruleId, data?.programRule, Boolean(error)), error };
583
+ }
584
+ var EM_DASH = "\u2014";
585
+ function formatTimestamp(value) {
586
+ if (!value) {
587
+ return null;
588
+ }
589
+ const date = new Date(value);
590
+ if (Number.isNaN(date.getTime())) {
591
+ return null;
592
+ }
593
+ const day = String(date.getDate());
594
+ const month = date.toLocaleString("en-GB", { month: "short" });
595
+ const year = String(date.getFullYear());
596
+ const hours = String(date.getHours()).padStart(2, "0");
597
+ const minutes = String(date.getMinutes()).padStart(2, "0");
598
+ return `${day} ${month} ${year} ${hours}:${minutes}`;
599
+ }
600
+ function resolveStatusChip(status) {
601
+ switch (status) {
602
+ case "firing":
603
+ return {
604
+ label: translate("Firing"),
605
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
606
+ };
607
+ case "idle":
608
+ return { label: translate("Idle"), className: "bg-dhis2-grey-200 text-dhis2-grey-700" };
609
+ case "out-of-scope":
610
+ return {
611
+ label: translate("Out of scope"),
612
+ className: "bg-dhis2-grey-200 text-dhis2-grey-600"
613
+ };
614
+ }
615
+ }
616
+ var VARIABLE_PATTERNS = [
617
+ {
618
+ pattern: /#\{[^}]*\}/g,
619
+ kind: "Data element",
620
+ className: "bg-dhis2-blue-100 text-dhis2-blue-900"
621
+ },
622
+ {
623
+ pattern: /A\{[^}]*\}/g,
624
+ kind: "Tracked entity attribute",
625
+ className: "bg-dhis2-teal-100 text-dhis2-teal-900"
626
+ },
627
+ {
628
+ pattern: /V\{[^}]*\}/g,
629
+ kind: "Environment variable",
630
+ className: "bg-dhis2-grey-200 text-dhis2-grey-900"
631
+ },
632
+ {
633
+ pattern: /d2:\w+(?=\()/g,
634
+ kind: "Function",
635
+ className: "bg-dhis2-yellow-100 text-dhis2-yellow-900"
636
+ }
637
+ ];
638
+ var RESOLVABLE_VARIABLE_KINDS = /* @__PURE__ */ new Set([
639
+ "Data element",
640
+ "Tracked entity attribute"
641
+ ]);
642
+ function resolveVariableDisplayName(name, programRuleVariables) {
643
+ const variable = programRuleVariables.find((candidate) => candidate.name === name);
644
+ return variable?.dataElement?.displayName ?? variable?.trackedEntityAttribute?.displayName;
645
+ }
646
+ function resolveVariableToken(token, kind, programRuleVariables) {
647
+ if (kind === "Function") {
648
+ return `${token}()`;
649
+ }
650
+ if (!RESOLVABLE_VARIABLE_KINDS.has(kind)) {
651
+ return token;
652
+ }
653
+ return resolveVariableDisplayName(token.slice(2, -1), programRuleVariables) ?? token;
654
+ }
655
+ function matchDistinctTokens(condition, pattern, seen) {
656
+ const matches = condition.match(pattern) ?? [];
657
+ const distinct = matches.filter((token) => !seen.has(token));
658
+ for (const token of distinct) {
659
+ seen.add(token);
660
+ }
661
+ return distinct;
662
+ }
663
+ function parseConditionVariables(condition, programRuleVariables) {
664
+ if (!condition) {
665
+ return [];
666
+ }
667
+ const seen = /* @__PURE__ */ new Set();
668
+ return VARIABLE_PATTERNS.flatMap(
669
+ ({ pattern, kind, className }) => matchDistinctTokens(condition, pattern, seen).map((token) => ({
670
+ token,
671
+ kind,
672
+ label: resolveVariableToken(token, kind, programRuleVariables),
673
+ className
674
+ }))
675
+ );
676
+ }
677
+ var ACTION_TARGET_RESOLVERS = [
678
+ { label: "Data element", getRef: (action) => action.dataElement },
679
+ { label: "Tracked entity attribute", getRef: (action) => action.trackedEntityAttribute },
680
+ { label: "Program stage section", getRef: (action) => action.programStageSection },
681
+ { label: "Program stage", getRef: (action) => action.programStage },
682
+ { label: "Option", getRef: (action) => action.option },
683
+ { label: "Option group", getRef: (action) => action.optionGroup }
684
+ ];
685
+ function formatActionTargetValue(ref) {
686
+ return `${ref.displayName ?? ref.id} \xB7 ${ref.id}`;
687
+ }
688
+ function resolveActionTarget(action) {
689
+ for (const { label, getRef } of ACTION_TARGET_RESOLVERS) {
690
+ const ref = getRef(action);
691
+ if (ref?.id) {
692
+ return { label: translate(label), value: formatActionTargetValue(ref) };
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+ function BasicDetailCell({ label, value, mono }) {
698
+ return /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
699
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-xs text-dhis2-grey-600", children: label }),
700
+ /* @__PURE__ */ jsx("p", { className: `m-0 mt-[2px] text-sm text-dhis2-grey-900 ${mono ? "font-mono" : ""}`, children: value })
701
+ ] });
702
+ }
703
+ var FEEDBACK_ACTION_TYPES = /* @__PURE__ */ new Set(["DISPLAYTEXT", "DISPLAYKEYVALUEPAIR"]);
704
+ function resolveDataRow(action) {
705
+ return action.data ? { label: translate("Data (expression)"), value: action.data, mono: true } : null;
706
+ }
707
+ function resolveContentRow(action) {
708
+ const showContent = Boolean(action.content) && FEEDBACK_ACTION_TYPES.has(action.programRuleActionType);
709
+ return showContent ? { label: translate("Content (static text)"), value: action.content ?? "" } : null;
710
+ }
711
+ function resolveLocationRow(action) {
712
+ return action.location ? { label: translate("Location"), value: action.location, mono: true } : null;
713
+ }
714
+ function resolveActionRows(action) {
715
+ const rows = [
716
+ resolveActionTarget(action),
717
+ resolveDataRow(action),
718
+ resolveContentRow(action),
719
+ resolveLocationRow(action)
720
+ ];
721
+ return rows.filter((row) => row !== null);
722
+ }
723
+ function ActionCard({ action, index }) {
724
+ const type = action.programRuleActionType;
725
+ const visual = getEffectVisual(type);
726
+ const Icon = EFFECT_ICONS[visual.variant];
727
+ const rows = resolveActionRows(action);
728
+ return /* @__PURE__ */ jsxs(
729
+ "div",
730
+ {
731
+ className: "relative rounded-[3px] border border-dhis2-grey-300 bg-white py-dp12 pe-dp16 ps-dp16 shadow-[0_1px_2px_rgb(0_0_0/4%)]",
732
+ style: { borderInlineStartWidth: 3, borderInlineStartColor: visual.edgeStroke },
733
+ children: [
734
+ /* @__PURE__ */ jsxs("div", { className: "mb-dp12 flex items-center justify-between gap-dp8", children: [
735
+ /* @__PURE__ */ jsxs(
736
+ "span",
737
+ {
738
+ className: `inline-flex items-center gap-[4px] rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${visual.tagClassName}`,
739
+ children: [
740
+ /* @__PURE__ */ jsx(Icon, {}),
741
+ type
742
+ ]
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] uppercase tracking-wide text-dhis2-grey-600", children: translate("Action {{n}}", { n: index + 1 }) })
746
+ ] }),
747
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-dp10", children: rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[172px_minmax(0,1fr)] gap-dp10", children: [
748
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-dhis2-grey-600", children: row.label }),
749
+ /* @__PURE__ */ jsx(
750
+ "span",
751
+ {
752
+ className: `min-w-0 break-words text-sm ${row.mono ? "font-mono text-[12px]" : ""}`,
753
+ children: row.value
754
+ }
755
+ )
756
+ ] }, row.label)) })
757
+ ]
758
+ }
759
+ );
760
+ }
761
+ function orDash(value) {
762
+ return value ?? EM_DASH;
763
+ }
764
+ function resolveDetailDisplayName(rule) {
765
+ return rule?.displayName;
766
+ }
767
+ function resolveDetailName(rule) {
768
+ return rule?.name;
769
+ }
770
+ function resolveRuleDisplayName(rule, fallback) {
771
+ const displayName = resolveDetailDisplayName(rule) ?? resolveDetailName(rule);
772
+ return displayName ?? fallback;
773
+ }
774
+ function resolveRuleStageName(rule) {
775
+ return rule?.programStage?.displayName;
776
+ }
777
+ function resolveStageName(rule, programStageName) {
778
+ if (programStageName === null) {
779
+ return translate("All stages (registration)");
780
+ }
781
+ return orDash(resolveRuleStageName(rule) ?? programStageName);
782
+ }
783
+ function resolveLastUpdatedByName(rule) {
784
+ return rule?.lastUpdatedBy?.displayName;
785
+ }
786
+ function resolveLastUpdatedValue(rule) {
787
+ const label = formatTimestamp(rule?.lastUpdated);
788
+ if (!label) {
789
+ return EM_DASH;
790
+ }
791
+ const by = resolveLastUpdatedByName(rule);
792
+ return by ? `${label} \xB7 ${by}` : label;
793
+ }
794
+ function resolvePriorityCellValue(rule) {
795
+ return rule?.priority != null ? String(rule.priority) : EM_DASH;
796
+ }
797
+ function resolveActionsCellValue(rule) {
798
+ return translate("{{n}} action(s)", { n: rule?.programRuleActions?.length ?? 0 });
799
+ }
800
+ function resolveBasicDetailCells(rule, ruleName, programStageName) {
801
+ return [
802
+ { label: translate("Name"), value: resolveRuleDisplayName(rule, ruleName) },
803
+ { label: translate("Code"), value: orDash(rule?.code), mono: true },
804
+ { label: translate("Identifier"), value: orDash(rule?.id), mono: true },
805
+ { label: translate("Program"), value: orDash(rule?.program?.displayName) },
806
+ { label: translate("Program stage"), value: resolveStageName(rule, programStageName) },
807
+ { label: translate("Priority"), value: resolvePriorityCellValue(rule) },
808
+ { label: translate("Actions"), value: resolveActionsCellValue(rule) },
809
+ { label: translate("Last updated"), value: resolveLastUpdatedValue(rule) }
810
+ ];
811
+ }
812
+ function BasicDetails({
813
+ rule,
814
+ ruleName,
815
+ programStageName
816
+ }) {
817
+ const cells = resolveBasicDetailCells(rule, ruleName, programStageName);
818
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-x-dp24 gap-y-dp12 rounded-[3px] border border-dhis2-grey-300 bg-dhis2-grey-050 p-dp16", children: cells.map((cell) => /* @__PURE__ */ jsx(BasicDetailCell, { ...cell }, cell.label)) });
819
+ }
820
+ var SECTION_HEADING_CLASS = "m-0 mb-dp8 text-[13px] font-bold uppercase tracking-wide text-dhis2-grey-700";
821
+ function RuleDetailsHeader({
822
+ title,
823
+ chip,
824
+ description
825
+ }) {
826
+ return /* @__PURE__ */ jsxs("div", { className: "-mx-dp24 -mt-dp24 mb-0 border-b border-dhis2-grey-300 px-dp24 pb-dp16 pe-[44px] pt-[20px]", children: [
827
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-[11px] font-bold uppercase tracking-[.09em] text-dhis2-grey-600", children: translate("Program rule") }),
828
+ /* @__PURE__ */ jsxs("div", { className: "mt-[6px] flex flex-wrap items-center gap-dp8", children: [
829
+ /* @__PURE__ */ jsx("h2", { className: "m-0 text-xl font-medium leading-[1.3] text-dhis2-grey-900", children: title }),
830
+ /* @__PURE__ */ jsx(
831
+ "span",
832
+ {
833
+ className: `rounded-[4px] px-dp8 py-[2px] text-xs font-semibold ${chip.className}`,
834
+ children: chip.label
835
+ }
836
+ )
837
+ ] }),
838
+ description ? /* @__PURE__ */ jsx(
839
+ "p",
840
+ {
841
+ className: "m-0 mt-[6px] text-sm text-dhis2-grey-700",
842
+ style: { textWrap: "pretty" },
843
+ children: description
844
+ }
845
+ ) : null
846
+ ] });
847
+ }
848
+ function ConditionSection({
849
+ condition,
850
+ variables
851
+ }) {
852
+ return /* @__PURE__ */ jsxs("section", { children: [
853
+ /* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Condition") }),
854
+ /* @__PURE__ */ jsx("pre", { className: "m-0 whitespace-pre-wrap break-words rounded-[3px] border border-dhis2-grey-300 bg-dhis2-grey-200 px-dp16 py-[14px] font-mono text-[13px] leading-[1.6] text-dhis2-grey-900", children: condition ?? EM_DASH }),
855
+ variables.length ? /* @__PURE__ */ jsxs("div", { className: "mt-dp12", children: [
856
+ /* @__PURE__ */ jsx("p", { className: "m-0 mb-dp8 text-xs text-dhis2-grey-600", children: translate("Variables referenced") }),
857
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-dp8", children: variables.map((variable) => /* @__PURE__ */ jsxs(
858
+ "span",
859
+ {
860
+ className: `inline-flex items-center gap-[4px] rounded-full px-dp8 py-[2px] text-xs ${variable.className}`,
861
+ children: [
862
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-medium", children: variable.label }),
863
+ /* @__PURE__ */ jsx("span", { className: "opacity-70", children: translate(variable.kind) })
864
+ ]
865
+ },
866
+ variable.token
867
+ )) })
868
+ ] }) : null
869
+ ] });
870
+ }
871
+ function ActionsSection({ actions }) {
872
+ return /* @__PURE__ */ jsxs("section", { children: [
873
+ /* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Actions") }),
874
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-dp12", children: actions.map((action, index) => /* @__PURE__ */ jsx(ActionCard, { action, index }, action.id)) })
875
+ ] });
876
+ }
877
+ function RuleDetailsContent({
878
+ detail,
879
+ ruleName,
880
+ programStageName,
881
+ variables
882
+ }) {
883
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-dp24 pt-dp16", children: [
884
+ /* @__PURE__ */ jsxs("section", { children: [
885
+ /* @__PURE__ */ jsx("h3", { className: SECTION_HEADING_CLASS, children: translate("Basic details") }),
886
+ /* @__PURE__ */ jsx(
887
+ BasicDetails,
888
+ {
889
+ rule: detail,
890
+ ruleName,
891
+ programStageName
892
+ }
893
+ )
894
+ ] }),
895
+ /* @__PURE__ */ jsx(ConditionSection, { condition: detail.condition, variables }),
896
+ /* @__PURE__ */ jsx(ActionsSection, { actions: detail.programRuleActions ?? [] })
897
+ ] });
898
+ }
899
+ function RuleDetailsBody({
900
+ detail,
901
+ loading,
902
+ error,
903
+ ruleName,
904
+ programStageName,
905
+ variables
906
+ }) {
907
+ if (loading) {
908
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-[280px] items-center justify-center", children: /* @__PURE__ */ jsx(CircularLoader, { small: true }) });
909
+ }
910
+ if (error) {
911
+ return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-dhis2-red-700", children: translate("Could not load this rule: {{message}}", { message: error.message }) });
912
+ }
913
+ if (!detail) {
914
+ return null;
915
+ }
916
+ return /* @__PURE__ */ jsx(
917
+ RuleDetailsContent,
918
+ {
919
+ detail,
920
+ ruleName,
921
+ programStageName,
922
+ variables
923
+ }
924
+ );
925
+ }
926
+ function RuleDetailsFooter({ onClose }) {
927
+ return /* @__PURE__ */ jsxs(
928
+ "div",
929
+ {
930
+ style: { order: 3 },
931
+ className: "-mx-dp24 -mb-dp24 mt-dp16 flex items-center justify-between gap-dp12 border-t border-dhis2-grey-300 bg-dhis2-grey-050 px-dp24 py-[14px]",
932
+ children: [
933
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-sm text-dhis2-grey-600", children: translate("Read-only view. Edit rules in the Maintenance app.") }),
934
+ /* @__PURE__ */ jsx(ButtonStrip, { children: /* @__PURE__ */ jsx(Button, { secondary: true, onClick: onClose, children: translate("Close") }) })
935
+ ]
936
+ }
937
+ );
938
+ }
939
+ function resolveActiveRuleId(open, ruleId) {
940
+ return open ? ruleId : null;
941
+ }
942
+ function shouldRenderModal(open, ruleId) {
943
+ return open && ruleId != null;
944
+ }
945
+ function RuleDetailsModal({
946
+ open,
947
+ onClose,
948
+ ruleId,
949
+ ruleName,
950
+ status,
951
+ programStageName,
952
+ programRuleVariables
953
+ }) {
954
+ const { detail, loading, error } = useProgramRuleDetail(resolveActiveRuleId(open, ruleId));
955
+ if (!shouldRenderModal(open, ruleId)) {
956
+ return null;
957
+ }
958
+ const chip = resolveStatusChip(status);
959
+ const variables = parseConditionVariables(detail?.condition, programRuleVariables);
960
+ const title = resolveRuleDisplayName(detail, ruleName);
961
+ return /* @__PURE__ */ jsxs(Modal, { position: "middle", onClose, children: [
962
+ /* @__PURE__ */ jsx(RuleDetailsHeader, { title, chip, description: detail?.description }),
963
+ /* @__PURE__ */ jsx(ModalContent, { children: /* @__PURE__ */ jsx(
964
+ RuleDetailsBody,
965
+ {
966
+ detail,
967
+ loading,
968
+ error,
969
+ ruleName,
970
+ programStageName,
971
+ variables
972
+ }
973
+ ) }),
974
+ /* @__PURE__ */ jsx(RuleDetailsFooter, { onClose })
975
+ ] });
976
+ }
977
+
520
978
  // src/graphLayout.ts
521
979
  var ROLE_COLUMNS = {
522
980
  source: 0,
@@ -1310,6 +1768,52 @@ function TraceTimeline({
1310
1768
  ) }, entry.id);
1311
1769
  }) });
1312
1770
  }
1771
+ var REORDER_DURATION_MS = 220;
1772
+ var REORDER_EASING = "ease-out";
1773
+ function prefersReducedMotion() {
1774
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1775
+ }
1776
+ function useFlipReorder(orderedIds, containerRef) {
1777
+ const positionsRef = useRef(/* @__PURE__ */ new Map());
1778
+ useLayoutEffect(() => {
1779
+ const container = containerRef.current;
1780
+ if (!container) {
1781
+ return;
1782
+ }
1783
+ const previousPositions = positionsRef.current;
1784
+ const items = container.querySelectorAll("[data-rule-id]");
1785
+ if (!prefersReducedMotion()) {
1786
+ items.forEach((item) => {
1787
+ const id = item.dataset.ruleId;
1788
+ if (!id) {
1789
+ return;
1790
+ }
1791
+ const previousRect = previousPositions.get(id);
1792
+ if (!previousRect) {
1793
+ return;
1794
+ }
1795
+ const currentRect = item.getBoundingClientRect();
1796
+ const deltaY = previousRect.top - currentRect.top;
1797
+ if (deltaY === 0) {
1798
+ return;
1799
+ }
1800
+ item.style.transition = "none";
1801
+ item.style.transform = `translateY(${String(deltaY)}px)`;
1802
+ item.getBoundingClientRect();
1803
+ item.style.transition = `transform ${String(REORDER_DURATION_MS)}ms ${REORDER_EASING}`;
1804
+ item.style.transform = "";
1805
+ });
1806
+ }
1807
+ const nextPositions = /* @__PURE__ */ new Map();
1808
+ items.forEach((item) => {
1809
+ const id = item.dataset.ruleId;
1810
+ if (id) {
1811
+ nextPositions.set(id, item.getBoundingClientRect());
1812
+ }
1813
+ });
1814
+ positionsRef.current = nextPositions;
1815
+ }, [orderedIds.join("|")]);
1816
+ }
1313
1817
  function resolveScopeStageId(metadata) {
1314
1818
  return metadata.formKind === "event" ? metadata.programStageId : null;
1315
1819
  }
@@ -1331,6 +1835,12 @@ function resolveCardStatus(inScope, firing) {
1331
1835
  }
1332
1836
  return firing ? { label: translate("Firing"), className: "text-dhis2-teal-700" } : { label: translate("Idle"), className: "text-dhis2-grey-600" };
1333
1837
  }
1838
+ function resolveDetailsStatus(inScope, firing) {
1839
+ if (!inScope) {
1840
+ return "out-of-scope";
1841
+ }
1842
+ return firing ? "firing" : "idle";
1843
+ }
1334
1844
  function formatActionLabel(action) {
1335
1845
  if (action.targetLabel && action.detail) {
1336
1846
  return `${action.type} \xB7 ${action.targetLabel} = ${action.detail}`;
@@ -1373,6 +1883,7 @@ function RulesPanel({ metadata, showConditions = true }) {
1373
1883
  const [highlightRuleId, setHighlightRuleId] = useState(null);
1374
1884
  const [graphModalOpen, setGraphModalOpen] = useState(false);
1375
1885
  const [scopeFilter, setScopeFilter] = useState("scoped");
1886
+ const [detailsRuleId, setDetailsRuleId] = useState(null);
1376
1887
  const labelLookup = useMemo(() => createLabelLookup(metadata), [metadata]);
1377
1888
  const catalog = useMemo(() => resolveProgramRulesList(metadata), [metadata]);
1378
1889
  const scopeStageId = useMemo(() => resolveScopeStageId(metadata), [metadata]);
@@ -1427,6 +1938,15 @@ function RulesPanel({ metadata, showConditions = true }) {
1427
1938
  }
1428
1939
  return null;
1429
1940
  }, [highlightedRuleName, selectedEntry]);
1941
+ const detailsRule = useMemo(
1942
+ () => catalog.find((rule) => rule.id === detailsRuleId) ?? null,
1943
+ [catalog, detailsRuleId]
1944
+ );
1945
+ const detailsStatus = detailsRule ? resolveDetailsStatus(
1946
+ isRuleInScope(detailsRule, scopeStageId),
1947
+ activeRuleIds.has(detailsRule.id)
1948
+ ) : "idle";
1949
+ const detailsProgramStageName = detailsRule ? detailsRule.programStageId === null ? null : labelLookup.resolveStageName(detailsRule.programStageId) : void 0;
1430
1950
  const graphProps = {
1431
1951
  entries,
1432
1952
  fieldState,
@@ -1528,6 +2048,9 @@ function RulesPanel({ metadata, showConditions = true }) {
1528
2048
  onSelectRule: (ruleId) => {
1529
2049
  setHighlightRuleId(ruleId);
1530
2050
  setTab("graph");
2051
+ },
2052
+ onOpenDetails: (ruleId) => {
2053
+ setDetailsRuleId(ruleId);
1531
2054
  }
1532
2055
  }
1533
2056
  ) : tab === "trace" ? /* @__PURE__ */ jsx(
@@ -1569,11 +2092,33 @@ function RulesPanel({ metadata, showConditions = true }) {
1569
2092
  subtitle: graphSubtitle,
1570
2093
  layoutKey: graphModalOpen ? "open" : "closed"
1571
2094
  }
2095
+ ),
2096
+ /* @__PURE__ */ jsx(
2097
+ RuleDetailsModal,
2098
+ {
2099
+ open: detailsRuleId != null,
2100
+ onClose: () => {
2101
+ setDetailsRuleId(null);
2102
+ },
2103
+ ruleId: detailsRuleId,
2104
+ ruleName: detailsRule?.name ?? "",
2105
+ status: detailsStatus,
2106
+ programStageName: detailsProgramStageName,
2107
+ programRuleVariables: metadata.metadata.programRuleVariables
2108
+ }
1572
2109
  )
1573
2110
  ]
1574
2111
  }
1575
2112
  );
1576
2113
  }
2114
+ function sortRulesFiringFirst(visibleRules, activeRuleIds) {
2115
+ const firing = [];
2116
+ const idle = [];
2117
+ for (const rule of visibleRules) {
2118
+ (activeRuleIds.has(rule.id) ? firing : idle).push(rule);
2119
+ }
2120
+ return [...firing, ...idle];
2121
+ }
1577
2122
  function RulesTab({
1578
2123
  catalog,
1579
2124
  scopeStageId,
@@ -1582,21 +2127,31 @@ function RulesTab({
1582
2127
  selectedRuleId,
1583
2128
  showConditions,
1584
2129
  labelLookup,
1585
- onSelectRule
2130
+ onSelectRule,
2131
+ onOpenDetails
1586
2132
  }) {
2133
+ const listRef = useRef(null);
2134
+ const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
2135
+ const sortedRules = useMemo(
2136
+ () => sortRulesFiringFirst(visibleRules, activeRuleIds),
2137
+ [visibleRules, activeRuleIds]
2138
+ );
2139
+ useFlipReorder(
2140
+ useMemo(() => sortedRules.map((rule) => rule.id), [sortedRules]),
2141
+ listRef
2142
+ );
1587
2143
  if (!catalog.length) {
1588
2144
  return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("This program has no rules.") });
1589
2145
  }
1590
- const visibleRules = scopeFilter === "all" ? catalog : catalog.filter((rule) => isRuleInScope(rule, scopeStageId));
1591
- if (!visibleRules.length) {
2146
+ if (!sortedRules.length) {
1592
2147
  return /* @__PURE__ */ jsx("p", { className: "m-0 text-sm leading-normal text-dhis2-grey-600", children: translate("No rules are in scope for this stage.") });
1593
2148
  }
1594
- return /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-[10px] p-0", children: visibleRules.map((rule) => {
2149
+ return /* @__PURE__ */ jsx("ul", { ref: listRef, className: "m-0 flex list-none flex-col gap-[10px] p-0", children: sortedRules.map((rule) => {
1595
2150
  const inScope = isRuleInScope(rule, scopeStageId);
1596
2151
  const firing = activeRuleIds.has(rule.id);
1597
2152
  const isSelected = selectedRuleId === rule.id;
1598
2153
  const status = resolveCardStatus(inScope, firing);
1599
- return /* @__PURE__ */ jsx("li", { className: "m-0 shrink-0", children: /* @__PURE__ */ jsxs(
2154
+ return /* @__PURE__ */ jsx("li", { "data-rule-id": rule.id, className: "m-0 shrink-0", children: /* @__PURE__ */ jsxs(
1600
2155
  "article",
1601
2156
  {
1602
2157
  role: "button",
@@ -1627,13 +2182,29 @@ function RulesTab({
1627
2182
  children: rule.name
1628
2183
  }
1629
2184
  ),
1630
- /* @__PURE__ */ jsx(
1631
- "span",
1632
- {
1633
- className: `shrink-0 text-[11px] font-semibold ${status.className}`,
1634
- children: status.label
1635
- }
1636
- )
2185
+ /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-[6px]", children: [
2186
+ /* @__PURE__ */ jsx(
2187
+ "span",
2188
+ {
2189
+ className: `text-[11px] font-semibold ${status.className}`,
2190
+ children: status.label
2191
+ }
2192
+ ),
2193
+ /* @__PURE__ */ jsx(
2194
+ "button",
2195
+ {
2196
+ type: "button",
2197
+ title: translate("Program rule details"),
2198
+ "aria-label": translate("Program rule details"),
2199
+ onClick: (event) => {
2200
+ event.stopPropagation();
2201
+ onOpenDetails(rule.id);
2202
+ },
2203
+ className: "inline-flex size-[22px] shrink-0 cursor-pointer items-center justify-center rounded-[3px] border-0 bg-transparent text-dhis2-grey-600 hover:bg-dhis2-grey-200 hover:text-dhis2-blue-600",
2204
+ children: /* @__PURE__ */ jsx(IconInfo16, {})
2205
+ }
2206
+ )
2207
+ ] })
1637
2208
  ] }),
1638
2209
  rule.programRuleActions.length ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-[6px]", children: rule.programRuleActions.map((action, index) => {
1639
2210
  const summary = formatRuleActionSummary(
@@ -1666,6 +2237,6 @@ function RulesTab({
1666
2237
  }) });
1667
2238
  }
1668
2239
 
1669
- export { EFFECT_ICONS, RuleDevtoolsScope, RulesPanel, createLabelLookup, getEffectEdgeStroke, getEffectShortLabel, getEffectTagRenderProps, getEffectTagRenderPropsForVariant, getEffectVariant, getEffectVisual };
2240
+ export { EFFECT_ICONS, RuleDetailsModal, RuleDevtoolsScope, RulesPanel, createLabelLookup, getEffectEdgeStroke, getEffectShortLabel, getEffectTagRenderProps, getEffectTagRenderPropsForVariant, getEffectVariant, getEffectVisual };
1670
2241
  //# sourceMappingURL=index.js.map
1671
2242
  //# sourceMappingURL=index.js.map