@beyondwork/docx-react-component 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.8",
4
+ "version": "1.0.10",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -29,6 +29,18 @@
29
29
  "types": "./src/ui-tailwind/index.ts",
30
30
  "import": "./src/ui-tailwind/index.ts"
31
31
  },
32
+ "./api/public-types": {
33
+ "types": "./src/api/public-types.ts",
34
+ "import": "./src/api/public-types.ts"
35
+ },
36
+ "./io/docx-session": {
37
+ "types": "./src/io/docx-session.ts",
38
+ "import": "./src/io/docx-session.ts"
39
+ },
40
+ "./runtime/document-runtime": {
41
+ "types": "./src/runtime/document-runtime.ts",
42
+ "import": "./src/runtime/document-runtime.ts"
43
+ },
32
44
  "./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css",
33
45
  "./package.json": "./package.json"
34
46
  },
@@ -78,7 +90,7 @@
78
90
  "tailwindcss": "^4.2.2"
79
91
  },
80
92
  "devDependencies": {
81
- "@chllming/wave-orchestration": "^0.9.8",
93
+ "@chllming/wave-orchestration": "^0.9.12",
82
94
  "@types/react": "19.2.14",
83
95
  "@types/react-dom": "19.2.3",
84
96
  "@typescript/native-preview": "7.0.0-dev.20260409.1",
@@ -508,6 +508,7 @@ export interface WordReviewEditorRef {
508
508
  getWarnings(): EditorWarning[];
509
509
  getComments(): CommentSidebarSnapshot;
510
510
  getTrackedChanges(): TrackedChangesSnapshot;
511
+ scrollToRevision(revisionId: string): void;
511
512
  }
512
513
 
513
514
  export interface WordReviewEditorProps {
@@ -48,6 +48,7 @@ export interface CompareDocumentVersionsOptions {
48
48
  }
49
49
 
50
50
  interface BuildEntry {
51
+ changeId?: string;
51
52
  block: BlockNode;
52
53
  trackChange?: "insertion" | "deletion";
53
54
  tracked: boolean;
@@ -196,6 +197,7 @@ function addTrackedOrStructuralEntry(
196
197
  const text = getBlockDisplayText(block);
197
198
 
198
199
  buildEntries.push({
200
+ ...(tracked ? { changeId } : {}),
199
201
  block: cloned,
200
202
  ...(tracked ? { trackChange: direction } : {}),
201
203
  tracked,
@@ -243,7 +245,7 @@ function buildComparedDocument(
243
245
  if (entry.block.type === "paragraph") {
244
246
  if (entry.trackChange) {
245
247
  pendingRevisions.push({
246
- changeId: `change-${pendingRevisions.length + 1}`,
248
+ changeId: entry.changeId ?? `change-${pendingRevisions.length + 1}`,
247
249
  kind: entry.trackChange,
248
250
  paragraphIndex,
249
251
  });
@@ -281,7 +283,7 @@ function buildComparedDocument(
281
283
  children: contentChildren,
282
284
  },
283
285
  review: {
284
- comments: {},
286
+ comments: projectValue(target.review.comments),
285
287
  revisions,
286
288
  },
287
289
  preservation: mergePreservationStores(base.preservation, target.preservation),
@@ -321,15 +323,14 @@ function createParagraphRevisionRecords(
321
323
  }
322
324
 
323
325
  const entries: Array<[string, RevisionRecord]> = [];
324
- pendingRevisions.forEach((revision, index) => {
326
+ pendingRevisions.forEach((revision) => {
325
327
  const position = positionByParagraphIndex.get(revision.paragraphIndex);
326
328
  if (position === undefined) {
327
329
  return;
328
330
  }
329
331
 
330
- const changeId = `change-${index + 1}`;
331
332
  const record: RevisionRecord = {
332
- changeId,
333
+ changeId: revision.changeId,
333
334
  kind: revision.kind,
334
335
  anchor: {
335
336
  kind: "range",
@@ -346,7 +347,7 @@ function createParagraphRevisionRecords(
346
347
  revision.kind === "insertion" ? "paragraph-insertion" : "paragraph-deletion",
347
348
  },
348
349
  };
349
- entries.push([changeId, record]);
350
+ entries.push([revision.changeId, record]);
350
351
  });
351
352
  return Object.fromEntries(entries);
352
353
  }
@@ -5,6 +5,12 @@ import {
5
5
  serializeNumberingXml,
6
6
  } from "../io/export/serialize-numbering.ts";
7
7
  import { serializeRuntimeRevisionsIntoDocumentXml } from "../io/export/serialize-runtime-revisions.ts";
8
+ import { splitDocumentAtReviewBoundaries } from "../io/export/split-review-boundaries.ts";
9
+ import {
10
+ createCommentExportIdMap,
11
+ serializeCommentAnchorsIntoDocumentXml,
12
+ serializeMergedCommentsXml,
13
+ } from "../io/export/serialize-comments.ts";
8
14
  import { createExportSession } from "../io/export/export-session.ts";
9
15
  import {
10
16
  DOCX_DOCUMENT_CONTENT_TYPE,
@@ -13,13 +19,34 @@ import {
13
19
  } from "../io/opc/docx-package.ts";
14
20
  import type { OpcPackage } from "../io/opc/package-reader.ts";
15
21
  import { readOpcPackage } from "../io/opc/package-reader.ts";
22
+ import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
16
23
  import type { RevisionRecord as ReviewRevisionRecord } from "../review/store/revision-types.ts";
17
24
  import type { VersionCompareResult } from "./diff-engine.ts";
18
25
 
19
26
  const MAIN_DOCUMENT_PATH = "/word/document.xml";
20
27
  const NUMBERING_PART_PATH = "/word/numbering.xml";
28
+ const COMMENTS_PART_PATH = "/word/comments.xml";
29
+ const COMMENTS_EXTENDED_PART_PATH = "/word/commentsExtended.xml";
30
+ const COMMENTS_IDS_PART_PATH = "/word/commentsIds.xml";
31
+ const PEOPLE_PART_PATH = "/word/people.xml";
21
32
  const NUMBERING_RELATIONSHIP_TYPE =
22
33
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
34
+ const COMMENTS_RELATIONSHIP_TYPE =
35
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
36
+ const COMMENTS_EXTENDED_RELATIONSHIP_TYPE =
37
+ "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
38
+ const COMMENTS_IDS_RELATIONSHIP_TYPE =
39
+ "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds";
40
+ const PEOPLE_RELATIONSHIP_TYPE =
41
+ "http://schemas.microsoft.com/office/2011/relationships/people";
42
+ const COMMENTS_CONTENT_TYPE =
43
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
44
+ const COMMENTS_EXTENDED_CONTENT_TYPE =
45
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
46
+ const COMMENTS_IDS_CONTENT_TYPE =
47
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml";
48
+ const PEOPLE_CONTENT_TYPE =
49
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
23
50
 
24
51
  export interface ExportComparedDocumentOptions {
25
52
  fileName?: string;
@@ -46,15 +73,22 @@ export function exportComparedDocumentRedlines(
46
73
  const existingRelationships =
47
74
  sourcePackage.parts.get(MAIN_DOCUMENT_PATH)?.relationships ?? [];
48
75
 
76
+ const commentThreads = Object.values(
77
+ createCommentStoreFromRuntimeComments(result.comparedDocument.review.comments).threads,
78
+ );
79
+ const actionableRevisions = toReviewRevisionRecords(result.comparedDocument.review.revisions);
49
80
  const serialized = serializeMainDocument(
50
- result.comparedDocument.content,
81
+ splitDocumentAtReviewBoundaries(
82
+ result.comparedDocument.content as never,
83
+ commentThreads,
84
+ actionableRevisions,
85
+ ) as never,
51
86
  result.comparedDocument.preservation,
52
87
  existingRelationships,
53
88
  {
54
89
  media: result.comparedDocument.media,
55
90
  },
56
91
  );
57
- const actionableRevisions = toReviewRevisionRecords(result.comparedDocument.review.revisions);
58
92
  const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
59
93
  serialized.documentXml,
60
94
  actionableRevisions,
@@ -65,23 +99,70 @@ export function exportComparedDocumentRedlines(
65
99
  `Compare export skipped ${revisionDocument.skippedRevisionIds.length} revision markers.`,
66
100
  );
67
101
  }
102
+ const exportCommentIds = createCommentExportIdMap(commentThreads);
103
+ const serializedComments = serializeMergedCommentsXml(commentThreads, {
104
+ exportCommentIds,
105
+ });
106
+ const annotatedDocument = serializeCommentAnchorsIntoDocumentXml(
107
+ revisionDocument.documentXml,
108
+ commentThreads,
109
+ undefined,
110
+ {
111
+ exportCommentIds,
112
+ },
113
+ );
114
+ if (annotatedDocument.skippedCommentIds.length > 0) {
115
+ throw new Error(
116
+ `Compare export skipped ${annotatedDocument.skippedCommentIds.length} comment anchors.`,
117
+ );
118
+ }
68
119
 
69
120
  const numberingXml = hasNumberingEntries(result.comparedDocument.numbering)
70
121
  ? serializeNumberingXml(result.comparedDocument.numbering)
71
122
  : undefined;
72
- const relationships = withOptionalNumberingRelationship(
73
- serialized.relationships,
123
+ const relationships = withOptionalRelatedPart(
124
+ withOptionalRelatedPart(
125
+ withOptionalRelatedPart(
126
+ withOptionalRelatedPart(
127
+ withOptionalRelatedPart(
128
+ serialized.relationships,
129
+ existingRelationships,
130
+ NUMBERING_RELATIONSHIP_TYPE,
131
+ "numbering.xml",
132
+ Boolean(numberingXml),
133
+ ),
134
+ existingRelationships,
135
+ COMMENTS_RELATIONSHIP_TYPE,
136
+ "comments.xml",
137
+ serializedComments.serializedCommentIds.length > 0,
138
+ ),
139
+ existingRelationships,
140
+ COMMENTS_EXTENDED_RELATIONSHIP_TYPE,
141
+ "commentsExtended.xml",
142
+ Boolean(serializedComments.commentsExtendedXml),
143
+ ),
144
+ existingRelationships,
145
+ COMMENTS_IDS_RELATIONSHIP_TYPE,
146
+ "commentsIds.xml",
147
+ Boolean(serializedComments.commentsIdsXml),
148
+ ),
74
149
  existingRelationships,
75
- Boolean(numberingXml),
150
+ PEOPLE_RELATIONSHIP_TYPE,
151
+ "people.xml",
152
+ Boolean(serializedComments.peopleXml),
76
153
  );
77
154
  const exportSession = createExportSession(sourcePackage, [
78
155
  MAIN_DOCUMENT_PATH,
79
156
  ...(numberingXml ? [NUMBERING_PART_PATH] : []),
157
+ ...(serializedComments.serializedCommentIds.length > 0 ? [COMMENTS_PART_PATH] : []),
158
+ ...(serializedComments.commentsExtendedXml ? [COMMENTS_EXTENDED_PART_PATH] : []),
159
+ ...(serializedComments.commentsIdsXml ? [COMMENTS_IDS_PART_PATH] : []),
160
+ ...(serializedComments.peopleXml ? [PEOPLE_PART_PATH] : []),
80
161
  ]);
81
162
 
82
163
  exportSession.replaceOwnedPart({
83
164
  path: MAIN_DOCUMENT_PATH,
84
- bytes: new TextEncoder().encode(revisionDocument.documentXml),
165
+ bytes: new TextEncoder().encode(annotatedDocument.documentXml),
85
166
  contentType: DOCX_DOCUMENT_CONTENT_TYPE,
86
167
  relationships,
87
168
  });
@@ -95,6 +176,43 @@ export function exportComparedDocumentRedlines(
95
176
  });
96
177
  }
97
178
 
179
+ if (serializedComments.serializedCommentIds.length > 0) {
180
+ exportSession.replaceOwnedPart({
181
+ path: COMMENTS_PART_PATH,
182
+ bytes: new TextEncoder().encode(serializedComments.commentsXml),
183
+ contentType:
184
+ sourcePackage.parts.get(COMMENTS_PART_PATH)?.contentType ?? COMMENTS_CONTENT_TYPE,
185
+ });
186
+ }
187
+
188
+ if (serializedComments.commentsExtendedXml) {
189
+ exportSession.replaceOwnedPart({
190
+ path: COMMENTS_EXTENDED_PART_PATH,
191
+ bytes: new TextEncoder().encode(serializedComments.commentsExtendedXml),
192
+ contentType:
193
+ sourcePackage.parts.get(COMMENTS_EXTENDED_PART_PATH)?.contentType ??
194
+ COMMENTS_EXTENDED_CONTENT_TYPE,
195
+ });
196
+ }
197
+
198
+ if (serializedComments.commentsIdsXml) {
199
+ exportSession.replaceOwnedPart({
200
+ path: COMMENTS_IDS_PART_PATH,
201
+ bytes: new TextEncoder().encode(serializedComments.commentsIdsXml),
202
+ contentType:
203
+ sourcePackage.parts.get(COMMENTS_IDS_PART_PATH)?.contentType ?? COMMENTS_IDS_CONTENT_TYPE,
204
+ });
205
+ }
206
+
207
+ if (serializedComments.peopleXml) {
208
+ exportSession.replaceOwnedPart({
209
+ path: PEOPLE_PART_PATH,
210
+ bytes: new TextEncoder().encode(serializedComments.peopleXml),
211
+ contentType:
212
+ sourcePackage.parts.get(PEOPLE_PART_PATH)?.contentType ?? PEOPLE_CONTENT_TYPE,
213
+ });
214
+ }
215
+
98
216
  return {
99
217
  bytes: exportSession.serialize(),
100
218
  mimeType: DOCX_MIME_TYPE,
@@ -132,28 +250,30 @@ function hasNumberingEntries(
132
250
  );
133
251
  }
134
252
 
135
- function withOptionalNumberingRelationship(
253
+ function withOptionalRelatedPart(
136
254
  relationships: readonly OpcRelationship[],
137
255
  existingRelationships: readonly OpcRelationship[],
138
- includeNumbering: boolean,
256
+ relationshipType: string,
257
+ target: string,
258
+ includePart: boolean,
139
259
  ): OpcRelationship[] {
140
260
  const next = relationships.map((relationship) => ({ ...relationship }));
141
- if (!includeNumbering) {
261
+ if (!includePart) {
142
262
  return next;
143
263
  }
144
264
 
145
- const existing = next.find((relationship) => relationship.type === NUMBERING_RELATIONSHIP_TYPE);
265
+ const existing = next.find((relationship) => relationship.type === relationshipType);
146
266
  if (existing) {
147
267
  return next;
148
268
  }
149
269
 
150
270
  const fallbackId =
151
- existingRelationships.find((relationship) => relationship.type === NUMBERING_RELATIONSHIP_TYPE)?.id ??
271
+ existingRelationships.find((relationship) => relationship.type === relationshipType)?.id ??
152
272
  `rId${next.length + 1}`;
153
273
  next.push({
154
274
  id: fallbackId,
155
- type: NUMBERING_RELATIONSHIP_TYPE,
156
- target: "numbering.xml",
275
+ type: relationshipType,
276
+ target,
157
277
  targetMode: "internal",
158
278
  });
159
279
  return next;
@@ -20,6 +20,13 @@ export interface CreateDocumentVersionSnapshotOptions {
20
20
  createdAt?: string;
21
21
  }
22
22
 
23
+ export function createDocumentVersionSnapshotId(
24
+ document: Pick<CanonicalDocument, "docId">,
25
+ name: string,
26
+ ): string {
27
+ return `${document.docId}:${name}`;
28
+ }
29
+
23
30
  export function createDocumentVersionSnapshot(
24
31
  document: CanonicalDocument,
25
32
  options: CreateDocumentVersionSnapshotOptions,
@@ -27,7 +34,8 @@ export function createDocumentVersionSnapshot(
27
34
  const canonicalDocument = projectCanonicalDocument(document);
28
35
  return {
29
36
  snapshotVersion: "document-version-snapshot/1",
30
- versionId: options.versionId ?? `${canonicalDocument.docId}:${options.name}`,
37
+ versionId:
38
+ options.versionId ?? createDocumentVersionSnapshotId(canonicalDocument, options.name),
31
39
  name: options.name,
32
40
  createdAt: options.createdAt ?? canonicalDocument.updatedAt,
33
41
  documentId: canonicalDocument.docId,
@@ -35,3 +43,24 @@ export function createDocumentVersionSnapshot(
35
43
  canonicalDocument,
36
44
  };
37
45
  }
46
+
47
+ export function saveDocumentVersionSnapshot(
48
+ snapshots: readonly DocumentVersionSnapshot[],
49
+ snapshot: DocumentVersionSnapshot,
50
+ ): DocumentVersionSnapshot[] {
51
+ const next = snapshots.filter((entry) => entry.versionId !== snapshot.versionId);
52
+ next.push(snapshot);
53
+ return next.sort(compareDocumentVersionSnapshots);
54
+ }
55
+
56
+ function compareDocumentVersionSnapshots(
57
+ left: DocumentVersionSnapshot,
58
+ right: DocumentVersionSnapshot,
59
+ ): number {
60
+ const createdAtComparison = right.createdAt.localeCompare(left.createdAt);
61
+ if (createdAtComparison !== 0) {
62
+ return createdAtComparison;
63
+ }
64
+
65
+ return left.versionId.localeCompare(right.versionId);
66
+ }
@@ -114,6 +114,16 @@ export function __createWordReviewEditorRefBridge(
114
114
  getWarnings: () => runtime.getWarnings(),
115
115
  getComments: () => runtime.getRenderSnapshot().comments,
116
116
  getTrackedChanges: () => runtime.getRenderSnapshot().trackedChanges,
117
+ scrollToRevision: (revisionId: string) => {
118
+ const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
119
+ (r) => r.revisionId === revisionId,
120
+ );
121
+ if (!revision || revision.anchor.kind === "detached") return;
122
+ runtime.dispatch({
123
+ type: "selection.set",
124
+ selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
125
+ });
126
+ },
117
127
  };
118
128
  }
119
129
 
@@ -500,6 +510,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
500
510
  getWarnings: () => activeRuntime.getWarnings(),
501
511
  getComments: () => activeRuntime.getRenderSnapshot().comments,
502
512
  getTrackedChanges: () => activeRuntime.getRenderSnapshot().trackedChanges,
513
+ scrollToRevision: (revisionId: string) => {
514
+ const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
515
+ (r) => r.revisionId === revisionId,
516
+ );
517
+ if (!revision || revision.anchor.kind === "detached") return;
518
+ activeRuntime.dispatch({
519
+ type: "selection.set",
520
+ selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
521
+ });
522
+ },
503
523
  }),
504
524
  [activeRuntime, currentUser.userId, documentId, runtime],
505
525
  );