@beyondwork/docx-react-component 1.0.85 → 1.0.86
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/ui/WordReviewEditor.tsx +104 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +46 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +9 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.86",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -1062,7 +1062,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1062
1062
|
} = props;
|
|
1063
1063
|
|
|
1064
1064
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
1065
|
-
const [showTrackedChanges, setShowTrackedChanges] = useState(() => suggestionsEnabled);
|
|
1066
1065
|
const [localMarkupDisplay, setLocalMarkupDisplay] =
|
|
1067
1066
|
useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
|
|
1068
1067
|
const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
|
|
@@ -1368,6 +1367,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1368
1367
|
const isPageWorkspace = viewState.workspaceMode === "page";
|
|
1369
1368
|
const liveMarkupDisplay = localMarkupDisplay ??
|
|
1370
1369
|
__resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
|
|
1370
|
+
const trackedChangesAuthoringEnabled = viewState.documentMode === "suggesting";
|
|
1371
|
+
const showTrackedChanges = toWorkflowMarkupMode(liveMarkupDisplay) !== "clean";
|
|
1371
1372
|
const documentNavigation = useRuntimeValue(
|
|
1372
1373
|
runtime
|
|
1373
1374
|
? {
|
|
@@ -1510,16 +1511,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1510
1511
|
const setReviewMarkupMode = useCallback((mode: MarkupDisplay) => {
|
|
1511
1512
|
const workflowMode = toWorkflowMarkupMode(mode);
|
|
1512
1513
|
setLocalMarkupDisplay(workflowMode);
|
|
1513
|
-
setShowTrackedChanges(workflowMode !== "clean");
|
|
1514
1514
|
api.ui?.viewport.setLocalMarkupMode(workflowMode);
|
|
1515
1515
|
}, [api]);
|
|
1516
1516
|
|
|
1517
1517
|
const setTrackedChangesAuthoring = useCallback((enabled: boolean) => {
|
|
1518
|
-
const workflowMode: WorkflowMarkupMode = enabled ? "all" : "clean";
|
|
1519
|
-
setShowTrackedChanges(enabled);
|
|
1520
|
-
setLocalMarkupDisplay(workflowMode);
|
|
1521
1518
|
api.runtime.document.setMode(enabled ? "suggesting" : "editing");
|
|
1522
|
-
|
|
1519
|
+
if (enabled) {
|
|
1520
|
+
setLocalMarkupDisplay("all");
|
|
1521
|
+
api.ui?.viewport.setLocalMarkupMode("all");
|
|
1522
|
+
}
|
|
1523
1523
|
}, [api]);
|
|
1524
1524
|
|
|
1525
1525
|
useEffect(() => {
|
|
@@ -1527,7 +1527,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1527
1527
|
setTrackedChangesAuthoring(true);
|
|
1528
1528
|
return;
|
|
1529
1529
|
}
|
|
1530
|
-
setShowTrackedChanges(false);
|
|
1531
1530
|
api.runtime.document.setMode("editing");
|
|
1532
1531
|
}, [api, setTrackedChangesAuthoring, suggestionsEnabled]);
|
|
1533
1532
|
|
|
@@ -2467,13 +2466,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2467
2466
|
|
|
2468
2467
|
function addReviewComment(): string | null {
|
|
2469
2468
|
try {
|
|
2469
|
+
const currentSnapshot = activeRuntime.getRenderSnapshot();
|
|
2470
2470
|
const { commentId } = activeRuntime.addComment({
|
|
2471
|
-
anchor: resolveCommentCommandAnchor(
|
|
2471
|
+
anchor: resolveCommentCommandAnchor(currentSnapshot),
|
|
2472
2472
|
body: "",
|
|
2473
2473
|
authorId: currentUser.userId,
|
|
2474
2474
|
snapToSafeBoundary: true,
|
|
2475
2475
|
});
|
|
2476
2476
|
activeRuntime.openComment(commentId);
|
|
2477
|
+
setActiveReviewQueueItemId(`comment:${commentId}`);
|
|
2477
2478
|
setActiveRailTab("comments");
|
|
2478
2479
|
return commentId;
|
|
2479
2480
|
} catch {
|
|
@@ -2814,11 +2815,49 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2814
2815
|
}, [
|
|
2815
2816
|
activeRuntime,
|
|
2816
2817
|
activeSelectionTool,
|
|
2818
|
+
addReviewComment,
|
|
2817
2819
|
currentUser.userId,
|
|
2818
2820
|
dismissSelectionToolbar,
|
|
2819
2821
|
focusDocumentSurface,
|
|
2820
2822
|
]);
|
|
2821
2823
|
|
|
2824
|
+
const acceptActiveSuggestion = useCallback(() => {
|
|
2825
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
for (const changeId of activeSelectionTool.changeIds) {
|
|
2829
|
+
activeRuntime.acceptChange(changeId);
|
|
2830
|
+
}
|
|
2831
|
+
dismissSelectionToolbar("chrome-action");
|
|
2832
|
+
}, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
|
|
2833
|
+
|
|
2834
|
+
const rejectActiveSuggestion = useCallback(() => {
|
|
2835
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
for (const changeId of activeSelectionTool.changeIds) {
|
|
2839
|
+
activeRuntime.rejectChange(changeId);
|
|
2840
|
+
}
|
|
2841
|
+
dismissSelectionToolbar("chrome-action");
|
|
2842
|
+
}, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
|
|
2843
|
+
|
|
2844
|
+
const editActiveSuggestion = useCallback(() => {
|
|
2845
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
|
|
2849
|
+
const activeSuggestion = suggestionsSnapshot.suggestions.find(
|
|
2850
|
+
(suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
|
|
2851
|
+
);
|
|
2852
|
+
if (activeSuggestion) {
|
|
2853
|
+
applyRuntimeSelection(
|
|
2854
|
+
activeRuntime,
|
|
2855
|
+
createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
setSelectionToolbarFocusWithin(true);
|
|
2859
|
+
}, [activeRuntime, activeSelectionTool, suggestionsSnapshot.suggestions]);
|
|
2860
|
+
|
|
2822
2861
|
const handleSelectionToolbarAnchorChange = useCallback(
|
|
2823
2862
|
(nextAnchor: SelectionToolbarAnchor | null) => {
|
|
2824
2863
|
setSelectionToolbarAnchor((current) =>
|
|
@@ -3209,6 +3248,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3209
3248
|
focusAnchor(revision.anchor, revision.storyTarget);
|
|
3210
3249
|
setActiveRailTab("changes");
|
|
3211
3250
|
},
|
|
3251
|
+
onReplyToRevision: (revision: typeof snapshot.trackedChanges.revisions[number]) => {
|
|
3252
|
+
const linkedThread = activeRuntime.ensureCommentThreadForChange(
|
|
3253
|
+
revision.revisionId,
|
|
3254
|
+
currentUser.userId,
|
|
3255
|
+
);
|
|
3256
|
+
if (!linkedThread?.commentId) {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
setActiveRevisionId(revision.revisionId);
|
|
3260
|
+
focusAnchor(revision.anchor, revision.storyTarget);
|
|
3261
|
+
activeRuntime.openComment(linkedThread.commentId);
|
|
3262
|
+
setActiveRailTab("comments");
|
|
3263
|
+
},
|
|
3212
3264
|
onAcceptRevision: (revisionId: string) => {
|
|
3213
3265
|
activeRuntime.acceptChange(revisionId);
|
|
3214
3266
|
setActiveRailTab("changes");
|
|
@@ -3237,6 +3289,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3237
3289
|
onActiveRailTabChange: setActiveRailTab,
|
|
3238
3290
|
onShowTrackedChangesChange: setTrackedChangesAuthoring,
|
|
3239
3291
|
onReviewMarkupModeChange: setReviewMarkupMode,
|
|
3292
|
+
onChromePinChange: (surface, pin) => activeRuntime.setChromePin(surface, pin),
|
|
3240
3293
|
onToggleBold: () =>
|
|
3241
3294
|
applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
|
|
3242
3295
|
onToggleItalic: () =>
|
|
@@ -3449,6 +3502,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3449
3502
|
onInsertPageBreak: commands.onInsertPageBreak,
|
|
3450
3503
|
onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
|
|
3451
3504
|
onInsertTable: commands.onInsertTable,
|
|
3505
|
+
onInsertImage: commands.onInsertImage
|
|
3506
|
+
? () => requestImageInsertFromPicker(commands.onInsertImage)
|
|
3507
|
+
: undefined,
|
|
3452
3508
|
onAddComment: commands.onAddComment,
|
|
3453
3509
|
onFindRequested: onFindRequested
|
|
3454
3510
|
? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
|
|
@@ -3592,6 +3648,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3592
3648
|
activeRailTab={activeRailTab}
|
|
3593
3649
|
activeCommentId={snapshot.comments.activeCommentId}
|
|
3594
3650
|
activeRevisionId={activeRevisionId}
|
|
3651
|
+
trackedChangesAuthoringEnabled={trackedChangesAuthoringEnabled}
|
|
3595
3652
|
showTrackedChanges={showTrackedChanges}
|
|
3596
3653
|
workflowScopeSnapshot={workflowScopeSnapshot}
|
|
3597
3654
|
layoutFacet={activeRuntime.layout}
|
|
@@ -3626,35 +3683,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3626
3683
|
onAddCommentFromSelection={addSelectionToolbarComment}
|
|
3627
3684
|
onAddCommentFromSuggestion={addSelectionToolbarComment}
|
|
3628
3685
|
onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3629
|
-
?
|
|
3630
|
-
for (const changeId of activeSelectionTool.changeIds) {
|
|
3631
|
-
activeRuntime.acceptChange(changeId);
|
|
3632
|
-
}
|
|
3633
|
-
dismissSelectionToolbar("chrome-action");
|
|
3634
|
-
}
|
|
3686
|
+
? acceptActiveSuggestion
|
|
3635
3687
|
: undefined}
|
|
3636
3688
|
onRejectSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3637
|
-
?
|
|
3638
|
-
for (const changeId of activeSelectionTool.changeIds) {
|
|
3639
|
-
activeRuntime.rejectChange(changeId);
|
|
3640
|
-
}
|
|
3641
|
-
dismissSelectionToolbar("chrome-action");
|
|
3642
|
-
}
|
|
3689
|
+
? rejectActiveSuggestion
|
|
3643
3690
|
: undefined}
|
|
3644
3691
|
onEditSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3645
|
-
?
|
|
3646
|
-
setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
|
|
3647
|
-
const activeSuggestion = suggestionsSnapshot.suggestions.find(
|
|
3648
|
-
(suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
|
|
3649
|
-
);
|
|
3650
|
-
if (activeSuggestion) {
|
|
3651
|
-
applyRuntimeSelection(
|
|
3652
|
-
activeRuntime,
|
|
3653
|
-
createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
|
|
3654
|
-
);
|
|
3655
|
-
}
|
|
3656
|
-
setSelectionToolbarFocusWithin(true);
|
|
3657
|
-
}
|
|
3692
|
+
? editActiveSuggestion
|
|
3658
3693
|
: undefined}
|
|
3659
3694
|
onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
|
|
3660
3695
|
onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
|
|
@@ -4296,6 +4331,42 @@ function resolveCommentCommandAnchor(
|
|
|
4296
4331
|
: selection.activeRange;
|
|
4297
4332
|
}
|
|
4298
4333
|
|
|
4334
|
+
function requestImageInsertFromPicker(
|
|
4335
|
+
onInsertImage: ((options: InsertImageOptions) => void) | undefined,
|
|
4336
|
+
): void {
|
|
4337
|
+
const document = globalThis.document;
|
|
4338
|
+
if (!onInsertImage || typeof document?.createElement !== "function" || !document.body) {
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
const input = document.createElement("input");
|
|
4343
|
+
input.type = "file";
|
|
4344
|
+
input.accept = "image/png,image/jpeg,image/gif";
|
|
4345
|
+
input.style.position = "fixed";
|
|
4346
|
+
input.style.left = "-9999px";
|
|
4347
|
+
input.style.top = "-9999px";
|
|
4348
|
+
input.addEventListener("change", () => {
|
|
4349
|
+
const file = input.files?.[0];
|
|
4350
|
+
if (!file) {
|
|
4351
|
+
input.remove();
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
void file.arrayBuffer()
|
|
4355
|
+
.then((buffer) => {
|
|
4356
|
+
onInsertImage({
|
|
4357
|
+
data: new Uint8Array(buffer),
|
|
4358
|
+
mimeType: file.type || "image/png",
|
|
4359
|
+
altText: file.name,
|
|
4360
|
+
});
|
|
4361
|
+
})
|
|
4362
|
+
.finally(() => {
|
|
4363
|
+
input.remove();
|
|
4364
|
+
});
|
|
4365
|
+
}, { once: true });
|
|
4366
|
+
document.body.appendChild(input);
|
|
4367
|
+
input.click();
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4299
4370
|
function resolveCollapsedCommentRange(
|
|
4300
4371
|
surface: RuntimeRenderSnapshot["surface"],
|
|
4301
4372
|
selection: RuntimeRenderSnapshot["selection"],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
ChromePinSurface,
|
|
4
5
|
CommentSidebarThreadSnapshot,
|
|
5
6
|
EditorStoryTarget,
|
|
6
7
|
FormattingAlignment,
|
|
@@ -9,6 +10,7 @@ import type {
|
|
|
9
10
|
SectionBreakType,
|
|
10
11
|
SectionLayoutPatch,
|
|
11
12
|
SectionPageNumberingPatch,
|
|
13
|
+
PinState,
|
|
12
14
|
TrackedChangeEntrySnapshot,
|
|
13
15
|
ZoomLevel,
|
|
14
16
|
WorkspaceMode,
|
|
@@ -24,6 +26,7 @@ export interface EditorCommandBag {
|
|
|
24
26
|
onActiveRailTabChange(value: ReviewRailTab): void;
|
|
25
27
|
onShowTrackedChangesChange(show: boolean): void;
|
|
26
28
|
onReviewMarkupModeChange?(mode: MarkupDisplay): void;
|
|
29
|
+
onChromePinChange?(surface: ChromePinSurface, pin: PinState | null): void;
|
|
27
30
|
onUndo(): void;
|
|
28
31
|
onRedo(): void;
|
|
29
32
|
onSetParagraphStyle?(styleId: string): void;
|
|
@@ -85,6 +88,7 @@ export interface EditorCommandBag {
|
|
|
85
88
|
onAddReply?(commentId: string, body: string): void;
|
|
86
89
|
onEditBody?(commentId: string, body: string): void;
|
|
87
90
|
onOpenRevision(revision: TrackedChangeEntrySnapshot): void;
|
|
91
|
+
onReplyToRevision?(revision: TrackedChangeEntrySnapshot): void;
|
|
88
92
|
onAcceptRevision(revisionId: string): void;
|
|
89
93
|
onRejectRevision(revisionId: string): void;
|
|
90
94
|
onAcceptAllChanges(): void;
|
|
@@ -55,6 +55,7 @@ export interface EditorShellViewProps {
|
|
|
55
55
|
activeRailTab: ReviewRailTab;
|
|
56
56
|
activeCommentId?: string;
|
|
57
57
|
activeRevisionId?: string;
|
|
58
|
+
trackedChangesAuthoringEnabled?: boolean;
|
|
58
59
|
showTrackedChanges: boolean;
|
|
59
60
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
60
61
|
/**
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
* contextual actions exist, keyed by `TargetKind`.
|
|
4
4
|
*
|
|
5
5
|
* Consumed by three access routes so DESIGN-EDITOR.md §6.4 holds by
|
|
6
|
-
* construction
|
|
7
|
-
* cannot duplicate command trees"):
|
|
6
|
+
* construction for general editor actions:
|
|
8
7
|
*
|
|
9
8
|
* 1. Right-click context menu (via `build-context-menu-entries.ts`)
|
|
10
9
|
* 2. Inline "More…" affordance on reduced floating toolbars (Phase D)
|
|
11
10
|
* 3. Command palette groups (Phase C.δ)
|
|
12
11
|
*
|
|
13
|
-
* All three routes produce the same actions from this registry
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* All three routes produce the same general actions from this registry.
|
|
13
|
+
* Suggestion-specific review actions are intentionally absent for now:
|
|
14
|
+
* they require an exact revision/suggestion id payload, so they live on
|
|
15
|
+
* the floating suggestion card and Changes rail until the context-menu
|
|
16
|
+
* target resolver can supply that payload.
|
|
16
17
|
*
|
|
17
18
|
* Perf discipline: pure data + pure filter helpers. No DOM reads, no
|
|
18
19
|
* runtime calls, no observers. Actions are dispatched through host-
|
|
@@ -106,10 +107,6 @@ export interface EditorActionHostCallbacks {
|
|
|
106
107
|
readonly onPrintRequested?: () => void;
|
|
107
108
|
readonly onGoToRequested?: () => void;
|
|
108
109
|
|
|
109
|
-
// Tracked-change operations (suggestion target)
|
|
110
|
-
readonly onAcceptSuggestion?: () => void;
|
|
111
|
-
readonly onRejectSuggestion?: () => void;
|
|
112
|
-
|
|
113
110
|
// Comment operations (comment-anchor target)
|
|
114
111
|
readonly onResolveComment?: () => void;
|
|
115
112
|
readonly onReplyToComment?: () => void;
|
|
@@ -576,7 +573,7 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
576
573
|
mkImportant({
|
|
577
574
|
id: "insert-image",
|
|
578
575
|
label: "Insert image…",
|
|
579
|
-
description: "
|
|
576
|
+
description: "Choose an image file and insert it at the caret.",
|
|
580
577
|
group: "misc",
|
|
581
578
|
targetKinds: [],
|
|
582
579
|
callback: "onInsertImage",
|
|
@@ -636,22 +633,6 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
636
633
|
callback: "onPrintRequested",
|
|
637
634
|
}),
|
|
638
635
|
|
|
639
|
-
// -------- Suggestion / tracked change --------
|
|
640
|
-
mk({
|
|
641
|
-
id: "accept-suggestion",
|
|
642
|
-
label: "Accept suggestion",
|
|
643
|
-
group: "suggestion",
|
|
644
|
-
targetKinds: ["suggestion"],
|
|
645
|
-
callback: "onAcceptSuggestion",
|
|
646
|
-
}),
|
|
647
|
-
mk({
|
|
648
|
-
id: "reject-suggestion",
|
|
649
|
-
label: "Reject suggestion",
|
|
650
|
-
group: "suggestion",
|
|
651
|
-
targetKinds: ["suggestion"],
|
|
652
|
-
callback: "onRejectSuggestion",
|
|
653
|
-
}),
|
|
654
|
-
|
|
655
636
|
// -------- Comment --------
|
|
656
637
|
mk({
|
|
657
638
|
id: "resolve-comment",
|
|
@@ -43,6 +43,10 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
43
43
|
const { surface, pin, onChange, label } = props;
|
|
44
44
|
const isDetached = pin?.detached ?? false;
|
|
45
45
|
const offset = pin?.offset ?? { x: 0, y: 0 };
|
|
46
|
+
const dragHandleTestId =
|
|
47
|
+
surface === "selectionTier" ? "selection-tool-drag-handle" : `${surface}-detach-drag-handle`;
|
|
48
|
+
const toggleTestId =
|
|
49
|
+
surface === "selectionTier" ? "selection-tool-attach-toggle" : `${surface}-detach-toggle`;
|
|
46
50
|
const dragState = useRef<
|
|
47
51
|
| {
|
|
48
52
|
startX: number;
|
|
@@ -117,7 +121,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
117
121
|
<button
|
|
118
122
|
type="button"
|
|
119
123
|
aria-label={isDetached ? "Drag floating menu" : "Drag to float menu"}
|
|
120
|
-
data-testid={
|
|
124
|
+
data-testid={dragHandleTestId}
|
|
121
125
|
className="inline-flex h-6 items-center justify-center rounded-md border border-transparent px-1.5 text-tertiary transition-colors hover:border-border/60 hover:bg-surface hover:text-primary"
|
|
122
126
|
onMouseDown={beginDrag}
|
|
123
127
|
>
|
|
@@ -133,7 +137,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
133
137
|
type="button"
|
|
134
138
|
aria-label={isDetached ? "Dock menu" : "Float menu"}
|
|
135
139
|
aria-pressed={isDetached}
|
|
136
|
-
data-testid={
|
|
140
|
+
data-testid={toggleTestId}
|
|
137
141
|
className="inline-flex h-6 items-center rounded-md border border-border/60 px-2 text-[10px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
|
|
138
142
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
139
143
|
onClick={toggle}
|
|
@@ -153,6 +153,7 @@ function CommentThreadCard(props: {
|
|
|
153
153
|
}, [presentation]);
|
|
154
154
|
const leadEntry = thread.entries[0];
|
|
155
155
|
const isDraftThread = thread.status === "open" && thread.entryCount === 1 && isEmptyCommentBody(leadEntry?.body);
|
|
156
|
+
const isLinkedRevisionThread = thread.linkedRevisionId != null;
|
|
156
157
|
const isOwnComment = props.currentUserId != null && leadEntry?.authorId === props.currentUserId;
|
|
157
158
|
const canEdit = isOwnComment && thread.status === "open" && props.onEditBody != null;
|
|
158
159
|
const hasNoBody = isEmptyCommentBody(leadEntry?.body);
|
|
@@ -205,6 +206,7 @@ function CommentThreadCard(props: {
|
|
|
205
206
|
{formatCommentDate(thread.createdAt)}
|
|
206
207
|
</span>
|
|
207
208
|
<span className="flex-1" />
|
|
209
|
+
{isLinkedRevisionThread ? <StatusBadge label="tracked change" tone="revision" /> : null}
|
|
208
210
|
{isDraftThread ? <StatusBadge label="draft" tone="draft" /> : null}
|
|
209
211
|
{thread.status === "resolved" ? <StatusBadge label="resolved" tone="resolved" /> : null}
|
|
210
212
|
</div>
|
|
@@ -222,7 +224,9 @@ function CommentThreadCard(props: {
|
|
|
222
224
|
body={leadEntry?.body ?? ""}
|
|
223
225
|
autoFocus={isActive && hasNoBody}
|
|
224
226
|
onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
|
|
225
|
-
label={isDraftThread
|
|
227
|
+
label={isDraftThread
|
|
228
|
+
? (isLinkedRevisionThread ? "Tracked change discussion" : "New comment")
|
|
229
|
+
: undefined}
|
|
226
230
|
/>
|
|
227
231
|
) : presentation ? (
|
|
228
232
|
<CommentMarkdownRenderer
|
|
@@ -247,7 +251,7 @@ function CommentThreadCard(props: {
|
|
|
247
251
|
props.onOpenComment?.(thread);
|
|
248
252
|
}}
|
|
249
253
|
>
|
|
250
|
-
New comment
|
|
254
|
+
{isLinkedRevisionThread ? "Tracked change discussion" : "New comment"}
|
|
251
255
|
</p>
|
|
252
256
|
) : null}
|
|
253
257
|
|
|
@@ -494,11 +498,12 @@ function formatCommentDate(raw: string): string {
|
|
|
494
498
|
}
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" }) {
|
|
501
|
+
function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" | "revision" }) {
|
|
498
502
|
const styles: Record<string, string> = {
|
|
499
503
|
resolved: "text-insert bg-insert-soft",
|
|
500
504
|
detached: "text-comment bg-warning-soft",
|
|
501
505
|
draft: "text-secondary bg-subtle",
|
|
506
|
+
revision: "text-accent bg-accent-soft",
|
|
502
507
|
};
|
|
503
508
|
return (
|
|
504
509
|
<span
|
|
@@ -111,6 +111,7 @@ export interface TwReviewRailProps {
|
|
|
111
111
|
onAddReply?: (commentId: string, body: string) => void;
|
|
112
112
|
onEditBody?: (commentId: string, body: string) => void;
|
|
113
113
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
114
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
114
115
|
onAcceptRevision?: (revisionId: string) => void;
|
|
115
116
|
onRejectRevision?: (revisionId: string) => void;
|
|
116
117
|
onAcceptAllChanges?: () => void;
|
|
@@ -285,6 +286,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
285
286
|
markupDisplay={props.markupDisplay}
|
|
286
287
|
activeRevisionId={props.activeRevisionId}
|
|
287
288
|
onOpenRevision={props.onOpenRevision}
|
|
289
|
+
onReplyToRevision={props.onReplyToRevision}
|
|
288
290
|
onAcceptRevision={props.onAcceptRevision}
|
|
289
291
|
onRejectRevision={props.onRejectRevision}
|
|
290
292
|
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Check, X } from "lucide-react";
|
|
2
|
+
import { Check, MessageSquare, X } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
|
|
5
5
|
import { selectVisibleRevisions } from "../../ui/shared/revision-filters";
|
|
@@ -10,6 +10,7 @@ export interface TwRevisionSidebarProps {
|
|
|
10
10
|
markupDisplay: MarkupDisplay;
|
|
11
11
|
activeRevisionId?: string;
|
|
12
12
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
13
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
13
14
|
onAcceptRevision?: (revisionId: string) => void;
|
|
14
15
|
onRejectRevision?: (revisionId: string) => void;
|
|
15
16
|
onAcceptAllChanges?: () => void;
|
|
@@ -120,46 +121,61 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
120
121
|
const isActive = activeRevisionId === rev.revisionId;
|
|
121
122
|
|
|
122
123
|
return (
|
|
123
|
-
<
|
|
124
|
+
<div
|
|
124
125
|
key={rev.revisionId}
|
|
125
|
-
|
|
126
|
-
className={`w-full text-left flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
127
|
-
onClick={() => props.onOpenRevision?.(rev)}
|
|
126
|
+
className={`w-full text-left flex rounded-md bg-surface/90 transition-colors ring-1 ring-border ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
128
127
|
>
|
|
129
128
|
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
130
129
|
rev.kind === "insertion" ? "bg-insert"
|
|
131
130
|
: rev.kind === "deletion" ? "bg-danger"
|
|
132
131
|
: "bg-tertiary"
|
|
133
132
|
}`} />
|
|
134
|
-
<div className="
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
rev.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
133
|
+
<div className="flex-1 min-w-0">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className={`w-full p-2 text-left ${focusRingClass}`}
|
|
137
|
+
onClick={() => props.onOpenRevision?.(rev)}
|
|
138
|
+
>
|
|
139
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
140
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
141
|
+
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
142
|
+
</div>
|
|
143
|
+
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
144
|
+
{rev.excerpt ? (
|
|
145
|
+
<p className={`text-[11px] ${
|
|
146
|
+
rev.kind === "insertion" ? "text-insert"
|
|
147
|
+
: rev.kind === "deletion" ? "text-danger line-through"
|
|
148
|
+
: "text-secondary"
|
|
149
|
+
}`}>
|
|
150
|
+
{rev.excerpt}
|
|
151
|
+
</p>
|
|
152
|
+
) : (
|
|
153
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
154
|
+
)}
|
|
155
|
+
{rev.detail ? (
|
|
156
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
157
|
+
) : null}
|
|
158
|
+
</button>
|
|
159
|
+
<div className="flex flex-wrap gap-1.5 px-2 pb-2">
|
|
160
|
+
{props.onReplyToRevision ? (
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-secondary hover:bg-canvas transition-colors"
|
|
164
|
+
onClick={() => props.onReplyToRevision?.(rev)}
|
|
165
|
+
>
|
|
166
|
+
<MessageSquare className="h-3 w-3" />
|
|
167
|
+
{rev.replyCount && rev.replyCount > 0
|
|
168
|
+
? `Reply ${rev.replyCount}`
|
|
169
|
+
: "Reply"}
|
|
170
|
+
</button>
|
|
171
|
+
) : null}
|
|
155
172
|
{rev.actionability === "actionable" ? (
|
|
156
173
|
<>
|
|
157
174
|
<button
|
|
158
175
|
type="button"
|
|
159
176
|
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
160
177
|
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
161
|
-
onClick={(
|
|
162
|
-
e.stopPropagation();
|
|
178
|
+
onClick={() => {
|
|
163
179
|
props.onAcceptRevision?.(rev.revisionId);
|
|
164
180
|
}}
|
|
165
181
|
>
|
|
@@ -169,8 +185,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
169
185
|
type="button"
|
|
170
186
|
disabled={!rev.canReject || rev.status === "rejected"}
|
|
171
187
|
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
172
|
-
onClick={(
|
|
173
|
-
e.stopPropagation();
|
|
188
|
+
onClick={() => {
|
|
174
189
|
props.onRejectRevision?.(rev.revisionId);
|
|
175
190
|
}}
|
|
176
191
|
>
|
|
@@ -182,7 +197,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
182
197
|
)}
|
|
183
198
|
</div>
|
|
184
199
|
</div>
|
|
185
|
-
</
|
|
200
|
+
</div>
|
|
186
201
|
);
|
|
187
202
|
})}
|
|
188
203
|
</div>
|
|
@@ -36,6 +36,24 @@ const POSTURE_META: Record<
|
|
|
36
36
|
"blocked-import": { chip: "BLOCKED REGION", kind: "danger" },
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const focusRingClass =
|
|
40
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
41
|
+
|
|
42
|
+
type ScopeFilterKey = "edit" | "suggest" | "comment" | "view" | "candidate" | "blocked";
|
|
43
|
+
|
|
44
|
+
const SCOPE_FILTERS: ReadonlyArray<{
|
|
45
|
+
key: ScopeFilterKey;
|
|
46
|
+
label: string;
|
|
47
|
+
postures: readonly ScopeRailPosture[];
|
|
48
|
+
}> = [
|
|
49
|
+
{ key: "edit", label: "Edit", postures: ["edit"] },
|
|
50
|
+
{ key: "suggest", label: "Suggest", postures: ["suggest"] },
|
|
51
|
+
{ key: "comment", label: "Comment", postures: ["comment"] },
|
|
52
|
+
{ key: "view", label: "Review", postures: ["view"] },
|
|
53
|
+
{ key: "candidate", label: "Scheduled", postures: ["candidate"] },
|
|
54
|
+
{ key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
|
|
55
|
+
];
|
|
56
|
+
|
|
39
57
|
export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
40
58
|
segments,
|
|
41
59
|
activeScopeId,
|
|
@@ -43,13 +61,38 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
43
61
|
onActiveScopeChange,
|
|
44
62
|
}) => {
|
|
45
63
|
// Dedupe by scopeId so a scope spanning multiple pages shows once.
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
byScopeId.
|
|
64
|
+
const uniqueSegments = React.useMemo(() => {
|
|
65
|
+
const byScopeId = new Map<string, ScopeRailSegment>();
|
|
66
|
+
for (const segment of segments) {
|
|
67
|
+
if (!byScopeId.has(segment.scopeId)) {
|
|
68
|
+
byScopeId.set(segment.scopeId, segment);
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
|
|
72
|
+
}, [activeScopeId, segments]);
|
|
73
|
+
const [query, setQuery] = React.useState("");
|
|
74
|
+
const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
|
|
75
|
+
() => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
|
|
76
|
+
);
|
|
77
|
+
const availableFilters = React.useMemo(() => {
|
|
78
|
+
const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
|
|
79
|
+
return SCOPE_FILTERS.filter((filter) =>
|
|
80
|
+
filter.postures.some((posture) => presentPostures.has(posture)),
|
|
81
|
+
);
|
|
82
|
+
}, [uniqueSegments]);
|
|
83
|
+
const visibleSegments = React.useMemo(() => {
|
|
84
|
+
const normalizedQuery = normalizeScopeQuery(query);
|
|
85
|
+
return uniqueSegments.filter((segment) => {
|
|
86
|
+
const filterKey = filterKeyForPosture(segment.posture);
|
|
87
|
+
if (!enabledFilters.has(filterKey)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (!normalizedQuery) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return scopeSearchText(segment).includes(normalizedQuery);
|
|
94
|
+
});
|
|
95
|
+
}, [enabledFilters, query, uniqueSegments]);
|
|
53
96
|
|
|
54
97
|
if (uniqueSegments.length === 0) {
|
|
55
98
|
return (
|
|
@@ -73,9 +116,79 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
73
116
|
<div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-accent">
|
|
74
117
|
Document Intelligence
|
|
75
118
|
</div>
|
|
76
|
-
<div className="
|
|
119
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
120
|
+
<div className="text-[15px] font-semibold text-primary">Workflow Scopes</div>
|
|
121
|
+
<div
|
|
122
|
+
className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary"
|
|
123
|
+
data-testid="workflow-scope-count"
|
|
124
|
+
>
|
|
125
|
+
{visibleSegments.length}/{uniqueSegments.length} shown
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
77
128
|
</div>
|
|
78
|
-
|
|
129
|
+
|
|
130
|
+
<div
|
|
131
|
+
className="wre-workflow-tab-controls rounded-lg border border-border bg-surface/55 p-2"
|
|
132
|
+
data-testid="workflow-scope-controls"
|
|
133
|
+
>
|
|
134
|
+
<input
|
|
135
|
+
aria-label="Search workflow scopes"
|
|
136
|
+
className={`h-8 w-full rounded-md border border-border bg-canvas px-2 text-[12px] text-primary placeholder:text-tertiary ${focusRingClass}`}
|
|
137
|
+
placeholder="Search scope, page, section..."
|
|
138
|
+
type="search"
|
|
139
|
+
value={query}
|
|
140
|
+
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
141
|
+
/>
|
|
142
|
+
{availableFilters.length > 1 ? (
|
|
143
|
+
<div
|
|
144
|
+
aria-label="Workflow scope layers"
|
|
145
|
+
className="mt-2 flex flex-wrap gap-1"
|
|
146
|
+
role="group"
|
|
147
|
+
>
|
|
148
|
+
{availableFilters.map((filter) => {
|
|
149
|
+
const isEnabled = enabledFilters.has(filter.key);
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
key={filter.key}
|
|
153
|
+
type="button"
|
|
154
|
+
aria-pressed={isEnabled}
|
|
155
|
+
className={[
|
|
156
|
+
"rounded-full border px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.1em] transition-colors",
|
|
157
|
+
isEnabled
|
|
158
|
+
? "border-accent/50 bg-accent/10 text-accent"
|
|
159
|
+
: "border-border bg-canvas text-tertiary hover:text-secondary",
|
|
160
|
+
].join(" ")}
|
|
161
|
+
data-testid={`workflow-scope-filter-${filter.key}`}
|
|
162
|
+
onClick={() => {
|
|
163
|
+
setEnabledFilters((current) => {
|
|
164
|
+
const next = new Set(current);
|
|
165
|
+
if (next.has(filter.key)) {
|
|
166
|
+
next.delete(filter.key);
|
|
167
|
+
} else {
|
|
168
|
+
next.add(filter.key);
|
|
169
|
+
}
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{filter.label}
|
|
175
|
+
</button>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{visibleSegments.length === 0 ? (
|
|
183
|
+
<div
|
|
184
|
+
className="rounded-md border border-dashed border-border bg-canvas/50 p-3 text-[11px] text-tertiary"
|
|
185
|
+
data-testid="workflow-scope-filter-empty"
|
|
186
|
+
>
|
|
187
|
+
No workflow scopes match the current search or layer filters.
|
|
188
|
+
</div>
|
|
189
|
+
) : null}
|
|
190
|
+
|
|
191
|
+
{visibleSegments.map((segment) => {
|
|
79
192
|
const meta = POSTURE_META[segment.posture];
|
|
80
193
|
const isActive = activeScopeId === segment.scopeId || segment.isActiveWorkItem;
|
|
81
194
|
return (
|
|
@@ -115,4 +228,42 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
115
228
|
);
|
|
116
229
|
};
|
|
117
230
|
|
|
231
|
+
function compareWorkflowSegments(activeScopeId: string | null) {
|
|
232
|
+
return (left: ScopeRailSegment, right: ScopeRailSegment): number => {
|
|
233
|
+
const leftActive = left.scopeId === activeScopeId || left.isActiveWorkItem;
|
|
234
|
+
const rightActive = right.scopeId === activeScopeId || right.isActiveWorkItem;
|
|
235
|
+
if (leftActive !== rightActive) {
|
|
236
|
+
return leftActive ? -1 : 1;
|
|
237
|
+
}
|
|
238
|
+
if (left.pageIndex !== right.pageIndex) {
|
|
239
|
+
return left.pageIndex - right.pageIndex;
|
|
240
|
+
}
|
|
241
|
+
if (left.sectionIndex !== right.sectionIndex) {
|
|
242
|
+
return left.sectionIndex - right.sectionIndex;
|
|
243
|
+
}
|
|
244
|
+
return (left.label || left.scopeId).localeCompare(right.label || right.scopeId);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
|
|
249
|
+
if (posture === "preserve-only" || posture === "blocked-import") {
|
|
250
|
+
return "blocked";
|
|
251
|
+
}
|
|
252
|
+
return posture;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeScopeQuery(value: string): string {
|
|
256
|
+
return value.trim().toLocaleLowerCase();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scopeSearchText(segment: ScopeRailSegment): string {
|
|
260
|
+
return normalizeScopeQuery([
|
|
261
|
+
segment.label,
|
|
262
|
+
segment.scopeId,
|
|
263
|
+
segment.posture,
|
|
264
|
+
`page ${segment.pageIndex + 1}`,
|
|
265
|
+
`section ${segment.sectionIndex + 1}`,
|
|
266
|
+
].filter(Boolean).join(" "));
|
|
267
|
+
}
|
|
268
|
+
|
|
118
269
|
export default TwWorkflowTab;
|
|
@@ -145,6 +145,9 @@ export interface TwReviewWorkspaceProps {
|
|
|
145
145
|
activeRailTab: ReviewRailTab;
|
|
146
146
|
activeCommentId?: string;
|
|
147
147
|
activeRevisionId?: string;
|
|
148
|
+
/** Authoring mode toggle state: whether new edits are recorded as tracked changes. */
|
|
149
|
+
trackedChangesAuthoringEnabled?: boolean;
|
|
150
|
+
/** Visual markup state: whether tracked-change decorations are currently shown. */
|
|
148
151
|
showTrackedChanges: boolean;
|
|
149
152
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
150
153
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
@@ -286,6 +289,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
286
289
|
onAddReply?: (commentId: string, body: string) => void;
|
|
287
290
|
onEditBody?: (commentId: string, body: string) => void;
|
|
288
291
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
292
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
289
293
|
onAcceptRevision?: (revisionId: string) => void;
|
|
290
294
|
onRejectRevision?: (revisionId: string) => void;
|
|
291
295
|
onAcceptAllChanges?: () => void;
|
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
ChevronLeft,
|
|
26
26
|
ChevronRight,
|
|
27
27
|
CircleOff,
|
|
28
|
-
Eye,
|
|
29
|
-
EyeOff,
|
|
30
28
|
FileDiff,
|
|
31
29
|
Flag,
|
|
32
30
|
Hand,
|
|
@@ -226,33 +224,33 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
|
|
|
226
224
|
</Tooltip.Portal>
|
|
227
225
|
</Tooltip.Root>
|
|
228
226
|
);
|
|
229
|
-
case "tracked-changes-toggle":
|
|
227
|
+
case "tracked-changes-toggle": {
|
|
228
|
+
const trackChangesLabel = (props.showTrackedChanges ?? false)
|
|
229
|
+
? "Stop tracking changes"
|
|
230
|
+
: "Start tracking changes";
|
|
230
231
|
return (
|
|
231
232
|
<Tooltip.Root>
|
|
232
233
|
<Tooltip.Trigger asChild>
|
|
233
234
|
<Toggle.Root
|
|
234
235
|
pressed={props.showTrackedChanges ?? false}
|
|
235
236
|
onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
|
|
236
|
-
aria-label={
|
|
237
|
+
aria-label={trackChangesLabel}
|
|
237
238
|
disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
|
|
238
239
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
239
240
|
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
240
241
|
data-testid="role-tracked-changes-toggle"
|
|
241
242
|
>
|
|
242
|
-
|
|
243
|
-
<Eye className="h-3.5 w-3.5" />
|
|
244
|
-
) : (
|
|
245
|
-
<EyeOff className="h-3.5 w-3.5" />
|
|
246
|
-
)}
|
|
243
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
247
244
|
</Toggle.Root>
|
|
248
245
|
</Tooltip.Trigger>
|
|
249
246
|
<Tooltip.Portal>
|
|
250
247
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
251
|
-
{
|
|
248
|
+
{trackChangesLabel}
|
|
252
249
|
</Tooltip.Content>
|
|
253
250
|
</Tooltip.Portal>
|
|
254
251
|
</Tooltip.Root>
|
|
255
252
|
);
|
|
253
|
+
}
|
|
256
254
|
case "review-sidebar-tracked-changes":
|
|
257
255
|
return (
|
|
258
256
|
<Tooltip.Root>
|
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
Bold,
|
|
16
16
|
ChevronDown,
|
|
17
17
|
Download,
|
|
18
|
-
|
|
19
|
-
EyeOff,
|
|
18
|
+
FileDiff,
|
|
20
19
|
FileText,
|
|
21
20
|
Highlighter,
|
|
22
21
|
ImagePlus,
|
|
@@ -89,7 +88,7 @@ export interface TwToolbarProps {
|
|
|
89
88
|
formattingState?: FormattingStateSnapshot;
|
|
90
89
|
activeListContext?: ActiveListContext | null;
|
|
91
90
|
styleCatalog?: StyleCatalogSnapshot;
|
|
92
|
-
/**
|
|
91
|
+
/** Authoring toggle for recording new edits as tracked changes. */
|
|
93
92
|
showTrackedChanges: boolean;
|
|
94
93
|
/** Active story target — shows a breadcrumb when editing a secondary story. */
|
|
95
94
|
activeStory?: EditorStoryTarget;
|
|
@@ -378,7 +377,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
378
377
|
label="Bold"
|
|
379
378
|
shortcut="⌘B"
|
|
380
379
|
active={props.formattingState?.bold ?? false}
|
|
381
|
-
disabled={!canEdit}
|
|
380
|
+
disabled={!canEdit || !props.onToggleBold}
|
|
382
381
|
onClick={props.onToggleBold}
|
|
383
382
|
/>
|
|
384
383
|
<TwToolbarIconButton
|
|
@@ -386,7 +385,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
386
385
|
label="Italic"
|
|
387
386
|
shortcut="⌘I"
|
|
388
387
|
active={props.formattingState?.italic ?? false}
|
|
389
|
-
disabled={!canEdit}
|
|
388
|
+
disabled={!canEdit || !props.onToggleItalic}
|
|
390
389
|
onClick={props.onToggleItalic}
|
|
391
390
|
/>
|
|
392
391
|
<TwToolbarIconButton
|
|
@@ -394,12 +393,17 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
394
393
|
label="Underline"
|
|
395
394
|
shortcut="⌘U"
|
|
396
395
|
active={props.formattingState?.underline ?? false}
|
|
397
|
-
disabled={!canEdit}
|
|
396
|
+
disabled={!canEdit || !props.onToggleUnderline}
|
|
398
397
|
onClick={props.onToggleUnderline}
|
|
399
398
|
/>
|
|
400
399
|
{showAdvancedFormatting ? (
|
|
401
400
|
<ToolbarFormattingOverflow
|
|
402
|
-
disabled={
|
|
401
|
+
disabled={
|
|
402
|
+
!canEdit ||
|
|
403
|
+
(!props.onToggleStrikethrough &&
|
|
404
|
+
!props.onToggleSuperscript &&
|
|
405
|
+
!props.onToggleSubscript)
|
|
406
|
+
}
|
|
403
407
|
formattingState={props.formattingState}
|
|
404
408
|
onToggleStrikethrough={props.onToggleStrikethrough}
|
|
405
409
|
onToggleSuperscript={props.onToggleSuperscript}
|
|
@@ -452,14 +456,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
452
456
|
icon={List}
|
|
453
457
|
label="Bulleted list"
|
|
454
458
|
active={Boolean(props.activeListContext && !props.activeListContext.isOrdered)}
|
|
455
|
-
disabled={!canEdit}
|
|
459
|
+
disabled={!canEdit || !props.onToggleBulletedList}
|
|
456
460
|
onClick={props.onToggleBulletedList}
|
|
457
461
|
/>
|
|
458
462
|
<TwToolbarIconButton
|
|
459
463
|
icon={Rows3}
|
|
460
464
|
label="Numbered list"
|
|
461
465
|
active={Boolean(props.activeListContext?.isOrdered)}
|
|
462
|
-
disabled={!canEdit}
|
|
466
|
+
disabled={!canEdit || !props.onToggleNumberedList}
|
|
463
467
|
onClick={props.onToggleNumberedList}
|
|
464
468
|
/>
|
|
465
469
|
</>
|
|
@@ -469,13 +473,13 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
469
473
|
<TwToolbarIconButton
|
|
470
474
|
icon={Outdent}
|
|
471
475
|
label="Outdent"
|
|
472
|
-
disabled={!canEdit}
|
|
476
|
+
disabled={!canEdit || !props.onOutdent}
|
|
473
477
|
onClick={props.onOutdent}
|
|
474
478
|
/>
|
|
475
479
|
<TwToolbarIconButton
|
|
476
480
|
icon={Indent}
|
|
477
481
|
label="Indent"
|
|
478
|
-
disabled={!canEdit}
|
|
482
|
+
disabled={!canEdit || !props.onIndent}
|
|
479
483
|
onClick={props.onIndent}
|
|
480
484
|
/>
|
|
481
485
|
</>
|
|
@@ -669,12 +673,12 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
669
673
|
<Toggle.Root
|
|
670
674
|
pressed={props.showTrackedChanges}
|
|
671
675
|
onPressedChange={props.onShowTrackedChangesChange}
|
|
672
|
-
aria-label={props.showTrackedChanges ? "
|
|
676
|
+
aria-label={props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
673
677
|
disabled={caps ? !caps.trackChangesSupported : false}
|
|
674
678
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
675
679
|
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
676
680
|
>
|
|
677
|
-
|
|
681
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
678
682
|
</Toggle.Root>
|
|
679
683
|
</Tooltip.Trigger>
|
|
680
684
|
<Tooltip.Portal>
|
|
@@ -682,7 +686,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
682
686
|
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
683
687
|
sideOffset={6}
|
|
684
688
|
>
|
|
685
|
-
{props.showTrackedChanges ? "
|
|
689
|
+
{props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
686
690
|
</Tooltip.Content>
|
|
687
691
|
</Tooltip.Portal>
|
|
688
692
|
</Tooltip.Root>
|
|
@@ -1420,7 +1424,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1420
1424
|
<ToolbarPopoverActionButton
|
|
1421
1425
|
active={props.formattingState?.strikethrough ?? false}
|
|
1422
1426
|
ariaLabel="Strikethrough"
|
|
1423
|
-
disabled={props.disabled}
|
|
1427
|
+
disabled={props.disabled || !props.onToggleStrikethrough}
|
|
1424
1428
|
icon={<Strikethrough className="h-3.5 w-3.5" />}
|
|
1425
1429
|
onClick={() => {
|
|
1426
1430
|
props.onToggleStrikethrough?.();
|
|
@@ -1430,7 +1434,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1430
1434
|
<ToolbarPopoverActionButton
|
|
1431
1435
|
active={props.formattingState?.superscript ?? false}
|
|
1432
1436
|
ariaLabel="Superscript"
|
|
1433
|
-
disabled={props.disabled}
|
|
1437
|
+
disabled={props.disabled || !props.onToggleSuperscript}
|
|
1434
1438
|
icon={<Superscript className="h-3.5 w-3.5" />}
|
|
1435
1439
|
onClick={() => {
|
|
1436
1440
|
props.onToggleSuperscript?.();
|
|
@@ -1440,7 +1444,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1440
1444
|
<ToolbarPopoverActionButton
|
|
1441
1445
|
active={props.formattingState?.subscript ?? false}
|
|
1442
1446
|
ariaLabel="Subscript"
|
|
1443
|
-
disabled={props.disabled}
|
|
1447
|
+
disabled={props.disabled || !props.onToggleSubscript}
|
|
1444
1448
|
icon={<Subscript className="h-3.5 w-3.5" />}
|
|
1445
1449
|
onClick={() => {
|
|
1446
1450
|
props.onToggleSubscript?.();
|
|
@@ -184,6 +184,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
184
184
|
const caps = props.capabilities;
|
|
185
185
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
186
186
|
const markupDisplay = props.markupDisplay;
|
|
187
|
+
const trackedChangesAuthoringEnabled =
|
|
188
|
+
props.trackedChangesAuthoringEnabled ?? props.showTrackedChanges;
|
|
187
189
|
const [navOpen, setNavOpen] = useState(false);
|
|
188
190
|
const handleOpenPageModeStory = useCallback(
|
|
189
191
|
(target: EditorStoryTarget) => {
|
|
@@ -609,7 +611,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
609
611
|
toolbarInteractionPolicy?.canAddComment ??
|
|
610
612
|
(caps ? caps.canAddComment : false)
|
|
611
613
|
}
|
|
612
|
-
showTrackedChanges={
|
|
614
|
+
showTrackedChanges={trackedChangesAuthoringEnabled}
|
|
613
615
|
capabilities={caps}
|
|
614
616
|
onAddComment={
|
|
615
617
|
props.onAddComment
|
|
@@ -744,7 +746,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
744
746
|
formattingState={props.formattingState}
|
|
745
747
|
activeListContext={props.activeListContext}
|
|
746
748
|
styleCatalog={props.styleCatalog}
|
|
747
|
-
showTrackedChanges={
|
|
749
|
+
showTrackedChanges={trackedChangesAuthoringEnabled}
|
|
748
750
|
showSidebarToggle={responsiveChrome.showSidebarToggle}
|
|
749
751
|
isSidebarOpen={reviewRailOpen}
|
|
750
752
|
onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
|
|
@@ -1243,6 +1245,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1243
1245
|
onAddReply: props.onAddReply,
|
|
1244
1246
|
onEditBody: props.onEditBody,
|
|
1245
1247
|
onOpenRevision: props.onOpenRevision,
|
|
1248
|
+
onReplyToRevision: props.onReplyToRevision,
|
|
1246
1249
|
onAcceptRevision: props.onAcceptRevision,
|
|
1247
1250
|
onRejectRevision: props.onRejectRevision,
|
|
1248
1251
|
onAcceptAllChanges: props.onAcceptAllChanges,
|
|
@@ -1251,6 +1254,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1251
1254
|
// Layer-06 workflow facet. Layout facet no longer exposes
|
|
1252
1255
|
// `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
|
|
1253
1256
|
scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
|
|
1257
|
+
activeScopeId,
|
|
1258
|
+
onOpenScope: (segment) => {
|
|
1259
|
+
handleScopeStripeClick({ scopeId: segment.scopeId });
|
|
1260
|
+
},
|
|
1254
1261
|
workflowTab: props.reviewRailWorkflowTab,
|
|
1255
1262
|
workflowCount: props.reviewRailWorkflowCount,
|
|
1256
1263
|
workflowScopesTitle: props.reviewRailWorkflowScopesTitle,
|