@outbuild-company/schedule-core 1.9.4 → 1.10.1

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
@@ -338,6 +338,7 @@ var FIELD_REGISTRY = {
338
338
  },
339
339
  expectedProgressBaseline: {
340
340
  valueKind: "number",
341
+ equalityPrecision: 2,
341
342
  extract: (activity) => activity.expectedProgressBaseline
342
343
  },
343
344
  expectedProgress: {
@@ -426,6 +427,9 @@ function toTargetDate(value) {
426
427
  }
427
428
  return new Date(Number.NaN);
428
429
  }
430
+ function utcDayMs(date2) {
431
+ return Date.UTC(date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate());
432
+ }
429
433
  function matchString(value, target, operator) {
430
434
  const isAbsent = value === null;
431
435
  if (operator === "includes") {
@@ -438,12 +442,13 @@ function matchString(value, target, operator) {
438
442
  if (operator === "isNot") return isAbsent || String(value) !== target;
439
443
  return false;
440
444
  }
441
- function matchNumber(value, target, operator) {
445
+ function matchNumber(value, target, operator, equalityPrecision) {
442
446
  if (value === null || typeof value !== "number") {
443
447
  return operator === "notEquals";
444
448
  }
445
- if (operator === "equals") return value === target;
446
- if (operator === "notEquals") return value !== target;
449
+ const equalityValue = equalityPrecision === void 0 ? value : Number(value.toFixed(equalityPrecision));
450
+ if (operator === "equals") return equalityValue === target;
451
+ if (operator === "notEquals") return equalityValue !== target;
447
452
  if (operator === "greaterThan") return value > target;
448
453
  if (operator === "lessThan") return value < target;
449
454
  if (operator === "greaterOrEqual") return value >= target;
@@ -455,14 +460,16 @@ function matchNumberBetween(value, minimum, maximum) {
455
460
  }
456
461
  function matchDate(value, target, operator) {
457
462
  if (!(value instanceof Date)) return false;
458
- if (operator === "after") return value.getTime() > target.getTime();
459
- if (operator === "before") return value.getTime() < target.getTime();
463
+ const valueDay = utcDayMs(value);
464
+ const targetDay = utcDayMs(target);
465
+ if (operator === "after") return valueDay > targetDay;
466
+ if (operator === "before") return valueDay < targetDay;
460
467
  return false;
461
468
  }
462
469
  function matchDateBetween(value, minimum, maximum) {
463
470
  if (!(value instanceof Date)) return false;
464
- const time = value.getTime();
465
- return time >= minimum.getTime() && time <= maximum.getTime();
471
+ const day = utcDayMs(value);
472
+ return day >= utcDayMs(minimum) && day <= utcDayMs(maximum);
466
473
  }
467
474
  function matchIdArray(value, allowed, operator) {
468
475
  const ids = Array.isArray(value) ? value : [];
@@ -507,7 +514,12 @@ function matchCriterion(activity, criterion, context) {
507
514
  return matchString(value, String(criterion.value), operator);
508
515
  }
509
516
  if (descriptor.valueKind === "number") {
510
- return matchNumber(value, Number(criterion.value), operator);
517
+ return matchNumber(
518
+ value,
519
+ Number(criterion.value),
520
+ operator,
521
+ descriptor.equalityPrecision
522
+ );
511
523
  }
512
524
  if (descriptor.valueKind === "date") {
513
525
  return matchDate(value, toTargetDate(criterion.value), operator);
@@ -529,10 +541,10 @@ function matchesCriteria(activity, filter, context) {
529
541
  );
530
542
  }
531
543
  function overlapsRange(activity, range) {
532
- const taskStart = activity.startDate.getTime();
533
- const taskEnd = activity.endDate.getTime();
534
- const windowStart = range.start.getTime();
535
- const windowEnd = range.end.getTime();
544
+ const taskStart = utcDayMs(activity.startDate);
545
+ const taskEnd = utcDayMs(activity.endDate);
546
+ const windowStart = utcDayMs(range.start);
547
+ const windowEnd = utcDayMs(range.end);
536
548
  const fullyInside = taskStart >= windowStart && taskEnd <= windowEnd;
537
549
  const startsInside = taskStart >= windowStart && taskStart <= windowEnd;
538
550
  const windowStartsInside = windowStart >= taskStart && windowStart <= taskEnd;
@@ -880,6 +892,7 @@ function dispatchFilterSet(action, deps) {
880
892
 
881
893
  // src/internal/hierarchy/root-parent.ts
882
894
  var ROOT_PARENT_ID = "0";
895
+ var ROOT_PROJECT_CORRELATIVE_ID = 0;
883
896
  function isRootParent(parent) {
884
897
  return parent == null || parent === 0 || parent === ROOT_PARENT_ID;
885
898
  }
@@ -1681,13 +1694,19 @@ function resolveExpansionAnchors(originalLink, adapter) {
1681
1694
  userLag: originalLink.lag ?? 0,
1682
1695
  sourceEndpointIsStart,
1683
1696
  targetLag: targetEndpointIsFinish ? -endpointSpanHours(target, targetBounds, adapter) : 0,
1684
- summarySourceId: originalLink.type === LINK_TYPE.START_TO_START && sourceBounds ? originalLink.source : void 0
1697
+ summarySourceId: originalLink.type === LINK_TYPE.START_TO_START && sourceBounds ? originalLink.source : void 0,
1698
+ finishAnchoredSummaryTargetId: targetEndpointIsFinish && targetBounds ? originalLink.target : void 0
1685
1699
  };
1686
1700
  }
1687
1701
  function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, adapter, virtualIdCounter, linksByActivity) {
1688
1702
  const anchors = resolveExpansionAnchors(originalLink, adapter);
1689
1703
  const { source, sourceBounds, targetStart, userLag } = anchors;
1690
- const { sourceEndpointIsStart, targetLag, summarySourceId } = anchors;
1704
+ const {
1705
+ sourceEndpointIsStart,
1706
+ targetLag,
1707
+ summarySourceId,
1708
+ finishAnchoredSummaryTargetId
1709
+ } = anchors;
1691
1710
  let counter = virtualIdCounter;
1692
1711
  for (const srcId of sourceIds) {
1693
1712
  const sourceLag = sourceEndpointIsStart ? sourceLagForLeaf(srcId, source, sourceBounds, adapter) : 0;
@@ -1710,7 +1729,7 @@ function pushOffsetPreservingLinks(expanded, sourceIds, leafIds, originalLink, a
1710
1729
  targetLag,
1711
1730
  userLag + offTarget,
1712
1731
  summarySourceId,
1713
- offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0
1732
+ finishAnchoredSummaryTargetId ?? (offTarget !== 0 && targetLag === 0 ? originalLink.target : void 0)
1714
1733
  )
1715
1734
  );
1716
1735
  counter += 1;
@@ -1799,6 +1818,9 @@ function findConnectedGroupForTrigger(triggerId, links) {
1799
1818
 
1800
1819
  // src/constants/progress.ts
1801
1820
  var COMPLETED_PROGRESS = 100;
1821
+ function isAutoSchedulingEnabledForProgress(progress) {
1822
+ return progress === 0;
1823
+ }
1802
1824
 
1803
1825
  // src/autoscheduler/engine/link-calculator.ts
1804
1826
  function normalize(predecessor, successor, link) {
@@ -2158,7 +2180,7 @@ async function asapPass(orderedIds, links, adapter, options, isCurrent, plans) {
2158
2180
  if (!maxStart || result.successorStart > maxStart) {
2159
2181
  maxStart = result.successorStart;
2160
2182
  drivingLinkId = String(link.id);
2161
- drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun;
2183
+ drivingOffsetIsStale = link._summaryTargetId !== void 0 && !predecessorMovedThisRun && (link._targetLag === 0 || options.preventSummaryTargetPullback === true);
2162
2184
  }
2163
2185
  }
2164
2186
  const staleOffsetWouldPullBack = maxStart !== null && drivingOffsetIsStale && maxStart < activity.startDate;
@@ -3937,7 +3959,6 @@ var setRealWork = (value) => ({
3937
3959
  // src/columns/progress/progress-pipeline.ts
3938
3960
  var MIN_PROGRESS = 0;
3939
3961
  var MAX_PROGRESS = PROGRESS_COMPLETE;
3940
- var PROGRESS_NONE = 0;
3941
3962
  var progressPipeline = {
3942
3963
  targetField: "progress",
3943
3964
  canEdit(activity, hierarchy) {
@@ -3957,7 +3978,8 @@ var progressPipeline = {
3957
3978
  return valid();
3958
3979
  },
3959
3980
  transform(activity, newValue, ctx) {
3960
- const autoScheduling = determineAutoScheduling(newValue);
3981
+ const autoScheduling = isAutoSchedulingEnabledForProgress(newValue);
3982
+ const needsAutoSchedule = activity.progress !== newValue && autoScheduling;
3961
3983
  const recursive = isRecursiveProgressChange(newValue);
3962
3984
  const realWorkByActivity = buildCanonicalRealWorkOverlay(
3963
3985
  activity,
@@ -3981,7 +4003,7 @@ var progressPipeline = {
3981
4003
  const postProcessors = [];
3982
4004
  const trackingEvents = recursive ? buildRecursiveTrackingEvents(activity, newValue) : void 0;
3983
4005
  return fieldChanges(patch, {
3984
- autoSchedule: false,
4006
+ autoSchedule: needsAutoSchedule,
3985
4007
  postProcessors,
3986
4008
  cascadeMutations,
3987
4009
  trackingEvents
@@ -3992,11 +4014,8 @@ var progressPipeline = {
3992
4014
  function isOutOfRange(value) {
3993
4015
  return value < MIN_PROGRESS || value > MAX_PROGRESS;
3994
4016
  }
3995
- function determineAutoScheduling(progress) {
3996
- return progress === PROGRESS_NONE;
3997
- }
3998
4017
  function isRecursiveProgressChange(value) {
3999
- return value === PROGRESS_COMPLETE || value === PROGRESS_NONE;
4018
+ return value === PROGRESS_COMPLETE || isAutoSchedulingEnabledForProgress(value);
4000
4019
  }
4001
4020
  function buildDescendantCascade(activity, newValue, autoScheduling, hierarchy, realWorkByActivity) {
4002
4021
  const descendants = hierarchy.getDescendantIds(activity.id);
@@ -10021,7 +10040,11 @@ async function runPostMutation(deps, args) {
10021
10040
  if (passes.has(DERIVED_PASS.AUTOSCHEDULE)) {
10022
10041
  await refreshMutatedParentBounds(deps.adapter, args);
10023
10042
  }
10024
- const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(deps, args.autoscheduleFrom) : emptySchedule();
10043
+ const schedule = passes.has(DERIVED_PASS.AUTOSCHEDULE) ? await runAutoschedule(
10044
+ deps,
10045
+ args.autoscheduleFrom,
10046
+ args.preventSummaryTargetPullback
10047
+ ) : emptySchedule();
10025
10048
  await rollUpParentBounds(
10026
10049
  deps.adapter,
10027
10050
  args,
@@ -10092,8 +10115,11 @@ function collectFieldProgressInvalidations(adapter, beforeById, dirtySeeds, affe
10092
10115
  }
10093
10116
  }
10094
10117
  }
10095
- async function runAutoschedule(deps, autoscheduleFrom) {
10118
+ async function runAutoschedule(deps, autoscheduleFrom, preventSummaryTargetPullback = false) {
10096
10119
  const options = autoscheduleFrom === "roots" ? {} : { triggerId: autoscheduleFrom };
10120
+ if (preventSummaryTargetPullback) {
10121
+ options.preventSummaryTargetPullback = true;
10122
+ }
10097
10123
  const result = await deps.scheduler.schedule(options);
10098
10124
  const outcome = captureScheduledRows(deps.adapter, result.updatedIds);
10099
10125
  applyResults(deps.adapter, result);
@@ -10274,10 +10300,6 @@ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
10274
10300
  }
10275
10301
 
10276
10302
  // src/dispatch/sir-auto-reject.ts
10277
- function datesChanged(before, after) {
10278
- const toTime = (value) => value.getTime();
10279
- return toTime(before.startDate) !== toTime(after.startDate) || toTime(before.endDate) !== toTime(after.endDate) || before.durationHours !== after.durationHours;
10280
- }
10281
10303
  function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
10282
10304
  const effects = [];
10283
10305
  for (const id of touchedIds) {
@@ -10285,21 +10307,15 @@ function detectSirAutoReject(beforeSnap, adapter, touchedIds) {
10285
10307
  if (!before) continue;
10286
10308
  const pendingRequestIds = before.pendingRequestIds ?? [];
10287
10309
  if (!isFrozen({ pendingRequestIds })) continue;
10288
- const after = adapter.getActivity(id);
10289
- const deleted = after == null;
10290
- if (!deleted && !datesChanged(before, after)) continue;
10291
- const reason = deleted ? "activity_deleted" : "date_changed";
10310
+ if (adapter.getActivity(id) != null) continue;
10292
10311
  for (const pending of pendingRequestIds) {
10293
10312
  effects.push({
10294
10313
  kind: "sir-auto-reject",
10295
10314
  activityId: id,
10296
10315
  sirId: pending,
10297
- reason
10316
+ reason: "activity_deleted"
10298
10317
  });
10299
10318
  }
10300
- if (!deleted) {
10301
- adapter.setActivityField(id, "pendingRequestIds", []);
10302
- }
10303
10319
  }
10304
10320
  return effects;
10305
10321
  }
@@ -11562,14 +11578,21 @@ async function createActivityCore(action, deps, opts = {}) {
11562
11578
  recomputeUsedCostCascadeForActivity(newId, adapter);
11563
11579
  recomputeCostCascadeForActivity(newId, adapter);
11564
11580
  }
11565
- recomputeAndFoldCorrelatives(
11581
+ const correlativeShifts = recomputeCorrelatives(
11566
11582
  adapter,
11567
11583
  newId,
11568
- beforeSnap,
11569
11584
  initialTouched,
11570
11585
  opts
11571
11586
  );
11572
- return { ok: true, newId, parent, newActivity, beforeSnap, initialTouched };
11587
+ return {
11588
+ ok: true,
11589
+ newId,
11590
+ parent,
11591
+ newActivity,
11592
+ beforeSnap,
11593
+ initialTouched,
11594
+ correlativeShifts
11595
+ };
11573
11596
  }
11574
11597
  function validateCreateTarget(action, adapter) {
11575
11598
  const isRoot = isRootParent(action.parentId);
@@ -11664,20 +11687,16 @@ function applyParentMutations(action, newId, parent, adapter, parentWasLeaf) {
11664
11687
  );
11665
11688
  }
11666
11689
  }
11667
- function recomputeAndFoldCorrelatives(adapter, newId, beforeSnap, initialTouched, opts) {
11668
- if (opts.skipCorrelativeRecompute) return;
11669
- const correlativeShifts = recomputeCorrelativeIds(adapter);
11670
- if (opts.skipBeforeSnap) return;
11671
- foldCorrelativeShifts(
11672
- adapter,
11673
- correlativeShifts,
11674
- /* @__PURE__ */ new Set([String(newId)]),
11675
- beforeSnap,
11676
- initialTouched
11690
+ function recomputeCorrelatives(adapter, newId, initialTouched, opts) {
11691
+ if (opts.skipCorrelativeRecompute) return [];
11692
+ const shifts = recomputeCorrelativeIds(adapter).filter(
11693
+ (shift) => shift.activityId !== String(newId)
11677
11694
  );
11695
+ for (const shift of shifts) initialTouched.add(shift.activityId);
11696
+ return shifts;
11678
11697
  }
11679
11698
  async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, hoursPerDay) {
11680
- const { newId, newActivity, beforeSnap, initialTouched } = coreResult;
11699
+ const { newId, newActivity, beforeSnap, initialTouched, correlativeShifts } = coreResult;
11681
11700
  const trackingEvent = {
11682
11701
  name: DISPATCH_TRACK_EVENT.ACTIVITY_CREATION,
11683
11702
  properties: {
@@ -11694,6 +11713,7 @@ async function buildCreateChangeSet(adapter, action, coreResult, scheduledIds, h
11694
11713
  beforeSnap,
11695
11714
  touchedIds: initialTouched,
11696
11715
  scheduledIds,
11716
+ correlativeShifts,
11697
11717
  trackingEvents: [trackingEvent]});
11698
11718
  }
11699
11719
  async function dispatchActivityCreate(action, options, deps) {
@@ -11722,8 +11742,9 @@ async function dispatchActivityCreate(action, options, deps) {
11722
11742
  },
11723
11743
  {
11724
11744
  action,
11725
- autoscheduleFrom: "roots",
11745
+ autoscheduleFrom: newId,
11726
11746
  recomputeParentsFrom: [newId],
11747
+ preventSummaryTargetPullback: true,
11727
11748
  now: deps.now,
11728
11749
  options
11729
11750
  }
@@ -11880,7 +11901,11 @@ async function dispatchActivityDelete(action, options, deps) {
11880
11901
  return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
11881
11902
  }
11882
11903
  }
11883
- const toDelete = collectDescendantIds(action.activityIds, adapter);
11904
+ const deletableActivityIds = action.activityIds.filter((id) => {
11905
+ const activity = adapter.getActivity(id);
11906
+ return !(activity?.correlativeId === ROOT_PROJECT_CORRELATIVE_ID && activity.type === ACTIVITY_TYPE.PROJECT);
11907
+ });
11908
+ const toDelete = collectDescendantIds(deletableActivityIds, adapter);
11884
11909
  const incidentLinkIds = collectIncidentLinkIds(toDelete, adapter);
11885
11910
  const parentsToCheck = /* @__PURE__ */ new Set();
11886
11911
  for (const id of action.activityIds) {
@@ -12617,10 +12642,12 @@ async function applyReconciledProgress(entries, deps) {
12617
12642
  const before = await snapshotActivities(deps.adapter, touched);
12618
12643
  deps.adapter.batchUpdate(() => {
12619
12644
  for (const entry of progressToApply) {
12645
+ const activityId = String(entry.activityId);
12646
+ deps.adapter.setActivityField(activityId, "progress", entry.progress);
12620
12647
  deps.adapter.setActivityField(
12621
- String(entry.activityId),
12622
- "progress",
12623
- entry.progress
12648
+ activityId,
12649
+ "autoScheduling",
12650
+ isAutoSchedulingEnabledForProgress(entry.progress)
12624
12651
  );
12625
12652
  }
12626
12653
  recomputeProgressRollupForParents(ancestors, deps.adapter);
@@ -12791,7 +12818,7 @@ async function dispatchActivityLookaheadSync(action, options, deps) {
12791
12818
  },
12792
12819
  {
12793
12820
  action,
12794
- autoscheduleFrom: null,
12821
+ autoscheduleFrom: action.persistedProgress === 0 ? ids[0] : null,
12795
12822
  recomputeParentsFrom: [],
12796
12823
  now: deps.now,
12797
12824
  options
@@ -14408,9 +14435,13 @@ function resolveDestination(afterSiblingId, adapter) {
14408
14435
  if (!sibling) {
14409
14436
  return { ok: false, reason: REJECTION_REASON.ANCHOR_SIBLING_NOT_FOUND };
14410
14437
  }
14438
+ if (sibling.parentId == null) {
14439
+ return adapter.getRootIds().length === 1 ? { ok: true, parentId: sibling.id } : { ok: true, parentId: ROOT_PARENT_ID, afterSiblingId };
14440
+ }
14411
14441
  return {
14412
14442
  ok: true,
14413
- parentId: sibling.parentId == null ? ROOT_PARENT_ID : String(sibling.parentId)
14443
+ parentId: String(sibling.parentId),
14444
+ afterSiblingId
14414
14445
  };
14415
14446
  }
14416
14447
  var invariantFailure = () => ({
@@ -14437,12 +14468,11 @@ async function dispatchActivityPasteCopied(action, options, deps) {
14437
14468
  let previousRootId;
14438
14469
  for (const copied of clipboard.activities) {
14439
14470
  const mappedParent = originalToNew.get(copied.originalParentId);
14471
+ const afterSiblingId = mappedParent === void 0 ? previousRootId ?? destination.afterSiblingId : void 0;
14440
14472
  const created = await createActivityCore(
14441
14473
  {
14442
14474
  parentId: mappedParent ?? destination.parentId,
14443
- ...mappedParent === void 0 ? {
14444
- afterSiblingId: previousRootId ?? action.destination.afterSiblingId
14445
- } : {},
14475
+ ...afterSiblingId === void 0 ? {} : { afterSiblingId },
14446
14476
  overrides: cloneDomainValue(copied.overrides),
14447
14477
  eventSource: action.eventSource
14448
14478
  },
@@ -19285,7 +19315,10 @@ var SaveTracker = class _SaveTracker {
19285
19315
  const persistable = {
19286
19316
  ...saved.persistable,
19287
19317
  ...acknowledgesLookahead ? { isLookahead: current.isLookahead } : {},
19288
- ...acknowledgesProgress ? { progress: current.progress } : {}
19318
+ ...acknowledgesProgress ? {
19319
+ progress: current.progress,
19320
+ autoScheduling: current.autoScheduling
19321
+ } : {}
19289
19322
  };
19290
19323
  this.activitiesAtLastSave.set(activityId, {
19291
19324
  ...saved,
@@ -19444,9 +19477,6 @@ function normalizeActivities(rawActivities, context) {
19444
19477
  reparentOrphansToRoot(activities, activityIds, context.reporter);
19445
19478
  return { activities, activityIds, hiddenIds };
19446
19479
  }
19447
- function isAutoSchedulable(progress) {
19448
- return progress === 0;
19449
- }
19450
19480
  function parseConstraintLabel(label, reporter) {
19451
19481
  if (!label) return null;
19452
19482
  const normalized2 = CONSTRAINT_LABEL_MAP[label.toUpperCase()];
@@ -19548,7 +19578,7 @@ function parseActivity(raw, context) {
19548
19578
  durationHours: durationToHours(durationDays, context.hoursPerDay),
19549
19579
  constraintType,
19550
19580
  constraintDate,
19551
- autoScheduling: isAutoSchedulable(progress),
19581
+ autoScheduling: isAutoSchedulingEnabledForProgress(progress),
19552
19582
  calendarId,
19553
19583
  assignedDefaultCalendar: calendarId != null && inheritedDefaultCalendar,
19554
19584
  dateOrigin: raw.date_origin == null ? null : parseBackendDate(raw.date_origin),
@@ -21410,6 +21440,11 @@ var ScheduleCore = class _ScheduleCore {
21410
21440
  this.assertReady();
21411
21441
  return this.coreRuntime.state.hiddenIds().map(String);
21412
21442
  }
21443
+ getVisibleActivityIds() {
21444
+ this.assertReady();
21445
+ const hiddenIds = new Set(this.coreRuntime.state.hiddenIds().map(String));
21446
+ return this.coreRuntime.state.getAllIds().map(String).filter((activityId) => !hiddenIds.has(activityId));
21447
+ }
21413
21448
  getActiveFilter() {
21414
21449
  this.assertReady();
21415
21450
  return cloneDomainValue(this.coreRuntime.state.getActiveFilter());