@beyondwork/docx-react-component 1.0.34 → 1.0.36
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 +35 -40
- package/src/api/public-types.ts +9 -1
- package/src/api/session-state.ts +24 -0
- package/src/core/commands/index.ts +74 -1
- package/src/index.ts +2 -0
- package/src/io/docx-session.ts +260 -3
- package/src/io/ooxml/workflow-payload.ts +122 -0
- package/src/model/snapshot.ts +58 -0
- package/src/runtime/document-runtime.ts +9 -8
- package/src/runtime/session-capabilities.ts +7 -4
- package/src/runtime/surface-projection.ts +8 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +8 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.36",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -89,31 +88,6 @@
|
|
|
89
88
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
90
89
|
},
|
|
91
90
|
"types": "./src/index.ts",
|
|
92
|
-
"scripts": {
|
|
93
|
-
"build": "tsup",
|
|
94
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
95
|
-
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
96
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
97
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
98
|
-
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
99
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
100
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
101
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
102
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
103
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
104
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
105
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
106
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
107
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
108
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
109
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
110
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
111
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
112
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
113
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
114
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
115
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
116
|
-
},
|
|
117
91
|
"keywords": [
|
|
118
92
|
"docx",
|
|
119
93
|
"word",
|
|
@@ -162,9 +136,15 @@
|
|
|
162
136
|
"y-protocols": "^1.0.0"
|
|
163
137
|
},
|
|
164
138
|
"peerDependenciesMeta": {
|
|
165
|
-
"yjs": {
|
|
166
|
-
|
|
167
|
-
|
|
139
|
+
"yjs": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
142
|
+
"y-prosemirror": {
|
|
143
|
+
"optional": true
|
|
144
|
+
},
|
|
145
|
+
"y-protocols": {
|
|
146
|
+
"optional": true
|
|
147
|
+
}
|
|
168
148
|
},
|
|
169
149
|
"devDependencies": {
|
|
170
150
|
"@chllming/wave-orchestration": "^0.9.15",
|
|
@@ -187,14 +167,29 @@
|
|
|
187
167
|
"y-protocols": "^1.0.7",
|
|
188
168
|
"yjs": "^13.6.30"
|
|
189
169
|
},
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
"
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
170
|
+
"scripts": {
|
|
171
|
+
"build": "tsup",
|
|
172
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
173
|
+
"test:repo": "node scripts/run-repo-tests.mjs core",
|
|
174
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
175
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
176
|
+
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
177
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
178
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
179
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
180
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
181
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
182
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
183
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
184
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
185
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
186
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
187
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
188
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
189
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
190
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
191
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
192
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
193
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
199
194
|
}
|
|
200
|
-
}
|
|
195
|
+
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -920,7 +920,7 @@ export type ViewMode = "editing" | "review" | "view";
|
|
|
920
920
|
* Distinct from `ViewMode` (editor posture) and `WorkspaceMode` (shell layout).
|
|
921
921
|
* `DocumentMode` reflects document-level editing authority, not presentation.
|
|
922
922
|
*/
|
|
923
|
-
export type DocumentMode = "editing" | "suggesting" | "viewing";
|
|
923
|
+
export type DocumentMode = "editing" | "suggesting" | "viewing" | "commenting";
|
|
924
924
|
|
|
925
925
|
/**
|
|
926
926
|
* A single permission range parsed from `w:permStart` / `w:permEnd` in the
|
|
@@ -1145,6 +1145,7 @@ export interface WorkflowCandidateRange {
|
|
|
1145
1145
|
|
|
1146
1146
|
export interface WorkflowScope {
|
|
1147
1147
|
scopeId: string;
|
|
1148
|
+
version?: number;
|
|
1148
1149
|
mode: WorkflowScopeMode;
|
|
1149
1150
|
anchor: EditorAnchorProjection;
|
|
1150
1151
|
storyTarget?: EditorStoryTarget;
|
|
@@ -1152,6 +1153,13 @@ export interface WorkflowScope {
|
|
|
1152
1153
|
label?: string;
|
|
1153
1154
|
domain?: "legal" | "commercial" | "finance" | "other";
|
|
1154
1155
|
metadataRefs?: string[];
|
|
1156
|
+
metadata?: WorkflowScopeMetadataField[];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export interface WorkflowScopeMetadataField {
|
|
1160
|
+
key: string;
|
|
1161
|
+
valueType?: "string" | "number" | "boolean" | "json";
|
|
1162
|
+
value?: string | number | boolean | Record<string, unknown>;
|
|
1155
1163
|
}
|
|
1156
1164
|
|
|
1157
1165
|
export interface WorkflowWorkItem {
|
package/src/api/session-state.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type {
|
|
|
2
2
|
EditorSessionState,
|
|
3
3
|
PersistedEditorSnapshot,
|
|
4
4
|
} from "./public-types.ts";
|
|
5
|
+
import {
|
|
6
|
+
assertPersistedEditorSnapshot,
|
|
7
|
+
validatePersistedEditorSnapshot,
|
|
8
|
+
} from "../model/snapshot.ts";
|
|
5
9
|
|
|
6
10
|
export const EDITOR_SESSION_STATE_VERSION = "editor-session-state/1" as const;
|
|
7
11
|
|
|
@@ -62,3 +66,23 @@ export function persistedSnapshotFromEditorSessionState(
|
|
|
62
66
|
workflowMetadata: sessionState.workflowMetadata,
|
|
63
67
|
});
|
|
64
68
|
}
|
|
69
|
+
|
|
70
|
+
export function validateEditorSessionState(
|
|
71
|
+
sessionState: EditorSessionState,
|
|
72
|
+
): ReturnType<typeof validatePersistedEditorSnapshot> {
|
|
73
|
+
return validatePersistedEditorSnapshot(
|
|
74
|
+
persistedSnapshotFromEditorSessionState(sessionState, {
|
|
75
|
+
savedAt: sessionState.updatedAt,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function assertEditorSessionState(
|
|
81
|
+
sessionState: EditorSessionState,
|
|
82
|
+
): void {
|
|
83
|
+
assertPersistedEditorSnapshot(
|
|
84
|
+
persistedSnapshotFromEditorSessionState(sessionState, {
|
|
85
|
+
savedAt: sessionState.updatedAt,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -195,7 +195,7 @@ export interface EditorTransaction {
|
|
|
195
195
|
|
|
196
196
|
export interface CommandExecutionContext {
|
|
197
197
|
timestamp: string;
|
|
198
|
-
documentMode?: "editing" | "suggesting" | "viewing";
|
|
198
|
+
documentMode?: "editing" | "suggesting" | "viewing" | "commenting";
|
|
199
199
|
defaultAuthorId?: string;
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -1365,6 +1365,59 @@ function applySuggestingInsert(
|
|
|
1365
1365
|
result.mapping,
|
|
1366
1366
|
);
|
|
1367
1367
|
|
|
1368
|
+
// Extend an existing adjacent insertion revision if the cursor is right at its end.
|
|
1369
|
+
// After remapping, a prior insertion revision's `to` stays at `insertedFrom` (assoc.end = -1),
|
|
1370
|
+
// so consecutive keystrokes coalesce into one revision instead of one per character.
|
|
1371
|
+
const existingInsertion = findAdjacentInsertionRevision(
|
|
1372
|
+
reviewState.document.review.revisions,
|
|
1373
|
+
insertedFrom,
|
|
1374
|
+
authorId,
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
if (existingInsertion && existingInsertion.anchor.kind === "range") {
|
|
1378
|
+
const updatedRevision: CanonicalRevisionRecord = {
|
|
1379
|
+
...existingInsertion,
|
|
1380
|
+
anchor: {
|
|
1381
|
+
kind: "range",
|
|
1382
|
+
range: { from: existingInsertion.anchor.range.from, to: insertedTo },
|
|
1383
|
+
assoc: { start: 1, end: -1 },
|
|
1384
|
+
},
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const finalDocument: CanonicalDocumentEnvelope = {
|
|
1388
|
+
...reviewState.document,
|
|
1389
|
+
review: {
|
|
1390
|
+
...reviewState.document.review,
|
|
1391
|
+
revisions: {
|
|
1392
|
+
...reviewState.document.review.revisions,
|
|
1393
|
+
[updatedRevision.changeId]: updatedRevision,
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
return createTransaction(
|
|
1399
|
+
{
|
|
1400
|
+
...state,
|
|
1401
|
+
document: finalDocument,
|
|
1402
|
+
selection: result.selection,
|
|
1403
|
+
warnings: reviewState.warnings,
|
|
1404
|
+
runtime: {
|
|
1405
|
+
...state.runtime,
|
|
1406
|
+
activeCommentId: reviewState.activeCommentId,
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
historyBoundary: "push",
|
|
1411
|
+
markDirty: true,
|
|
1412
|
+
mapping: result.mapping,
|
|
1413
|
+
effects: {
|
|
1414
|
+
...reviewState.effects,
|
|
1415
|
+
revisionAuthored: { changeId: updatedRevision.changeId, kind: "insertion" },
|
|
1416
|
+
},
|
|
1417
|
+
},
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1368
1421
|
// Create the revision with pre-mapping positions — it refers to content
|
|
1369
1422
|
// that was just inserted, so its anchors are already correct in the new document
|
|
1370
1423
|
const revision = createAuthoredRevision(
|
|
@@ -1850,6 +1903,26 @@ function applySuggestingParagraphSplit(
|
|
|
1850
1903
|
);
|
|
1851
1904
|
}
|
|
1852
1905
|
|
|
1906
|
+
function findAdjacentInsertionRevision(
|
|
1907
|
+
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1908
|
+
cursorPos: number,
|
|
1909
|
+
authorId: string,
|
|
1910
|
+
): CanonicalRevisionRecord | undefined {
|
|
1911
|
+
for (const revision of Object.values(revisions)) {
|
|
1912
|
+
if (
|
|
1913
|
+
revision.kind === "insertion" &&
|
|
1914
|
+
revision.status === "open" &&
|
|
1915
|
+
revision.metadata?.source === "runtime" &&
|
|
1916
|
+
revision.anchor.kind === "range" &&
|
|
1917
|
+
revision.anchor.range.to === cursorPos &&
|
|
1918
|
+
revision.authorId === authorId
|
|
1919
|
+
) {
|
|
1920
|
+
return revision;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return undefined;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1853
1926
|
function findOverlappingAuthoredDeletion(
|
|
1854
1927
|
revisions: Record<string, CanonicalRevisionRecord>,
|
|
1855
1928
|
from: number,
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { WordReviewEditor } from "./ui/WordReviewEditor.tsx";
|
|
2
2
|
export {
|
|
3
3
|
createEditorSessionState,
|
|
4
|
+
assertEditorSessionState,
|
|
4
5
|
editorSessionStateFromPersistedSnapshot,
|
|
5
6
|
persistedSnapshotFromEditorSessionState,
|
|
7
|
+
validateEditorSessionState,
|
|
6
8
|
EDITOR_SESSION_STATE_VERSION,
|
|
7
9
|
} from "./api/session-state.ts";
|
|
8
10
|
export type {
|
package/src/io/docx-session.ts
CHANGED
|
@@ -9,6 +9,11 @@ import type {
|
|
|
9
9
|
PersistedEditorSnapshot,
|
|
10
10
|
ProtectionRange,
|
|
11
11
|
ProtectionSnapshot,
|
|
12
|
+
WorkflowMetadataSnapshot,
|
|
13
|
+
WorkflowOverlay,
|
|
14
|
+
WorkflowScope,
|
|
15
|
+
WorkflowScopeMetadataField,
|
|
16
|
+
WorkflowWorkItem,
|
|
12
17
|
} from "../api/public-types.ts";
|
|
13
18
|
import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
|
|
14
19
|
import type {
|
|
@@ -655,6 +660,12 @@ export function loadDocxEditorSession(
|
|
|
655
660
|
: undefined;
|
|
656
661
|
|
|
657
662
|
const timestamp = new Date().toISOString();
|
|
663
|
+
const translatedWorkflowState = translateClmCommentsToWorkflow({
|
|
664
|
+
comments: normalizedComments.threads,
|
|
665
|
+
workflowOverlay: embeddedWorkflowOverlay,
|
|
666
|
+
workflowMetadata: embeddedWorkflowMetadata,
|
|
667
|
+
timestamp,
|
|
668
|
+
});
|
|
658
669
|
const document = createImportedCanonicalDocument({
|
|
659
670
|
documentId: options.documentId,
|
|
660
671
|
timestamp,
|
|
@@ -703,7 +714,7 @@ export function loadDocxEditorSession(
|
|
|
703
714
|
errors: [],
|
|
704
715
|
},
|
|
705
716
|
review: {
|
|
706
|
-
comments: toRuntimeCommentRecords(
|
|
717
|
+
comments: toRuntimeCommentRecords(translatedWorkflowState.comments),
|
|
707
718
|
revisions: toRuntimeRevisionRecords([
|
|
708
719
|
...normalizedRevisions.revisions,
|
|
709
720
|
...importedStoryRevisions,
|
|
@@ -722,8 +733,8 @@ export function loadDocxEditorSession(
|
|
|
722
733
|
compatibility: toPublicCompatibilityReport(compatibility),
|
|
723
734
|
protectionSnapshot: importedProtectionSnapshot,
|
|
724
735
|
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
725
|
-
workflowOverlay:
|
|
726
|
-
workflowMetadata:
|
|
736
|
+
workflowOverlay: translatedWorkflowState.workflowOverlay,
|
|
737
|
+
workflowMetadata: translatedWorkflowState.workflowMetadata,
|
|
727
738
|
});
|
|
728
739
|
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
729
740
|
if (snapshotIssues.length > 0) {
|
|
@@ -1940,6 +1951,252 @@ function normalizeImportedCommentThreads(
|
|
|
1940
1951
|
};
|
|
1941
1952
|
}
|
|
1942
1953
|
|
|
1954
|
+
function translateClmCommentsToWorkflow(input: {
|
|
1955
|
+
comments: readonly CommentThread[];
|
|
1956
|
+
workflowOverlay?: WorkflowOverlay;
|
|
1957
|
+
workflowMetadata?: WorkflowMetadataSnapshot;
|
|
1958
|
+
timestamp: string;
|
|
1959
|
+
}): {
|
|
1960
|
+
comments: CommentThread[];
|
|
1961
|
+
workflowOverlay?: WorkflowOverlay;
|
|
1962
|
+
workflowMetadata?: WorkflowMetadataSnapshot;
|
|
1963
|
+
} {
|
|
1964
|
+
const nextComments = input.comments.map((thread) => structuredClone(thread));
|
|
1965
|
+
let nextOverlay = input.workflowOverlay ? structuredClone(input.workflowOverlay) : undefined;
|
|
1966
|
+
let overlayChanged = false;
|
|
1967
|
+
|
|
1968
|
+
for (const [index, thread] of nextComments.entries()) {
|
|
1969
|
+
const directive = parseClmCommentDirective(thread);
|
|
1970
|
+
if (!directive || thread.anchor.kind !== "range") {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (!nextOverlay) {
|
|
1975
|
+
nextOverlay = {
|
|
1976
|
+
overlayVersion: "workflow-overlay/1",
|
|
1977
|
+
scopes: [],
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
const existingScope = findExistingClmScope(nextOverlay.scopes, directive);
|
|
1982
|
+
if (!existingScope) {
|
|
1983
|
+
const version = getNextClmScopeVersion(nextOverlay.scopes, thread.anchor);
|
|
1984
|
+
const scopeId = `clm-scope-${thread.commentId}-v${version}`;
|
|
1985
|
+
const workItem = directive.tag === "TASK"
|
|
1986
|
+
? createClmWorkflowWorkItem(thread, directive.description, scopeId)
|
|
1987
|
+
: undefined;
|
|
1988
|
+
const scope = createClmWorkflowScope(thread, directive, version, scopeId, workItem?.workItemId);
|
|
1989
|
+
|
|
1990
|
+
nextOverlay.scopes = [...nextOverlay.scopes, scope];
|
|
1991
|
+
if (workItem) {
|
|
1992
|
+
nextOverlay.workItems = [...(nextOverlay.workItems ?? []), workItem];
|
|
1993
|
+
if (!nextOverlay.activeWorkItemId) {
|
|
1994
|
+
nextOverlay.activeWorkItemId = workItem.workItemId;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
overlayChanged = true;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
nextComments[index] = resolveImportedCommentThreadOnTranslation(thread, input.timestamp);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
return {
|
|
2004
|
+
comments: nextComments,
|
|
2005
|
+
workflowOverlay: overlayChanged || input.workflowOverlay ? nextOverlay : undefined,
|
|
2006
|
+
workflowMetadata: input.workflowMetadata,
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function parseClmCommentDirective(
|
|
2011
|
+
thread: CommentThread,
|
|
2012
|
+
): { tag: "TASK" | "READ" | "COMMENT" | "EDIT"; description: string; sourceCommentId: string; sourceCommentDurableId?: string; sourceCommentParaId?: string; mode: WorkflowScope["mode"] } | undefined {
|
|
2013
|
+
if (thread.metadata?.rootParaId === undefined && thread.entries[0]?.metadata?.parentParaId) {
|
|
2014
|
+
return undefined;
|
|
2015
|
+
}
|
|
2016
|
+
const firstMeaningfulLine = thread.entries
|
|
2017
|
+
.flatMap((entry) => entry.body.split(/\r?\n/u))
|
|
2018
|
+
.map((line) => line.trim())
|
|
2019
|
+
.find((line) => line.length > 0);
|
|
2020
|
+
if (!firstMeaningfulLine) {
|
|
2021
|
+
return undefined;
|
|
2022
|
+
}
|
|
2023
|
+
const match = /^CLM:([A-Z]+):(.*)$/u.exec(firstMeaningfulLine);
|
|
2024
|
+
if (!match) {
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
const tag = match[1] as "TASK" | "READ" | "COMMENT" | "EDIT";
|
|
2028
|
+
const description = (match[2] ?? "").trim();
|
|
2029
|
+
const mode = getClmWorkflowScopeMode(tag);
|
|
2030
|
+
if (!mode || description.length === 0) {
|
|
2031
|
+
return undefined;
|
|
2032
|
+
}
|
|
2033
|
+
return {
|
|
2034
|
+
tag,
|
|
2035
|
+
description,
|
|
2036
|
+
sourceCommentId: thread.metadata?.rootOoxmlCommentId ?? thread.commentId,
|
|
2037
|
+
sourceCommentDurableId: thread.entries[0]?.metadata?.durableId,
|
|
2038
|
+
sourceCommentParaId: thread.metadata?.rootParaId ?? thread.entries[0]?.metadata?.paraId,
|
|
2039
|
+
mode,
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function getClmWorkflowScopeMode(tag: string): WorkflowScope["mode"] | undefined {
|
|
2044
|
+
switch (tag) {
|
|
2045
|
+
case "TASK":
|
|
2046
|
+
return "suggest";
|
|
2047
|
+
case "READ":
|
|
2048
|
+
return "view";
|
|
2049
|
+
case "COMMENT":
|
|
2050
|
+
return "comment";
|
|
2051
|
+
case "EDIT":
|
|
2052
|
+
return "edit";
|
|
2053
|
+
default:
|
|
2054
|
+
return undefined;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function createClmWorkflowWorkItem(
|
|
2059
|
+
thread: CommentThread,
|
|
2060
|
+
description: string,
|
|
2061
|
+
scopeId: string,
|
|
2062
|
+
): WorkflowWorkItem {
|
|
2063
|
+
return {
|
|
2064
|
+
workItemId: `clm-task-${thread.commentId}`,
|
|
2065
|
+
title: description,
|
|
2066
|
+
description,
|
|
2067
|
+
status: "pending",
|
|
2068
|
+
scopeIds: [scopeId],
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function createClmWorkflowScope(
|
|
2073
|
+
thread: CommentThread,
|
|
2074
|
+
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
2075
|
+
version: number,
|
|
2076
|
+
scopeId: string,
|
|
2077
|
+
workItemId?: string,
|
|
2078
|
+
): WorkflowScope {
|
|
2079
|
+
if (workItemId) {
|
|
2080
|
+
return {
|
|
2081
|
+
scopeId,
|
|
2082
|
+
version,
|
|
2083
|
+
mode: directive.mode,
|
|
2084
|
+
anchor: toPublicAnchorProjection(thread.anchor),
|
|
2085
|
+
storyTarget: { kind: "main" },
|
|
2086
|
+
workItemId,
|
|
2087
|
+
label: directive.description,
|
|
2088
|
+
metadata: createClmScopeMetadata(directive),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
return {
|
|
2092
|
+
scopeId,
|
|
2093
|
+
version,
|
|
2094
|
+
mode: directive.mode,
|
|
2095
|
+
anchor: toPublicAnchorProjection(thread.anchor),
|
|
2096
|
+
storyTarget: { kind: "main" },
|
|
2097
|
+
label: directive.description,
|
|
2098
|
+
metadata: createClmScopeMetadata(directive),
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function createClmScopeMetadata(
|
|
2103
|
+
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
2104
|
+
): WorkflowScopeMetadataField[] {
|
|
2105
|
+
return [
|
|
2106
|
+
{
|
|
2107
|
+
key: "workblock.clm.tag",
|
|
2108
|
+
valueType: "string",
|
|
2109
|
+
value: directive.tag,
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
key: "workblock.clm.description",
|
|
2113
|
+
valueType: "string",
|
|
2114
|
+
value: directive.description,
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
key: "workblock.sourceCommentId",
|
|
2118
|
+
valueType: "string",
|
|
2119
|
+
value: directive.sourceCommentId,
|
|
2120
|
+
},
|
|
2121
|
+
...(directive.sourceCommentDurableId
|
|
2122
|
+
? [{
|
|
2123
|
+
key: "workblock.sourceCommentDurableId",
|
|
2124
|
+
valueType: "string" as const,
|
|
2125
|
+
value: directive.sourceCommentDurableId,
|
|
2126
|
+
}]
|
|
2127
|
+
: []),
|
|
2128
|
+
...(directive.sourceCommentParaId
|
|
2129
|
+
? [{
|
|
2130
|
+
key: "workblock.sourceCommentParaId",
|
|
2131
|
+
valueType: "string" as const,
|
|
2132
|
+
value: directive.sourceCommentParaId,
|
|
2133
|
+
}]
|
|
2134
|
+
: []),
|
|
2135
|
+
];
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function findExistingClmScope(
|
|
2139
|
+
scopes: readonly WorkflowScope[],
|
|
2140
|
+
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
2141
|
+
): WorkflowScope | undefined {
|
|
2142
|
+
return scopes.find((scope) => {
|
|
2143
|
+
const sourceCommentId = getWorkflowScopeMetadataValue(scope.metadata, "workblock.sourceCommentId");
|
|
2144
|
+
const sourceCommentDurableId = getWorkflowScopeMetadataValue(
|
|
2145
|
+
scope.metadata,
|
|
2146
|
+
"workblock.sourceCommentDurableId",
|
|
2147
|
+
);
|
|
2148
|
+
return (
|
|
2149
|
+
sourceCommentId === directive.sourceCommentId ||
|
|
2150
|
+
(directive.sourceCommentDurableId !== undefined &&
|
|
2151
|
+
sourceCommentDurableId === directive.sourceCommentDurableId)
|
|
2152
|
+
);
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function getWorkflowScopeMetadataValue(
|
|
2157
|
+
metadata: WorkflowScope["metadata"] | undefined,
|
|
2158
|
+
key: string,
|
|
2159
|
+
): string | undefined {
|
|
2160
|
+
const field = metadata?.find((entry) => entry.key === key);
|
|
2161
|
+
return typeof field?.value === "string" ? field.value : undefined;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function getNextClmScopeVersion(
|
|
2165
|
+
scopes: readonly WorkflowScope[],
|
|
2166
|
+
anchor: Extract<CommentThread["anchor"], { kind: "range" }>,
|
|
2167
|
+
): number {
|
|
2168
|
+
const anchorRange = {
|
|
2169
|
+
from: anchor.range.from,
|
|
2170
|
+
to: anchor.range.to,
|
|
2171
|
+
};
|
|
2172
|
+
const overlappingVersions = scopes.flatMap((scope) => {
|
|
2173
|
+
if (scope.anchor.kind !== "range") {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
return rangesIntersect(scope.anchor, anchorRange) && typeof scope.version === "number"
|
|
2177
|
+
? [scope.version]
|
|
2178
|
+
: [];
|
|
2179
|
+
});
|
|
2180
|
+
return overlappingVersions.length > 0 ? Math.max(...overlappingVersions) + 1 : 1;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function resolveImportedCommentThreadOnTranslation(
|
|
2184
|
+
thread: CommentThread,
|
|
2185
|
+
timestamp: string,
|
|
2186
|
+
): CommentThread {
|
|
2187
|
+
if (thread.status === "resolved") {
|
|
2188
|
+
return thread;
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
...thread,
|
|
2192
|
+
status: thread.status === "detached" ? "detached" : "resolved",
|
|
2193
|
+
resolution: thread.resolution ?? {
|
|
2194
|
+
resolvedAt: timestamp,
|
|
2195
|
+
resolvedBy: thread.entries[thread.entries.length - 1]?.authorId ?? thread.createdBy,
|
|
2196
|
+
},
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1943
2200
|
function resolveDefinitionRootCommentId(
|
|
1944
2201
|
definition: ImportedCommentDefinition,
|
|
1945
2202
|
definitions: readonly ImportedCommentDefinition[],
|
|
@@ -371,19 +371,46 @@ function buildWorkblockExtensionXml(workflowOverlay: WorkflowOverlay | undefined
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
function buildWorkflowScopeXml(scope: WorkflowScope): string {
|
|
374
|
+
const scopeMetadataXml = buildWorkflowScopeMetadataXml(scope.metadata);
|
|
374
375
|
return [
|
|
375
376
|
`<bw:scope`,
|
|
376
377
|
` id="${escapeXml(scope.scopeId)}"`,
|
|
378
|
+
typeof scope.version === "number" ? ` version="${String(scope.version)}"` : "",
|
|
377
379
|
` mode="${escapeXml(scope.mode)}"`,
|
|
378
380
|
scope.workItemId ? ` workItemRef="${escapeXml(scope.workItemId)}"` : "",
|
|
379
381
|
scope.label ? ` label="${escapeXml(scope.label)}"` : "",
|
|
380
382
|
scope.domain ? ` domain="${escapeXml(scope.domain)}"` : "",
|
|
381
383
|
`>`,
|
|
382
384
|
indentLines(buildWorkflowAnchorXml(scope.anchor, scope.storyTarget), 2),
|
|
385
|
+
scopeMetadataXml ? indentLines(scopeMetadataXml, 2) : "",
|
|
383
386
|
`</bw:scope>`,
|
|
384
387
|
].join("");
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
function buildWorkflowScopeMetadataXml(scopeMetadata: WorkflowScope["metadata"]): string {
|
|
391
|
+
if (!scopeMetadata || scopeMetadata.length === 0) {
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
return [
|
|
395
|
+
`<bw:scopeMetadata>`,
|
|
396
|
+
indentLines(
|
|
397
|
+
scopeMetadata
|
|
398
|
+
.map((field) => {
|
|
399
|
+
const serialized = serializeWorkflowScopeMetadataValue(field.valueType, field.value);
|
|
400
|
+
return [
|
|
401
|
+
`<bw:field`,
|
|
402
|
+
` key="${escapeXml(field.key)}"`,
|
|
403
|
+
serialized.valueType ? ` type="${escapeXml(serialized.valueType)}"` : "",
|
|
404
|
+
`>${escapeXml(serialized.text)}</bw:field>`,
|
|
405
|
+
].join("");
|
|
406
|
+
})
|
|
407
|
+
.join("\n"),
|
|
408
|
+
2,
|
|
409
|
+
),
|
|
410
|
+
`</bw:scopeMetadata>`,
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
|
|
387
414
|
function buildWorkflowAnchorXml(
|
|
388
415
|
anchor: EditorAnchorProjection,
|
|
389
416
|
storyTarget: WorkflowScope["storyTarget"],
|
|
@@ -563,15 +590,110 @@ function parseWorkflowScope(attributesSource: string, body: string): WorkflowSco
|
|
|
563
590
|
: createDefaultRangeAnchor(0, 0);
|
|
564
591
|
return {
|
|
565
592
|
scopeId: attributes.id,
|
|
593
|
+
version: attributes.version !== undefined ? Number(attributes.version) : undefined,
|
|
566
594
|
mode: attributes.mode as WorkflowScope["mode"],
|
|
567
595
|
anchor,
|
|
568
596
|
storyTarget: anchorMatch ? parseOverlayStoryTarget(parseAttributes(anchorMatch[1] ?? "")) : { kind: "main" },
|
|
569
597
|
workItemId: attributes.workItemRef,
|
|
570
598
|
label: attributes.label,
|
|
571
599
|
domain: attributes.domain as WorkflowScope["domain"],
|
|
600
|
+
metadata: parseWorkflowScopeMetadata(body),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function parseWorkflowScopeMetadata(body: string): WorkflowScope["metadata"] | undefined {
|
|
605
|
+
const metadataMatch = body.match(/<bw:scopeMetadata\b[^>]*>([\s\S]*?)<\/bw:scopeMetadata>/u);
|
|
606
|
+
if (!metadataMatch) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
const fields = [...(metadataMatch[1] ?? "").matchAll(/<bw:field\b([^>]*)>([\s\S]*?)<\/bw:field>/gu)]
|
|
610
|
+
.map((match) => parseWorkflowScopeMetadataField(match[1] ?? "", match[2] ?? ""))
|
|
611
|
+
.filter((field): field is NonNullable<WorkflowScope["metadata"]>[number] => field !== null);
|
|
612
|
+
return fields.length > 0 ? fields : undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function parseWorkflowScopeMetadataField(
|
|
616
|
+
attributesSource: string,
|
|
617
|
+
body: string,
|
|
618
|
+
): NonNullable<WorkflowScope["metadata"]>[number] | null {
|
|
619
|
+
const attributes = parseAttributes(attributesSource);
|
|
620
|
+
if (!attributes.key) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const valueType = attributes.type as NonNullable<WorkflowScope["metadata"]>[number]["valueType"] | undefined;
|
|
624
|
+
return {
|
|
625
|
+
key: attributes.key,
|
|
626
|
+
...(valueType ? { valueType } : {}),
|
|
627
|
+
value: parseWorkflowScopeMetadataValue(valueType, body),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function serializeWorkflowScopeMetadataValue(
|
|
632
|
+
valueType: NonNullable<WorkflowScope["metadata"]>[number]["valueType"],
|
|
633
|
+
value: NonNullable<WorkflowScope["metadata"]>[number]["value"],
|
|
634
|
+
): { valueType?: "string" | "number" | "boolean" | "json"; text: string } {
|
|
635
|
+
const resolvedValueType =
|
|
636
|
+
valueType ??
|
|
637
|
+
(typeof value === "number"
|
|
638
|
+
? "number"
|
|
639
|
+
: typeof value === "boolean"
|
|
640
|
+
? "boolean"
|
|
641
|
+
: typeof value === "string"
|
|
642
|
+
? "string"
|
|
643
|
+
: value && typeof value === "object"
|
|
644
|
+
? "json"
|
|
645
|
+
: undefined);
|
|
646
|
+
|
|
647
|
+
if (resolvedValueType === "json") {
|
|
648
|
+
return {
|
|
649
|
+
valueType: "json",
|
|
650
|
+
text: JSON.stringify(value ?? {}),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (resolvedValueType === "number") {
|
|
654
|
+
return {
|
|
655
|
+
valueType: "number",
|
|
656
|
+
text: typeof value === "number" ? String(value) : "0",
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (resolvedValueType === "boolean") {
|
|
660
|
+
return {
|
|
661
|
+
valueType: "boolean",
|
|
662
|
+
text: String(value === true),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
valueType: resolvedValueType,
|
|
667
|
+
text: typeof value === "string" ? value : value === undefined ? "" : String(value),
|
|
572
668
|
};
|
|
573
669
|
}
|
|
574
670
|
|
|
671
|
+
function parseWorkflowScopeMetadataValue(
|
|
672
|
+
valueType: NonNullable<WorkflowScope["metadata"]>[number]["valueType"],
|
|
673
|
+
payloadText: string,
|
|
674
|
+
): NonNullable<WorkflowScope["metadata"]>[number]["value"] {
|
|
675
|
+
const decodedText = decodeXmlEntities(payloadText);
|
|
676
|
+
if (valueType === "json") {
|
|
677
|
+
try {
|
|
678
|
+
const parsed = JSON.parse(decodedText) as unknown;
|
|
679
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
680
|
+
return parsed as Record<string, unknown>;
|
|
681
|
+
}
|
|
682
|
+
return { value: parsed };
|
|
683
|
+
} catch {
|
|
684
|
+
return { value: decodedText };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (valueType === "number") {
|
|
688
|
+
const numericValue = Number(decodedText);
|
|
689
|
+
return Number.isFinite(numericValue) ? numericValue : 0;
|
|
690
|
+
}
|
|
691
|
+
if (valueType === "boolean") {
|
|
692
|
+
return decodedText === "true" || decodedText === "1";
|
|
693
|
+
}
|
|
694
|
+
return decodedText;
|
|
695
|
+
}
|
|
696
|
+
|
|
575
697
|
function parseWorkflowWorkItem(attributesSource: string, body: string): WorkflowWorkItem | null {
|
|
576
698
|
const attributes = parseAttributes(attributesSource);
|
|
577
699
|
if (!attributes.id || !attributes.title) {
|
package/src/model/snapshot.ts
CHANGED
|
@@ -124,12 +124,20 @@ export interface PersistedWorkflowMetadataSnapshot {
|
|
|
124
124
|
|
|
125
125
|
export interface PersistedWorkflowScope {
|
|
126
126
|
scopeId: string;
|
|
127
|
+
version?: number;
|
|
127
128
|
mode: "edit" | "suggest" | "comment" | "view";
|
|
128
129
|
anchor: Record<string, unknown>;
|
|
129
130
|
storyTarget?: Record<string, unknown>;
|
|
130
131
|
workItemId?: string;
|
|
131
132
|
label?: string;
|
|
132
133
|
domain?: "legal" | "commercial" | "finance" | "other";
|
|
134
|
+
metadata?: PersistedWorkflowScopeMetadataField[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface PersistedWorkflowScopeMetadataField {
|
|
138
|
+
key: string;
|
|
139
|
+
valueType?: "string" | "number" | "boolean" | "json";
|
|
140
|
+
value?: string | number | boolean | Record<string, unknown>;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
export interface PersistedWorkflowWorkItem {
|
|
@@ -726,12 +734,62 @@ function validateWorkflowScope(
|
|
|
726
734
|
issues.push({ path: `${path}.mode`, message: "mode must be edit, suggest, comment, or view." });
|
|
727
735
|
}
|
|
728
736
|
asPlainObject(record.anchor, `${path}.anchor`, issues);
|
|
737
|
+
if (record.version !== undefined && !Number.isInteger(record.version)) {
|
|
738
|
+
issues.push({ path: `${path}.version`, message: "version must be an integer." });
|
|
739
|
+
}
|
|
729
740
|
if (record.workItemId !== undefined) {
|
|
730
741
|
expectString(record.workItemId, `${path}.workItemId`, issues);
|
|
731
742
|
}
|
|
732
743
|
if (record.label !== undefined) {
|
|
733
744
|
expectString(record.label, `${path}.label`, issues);
|
|
734
745
|
}
|
|
746
|
+
if (record.metadata !== undefined) {
|
|
747
|
+
if (!Array.isArray(record.metadata)) {
|
|
748
|
+
issues.push({ path: `${path}.metadata`, message: "metadata must be an array." });
|
|
749
|
+
} else {
|
|
750
|
+
record.metadata.forEach((field, index) =>
|
|
751
|
+
validateWorkflowScopeMetadataField(field, `${path}.metadata[${index}]`, issues),
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function validateWorkflowScopeMetadataField(
|
|
758
|
+
value: unknown,
|
|
759
|
+
path: string,
|
|
760
|
+
issues: ModelValidationIssue[],
|
|
761
|
+
): void {
|
|
762
|
+
const record = asPlainObject(value, path, issues);
|
|
763
|
+
if (!record) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
expectString(record.key, `${path}.key`, issues);
|
|
767
|
+
if (
|
|
768
|
+
record.valueType !== undefined &&
|
|
769
|
+
record.valueType !== "string" &&
|
|
770
|
+
record.valueType !== "number" &&
|
|
771
|
+
record.valueType !== "boolean" &&
|
|
772
|
+
record.valueType !== "json"
|
|
773
|
+
) {
|
|
774
|
+
issues.push({
|
|
775
|
+
path: `${path}.valueType`,
|
|
776
|
+
message: "valueType must be string, number, boolean, or json.",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (
|
|
780
|
+
record.value !== undefined &&
|
|
781
|
+
typeof record.value !== "string" &&
|
|
782
|
+
typeof record.value !== "number" &&
|
|
783
|
+
typeof record.value !== "boolean"
|
|
784
|
+
) {
|
|
785
|
+
const nestedRecord = asPlainObject(record.value, `${path}.value`, issues);
|
|
786
|
+
if (!nestedRecord) {
|
|
787
|
+
issues.push({
|
|
788
|
+
path: `${path}.value`,
|
|
789
|
+
message: "value must be a string, number, boolean, or plain object.",
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
735
793
|
}
|
|
736
794
|
|
|
737
795
|
function validateWorkflowWorkItem(
|
|
@@ -738,7 +738,7 @@ export function createDocumentRuntime(
|
|
|
738
738
|
});
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
-
if (viewState.documentMode === "viewing") {
|
|
741
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
|
|
742
742
|
reasons.push({
|
|
743
743
|
code: "document_viewing_mode",
|
|
744
744
|
message: "Document is in viewing mode.",
|
|
@@ -822,8 +822,8 @@ export function createDocumentRuntime(
|
|
|
822
822
|
function getEffectiveDocumentMode(
|
|
823
823
|
selection: EditorState["selection"],
|
|
824
824
|
): DocumentMode {
|
|
825
|
-
if (viewState.documentMode === "viewing") {
|
|
826
|
-
return
|
|
825
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
|
|
826
|
+
return viewState.documentMode;
|
|
827
827
|
}
|
|
828
828
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
829
829
|
if (matchingScope?.mode === "suggest") {
|
|
@@ -1529,13 +1529,13 @@ export function createDocumentRuntime(
|
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
1531
|
if (command.type === "history.undo") {
|
|
1532
|
-
if (viewState.documentMode === "viewing") return;
|
|
1532
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") return;
|
|
1533
1533
|
applyHistory("undo");
|
|
1534
1534
|
return;
|
|
1535
1535
|
}
|
|
1536
1536
|
|
|
1537
1537
|
if (command.type === "history.redo") {
|
|
1538
|
-
if (viewState.documentMode === "viewing") return;
|
|
1538
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") return;
|
|
1539
1539
|
applyHistory("redo");
|
|
1540
1540
|
return;
|
|
1541
1541
|
}
|
|
@@ -3949,10 +3949,11 @@ function resolveSupportedFieldDisplay(
|
|
|
3949
3949
|
if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
|
|
3950
3950
|
return undefined;
|
|
3951
3951
|
}
|
|
3952
|
+
if (field.fieldFamily === "TOC") {
|
|
3953
|
+
return undefined;
|
|
3954
|
+
}
|
|
3952
3955
|
if (!field.fieldTarget) {
|
|
3953
|
-
return
|
|
3954
|
-
? undefined
|
|
3955
|
-
: { displayText: "", refreshStatus: "unresolvable" };
|
|
3956
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
3956
3957
|
}
|
|
3957
3958
|
if (field.fieldFamily === "REF") {
|
|
3958
3959
|
const result = resolveRefFieldText(document, bookmarkMap, field.fieldTarget);
|
|
@@ -23,7 +23,7 @@ export interface SessionCapabilities {
|
|
|
23
23
|
|
|
24
24
|
// ── Document mode ──
|
|
25
25
|
/** Runtime document mode — editing authority, distinct from view/workspace mode. */
|
|
26
|
-
documentMode: "editing" | "suggesting" | "viewing";
|
|
26
|
+
documentMode: "editing" | "suggesting" | "viewing" | "commenting";
|
|
27
27
|
|
|
28
28
|
// ── Command capabilities ──
|
|
29
29
|
canUndo: boolean;
|
|
@@ -101,12 +101,15 @@ export function deriveCapabilities(
|
|
|
101
101
|
? "read-only-diagnostics"
|
|
102
102
|
: reviewMode;
|
|
103
103
|
|
|
104
|
-
// Command capabilities —
|
|
105
|
-
|
|
104
|
+
// Command capabilities — "viewing" and "commenting" modes both disable editing;
|
|
105
|
+
// "commenting" additionally keeps comment creation enabled.
|
|
106
|
+
const canEdit = isReady && !isReadOnly && !hasFatalError
|
|
107
|
+
&& documentMode !== "viewing" && documentMode !== "commenting";
|
|
106
108
|
const canUndo = snapshot.commandState.canUndo && canEdit;
|
|
107
109
|
const canRedo = snapshot.commandState.canRedo && canEdit;
|
|
110
|
+
const canComment = isReady && !hasFatalError && documentMode !== "viewing";
|
|
108
111
|
const canAddComment =
|
|
109
|
-
|
|
112
|
+
canComment &&
|
|
110
113
|
activeStory.kind === "main" &&
|
|
111
114
|
!snapshot.selection.isCollapsed &&
|
|
112
115
|
Boolean(snapshot.surface) &&
|
|
@@ -1427,6 +1427,14 @@ function describePreservedInlinePreview(
|
|
|
1427
1427
|
};
|
|
1428
1428
|
}
|
|
1429
1429
|
|
|
1430
|
+
if (/\b(?:w:)?br\b[^>]*\b(?:w:)?type="page"/u.test(payloadReference)) {
|
|
1431
|
+
return {
|
|
1432
|
+
label: "Page break",
|
|
1433
|
+
detail: "Word page-break marker preserved for export safety.",
|
|
1434
|
+
presentation: "quiet-marker",
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1430
1438
|
if (/\b(?:w:)?permStart\b/u.test(payloadReference)) {
|
|
1431
1439
|
const editorGroup = /\bw:edGrp="([^"]+)"/u.exec(payloadReference)?.[1];
|
|
1432
1440
|
return {
|
|
@@ -309,11 +309,11 @@ export function buildDecorations(
|
|
|
309
309
|
// This is the critical behavior: "hide tracked changes" must show
|
|
310
310
|
// the document as if accepted, not show deleted text as kept text.
|
|
311
311
|
if (markupDisplay === "clean" && rev.kind === "deletion") {
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
312
|
+
const cleanPmFrom = positionMap.runtimeToPm(rev.from);
|
|
313
|
+
const cleanPmTo = positionMap.runtimeToPm(rev.to);
|
|
314
|
+
if (cleanPmFrom < cleanPmTo) {
|
|
315
315
|
decorations.push(
|
|
316
|
-
Decoration.inline(
|
|
316
|
+
Decoration.inline(cleanPmFrom, cleanPmTo, {
|
|
317
317
|
class: "hidden",
|
|
318
318
|
"data-revision-id": rev.revisionId,
|
|
319
319
|
}),
|
|
@@ -322,13 +322,11 @@ export function buildDecorations(
|
|
|
322
322
|
continue;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
// Skip visual styling when tracked changes display is off
|
|
326
|
-
if (!showTrackedChanges) continue;
|
|
327
|
-
|
|
328
325
|
const pmFrom = positionMap.runtimeToPm(rev.from);
|
|
329
326
|
const pmTo = positionMap.runtimeToPm(rev.to);
|
|
330
327
|
if (pmFrom >= pmTo) continue;
|
|
331
328
|
|
|
329
|
+
// Suggestions styling is always shown regardless of showTrackedChanges toggle.
|
|
332
330
|
if (suggestionsEnabled) {
|
|
333
331
|
if (rev.kind === "insertion") {
|
|
334
332
|
decorations.push(
|
|
@@ -366,6 +364,9 @@ export function buildDecorations(
|
|
|
366
364
|
continue;
|
|
367
365
|
}
|
|
368
366
|
|
|
367
|
+
// Skip normal markup styling when tracked changes display is off
|
|
368
|
+
if (!showTrackedChanges) continue;
|
|
369
|
+
|
|
369
370
|
const cls = getRevisionHighlightClass(
|
|
370
371
|
revisionModel,
|
|
371
372
|
rev.from,
|
|
@@ -37,6 +37,7 @@ export function createSurfaceDocumentBuildKey(input: {
|
|
|
37
37
|
export function createSurfaceDecorationKey(input: {
|
|
38
38
|
markupDisplay: string;
|
|
39
39
|
showTrackedChanges: boolean;
|
|
40
|
+
suggestionsEnabled?: boolean;
|
|
40
41
|
canEdit: boolean;
|
|
41
42
|
activeCommentId?: string;
|
|
42
43
|
activeRevisionId?: string;
|
|
@@ -51,6 +52,7 @@ export function createSurfaceDecorationKey(input: {
|
|
|
51
52
|
return JSON.stringify({
|
|
52
53
|
markupDisplay: input.markupDisplay,
|
|
53
54
|
showTrackedChanges: input.showTrackedChanges,
|
|
55
|
+
suggestionsEnabled: input.suggestionsEnabled ?? false,
|
|
54
56
|
canEdit: input.canEdit,
|
|
55
57
|
activeCommentId: input.activeCommentId ?? null,
|
|
56
58
|
activeRevisionId: input.activeRevisionId ?? null,
|
|
@@ -223,6 +223,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
223
223
|
createSurfaceDecorationKey({
|
|
224
224
|
markupDisplay,
|
|
225
225
|
showTrackedChanges,
|
|
226
|
+
suggestionsEnabled,
|
|
226
227
|
canEdit,
|
|
227
228
|
activeCommentId: snapshot.comments.activeCommentId,
|
|
228
229
|
activeRevisionId: props.activeRevisionId,
|
|
@@ -246,6 +247,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
246
247
|
props.activeWorkflowScopeIds,
|
|
247
248
|
props.workflowScopes,
|
|
248
249
|
showTrackedChanges,
|
|
250
|
+
suggestionsEnabled,
|
|
249
251
|
snapshot.comments.activeCommentId,
|
|
250
252
|
],
|
|
251
253
|
);
|