@outbuild-company/schedule-core 1.20.1 → 1.22.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
@@ -345,6 +345,10 @@ var DISPATCH_ACTION_KIND = {
345
345
  ACTIVITY_COPY: "activity-copy",
346
346
  CALENDAR_RECONCILE: "calendar-reconcile",
347
347
  ACTIVITY_PASTE_COPIED: "activity-paste-copied",
348
+ ACTIVITY_DUPLICATE: "activity-duplicate",
349
+ CELL_COPY: "cell-copy",
350
+ CELL_CLIPBOARD_CLEAR: "cell-clipboard-clear",
351
+ CELL_PASTE_COPIED: "cell-paste-copied",
348
352
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
349
353
  BASELINE_APPLY: "baseline-apply",
350
354
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -593,6 +597,11 @@ function createActivitiesApi(engine) {
593
597
  kind: DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED,
594
598
  destination: { afterSiblingId }
595
599
  }),
600
+ duplicate: (activityIds, context = {}) => engine.dispatch({
601
+ ...context,
602
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_DUPLICATE,
603
+ activityIds
604
+ }),
596
605
  clearClipboard: (context = {}) => engine.dispatch({
597
606
  ...context,
598
607
  kind: DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR
@@ -611,6 +620,26 @@ function createActivitiesApi(engine) {
611
620
  createDateGesturePreview: (gesture) => engine.createDateGesturePreview(gesture)
612
621
  };
613
622
  }
623
+ function createCellsApi(engine) {
624
+ return {
625
+ copy: (activityId, column, context = {}) => engine.dispatch({
626
+ ...context,
627
+ kind: DISPATCH_ACTION_KIND.CELL_COPY,
628
+ activityId,
629
+ column
630
+ }),
631
+ paste: (activityIds, column, context = {}) => engine.dispatch({
632
+ ...context,
633
+ kind: DISPATCH_ACTION_KIND.CELL_PASTE_COPIED,
634
+ activityIds,
635
+ column
636
+ }),
637
+ clearClipboard: (context = {}) => engine.dispatch({
638
+ ...context,
639
+ kind: DISPATCH_ACTION_KIND.CELL_CLIPBOARD_CLEAR
640
+ })
641
+ };
642
+ }
614
643
  function createLinksApi(engine) {
615
644
  return {
616
645
  get: (id) => engine.getLinkView(id),
@@ -658,6 +687,7 @@ function createScheduleNamespaces(engine) {
658
687
  );
659
688
  return {
660
689
  activities: createActivitiesApi(engine),
690
+ cells: createCellsApi(engine),
661
691
  links: createLinksApi(engine),
662
692
  calendars: {
663
693
  get: (id) => engine.getCalendar(id),
@@ -919,6 +949,7 @@ var REJECTION_REASON = {
919
949
  NO_ACTIVITY_SELECTION: "no_activity_selection",
920
950
  CLIPBOARD_EMPTY: "clipboard_empty",
921
951
  ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
952
+ SOLE_SCHEDULE_ROOT: "sole_schedule_root",
922
953
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
923
954
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
924
955
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
@@ -3887,7 +3918,7 @@ function applyLinkOperation(op, deps) {
3887
3918
  return applyDelete(op, deps);
3888
3919
  }
3889
3920
  }
3890
- function applyLinkCreates(links, deps) {
3921
+ function validateLinkCreateBatch(links, deps) {
3891
3922
  if (links.length === 0) return { applied: true };
3892
3923
  const existingLinks = deps.port.getAllLinks();
3893
3924
  const duplicateKeys = /* @__PURE__ */ new Map();
@@ -3971,6 +4002,12 @@ function applyLinkCreates(links, deps) {
3971
4002
  if (!effectiveVerdict.ok) {
3972
4003
  return { applied: false, rejected: LINK_REJECTION_REASON.CYCLE };
3973
4004
  }
4005
+ return { applied: true };
4006
+ }
4007
+ function applyLinkCreates(links, deps) {
4008
+ if (links.length === 0) return { applied: true };
4009
+ const validation = validateLinkCreateBatch(links, deps);
4010
+ if (!validation.applied) return validation;
3974
4011
  for (const { operation, linkId } of links) {
3975
4012
  const preserved = deps.preserved?.get(linkId);
3976
4013
  deps.port.addLink({
@@ -11708,6 +11745,8 @@ var NON_SCHEDULING_INLINE_COLUMNS = /* @__PURE__ */ new Set([
11708
11745
  var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
11709
11746
  DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR,
11710
11747
  DISPATCH_ACTION_KIND.ACTIVITY_COPY,
11748
+ DISPATCH_ACTION_KIND.CELL_CLIPBOARD_CLEAR,
11749
+ DISPATCH_ACTION_KIND.CELL_COPY,
11711
11750
  DISPATCH_ACTION_KIND.SELECTION_TOGGLE,
11712
11751
  DISPATCH_ACTION_KIND.SELECTION_REPLACE,
11713
11752
  DISPATCH_ACTION_KIND.VISIBILITY_SET,
@@ -11726,6 +11765,8 @@ var NON_SCHEDULING_KINDS = /* @__PURE__ */ new Set([
11726
11765
  var PURE_VIEW_STATE_KINDS = /* @__PURE__ */ new Set([
11727
11766
  DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR,
11728
11767
  DISPATCH_ACTION_KIND.ACTIVITY_COPY,
11768
+ DISPATCH_ACTION_KIND.CELL_CLIPBOARD_CLEAR,
11769
+ DISPATCH_ACTION_KIND.CELL_COPY,
11729
11770
  DISPATCH_ACTION_KIND.SELECTION_TOGGLE,
11730
11771
  DISPATCH_ACTION_KIND.SELECTION_REPLACE,
11731
11772
  DISPATCH_ACTION_KIND.VISIBILITY_SET,
@@ -11743,7 +11784,7 @@ function reappliesViewState(action) {
11743
11784
  }
11744
11785
  var NO_STRUCTURAL_EXEMPTIONS = /* @__PURE__ */ new Set();
11745
11786
  function editsOnlyNonSchedulingColumns(action) {
11746
- if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT) {
11787
+ if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT || action.kind === DISPATCH_ACTION_KIND.CELL_PASTE_COPIED) {
11747
11788
  return NON_SCHEDULING_INLINE_COLUMNS.has(action.column);
11748
11789
  }
11749
11790
  if (action.kind === DISPATCH_ACTION_KIND.BULK_EDIT) {
@@ -11754,7 +11795,7 @@ function editsOnlyNonSchedulingColumns(action) {
11754
11795
  return false;
11755
11796
  }
11756
11797
  function editsOnlyDataDateColumns(action) {
11757
- if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT) {
11798
+ if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT || action.kind === DISPATCH_ACTION_KIND.CELL_PASTE_COPIED) {
11758
11799
  return DATA_DATE_COLUMNS.has(action.column);
11759
11800
  }
11760
11801
  return action.kind === DISPATCH_ACTION_KIND.BULK_EDIT && action.edits.every((edit) => DATA_DATE_COLUMNS.has(edit.column));
@@ -11796,6 +11837,9 @@ function dispatchChangesProgress(action) {
11796
11837
  if (action.kind === DISPATCH_ACTION_KIND.BULK_EDIT) {
11797
11838
  return action.edits.some((edit) => edit.column === COLUMN.PROGRESS);
11798
11839
  }
11840
+ if (action.kind === DISPATCH_ACTION_KIND.CELL_PASTE_COPIED) {
11841
+ return action.column === COLUMN.PROGRESS;
11842
+ }
11799
11843
  return false;
11800
11844
  }
11801
11845
  function resolveRunRealCost(action) {
@@ -11811,7 +11855,7 @@ var PROGRESS_AFFECTING_COLUMNS = /* @__PURE__ */ new Set([
11811
11855
  ]);
11812
11856
  function shouldRecomputeParentProgress(action) {
11813
11857
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_SET_PROGRESS) return true;
11814
- if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT) {
11858
+ if (action.kind === DISPATCH_ACTION_KIND.INLINE_EDIT || action.kind === DISPATCH_ACTION_KIND.CELL_PASTE_COPIED) {
11815
11859
  return PROGRESS_AFFECTING_COLUMNS.has(action.column);
11816
11860
  }
11817
11861
  if (action.kind === DISPATCH_ACTION_KIND.BULK_EDIT) {
@@ -12783,6 +12827,15 @@ function mergeScheduledIds(firstSettleIds, secondSettleIds) {
12783
12827
  }
12784
12828
 
12785
12829
  // src/dispatch/shared/helpers.ts
12830
+ function getIncidentLinkSnapshots(adapter, activityId, isPredecessors) {
12831
+ const incidentLinkIds = isPredecessors ? adapter.getIncomingLinkIds(activityId) : adapter.getOutgoingLinkIds(activityId);
12832
+ const links = [];
12833
+ for (const linkId of incidentLinkIds) {
12834
+ const link = adapter.getLink(linkId);
12835
+ if (link) links.push(link);
12836
+ }
12837
+ return links;
12838
+ }
12786
12839
  function buildLinkChangesForBatch(applied, beforeLinks, adapter) {
12787
12840
  const out = [];
12788
12841
  for (const { op, finalLinkId, rejected: rejected2 } of applied) {
@@ -14009,12 +14062,11 @@ async function dispatchInlineEditLinks(action, options, deps) {
14009
14062
  byCorrelative.set(String(a.correlativeId), a.id);
14010
14063
  });
14011
14064
  const resolve = (corr) => byCorrelative.get(corr) ?? null;
14012
- const incidentLinkIds = isPredecessors ? adapter.getIncomingLinkIds(action.activityId) : adapter.getOutgoingLinkIds(action.activityId);
14013
- const currentLinks = [];
14014
- for (const linkId of incidentLinkIds) {
14015
- const link = adapter.getLink(linkId);
14016
- if (link) currentLinks.push(link);
14017
- }
14065
+ const currentLinks = getIncidentLinkSnapshots(
14066
+ adapter,
14067
+ action.activityId,
14068
+ isPredecessors
14069
+ );
14018
14070
  const diff = isPredecessors ? diffLinks({
14019
14071
  targetActivityId: action.activityId,
14020
14072
  currentLinks,
@@ -18482,6 +18534,457 @@ async function dispatchActivityPasteCopied(action, options, deps) {
18482
18534
  };
18483
18535
  }
18484
18536
 
18537
+ // src/activity-copy-paste/duplicate.ts
18538
+ var CLONE_YIELD_EVERY_N = 200;
18539
+ var CREATE_OPTIONS = {
18540
+ skipCorrelativeRecompute: true,
18541
+ skipBeforeSnap: true,
18542
+ skipCacheInvalidation: true,
18543
+ skipRollupCascades: true
18544
+ };
18545
+ async function dispatchActivityDuplicate(action, options, deps) {
18546
+ const resolved = resolveDuplicateRuns(action.activityIds, deps.adapter);
18547
+ if (!resolved.ok) return resolved;
18548
+ const frozenParent = firstFrozenRunParent(resolved.runs, deps.adapter);
18549
+ if (frozenParent) {
18550
+ return { ok: false, reason: REJECTION_REASON.PARENT_FROZEN_BY_SIR };
18551
+ }
18552
+ deps.scheduler.invalidateAllCaches();
18553
+ const beforeSnapshot = await snapshotActivities(
18554
+ deps.adapter,
18555
+ /* @__PURE__ */ new Set()
18556
+ );
18557
+ const state = {
18558
+ originalToNew: /* @__PURE__ */ new Map(),
18559
+ createdIds: [],
18560
+ createdParentIds: []
18561
+ };
18562
+ for (const run of resolved.runs) {
18563
+ const lastOriginal = run.originals.at(-1);
18564
+ if (!lastOriginal) continue;
18565
+ let anchorId = lastOriginal.id;
18566
+ for (const original of run.originals) {
18567
+ anchorId = await cloneSubtree(
18568
+ original,
18569
+ run.parentId,
18570
+ anchorId,
18571
+ action,
18572
+ deps,
18573
+ state
18574
+ );
18575
+ }
18576
+ }
18577
+ recomputeCreatedParentRollups(state, deps);
18578
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
18579
+ const createdIdSet = new Set(state.createdIds.map(String));
18580
+ const touched = /* @__PURE__ */ new Set();
18581
+ seedJournalBeforeImages(
18582
+ deps.adapter,
18583
+ correlativeShifts,
18584
+ createdIdSet,
18585
+ beforeSnapshot
18586
+ );
18587
+ foldCorrelativeShifts(
18588
+ deps.adapter,
18589
+ correlativeShifts,
18590
+ createdIdSet,
18591
+ beforeSnapshot,
18592
+ touched
18593
+ );
18594
+ for (const createdId of state.createdIds) touched.add(String(createdId));
18595
+ const applied = replicateInternalLinks(state, deps);
18596
+ const { scheduledIds } = await runPostMutation(
18597
+ {
18598
+ adapter: deps.adapter,
18599
+ scheduler: deps.scheduler,
18600
+ sector: deps.sector,
18601
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
18602
+ },
18603
+ {
18604
+ action,
18605
+ autoscheduleFrom: state.createdIds.length > 0 ? AUTOSCHEDULE_SCOPE.ROOTS : null,
18606
+ recomputeParentsFrom: state.createdIds,
18607
+ preventSummaryTargetPullback: true,
18608
+ now: deps.now,
18609
+ options
18610
+ }
18611
+ );
18612
+ return {
18613
+ ok: true,
18614
+ changes: await assembleChangeSet(deps.adapter, {
18615
+ source: action,
18616
+ beforeSnap: beforeSnapshot,
18617
+ touchedIds: touched,
18618
+ scheduledIds,
18619
+ links: buildLinkChangesForBatch(
18620
+ applied,
18621
+ /* @__PURE__ */ new Map(),
18622
+ deps.adapter
18623
+ ),
18624
+ hoursPerDay: deps.sector.hoursPerDay
18625
+ })
18626
+ };
18627
+ }
18628
+ function resolveDuplicateRuns(activityIds, adapter) {
18629
+ if (activityIds.length === 0) {
18630
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
18631
+ }
18632
+ const requested = /* @__PURE__ */ new Set();
18633
+ const originals = [];
18634
+ for (const rawId of activityIds) {
18635
+ const activityId = String(rawId);
18636
+ if (requested.has(activityId)) continue;
18637
+ const activity = adapter.getActivity(activityId);
18638
+ if (!activity) {
18639
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
18640
+ }
18641
+ requested.add(activityId);
18642
+ originals.push(activity);
18643
+ }
18644
+ const roots = originals.filter(
18645
+ (activity) => !hasAncestorInSet(activity.id, requested, adapter)
18646
+ );
18647
+ const duplicatesSoleScheduleRoot = adapter.getRootIds().length === 1 && roots.some((activity) => activity.parentId == null);
18648
+ if (duplicatesSoleScheduleRoot) {
18649
+ return { ok: false, reason: REJECTION_REASON.SOLE_SCHEDULE_ROOT };
18650
+ }
18651
+ return { ok: true, runs: groupIntoContiguousRuns(roots, adapter) };
18652
+ }
18653
+ function firstFrozenRunParent(runs, adapter) {
18654
+ const checked = /* @__PURE__ */ new Set();
18655
+ for (const run of runs) {
18656
+ if (run.parentId === ROOT_PARENT_ID || checked.has(String(run.parentId))) {
18657
+ continue;
18658
+ }
18659
+ checked.add(String(run.parentId));
18660
+ const parent = adapter.getActivity(run.parentId);
18661
+ if (parent && isFrozen(parent)) return parent;
18662
+ }
18663
+ return null;
18664
+ }
18665
+ function groupIntoContiguousRuns(roots, adapter) {
18666
+ const byParent = /* @__PURE__ */ new Map();
18667
+ for (const activity of roots) {
18668
+ const parentKey = normalizeParentKey(activity.parentId);
18669
+ const group = byParent.get(parentKey) ?? [];
18670
+ group.push(activity);
18671
+ byParent.set(parentKey, group);
18672
+ }
18673
+ const runs = [];
18674
+ for (const [parentKey, group] of byParent) {
18675
+ const parentId = parentKey === ROOT_PARENT_ID ? ROOT_PARENT_ID : parentKey;
18676
+ const positions = new Map(
18677
+ childrenInCorrelativeOrder(adapter, parentKey).map(
18678
+ (sibling, index) => [String(sibling.id), index]
18679
+ )
18680
+ );
18681
+ const positionOf = (activity) => positions.get(String(activity.id)) ?? -1;
18682
+ const ordered = [...group].sort(
18683
+ (left, right) => positionOf(left) - positionOf(right)
18684
+ );
18685
+ for (const originals of splitContiguous(ordered, positionOf)) {
18686
+ runs.push({ parentId, originals });
18687
+ }
18688
+ }
18689
+ return runs;
18690
+ }
18691
+ function splitContiguous(ordered, positionOf) {
18692
+ const runs = [];
18693
+ let previousPosition = null;
18694
+ for (const activity of ordered) {
18695
+ const position = positionOf(activity);
18696
+ const lastRun = runs.at(-1);
18697
+ if (lastRun && previousPosition !== null && position === previousPosition + 1) {
18698
+ lastRun.push(activity);
18699
+ } else {
18700
+ runs.push([activity]);
18701
+ }
18702
+ previousPosition = position;
18703
+ }
18704
+ return runs;
18705
+ }
18706
+ function seedJournalBeforeImages(adapter, shifts, excludeIds, beforeSnap) {
18707
+ const journal = adapter.peekWriteCapture();
18708
+ if (!journal) return;
18709
+ for (const shift of shifts) {
18710
+ if (excludeIds.has(shift.activityId) || beforeSnap.has(shift.activityId)) {
18711
+ continue;
18712
+ }
18713
+ const beforeImage = journal.before.get(shift.activityId);
18714
+ if (beforeImage) beforeSnap.set(shift.activityId, beforeImage);
18715
+ }
18716
+ }
18717
+ function childrenInCorrelativeOrder(adapter, parentKey) {
18718
+ return collectSiblings(adapter, parentKey).sort((left, right) => {
18719
+ const diff = readCorrelativeId(left) - readCorrelativeId(right);
18720
+ return diff || String(left.id).localeCompare(String(right.id));
18721
+ });
18722
+ }
18723
+ async function cloneSubtree(original, parentId, anchorId, action, deps, state) {
18724
+ const root = await createActivityCore(
18725
+ {
18726
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
18727
+ parentId,
18728
+ afterSiblingId: anchorId,
18729
+ overrides: copyOverrides(original),
18730
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
18731
+ },
18732
+ deps,
18733
+ { ...CREATE_OPTIONS, customIdReferenceId: original.id }
18734
+ );
18735
+ if (!root.ok) throw invariantViolation("create", root.reason);
18736
+ registerCreated(state, original, root.newId, parentId);
18737
+ await cloneDescendantsIteratively(original, root.newId, action, deps, state);
18738
+ return root.newId;
18739
+ }
18740
+ async function cloneDescendantsIteratively(rootOriginal, rootNewParentId, action, deps, state) {
18741
+ const pending = new Queue();
18742
+ pending.enqueue({ original: rootOriginal, newParentId: rootNewParentId });
18743
+ let processed = 0;
18744
+ while (!pending.isEmpty()) {
18745
+ const { original, newParentId } = pending.dequeue();
18746
+ for (const child of childrenInCorrelativeOrder(
18747
+ deps.adapter,
18748
+ String(original.id)
18749
+ )) {
18750
+ const created = await createActivityCore(
18751
+ {
18752
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
18753
+ parentId: newParentId,
18754
+ overrides: copyOverrides(child),
18755
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
18756
+ },
18757
+ deps,
18758
+ CREATE_OPTIONS
18759
+ );
18760
+ if (!created.ok) throw invariantViolation("create", created.reason);
18761
+ registerCreated(state, child, created.newId, newParentId);
18762
+ pending.enqueue({ original: child, newParentId: created.newId });
18763
+ processed += 1;
18764
+ if (processed % CLONE_YIELD_EVERY_N === 0) await yieldToBrowser();
18765
+ }
18766
+ }
18767
+ }
18768
+ function invariantViolation(step, reason) {
18769
+ return new Error(
18770
+ `[ScheduleCore] activity-duplicate: unexpected ${step} rejection after preflight passed (${reason})`
18771
+ );
18772
+ }
18773
+ function registerCreated(state, original, newId, parentId) {
18774
+ state.originalToNew.set(String(original.id), newId);
18775
+ state.createdIds.push(newId);
18776
+ state.createdParentIds.push(parentId);
18777
+ }
18778
+ function recomputeCreatedParentRollups(state, deps) {
18779
+ const recomputed = /* @__PURE__ */ new Set();
18780
+ for (const parentId of [...state.createdParentIds].reverse()) {
18781
+ if (parentId === ROOT_PARENT_ID || recomputed.has(String(parentId))) {
18782
+ continue;
18783
+ }
18784
+ recomputed.add(String(parentId));
18785
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
18786
+ }
18787
+ }
18788
+ function replicateInternalLinks(state, deps) {
18789
+ const applied = [];
18790
+ for (const originalId of state.originalToNew.keys()) {
18791
+ for (const linkId of deps.adapter.getOutgoingLinkIds(originalId)) {
18792
+ const link = deps.adapter.getLink(linkId);
18793
+ if (!link) continue;
18794
+ const source = state.originalToNew.get(String(link.source));
18795
+ const target = state.originalToNew.get(String(link.target));
18796
+ if (source === void 0 || target === void 0) continue;
18797
+ const operation = {
18798
+ kind: LINK_OPERATION_KIND.CREATE,
18799
+ source,
18800
+ target,
18801
+ type: link.type,
18802
+ lag: link.lag
18803
+ };
18804
+ const result = applyLinkOperation(operation, {
18805
+ port: deps.adapter,
18806
+ newLinkId: deps.linkIdGen.next
18807
+ });
18808
+ if (!result.applied) {
18809
+ throw invariantViolation(
18810
+ "link",
18811
+ result.rejected ?? REJECTION_REASON.INVALID
18812
+ );
18813
+ }
18814
+ applied.push({
18815
+ op: operation,
18816
+ finalLinkId: result.linkId,
18817
+ rejected: null
18818
+ });
18819
+ }
18820
+ }
18821
+ return applied;
18822
+ }
18823
+
18824
+ // src/cell-copy-paste/copy.ts
18825
+ var CELL_CLIPBOARD_KIND = {
18826
+ FIELD: "field",
18827
+ LINKS: "links"
18828
+ };
18829
+ function copyLinkSpecs(adapter, activityId, isPredecessors) {
18830
+ return getIncidentLinkSnapshots(adapter, activityId, isPredecessors).map(
18831
+ (link) => ({
18832
+ otherActivityId: isPredecessors ? link.source : link.target,
18833
+ type: link.type,
18834
+ lag: link.lag
18835
+ })
18836
+ );
18837
+ }
18838
+ function emptyChangeSet(action) {
18839
+ return {
18840
+ ok: true,
18841
+ changes: {
18842
+ source: action,
18843
+ activities: [],
18844
+ links: [],
18845
+ calendars: [],
18846
+ trackingEvents: []
18847
+ }
18848
+ };
18849
+ }
18850
+ function dispatchCellCopy(action, deps) {
18851
+ const activity = deps.adapter.getActivity(action.activityId);
18852
+ if (!activity) {
18853
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
18854
+ }
18855
+ if (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS) {
18856
+ deps.clipboard.value = {
18857
+ kind: CELL_CLIPBOARD_KIND.LINKS,
18858
+ specs: copyLinkSpecs(
18859
+ deps.adapter,
18860
+ action.activityId,
18861
+ action.column === COLUMN.CUSTOM_PREDECESSORS
18862
+ )
18863
+ };
18864
+ return emptyChangeSet(action);
18865
+ }
18866
+ const pipeline = findPipelineForColumn(action.column);
18867
+ if (!pipeline) {
18868
+ return { ok: false, reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN };
18869
+ }
18870
+ const canonicalValue = readPipelineTargetValue(activity, pipeline.targetField);
18871
+ if (action.column === COLUMN.DURATION && !Number.isFinite(canonicalValue)) {
18872
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
18873
+ }
18874
+ const raw = action.column === COLUMN.DURATION ? canonicalValue / deps.hoursPerDay : cloneDomainValue(canonicalValue);
18875
+ deps.clipboard.value = { kind: CELL_CLIPBOARD_KIND.FIELD, raw };
18876
+ return emptyChangeSet(action);
18877
+ }
18878
+ function dispatchCellClipboardClear(action, clipboard) {
18879
+ clipboard.value = null;
18880
+ return emptyChangeSet(action);
18881
+ }
18882
+
18883
+ // src/cell-copy-paste/paste.ts
18884
+ function toDiffPlaceholder(activityId) {
18885
+ return String(activityId);
18886
+ }
18887
+ function fromDiffPlaceholder(adapter, placeholder) {
18888
+ const activityId = placeholder;
18889
+ return adapter.activityExists(activityId) ? activityId : null;
18890
+ }
18891
+ function linkOperationsForTarget(targetActivityId, isPredecessors, clipboard, deps) {
18892
+ const desiredSpecs = clipboard.specs.map((spec) => ({
18893
+ correlativeId: toDiffPlaceholder(spec.otherActivityId),
18894
+ type: spec.type,
18895
+ lag: spec.lag
18896
+ }));
18897
+ const currentLinks = getIncidentLinkSnapshots(
18898
+ deps.adapter,
18899
+ targetActivityId,
18900
+ isPredecessors
18901
+ );
18902
+ const resolve = (placeholder) => fromDiffPlaceholder(deps.adapter, placeholder);
18903
+ const diff = isPredecessors ? diffLinks({
18904
+ targetActivityId,
18905
+ currentLinks,
18906
+ desiredSpecs,
18907
+ resolveActivityByCorrelative: resolve
18908
+ }) : diffOutgoingLinks({
18909
+ sourceActivityId: targetActivityId,
18910
+ currentLinks,
18911
+ desiredSpecs,
18912
+ resolveActivityByCorrelative: resolve
18913
+ });
18914
+ return diff.operations;
18915
+ }
18916
+ async function dispatchCellPasteCopied(action, options, deps) {
18917
+ const clipboard = deps.cellClipboard.value;
18918
+ if (clipboard === null) {
18919
+ return { ok: false, reason: REJECTION_REASON.CLIPBOARD_EMPTY };
18920
+ }
18921
+ if (action.activityIds.length === 0) {
18922
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
18923
+ }
18924
+ const hasMissingActivity = action.activityIds.some(
18925
+ (activityId) => !deps.adapter.activityExists(activityId)
18926
+ );
18927
+ if (hasMissingActivity) {
18928
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
18929
+ }
18930
+ if (action.column === COLUMN.CUSTOM_PREDECESSORS || action.column === COLUMN.CUSTOM_SUCESSORS) {
18931
+ if (clipboard.kind !== CELL_CLIPBOARD_KIND.LINKS) {
18932
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
18933
+ }
18934
+ const hasSelfReference = action.activityIds.some(
18935
+ (activityId) => clipboard.specs.some(
18936
+ (spec) => String(spec.otherActivityId) === String(activityId)
18937
+ )
18938
+ );
18939
+ if (hasSelfReference) {
18940
+ return { ok: false, reason: LINK_REJECTION_REASON.SELF_LINK };
18941
+ }
18942
+ const isPredecessors = action.column === COLUMN.CUSTOM_PREDECESSORS;
18943
+ const operations = action.activityIds.flatMap(
18944
+ (activityId) => linkOperationsForTarget(activityId, isPredecessors, clipboard, deps)
18945
+ );
18946
+ const createEntries = operations.filter(
18947
+ (operation) => operation.kind === LINK_OPERATION_KIND.CREATE
18948
+ ).map((operation) => ({
18949
+ operation,
18950
+ linkId: deps.linkIdGen.next()
18951
+ }));
18952
+ const creation = validateLinkCreateBatch(createEntries, {
18953
+ port: deps.adapter,
18954
+ rejectParentLinkMutations: options.rejectParentLinkMutations ?? false
18955
+ });
18956
+ if (!creation.applied) {
18957
+ return {
18958
+ ok: false,
18959
+ reason: creation.rejected ?? REJECTION_REASON.INVALID
18960
+ };
18961
+ }
18962
+ return dispatchLinkBatch(operations, action, options, deps);
18963
+ }
18964
+ if (clipboard.kind !== CELL_CLIPBOARD_KIND.FIELD) {
18965
+ return { ok: false, reason: REJECTION_REASON.INVALID_VALUE };
18966
+ }
18967
+ if (!findPipelineForColumn(action.column)) {
18968
+ return { ok: false, reason: REJECTION_REASON.NO_PIPELINE_FOR_COLUMN };
18969
+ }
18970
+ const column = action.column;
18971
+ const result = await dispatchBulkEdit(
18972
+ {
18973
+ kind: DISPATCH_ACTION_KIND.BULK_EDIT,
18974
+ edits: action.activityIds.map((activityId) => ({
18975
+ activityId,
18976
+ column,
18977
+ newValue: clipboard.raw
18978
+ })),
18979
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
18980
+ },
18981
+ options,
18982
+ deps
18983
+ );
18984
+ if (!result.ok) return result;
18985
+ return { ...result, changes: { ...result.changes, source: action } };
18986
+ }
18987
+
18485
18988
  // src/dispatch/dispatch.ts
18486
18989
  async function performDispatch(action, options, deps) {
18487
18990
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_COPY) {
@@ -18499,6 +19002,22 @@ async function performDispatch(action, options, deps) {
18499
19002
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED) {
18500
19003
  return dispatchActivityPasteCopied(action, options, deps);
18501
19004
  }
19005
+ if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_DUPLICATE) {
19006
+ return dispatchActivityDuplicate(action, options, deps);
19007
+ }
19008
+ if (action.kind === DISPATCH_ACTION_KIND.CELL_COPY) {
19009
+ return dispatchCellCopy(action, {
19010
+ adapter: deps.adapter,
19011
+ clipboard: deps.cellClipboard,
19012
+ hoursPerDay: deps.sector.hoursPerDay
19013
+ });
19014
+ }
19015
+ if (action.kind === DISPATCH_ACTION_KIND.CELL_CLIPBOARD_CLEAR) {
19016
+ return dispatchCellClipboardClear(action, deps.cellClipboard);
19017
+ }
19018
+ if (action.kind === DISPATCH_ACTION_KIND.CELL_PASTE_COPIED) {
19019
+ return dispatchCellPasteCopied(action, options, deps);
19020
+ }
18502
19021
  if (action.kind === ACTIVITY_BATCH_ACTION_KIND) {
18503
19022
  return dispatchActivityBatch(action, options, deps);
18504
19023
  }
@@ -30171,7 +30690,7 @@ function validateP6EndingActivitySearch(input) {
30171
30690
  );
30172
30691
  }
30173
30692
  }
30174
- function emptyChangeSet(source) {
30693
+ function emptyChangeSet2(source) {
30175
30694
  return {
30176
30695
  source,
30177
30696
  activities: [],
@@ -30209,6 +30728,8 @@ function isCurrentFilterProjection(projection, filter, context) {
30209
30728
  var ScheduleCore = class _ScheduleCore {
30210
30729
  /** Activity reads and mutations. */
30211
30730
  activities;
30731
+ /** Single-cell clipboard reads and mutations. */
30732
+ cells;
30212
30733
  /** Dependency-link reads and mutations. */
30213
30734
  links;
30214
30735
  /** Calendar reads and reconciliation. */
@@ -30246,6 +30767,7 @@ var ScheduleCore = class _ScheduleCore {
30246
30767
  schedule: (patch) => this._scheduleP6(patch)
30247
30768
  });
30248
30769
  _activityClipboard = { value: null };
30770
+ _cellClipboard = { value: null };
30249
30771
  _status = SCHEDULE_CORE_STATUS.READY;
30250
30772
  coreRuntime;
30251
30773
  _isFork;
@@ -30290,6 +30812,9 @@ var ScheduleCore = class _ScheduleCore {
30290
30812
  this._activityClipboard.value = cloneDomainValue(
30291
30813
  source._activityClipboard.value
30292
30814
  );
30815
+ this._cellClipboard.value = cloneDomainValue(
30816
+ source._cellClipboard.value
30817
+ );
30293
30818
  this._undo = source._undo.fork();
30294
30819
  this._saveTracker = source._saveTracker.fork();
30295
30820
  this._scheduleRevision = source._scheduleRevision;
@@ -31518,7 +32043,7 @@ var ScheduleCore = class _ScheduleCore {
31518
32043
  });
31519
32044
  continue;
31520
32045
  }
31521
- const changes = emptyChangeSet(action);
32046
+ const changes = emptyChangeSet2(action);
31522
32047
  results.push({ ok: true, changes });
31523
32048
  acceptedChangeSets.push(changes);
31524
32049
  acceptedActions.push(action);
@@ -31647,6 +32172,7 @@ var ScheduleCore = class _ScheduleCore {
31647
32172
  customIdTracker: this.coreRuntime.customIdTracker,
31648
32173
  reporter: this.coreRuntime.reporter,
31649
32174
  activityClipboard: this._activityClipboard,
32175
+ cellClipboard: this._cellClipboard,
31650
32176
  stateRevision: this._stateRevision,
31651
32177
  defaultBaseCalendarId: this.coreRuntime.baseCalendars.find(
31652
32178
  (calendar) => calendar.baseDefault