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