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