@outbuild-company/schedule-core 1.35.2 → 1.36.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.d.cts CHANGED
@@ -685,9 +685,9 @@ interface GroupLevelWindow {
685
685
  readonly depth: number;
686
686
  }
687
687
  /**
688
- * One grouping level below the primary one. Nesting more than a second level is
689
- * expressed through the level window on a hierarchical attribute, not by chaining
690
- * these.
688
+ * One grouping level: the attribute to band by, the window of its outline levels
689
+ * that becomes bands, and the band order. The primary level lives on `GroupState`
690
+ * itself; every further level is chained through `GroupState.nested`.
691
691
  */
692
692
  interface GroupCriterion extends GroupLevelWindow {
693
693
  readonly attributeKey: string;
@@ -704,11 +704,15 @@ interface GroupCriterion extends GroupLevelWindow {
704
704
  type WbsDepth = number | null;
705
705
  /** The depth that leaves no WBS row on screen. */
706
706
  declare const FLAT_WBS_DEPTH = 0;
707
- /** Runtime grouping configuration active on a schedule. */
707
+ /**
708
+ * Runtime grouping configuration active on a schedule. The interface itself is
709
+ * the primary criterion; `nested` holds the criteria chained below it, in order,
710
+ * so a schedule can band by several attributes at once.
711
+ */
708
712
  interface GroupState extends GroupCriterion {
709
713
  readonly includeWbs: boolean;
710
714
  readonly collapsedIds: ReadonlySet<string>;
711
- readonly secondary: GroupCriterion | null;
715
+ readonly nested: ReadonlyArray<GroupCriterion>;
712
716
  }
713
717
  /**
714
718
  * JSON-friendly criterion. `startLevel` is optional so views persisted before
@@ -721,11 +725,17 @@ interface GroupCriterionInput {
721
725
  readonly depth: number;
722
726
  readonly direction: OrderDirection;
723
727
  }
724
- /** JSON-friendly grouping command accepted by `group-set`. */
728
+ /**
729
+ * JSON-friendly grouping command accepted by `group-set`. `nested` carries the
730
+ * levels chained below the primary, in order; `secondary` is the legacy
731
+ * single-nested-level field kept so views persisted before chaining still
732
+ * deserialize, and it is read only when `nested` is absent.
733
+ */
725
734
  interface GroupStateInput extends GroupCriterionInput {
726
735
  readonly includeWbs: boolean;
727
736
  readonly collapsedIds: ReadonlyArray<string>;
728
737
  readonly secondary?: GroupCriterionInput | null;
738
+ readonly nested?: ReadonlyArray<GroupCriterionInput>;
729
739
  }
730
740
  /** A single display row: either a synthetic group band or a real activity. */
731
741
  type VisualRow = {
@@ -743,7 +753,7 @@ type VisualRow = {
743
753
  declare function buildGroupRowId(attributeKey: string, valueId: string): string;
744
754
  /**
745
755
  * Extends a band id with a nested level. The parent path has to stay in the id
746
- * because the same secondary value shows up under several primary bands, and
756
+ * because the same nested value shows up under several parent bands, and
747
757
  * consumers key rows (and persisted collapse state) by this id.
748
758
  */
749
759
  declare function appendGroupRowId(parentRowId: string, attributeKey: string, valueId: string): string;
package/dist/index.d.ts CHANGED
@@ -685,9 +685,9 @@ interface GroupLevelWindow {
685
685
  readonly depth: number;
686
686
  }
687
687
  /**
688
- * One grouping level below the primary one. Nesting more than a second level is
689
- * expressed through the level window on a hierarchical attribute, not by chaining
690
- * these.
688
+ * One grouping level: the attribute to band by, the window of its outline levels
689
+ * that becomes bands, and the band order. The primary level lives on `GroupState`
690
+ * itself; every further level is chained through `GroupState.nested`.
691
691
  */
692
692
  interface GroupCriterion extends GroupLevelWindow {
693
693
  readonly attributeKey: string;
@@ -704,11 +704,15 @@ interface GroupCriterion extends GroupLevelWindow {
704
704
  type WbsDepth = number | null;
705
705
  /** The depth that leaves no WBS row on screen. */
706
706
  declare const FLAT_WBS_DEPTH = 0;
707
- /** Runtime grouping configuration active on a schedule. */
707
+ /**
708
+ * Runtime grouping configuration active on a schedule. The interface itself is
709
+ * the primary criterion; `nested` holds the criteria chained below it, in order,
710
+ * so a schedule can band by several attributes at once.
711
+ */
708
712
  interface GroupState extends GroupCriterion {
709
713
  readonly includeWbs: boolean;
710
714
  readonly collapsedIds: ReadonlySet<string>;
711
- readonly secondary: GroupCriterion | null;
715
+ readonly nested: ReadonlyArray<GroupCriterion>;
712
716
  }
713
717
  /**
714
718
  * JSON-friendly criterion. `startLevel` is optional so views persisted before
@@ -721,11 +725,17 @@ interface GroupCriterionInput {
721
725
  readonly depth: number;
722
726
  readonly direction: OrderDirection;
723
727
  }
724
- /** JSON-friendly grouping command accepted by `group-set`. */
728
+ /**
729
+ * JSON-friendly grouping command accepted by `group-set`. `nested` carries the
730
+ * levels chained below the primary, in order; `secondary` is the legacy
731
+ * single-nested-level field kept so views persisted before chaining still
732
+ * deserialize, and it is read only when `nested` is absent.
733
+ */
725
734
  interface GroupStateInput extends GroupCriterionInput {
726
735
  readonly includeWbs: boolean;
727
736
  readonly collapsedIds: ReadonlyArray<string>;
728
737
  readonly secondary?: GroupCriterionInput | null;
738
+ readonly nested?: ReadonlyArray<GroupCriterionInput>;
729
739
  }
730
740
  /** A single display row: either a synthetic group band or a real activity. */
731
741
  type VisualRow = {
@@ -743,7 +753,7 @@ type VisualRow = {
743
753
  declare function buildGroupRowId(attributeKey: string, valueId: string): string;
744
754
  /**
745
755
  * Extends a band id with a nested level. The parent path has to stay in the id
746
- * because the same secondary value shows up under several primary bands, and
756
+ * because the same nested value shows up under several parent bands, and
747
757
  * consumers key rows (and persisted collapse state) by this id.
748
758
  */
749
759
  declare function appendGroupRowId(parentRowId: string, attributeKey: string, valueId: string): string;
package/dist/index.js CHANGED
@@ -2304,24 +2304,37 @@ function toLevelWindow(input) {
2304
2304
  return { startLevel, depth: input.depth };
2305
2305
  }
2306
2306
  var INVALID_CRITERION = /* @__PURE__ */ Symbol("invalid-group-criterion");
2307
- function toSecondaryCriterion(input) {
2308
- const secondary = input.secondary;
2309
- if (secondary === void 0 || secondary === null) return null;
2310
- if (typeof secondary !== "object") return INVALID_CRITERION;
2311
- if (typeof secondary.attributeKey !== "string" || secondary.attributeKey.length === 0) {
2307
+ var MAX_NESTED_GROUP_CRITERIA = 3;
2308
+ function toNestedCriterion(input, usedKeys) {
2309
+ if (typeof input !== "object" || input === null) return INVALID_CRITERION;
2310
+ if (typeof input.attributeKey !== "string" || input.attributeKey.length === 0) {
2312
2311
  return INVALID_CRITERION;
2313
2312
  }
2314
- if (secondary.attributeKey === input.attributeKey) return INVALID_CRITERION;
2315
- const window2 = toLevelWindow(secondary);
2313
+ if (usedKeys.has(input.attributeKey)) return INVALID_CRITERION;
2314
+ const window2 = toLevelWindow(input);
2316
2315
  if (window2 === null) return INVALID_CRITERION;
2317
- if (!isValidDirection(secondary.direction)) return INVALID_CRITERION;
2316
+ if (!isValidDirection(input.direction)) return INVALID_CRITERION;
2318
2317
  return {
2319
- attributeKey: secondary.attributeKey,
2318
+ attributeKey: input.attributeKey,
2320
2319
  startLevel: window2.startLevel,
2321
2320
  depth: window2.depth,
2322
- direction: secondary.direction
2321
+ direction: input.direction
2323
2322
  };
2324
2323
  }
2324
+ function toNestedCriteria(input, primaryKey) {
2325
+ const rawNested = input.nested ?? (input.secondary == null ? [] : [input.secondary]);
2326
+ if (!Array.isArray(rawNested)) return INVALID_CRITERION;
2327
+ if (rawNested.length > MAX_NESTED_GROUP_CRITERIA) return INVALID_CRITERION;
2328
+ const usedKeys = /* @__PURE__ */ new Set([primaryKey]);
2329
+ const nested = [];
2330
+ for (const rawCriterion of rawNested) {
2331
+ const criterion = toNestedCriterion(rawCriterion, usedKeys);
2332
+ if (criterion === INVALID_CRITERION) return INVALID_CRITERION;
2333
+ usedKeys.add(criterion.attributeKey);
2334
+ nested.push(criterion);
2335
+ }
2336
+ return nested;
2337
+ }
2325
2338
  function toGroupState(input) {
2326
2339
  if (typeof input.attributeKey !== "string" || input.attributeKey.length === 0) {
2327
2340
  return null;
@@ -2335,8 +2348,8 @@ function toGroupState(input) {
2335
2348
  if (!Array.isArray(input.collapsedIds) || input.collapsedIds.some((id) => typeof id !== "string")) {
2336
2349
  return null;
2337
2350
  }
2338
- const secondary = toSecondaryCriterion(input);
2339
- if (secondary === INVALID_CRITERION) return null;
2351
+ const nested = toNestedCriteria(input, input.attributeKey);
2352
+ if (nested === INVALID_CRITERION) return null;
2340
2353
  return {
2341
2354
  attributeKey: input.attributeKey,
2342
2355
  startLevel: window2.startLevel,
@@ -2344,7 +2357,7 @@ function toGroupState(input) {
2344
2357
  direction: input.direction,
2345
2358
  includeWbs: input.includeWbs,
2346
2359
  collapsedIds: new Set(input.collapsedIds),
2347
- secondary
2360
+ nested
2348
2361
  };
2349
2362
  }
2350
2363
  function emptyViewChanges(action) {
@@ -13025,7 +13038,7 @@ function resolveDataDateProgressButton(activity, progress, progressChanges, deps
13025
13038
  );
13026
13039
  }
13027
13040
  function resolveDataDateFieldChanges(activity, changes, deps, column, confirmations, progressRouting, editedStart = null) {
13028
- const generic = routeGenericColumnEdit(
13041
+ const routed = routeGenericColumnEdit(
13029
13042
  activity,
13030
13043
  routeConstraintTypeEdit(activity, changes, column),
13031
13044
  deps,
@@ -13034,9 +13047,8 @@ function resolveDataDateFieldChanges(activity, changes, deps, column, confirmati
13034
13047
  progressRouting,
13035
13048
  editedStart
13036
13049
  );
13037
- if (generic === null) return rejected(REJECTION_REASON.INVALID_VALUE);
13038
- if ("accepted" in generic) return generic;
13039
- const routed = limitTransitionActuals(activity, generic, deps, column);
13050
+ if (routed === null) return rejected(REJECTION_REASON.INVALID_VALUE);
13051
+ if ("accepted" in routed) return routed;
13040
13052
  if (routed.dataDatePatch === void 0) return accepted(routed);
13041
13053
  if (activity.p6 === void 0 || deps.dataDateCapability === null) {
13042
13054
  return rejected(REJECTION_REASON.INVALID_VALUE);
@@ -13096,32 +13108,6 @@ function withMirroredProgress(activity, changes, next, deps, column) {
13096
13108
  }
13097
13109
  return { ...changes, patch: { ...changes.patch, progress: percent } };
13098
13110
  }
13099
- var EXECUTION_TRANSITION_COLUMNS = /* @__PURE__ */ new Set([
13100
- COLUMN.PROGRESS,
13101
- COLUMN.EXECUTION_STATUS,
13102
- COLUMN.PHYSICAL_PERCENT_COMPLETE
13103
- ]);
13104
- function limitTransitionActuals(activity, changes, deps, column) {
13105
- const dataDate = deps.dataDateSettings?.dataDate ?? null;
13106
- if (!EXECUTION_TRANSITION_COLUMNS.has(column) || dataDate === null || changes.dataDatePatch === void 0 || !hasActiveDataDate(deps.dataDateSettings, deps.dataDateEnabled)) {
13107
- return changes;
13108
- }
13109
- const calendar = deps.ctx.calendars.getCalendar(activity.calendarId);
13110
- const limit = calendar === null ? { start: dataDate, finish: dataDate } : completionActualsLimit(dataDate, calendar);
13111
- const recordedStart = activity.p6?.actualStart ?? null;
13112
- const floor = recordedStart !== null && recordedStart <= dataDate ? recordedStart : null;
13113
- const alternative = changes.confirmationAlternative;
13114
- return {
13115
- ...changes,
13116
- dataDatePatch: capActualDates(changes.dataDatePatch, floor, limit),
13117
- ...alternative === void 0 ? {} : {
13118
- confirmationAlternative: {
13119
- ...alternative,
13120
- effects: capActualDates(alternative.effects, floor, limit)
13121
- }
13122
- }
13123
- };
13124
- }
13125
13111
  function accepted(changes) {
13126
13112
  return { accepted: true, changes };
13127
13113
  }
@@ -25913,7 +25899,7 @@ function emitActivities(node, context) {
25913
25899
  });
25914
25900
  }
25915
25901
  }
25916
- function emitNode(node, childDirection, allowSecondary, context) {
25902
+ function emitNode(node, childDirection, nextNestedIndex, context) {
25917
25903
  const { group, rows } = context;
25918
25904
  rows.push({
25919
25905
  kind: VISUAL_ROW_KIND.GROUP,
@@ -25925,20 +25911,24 @@ function emitNode(node, childDirection, allowSecondary, context) {
25925
25911
  });
25926
25912
  if (group.collapsedIds.has(node.id)) return;
25927
25913
  for (const child of sortedChildNodes(node, childDirection)) {
25928
- emitNode(child, childDirection, allowSecondary, context);
25914
+ emitNode(child, childDirection, nextNestedIndex, context);
25929
25915
  }
25930
25916
  if (node.activities.length === 0) return;
25931
- const secondary = group.secondary;
25932
- if (allowSecondary && secondary != null && context.secondaryCatalog != null) {
25933
- const secondaryRoot = buildGroupTree(
25917
+ const nestedCriterion = group.nested[nextNestedIndex];
25918
+ const nestedCatalog = context.nestedCatalogs[nextNestedIndex] ?? null;
25919
+ if (nestedCriterion != null && nestedCatalog != null) {
25920
+ const nestedRoot = buildGroupTree(
25934
25921
  node.activities,
25935
- secondary,
25936
- context.secondaryCatalog,
25922
+ nestedCriterion,
25923
+ nestedCatalog,
25937
25924
  createNode(node.id, "", "", null, node.level, 0),
25938
25925
  true
25939
25926
  );
25940
- for (const child of sortedChildNodes(secondaryRoot, secondary.direction)) {
25941
- emitNode(child, secondary.direction, false, context);
25927
+ for (const child of sortedChildNodes(
25928
+ nestedRoot,
25929
+ nestedCriterion.direction
25930
+ )) {
25931
+ emitNode(child, nestedCriterion.direction, nextNestedIndex + 1, context);
25942
25932
  }
25943
25933
  return;
25944
25934
  }
@@ -25981,11 +25971,11 @@ function projectVisualRows(input) {
25981
25971
  comparator: input.comparator,
25982
25972
  orderMode: input.orderMode,
25983
25973
  wbsDepth: input.wbsDepth,
25984
- secondaryCatalog: input.secondaryCatalog,
25974
+ nestedCatalogs: input.nestedCatalogs,
25985
25975
  rows
25986
25976
  };
25987
25977
  for (const child of sortedChildNodes(root, group.direction)) {
25988
- emitNode(child, group.direction, true, context);
25978
+ emitNode(child, group.direction, 0, context);
25989
25979
  }
25990
25980
  return rows;
25991
25981
  }
@@ -26391,7 +26381,9 @@ var ScheduleState = class _ScheduleState {
26391
26381
  if (this._cache.visualRows) return this._cache.visualRows;
26392
26382
  const group = this._viewAttributes.getGroup();
26393
26383
  const catalog = group === null ? null : this._viewAttributes.getCatalog(group.attributeKey);
26394
- const secondaryCatalog = group?.secondary == null ? null : this._viewAttributes.getCatalog(group.secondary.attributeKey);
26384
+ const nestedCatalogs = group === null ? [] : group.nested.map(
26385
+ (criterion) => this._viewAttributes.getCatalog(criterion.attributeKey)
26386
+ );
26395
26387
  const activitiesInVisualOrder = [];
26396
26388
  for (const activityId of this.getVisualOrderIds()) {
26397
26389
  const activity = this.getActivity(activityId);
@@ -26404,7 +26396,7 @@ var ScheduleState = class _ScheduleState {
26404
26396
  hasChildren: (activityId) => this.hasChildren(activityId),
26405
26397
  group,
26406
26398
  catalog,
26407
- secondaryCatalog,
26399
+ nestedCatalogs,
26408
26400
  comparator: this.getOrderComparator(),
26409
26401
  orderMode: this.getOrderMode(),
26410
26402
  wbsDepth: this._viewAttributes.getWbsDepth()
@@ -31160,13 +31152,11 @@ function prepareDataDateSettingsUpdate(current, patch) {
31160
31152
  };
31161
31153
  }
31162
31154
  function prepareDataDateActivityUpdate(current, patch, primaryConstraint) {
31163
- const percentRouted = routeActivityPercentComplete(current, patch);
31155
+ const synchronized = synchronizeNotStartedDurations(current, patch);
31156
+ const percentRouted = routeActivityPercentComplete(current, synchronized);
31164
31157
  validateActivityInputPatch(percentRouted);
31165
31158
  validateSecondaryConstraint(percentRouted, primaryConstraint);
31166
- const routed = synchronizeNotStartedDurations(
31167
- current,
31168
- gatePhysicalPercentInput(current, percentRouted)
31169
- );
31159
+ const routed = gatePhysicalPercentInput(current, percentRouted);
31170
31160
  const next = cloneDomainValue({
31171
31161
  ...current,
31172
31162
  ...routed,
@@ -31179,7 +31169,7 @@ function prepareDataDateActivityUpdate(current, patch, primaryConstraint) {
31179
31169
  };
31180
31170
  }
31181
31171
  function synchronizeNotStartedDurations(current, patch) {
31182
- if (current.executionStatus !== P6_EXECUTION_STATUS.NOT_STARTED || patch.remainingDurationHours === void 0 || patch.originalDurationHours !== void 0) {
31172
+ if (current.executionStatus !== P6_EXECUTION_STATUS.NOT_STARTED || patch.actualStart != null || patch.activityPercentComplete !== void 0 || patch.remainingDurationHours === void 0 || patch.originalDurationHours !== void 0) {
31183
31173
  return patch;
31184
31174
  }
31185
31175
  return {
@@ -31224,7 +31214,7 @@ function routeActivityPercentComplete(current, patch) {
31224
31214
  case P6_PERCENT_COMPLETE_TYPE.DURATION:
31225
31215
  return {
31226
31216
  ...rest,
31227
- remainingDurationHours: current.originalDurationHours * (1 - activityPercentComplete / FULL_PERCENT)
31217
+ remainingDurationHours: (rest.originalDurationHours ?? current.originalDurationHours) * (1 - activityPercentComplete / FULL_PERCENT)
31228
31218
  };
31229
31219
  case P6_PERCENT_COMPLETE_TYPE.UNITS:
31230
31220
  throw new Error(