@beyondwork/docx-react-component 1.0.22 → 1.0.24-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -38
- package/package.json +1 -1
- package/src/api/public-types.ts +67 -1
- package/src/core/commands/index.ts +625 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +667 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +6 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +96 -28
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +6 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -30,6 +30,8 @@ import type {
|
|
|
30
30
|
HeaderFooterLinkPatch,
|
|
31
31
|
ExportDocxOptions,
|
|
32
32
|
ExportResult,
|
|
33
|
+
HostAnnotationOverlay,
|
|
34
|
+
HostAnnotationSnapshot,
|
|
33
35
|
InteractionGuardSnapshot,
|
|
34
36
|
PageLayoutSnapshot,
|
|
35
37
|
PersistedEditorSnapshot,
|
|
@@ -65,7 +67,6 @@ import {
|
|
|
65
67
|
type EditorCommand,
|
|
66
68
|
type EditorTransaction,
|
|
67
69
|
} from "../core/commands/index.ts";
|
|
68
|
-
import { insertText } from "../core/commands/text-commands.ts";
|
|
69
70
|
import {
|
|
70
71
|
createDetachedAnchor,
|
|
71
72
|
createEmptyMapping,
|
|
@@ -105,7 +106,11 @@ import {
|
|
|
105
106
|
resolveActiveSection,
|
|
106
107
|
} from "./document-layout.ts";
|
|
107
108
|
import { normalizeHeaderFooterTarget } from "./story-context.ts";
|
|
108
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
getStoryBlocks,
|
|
111
|
+
replaceStoryBlocks,
|
|
112
|
+
storyTargetKey,
|
|
113
|
+
} from "./story-targeting.ts";
|
|
109
114
|
import {
|
|
110
115
|
createViewState,
|
|
111
116
|
setViewMode as applyViewMode,
|
|
@@ -149,12 +154,24 @@ export type DocumentRuntimeEvent =
|
|
|
149
154
|
})
|
|
150
155
|
| Exclude<WordReviewEditorEvent, { type: "ready" }>;
|
|
151
156
|
|
|
157
|
+
export type ActiveStoryTextCommand =
|
|
158
|
+
| Extract<EditorCommand, { type: "text.insert" }>
|
|
159
|
+
| Extract<EditorCommand, { type: "text.delete-backward" }>
|
|
160
|
+
| Extract<EditorCommand, { type: "text.delete-forward" }>
|
|
161
|
+
| Extract<EditorCommand, { type: "text.insert-tab" }>
|
|
162
|
+
| Extract<EditorCommand, { type: "text.insert-hard-break" }>
|
|
163
|
+
| Extract<EditorCommand, { type: "paragraph.split" }>;
|
|
164
|
+
|
|
152
165
|
export interface DocumentRuntime {
|
|
153
166
|
subscribe(listener: () => void): Unsubscribe;
|
|
154
167
|
subscribeToEvents(listener: (event: DocumentRuntimeEvent) => void): Unsubscribe;
|
|
155
168
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
169
|
+
getCanonicalDocument(): CanonicalDocumentEnvelope;
|
|
170
|
+
getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
|
|
156
171
|
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
172
|
+
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): void;
|
|
157
173
|
dispatch(command: EditorCommand): void;
|
|
174
|
+
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
158
175
|
undo(): void;
|
|
159
176
|
redo(): void;
|
|
160
177
|
focus(): void;
|
|
@@ -194,6 +211,9 @@ export interface DocumentRuntime {
|
|
|
194
211
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
195
212
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
196
213
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
214
|
+
setHostAnnotationOverlay(overlay: HostAnnotationOverlay): void;
|
|
215
|
+
clearHostAnnotationOverlay(): void;
|
|
216
|
+
getHostAnnotationSnapshot(): HostAnnotationSnapshot;
|
|
197
217
|
getWorkflowCandidateRanges(options?: WorkflowCandidateRangeOptions): WorkflowCandidateRange[];
|
|
198
218
|
replaceWorkflowMarkupText(markupId: string, text: string): void;
|
|
199
219
|
}
|
|
@@ -254,6 +274,7 @@ export function createDocumentRuntime(
|
|
|
254
274
|
preservedRangeCount: 0,
|
|
255
275
|
};
|
|
256
276
|
let workflowOverlay: WorkflowOverlay | null = null;
|
|
277
|
+
let hostAnnotationOverlay: HostAnnotationOverlay | null = null;
|
|
257
278
|
const initialPersistedSnapshot = options.initialSessionState
|
|
258
279
|
? persistedSnapshotFromEditorSessionState(options.initialSessionState, {
|
|
259
280
|
savedAt: options.initialSessionState.updatedAt,
|
|
@@ -295,7 +316,7 @@ export function createDocumentRuntime(
|
|
|
295
316
|
let cachedTrackedChanges:
|
|
296
317
|
| {
|
|
297
318
|
revisions: CanonicalDocumentEnvelope["review"]["revisions"];
|
|
298
|
-
|
|
319
|
+
revisionToken: string;
|
|
299
320
|
snapshot: TrackedChangesSnapshot;
|
|
300
321
|
}
|
|
301
322
|
| undefined;
|
|
@@ -344,6 +365,15 @@ export function createDocumentRuntime(
|
|
|
344
365
|
snapshot: WorkflowScopeSnapshot;
|
|
345
366
|
}
|
|
346
367
|
| undefined;
|
|
368
|
+
let cachedWorkflowMarkupSnapshot:
|
|
369
|
+
| {
|
|
370
|
+
revisionToken: string;
|
|
371
|
+
activeStoryKey: string;
|
|
372
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
373
|
+
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
374
|
+
snapshot: WorkflowMarkupSnapshot;
|
|
375
|
+
}
|
|
376
|
+
| undefined;
|
|
347
377
|
|
|
348
378
|
function getCachedSurface(
|
|
349
379
|
document: CanonicalDocumentEnvelope,
|
|
@@ -422,21 +452,20 @@ export function createDocumentRuntime(
|
|
|
422
452
|
|
|
423
453
|
function getCachedTrackedChangesSnapshot(
|
|
424
454
|
nextState: EditorState,
|
|
425
|
-
|
|
455
|
+
_surface: RuntimeRenderSnapshot["surface"],
|
|
426
456
|
): TrackedChangesSnapshot {
|
|
427
|
-
const plainText = surface?.plainText ?? "";
|
|
428
457
|
if (
|
|
429
458
|
cachedTrackedChanges &&
|
|
430
459
|
cachedTrackedChanges.revisions === nextState.document.review.revisions &&
|
|
431
|
-
cachedTrackedChanges.
|
|
460
|
+
cachedTrackedChanges.revisionToken === nextState.revisionToken
|
|
432
461
|
) {
|
|
433
462
|
return cachedTrackedChanges.snapshot;
|
|
434
463
|
}
|
|
435
464
|
|
|
436
|
-
const snapshot = toPublicTrackedChangesSnapshot(nextState
|
|
465
|
+
const snapshot = toPublicTrackedChangesSnapshot(nextState);
|
|
437
466
|
cachedTrackedChanges = {
|
|
438
467
|
revisions: nextState.document.review.revisions,
|
|
439
|
-
|
|
468
|
+
revisionToken: nextState.revisionToken,
|
|
440
469
|
snapshot,
|
|
441
470
|
};
|
|
442
471
|
return snapshot;
|
|
@@ -580,14 +609,28 @@ export function createDocumentRuntime(
|
|
|
580
609
|
});
|
|
581
610
|
}
|
|
582
611
|
|
|
612
|
+
const effectiveDocumentMode = getEffectiveDocumentMode(selection);
|
|
613
|
+
|
|
614
|
+
if (effectiveDocumentMode === "suggesting" && commandType) {
|
|
615
|
+
if (
|
|
616
|
+
activeStory.kind !== "main" &&
|
|
617
|
+
SUGGESTING_SECONDARY_STORY_UNSUPPORTED_COMMANDS.has(commandType)
|
|
618
|
+
) {
|
|
619
|
+
reasons.push({
|
|
620
|
+
code: "suggesting_unsupported",
|
|
621
|
+
message: "Suggesting mode is not yet export-safe in this story.",
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (SUGGESTING_UNSUPPORTED_COMMANDS.has(commandType)) {
|
|
625
|
+
reasons.push({
|
|
626
|
+
code: "suggesting_unsupported",
|
|
627
|
+
message: `"${commandType}" is not supported in suggesting mode.`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
583
632
|
if (workflowOverlay) {
|
|
584
|
-
const
|
|
585
|
-
const matchingScope = activeScopes.find((scope) => {
|
|
586
|
-
if (scope.anchor.kind === "detached") return false;
|
|
587
|
-
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
588
|
-
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
589
|
-
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
590
|
-
});
|
|
633
|
+
const matchingScope = getMatchingWorkflowScope(selection);
|
|
591
634
|
|
|
592
635
|
if (!matchingScope && workflowOverlay.scopes.length > 0) {
|
|
593
636
|
reasons.push({
|
|
@@ -620,6 +663,39 @@ export function createDocumentRuntime(
|
|
|
620
663
|
return reasons;
|
|
621
664
|
}
|
|
622
665
|
|
|
666
|
+
function getMatchingWorkflowScope(
|
|
667
|
+
selection: EditorState["selection"],
|
|
668
|
+
): WorkflowOverlay["scopes"][number] | null {
|
|
669
|
+
if (!workflowOverlay) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const selectionBounds = {
|
|
674
|
+
from: Math.min(selection.anchor, selection.head),
|
|
675
|
+
to: Math.max(selection.anchor, selection.head),
|
|
676
|
+
};
|
|
677
|
+
const activeScopes = getEffectiveWorkflowScopes(workflowOverlay);
|
|
678
|
+
return activeScopes.find((scope) => {
|
|
679
|
+
if (scope.anchor.kind === "detached") return false;
|
|
680
|
+
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
681
|
+
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
682
|
+
return selectionBounds.from >= scopeFrom && selectionBounds.to <= scopeTo;
|
|
683
|
+
}) ?? null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function getEffectiveDocumentMode(
|
|
687
|
+
selection: EditorState["selection"],
|
|
688
|
+
): DocumentMode {
|
|
689
|
+
if (viewState.documentMode === "viewing") {
|
|
690
|
+
return "viewing";
|
|
691
|
+
}
|
|
692
|
+
const matchingScope = getMatchingWorkflowScope(selection);
|
|
693
|
+
if (matchingScope?.mode === "suggest") {
|
|
694
|
+
return "suggesting";
|
|
695
|
+
}
|
|
696
|
+
return viewState.documentMode;
|
|
697
|
+
}
|
|
698
|
+
|
|
623
699
|
function expandSelectionRange(
|
|
624
700
|
range: { from: number; to: number },
|
|
625
701
|
): { from: number; to: number } {
|
|
@@ -632,10 +708,14 @@ export function createDocumentRuntime(
|
|
|
632
708
|
function deriveOpaqueWorkflowBlockedReason(
|
|
633
709
|
range: { from: number; to: number },
|
|
634
710
|
): WorkflowBlockedCommandReason | null {
|
|
711
|
+
const targetPartPath = getStoryTargetOpaquePartPath(activeStory);
|
|
712
|
+
if (!targetPartPath) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
635
715
|
const fragments = findOpaqueFragmentsIntersectingRange(
|
|
636
716
|
state.document.preservation,
|
|
637
717
|
range,
|
|
638
|
-
);
|
|
718
|
+
).filter((fragment) => fragment.packagePartName === targetPartPath);
|
|
639
719
|
|
|
640
720
|
if (fragments.length === 0) {
|
|
641
721
|
return null;
|
|
@@ -669,6 +749,35 @@ export function createDocumentRuntime(
|
|
|
669
749
|
};
|
|
670
750
|
}
|
|
671
751
|
|
|
752
|
+
function getStoryTargetOpaquePartPath(storyTarget: EditorStoryTarget): string | null {
|
|
753
|
+
if (storyTarget.kind === "main") {
|
|
754
|
+
return "/word/document.xml";
|
|
755
|
+
}
|
|
756
|
+
if (storyTarget.kind === "header") {
|
|
757
|
+
return state.document.subParts?.headers.find(
|
|
758
|
+
(header) =>
|
|
759
|
+
header.relationshipId === storyTarget.relationshipId
|
|
760
|
+
&& header.variant === storyTarget.variant
|
|
761
|
+
&& header.sectionIndex === storyTarget.sectionIndex,
|
|
762
|
+
)?.partPath ?? null;
|
|
763
|
+
}
|
|
764
|
+
if (storyTarget.kind === "footer") {
|
|
765
|
+
return state.document.subParts?.footers.find(
|
|
766
|
+
(footer) =>
|
|
767
|
+
footer.relationshipId === storyTarget.relationshipId
|
|
768
|
+
&& footer.variant === storyTarget.variant
|
|
769
|
+
&& footer.sectionIndex === storyTarget.sectionIndex,
|
|
770
|
+
)?.partPath ?? null;
|
|
771
|
+
}
|
|
772
|
+
if (storyTarget.kind === "footnote") {
|
|
773
|
+
return "/word/footnotes.xml";
|
|
774
|
+
}
|
|
775
|
+
if (storyTarget.kind === "endnote") {
|
|
776
|
+
return "/word/endnotes.xml";
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
672
781
|
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
673
782
|
if (!workflowOverlay) return null;
|
|
674
783
|
const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
|
|
@@ -687,6 +796,13 @@ export function createDocumentRuntime(
|
|
|
687
796
|
};
|
|
688
797
|
}
|
|
689
798
|
|
|
799
|
+
function deriveHostAnnotationSnapshot(): HostAnnotationSnapshot {
|
|
800
|
+
return {
|
|
801
|
+
totalCount: hostAnnotationOverlay?.annotations.length ?? 0,
|
|
802
|
+
annotations: structuredClone(hostAnnotationOverlay?.annotations ?? []),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
690
806
|
function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
|
|
691
807
|
const activeWorkItemId = overlay.activeWorkItemId ?? null;
|
|
692
808
|
const activeWorkItemScopeIds =
|
|
@@ -728,8 +844,25 @@ export function createDocumentRuntime(
|
|
|
728
844
|
return cachedInteractionGuardSnapshot.snapshot;
|
|
729
845
|
}
|
|
730
846
|
|
|
731
|
-
const
|
|
732
|
-
|
|
847
|
+
const blockedReasons = evaluateWorkflowBlockedReasons(state.selection);
|
|
848
|
+
const matchingScope = getMatchingWorkflowScope(state.selection);
|
|
849
|
+
const primaryBlockedReason = blockedReasons[0];
|
|
850
|
+
const snapshot: InteractionGuardSnapshot = {
|
|
851
|
+
effectiveMode: primaryBlockedReason
|
|
852
|
+
? (
|
|
853
|
+
primaryBlockedReason.code === "workflow_comment_only"
|
|
854
|
+
? "comment"
|
|
855
|
+
: primaryBlockedReason.code === "workflow_view_only"
|
|
856
|
+
? "view"
|
|
857
|
+
: "blocked"
|
|
858
|
+
)
|
|
859
|
+
: getEffectiveDocumentMode(state.selection) === "suggesting"
|
|
860
|
+
? "suggest"
|
|
861
|
+
: matchingScope?.mode ?? "edit",
|
|
862
|
+
...(matchingScope?.scopeId ? { matchedScopeId: matchingScope.scopeId } : {}),
|
|
863
|
+
...(matchingScope?.mode ? { matchedScopeMode: matchingScope.mode } : {}),
|
|
864
|
+
...(primaryBlockedReason ? { disabledReason: primaryBlockedReason.message } : {}),
|
|
865
|
+
blockedReasons,
|
|
733
866
|
};
|
|
734
867
|
cachedInteractionGuardSnapshot = {
|
|
735
868
|
revisionToken: state.revisionToken,
|
|
@@ -767,6 +900,34 @@ export function createDocumentRuntime(
|
|
|
767
900
|
return snapshot;
|
|
768
901
|
}
|
|
769
902
|
|
|
903
|
+
function getCachedWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot {
|
|
904
|
+
const activeStoryKey = storyTargetKey(activeStory);
|
|
905
|
+
if (
|
|
906
|
+
cachedWorkflowMarkupSnapshot &&
|
|
907
|
+
cachedWorkflowMarkupSnapshot.revisionToken === state.revisionToken &&
|
|
908
|
+
cachedWorkflowMarkupSnapshot.activeStoryKey === activeStoryKey &&
|
|
909
|
+
cachedWorkflowMarkupSnapshot.protectionSnapshot === protectionSnapshot &&
|
|
910
|
+
cachedWorkflowMarkupSnapshot.preservation === state.document.preservation
|
|
911
|
+
) {
|
|
912
|
+
return cachedWorkflowMarkupSnapshot.snapshot;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const snapshot = collectWorkflowMarkupSnapshot({
|
|
916
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
917
|
+
fieldSnapshot: buildFieldSnapshot(state.document),
|
|
918
|
+
protectionSnapshot,
|
|
919
|
+
preservation: state.document.preservation,
|
|
920
|
+
});
|
|
921
|
+
cachedWorkflowMarkupSnapshot = {
|
|
922
|
+
revisionToken: state.revisionToken,
|
|
923
|
+
activeStoryKey,
|
|
924
|
+
protectionSnapshot,
|
|
925
|
+
preservation: state.document.preservation,
|
|
926
|
+
snapshot,
|
|
927
|
+
};
|
|
928
|
+
return snapshot;
|
|
929
|
+
}
|
|
930
|
+
|
|
770
931
|
function refreshRenderSnapshot(): RuntimeRenderSnapshot {
|
|
771
932
|
const surface = getCachedSurface(state.document, activeStory);
|
|
772
933
|
return {
|
|
@@ -878,10 +1039,25 @@ export function createDocumentRuntime(
|
|
|
878
1039
|
getRenderSnapshot() {
|
|
879
1040
|
return cachedRenderSnapshot;
|
|
880
1041
|
},
|
|
1042
|
+
getCanonicalDocument() {
|
|
1043
|
+
return state.document;
|
|
1044
|
+
},
|
|
1045
|
+
getSourcePackage() {
|
|
1046
|
+
return state.sourcePackage;
|
|
1047
|
+
},
|
|
1048
|
+
emitBlockedCommand(command, reasons) {
|
|
1049
|
+
emit({
|
|
1050
|
+
type: "command_blocked",
|
|
1051
|
+
documentId: state.documentId,
|
|
1052
|
+
command,
|
|
1053
|
+
reasons,
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
881
1056
|
dispatch(command) {
|
|
1057
|
+
const commandSelection = getCommandSelection(command, state.selection);
|
|
882
1058
|
if (isMutationCommand(command)) {
|
|
883
1059
|
const blockedReasons = evaluateWorkflowBlockedReasons(
|
|
884
|
-
|
|
1060
|
+
commandSelection,
|
|
885
1061
|
command.type,
|
|
886
1062
|
);
|
|
887
1063
|
if (blockedReasons.length > 0) {
|
|
@@ -909,6 +1085,8 @@ export function createDocumentRuntime(
|
|
|
909
1085
|
try {
|
|
910
1086
|
const transaction = executeEditorCommand(state, command, {
|
|
911
1087
|
timestamp: command.origin?.timestamp ?? clock(),
|
|
1088
|
+
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1089
|
+
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
912
1090
|
});
|
|
913
1091
|
commit(transaction);
|
|
914
1092
|
} catch (error) {
|
|
@@ -949,29 +1127,24 @@ export function createDocumentRuntime(
|
|
|
949
1127
|
replaceText(text, target) {
|
|
950
1128
|
try {
|
|
951
1129
|
const timestamp = clock();
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
mapping: result.mapping,
|
|
971
|
-
selection: result.selection,
|
|
972
|
-
protectionSelection: selection,
|
|
973
|
-
origin: createOrigin("api", timestamp),
|
|
974
|
-
});
|
|
1130
|
+
applyTextCommandInActiveStory(
|
|
1131
|
+
{
|
|
1132
|
+
type: "text.insert",
|
|
1133
|
+
text,
|
|
1134
|
+
origin: createOrigin("api", timestamp),
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
selection: target ? createSelectionFromPublicAnchor(target) : state.selection,
|
|
1138
|
+
blockedCommandName: "replaceText",
|
|
1139
|
+
},
|
|
1140
|
+
);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
emitError(toRuntimeError(error));
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
applyActiveStoryTextCommand(command) {
|
|
1146
|
+
try {
|
|
1147
|
+
applyTextCommandInActiveStory(command);
|
|
975
1148
|
} catch (error) {
|
|
976
1149
|
emitError(toRuntimeError(error));
|
|
977
1150
|
}
|
|
@@ -1308,12 +1481,32 @@ export function createDocumentRuntime(
|
|
|
1308
1481
|
return getCachedInteractionGuardSnapshot();
|
|
1309
1482
|
},
|
|
1310
1483
|
getWorkflowMarkupSnapshot() {
|
|
1311
|
-
return
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1484
|
+
return getCachedWorkflowMarkupSnapshot();
|
|
1485
|
+
},
|
|
1486
|
+
setHostAnnotationOverlay(overlay) {
|
|
1487
|
+
hostAnnotationOverlay = structuredClone(overlay);
|
|
1488
|
+
emit({
|
|
1489
|
+
type: "host_annotation_overlay_changed",
|
|
1490
|
+
documentId: state.documentId,
|
|
1491
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
1316
1492
|
});
|
|
1493
|
+
for (const listener of listeners) {
|
|
1494
|
+
listener();
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
clearHostAnnotationOverlay() {
|
|
1498
|
+
hostAnnotationOverlay = null;
|
|
1499
|
+
emit({
|
|
1500
|
+
type: "host_annotation_overlay_changed",
|
|
1501
|
+
documentId: state.documentId,
|
|
1502
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
1503
|
+
});
|
|
1504
|
+
for (const listener of listeners) {
|
|
1505
|
+
listener();
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
getHostAnnotationSnapshot() {
|
|
1509
|
+
return deriveHostAnnotationSnapshot();
|
|
1317
1510
|
},
|
|
1318
1511
|
getWorkflowCandidateRanges(options) {
|
|
1319
1512
|
return deriveWorkflowCandidateRangesFromMarkup(this.getWorkflowMarkupSnapshot(), options);
|
|
@@ -1325,6 +1518,14 @@ export function createDocumentRuntime(
|
|
|
1325
1518
|
if (!target || target.anchor.kind === "detached") {
|
|
1326
1519
|
return;
|
|
1327
1520
|
}
|
|
1521
|
+
const targetStory = target.storyTarget ?? MAIN_STORY_TARGET;
|
|
1522
|
+
if (!storyTargetsEqual(activeStory, targetStory)) {
|
|
1523
|
+
if (targetStory.kind === "main") {
|
|
1524
|
+
this.closeStory();
|
|
1525
|
+
} else if (!this.openStory(targetStory)) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1328
1529
|
this.replaceText(text, target.anchor);
|
|
1329
1530
|
},
|
|
1330
1531
|
};
|
|
@@ -1343,7 +1544,7 @@ export function createDocumentRuntime(
|
|
|
1343
1544
|
const previous = state;
|
|
1344
1545
|
// Undo/redo changes the document — must mint a new revisionToken so
|
|
1345
1546
|
// autosave/export checkpoint dedup treats it as fresh content.
|
|
1346
|
-
state = finalizeState(target, true, clock());
|
|
1547
|
+
state = finalizeState(target, true, clock(), previous.revision);
|
|
1347
1548
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
1348
1549
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1349
1550
|
notify(previous, state, {
|
|
@@ -1427,6 +1628,27 @@ export function createDocumentRuntime(
|
|
|
1427
1628
|
});
|
|
1428
1629
|
}
|
|
1429
1630
|
|
|
1631
|
+
if (transaction.effects.revisionAuthored) {
|
|
1632
|
+
emit({
|
|
1633
|
+
type: "change_authored",
|
|
1634
|
+
documentId: next.documentId,
|
|
1635
|
+
changeId: transaction.effects.revisionAuthored.changeId,
|
|
1636
|
+
kind: transaction.effects.revisionAuthored.kind,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (transaction.effects.commandBlocked) {
|
|
1641
|
+
emit({
|
|
1642
|
+
type: "command_blocked",
|
|
1643
|
+
documentId: next.documentId,
|
|
1644
|
+
command: transaction.effects.commandBlocked.code,
|
|
1645
|
+
reasons: [{
|
|
1646
|
+
code: transaction.effects.commandBlocked.code as WorkflowBlockedCommandReason["code"],
|
|
1647
|
+
message: transaction.effects.commandBlocked.message,
|
|
1648
|
+
}],
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1430
1652
|
for (const warning of transaction.effects.warningsAdded) {
|
|
1431
1653
|
const publicWarning = toPublicWarning(warning);
|
|
1432
1654
|
emit({
|
|
@@ -1451,6 +1673,164 @@ export function createDocumentRuntime(
|
|
|
1451
1673
|
}
|
|
1452
1674
|
}
|
|
1453
1675
|
|
|
1676
|
+
function applyTextCommandInActiveStory(
|
|
1677
|
+
command: ActiveStoryTextCommand,
|
|
1678
|
+
options: {
|
|
1679
|
+
selection?: EditorState["selection"];
|
|
1680
|
+
blockedCommandName?: string;
|
|
1681
|
+
} = {},
|
|
1682
|
+
): void {
|
|
1683
|
+
const selection = options.selection ?? state.selection;
|
|
1684
|
+
const blockedReasons = evaluateWorkflowBlockedReasons(selection, command.type);
|
|
1685
|
+
if (blockedReasons.length > 0) {
|
|
1686
|
+
emit({
|
|
1687
|
+
type: "command_blocked",
|
|
1688
|
+
documentId: state.documentId,
|
|
1689
|
+
command: options.blockedCommandName ?? command.type,
|
|
1690
|
+
reasons: blockedReasons,
|
|
1691
|
+
});
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const timestamp = command.origin?.timestamp ?? clock();
|
|
1696
|
+
const context = {
|
|
1697
|
+
timestamp,
|
|
1698
|
+
documentMode: getEffectiveDocumentMode(selection),
|
|
1699
|
+
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1700
|
+
} as const;
|
|
1701
|
+
const baseState = selection === state.selection
|
|
1702
|
+
? state
|
|
1703
|
+
: {
|
|
1704
|
+
...state,
|
|
1705
|
+
selection,
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
if (activeStory.kind === "main") {
|
|
1709
|
+
commit(executeEditorCommand(baseState, command, context));
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const localState = createEditorState({
|
|
1714
|
+
documentId: state.documentId,
|
|
1715
|
+
sessionId,
|
|
1716
|
+
sourceLabel: state.sourceLabel,
|
|
1717
|
+
readOnly: state.readOnly,
|
|
1718
|
+
canonicalDocument: {
|
|
1719
|
+
...state.document,
|
|
1720
|
+
content: {
|
|
1721
|
+
type: "doc",
|
|
1722
|
+
children: [...getStoryBlocks(state.document, activeStory)],
|
|
1723
|
+
},
|
|
1724
|
+
review: createSecondaryStoryLocalReviewState(state.document.review, activeStory),
|
|
1725
|
+
},
|
|
1726
|
+
compatibility: state.compatibility,
|
|
1727
|
+
warnings: state.warnings,
|
|
1728
|
+
fatalError: state.fatalError,
|
|
1729
|
+
});
|
|
1730
|
+
localState.selection = selection;
|
|
1731
|
+
const localTransaction = executeEditorCommand(localState, command, context);
|
|
1732
|
+
|
|
1733
|
+
if (!localTransaction.markDirty) {
|
|
1734
|
+
notify(state, state, {
|
|
1735
|
+
nextState: state,
|
|
1736
|
+
mapping: createEmptyMapping(),
|
|
1737
|
+
effects: localTransaction.effects,
|
|
1738
|
+
historyBoundary: "skip",
|
|
1739
|
+
markDirty: false,
|
|
1740
|
+
});
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const nextDocument = replaceStoryBlocks(
|
|
1745
|
+
state.document,
|
|
1746
|
+
activeStory,
|
|
1747
|
+
localTransaction.nextState.document.content.children,
|
|
1748
|
+
);
|
|
1749
|
+
const nextDocumentWithReview = {
|
|
1750
|
+
...nextDocument,
|
|
1751
|
+
review: mergeSecondaryStoryReviewState(
|
|
1752
|
+
state.document.review,
|
|
1753
|
+
localTransaction.nextState.document.review,
|
|
1754
|
+
localTransaction.effects,
|
|
1755
|
+
activeStory,
|
|
1756
|
+
),
|
|
1757
|
+
};
|
|
1758
|
+
const fullTransaction = executeEditorCommand(
|
|
1759
|
+
baseState,
|
|
1760
|
+
{
|
|
1761
|
+
type: "document.replace",
|
|
1762
|
+
document: nextDocumentWithReview,
|
|
1763
|
+
selection: localTransaction.nextState.selection,
|
|
1764
|
+
mapping: createEmptyMapping(),
|
|
1765
|
+
protectionSelection: selection,
|
|
1766
|
+
origin: command.origin,
|
|
1767
|
+
},
|
|
1768
|
+
context,
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
commit({
|
|
1772
|
+
...fullTransaction,
|
|
1773
|
+
effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function mergeTransactionEffects(
|
|
1778
|
+
base: EditorTransaction["effects"],
|
|
1779
|
+
local: EditorTransaction["effects"],
|
|
1780
|
+
): EditorTransaction["effects"] {
|
|
1781
|
+
return {
|
|
1782
|
+
warningsAdded: [...base.warningsAdded, ...local.warningsAdded],
|
|
1783
|
+
warningsCleared: [...base.warningsCleared, ...local.warningsCleared],
|
|
1784
|
+
commentAdded: base.commentAdded ?? local.commentAdded,
|
|
1785
|
+
commentResolved: base.commentResolved ?? local.commentResolved,
|
|
1786
|
+
commentReopened: base.commentReopened ?? local.commentReopened,
|
|
1787
|
+
commentReplyAdded: base.commentReplyAdded ?? local.commentReplyAdded,
|
|
1788
|
+
commentBodyEdited: base.commentBodyEdited ?? local.commentBodyEdited,
|
|
1789
|
+
changeAccepted: base.changeAccepted ?? local.changeAccepted,
|
|
1790
|
+
changeRejected: base.changeRejected ?? local.changeRejected,
|
|
1791
|
+
revisionAuthored: base.revisionAuthored ?? local.revisionAuthored,
|
|
1792
|
+
commandBlocked: base.commandBlocked ?? local.commandBlocked,
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function mergeSecondaryStoryReviewState(
|
|
1797
|
+
currentReview: EditorState["document"]["review"],
|
|
1798
|
+
localReview: EditorState["document"]["review"],
|
|
1799
|
+
effects: EditorTransaction["effects"],
|
|
1800
|
+
storyTarget: EditorStoryTarget,
|
|
1801
|
+
): EditorState["document"]["review"] {
|
|
1802
|
+
const nextReview: EditorState["document"]["review"] = {
|
|
1803
|
+
comments: { ...currentReview.comments },
|
|
1804
|
+
revisions: { ...currentReview.revisions },
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
const currentStoryRevisionIds = Object.values(currentReview.revisions)
|
|
1808
|
+
.filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
|
|
1809
|
+
.map((revision) => revision.changeId);
|
|
1810
|
+
for (const revisionId of currentStoryRevisionIds) {
|
|
1811
|
+
delete nextReview.revisions[revisionId];
|
|
1812
|
+
}
|
|
1813
|
+
for (const revision of Object.values(localReview.revisions)) {
|
|
1814
|
+
nextReview.revisions[revision.changeId] = {
|
|
1815
|
+
...revision,
|
|
1816
|
+
metadata: {
|
|
1817
|
+
...revision.metadata,
|
|
1818
|
+
storyTarget: createRevisionStoryTargetRecord(storyTarget),
|
|
1819
|
+
},
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (effects.commentAdded) {
|
|
1824
|
+
const commentId = effects.commentAdded.commentId;
|
|
1825
|
+
const comment = localReview.comments[commentId];
|
|
1826
|
+
if (comment) {
|
|
1827
|
+
nextReview.comments[commentId] = comment;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return nextReview;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1454
1834
|
function emit(event: DocumentRuntimeEvent): void {
|
|
1455
1835
|
options.onEvent?.(event);
|
|
1456
1836
|
for (const listener of eventListeners) {
|
|
@@ -1547,11 +1927,12 @@ function finalizeState(
|
|
|
1547
1927
|
state: EditorState,
|
|
1548
1928
|
markDirty: boolean,
|
|
1549
1929
|
timestamp: string,
|
|
1930
|
+
baseRevision?: number,
|
|
1550
1931
|
): EditorState {
|
|
1551
1932
|
// Only increment revision on actual document mutations (markDirty=true).
|
|
1552
1933
|
// Selection-only changes must not churn the revisionToken, which would
|
|
1553
1934
|
// cause autosave/checkpoint dedup to treat cursor movement as new content.
|
|
1554
|
-
const revision = markDirty ? state.revision + 1 : state.revision;
|
|
1935
|
+
const revision = markDirty ? (baseRevision ?? state.revision) + 1 : state.revision;
|
|
1555
1936
|
|
|
1556
1937
|
return {
|
|
1557
1938
|
...state,
|
|
@@ -1780,11 +2161,11 @@ function toPublicCommentSidebarSnapshot(
|
|
|
1780
2161
|
|
|
1781
2162
|
function toPublicTrackedChangesSnapshot(
|
|
1782
2163
|
state: EditorState,
|
|
1783
|
-
surfaceText = "",
|
|
1784
2164
|
): TrackedChangesSnapshot {
|
|
1785
2165
|
const projection = createRevisionSidebarProjection(
|
|
1786
2166
|
createRevisionStoreFromDocument(state),
|
|
1787
2167
|
);
|
|
2168
|
+
const storyPlainTextCache = new Map<string, string>();
|
|
1788
2169
|
|
|
1789
2170
|
return {
|
|
1790
2171
|
pendingChangeIds: projection.activeRevisionIds,
|
|
@@ -1796,11 +2177,12 @@ function toPublicTrackedChangesSnapshot(
|
|
|
1796
2177
|
totalCount: projection.totalCount,
|
|
1797
2178
|
revisions: projection.revisions.map((revision): TrackedChangeEntrySnapshot => {
|
|
1798
2179
|
const sourceRevision = state.document.review.revisions[revision.revisionId];
|
|
2180
|
+
const storyTarget = getRevisionStoryTarget(sourceRevision);
|
|
1799
2181
|
const preview = describeRevisionPreview(
|
|
1800
2182
|
revision,
|
|
1801
2183
|
sourceRevision?.anchor ??
|
|
1802
2184
|
createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
|
|
1803
|
-
|
|
2185
|
+
getStoryPlainText(state.document, storyTarget, storyPlainTextCache),
|
|
1804
2186
|
);
|
|
1805
2187
|
|
|
1806
2188
|
return {
|
|
@@ -1809,6 +2191,7 @@ function toPublicTrackedChangesSnapshot(
|
|
|
1809
2191
|
label: revision.label,
|
|
1810
2192
|
status: revision.status,
|
|
1811
2193
|
actionability: revision.actionability,
|
|
2194
|
+
storyTarget,
|
|
1812
2195
|
anchor: toPublicAnchorProjection(
|
|
1813
2196
|
sourceRevision?.anchor ??
|
|
1814
2197
|
createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
|
|
@@ -1849,6 +2232,7 @@ function createRevisionStoreFromDocument(
|
|
|
1849
2232
|
warningIds: [...(revision.warningIds ?? [])],
|
|
1850
2233
|
metadata: {
|
|
1851
2234
|
source: revision.metadata?.source ?? "runtime",
|
|
2235
|
+
storyTarget: revision.metadata?.storyTarget,
|
|
1852
2236
|
preserveOnlyReason: revision.metadata?.preserveOnlyReason,
|
|
1853
2237
|
importedRevisionForm: revision.metadata?.importedRevisionForm,
|
|
1854
2238
|
originalRevisionType: revision.metadata?.originalRevisionType,
|
|
@@ -1860,6 +2244,61 @@ function createRevisionStoreFromDocument(
|
|
|
1860
2244
|
};
|
|
1861
2245
|
}
|
|
1862
2246
|
|
|
2247
|
+
function getRevisionStoryTarget(
|
|
2248
|
+
revision: EditorState["document"]["review"]["revisions"][string] | undefined,
|
|
2249
|
+
): EditorStoryTarget {
|
|
2250
|
+
const storyTarget = revision?.metadata?.storyTarget;
|
|
2251
|
+
return storyTarget ? { ...storyTarget } : MAIN_STORY_TARGET;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function createSecondaryStoryLocalReviewState(
|
|
2255
|
+
review: EditorState["document"]["review"],
|
|
2256
|
+
storyTarget: EditorStoryTarget,
|
|
2257
|
+
): EditorState["document"]["review"] {
|
|
2258
|
+
return {
|
|
2259
|
+
comments: {},
|
|
2260
|
+
revisions: Object.fromEntries(
|
|
2261
|
+
Object.values(review.revisions)
|
|
2262
|
+
.filter((revision) => storyTargetsEqual(getRevisionStoryTarget(revision), storyTarget))
|
|
2263
|
+
.map((revision) => [
|
|
2264
|
+
revision.changeId,
|
|
2265
|
+
{
|
|
2266
|
+
...revision,
|
|
2267
|
+
metadata: {
|
|
2268
|
+
...revision.metadata,
|
|
2269
|
+
storyTarget: createRevisionStoryTargetRecord(storyTarget),
|
|
2270
|
+
},
|
|
2271
|
+
},
|
|
2272
|
+
]),
|
|
2273
|
+
),
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function getStoryPlainText(
|
|
2278
|
+
document: CanonicalDocumentEnvelope,
|
|
2279
|
+
storyTarget: EditorStoryTarget,
|
|
2280
|
+
cache: Map<string, string>,
|
|
2281
|
+
): string {
|
|
2282
|
+
const key = storyTargetKey(storyTarget);
|
|
2283
|
+
const cached = cache.get(key);
|
|
2284
|
+
if (cached !== undefined) {
|
|
2285
|
+
return cached;
|
|
2286
|
+
}
|
|
2287
|
+
const plainText = createEditorSurfaceSnapshot(
|
|
2288
|
+
document,
|
|
2289
|
+
createSelectionSnapshot(0, 0),
|
|
2290
|
+
storyTarget,
|
|
2291
|
+
).plainText;
|
|
2292
|
+
cache.set(key, plainText);
|
|
2293
|
+
return plainText;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function createRevisionStoryTargetRecord(
|
|
2297
|
+
storyTarget: EditorStoryTarget,
|
|
2298
|
+
): NonNullable<NonNullable<EditorState["document"]["review"]["revisions"][string]["metadata"]>["storyTarget"]> {
|
|
2299
|
+
return { ...storyTarget };
|
|
2300
|
+
}
|
|
2301
|
+
|
|
1863
2302
|
function listBlockExportReasons(
|
|
1864
2303
|
report: InternalCompatibilityReport,
|
|
1865
2304
|
): string[] {
|
|
@@ -2011,6 +2450,19 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
2011
2450
|
"comment.open",
|
|
2012
2451
|
]);
|
|
2013
2452
|
|
|
2453
|
+
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
2454
|
+
const SUGGESTING_UNSUPPORTED_COMMANDS = new Set([
|
|
2455
|
+
"paragraph.split",
|
|
2456
|
+
]);
|
|
2457
|
+
|
|
2458
|
+
const SUGGESTING_SECONDARY_STORY_UNSUPPORTED_COMMANDS = new Set([
|
|
2459
|
+
"text.insert",
|
|
2460
|
+
"text.delete-backward",
|
|
2461
|
+
"text.delete-forward",
|
|
2462
|
+
"text.insert-tab",
|
|
2463
|
+
"text.insert-hard-break",
|
|
2464
|
+
]);
|
|
2465
|
+
|
|
2014
2466
|
function isMutationCommand(command: EditorCommand): boolean {
|
|
2015
2467
|
return !NON_MUTATION_COMMANDS.has(command.type);
|
|
2016
2468
|
}
|