@outbuild-company/schedule-core 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -245,6 +245,23 @@ function dispatchVisibilitySet(action, deps) {
245
245
  }
246
246
 
247
247
  // src/internal/filter/field-registry.ts
248
+ var STATUS_ORDER = [
249
+ "Waiting",
250
+ "Done",
251
+ "Doing",
252
+ "Overdue",
253
+ "Advancement"
254
+ ];
255
+ var CONSTRAINT_TYPE_ORDER = [
256
+ "mfo",
257
+ "mso",
258
+ "snlt",
259
+ "snet",
260
+ "alap",
261
+ "asap",
262
+ "fnet",
263
+ "fnlt"
264
+ ];
248
265
  var MILLISECONDS_PER_DAY = 1e3 * 60 * 60 * 24;
249
266
  function toDays(hours, hoursPerDay) {
250
267
  if (hours === null) return null;
@@ -353,20 +370,34 @@ var FIELD_REGISTRY = {
353
370
  extract: (activity) => activity.responsableIds
354
371
  },
355
372
  tagIds: { valueKind: "id-array", extract: (activity) => activity.tagIds },
356
- status: { valueKind: "enum", extract: (activity) => activity.status },
373
+ status: {
374
+ valueKind: "enum",
375
+ order: STATUS_ORDER,
376
+ extract: (activity) => activity.status
377
+ },
357
378
  constraintType: {
358
379
  valueKind: "enum",
380
+ order: CONSTRAINT_TYPE_ORDER,
359
381
  extract: (activity) => activity.constraintType
360
382
  },
361
383
  calendarId: {
362
- valueKind: "enum",
384
+ valueKind: "reference",
363
385
  extract: (activity) => activity.calendarId === null ? null : String(activity.calendarId)
364
386
  },
387
+ // Normalized to string like calendarId. It is stored as a number, so it used
388
+ // to reach compareValues through the numeric branch while its sibling
389
+ // reference went through the string one — the same field kind comparing two
390
+ // different ways. localeCompare with numeric:true keeps the digit ordering.
365
391
  subcontractId: {
366
- valueKind: "enum",
367
- extract: (activity) => activity.subcontractId
392
+ valueKind: "reference",
393
+ extract: (activity) => activity.subcontractId === null ? null : String(activity.subcontractId)
368
394
  },
369
- isCritical: { valueKind: "enum", extract: (activity) => activity.isCritical }
395
+ // Declared boolean, not enum: it is one, and saying so is what lets the enum
396
+ // branch demand an order without inventing one for true/false.
397
+ isCritical: {
398
+ valueKind: "boolean",
399
+ extract: (activity) => activity.isCritical
400
+ }
370
401
  };
371
402
  function getFieldDescriptor(field) {
372
403
  return FIELD_REGISTRY[field] ?? null;
@@ -493,6 +524,12 @@ function evaluateVisibleIds(input) {
493
524
  return visibleIds;
494
525
  }
495
526
 
527
+ // src/internal/filter/context.ts
528
+ var COLLATION_LOCALE = "en";
529
+ function buildFilterContext(hoursPerDay) {
530
+ return { hoursPerDay, locale: COLLATION_LOCALE };
531
+ }
532
+
496
533
  // src/internal/filter/validate.ts
497
534
  var OPERATORS_BY_KIND = {
498
535
  string: /* @__PURE__ */ new Set(["includes", "notIncludes", "is", "isNot"]),
@@ -506,7 +543,11 @@ var OPERATORS_BY_KIND = {
506
543
  ]),
507
544
  date: /* @__PURE__ */ new Set(["after", "before"]),
508
545
  "id-array": /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
509
- enum: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
546
+ enum: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
547
+ // Same membership operators as enum: splitting the kinds was about ordering,
548
+ // not about filtering, and these two filter exactly like an enum.
549
+ boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
550
+ reference: /* @__PURE__ */ new Set(["someOf", "notSomeOf"])
510
551
  };
511
552
  function validateCriterion(criterion) {
512
553
  const descriptor = getFieldDescriptor(criterion.field);
@@ -551,7 +592,7 @@ function resolveVisibleIds(adapter, filter, hoursPerDay) {
551
592
  activities,
552
593
  parentOf: (activityId) => adapter.getParentId(activityId),
553
594
  filter,
554
- context: { hoursPerDay }
595
+ context: buildFilterContext(hoursPerDay)
555
596
  });
556
597
  }
557
598
  function dispatchFilterSet(action, deps) {
@@ -579,6 +620,140 @@ function dispatchFilterSet(action, deps) {
579
620
  return { ok: true, changes };
580
621
  }
581
622
 
623
+ // src/internal/hierarchy/root-parent.ts
624
+ var ROOT_PARENT_ID = "0";
625
+ function isRootParent(parent) {
626
+ return parent == null || parent === 0 || parent === ROOT_PARENT_ID;
627
+ }
628
+ function normalizeParentKey(parent) {
629
+ return isRootParent(parent) ? ROOT_PARENT_ID : String(parent);
630
+ }
631
+
632
+ // src/internal/hierarchy/visual-order.ts
633
+ var NOT_SET_SORT_VALUE = Number.MAX_SAFE_INTEGER;
634
+ function getChildrenInVisualOrder(parentId, adapter) {
635
+ const children = collectChildrenSnapshots(parentId, adapter);
636
+ const userOrder = adapter.getOrderComparator?.() ?? null;
637
+ children.sort(
638
+ userOrder === null ? compareActivitiesInVisualOrder : (a, b) => userOrder(a, b) || compareActivitiesInVisualOrder(a, b)
639
+ );
640
+ return children.map((a) => String(a.id));
641
+ }
642
+ function iterateInVisualOrder(adapter) {
643
+ const result = [];
644
+ const visit = (activity) => {
645
+ result.push(activity);
646
+ const childIds = getChildrenInVisualOrder(String(activity.id), adapter);
647
+ for (const childId of childIds) {
648
+ const child = adapter.getActivity(childId);
649
+ if (child) visit(child);
650
+ }
651
+ };
652
+ const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter);
653
+ for (const rootId of rootIds) {
654
+ const root = adapter.getActivity(rootId);
655
+ if (root) visit(root);
656
+ }
657
+ return result;
658
+ }
659
+ function findPreviousNonSelectedSibling(taskId, selected, adapter) {
660
+ const activity = adapter.getActivity(taskId);
661
+ if (!activity) return null;
662
+ const parentKey = parentKeyOf(activity);
663
+ const siblings = getChildrenInVisualOrder(parentKey, adapter);
664
+ const idx = siblings.findIndex((id) => String(id) === String(taskId));
665
+ if (idx <= 0) return null;
666
+ for (let i = idx - 1; i >= 0; i--) {
667
+ const candidateId = siblings[i];
668
+ if (candidateId === void 0) continue;
669
+ if (!setHas(selected, candidateId)) return candidateId;
670
+ }
671
+ return null;
672
+ }
673
+ function visualIndexInParent(taskId, adapter) {
674
+ const activity = adapter.getActivity(taskId);
675
+ if (!activity) return -1;
676
+ const parentKey = parentKeyOf(activity);
677
+ const siblings = getChildrenInVisualOrder(parentKey, adapter);
678
+ return siblings.findIndex((id) => String(id) === String(taskId));
679
+ }
680
+ function collectChildrenSnapshots(parentId, adapter) {
681
+ const key = String(parentId);
682
+ if (key === "0") {
683
+ return adapter.getAllActivities().filter((a) => a.parentId === null);
684
+ }
685
+ const childIds = adapter.getChildren(parentId);
686
+ const out = [];
687
+ for (const id of childIds) {
688
+ const snap = adapter.getActivity(id);
689
+ if (snap) out.push(snap);
690
+ }
691
+ return out;
692
+ }
693
+ function compareActivitiesInVisualOrder(a, b) {
694
+ const ac = correlativeIdOf(a);
695
+ const bc = correlativeIdOf(b);
696
+ if (ac !== bc) return ac - bc;
697
+ return String(a.id).localeCompare(String(b.id));
698
+ }
699
+ function correlativeIdOf(activity) {
700
+ const raw = activity.correlativeId;
701
+ const n = typeof raw === "number" ? raw : Number(raw);
702
+ return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
703
+ }
704
+ function parentKeyOf(activity) {
705
+ const parentId = activity.parentId;
706
+ if (parentId === null) return "0";
707
+ return parentId;
708
+ }
709
+ function setHas(set, id) {
710
+ if (set.has(id)) return true;
711
+ return set.has(String(id));
712
+ }
713
+
714
+ // src/dispatch/shared/collect-branch-order.ts
715
+ function collectBranchOrder(adapter) {
716
+ const branches = [];
717
+ const visit = (parentId) => {
718
+ const childIds = getChildrenInVisualOrder(parentId, adapter);
719
+ if (childIds.length > 1) {
720
+ branches.push({ parentId, childIds });
721
+ }
722
+ for (const childId of childIds) {
723
+ visit(childId);
724
+ }
725
+ };
726
+ visit(ROOT_PARENT_ID);
727
+ return branches;
728
+ }
729
+
730
+ // src/dispatch/order.ts
731
+ function validateRule(rule) {
732
+ if (getFieldDescriptor(rule.field) === null) return "unknown_field";
733
+ if (rule.direction !== "asc" && rule.direction !== "desc") {
734
+ return "unknown_operator";
735
+ }
736
+ return null;
737
+ }
738
+ function dispatchSortSet(action, deps) {
739
+ const { adapter } = deps;
740
+ for (const rule of action.rules) {
741
+ const rejection = validateRule(rule);
742
+ if (rejection !== null) return { ok: false, reason: rejection };
743
+ }
744
+ const order = action.rules.length === 0 ? null : { rules: action.rules };
745
+ adapter.setActiveOrder(order);
746
+ const changes = {
747
+ source: action,
748
+ activities: [],
749
+ links: [],
750
+ calendars: [],
751
+ trackingEvents: [],
752
+ order: collectBranchOrder(adapter)
753
+ };
754
+ return { ok: true, changes };
755
+ }
756
+
582
757
  // src/columns/text/constants.ts
583
758
  var TEXT = "text";
584
759
 
@@ -1885,15 +2060,6 @@ function isSummaryActivity(activity, adapter) {
1885
2060
  return isSummary(activity.type, adapter.getChildren(activity.id).length > 0);
1886
2061
  }
1887
2062
 
1888
- // src/internal/hierarchy/root-parent.ts
1889
- var ROOT_PARENT_ID = "0";
1890
- function isRootParent(parent) {
1891
- return parent == null || parent === 0 || parent === ROOT_PARENT_ID;
1892
- }
1893
- function normalizeParentKey(parent) {
1894
- return isRootParent(parent) ? ROOT_PARENT_ID : String(parent);
1895
- }
1896
-
1897
2063
  // src/autoscheduler/engine/alap-pass.ts
1898
2064
  async function alapPass(reversedIds, links, asapPlans, adapter, _options, isCurrent) {
1899
2065
  const plans = new Map(asapPlans);
@@ -8943,6 +9109,7 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
8943
9109
  "selection-replace",
8944
9110
  "visibility-set",
8945
9111
  "filter-set",
9112
+ "sort-set",
8946
9113
  "sir-sync",
8947
9114
  "activity-lookahead-sync",
8948
9115
  "persistence-acknowledge",
@@ -8954,9 +9121,10 @@ var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
8954
9121
  "selection-toggle",
8955
9122
  "selection-replace",
8956
9123
  "visibility-set",
8957
- "filter-set"
9124
+ "filter-set",
9125
+ "sort-set"
8958
9126
  ]);
8959
- function reappliesActiveFilter(action) {
9127
+ function reappliesViewState(action) {
8960
9128
  return !PURE_VIEW_STATE_KINDS.has(action.kind);
8961
9129
  }
8962
9130
  var NO_STRUCTURAL_EXEMPTIONS = /* @__PURE__ */ new Set();
@@ -9828,85 +9996,6 @@ var DISPATCH_TRACK_EVENT = {
9828
9996
  ACTIVITY_OUTDENT: "schedule_activity_outdent"
9829
9997
  };
9830
9998
 
9831
- // src/internal/hierarchy/visual-order.ts
9832
- var NOT_SET_SORT_VALUE = Number.MAX_SAFE_INTEGER;
9833
- function getChildrenInVisualOrder(parentId, adapter) {
9834
- const children = collectChildrenSnapshots(parentId, adapter);
9835
- children.sort(compareActivitiesInVisualOrder);
9836
- return children.map((a) => String(a.id));
9837
- }
9838
- function iterateInVisualOrder(adapter) {
9839
- const result = [];
9840
- const visit = (activity) => {
9841
- result.push(activity);
9842
- const childIds = getChildrenInVisualOrder(String(activity.id), adapter);
9843
- for (const childId of childIds) {
9844
- const child = adapter.getActivity(childId);
9845
- if (child) visit(child);
9846
- }
9847
- };
9848
- const rootIds = getChildrenInVisualOrder(ROOT_PARENT_ID, adapter);
9849
- for (const rootId of rootIds) {
9850
- const root = adapter.getActivity(rootId);
9851
- if (root) visit(root);
9852
- }
9853
- return result;
9854
- }
9855
- function findPreviousNonSelectedSibling(taskId, selected, adapter) {
9856
- const activity = adapter.getActivity(taskId);
9857
- if (!activity) return null;
9858
- const parentKey = parentKeyOf(activity);
9859
- const siblings = getChildrenInVisualOrder(parentKey, adapter);
9860
- const idx = siblings.findIndex((id) => String(id) === String(taskId));
9861
- if (idx <= 0) return null;
9862
- for (let i = idx - 1; i >= 0; i--) {
9863
- const candidateId = siblings[i];
9864
- if (candidateId === void 0) continue;
9865
- if (!setHas(selected, candidateId)) return candidateId;
9866
- }
9867
- return null;
9868
- }
9869
- function visualIndexInParent(taskId, adapter) {
9870
- const activity = adapter.getActivity(taskId);
9871
- if (!activity) return -1;
9872
- const parentKey = parentKeyOf(activity);
9873
- const siblings = getChildrenInVisualOrder(parentKey, adapter);
9874
- return siblings.findIndex((id) => String(id) === String(taskId));
9875
- }
9876
- function collectChildrenSnapshots(parentId, adapter) {
9877
- const key = String(parentId);
9878
- if (key === "0") {
9879
- return adapter.getAllActivities().filter((a) => a.parentId === null);
9880
- }
9881
- const childIds = adapter.getChildren(parentId);
9882
- const out = [];
9883
- for (const id of childIds) {
9884
- const snap = adapter.getActivity(id);
9885
- if (snap) out.push(snap);
9886
- }
9887
- return out;
9888
- }
9889
- function compareActivitiesInVisualOrder(a, b) {
9890
- const ac = correlativeIdOf(a);
9891
- const bc = correlativeIdOf(b);
9892
- if (ac !== bc) return ac - bc;
9893
- return String(a.id).localeCompare(String(b.id));
9894
- }
9895
- function correlativeIdOf(activity) {
9896
- const raw = activity.correlativeId;
9897
- const n = typeof raw === "number" ? raw : Number(raw);
9898
- return Number.isFinite(n) ? n : NOT_SET_SORT_VALUE;
9899
- }
9900
- function parentKeyOf(activity) {
9901
- const parentId = activity.parentId;
9902
- if (parentId === null) return "0";
9903
- return parentId;
9904
- }
9905
- function setHas(set, id) {
9906
- if (set.has(id)) return true;
9907
- return set.has(String(id));
9908
- }
9909
-
9910
9999
  // src/internal/hierarchy/recompute-correlative-ids.ts
9911
10000
  function recomputeCorrelativeIds(adapter) {
9912
10001
  adapter.invalidateVisualOrderIds?.();
@@ -12752,6 +12841,9 @@ async function dispatch(action, options, deps) {
12752
12841
  hoursPerDay: deps.sector.hoursPerDay
12753
12842
  });
12754
12843
  }
12844
+ if (action.kind === "sort-set") {
12845
+ return dispatchSortSet(action, { adapter: deps.adapter });
12846
+ }
12755
12847
  if (action.kind === "sir-sync") {
12756
12848
  return dispatchSirSync(action, deps);
12757
12849
  }
@@ -15839,6 +15931,7 @@ var ViewStateStore = class {
15839
15931
  checkedSet = /* @__PURE__ */ new Set();
15840
15932
  hiddenSet = /* @__PURE__ */ new Set();
15841
15933
  activeFilter = null;
15934
+ activeOrder = null;
15842
15935
  isChecked(activityId) {
15843
15936
  return this.checkedSet.has(activityId);
15844
15937
  }
@@ -15854,6 +15947,12 @@ var ViewStateStore = class {
15854
15947
  setActiveFilter(nextFilter) {
15855
15948
  this.activeFilter = nextFilter;
15856
15949
  }
15950
+ getActiveOrder() {
15951
+ return this.activeOrder;
15952
+ }
15953
+ setActiveOrder(nextOrder) {
15954
+ this.activeOrder = nextOrder;
15955
+ }
15857
15956
  checkedIds() {
15858
15957
  return [...this.checkedSet];
15859
15958
  }
@@ -15880,18 +15979,21 @@ var ViewStateStore = class {
15880
15979
  this.checkedSet = new Set(seed2.checkedIds ?? []);
15881
15980
  this.hiddenSet = new Set(seed2.hiddenIds ?? []);
15882
15981
  this.activeFilter = null;
15982
+ this.activeOrder = null;
15883
15983
  }
15884
15984
  snapshot() {
15885
15985
  return {
15886
15986
  checked: new Set(this.checkedSet),
15887
15987
  hidden: new Set(this.hiddenSet),
15888
- activeFilter: this.activeFilter
15988
+ activeFilter: this.activeFilter,
15989
+ activeOrder: this.activeOrder
15889
15990
  };
15890
15991
  }
15891
15992
  restore(snapshot) {
15892
15993
  this.checkedSet = new Set(snapshot.checked);
15893
15994
  this.hiddenSet = new Set(snapshot.hidden);
15894
15995
  this.activeFilter = snapshot.activeFilter;
15996
+ this.activeOrder = snapshot.activeOrder;
15895
15997
  }
15896
15998
  };
15897
15999
 
@@ -15904,9 +16006,11 @@ var ScheduleState = class {
15904
16006
  _outgoing = /* @__PURE__ */ new Map();
15905
16007
  _incoming = /* @__PURE__ */ new Map();
15906
16008
  _hierarchy;
16009
+ _buildOrderComparator;
15907
16010
  _cache = {
15908
16011
  ids: null,
15909
- visualOrderIds: null
16012
+ visualOrderIds: null,
16013
+ orderComparator: null
15910
16014
  };
15911
16015
  _writeCapture = new WriteCapture();
15912
16016
  _viewState = new ViewStateStore();
@@ -15915,7 +16019,8 @@ var ScheduleState = class {
15915
16019
  _promotionSnapshotBefore = null;
15916
16020
  _calendar;
15917
16021
  _flags;
15918
- constructor(snapshot) {
16022
+ constructor(snapshot, buildOrderComparator = () => null) {
16023
+ this._buildOrderComparator = buildOrderComparator;
15919
16024
  for (const activity of snapshot.activities) {
15920
16025
  this._activities.set(activity.id, activity);
15921
16026
  }
@@ -16024,8 +16129,47 @@ var ScheduleState = class {
16024
16129
  this._captureViewStateOnce();
16025
16130
  this._viewState.setActiveFilter(nextFilter);
16026
16131
  }
16132
+ getActiveOrder() {
16133
+ return this._viewState.getActiveOrder();
16134
+ }
16135
+ setActiveOrder(nextOrder) {
16136
+ this._captureViewStateOnce();
16137
+ this._viewState.setActiveOrder(nextOrder);
16138
+ this.invalidateDerivedOrder();
16139
+ }
16140
+ /**
16141
+ * Rebuild-on-demand, memoized: at most one build per invalidation, never one
16142
+ * per comparison. getChildrenInVisualOrder asks for this once and then sorts a
16143
+ * whole sibling group with it, and collectBranchOrder walks every branch, so a
16144
+ * build per read would be thousands per dispatch.
16145
+ *
16146
+ * The factory reads hoursPerDay and the collation locale at build time, so a
16147
+ * rebuilt comparator always reflects the current context — that is what keeps
16148
+ * ordering and filtering from seeing two different hoursPerDay.
16149
+ */
16150
+ getOrderComparator() {
16151
+ const activeOrder = this._viewState.getActiveOrder();
16152
+ if (activeOrder === null) {
16153
+ this._cache.orderComparator = null;
16154
+ return null;
16155
+ }
16156
+ if (this._cache.orderComparator) return this._cache.orderComparator;
16157
+ this._cache.orderComparator = this._buildOrderComparator(activeOrder);
16158
+ return this._cache.orderComparator;
16159
+ }
16160
+ /**
16161
+ * Both order caches, always together. The comparator decides the sequence, so
16162
+ * a rebuilt comparator with a surviving sequence would paint the old order.
16163
+ * invalidateVisualOrderIds stays separate on purpose: a structural change
16164
+ * moves rows without touching the rules, so the comparator is still valid.
16165
+ */
16166
+ invalidateDerivedOrder() {
16167
+ this._cache.orderComparator = null;
16168
+ this._cache.visualOrderIds = null;
16169
+ }
16027
16170
  hydrateViewState(seed2) {
16028
16171
  this._viewState.hydrate(seed2);
16172
+ this.invalidateDerivedOrder();
16029
16173
  }
16030
16174
  _captureViewStateOnce() {
16031
16175
  if (this._writeCapture.peek() === null) return;
@@ -16225,6 +16369,7 @@ var ScheduleState = class {
16225
16369
  this._restoreActivityFields(before);
16226
16370
  if (this._viewStateBefore !== null) {
16227
16371
  this._viewState.restore(this._viewStateBefore);
16372
+ this.invalidateDerivedOrder();
16228
16373
  }
16229
16374
  if (this._promotionSnapshotBefore !== null) {
16230
16375
  this._promotionSnapshot = this._promotionSnapshotBefore;
@@ -16350,7 +16495,7 @@ function readChildrenIds(state, parentId) {
16350
16495
  });
16351
16496
  return roots;
16352
16497
  }
16353
- return [...state.getChildren(key)].map(String);
16498
+ return getChildrenInVisualOrder(key, state);
16354
16499
  }
16355
16500
  function readSelectedActivityIds(state) {
16356
16501
  return state.checkedIds().map(String);
@@ -16494,7 +16639,7 @@ function mergeCoalesced(top, next) {
16494
16639
  }
16495
16640
  function getDispatchHistoryPolicy(action) {
16496
16641
  if (action.kind === "persistence-acknowledge") return "clear-on-success";
16497
- if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set") {
16642
+ if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
16498
16643
  return "skip";
16499
16644
  }
16500
16645
  return "record";
@@ -16781,6 +16926,45 @@ var UndoRecorder = class {
16781
16926
  }
16782
16927
  };
16783
16928
 
16929
+ // src/dispatch/shared/resequenced-parents.ts
16930
+ var POSITIONAL_FIELDS = ["parentId", "correlativeId"];
16931
+ function collectResequencedParents(changes, parentOf) {
16932
+ const touched = /* @__PURE__ */ new Set();
16933
+ const addParentOf = (activityId) => {
16934
+ const parentId = parentOf(activityId);
16935
+ touched.add(parentId === null ? ROOT_PARENT_ID : String(parentId));
16936
+ };
16937
+ for (const change of changes.activities) {
16938
+ const activityId = String(change.id);
16939
+ if (change.kind === "created") {
16940
+ addParentOf(activityId);
16941
+ continue;
16942
+ }
16943
+ if (change.kind === "deleted") {
16944
+ const deletedFields = change.fields;
16945
+ if (deletedFields && "parentId" in deletedFields) {
16946
+ touched.add(parentKeyOf2(deletedFields.parentId?.before));
16947
+ }
16948
+ continue;
16949
+ }
16950
+ const fields = change.fields;
16951
+ if (!fields) continue;
16952
+ const movedPositionally = POSITIONAL_FIELDS.some(
16953
+ (field) => field in fields
16954
+ );
16955
+ if (!movedPositionally) continue;
16956
+ addParentOf(activityId);
16957
+ if ("parentId" in fields) {
16958
+ touched.add(parentKeyOf2(fields.parentId?.before));
16959
+ }
16960
+ }
16961
+ return touched;
16962
+ }
16963
+ function parentKeyOf2(parentId) {
16964
+ if (parentId === null || parentId === void 0) return ROOT_PARENT_ID;
16965
+ return String(parentId);
16966
+ }
16967
+
16784
16968
  // src/init/build-state-snapshot.ts
16785
16969
  function buildStoreLink(link) {
16786
16970
  return {
@@ -18094,6 +18278,65 @@ var CustomIdTracker = class {
18094
18278
  }
18095
18279
  };
18096
18280
 
18281
+ // src/internal/order/build-comparator.ts
18282
+ var MISSING_LAST = 1;
18283
+ var MISSING_FIRST = -1;
18284
+ function compareMissing(aMissing, bMissing) {
18285
+ if (!aMissing && !bMissing) return null;
18286
+ if (aMissing && bMissing) return 0;
18287
+ return aMissing ? MISSING_LAST : MISSING_FIRST;
18288
+ }
18289
+ function isMissing(value) {
18290
+ return value === null || value === void 0;
18291
+ }
18292
+ function enumRank(value, order) {
18293
+ const index = order.indexOf(String(value));
18294
+ return index === -1 ? order.length : index;
18295
+ }
18296
+ function compareValues(a, b, locale) {
18297
+ if (a instanceof Date && b instanceof Date) {
18298
+ return a.getTime() - b.getTime();
18299
+ }
18300
+ if (typeof a === "number" && typeof b === "number") {
18301
+ return a - b;
18302
+ }
18303
+ if (typeof a === "boolean" && typeof b === "boolean") {
18304
+ return Number(a) - Number(b);
18305
+ }
18306
+ if (Array.isArray(a) && Array.isArray(b)) {
18307
+ return a.length - b.length;
18308
+ }
18309
+ return String(a).localeCompare(String(b), locale, {
18310
+ sensitivity: "base",
18311
+ numeric: true
18312
+ });
18313
+ }
18314
+ function compareByRule(rule, context) {
18315
+ const descriptor = getFieldDescriptor(rule.field);
18316
+ if (descriptor === null) return null;
18317
+ return (a, b) => {
18318
+ const valueA = descriptor.extract(a, context);
18319
+ const valueB = descriptor.extract(b, context);
18320
+ const missing = compareMissing(isMissing(valueA), isMissing(valueB));
18321
+ if (missing !== null) return missing;
18322
+ const result = descriptor.valueKind === "enum" ? enumRank(valueA, descriptor.order) - enumRank(valueB, descriptor.order) : compareValues(valueA, valueB, context.locale);
18323
+ return rule.direction === "desc" ? -result : result;
18324
+ };
18325
+ }
18326
+ function buildComparator(order, context) {
18327
+ const comparators = order.rules.map((rule) => compareByRule(rule, context)).filter(
18328
+ (comparator) => comparator !== null
18329
+ );
18330
+ if (comparators.length === 0) return null;
18331
+ return (a, b) => {
18332
+ for (const comparator of comparators) {
18333
+ const result = comparator(a, b);
18334
+ if (result !== 0) return result;
18335
+ }
18336
+ return 0;
18337
+ };
18338
+ }
18339
+
18097
18340
  // src/internal/post-processors/demote-childless-summaries.ts
18098
18341
  function demoteChildlessSummaries(state) {
18099
18342
  const idsToDemote = [];
@@ -18274,7 +18517,8 @@ function initializeCore(input) {
18274
18517
  parsed2.calendars,
18275
18518
  baseCalendars
18276
18519
  );
18277
- const state = new ScheduleState(snapshot);
18520
+ const buildOrderComparator = (order) => buildComparator(order, buildFilterContext(sector.hoursPerDay));
18521
+ const state = new ScheduleState(snapshot, buildOrderComparator);
18278
18522
  state.hydrateViewState({ hiddenIds: parsed2.viewStateSeed.hiddenIds });
18279
18523
  const scheduler = new AutoScheduler(state, reporter);
18280
18524
  demoteChildlessSummaries(state);
@@ -18426,6 +18670,14 @@ var ScheduleCore = class {
18426
18670
  this.assertReady();
18427
18671
  return this.coreRuntime.state.hiddenIds().map(String);
18428
18672
  }
18673
+ getActiveFilter() {
18674
+ this.assertReady();
18675
+ return this.coreRuntime.state.getActiveFilter();
18676
+ }
18677
+ getActiveOrder() {
18678
+ this.assertReady();
18679
+ return this.coreRuntime.state.getActiveOrder();
18680
+ }
18429
18681
  getVisualOrderIds() {
18430
18682
  this.assertReady();
18431
18683
  return [...this.coreRuntime.state.getVisualOrderIds()];
@@ -18542,7 +18794,7 @@ var ScheduleCore = class {
18542
18794
  this._saveTracker.snapshotLinks(this.getAllLinksView());
18543
18795
  this._undo.clear();
18544
18796
  }
18545
- result = this._withReappliedFilter(action, result);
18797
+ result = this._withReappliedViewState(action, result);
18546
18798
  if (!result.ok) return result;
18547
18799
  if (dispatchChangesSchedulingState(action) && changeSetIsSubstantive(result.changes)) {
18548
18800
  this._recordScheduleMutation(options.skipCriticalPath !== true);
@@ -18564,11 +18816,82 @@ var ScheduleCore = class {
18564
18816
  await this.recomputeCriticalPath();
18565
18817
  }
18566
18818
  }
18567
- _withReappliedFilter(action, result) {
18568
- if (!result.ok || !reappliesActiveFilter(action)) return result;
18569
- const merged = this._reapplyActiveFilter(result.changes);
18819
+ _withReappliedViewState(action, result) {
18820
+ if (!result.ok || !reappliesViewState(action)) return result;
18821
+ const merged = this._reapplyViewState(result.changes);
18570
18822
  return merged === null ? result : { ...result, changes: merged };
18571
18823
  }
18824
+ /**
18825
+ * The single place the two view-state passes are chained. Dispatch, undo and
18826
+ * redo all route through here: when this existed only inside the dispatch
18827
+ * path, undo and redo reapplied the filter and forgot the order, so an edit
18828
+ * that moved a row and was then undone left the row in its new position.
18829
+ *
18830
+ * The sequence between the passes is indifferent. Filter and order are
18831
+ * orthogonal projections over the same tree — the filter decides which rows
18832
+ * exist on screen and never reads the order; the order sequences every
18833
+ * sibling group from the hierarchy index and never reads visibility. Either
18834
+ * sequence produces the same ChangeSet.
18835
+ *
18836
+ * Both passes answer null while their state is inactive, so an unfiltered,
18837
+ * unsorted schedule pays two property reads.
18838
+ */
18839
+ _reapplyViewState(changes) {
18840
+ const withFilter = this._reapplyActiveFilter(changes);
18841
+ const withOrder = this._reapplyActiveOrder(withFilter ?? changes);
18842
+ const sequenced = this._emitTouchedBranchOrder(
18843
+ withOrder ?? withFilter ?? changes
18844
+ );
18845
+ return sequenced ?? withOrder ?? withFilter;
18846
+ }
18847
+ /**
18848
+ * Emits `order` for the branches this mutation resequenced, when no user order
18849
+ * is active.
18850
+ *
18851
+ * The contract is that `order` reports a CHANGED SEQUENCE, not the presence of
18852
+ * a sort. Tying emission to the cause instead of the effect is what produced
18853
+ * the undo bug and, later, the reparent ones: without an active order a move,
18854
+ * an indent, an outdent or the undo of any of them rearranged rows and said
18855
+ * nothing, so an incremental consumer kept the old sequence. The undo of a
18856
+ * reparent was the worst of them — it carried neither `order` nor a single
18857
+ * correlativeId, so the position was not recoverable by any consumer.
18858
+ *
18859
+ * The touched branches are derived from the ChangeSet rather than accumulated
18860
+ * in the state: an entity whose parentId or correlativeId moved, plus the
18861
+ * parents of created and deleted rows, is exactly the set of branches whose
18862
+ * sequence can differ. That keeps this linear in the blast radius, adds
18863
+ * nothing to the write path, and cannot leak across dispatches.
18864
+ */
18865
+ _emitTouchedBranchOrder(changes) {
18866
+ const adapter = this.coreRuntime.state;
18867
+ if (adapter.getActiveOrder() !== null) return null;
18868
+ const touched = collectResequencedParents(
18869
+ changes,
18870
+ (activityId) => adapter.getParentId(activityId)
18871
+ );
18872
+ if (touched.size === 0) return null;
18873
+ const order = [...touched].map((parentId) => ({
18874
+ parentId,
18875
+ childIds: getChildrenInVisualOrder(parentId, adapter)
18876
+ })).filter((branch) => branch.childIds.length > 1);
18877
+ if (order.length === 0) return null;
18878
+ return { ...changes, order };
18879
+ }
18880
+ /**
18881
+ * Re-sequences the grid after any mutation that could have changed a value the
18882
+ * active order sorts by. Ordering is a view over the data, so an edit that
18883
+ * moves a row past its sibling must move the row, exactly as the filter makes
18884
+ * a no-longer-matching row disappear.
18885
+ *
18886
+ * Production only re-sorts after a bar drag; diverging from that is a
18887
+ * deliberate product decision, not an oversight.
18888
+ */
18889
+ _reapplyActiveOrder(changes) {
18890
+ const adapter = this.coreRuntime.state;
18891
+ if (adapter.getActiveOrder() === null) return null;
18892
+ adapter.invalidateDerivedOrder();
18893
+ return { ...changes, order: collectBranchOrder(adapter) };
18894
+ }
18572
18895
  _reapplyActiveFilter(changes) {
18573
18896
  const filter = this.coreRuntime.state.getActiveFilter();
18574
18897
  if (filter === null) return null;
@@ -18577,7 +18900,7 @@ var ScheduleCore = class {
18577
18900
  activities: adapter.getAllActivities(),
18578
18901
  parentOf: (activityId) => adapter.getParentId(activityId),
18579
18902
  filter,
18580
- context: { hoursPerDay: this.coreRuntime.sector.hoursPerDay }
18903
+ context: buildFilterContext(this.coreRuntime.sector.hoursPerDay)
18581
18904
  });
18582
18905
  const viewState = applyVisibleSet(adapter, visibleIds);
18583
18906
  if (viewState.length === 0) return null;
@@ -18736,7 +19059,7 @@ var ScheduleCore = class {
18736
19059
  entry,
18737
19060
  "before"
18738
19061
  );
18739
- return this._reapplyActiveFilter(changes) ?? changes;
19062
+ return this._reapplyViewState(changes) ?? changes;
18740
19063
  });
18741
19064
  void operation.then(
18742
19065
  (changes) => {
@@ -18769,7 +19092,7 @@ var ScheduleCore = class {
18769
19092
  entry,
18770
19093
  "after"
18771
19094
  );
18772
- return this._reapplyActiveFilter(changes) ?? changes;
19095
+ return this._reapplyViewState(changes) ?? changes;
18773
19096
  });
18774
19097
  void operation.then(
18775
19098
  (changes) => {
@@ -18903,6 +19226,7 @@ var DISPATCH_ACTION_KIND = {
18903
19226
  SELECTION_REPLACE: "selection-replace",
18904
19227
  VISIBILITY_SET: "visibility-set",
18905
19228
  FILTER_SET: "filter-set",
19229
+ SORT_SET: "sort-set",
18906
19230
  SIR_SYNC: "sir-sync",
18907
19231
  ACTIVITY_LOOKAHEAD_SYNC: "activity-lookahead-sync"
18908
19232
  };
@@ -18929,6 +19253,7 @@ var KIND_CATALOG_COVERS_UNION = {
18929
19253
  [DISPATCH_ACTION_KIND.SELECTION_REPLACE]: true,
18930
19254
  [DISPATCH_ACTION_KIND.VISIBILITY_SET]: true,
18931
19255
  [DISPATCH_ACTION_KIND.FILTER_SET]: true,
19256
+ [DISPATCH_ACTION_KIND.SORT_SET]: true,
18932
19257
  [DISPATCH_ACTION_KIND.SIR_SYNC]: true,
18933
19258
  [DISPATCH_ACTION_KIND.ACTIVITY_LOOKAHEAD_SYNC]: true
18934
19259
  };