@beyondwork/docx-react-component 1.0.41 → 1.0.43
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 +38 -37
- 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/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- 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 +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- 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/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -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 +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -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 +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- 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/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- 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/collab-top-nav-container.tsx +281 -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 +106 -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/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- 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 +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- 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 +455 -118
|
@@ -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,167 @@ 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
|
+
},
|
|
941
|
+
// Schema 1.2 — EditorStateChannel delegation.
|
|
942
|
+
configureEditorStatePolicy: (policy) => {
|
|
943
|
+
runtime.configureEditorStatePolicy(policy);
|
|
944
|
+
},
|
|
945
|
+
registerEditorStateResolver: (resolver) => {
|
|
946
|
+
runtime.registerEditorStateResolver(resolver);
|
|
947
|
+
},
|
|
948
|
+
registerEditorStatePersister: (persister) => {
|
|
949
|
+
runtime.registerEditorStatePersister(persister);
|
|
950
|
+
},
|
|
951
|
+
getEditorStateKey: (namespace) => {
|
|
952
|
+
return runtime.getEditorStateKey(namespace);
|
|
953
|
+
},
|
|
954
|
+
retryPendingPersist: async (namespace) => {
|
|
955
|
+
await runtime.retryPendingPersist(namespace);
|
|
956
|
+
},
|
|
578
957
|
setHostAnnotationOverlay: (overlay) => {
|
|
579
958
|
runtime.setHostAnnotationOverlay(clonePublicValue(overlay));
|
|
580
959
|
},
|
|
@@ -687,6 +1066,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
687
1066
|
const shellRef = useRef<HTMLDivElement | null>(null);
|
|
688
1067
|
const lastSelectionToolbarKeyRef = useRef<string | null>(null);
|
|
689
1068
|
const lastAnnouncedErrorIdRef = useRef<string | null>(null);
|
|
1069
|
+
const scopeMetadataResolverRef = useRef<ScopeMetadataResolver | null>(null);
|
|
1070
|
+
const metadataConflictResolutionsRef = useRef(
|
|
1071
|
+
new Map<string, { choice: string; mergedValue?: Record<string, unknown> }>(),
|
|
1072
|
+
);
|
|
1073
|
+
const metadataConflictPendingRef = useRef(new Map<string, PendingConflict>());
|
|
690
1074
|
const {
|
|
691
1075
|
runtime,
|
|
692
1076
|
loadError,
|
|
@@ -1094,6 +1478,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1094
1478
|
rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
|
|
1095
1479
|
acceptAllChanges: () => activeRuntime.acceptAllChanges(),
|
|
1096
1480
|
rejectAllChanges: () => activeRuntime.rejectAllChanges(),
|
|
1481
|
+
acceptSuggestionGroup: (groupId) =>
|
|
1482
|
+
applySuggestionGroupAction(activeRuntime, groupId, "accept"),
|
|
1483
|
+
rejectSuggestionGroup: (groupId) =>
|
|
1484
|
+
applySuggestionGroupAction(activeRuntime, groupId, "reject"),
|
|
1097
1485
|
exportDocx: (options) =>
|
|
1098
1486
|
runtime
|
|
1099
1487
|
? persistAndExportFromBoundary({
|
|
@@ -1446,6 +1834,165 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1446
1834
|
getWorkflowMetadataSnapshot: () => {
|
|
1447
1835
|
return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
|
|
1448
1836
|
},
|
|
1837
|
+
// P17 — metadata persistence toggle + convert methods.
|
|
1838
|
+
setMetadataPersistenceMode: (mode) => {
|
|
1839
|
+
if (mode === "external" && scopeMetadataResolverRef.current === null) {
|
|
1840
|
+
throw new MetadataResolverMissingError();
|
|
1841
|
+
}
|
|
1842
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1843
|
+
if (!overlay) return;
|
|
1844
|
+
activeRuntime.setWorkflowOverlay({ ...overlay, metadataPersistence: mode });
|
|
1845
|
+
onEventRef.current?.({
|
|
1846
|
+
type: "metadata_persistence_mode_changed",
|
|
1847
|
+
documentId,
|
|
1848
|
+
mode,
|
|
1849
|
+
});
|
|
1850
|
+
},
|
|
1851
|
+
getMetadataPersistenceMode: () => {
|
|
1852
|
+
return activeRuntime.getWorkflowOverlay()?.metadataPersistence ?? "internal";
|
|
1853
|
+
},
|
|
1854
|
+
setScopeMetadataPersistence: (scopeId, persistence) => {
|
|
1855
|
+
if (persistence === "external" && scopeMetadataResolverRef.current === null) {
|
|
1856
|
+
throw new MetadataResolverMissingError();
|
|
1857
|
+
}
|
|
1858
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1859
|
+
if (!overlay) return;
|
|
1860
|
+
const nextOverlay = {
|
|
1861
|
+
...overlay,
|
|
1862
|
+
scopes: overlay.scopes.map((s) => {
|
|
1863
|
+
if (s.scopeId !== scopeId) return s;
|
|
1864
|
+
if (persistence === "inherit") {
|
|
1865
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
1866
|
+
return rest as typeof s;
|
|
1867
|
+
}
|
|
1868
|
+
return { ...s, metadataPersistence: persistence };
|
|
1869
|
+
}),
|
|
1870
|
+
};
|
|
1871
|
+
activeRuntime.setWorkflowOverlay(nextOverlay);
|
|
1872
|
+
onEventRef.current?.({
|
|
1873
|
+
type: "scope_metadata_persistence_changed",
|
|
1874
|
+
documentId,
|
|
1875
|
+
scopeId,
|
|
1876
|
+
persistence,
|
|
1877
|
+
});
|
|
1878
|
+
},
|
|
1879
|
+
getScopeMetadataPersistence: (scopeId) => {
|
|
1880
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1881
|
+
return (
|
|
1882
|
+
overlay?.scopes.find((s) => s.scopeId === scopeId)?.metadataPersistence ?? "inherit"
|
|
1883
|
+
);
|
|
1884
|
+
},
|
|
1885
|
+
setAllScopesMetadataPersistence: (persistence) => {
|
|
1886
|
+
if (persistence === "external" && scopeMetadataResolverRef.current === null) {
|
|
1887
|
+
throw new MetadataResolverMissingError();
|
|
1888
|
+
}
|
|
1889
|
+
const overlay = activeRuntime.getWorkflowOverlay();
|
|
1890
|
+
if (!overlay) return;
|
|
1891
|
+
const nextOverlay = {
|
|
1892
|
+
...overlay,
|
|
1893
|
+
scopes: overlay.scopes.map((s) => {
|
|
1894
|
+
if (persistence === "inherit") {
|
|
1895
|
+
const { metadataPersistence: _, ...rest } = s;
|
|
1896
|
+
return rest as typeof s;
|
|
1897
|
+
}
|
|
1898
|
+
return { ...s, metadataPersistence: persistence };
|
|
1899
|
+
}),
|
|
1900
|
+
};
|
|
1901
|
+
activeRuntime.setWorkflowOverlay(nextOverlay);
|
|
1902
|
+
onEventRef.current?.({
|
|
1903
|
+
type: "scope_metadata_persistence_changed",
|
|
1904
|
+
documentId,
|
|
1905
|
+
scopeId: "*",
|
|
1906
|
+
persistence,
|
|
1907
|
+
});
|
|
1908
|
+
},
|
|
1909
|
+
setScopeMetadataResolver: (resolver) => {
|
|
1910
|
+
scopeMetadataResolverRef.current = resolver;
|
|
1911
|
+
},
|
|
1912
|
+
resolveMetadataConflict: (input) => {
|
|
1913
|
+
// Legacy: keep metadataConflictResolutionsRef updated so Task 8 tests pass.
|
|
1914
|
+
metadataConflictResolutionsRef.current.set(conflictKey(input), {
|
|
1915
|
+
choice: input.choice,
|
|
1916
|
+
mergedValue: input.mergedValue,
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
// New: apply the chosen value to the metadata snapshot.
|
|
1920
|
+
const pending = metadataConflictPendingRef.current.get(conflictKey(input));
|
|
1921
|
+
if (!pending) return; // No pending conflict — idempotent no-op.
|
|
1922
|
+
|
|
1923
|
+
let finalValue: Record<string, unknown> | undefined;
|
|
1924
|
+
if (input.choice === "embedded") {
|
|
1925
|
+
finalValue = pending.embedded?.value;
|
|
1926
|
+
} else if (input.choice === "external") {
|
|
1927
|
+
finalValue = pending.external?.value;
|
|
1928
|
+
} else if (input.choice === "merge") {
|
|
1929
|
+
finalValue = input.mergedValue;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (finalValue === undefined) {
|
|
1933
|
+
// Nothing to write (e.g., embedded side has no inline value).
|
|
1934
|
+
metadataConflictPendingRef.current.delete(conflictKey(input));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const winnerVersion =
|
|
1939
|
+
input.choice === "external"
|
|
1940
|
+
? pending.external?.version ?? undefined
|
|
1941
|
+
: input.choice === "embedded"
|
|
1942
|
+
? pending.embedded?.version ?? undefined
|
|
1943
|
+
: undefined;
|
|
1944
|
+
|
|
1945
|
+
const snapshot = activeRuntime.getWorkflowMetadataSnapshot();
|
|
1946
|
+
const nextEntries = snapshot.entries.map((entry) => {
|
|
1947
|
+
if (input.entryId && entry.entryId !== input.entryId) return entry;
|
|
1948
|
+
if (input.scopeId && entry.scopeId !== input.scopeId) return entry;
|
|
1949
|
+
return {
|
|
1950
|
+
...entry,
|
|
1951
|
+
value: finalValue,
|
|
1952
|
+
metadataPersistence: "internal" as const,
|
|
1953
|
+
storageRef: undefined,
|
|
1954
|
+
metadataVersion: winnerVersion ?? entry.metadataVersion,
|
|
1955
|
+
};
|
|
1956
|
+
});
|
|
1957
|
+
activeRuntime.setWorkflowMetadataEntries(nextEntries);
|
|
1958
|
+
metadataConflictPendingRef.current.delete(conflictKey(input));
|
|
1959
|
+
},
|
|
1960
|
+
convertScopesToInternal: async (scopeIds) => {
|
|
1961
|
+
await runConvertScopesToInternal({
|
|
1962
|
+
runtime: activeRuntime,
|
|
1963
|
+
scopeIds,
|
|
1964
|
+
resolver: scopeMetadataResolverRef.current,
|
|
1965
|
+
documentId,
|
|
1966
|
+
onEvent: onEventRef.current,
|
|
1967
|
+
pendingConflicts: metadataConflictPendingRef.current,
|
|
1968
|
+
});
|
|
1969
|
+
},
|
|
1970
|
+
convertScopesToExternal: async (scopeIds) => {
|
|
1971
|
+
await runConvertScopesToExternal({
|
|
1972
|
+
runtime: activeRuntime,
|
|
1973
|
+
scopeIds,
|
|
1974
|
+
resolver: scopeMetadataResolverRef.current,
|
|
1975
|
+
documentId,
|
|
1976
|
+
onEvent: onEventRef.current,
|
|
1977
|
+
pendingConflicts: metadataConflictPendingRef.current,
|
|
1978
|
+
});
|
|
1979
|
+
},
|
|
1980
|
+
// Schema 1.2 — EditorStateChannel delegation.
|
|
1981
|
+
configureEditorStatePolicy: (policy) => {
|
|
1982
|
+
activeRuntime.configureEditorStatePolicy(policy);
|
|
1983
|
+
},
|
|
1984
|
+
registerEditorStateResolver: (resolver) => {
|
|
1985
|
+
activeRuntime.registerEditorStateResolver(resolver);
|
|
1986
|
+
},
|
|
1987
|
+
registerEditorStatePersister: (persister) => {
|
|
1988
|
+
activeRuntime.registerEditorStatePersister(persister);
|
|
1989
|
+
},
|
|
1990
|
+
getEditorStateKey: (namespace) => {
|
|
1991
|
+
return activeRuntime.getEditorStateKey(namespace);
|
|
1992
|
+
},
|
|
1993
|
+
retryPendingPersist: async (namespace) => {
|
|
1994
|
+
await activeRuntime.retryPendingPersist(namespace);
|
|
1995
|
+
},
|
|
1449
1996
|
setHostAnnotationOverlay: (overlay) => {
|
|
1450
1997
|
setHostAnnotationOverlayState(clonePublicValue(overlay));
|
|
1451
1998
|
},
|
|
@@ -2103,6 +2650,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2103
2650
|
code: "unsupported_surface",
|
|
2104
2651
|
message,
|
|
2105
2652
|
}]),
|
|
2653
|
+
onPasteApplied: (meta: {
|
|
2654
|
+
segmentCount: number;
|
|
2655
|
+
charCount: number;
|
|
2656
|
+
source: "paste" | "drop";
|
|
2657
|
+
}) => {
|
|
2658
|
+
onEventRef.current?.({
|
|
2659
|
+
type: "paste_applied",
|
|
2660
|
+
documentId: props.documentId,
|
|
2661
|
+
segmentCount: meta.segmentCount,
|
|
2662
|
+
charCount: meta.charCount,
|
|
2663
|
+
source: meta.source,
|
|
2664
|
+
});
|
|
2665
|
+
},
|
|
2106
2666
|
};
|
|
2107
2667
|
|
|
2108
2668
|
const reviewCallbacks = {
|
|
@@ -2248,14 +2808,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2248
2808
|
applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
|
|
2249
2809
|
onSetImageFrame: (mediaId, offsets) =>
|
|
2250
2810
|
applyRuntimeImageReposition(activeRuntime, mediaId, offsets),
|
|
2251
|
-
onOpenHeaderStory
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2811
|
+
// P8.11 — `onOpenHeaderStory` / `onOpenFooterStory` retired from the
|
|
2812
|
+
// WordReviewEditor wiring. Per-page header / footer bands rendered
|
|
2813
|
+
// by `TwPageStackChromeLayer` call `onOpenStory(target)` with the
|
|
2814
|
+
// exact `EditorStoryTarget` they represent, so the variant /
|
|
2815
|
+
// relationship resolution happens inside the layout facet instead
|
|
2816
|
+
// of the UI. The deprecated props remain in the workspace type
|
|
2817
|
+
// with a mount-time `console.warn`; hosts that still pass them can
|
|
2818
|
+
// migrate to `onOpenStory` at their leisure.
|
|
2255
2819
|
onOpenHeaderStoryForPage: (pageIndex: number) =>
|
|
2256
2820
|
openStoryForPage(activeRuntime, pageIndex, "header"),
|
|
2257
2821
|
onOpenFooterStoryForPage: (pageIndex: number) =>
|
|
2258
2822
|
openStoryForPage(activeRuntime, pageIndex, "footer"),
|
|
2823
|
+
onOpenStory: (target) => {
|
|
2824
|
+
activeRuntime.openStory(target);
|
|
2825
|
+
},
|
|
2259
2826
|
onDeleteSectionBreak: (sectionIndex) =>
|
|
2260
2827
|
applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
|
|
2261
2828
|
onUpdateSectionLayout: (sectionIndex, patch) =>
|
|
@@ -2386,6 +2953,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2386
2953
|
interactionGuardSnapshot={interactionGuardSnapshot}
|
|
2387
2954
|
chromePreset={effectiveChromePreset}
|
|
2388
2955
|
chromeOptions={chromeOptions}
|
|
2956
|
+
{...(props.collabSession ? { collabSession: props.collabSession } : {})}
|
|
2957
|
+
{...(props.collabTransportStatus
|
|
2958
|
+
? { collabTransportStatus: props.collabTransportStatus }
|
|
2959
|
+
: {})}
|
|
2960
|
+
{...(props.activeCommentId !== undefined
|
|
2961
|
+
? { activeCommentId: props.activeCommentId }
|
|
2962
|
+
: {})}
|
|
2963
|
+
collabActorId={currentUser.userId}
|
|
2964
|
+
{...(props.collabSendBaseline
|
|
2965
|
+
? { collabSendBaseline: props.collabSendBaseline }
|
|
2966
|
+
: {})}
|
|
2389
2967
|
reviewQueue={reviewQueueSnapshot}
|
|
2390
2968
|
documentContextAnalytics={documentContextAnalytics}
|
|
2391
2969
|
selectionContextAnalytics={selectionContextAnalytics}
|
|
@@ -2450,11 +3028,83 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2450
3028
|
action: payload.action,
|
|
2451
3029
|
});
|
|
2452
3030
|
}}
|
|
3031
|
+
onScopeAcceptSuggestionGroup={(payload) => {
|
|
3032
|
+
applySuggestionGroupAction(activeRuntime, payload.groupId, "accept");
|
|
3033
|
+
}}
|
|
3034
|
+
onScopeRejectSuggestionGroup={(payload) => {
|
|
3035
|
+
applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
|
|
3036
|
+
}}
|
|
3037
|
+
onScopeAskAgent={(payload) => {
|
|
3038
|
+
// Resolve the scope's anchor + story from the facet's card
|
|
3039
|
+
// model so the agent request carries the canonical range.
|
|
3040
|
+
const facet = activeRuntime.layout;
|
|
3041
|
+
const models =
|
|
3042
|
+
facet && typeof facet.getAllScopeCardModels === "function"
|
|
3043
|
+
? facet.getAllScopeCardModels()
|
|
3044
|
+
: [];
|
|
3045
|
+
const model = models.find((entry) => entry.scopeId === payload.scopeId);
|
|
3046
|
+
if (!model) return;
|
|
3047
|
+
const scopeSnapshot = activeRuntime.getWorkflowScopeSnapshot();
|
|
3048
|
+
const scopes = scopeSnapshot?.scopes ?? [];
|
|
3049
|
+
const scope = scopes.find((entry) => entry.scopeId === payload.scopeId);
|
|
3050
|
+
if (!scope) return;
|
|
3051
|
+
const anchor = scope.anchor;
|
|
3052
|
+
if (!anchor) return;
|
|
3053
|
+
const requestId =
|
|
3054
|
+
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
3055
|
+
? `req-${crypto.randomUUID()}`
|
|
3056
|
+
: `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
3057
|
+
const eventPayload: Extract<
|
|
3058
|
+
WordReviewEditorEvent,
|
|
3059
|
+
{ type: "agent-on-selection-requested" }
|
|
3060
|
+
> = {
|
|
3061
|
+
type: "agent-on-selection-requested",
|
|
3062
|
+
documentId,
|
|
3063
|
+
requestId,
|
|
3064
|
+
scopeId: payload.scopeId,
|
|
3065
|
+
anchor,
|
|
3066
|
+
selectionText: model.label ?? "",
|
|
3067
|
+
...(scope.storyTarget ? { storyTarget: scope.storyTarget } : {}),
|
|
3068
|
+
};
|
|
3069
|
+
onEventRef.current?.(eventPayload);
|
|
3070
|
+
}}
|
|
2453
3071
|
/>
|
|
2454
3072
|
);
|
|
2455
3073
|
},
|
|
2456
3074
|
);
|
|
2457
3075
|
|
|
3076
|
+
/**
|
|
3077
|
+
* R3 — best-effort suggestion-group accept/reject fan-out. Resolves
|
|
3078
|
+
* the group's suggestions from the current snapshot, then fans out
|
|
3079
|
+
* `acceptChange` / `rejectChange` across every changeId in each
|
|
3080
|
+
* group member. P2 batches these in rapid succession; the runtime
|
|
3081
|
+
* commit boundary collapses them into a single logical transaction.
|
|
3082
|
+
* A future phase adds true atomicity at the runtime level.
|
|
3083
|
+
*/
|
|
3084
|
+
function applySuggestionGroupAction(
|
|
3085
|
+
runtime: WordReviewEditorRuntime,
|
|
3086
|
+
groupId: string,
|
|
3087
|
+
action: "accept" | "reject",
|
|
3088
|
+
): void {
|
|
3089
|
+
const snapshot = runtime.getSuggestionsSnapshot();
|
|
3090
|
+
const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
|
|
3091
|
+
if (!group) return;
|
|
3092
|
+
const byId = new Map(
|
|
3093
|
+
snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
|
|
3094
|
+
);
|
|
3095
|
+
for (const suggestionId of group.suggestionIds) {
|
|
3096
|
+
const suggestion = byId.get(suggestionId);
|
|
3097
|
+
if (!suggestion) continue;
|
|
3098
|
+
for (const changeId of suggestion.changeIds) {
|
|
3099
|
+
if (action === "accept") {
|
|
3100
|
+
runtime.acceptChange(changeId);
|
|
3101
|
+
} else {
|
|
3102
|
+
runtime.rejectChange(changeId);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
2458
3108
|
function applyRuntimeFormattingOperation(
|
|
2459
3109
|
runtime: WordReviewEditorRuntime,
|
|
2460
3110
|
operation:
|
|
@@ -4279,47 +4929,6 @@ function openStoryForPage(
|
|
|
4279
4929
|
runtime.openStory(target);
|
|
4280
4930
|
}
|
|
4281
4931
|
|
|
4282
|
-
function openDefaultStoryVariant(
|
|
4283
|
-
runtime: WordReviewEditorRuntime,
|
|
4284
|
-
pageLayout: PageLayoutSnapshot | undefined,
|
|
4285
|
-
navigation: ReturnType<WordReviewEditorRuntime["getDocumentNavigationSnapshot"]> | undefined,
|
|
4286
|
-
kind: "header" | "footer",
|
|
4287
|
-
): void {
|
|
4288
|
-
const variants =
|
|
4289
|
-
kind === "header"
|
|
4290
|
-
? pageLayout?.headerVariants
|
|
4291
|
-
: pageLayout?.footerVariants;
|
|
4292
|
-
const activePage = navigation?.pages[navigation.activePageIndex];
|
|
4293
|
-
const isFirstPageInSection =
|
|
4294
|
-
activePage !== undefined &&
|
|
4295
|
-
activePage.sectionIndex === pageLayout?.sectionIndex &&
|
|
4296
|
-
activePage.pageInSection === 0;
|
|
4297
|
-
const isEvenDocumentPage = activePage !== undefined && (activePage.pageIndex + 1) % 2 === 0;
|
|
4298
|
-
|
|
4299
|
-
let variant =
|
|
4300
|
-
pageLayout?.differentFirstPage && isFirstPageInSection
|
|
4301
|
-
? variants?.find((entry) => entry.variant === "first")
|
|
4302
|
-
: undefined;
|
|
4303
|
-
|
|
4304
|
-
if (!variant && pageLayout?.differentOddEvenPages && isEvenDocumentPage) {
|
|
4305
|
-
variant = variants?.find((entry) => entry.variant === "even");
|
|
4306
|
-
}
|
|
4307
|
-
|
|
4308
|
-
if (!variant) {
|
|
4309
|
-
variant = variants?.find((entry) => entry.variant === "default") ?? variants?.[0];
|
|
4310
|
-
}
|
|
4311
|
-
|
|
4312
|
-
if (!variant) {
|
|
4313
|
-
return;
|
|
4314
|
-
}
|
|
4315
|
-
runtime.openStory({
|
|
4316
|
-
kind,
|
|
4317
|
-
relationshipId: variant.relationshipId,
|
|
4318
|
-
variant: variant.variant,
|
|
4319
|
-
sectionIndex: pageLayout?.sectionIndex,
|
|
4320
|
-
});
|
|
4321
|
-
}
|
|
4322
|
-
|
|
4323
4932
|
function searchRuntimeDocument(
|
|
4324
4933
|
runtime: WordReviewEditorRuntime,
|
|
4325
4934
|
mountedSurface: TwProseMirrorSurfaceRef | null,
|