@beyondwork/docx-react-component 1.0.58 → 1.0.59
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/README.md +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +978 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +476 -34
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +5 -8
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -84,5 +84,11 @@ export interface ScopeMetadataResolver {
|
|
|
84
84
|
version: number;
|
|
85
85
|
}>;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Release a ref that is no longer needed (called when toggling a
|
|
89
|
+
* scope from `"external"` back to `"internal"`). Optional — hosts
|
|
90
|
+
* that don't need cleanup may omit it. Rejections are surfaced as
|
|
91
|
+
* non-fatal warnings; the scope continues to function internally.
|
|
92
|
+
*/
|
|
87
93
|
delete?(ref: ScopeMetadataStorageRef): Promise<void>;
|
|
88
94
|
}
|
|
@@ -523,6 +523,7 @@ function getInlineLength(node: InlineNode): number {
|
|
|
523
523
|
case "wordart":
|
|
524
524
|
case "vml_shape":
|
|
525
525
|
case "drawing_frame":
|
|
526
|
+
case "ole_embed":
|
|
526
527
|
return 1;
|
|
527
528
|
}
|
|
528
529
|
}
|
|
@@ -584,6 +585,8 @@ function getInlineDisplayText(node: InlineNode): string {
|
|
|
584
585
|
return node.text ?? "[VML Shape]";
|
|
585
586
|
case "drawing_frame":
|
|
586
587
|
return node.content.type === "picture" ? "[Image]" : "[Drawing]";
|
|
588
|
+
case "ole_embed":
|
|
589
|
+
return "[Embedded object]";
|
|
587
590
|
}
|
|
588
591
|
}
|
|
589
592
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type SelectionSnapshot,
|
|
9
9
|
} from "../state/editor-state.ts";
|
|
10
10
|
import {
|
|
11
|
+
anchorUnaffectedByMapping,
|
|
11
12
|
areAnchorsEqual,
|
|
12
13
|
createDetachedAnchor,
|
|
13
14
|
createEmptyMapping,
|
|
@@ -89,6 +90,14 @@ import {
|
|
|
89
90
|
setHeaderFooterLinkAtSectionIndex,
|
|
90
91
|
} from "./section-layout-commands.ts";
|
|
91
92
|
import { insertPageBreak, insertTable } from "./text-commands.ts";
|
|
93
|
+
// NOTE: This file imports `editLayer` from `runtime/edit-ops/index.ts`,
|
|
94
|
+
// which in turn imports command primitives from `./text-commands.ts`. That
|
|
95
|
+
// is a **diamond** dependency (A → B, A → C, B → C), NOT a module-level
|
|
96
|
+
// cycle — `text-commands.ts` does not re-enter `core/commands/index.ts`
|
|
97
|
+
// or `runtime/edit-ops/`. Verified 2026-04-20 by static DFS; see
|
|
98
|
+
// docs/plans/architecture-lane.md §F3. Keep it this way — don't add
|
|
99
|
+
// imports from `runtime/` that route through this file, as that could
|
|
100
|
+
// introduce a real cycle.
|
|
92
101
|
import { editLayer } from "../../runtime/edit-ops/index.ts";
|
|
93
102
|
import { structureLayer } from "../../runtime/structure-ops/index.ts";
|
|
94
103
|
import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
|
|
@@ -411,6 +420,14 @@ export type EditorCommand =
|
|
|
411
420
|
export interface TransactionEffects {
|
|
412
421
|
warningsAdded: EditorWarning[];
|
|
413
422
|
warningsCleared: Array<{ warningId: string; code: EditorWarning["code"] }>;
|
|
423
|
+
/**
|
|
424
|
+
* Fire-and-forget warnings emitted by the `notify` loop as `warning_added`
|
|
425
|
+
* events + `onWarning` callbacks, but NOT written to `state.warnings` or
|
|
426
|
+
* `CompatibilityReport.warnings`. Used for no-op diagnostic signaling such
|
|
427
|
+
* as `review_target_not_found` — the host should be told an op was
|
|
428
|
+
* skipped without accumulating a persistent entry every time.
|
|
429
|
+
*/
|
|
430
|
+
transientWarnings?: EditorWarning[];
|
|
414
431
|
commentAdded?: { commentId: string; anchor: EditorAnchorProjection };
|
|
415
432
|
commentResolved?: { commentId: string };
|
|
416
433
|
commentReopened?: { commentId: string };
|
|
@@ -800,6 +817,16 @@ export function executeEditorCommand(
|
|
|
800
817
|
return createTransaction(state, {
|
|
801
818
|
historyBoundary: "skip",
|
|
802
819
|
markDirty: false,
|
|
820
|
+
effects: {
|
|
821
|
+
transientWarnings: [
|
|
822
|
+
reviewTargetNotFoundWarning(
|
|
823
|
+
"resolveComment",
|
|
824
|
+
command.commentId,
|
|
825
|
+
"comment_unknown",
|
|
826
|
+
"unknown commentId",
|
|
827
|
+
),
|
|
828
|
+
],
|
|
829
|
+
},
|
|
803
830
|
});
|
|
804
831
|
}
|
|
805
832
|
|
|
@@ -847,8 +874,38 @@ export function executeEditorCommand(
|
|
|
847
874
|
}
|
|
848
875
|
case "comment.reopen": {
|
|
849
876
|
const existingThread = state.document.review.comments[command.commentId];
|
|
850
|
-
if (!existingThread
|
|
851
|
-
return createTransaction(state, {
|
|
877
|
+
if (!existingThread) {
|
|
878
|
+
return createTransaction(state, {
|
|
879
|
+
historyBoundary: "skip",
|
|
880
|
+
markDirty: false,
|
|
881
|
+
effects: {
|
|
882
|
+
transientWarnings: [
|
|
883
|
+
reviewTargetNotFoundWarning(
|
|
884
|
+
"reopenComment",
|
|
885
|
+
command.commentId,
|
|
886
|
+
"comment_unknown",
|
|
887
|
+
"unknown commentId",
|
|
888
|
+
),
|
|
889
|
+
],
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (existingThread.status === "detached") {
|
|
894
|
+
return createTransaction(state, {
|
|
895
|
+
historyBoundary: "skip",
|
|
896
|
+
markDirty: false,
|
|
897
|
+
effects: {
|
|
898
|
+
transientWarnings: [
|
|
899
|
+
reviewTargetNotFoundWarning(
|
|
900
|
+
"reopenComment",
|
|
901
|
+
command.commentId,
|
|
902
|
+
"comment_detached",
|
|
903
|
+
"thread is detached",
|
|
904
|
+
{ status: "detached" },
|
|
905
|
+
),
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
});
|
|
852
909
|
}
|
|
853
910
|
const reopenedComments = {
|
|
854
911
|
...state.document.review.comments,
|
|
@@ -880,11 +937,38 @@ export function executeEditorCommand(
|
|
|
880
937
|
case "comment.add-reply": {
|
|
881
938
|
const threadForReply = state.document.review.comments[command.commentId];
|
|
882
939
|
if (!threadForReply) {
|
|
883
|
-
return createTransaction(state, {
|
|
940
|
+
return createTransaction(state, {
|
|
941
|
+
historyBoundary: "skip",
|
|
942
|
+
markDirty: false,
|
|
943
|
+
effects: {
|
|
944
|
+
transientWarnings: [
|
|
945
|
+
reviewTargetNotFoundWarning(
|
|
946
|
+
"addCommentReply",
|
|
947
|
+
command.commentId,
|
|
948
|
+
"comment_unknown",
|
|
949
|
+
"unknown commentId",
|
|
950
|
+
),
|
|
951
|
+
],
|
|
952
|
+
},
|
|
953
|
+
});
|
|
884
954
|
}
|
|
885
955
|
// Block reply on resolved or detached threads (must reopen first)
|
|
886
956
|
if (threadForReply.status === "resolved" || threadForReply.status === "detached") {
|
|
887
|
-
return createTransaction(state, {
|
|
957
|
+
return createTransaction(state, {
|
|
958
|
+
historyBoundary: "skip",
|
|
959
|
+
markDirty: false,
|
|
960
|
+
effects: {
|
|
961
|
+
transientWarnings: [
|
|
962
|
+
reviewTargetNotFoundWarning(
|
|
963
|
+
"addCommentReply",
|
|
964
|
+
command.commentId,
|
|
965
|
+
"comment_status",
|
|
966
|
+
`thread is ${threadForReply.status}`,
|
|
967
|
+
{ status: threadForReply.status },
|
|
968
|
+
),
|
|
969
|
+
],
|
|
970
|
+
},
|
|
971
|
+
});
|
|
888
972
|
}
|
|
889
973
|
const entryId = `${command.commentId}-entry-${(threadForReply.entries?.length ?? 0) + 1}`;
|
|
890
974
|
const newEntry = {
|
|
@@ -919,8 +1003,37 @@ export function executeEditorCommand(
|
|
|
919
1003
|
}
|
|
920
1004
|
case "comment.edit-body": {
|
|
921
1005
|
const threadToEdit = state.document.review.comments[command.commentId];
|
|
922
|
-
if (!threadToEdit
|
|
923
|
-
return createTransaction(state, {
|
|
1006
|
+
if (!threadToEdit) {
|
|
1007
|
+
return createTransaction(state, {
|
|
1008
|
+
historyBoundary: "skip",
|
|
1009
|
+
markDirty: false,
|
|
1010
|
+
effects: {
|
|
1011
|
+
transientWarnings: [
|
|
1012
|
+
reviewTargetNotFoundWarning(
|
|
1013
|
+
"editCommentBody",
|
|
1014
|
+
command.commentId,
|
|
1015
|
+
"comment_unknown",
|
|
1016
|
+
"unknown commentId",
|
|
1017
|
+
),
|
|
1018
|
+
],
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (!threadToEdit.entries?.length) {
|
|
1023
|
+
return createTransaction(state, {
|
|
1024
|
+
historyBoundary: "skip",
|
|
1025
|
+
markDirty: false,
|
|
1026
|
+
effects: {
|
|
1027
|
+
transientWarnings: [
|
|
1028
|
+
reviewTargetNotFoundWarning(
|
|
1029
|
+
"editCommentBody",
|
|
1030
|
+
command.commentId,
|
|
1031
|
+
"comment_no_entries",
|
|
1032
|
+
"thread has no entries to edit",
|
|
1033
|
+
),
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
924
1037
|
}
|
|
925
1038
|
const editedEntries = [...threadToEdit.entries];
|
|
926
1039
|
editedEntries[0] = { ...editedEntries[0], body: command.body };
|
|
@@ -1443,6 +1556,9 @@ function createTransaction(
|
|
|
1443
1556
|
effects: {
|
|
1444
1557
|
warningsAdded: options.effects?.warningsAdded ?? [],
|
|
1445
1558
|
warningsCleared: options.effects?.warningsCleared ?? [],
|
|
1559
|
+
...(options.effects?.transientWarnings
|
|
1560
|
+
? { transientWarnings: options.effects.transientWarnings }
|
|
1561
|
+
: {}),
|
|
1446
1562
|
commentAdded: options.effects?.commentAdded,
|
|
1447
1563
|
commentResolved: options.effects?.commentResolved,
|
|
1448
1564
|
changeAccepted: options.effects?.changeAccepted,
|
|
@@ -1453,6 +1569,71 @@ function createTransaction(
|
|
|
1453
1569
|
};
|
|
1454
1570
|
}
|
|
1455
1571
|
|
|
1572
|
+
/**
|
|
1573
|
+
* Classify why `applyReviewCommand` produced no applied outcome so hosts can
|
|
1574
|
+
* branch on `details.reason`. Inspects the pre-command revision store.
|
|
1575
|
+
*/
|
|
1576
|
+
function classifyReviewSkipReason(
|
|
1577
|
+
store: RevisionStore,
|
|
1578
|
+
command: ReviewCommand,
|
|
1579
|
+
): { reason: string; message: string; extraDetails?: Record<string, unknown> } {
|
|
1580
|
+
if (command.type === "review.accept-all-revisions" || command.type === "review.reject-all-revisions") {
|
|
1581
|
+
return {
|
|
1582
|
+
reason: "no_actionable_revisions",
|
|
1583
|
+
message: "no actionable revisions to apply",
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
const existing = store.revisions[command.revisionId];
|
|
1587
|
+
if (!existing) {
|
|
1588
|
+
return { reason: "revision_unknown", message: "unknown revisionId" };
|
|
1589
|
+
}
|
|
1590
|
+
if (existing.status === "detached") {
|
|
1591
|
+
return {
|
|
1592
|
+
reason: "revision_detached",
|
|
1593
|
+
message: "revision anchor is detached",
|
|
1594
|
+
extraDetails: { status: "detached" },
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if (existing.status === "accepted" || existing.status === "rejected") {
|
|
1598
|
+
return {
|
|
1599
|
+
reason: "revision_status",
|
|
1600
|
+
message: `revision is already ${existing.status}`,
|
|
1601
|
+
extraDetails: { status: existing.status },
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
reason: "revision_preserve_only",
|
|
1606
|
+
message: "revision is preserve-only and cannot be accepted or rejected",
|
|
1607
|
+
extraDetails: { kind: existing.kind },
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Build a fire-and-forget `review_target_not_found` warning for a review op
|
|
1613
|
+
* whose target id was unknown, resolved, detached, or preserve-only. Emitted
|
|
1614
|
+
* via `effects.transientWarnings` — surfaces on `onWarning` + `warning_added`
|
|
1615
|
+
* event without accumulating in `state.warnings` or `CompatibilityReport`.
|
|
1616
|
+
*
|
|
1617
|
+
* `reason` is a machine-readable code consumers can branch on; `message`
|
|
1618
|
+
* is the human-readable string that lands on `EditorWarning.message`.
|
|
1619
|
+
*/
|
|
1620
|
+
function reviewTargetNotFoundWarning(
|
|
1621
|
+
op: string,
|
|
1622
|
+
targetId: string,
|
|
1623
|
+
reason: string,
|
|
1624
|
+
message: string,
|
|
1625
|
+
extraDetails?: Record<string, unknown>,
|
|
1626
|
+
): EditorWarning {
|
|
1627
|
+
return {
|
|
1628
|
+
warningId: `review-target-not-found-${op}-${targetId}-${Date.now()}`,
|
|
1629
|
+
code: "review_target_not_found",
|
|
1630
|
+
severity: "info",
|
|
1631
|
+
message: `${op}("${targetId}") skipped: ${message}.`,
|
|
1632
|
+
source: "review",
|
|
1633
|
+
details: { op, targetId, reason, ...(extraDetails ?? {}) },
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1456
1637
|
function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
|
|
1457
1638
|
const activeRange = normalizeSelectionAnchor(
|
|
1458
1639
|
selection.activeRange,
|
|
@@ -1673,10 +1854,11 @@ function applyReviewCommand(
|
|
|
1673
1854
|
});
|
|
1674
1855
|
}
|
|
1675
1856
|
|
|
1857
|
+
const priorStore = createRevisionStoreFromState(state);
|
|
1676
1858
|
const result = applyRevisionRuntimeCommand({
|
|
1677
1859
|
state: {
|
|
1678
1860
|
document: state.document,
|
|
1679
|
-
store:
|
|
1861
|
+
store: priorStore,
|
|
1680
1862
|
},
|
|
1681
1863
|
command,
|
|
1682
1864
|
timestamp,
|
|
@@ -1684,9 +1866,27 @@ function applyReviewCommand(
|
|
|
1684
1866
|
const hasAppliedOutcome = result.outcomes.some((outcome) => outcome.kind === "applied");
|
|
1685
1867
|
|
|
1686
1868
|
if (!hasAppliedOutcome) {
|
|
1869
|
+
const op =
|
|
1870
|
+
command.type === "review.accept-revision"
|
|
1871
|
+
? "acceptChange"
|
|
1872
|
+
: command.type === "review.reject-revision"
|
|
1873
|
+
? "rejectChange"
|
|
1874
|
+
: command.type === "review.accept-all-revisions"
|
|
1875
|
+
? "acceptAllChanges"
|
|
1876
|
+
: "rejectAllChanges";
|
|
1877
|
+
const targetId =
|
|
1878
|
+
command.type === "review.accept-revision" || command.type === "review.reject-revision"
|
|
1879
|
+
? command.revisionId
|
|
1880
|
+
: "*";
|
|
1881
|
+
const { reason, message, extraDetails } = classifyReviewSkipReason(priorStore, command);
|
|
1687
1882
|
return createTransaction(state, {
|
|
1688
1883
|
historyBoundary: "skip",
|
|
1689
1884
|
markDirty: false,
|
|
1885
|
+
effects: {
|
|
1886
|
+
transientWarnings: [
|
|
1887
|
+
reviewTargetNotFoundWarning(op, targetId, reason, message, extraDetails),
|
|
1888
|
+
],
|
|
1889
|
+
},
|
|
1690
1890
|
});
|
|
1691
1891
|
}
|
|
1692
1892
|
|
|
@@ -1781,15 +1981,24 @@ function remapReviewStateAfterContentChange(
|
|
|
1781
1981
|
nextContent: nextDocument.content,
|
|
1782
1982
|
existingWarnings: mappedWarnings,
|
|
1783
1983
|
});
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
])
|
|
1792
|
-
|
|
1984
|
+
// Remap revision anchors through the mapping. Most revisions are outside the
|
|
1985
|
+
// edited range and don't need new objects — skip allocation for those and
|
|
1986
|
+
// only rebuild the record when at least one anchor actually moved.
|
|
1987
|
+
const sourceRevisions = nextDocument.review.revisions;
|
|
1988
|
+
let revisions = sourceRevisions;
|
|
1989
|
+
if (mapping.steps.length > 0) {
|
|
1990
|
+
let changed = false;
|
|
1991
|
+
const remappedEntries = Object.entries(sourceRevisions).map(([changeId, revision]) => {
|
|
1992
|
+
if (anchorUnaffectedByMapping(revision.anchor, mapping)) {
|
|
1993
|
+
return [changeId, revision] as const;
|
|
1994
|
+
}
|
|
1995
|
+
changed = true;
|
|
1996
|
+
return [changeId, { ...revision, anchor: mapAnchor(revision.anchor, mapping) }] as const;
|
|
1997
|
+
});
|
|
1998
|
+
if (changed) {
|
|
1999
|
+
revisions = Object.fromEntries(remappedEntries);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1793
2002
|
const activeCommentId =
|
|
1794
2003
|
state.runtime.activeCommentId &&
|
|
1795
2004
|
remappedComments.comments[state.runtime.activeCommentId]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockNode,
|
|
3
|
+
DocumentRootNode,
|
|
4
|
+
FieldNode,
|
|
5
|
+
InlineNode,
|
|
6
|
+
LegacyFormFieldNode,
|
|
7
|
+
ParagraphNode,
|
|
8
|
+
TableCellNode,
|
|
9
|
+
TableNode,
|
|
10
|
+
TableRowNode,
|
|
11
|
+
} from "../../model/canonical-document.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Any document shape that carries a `DocumentRootNode` on a `content` field.
|
|
15
|
+
* `CanonicalDocumentEnvelope`, `NormalizedTextDocument`, and the editor
|
|
16
|
+
* state snapshot all match this shape structurally.
|
|
17
|
+
*/
|
|
18
|
+
export interface DocWithContent {
|
|
19
|
+
content: DocumentRootNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Phase V.a — update a legacy form field value by name.
|
|
24
|
+
*
|
|
25
|
+
* For each `FieldNode` in the document whose `legacyFormField.name` matches
|
|
26
|
+
* the supplied name, the relevant typed field is updated:
|
|
27
|
+
*
|
|
28
|
+
* kind="textInput" → `textInput.default = value` (value must be string)
|
|
29
|
+
* kind="checkBox" → `checkBox.checked = value` (value must be boolean)
|
|
30
|
+
* kind="ddList" → `ddList.default = value` (value must be numeric
|
|
31
|
+
* index into `ddList.listEntry`)
|
|
32
|
+
*
|
|
33
|
+
* The node is marked `mutated: true` so the serializer regenerates the
|
|
34
|
+
* `<w:ffData>` XML from the typed fields (via `regenerateFFDataRawXml`)
|
|
35
|
+
* instead of emitting the preserved `rawXml` verbatim.
|
|
36
|
+
*
|
|
37
|
+
* Walk covers body, tables, SDT/customXml wrappers. Headers and footers are
|
|
38
|
+
* owned by their own document parts; call `setLegacyFormFieldValue` on
|
|
39
|
+
* those parts separately if the host needs to mutate them.
|
|
40
|
+
*
|
|
41
|
+
* If no field matches the name, the document is returned unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export function setLegacyFormFieldValue<D extends DocWithContent>(
|
|
44
|
+
document: D,
|
|
45
|
+
fieldName: string,
|
|
46
|
+
value: string | boolean | number,
|
|
47
|
+
): D {
|
|
48
|
+
const nextChildren = document.content.children.map((child) =>
|
|
49
|
+
rewriteBlock(child, fieldName, value),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (arraysShallowEqual(nextChildren, document.content.children)) {
|
|
53
|
+
return document;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...document,
|
|
58
|
+
content: { ...document.content, children: nextChildren },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rewriteBlock(
|
|
63
|
+
block: BlockNode,
|
|
64
|
+
fieldName: string,
|
|
65
|
+
value: string | boolean | number,
|
|
66
|
+
): BlockNode {
|
|
67
|
+
if (block.type === "paragraph") {
|
|
68
|
+
return rewriteParagraph(block, fieldName, value);
|
|
69
|
+
}
|
|
70
|
+
if (block.type === "table") {
|
|
71
|
+
return rewriteTable(block, fieldName, value);
|
|
72
|
+
}
|
|
73
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
74
|
+
const nextChildren = block.children.map((c) => rewriteBlock(c, fieldName, value));
|
|
75
|
+
if (arraysShallowEqual(nextChildren, block.children)) return block;
|
|
76
|
+
return { ...block, children: nextChildren };
|
|
77
|
+
}
|
|
78
|
+
return block;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rewriteParagraph(
|
|
82
|
+
paragraph: ParagraphNode,
|
|
83
|
+
fieldName: string,
|
|
84
|
+
value: string | boolean | number,
|
|
85
|
+
): ParagraphNode {
|
|
86
|
+
const nextChildren = paragraph.children.map((inline) =>
|
|
87
|
+
rewriteInline(inline, fieldName, value),
|
|
88
|
+
);
|
|
89
|
+
if (arraysShallowEqual(nextChildren, paragraph.children)) return paragraph;
|
|
90
|
+
return { ...paragraph, children: nextChildren };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rewriteInline(
|
|
94
|
+
inline: InlineNode,
|
|
95
|
+
fieldName: string,
|
|
96
|
+
value: string | boolean | number,
|
|
97
|
+
): InlineNode {
|
|
98
|
+
if (inline.type !== "field") return inline;
|
|
99
|
+
const field = inline as FieldNode;
|
|
100
|
+
const lff = field.legacyFormField;
|
|
101
|
+
if (!lff || lff.name !== fieldName) return inline;
|
|
102
|
+
|
|
103
|
+
const nextLff = applyValueToLegacyFormField(lff, value);
|
|
104
|
+
if (nextLff === lff) return inline;
|
|
105
|
+
return { ...field, legacyFormField: nextLff };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyValueToLegacyFormField(
|
|
109
|
+
lff: LegacyFormFieldNode,
|
|
110
|
+
value: string | boolean | number,
|
|
111
|
+
): LegacyFormFieldNode {
|
|
112
|
+
switch (lff.kind) {
|
|
113
|
+
case "textInput": {
|
|
114
|
+
if (typeof value !== "string") return lff;
|
|
115
|
+
const currentDefault = lff.textInput?.default;
|
|
116
|
+
if (currentDefault === value) return lff;
|
|
117
|
+
return {
|
|
118
|
+
...lff,
|
|
119
|
+
textInput: { ...(lff.textInput ?? {}), default: value },
|
|
120
|
+
mutated: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "checkBox": {
|
|
124
|
+
if (typeof value !== "boolean") return lff;
|
|
125
|
+
if (lff.checkBox?.checked === value) return lff;
|
|
126
|
+
return {
|
|
127
|
+
...lff,
|
|
128
|
+
checkBox: { ...(lff.checkBox ?? {}), checked: value },
|
|
129
|
+
mutated: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case "ddList": {
|
|
133
|
+
if (typeof value !== "number") return lff;
|
|
134
|
+
if (lff.ddList?.default === value) return lff;
|
|
135
|
+
return {
|
|
136
|
+
...lff,
|
|
137
|
+
ddList: { ...(lff.ddList ?? {}), default: value },
|
|
138
|
+
mutated: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function rewriteTable(
|
|
145
|
+
table: TableNode,
|
|
146
|
+
fieldName: string,
|
|
147
|
+
value: string | boolean | number,
|
|
148
|
+
): TableNode {
|
|
149
|
+
const nextRows = table.rows.map((row) => rewriteRow(row, fieldName, value));
|
|
150
|
+
if (arraysShallowEqual(nextRows, table.rows)) return table;
|
|
151
|
+
return { ...table, rows: nextRows };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rewriteRow(
|
|
155
|
+
row: TableRowNode,
|
|
156
|
+
fieldName: string,
|
|
157
|
+
value: string | boolean | number,
|
|
158
|
+
): TableRowNode {
|
|
159
|
+
const nextCells = row.cells.map((cell) => rewriteCell(cell, fieldName, value));
|
|
160
|
+
if (arraysShallowEqual(nextCells, row.cells)) return row;
|
|
161
|
+
return { ...row, cells: nextCells };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function rewriteCell(
|
|
165
|
+
cell: TableCellNode,
|
|
166
|
+
fieldName: string,
|
|
167
|
+
value: string | boolean | number,
|
|
168
|
+
): TableCellNode {
|
|
169
|
+
const nextChildren = cell.children.map((child) => rewriteBlock(child, fieldName, value));
|
|
170
|
+
if (arraysShallowEqual(nextChildren, cell.children)) return cell;
|
|
171
|
+
return { ...cell, children: nextChildren };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function arraysShallowEqual<T>(a: readonly T[], b: readonly T[]): boolean {
|
|
175
|
+
if (a === b) return true;
|
|
176
|
+
if (a.length !== b.length) return false;
|
|
177
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
178
|
+
if (a[i] !== b[i]) return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|