@beyondwork/docx-react-component 1.0.82 → 1.0.84
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 +1 -1
- package/src/api/public-types.ts +48 -4
- package/src/api/v3/_runtime-handle.ts +4 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/runtime/document-runtime.ts +41 -14
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/session-bootstrap.ts +2 -0
- package/src/ui/WordReviewEditor.tsx +176 -1
- package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +32 -6
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +21 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +1 -0
|
@@ -393,6 +393,8 @@ export function __createWordReviewEditorRefBridge(
|
|
|
393
393
|
addInvisibleScope: (params) => runtime.addInvisibleScope(params),
|
|
394
394
|
setScopeVisibility: (scopeId, visibility) => runtime.setScopeVisibility(scopeId, visibility),
|
|
395
395
|
getScopeVisibility: (scopeId) => runtime.getScopeVisibility(scopeId),
|
|
396
|
+
setScopeGuardPolicy: (scopeId, guardPolicy) => runtime.setScopeGuardPolicy(scopeId, guardPolicy),
|
|
397
|
+
getScopeGuardPolicy: (scopeId) => runtime.getScopeGuardPolicy(scopeId),
|
|
396
398
|
setScopeChromeVisibility: (state) => runtime.setScopeChromeVisibility(state),
|
|
397
399
|
getScopeChromeVisibility: () => runtime.getScopeChromeVisibility(),
|
|
398
400
|
subscribeToScopeQuery: (filter, callback) => runtime.subscribeToScopeQuery(filter, callback),
|
|
@@ -1654,6 +1656,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1654
1656
|
addInvisibleScope: (params) => activeRuntime.addInvisibleScope(params),
|
|
1655
1657
|
setScopeVisibility: (scopeId, visibility) => activeRuntime.setScopeVisibility(scopeId, visibility),
|
|
1656
1658
|
getScopeVisibility: (scopeId) => activeRuntime.getScopeVisibility(scopeId),
|
|
1659
|
+
setScopeGuardPolicy: (scopeId, guardPolicy) => activeRuntime.setScopeGuardPolicy(scopeId, guardPolicy),
|
|
1660
|
+
getScopeGuardPolicy: (scopeId) => activeRuntime.getScopeGuardPolicy(scopeId),
|
|
1657
1661
|
setScopeChromeVisibility: (state) => activeRuntime.setScopeChromeVisibility(state),
|
|
1658
1662
|
getScopeChromeVisibility: () => activeRuntime.getScopeChromeVisibility(),
|
|
1659
1663
|
subscribeToScopeQuery: (filter, callback) => activeRuntime.subscribeToScopeQuery(filter, callback),
|
|
@@ -2414,9 +2418,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2414
2418
|
function addReviewComment(): string | null {
|
|
2415
2419
|
try {
|
|
2416
2420
|
const { commentId } = activeRuntime.addComment({
|
|
2417
|
-
anchor: snapshot
|
|
2421
|
+
anchor: resolveCommentCommandAnchor(snapshot),
|
|
2418
2422
|
body: "",
|
|
2419
2423
|
authorId: currentUser.userId,
|
|
2424
|
+
snapToSafeBoundary: true,
|
|
2420
2425
|
});
|
|
2421
2426
|
activeRuntime.openComment(commentId);
|
|
2422
2427
|
setActiveRailTab("comments");
|
|
@@ -4192,6 +4197,176 @@ function summarizeSelectionPreview(snapshot: RuntimeRenderSnapshot): string | nu
|
|
|
4192
4197
|
return preview.length > 48 ? `${preview.slice(0, 45)}...` : preview;
|
|
4193
4198
|
}
|
|
4194
4199
|
|
|
4200
|
+
function resolveCommentCommandAnchor(
|
|
4201
|
+
snapshot: RuntimeRenderSnapshot,
|
|
4202
|
+
): PublicSelectionSnapshot["activeRange"] {
|
|
4203
|
+
const selection = snapshot.selection;
|
|
4204
|
+
if (!selection.isCollapsed && selection.activeRange.kind === "range") {
|
|
4205
|
+
return selection.activeRange;
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
const collapsedRange = resolveCollapsedCommentRange(snapshot.surface, selection);
|
|
4209
|
+
return collapsedRange
|
|
4210
|
+
? {
|
|
4211
|
+
kind: "range",
|
|
4212
|
+
from: collapsedRange.from,
|
|
4213
|
+
to: collapsedRange.to,
|
|
4214
|
+
assoc: { start: -1, end: 1 },
|
|
4215
|
+
}
|
|
4216
|
+
: selection.activeRange;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
function resolveCollapsedCommentRange(
|
|
4220
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
4221
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
4222
|
+
): { from: number; to: number } | null {
|
|
4223
|
+
if (!surface) {
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
const position = selection.activeRange.kind === "node"
|
|
4228
|
+
? selection.activeRange.at
|
|
4229
|
+
: selection.head;
|
|
4230
|
+
const paragraph =
|
|
4231
|
+
findParagraphRangeAtPosition(surface.blocks, position) ??
|
|
4232
|
+
findFirstNonEmptyParagraphRange(surface.blocks);
|
|
4233
|
+
return paragraph
|
|
4234
|
+
? resolveWordRangeInsideParagraph(surface.plainText, paragraph, position)
|
|
4235
|
+
: null;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
function findParagraphRangeAtPosition(
|
|
4239
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
4240
|
+
position: number,
|
|
4241
|
+
): { from: number; to: number } | null {
|
|
4242
|
+
for (const block of blocks) {
|
|
4243
|
+
if (
|
|
4244
|
+
block.kind === "paragraph" &&
|
|
4245
|
+
block.to > block.from &&
|
|
4246
|
+
position >= block.from &&
|
|
4247
|
+
position <= block.to
|
|
4248
|
+
) {
|
|
4249
|
+
return { from: block.from, to: block.to };
|
|
4250
|
+
}
|
|
4251
|
+
const nested = findParagraphRangeInNestedBlocks(block, position);
|
|
4252
|
+
if (nested) {
|
|
4253
|
+
return nested;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
return null;
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
function findFirstNonEmptyParagraphRange(
|
|
4260
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
4261
|
+
): { from: number; to: number } | null {
|
|
4262
|
+
for (const block of blocks) {
|
|
4263
|
+
if (block.kind === "paragraph" && block.to > block.from) {
|
|
4264
|
+
return { from: block.from, to: block.to };
|
|
4265
|
+
}
|
|
4266
|
+
const nested = findFirstNonEmptyParagraphInNestedBlocks(block);
|
|
4267
|
+
if (nested) {
|
|
4268
|
+
return nested;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
return null;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
function findParagraphRangeInNestedBlocks(
|
|
4275
|
+
block: SurfaceBlockSnapshot,
|
|
4276
|
+
position: number,
|
|
4277
|
+
): { from: number; to: number } | null {
|
|
4278
|
+
if (block.kind === "sdt_block") {
|
|
4279
|
+
return findParagraphRangeAtPosition(block.children, position);
|
|
4280
|
+
}
|
|
4281
|
+
if (block.kind === "table") {
|
|
4282
|
+
for (const row of block.rows) {
|
|
4283
|
+
for (const cell of row.cells) {
|
|
4284
|
+
const nested = findParagraphRangeAtPosition(cell.content, position);
|
|
4285
|
+
if (nested) {
|
|
4286
|
+
return nested;
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
return null;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
function findFirstNonEmptyParagraphInNestedBlocks(
|
|
4295
|
+
block: SurfaceBlockSnapshot,
|
|
4296
|
+
): { from: number; to: number } | null {
|
|
4297
|
+
if (block.kind === "sdt_block") {
|
|
4298
|
+
return findFirstNonEmptyParagraphRange(block.children);
|
|
4299
|
+
}
|
|
4300
|
+
if (block.kind === "table") {
|
|
4301
|
+
for (const row of block.rows) {
|
|
4302
|
+
for (const cell of row.cells) {
|
|
4303
|
+
const nested = findFirstNonEmptyParagraphRange(cell.content);
|
|
4304
|
+
if (nested) {
|
|
4305
|
+
return nested;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
return null;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
function resolveWordRangeInsideParagraph(
|
|
4314
|
+
plainText: string,
|
|
4315
|
+
paragraph: { from: number; to: number },
|
|
4316
|
+
position: number,
|
|
4317
|
+
): { from: number; to: number } {
|
|
4318
|
+
const paragraphFrom = paragraph.from;
|
|
4319
|
+
const paragraphTo = paragraph.to;
|
|
4320
|
+
const fallback = { from: paragraphFrom, to: paragraphTo };
|
|
4321
|
+
if (paragraphTo <= paragraphFrom) {
|
|
4322
|
+
return fallback;
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
let cursor = Math.max(paragraphFrom, Math.min(position, paragraphTo - 1));
|
|
4326
|
+
if (isCommentWordBoundary(plainText.charAt(cursor))) {
|
|
4327
|
+
const next = findNearestCommentTextOffset(plainText, cursor, paragraphFrom, paragraphTo);
|
|
4328
|
+
if (next === null) {
|
|
4329
|
+
return fallback;
|
|
4330
|
+
}
|
|
4331
|
+
cursor = next;
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
let from = cursor;
|
|
4335
|
+
while (from > paragraphFrom && !isCommentWordBoundary(plainText.charAt(from - 1))) {
|
|
4336
|
+
from -= 1;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
let to = cursor + 1;
|
|
4340
|
+
while (to < paragraphTo && !isCommentWordBoundary(plainText.charAt(to))) {
|
|
4341
|
+
to += 1;
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
return to > from ? { from, to } : fallback;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function findNearestCommentTextOffset(
|
|
4348
|
+
plainText: string,
|
|
4349
|
+
cursor: number,
|
|
4350
|
+
from: number,
|
|
4351
|
+
to: number,
|
|
4352
|
+
): number | null {
|
|
4353
|
+
for (let distance = 1; distance < to - from; distance += 1) {
|
|
4354
|
+
const right = cursor + distance;
|
|
4355
|
+
if (right < to && !isCommentWordBoundary(plainText.charAt(right))) {
|
|
4356
|
+
return right;
|
|
4357
|
+
}
|
|
4358
|
+
const left = cursor - distance;
|
|
4359
|
+
if (left >= from && !isCommentWordBoundary(plainText.charAt(left))) {
|
|
4360
|
+
return left;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
return null;
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
function isCommentWordBoundary(value: string): boolean {
|
|
4367
|
+
return value.length === 0 || /\s/.test(value);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4195
4370
|
function selectionToolbarAnchorsEqual(
|
|
4196
4371
|
left: SelectionToolbarAnchor | null,
|
|
4197
4372
|
right: SelectionToolbarAnchor | null,
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
ActiveListContext,
|
|
5
|
+
EditorStoryTarget,
|
|
5
6
|
EditorViewStateSnapshot,
|
|
6
7
|
InteractionGuardSnapshot,
|
|
7
8
|
WordReviewEditorChromePreset,
|
|
@@ -24,6 +25,7 @@ export interface UseChromePolicyOptions {
|
|
|
24
25
|
role: EditorViewStateSnapshot["editorRole"];
|
|
25
26
|
hasSidebarPanelAccess: boolean;
|
|
26
27
|
effectiveSelectionMode: ToolbarInteractionPolicy["mode"];
|
|
28
|
+
activeStoryKind?: EditorStoryTarget["kind"];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface ChromePolicy {
|
|
@@ -52,8 +54,22 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
|
|
|
52
54
|
role,
|
|
53
55
|
hasSidebarPanelAccess,
|
|
54
56
|
effectiveSelectionMode,
|
|
57
|
+
activeStoryKind,
|
|
55
58
|
} = options;
|
|
56
59
|
|
|
60
|
+
const canRunCommentCommand = resolveTopBarCommentAvailability({
|
|
61
|
+
caps,
|
|
62
|
+
effectiveSelectionMode,
|
|
63
|
+
activeStoryKind,
|
|
64
|
+
});
|
|
65
|
+
const chromeCaps = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
caps && caps.canAddComment !== canRunCommentCommand
|
|
68
|
+
? { ...caps, canAddComment: canRunCommentCommand }
|
|
69
|
+
: caps,
|
|
70
|
+
[canRunCommentCommand, caps],
|
|
71
|
+
);
|
|
72
|
+
|
|
57
73
|
const responsiveChrome = useMemo(
|
|
58
74
|
() =>
|
|
59
75
|
resolveResponsiveChromeState({
|
|
@@ -69,7 +85,7 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
|
|
|
69
85
|
resolveScopedChromePolicy({
|
|
70
86
|
preset: chromePreset,
|
|
71
87
|
compactMode: responsiveChrome.isNarrow,
|
|
72
|
-
capabilities:
|
|
88
|
+
capabilities: chromeCaps,
|
|
73
89
|
interactionGuardSnapshot,
|
|
74
90
|
workflowScopeSnapshot,
|
|
75
91
|
activeListContext,
|
|
@@ -77,7 +93,7 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
|
|
|
77
93
|
hasSidebarPanelAccess,
|
|
78
94
|
}),
|
|
79
95
|
[
|
|
80
|
-
|
|
96
|
+
chromeCaps,
|
|
81
97
|
chromePreset,
|
|
82
98
|
hasSidebarPanelAccess,
|
|
83
99
|
activeListContext,
|
|
@@ -93,12 +109,22 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
|
|
|
93
109
|
mode: effectiveSelectionMode,
|
|
94
110
|
canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
|
|
95
111
|
canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
|
|
96
|
-
canAddComment:
|
|
97
|
-
caps.canAddComment &&
|
|
98
|
-
effectiveSelectionMode !== "view" &&
|
|
99
|
-
effectiveSelectionMode !== "blocked",
|
|
112
|
+
canAddComment: canRunCommentCommand,
|
|
100
113
|
}
|
|
101
114
|
: undefined;
|
|
102
115
|
|
|
103
116
|
return { responsiveChrome, scopedChromePolicy, toolbarInteractionPolicy };
|
|
104
117
|
}
|
|
118
|
+
|
|
119
|
+
export function resolveTopBarCommentAvailability(input: {
|
|
120
|
+
caps: SessionCapabilities | undefined;
|
|
121
|
+
effectiveSelectionMode: ToolbarInteractionPolicy["mode"];
|
|
122
|
+
activeStoryKind?: EditorStoryTarget["kind"];
|
|
123
|
+
}): boolean {
|
|
124
|
+
const { caps, effectiveSelectionMode, activeStoryKind } = input;
|
|
125
|
+
if (!caps) return false;
|
|
126
|
+
if (caps.phase !== "ready" || caps.hasFatalError) return false;
|
|
127
|
+
if (caps.documentMode === "viewing") return false;
|
|
128
|
+
if (activeStoryKind !== undefined && activeStoryKind !== "main") return false;
|
|
129
|
+
return effectiveSelectionMode !== "view" && effectiveSelectionMode !== "blocked";
|
|
130
|
+
}
|
|
@@ -139,6 +139,7 @@ export function TwRoleActionRegion(
|
|
|
139
139
|
for (const id of order) {
|
|
140
140
|
const entry = props.policy.toolbar[id];
|
|
141
141
|
if (!entry?.visible) continue;
|
|
142
|
+
if (!isRoleActionRenderable(id, props)) continue;
|
|
142
143
|
if (entry.placement === "overflow") {
|
|
143
144
|
overflowIds.push(id);
|
|
144
145
|
} else if (entry.placement === "inline") {
|
|
@@ -171,6 +172,26 @@ export function TwRoleActionRegion(
|
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
function isRoleActionRenderable(
|
|
176
|
+
id: ToolbarChromeItemId,
|
|
177
|
+
props: TwRoleActionRegionProps,
|
|
178
|
+
): boolean {
|
|
179
|
+
const reviewQueueTotal = props.reviewQueue?.totalCount ?? 0;
|
|
180
|
+
switch (id) {
|
|
181
|
+
case "review-queue-prev":
|
|
182
|
+
case "review-queue-next":
|
|
183
|
+
case "review-queue-counts":
|
|
184
|
+
case "review-queue-active-label":
|
|
185
|
+
case "review-accept":
|
|
186
|
+
case "review-reject":
|
|
187
|
+
case "review-accept-all":
|
|
188
|
+
case "review-reject-all":
|
|
189
|
+
return reviewQueueTotal > 0;
|
|
190
|
+
default:
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
174
195
|
interface RoleActionButtonProps {
|
|
175
196
|
id: ToolbarChromeItemId;
|
|
176
197
|
compact: boolean;
|
|
@@ -357,6 +357,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
357
357
|
role: viewState.editorRole,
|
|
358
358
|
hasSidebarPanelAccess,
|
|
359
359
|
effectiveSelectionMode,
|
|
360
|
+
activeStoryKind: snapshot.activeStory.kind,
|
|
360
361
|
});
|
|
361
362
|
|
|
362
363
|
// L7 Phase 2 Task 2.2.4a — viewport-scroll wiring. Page marker
|