@beyondwork/docx-react-component 1.0.41 → 1.0.42
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 +13 -1
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +568 -1
- package/src/index.ts +118 -1
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
|
@@ -32,11 +32,14 @@ import type {
|
|
|
32
32
|
InteractionGuardSnapshot,
|
|
33
33
|
InsertImageOptions,
|
|
34
34
|
InsertTableOptions,
|
|
35
|
+
MetadataPersistenceMode,
|
|
35
36
|
PageLayoutSnapshot,
|
|
36
37
|
PersistedEditorSnapshot,
|
|
38
|
+
ResolveMetadataConflictInput,
|
|
37
39
|
ReviewQueueItem,
|
|
38
40
|
ReviewQueueSnapshot,
|
|
39
41
|
RuntimeRenderSnapshot,
|
|
42
|
+
ScopeMetadataPersistence,
|
|
40
43
|
SectionBreakType,
|
|
41
44
|
SectionLayoutPatch,
|
|
42
45
|
SectionPageNumberingPatch,
|
|
@@ -71,6 +74,8 @@ import type {
|
|
|
71
74
|
WorkspaceMode,
|
|
72
75
|
ZoomLevel,
|
|
73
76
|
} from "../api/public-types";
|
|
77
|
+
import { MetadataResolverMissingError } from "../api/public-types";
|
|
78
|
+
import type { ScopeMetadataResolver } from "../api/scope-metadata-resolver-types.ts";
|
|
74
79
|
import {
|
|
75
80
|
editorSessionStateFromPersistedSnapshot,
|
|
76
81
|
persistedSnapshotFromEditorSessionState,
|
|
@@ -254,10 +259,219 @@ type SelectionToolbarDismissReason =
|
|
|
254
259
|
| "comment-action"
|
|
255
260
|
| "escape";
|
|
256
261
|
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// P17 module-level helpers — metadata persistence
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
function conflictKey(input: {
|
|
267
|
+
scopeId?: string;
|
|
268
|
+
entryId?: string;
|
|
269
|
+
fieldKey?: string;
|
|
270
|
+
}): string {
|
|
271
|
+
return `${input.scopeId ?? ""}|${input.entryId ?? ""}|${input.fieldKey ?? ""}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveEffective(input: {
|
|
275
|
+
overlay?: MetadataPersistenceMode;
|
|
276
|
+
scope?: ScopeMetadataPersistence;
|
|
277
|
+
field?: ScopeMetadataPersistence;
|
|
278
|
+
}): "internal" | "external" {
|
|
279
|
+
if (input.field === "internal" || input.field === "external") return input.field;
|
|
280
|
+
if (input.scope === "internal" || input.scope === "external") return input.scope;
|
|
281
|
+
return input.overlay ?? "internal";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A pending conflict record held in `metadataConflictPendingRef` while
|
|
286
|
+
* the host decides how to resolve. Keyed by `conflictKey(...)`.
|
|
287
|
+
*/
|
|
288
|
+
interface PendingConflict {
|
|
289
|
+
scopeId?: string;
|
|
290
|
+
entryId?: string;
|
|
291
|
+
fieldKey?: string;
|
|
292
|
+
embedded: { value?: Record<string, unknown>; version?: number } | null;
|
|
293
|
+
external: { value?: Record<string, unknown>; version?: number } | null;
|
|
294
|
+
defaultPolicy: "prefer-latest";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Emit a single `metadata_conflict_detected` event and register the pending conflict. */
|
|
298
|
+
function registerAndEmitConflict(args: {
|
|
299
|
+
onEvent: ((event: WordReviewEditorEvent) => void) | undefined;
|
|
300
|
+
documentId: string;
|
|
301
|
+
conflict: PendingConflict;
|
|
302
|
+
pendingConflicts: Map<string, PendingConflict>;
|
|
303
|
+
}): void {
|
|
304
|
+
const key = conflictKey(args.conflict);
|
|
305
|
+
// Guard: do not emit duplicate events for the same key in a single pass.
|
|
306
|
+
if (args.pendingConflicts.has(key)) return;
|
|
307
|
+
args.pendingConflicts.set(key, args.conflict);
|
|
308
|
+
args.onEvent?.({
|
|
309
|
+
type: "metadata_conflict_detected",
|
|
310
|
+
documentId: args.documentId,
|
|
311
|
+
scopeId: args.conflict.scopeId,
|
|
312
|
+
entryId: args.conflict.entryId,
|
|
313
|
+
fieldKey: args.conflict.fieldKey,
|
|
314
|
+
embedded: args.conflict.embedded,
|
|
315
|
+
external: args.conflict.external,
|
|
316
|
+
defaultPolicy: args.conflict.defaultPolicy,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runConvertScopesToInternal(args: {
|
|
321
|
+
runtime: WordReviewEditorRuntime;
|
|
322
|
+
scopeIds: string[];
|
|
323
|
+
resolver: ScopeMetadataResolver | null;
|
|
324
|
+
/** When provided, version mismatches emit `metadata_conflict_detected`. */
|
|
325
|
+
documentId?: string;
|
|
326
|
+
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
327
|
+
pendingConflicts?: Map<string, PendingConflict>;
|
|
328
|
+
}): Promise<void> {
|
|
329
|
+
if (!args.resolver) throw new MetadataResolverMissingError();
|
|
330
|
+
const snapshot = args.runtime.getWorkflowMetadataSnapshot();
|
|
331
|
+
const overlay = args.runtime.getWorkflowOverlay();
|
|
332
|
+
|
|
333
|
+
const nextEntries = await Promise.all(
|
|
334
|
+
snapshot.entries.map(async (entry) => {
|
|
335
|
+
if (!entry.scopeId || !args.scopeIds.includes(entry.scopeId)) return entry;
|
|
336
|
+
const scope = overlay?.scopes.find((s) => s.scopeId === entry.scopeId);
|
|
337
|
+
const effective = resolveEffective({
|
|
338
|
+
overlay: overlay?.metadataPersistence,
|
|
339
|
+
scope: scope?.metadataPersistence,
|
|
340
|
+
field: entry.metadataPersistence,
|
|
341
|
+
});
|
|
342
|
+
if (effective !== "external" || !entry.storageRef) return entry;
|
|
343
|
+
const resolved = await args.resolver!.resolve(entry.storageRef);
|
|
344
|
+
if (!resolved) return entry;
|
|
345
|
+
|
|
346
|
+
// Conflict detection: compare embedded metadataVersion vs resolver version.
|
|
347
|
+
if (
|
|
348
|
+
args.pendingConflicts &&
|
|
349
|
+
args.documentId !== undefined &&
|
|
350
|
+
entry.metadataVersion !== undefined &&
|
|
351
|
+
resolved.version !== undefined &&
|
|
352
|
+
entry.metadataVersion !== resolved.version
|
|
353
|
+
) {
|
|
354
|
+
registerAndEmitConflict({
|
|
355
|
+
onEvent: args.onEvent,
|
|
356
|
+
documentId: args.documentId,
|
|
357
|
+
pendingConflicts: args.pendingConflicts,
|
|
358
|
+
conflict: {
|
|
359
|
+
scopeId: entry.scopeId,
|
|
360
|
+
entryId: entry.entryId,
|
|
361
|
+
// External entries have no inline value; embedded side carries version only.
|
|
362
|
+
embedded: { version: entry.metadataVersion },
|
|
363
|
+
external: { value: resolved.value, version: resolved.version },
|
|
364
|
+
defaultPolicy: "prefer-latest",
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Default behavior: always apply the resolved value (host can override via resolveMetadataConflict).
|
|
370
|
+
return {
|
|
371
|
+
...entry,
|
|
372
|
+
value: resolved.value,
|
|
373
|
+
metadataPersistence: "internal" as const,
|
|
374
|
+
storageRef: undefined,
|
|
375
|
+
metadataVersion: resolved.version ?? entry.metadataVersion,
|
|
376
|
+
};
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
args.runtime.setWorkflowMetadataEntries(nextEntries);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function runConvertScopesToExternal(args: {
|
|
384
|
+
runtime: WordReviewEditorRuntime;
|
|
385
|
+
scopeIds: string[];
|
|
386
|
+
resolver: ScopeMetadataResolver | null;
|
|
387
|
+
/** When provided, version-conflict publish errors emit `metadata_conflict_detected`. */
|
|
388
|
+
documentId?: string;
|
|
389
|
+
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
390
|
+
pendingConflicts?: Map<string, PendingConflict>;
|
|
391
|
+
}): Promise<void> {
|
|
392
|
+
if (!args.resolver) throw new MetadataResolverMissingError();
|
|
393
|
+
const snapshot = args.runtime.getWorkflowMetadataSnapshot();
|
|
394
|
+
const overlay = args.runtime.getWorkflowOverlay();
|
|
395
|
+
|
|
396
|
+
const nextEntries = await Promise.all(
|
|
397
|
+
snapshot.entries.map(async (entry) => {
|
|
398
|
+
if (!entry.scopeId || !args.scopeIds.includes(entry.scopeId)) return entry;
|
|
399
|
+
const scope = overlay?.scopes.find((s) => s.scopeId === entry.scopeId);
|
|
400
|
+
const effective = resolveEffective({
|
|
401
|
+
overlay: overlay?.metadataPersistence,
|
|
402
|
+
scope: scope?.metadataPersistence,
|
|
403
|
+
field: entry.metadataPersistence,
|
|
404
|
+
});
|
|
405
|
+
if (effective === "external") return entry;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const { ref, version } = await args.resolver!.publish({
|
|
409
|
+
scopeId: entry.scopeId,
|
|
410
|
+
metadataId: entry.metadataId,
|
|
411
|
+
entryId: entry.entryId,
|
|
412
|
+
value: entry.value ?? {},
|
|
413
|
+
expectedVersion: entry.metadataVersion,
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
...entry,
|
|
417
|
+
value: undefined,
|
|
418
|
+
metadataPersistence: "external" as const,
|
|
419
|
+
storageRef: ref,
|
|
420
|
+
metadataVersion: version,
|
|
421
|
+
};
|
|
422
|
+
} catch (err: unknown) {
|
|
423
|
+
// Duck-type version-conflict errors (HarnessVersionConflictError or compatible shapes).
|
|
424
|
+
if (
|
|
425
|
+
args.pendingConflicts &&
|
|
426
|
+
args.documentId !== undefined &&
|
|
427
|
+
err instanceof Error &&
|
|
428
|
+
(err.name === "HarnessVersionConflictError" ||
|
|
429
|
+
("ref" in err && "expected" in err && "actual" in err))
|
|
430
|
+
) {
|
|
431
|
+
const conflictErr = err as Error & { ref?: string; expected?: number; actual?: number };
|
|
432
|
+
registerAndEmitConflict({
|
|
433
|
+
onEvent: args.onEvent,
|
|
434
|
+
documentId: args.documentId,
|
|
435
|
+
pendingConflicts: args.pendingConflicts,
|
|
436
|
+
conflict: {
|
|
437
|
+
scopeId: entry.scopeId,
|
|
438
|
+
entryId: entry.entryId,
|
|
439
|
+
// Embedded side: the entry's current inline value and metadataVersion.
|
|
440
|
+
embedded: { value: entry.value, version: entry.metadataVersion },
|
|
441
|
+
// External side: only the rowstore's actual version (no value available from error).
|
|
442
|
+
external: { version: conflictErr.actual },
|
|
443
|
+
defaultPolicy: "prefer-latest",
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
// Skip this entry — do not publish; leave it unchanged.
|
|
447
|
+
return entry;
|
|
448
|
+
}
|
|
449
|
+
// Non-conflict errors propagate normally.
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
args.runtime.setWorkflowMetadataEntries(nextEntries);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
257
460
|
export function __createWordReviewEditorRefBridge(
|
|
258
461
|
runtime: WordReviewEditorRuntime,
|
|
259
462
|
mountedSurface?: TwProseMirrorSurfaceRef | null,
|
|
463
|
+
options?: {
|
|
464
|
+
documentId?: string;
|
|
465
|
+
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
466
|
+
resolverRef?: { current: ScopeMetadataResolver | null };
|
|
467
|
+
conflictResolutionsRef?: {
|
|
468
|
+
current: Map<string, { choice: string; mergedValue?: Record<string, unknown> }>;
|
|
469
|
+
};
|
|
470
|
+
},
|
|
260
471
|
): WordReviewEditorRef {
|
|
472
|
+
// Pending-conflict queue for this bridge instance. Keyed by conflictKey(...).
|
|
473
|
+
const pendingConflicts = new Map<string, PendingConflict>();
|
|
474
|
+
|
|
261
475
|
return {
|
|
262
476
|
focus: () => runtime.focus(),
|
|
263
477
|
blur: () => runtime.blur(),
|
|
@@ -277,6 +491,10 @@ export function __createWordReviewEditorRefBridge(
|
|
|
277
491
|
rejectChange: (changeId) => runtime.rejectChange(changeId),
|
|
278
492
|
acceptAllChanges: () => runtime.acceptAllChanges(),
|
|
279
493
|
rejectAllChanges: () => runtime.rejectAllChanges(),
|
|
494
|
+
acceptSuggestionGroup: (groupId) =>
|
|
495
|
+
applySuggestionGroupAction(runtime, groupId, "accept"),
|
|
496
|
+
rejectSuggestionGroup: (groupId) =>
|
|
497
|
+
applySuggestionGroupAction(runtime, groupId, "reject"),
|
|
280
498
|
exportDocx: (options) => runtime.exportDocx(options),
|
|
281
499
|
getSessionState: () => runtime.getSessionState(),
|
|
282
500
|
getSnapshot: () => runtime.getPersistedSnapshot(),
|
|
@@ -575,6 +793,151 @@ export function __createWordReviewEditorRefBridge(
|
|
|
575
793
|
getWorkflowMetadataSnapshot: () => {
|
|
576
794
|
return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
|
|
577
795
|
},
|
|
796
|
+
// P17 — metadata persistence toggle + convert methods.
|
|
797
|
+
setMetadataPersistenceMode: (mode) => {
|
|
798
|
+
if (mode === "external" && !(options?.resolverRef?.current ?? null)) {
|
|
799
|
+
throw new MetadataResolverMissingError();
|
|
800
|
+
}
|
|
801
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
802
|
+
if (!overlay) return;
|
|
803
|
+
runtime.setWorkflowOverlay({ ...overlay, metadataPersistence: mode });
|
|
804
|
+
options?.onEvent?.({
|
|
805
|
+
type: "metadata_persistence_mode_changed",
|
|
806
|
+
documentId: options?.documentId ?? "",
|
|
807
|
+
mode,
|
|
808
|
+
});
|
|
809
|
+
},
|
|
810
|
+
getMetadataPersistenceMode: () => {
|
|
811
|
+
return runtime.getWorkflowOverlay()?.metadataPersistence ?? "internal";
|
|
812
|
+
},
|
|
813
|
+
setScopeMetadataPersistence: (scopeId, persistence) => {
|
|
814
|
+
if (persistence === "external" && !(options?.resolverRef?.current ?? null)) {
|
|
815
|
+
throw new MetadataResolverMissingError();
|
|
816
|
+
}
|
|
817
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
818
|
+
if (!overlay) return;
|
|
819
|
+
const nextOverlay = {
|
|
820
|
+
...overlay,
|
|
821
|
+
scopes: overlay.scopes.map((s) => {
|
|
822
|
+
if (s.scopeId !== scopeId) return s;
|
|
823
|
+
if (persistence === "inherit") {
|
|
824
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
825
|
+
return rest as typeof s;
|
|
826
|
+
}
|
|
827
|
+
return { ...s, metadataPersistence: persistence };
|
|
828
|
+
}),
|
|
829
|
+
};
|
|
830
|
+
runtime.setWorkflowOverlay(nextOverlay);
|
|
831
|
+
options?.onEvent?.({
|
|
832
|
+
type: "scope_metadata_persistence_changed",
|
|
833
|
+
documentId: options?.documentId ?? "",
|
|
834
|
+
scopeId,
|
|
835
|
+
persistence,
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
getScopeMetadataPersistence: (scopeId) => {
|
|
839
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
840
|
+
return overlay?.scopes.find((s) => s.scopeId === scopeId)?.metadataPersistence ?? "inherit";
|
|
841
|
+
},
|
|
842
|
+
setAllScopesMetadataPersistence: (persistence) => {
|
|
843
|
+
if (persistence === "external" && !(options?.resolverRef?.current ?? null)) {
|
|
844
|
+
throw new MetadataResolverMissingError();
|
|
845
|
+
}
|
|
846
|
+
const overlay = runtime.getWorkflowOverlay();
|
|
847
|
+
if (!overlay) return;
|
|
848
|
+
const nextOverlay = {
|
|
849
|
+
...overlay,
|
|
850
|
+
scopes: overlay.scopes.map((s) => {
|
|
851
|
+
if (persistence === "inherit") {
|
|
852
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
853
|
+
return rest as typeof s;
|
|
854
|
+
}
|
|
855
|
+
return { ...s, metadataPersistence: persistence };
|
|
856
|
+
}),
|
|
857
|
+
};
|
|
858
|
+
runtime.setWorkflowOverlay(nextOverlay);
|
|
859
|
+
options?.onEvent?.({
|
|
860
|
+
type: "scope_metadata_persistence_changed",
|
|
861
|
+
documentId: options?.documentId ?? "",
|
|
862
|
+
scopeId: "*",
|
|
863
|
+
persistence,
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
setScopeMetadataResolver: (resolver) => {
|
|
867
|
+
if (options?.resolverRef) {
|
|
868
|
+
options.resolverRef.current = resolver;
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
resolveMetadataConflict: (input: ResolveMetadataConflictInput) => {
|
|
872
|
+
// Legacy: keep conflictResolutionsRef updated so Task 8 tests pass.
|
|
873
|
+
if (options?.conflictResolutionsRef) {
|
|
874
|
+
options.conflictResolutionsRef.current.set(conflictKey(input), {
|
|
875
|
+
choice: input.choice,
|
|
876
|
+
mergedValue: input.mergedValue,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// New: apply the chosen value to the metadata snapshot.
|
|
881
|
+
const pending = pendingConflicts.get(conflictKey(input));
|
|
882
|
+
if (!pending) return; // No pending conflict — idempotent no-op.
|
|
883
|
+
|
|
884
|
+
let finalValue: Record<string, unknown> | undefined;
|
|
885
|
+
if (input.choice === "embedded") {
|
|
886
|
+
finalValue = pending.embedded?.value;
|
|
887
|
+
} else if (input.choice === "external") {
|
|
888
|
+
finalValue = pending.external?.value;
|
|
889
|
+
} else if (input.choice === "merge") {
|
|
890
|
+
finalValue = input.mergedValue;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (finalValue === undefined) {
|
|
894
|
+
// Nothing to write (e.g., embedded side has no inline value).
|
|
895
|
+
pendingConflicts.delete(conflictKey(input));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const winnerVersion =
|
|
900
|
+
input.choice === "external"
|
|
901
|
+
? pending.external?.version ?? undefined
|
|
902
|
+
: input.choice === "embedded"
|
|
903
|
+
? pending.embedded?.version ?? undefined
|
|
904
|
+
: undefined;
|
|
905
|
+
|
|
906
|
+
const snapshot = runtime.getWorkflowMetadataSnapshot();
|
|
907
|
+
const nextEntries = snapshot.entries.map((entry) => {
|
|
908
|
+
if (input.entryId && entry.entryId !== input.entryId) return entry;
|
|
909
|
+
if (input.scopeId && entry.scopeId !== input.scopeId) return entry;
|
|
910
|
+
return {
|
|
911
|
+
...entry,
|
|
912
|
+
value: finalValue,
|
|
913
|
+
metadataPersistence: "internal" as const,
|
|
914
|
+
storageRef: undefined,
|
|
915
|
+
metadataVersion: winnerVersion ?? entry.metadataVersion,
|
|
916
|
+
};
|
|
917
|
+
});
|
|
918
|
+
runtime.setWorkflowMetadataEntries(nextEntries);
|
|
919
|
+
pendingConflicts.delete(conflictKey(input));
|
|
920
|
+
},
|
|
921
|
+
convertScopesToInternal: async (scopeIds) => {
|
|
922
|
+
await runConvertScopesToInternal({
|
|
923
|
+
runtime,
|
|
924
|
+
scopeIds,
|
|
925
|
+
resolver: options?.resolverRef?.current ?? null,
|
|
926
|
+
documentId: options?.documentId,
|
|
927
|
+
onEvent: options?.onEvent,
|
|
928
|
+
pendingConflicts,
|
|
929
|
+
});
|
|
930
|
+
},
|
|
931
|
+
convertScopesToExternal: async (scopeIds) => {
|
|
932
|
+
await runConvertScopesToExternal({
|
|
933
|
+
runtime,
|
|
934
|
+
scopeIds,
|
|
935
|
+
resolver: options?.resolverRef?.current ?? null,
|
|
936
|
+
documentId: options?.documentId,
|
|
937
|
+
onEvent: options?.onEvent,
|
|
938
|
+
pendingConflicts,
|
|
939
|
+
});
|
|
940
|
+
},
|
|
578
941
|
setHostAnnotationOverlay: (overlay) => {
|
|
579
942
|
runtime.setHostAnnotationOverlay(clonePublicValue(overlay));
|
|
580
943
|
},
|
|
@@ -687,6 +1050,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
687
1050
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
688
1051
|
const lastSelectionToolbarKeyRef = useRef<string | null>(null);
|
|
689
1052
|
const lastAnnouncedErrorIdRef = useRef<string | null>(null);
|
|
1053
|
+
const scopeMetadataResolverRef = useRef<ScopeMetadataResolver | null>(null);
|
|
1054
|
+
const metadataConflictResolutionsRef = useRef(
|
|
1055
|
+
new Map<string, { choice: string; mergedValue?: Record<string, unknown> }>(),
|
|
1056
|
+
);
|
|
1057
|
+
const metadataConflictPendingRef = useRef(new Map<string, PendingConflict>());
|
|
690
1058
|
const {
|
|
691
1059
|
runtime,
|
|
692
1060
|
loadError,
|
|
@@ -1094,6 +1462,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1094
1462
|
rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
|
|
1095
1463
|
acceptAllChanges: () => activeRuntime.acceptAllChanges(),
|
|
1096
1464
|
rejectAllChanges: () => activeRuntime.rejectAllChanges(),
|
|
1465
|
+
acceptSuggestionGroup: (groupId) =>
|
|
1466
|
+
applySuggestionGroupAction(activeRuntime, groupId, "accept"),
|
|
1467
|
+
rejectSuggestionGroup: (groupId) =>
|
|
1468
|
+
applySuggestionGroupAction(activeRuntime, groupId, "reject"),
|
|
1097
1469
|
exportDocx: (options) =>
|
|
1098
1470
|
runtime
|
|
1099
1471
|
? persistAndExportFromBoundary({
|
|
@@ -1446,6 +1818,149 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1446
1818
|
getWorkflowMetadataSnapshot: () => {
|
|
1447
1819
|
return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
|
|
1448
1820
|
},
|
|
1821
|
+
// P17 — metadata persistence toggle + convert methods.
|
|
1822
|
+
setMetadataPersistenceMode: (mode) => {
|
|
1823
|
+
if (mode === "external" && scopeMetadataResolverRef.current === null) {
|
|
1824
|
+
throw new MetadataResolverMissingError();
|
|
1825
|
+
}
|
|
1826
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1827
|
+
if (!overlay) return;
|
|
1828
|
+
activeRuntime.setWorkflowOverlay({ ...overlay, metadataPersistence: mode });
|
|
1829
|
+
onEventRef.current?.({
|
|
1830
|
+
type: "metadata_persistence_mode_changed",
|
|
1831
|
+
documentId,
|
|
1832
|
+
mode,
|
|
1833
|
+
});
|
|
1834
|
+
},
|
|
1835
|
+
getMetadataPersistenceMode: () => {
|
|
1836
|
+
return activeRuntime.getWorkflowOverlay()?.metadataPersistence ?? "internal";
|
|
1837
|
+
},
|
|
1838
|
+
setScopeMetadataPersistence: (scopeId, persistence) => {
|
|
1839
|
+
if (persistence === "external" && scopeMetadataResolverRef.current === null) {
|
|
1840
|
+
throw new MetadataResolverMissingError();
|
|
1841
|
+
}
|
|
1842
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1843
|
+
if (!overlay) return;
|
|
1844
|
+
const nextOverlay = {
|
|
1845
|
+
...overlay,
|
|
1846
|
+
scopes: overlay.scopes.map((s) => {
|
|
1847
|
+
if (s.scopeId !== scopeId) return s;
|
|
1848
|
+
if (persistence === "inherit") {
|
|
1849
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
1850
|
+
return rest as typeof s;
|
|
1851
|
+
}
|
|
1852
|
+
return { ...s, metadataPersistence: persistence };
|
|
1853
|
+
}),
|
|
1854
|
+
};
|
|
1855
|
+
activeRuntime.setWorkflowOverlay(nextOverlay);
|
|
1856
|
+
onEventRef.current?.({
|
|
1857
|
+
type: "scope_metadata_persistence_changed",
|
|
1858
|
+
documentId,
|
|
1859
|
+
scopeId,
|
|
1860
|
+
persistence,
|
|
1861
|
+
});
|
|
1862
|
+
},
|
|
1863
|
+
getScopeMetadataPersistence: (scopeId) => {
|
|
1864
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1865
|
+
return (
|
|
1866
|
+
overlay?.scopes.find((s) => s.scopeId === scopeId)?.metadataPersistence ?? "inherit"
|
|
1867
|
+
);
|
|
1868
|
+
},
|
|
1869
|
+
setAllScopesMetadataPersistence: (persistence) => {
|
|
1870
|
+
if (persistence === "external" && scopeMetadataResolverRef.current === null) {
|
|
1871
|
+
throw new MetadataResolverMissingError();
|
|
1872
|
+
}
|
|
1873
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1874
|
+
if (!overlay) return;
|
|
1875
|
+
const nextOverlay = {
|
|
1876
|
+
...overlay,
|
|
1877
|
+
scopes: overlay.scopes.map((s) => {
|
|
1878
|
+
if (persistence === "inherit") {
|
|
1879
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
1880
|
+
return rest as typeof s;
|
|
1881
|
+
}
|
|
1882
|
+
return { ...s, metadataPersistence: persistence };
|
|
1883
|
+
}),
|
|
1884
|
+
};
|
|
1885
|
+
activeRuntime.setWorkflowOverlay(nextOverlay);
|
|
1886
|
+
onEventRef.current?.({
|
|
1887
|
+
type: "scope_metadata_persistence_changed",
|
|
1888
|
+
documentId,
|
|
1889
|
+
scopeId: "*",
|
|
1890
|
+
persistence,
|
|
1891
|
+
});
|
|
1892
|
+
},
|
|
1893
|
+
setScopeMetadataResolver: (resolver) => {
|
|
1894
|
+
scopeMetadataResolverRef.current = resolver;
|
|
1895
|
+
},
|
|
1896
|
+
resolveMetadataConflict: (input) => {
|
|
1897
|
+
// Legacy: keep metadataConflictResolutionsRef updated so Task 8 tests pass.
|
|
1898
|
+
metadataConflictResolutionsRef.current.set(conflictKey(input), {
|
|
1899
|
+
choice: input.choice,
|
|
1900
|
+
mergedValue: input.mergedValue,
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
// New: apply the chosen value to the metadata snapshot.
|
|
1904
|
+
const pending = metadataConflictPendingRef.current.get(conflictKey(input));
|
|
1905
|
+
if (!pending) return; // No pending conflict — idempotent no-op.
|
|
1906
|
+
|
|
1907
|
+
let finalValue: Record<string, unknown> | undefined;
|
|
1908
|
+
if (input.choice === "embedded") {
|
|
1909
|
+
finalValue = pending.embedded?.value;
|
|
1910
|
+
} else if (input.choice === "external") {
|
|
1911
|
+
finalValue = pending.external?.value;
|
|
1912
|
+
} else if (input.choice === "merge") {
|
|
1913
|
+
finalValue = input.mergedValue;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (finalValue === undefined) {
|
|
1917
|
+
// Nothing to write (e.g., embedded side has no inline value).
|
|
1918
|
+
metadataConflictPendingRef.current.delete(conflictKey(input));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
const winnerVersion =
|
|
1923
|
+
input.choice === "external"
|
|
1924
|
+
? pending.external?.version ?? undefined
|
|
1925
|
+
: input.choice === "embedded"
|
|
1926
|
+
? pending.embedded?.version ?? undefined
|
|
1927
|
+
: undefined;
|
|
1928
|
+
|
|
1929
|
+
const snapshot = activeRuntime.getWorkflowMetadataSnapshot();
|
|
1930
|
+
const nextEntries = snapshot.entries.map((entry) => {
|
|
1931
|
+
if (input.entryId && entry.entryId !== input.entryId) return entry;
|
|
1932
|
+
if (input.scopeId && entry.scopeId !== input.scopeId) return entry;
|
|
1933
|
+
return {
|
|
1934
|
+
...entry,
|
|
1935
|
+
value: finalValue,
|
|
1936
|
+
metadataPersistence: "internal" as const,
|
|
1937
|
+
storageRef: undefined,
|
|
1938
|
+
metadataVersion: winnerVersion ?? entry.metadataVersion,
|
|
1939
|
+
};
|
|
1940
|
+
});
|
|
1941
|
+
activeRuntime.setWorkflowMetadataEntries(nextEntries);
|
|
1942
|
+
metadataConflictPendingRef.current.delete(conflictKey(input));
|
|
1943
|
+
},
|
|
1944
|
+
convertScopesToInternal: async (scopeIds) => {
|
|
1945
|
+
await runConvertScopesToInternal({
|
|
1946
|
+
runtime: activeRuntime,
|
|
1947
|
+
scopeIds,
|
|
1948
|
+
resolver: scopeMetadataResolverRef.current,
|
|
1949
|
+
documentId,
|
|
1950
|
+
onEvent: onEventRef.current,
|
|
1951
|
+
pendingConflicts: metadataConflictPendingRef.current,
|
|
1952
|
+
});
|
|
1953
|
+
},
|
|
1954
|
+
convertScopesToExternal: async (scopeIds) => {
|
|
1955
|
+
await runConvertScopesToExternal({
|
|
1956
|
+
runtime: activeRuntime,
|
|
1957
|
+
scopeIds,
|
|
1958
|
+
resolver: scopeMetadataResolverRef.current,
|
|
1959
|
+
documentId,
|
|
1960
|
+
onEvent: onEventRef.current,
|
|
1961
|
+
pendingConflicts: metadataConflictPendingRef.current,
|
|
1962
|
+
});
|
|
1963
|
+
},
|
|
1449
1964
|
setHostAnnotationOverlay: (overlay) => {
|
|
1450
1965
|
setHostAnnotationOverlayState(clonePublicValue(overlay));
|
|
1451
1966
|
},
|
|
@@ -2450,11 +2965,83 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2450
2965
|
action: payload.action,
|
|
2451
2966
|
});
|
|
2452
2967
|
}}
|
|
2968
|
+
onScopeAcceptSuggestionGroup={(payload) => {
|
|
2969
|
+
applySuggestionGroupAction(activeRuntime, payload.groupId, "accept");
|
|
2970
|
+
}}
|
|
2971
|
+
onScopeRejectSuggestionGroup={(payload) => {
|
|
2972
|
+
applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
|
|
2973
|
+
}}
|
|
2974
|
+
onScopeAskAgent={(payload) => {
|
|
2975
|
+
// Resolve the scope's anchor + story from the facet's card
|
|
2976
|
+
// model so the agent request carries the canonical range.
|
|
2977
|
+
const facet = activeRuntime.layout;
|
|
2978
|
+
const models =
|
|
2979
|
+
facet && typeof facet.getAllScopeCardModels === "function"
|
|
2980
|
+
? facet.getAllScopeCardModels()
|
|
2981
|
+
: [];
|
|
2982
|
+
const model = models.find((entry) => entry.scopeId === payload.scopeId);
|
|
2983
|
+
if (!model) return;
|
|
2984
|
+
const scopeSnapshot = activeRuntime.getWorkflowScopeSnapshot();
|
|
2985
|
+
const scopes = scopeSnapshot?.scopes ?? [];
|
|
2986
|
+
const scope = scopes.find((entry) => entry.scopeId === payload.scopeId);
|
|
2987
|
+
if (!scope) return;
|
|
2988
|
+
const anchor = scope.anchor;
|
|
2989
|
+
if (!anchor) return;
|
|
2990
|
+
const requestId =
|
|
2991
|
+
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
2992
|
+
? `req-${crypto.randomUUID()}`
|
|
2993
|
+
: `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2994
|
+
const eventPayload: Extract<
|
|
2995
|
+
WordReviewEditorEvent,
|
|
2996
|
+
{ type: "agent-on-selection-requested" }
|
|
2997
|
+
> = {
|
|
2998
|
+
type: "agent-on-selection-requested",
|
|
2999
|
+
documentId,
|
|
3000
|
+
requestId,
|
|
3001
|
+
scopeId: payload.scopeId,
|
|
3002
|
+
anchor,
|
|
3003
|
+
selectionText: model.label ?? "",
|
|
3004
|
+
...(scope.storyTarget ? { storyTarget: scope.storyTarget } : {}),
|
|
3005
|
+
};
|
|
3006
|
+
onEventRef.current?.(eventPayload);
|
|
3007
|
+
}}
|
|
2453
3008
|
/>
|
|
2454
3009
|
);
|
|
2455
3010
|
},
|
|
2456
3011
|
);
|
|
2457
3012
|
|
|
3013
|
+
/**
|
|
3014
|
+
* R3 — best-effort suggestion-group accept/reject fan-out. Resolves
|
|
3015
|
+
* the group's suggestions from the current snapshot, then fans out
|
|
3016
|
+
* `acceptChange` / `rejectChange` across every changeId in each
|
|
3017
|
+
* group member. P2 batches these in rapid succession; the runtime
|
|
3018
|
+
* commit boundary collapses them into a single logical transaction.
|
|
3019
|
+
* A future phase adds true atomicity at the runtime level.
|
|
3020
|
+
*/
|
|
3021
|
+
function applySuggestionGroupAction(
|
|
3022
|
+
runtime: WordReviewEditorRuntime,
|
|
3023
|
+
groupId: string,
|
|
3024
|
+
action: "accept" | "reject",
|
|
3025
|
+
): void {
|
|
3026
|
+
const snapshot = runtime.getSuggestionsSnapshot();
|
|
3027
|
+
const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
|
|
3028
|
+
if (!group) return;
|
|
3029
|
+
const byId = new Map(
|
|
3030
|
+
snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
|
|
3031
|
+
);
|
|
3032
|
+
for (const suggestionId of group.suggestionIds) {
|
|
3033
|
+
const suggestion = byId.get(suggestionId);
|
|
3034
|
+
if (!suggestion) continue;
|
|
3035
|
+
for (const changeId of suggestion.changeIds) {
|
|
3036
|
+
if (action === "accept") {
|
|
3037
|
+
runtime.acceptChange(changeId);
|
|
3038
|
+
} else {
|
|
3039
|
+
runtime.rejectChange(changeId);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2458
3045
|
function applyRuntimeFormattingOperation(
|
|
2459
3046
|
runtime: WordReviewEditorRuntime,
|
|
2460
3047
|
operation:
|
|
@@ -868,6 +868,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
868
868
|
Promise.reject(createLoadingBoundaryError(input.snapshot.documentId, "export")),
|
|
869
869
|
setWorkflowOverlay: () => undefined,
|
|
870
870
|
clearWorkflowOverlay: () => undefined,
|
|
871
|
+
getWorkflowOverlay: () => null,
|
|
871
872
|
getWorkflowScopeSnapshot: () => null,
|
|
872
873
|
getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
|
|
873
874
|
getWorkflowMarkupSnapshot: () => ({
|
|
@@ -105,6 +105,17 @@ export interface EditorShellViewProps {
|
|
|
105
105
|
issueId: string;
|
|
106
106
|
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
107
107
|
}) => void;
|
|
108
|
+
/** R3 — forwarded from workspace to WordReviewEditor. */
|
|
109
|
+
onScopeAcceptSuggestionGroup?: (payload: {
|
|
110
|
+
scopeId: string;
|
|
111
|
+
groupId: string;
|
|
112
|
+
}) => void;
|
|
113
|
+
onScopeRejectSuggestionGroup?: (payload: {
|
|
114
|
+
scopeId: string;
|
|
115
|
+
groupId: string;
|
|
116
|
+
}) => void;
|
|
117
|
+
/** K2 — forwarded from workspace to WordReviewEditor. */
|
|
118
|
+
onScopeAskAgent?: (payload: { scopeId: string }) => void;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
export function EditorShellView(props: EditorShellViewProps) {
|