@beyondwork/docx-react-component 1.0.22 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -38
- package/package.json +1 -1
- package/src/api/public-types.ts +66 -1
- package/src/core/commands/index.ts +574 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +661 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -718,7 +718,10 @@ function createLoadingRuntimeBridge(input: {
|
|
|
718
718
|
subscribeToEvents: () => () => undefined,
|
|
719
719
|
emitBlockedCommand: () => undefined,
|
|
720
720
|
getRenderSnapshot: () => input.snapshot,
|
|
721
|
+
getCanonicalDocument: () => input.sessionState.canonicalDocument,
|
|
722
|
+
getSourcePackage: () => input.sessionState.sourcePackage,
|
|
721
723
|
replaceText: () => undefined,
|
|
724
|
+
applyActiveStoryTextCommand: () => undefined,
|
|
722
725
|
dispatch: () => undefined,
|
|
723
726
|
undo: () => undefined,
|
|
724
727
|
redo: () => undefined,
|
|
@@ -770,7 +773,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
770
773
|
setWorkflowOverlay: () => undefined,
|
|
771
774
|
clearWorkflowOverlay: () => undefined,
|
|
772
775
|
getWorkflowScopeSnapshot: () => null,
|
|
773
|
-
getInteractionGuardSnapshot: () => ({ blockedReasons: [] }),
|
|
776
|
+
getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
|
|
774
777
|
getWorkflowMarkupSnapshot: () => ({
|
|
775
778
|
totalCount: 0,
|
|
776
779
|
items: [],
|
|
@@ -781,6 +784,12 @@ function createLoadingRuntimeBridge(input: {
|
|
|
781
784
|
protectedRanges: [],
|
|
782
785
|
opaqueFragments: [],
|
|
783
786
|
}),
|
|
787
|
+
setHostAnnotationOverlay: () => undefined,
|
|
788
|
+
clearHostAnnotationOverlay: () => undefined,
|
|
789
|
+
getHostAnnotationSnapshot: () => ({
|
|
790
|
+
totalCount: 0,
|
|
791
|
+
annotations: [],
|
|
792
|
+
}),
|
|
784
793
|
getWorkflowCandidateRanges: () => [],
|
|
785
794
|
replaceWorkflowMarkupText: () => undefined,
|
|
786
795
|
};
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
InteractionGuardSnapshot,
|
|
8
8
|
RuntimeRenderSnapshot,
|
|
9
9
|
StyleCatalogSnapshot,
|
|
10
|
+
WordReviewEditorChromeVisibility,
|
|
10
11
|
WorkflowScopeSnapshot,
|
|
11
12
|
WorkspaceMode,
|
|
12
13
|
ZoomLevel,
|
|
@@ -16,6 +17,7 @@ import type { MarkupDisplay } from "./headless/comment-decoration-model.ts";
|
|
|
16
17
|
import type {
|
|
17
18
|
SelectionToolbarAnchor,
|
|
18
19
|
SelectionToolbarModel,
|
|
20
|
+
SuggestionCardModel,
|
|
19
21
|
} from "./headless/selection-toolbar-model.ts";
|
|
20
22
|
import type { EditorCommandBag } from "./editor-command-bag.ts";
|
|
21
23
|
import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail.tsx";
|
|
@@ -56,17 +58,23 @@ export interface EditorShellViewProps {
|
|
|
56
58
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
57
59
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
58
60
|
selectionToolbar?: SelectionToolbarModel | null;
|
|
61
|
+
suggestionCard?: SuggestionCardModel | null;
|
|
59
62
|
selectionToolbarAnchor?: SelectionToolbarAnchor | null;
|
|
60
63
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
61
64
|
commands: EditorCommandBag;
|
|
62
65
|
document: ReactNode;
|
|
63
66
|
onAddCommentFromSelection?: () => void;
|
|
67
|
+
onAddCommentFromSuggestion?: () => void;
|
|
68
|
+
onAcceptSuggestion?: () => void;
|
|
69
|
+
onRejectSuggestion?: () => void;
|
|
70
|
+
onEditSuggestion?: () => void;
|
|
64
71
|
onDismissSelectionToolbar?: () => void;
|
|
65
72
|
onSelectionToolbarFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
66
73
|
onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
67
74
|
selectionToolbarRef?: React.Ref<HTMLDivElement>;
|
|
68
75
|
activeImageContext?: ActiveImageContext | null;
|
|
69
76
|
activeObjectContext?: ActiveObjectContext | null;
|
|
77
|
+
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export function EditorShellView(props: EditorShellViewProps) {
|
|
@@ -40,11 +40,16 @@ export interface EditorSurfaceControllerProps {
|
|
|
40
40
|
onOutdentTab?: () => void;
|
|
41
41
|
onInsertHardBreak?: () => void;
|
|
42
42
|
onSplitParagraph?: () => void;
|
|
43
|
+
onUndo?: () => void;
|
|
44
|
+
onRedo?: () => void;
|
|
45
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
43
46
|
onCommentActivated?: (commentId: string) => void;
|
|
44
47
|
onRevisionActivated?: (revisionId: string) => void;
|
|
45
48
|
workflowScopes?: readonly WorkflowScope[];
|
|
46
49
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
47
50
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
51
|
+
activeWorkflowWorkItemId?: string | null;
|
|
52
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export const EditorSurfaceController = forwardRef<
|
|
@@ -14,6 +14,18 @@ export interface SelectionToolbarModel {
|
|
|
14
14
|
disabledReason?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface SuggestionCardModel {
|
|
18
|
+
revisionId: string;
|
|
19
|
+
kindLabel: string;
|
|
20
|
+
previewText: string;
|
|
21
|
+
badges: SelectionToolbarBadge[];
|
|
22
|
+
canAccept: boolean;
|
|
23
|
+
canReject: boolean;
|
|
24
|
+
canEditSuggestion: boolean;
|
|
25
|
+
canAddComment: boolean;
|
|
26
|
+
disabledReason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export interface SelectionToolbarAnchor {
|
|
18
30
|
left: number;
|
|
19
31
|
right: number;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FocusEventHandler } from "react";
|
|
3
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
|
+
import { Check, MessageSquare, Pencil, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import type { SuggestionCardModel } from "../../ui/headless/selection-toolbar-model";
|
|
7
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
8
|
+
|
|
9
|
+
export interface TwSuggestionCardProps {
|
|
10
|
+
model: SuggestionCardModel;
|
|
11
|
+
onFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
12
|
+
onBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
13
|
+
onAccept?: () => void;
|
|
14
|
+
onReject?: () => void;
|
|
15
|
+
onEditSuggestion?: () => void;
|
|
16
|
+
onAddComment?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const focusRingClass =
|
|
20
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
21
|
+
|
|
22
|
+
export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
23
|
+
const contextLabel = summarizeSuggestionContext(props.model);
|
|
24
|
+
const commentDisabled = !props.model.canAddComment;
|
|
25
|
+
const tooltipLabel = commentDisabled
|
|
26
|
+
? props.model.disabledReason ?? "Commenting is unavailable for this selection"
|
|
27
|
+
: "Comment on suggestion";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
data-testid="suggestion-card"
|
|
32
|
+
className="inline-flex max-w-[min(28rem,calc(100vw-2rem))] flex-col gap-2 rounded-2xl border border-border/80 bg-canvas px-3 py-2 shadow-xl ring-1 ring-border/80"
|
|
33
|
+
onFocusCapture={props.onFocusCapture}
|
|
34
|
+
onBlurCapture={props.onBlurCapture}
|
|
35
|
+
role="group"
|
|
36
|
+
aria-label="Suggestion actions"
|
|
37
|
+
>
|
|
38
|
+
<div className="flex items-start justify-between gap-3">
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-warning">
|
|
41
|
+
{props.model.kindLabel}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="mt-1 max-w-[16rem] truncate text-sm text-primary">
|
|
44
|
+
{props.model.previewText}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
{contextLabel ? (
|
|
48
|
+
<div className="shrink-0 rounded-full bg-accent-soft px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-accent">
|
|
49
|
+
{contextLabel}
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
55
|
+
<SuggestionActionButton
|
|
56
|
+
icon={<Check className="h-3.5 w-3.5" />}
|
|
57
|
+
label="Accept suggestion"
|
|
58
|
+
disabled={!props.model.canAccept}
|
|
59
|
+
tone="accept"
|
|
60
|
+
onClick={props.onAccept}
|
|
61
|
+
/>
|
|
62
|
+
<SuggestionActionButton
|
|
63
|
+
icon={<X className="h-3.5 w-3.5" />}
|
|
64
|
+
label="Reject suggestion"
|
|
65
|
+
disabled={!props.model.canReject}
|
|
66
|
+
tone="reject"
|
|
67
|
+
onClick={props.onReject}
|
|
68
|
+
/>
|
|
69
|
+
<SuggestionActionButton
|
|
70
|
+
icon={<Pencil className="h-3.5 w-3.5" />}
|
|
71
|
+
label="Edit suggestion"
|
|
72
|
+
disabled={!props.model.canEditSuggestion}
|
|
73
|
+
tone="neutral"
|
|
74
|
+
onClick={props.onEditSuggestion}
|
|
75
|
+
/>
|
|
76
|
+
<Tooltip.Root>
|
|
77
|
+
<Tooltip.Trigger asChild>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
aria-label="Comment on suggestion"
|
|
81
|
+
disabled={commentDisabled}
|
|
82
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
83
|
+
onClick={props.onAddComment}
|
|
84
|
+
className={`inline-flex h-8 items-center gap-1 rounded-lg border border-border px-2.5 text-xs font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
85
|
+
>
|
|
86
|
+
<MessageSquare className="h-3.5 w-3.5" />
|
|
87
|
+
Comment
|
|
88
|
+
</button>
|
|
89
|
+
</Tooltip.Trigger>
|
|
90
|
+
<Tooltip.Portal>
|
|
91
|
+
<Tooltip.Content
|
|
92
|
+
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
93
|
+
sideOffset={6}
|
|
94
|
+
>
|
|
95
|
+
{tooltipLabel}
|
|
96
|
+
</Tooltip.Content>
|
|
97
|
+
</Tooltip.Portal>
|
|
98
|
+
</Tooltip.Root>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function summarizeSuggestionContext(model: SuggestionCardModel): string | null {
|
|
105
|
+
const labels = model.badges.map((badge) => badge.label.trim()).filter(Boolean);
|
|
106
|
+
if (labels.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const summary = labels.slice(0, 2).join(" · ");
|
|
110
|
+
return summary.length > 36 ? `${summary.slice(0, 33)}...` : summary;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SuggestionActionButton(props: {
|
|
114
|
+
icon: React.ReactNode;
|
|
115
|
+
label: string;
|
|
116
|
+
disabled: boolean;
|
|
117
|
+
tone: "accept" | "reject" | "neutral";
|
|
118
|
+
onClick?: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
const toneClass = props.tone === "accept"
|
|
121
|
+
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300"
|
|
122
|
+
: props.tone === "reject"
|
|
123
|
+
? "border-rose-500/30 bg-rose-500/10 text-rose-700 hover:bg-rose-500/15 dark:text-rose-300"
|
|
124
|
+
: "border-border text-secondary hover:bg-surface";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
aria-label={props.label}
|
|
130
|
+
disabled={props.disabled}
|
|
131
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
132
|
+
onClick={props.onClick}
|
|
133
|
+
className={`inline-flex h-8 items-center gap-1 rounded-lg border px-2.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
|
|
134
|
+
>
|
|
135
|
+
{props.icon}
|
|
136
|
+
{props.label.replace(" suggestion", "").replace(" on suggestion", "")}
|
|
137
|
+
</button>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -19,6 +19,7 @@ export interface CommandBridgeCallbacks {
|
|
|
19
19
|
onOutdentTab?: () => void;
|
|
20
20
|
onUndo: () => void;
|
|
21
21
|
onRedo: () => void;
|
|
22
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
22
23
|
onSelectionChange: (selection: SelectionSnapshot) => void;
|
|
23
24
|
getPositionMap: () => PositionMap | null;
|
|
24
25
|
isSelectionSyncSuppressed?: () => boolean;
|
|
@@ -100,17 +101,22 @@ export function createCommandBridgePlugins(
|
|
|
100
101
|
},
|
|
101
102
|
},
|
|
102
103
|
handleTextInput(_view, _from, _to, text) {
|
|
104
|
+
if (isComposing) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
103
107
|
callbacks.onInsertText(text);
|
|
104
108
|
return true; // Block PM from processing
|
|
105
109
|
},
|
|
106
110
|
|
|
107
111
|
// Block paste (rich paste is not safe, plain paste via text.insert is TODO)
|
|
108
112
|
handlePaste() {
|
|
113
|
+
callbacks.onBlockedInput?.("paste", "Paste is not supported in the mounted editor yet.");
|
|
109
114
|
return true; // Block
|
|
110
115
|
},
|
|
111
116
|
|
|
112
117
|
// Block drop
|
|
113
118
|
handleDrop() {
|
|
119
|
+
callbacks.onBlockedInput?.("drop", "Drag and drop is not supported in the mounted editor.");
|
|
114
120
|
return true; // Block
|
|
115
121
|
},
|
|
116
122
|
},
|
|
@@ -21,18 +21,28 @@ type RailDecorationSpec = {
|
|
|
21
21
|
attrs: Record<string, string>;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
function getWorkflowInlineClass(scope: WorkflowScope): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
function getWorkflowInlineClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
|
|
25
|
+
const base =
|
|
26
|
+
scope.mode === "edit"
|
|
27
|
+
? "wre-workflow-inline wre-workflow-inline-edit"
|
|
28
|
+
: scope.mode === "suggest"
|
|
29
|
+
? "wre-workflow-inline wre-workflow-inline-suggest"
|
|
30
|
+
: scope.mode === "comment"
|
|
31
|
+
? "wre-workflow-inline wre-workflow-inline-comment"
|
|
32
|
+
: "wre-workflow-inline wre-workflow-inline-view";
|
|
33
|
+
return isActiveWorkItem ? `${base} wre-workflow-inline-active` : base;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
function getWorkflowRailClass(scope: WorkflowScope): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
function getWorkflowRailClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
|
|
37
|
+
const base =
|
|
38
|
+
scope.mode === "edit"
|
|
39
|
+
? "wre-workflow-rail wre-workflow-rail-edit"
|
|
40
|
+
: scope.mode === "suggest"
|
|
41
|
+
? "wre-workflow-rail wre-workflow-rail-suggest"
|
|
42
|
+
: scope.mode === "comment"
|
|
43
|
+
? "wre-workflow-rail wre-workflow-rail-comment"
|
|
44
|
+
: "wre-workflow-rail wre-workflow-rail-view";
|
|
45
|
+
return isActiveWorkItem ? `${base} wre-workflow-rail-active` : base;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function getWorkflowCandidateInlineClass(): string {
|
|
@@ -142,8 +152,14 @@ function pushRailDecorations(
|
|
|
142
152
|
from: number,
|
|
143
153
|
to: number,
|
|
144
154
|
spec: RailDecorationSpec,
|
|
155
|
+
rangeCache: Map<string, Array<{ from: number; to: number }>>,
|
|
145
156
|
): void {
|
|
146
|
-
|
|
157
|
+
const cacheKey = `${from}:${to}`;
|
|
158
|
+
const ranges = rangeCache.get(cacheKey) ?? collectRailRanges(doc, from, to);
|
|
159
|
+
if (!rangeCache.has(cacheKey)) {
|
|
160
|
+
rangeCache.set(cacheKey, ranges);
|
|
161
|
+
}
|
|
162
|
+
for (const range of ranges) {
|
|
147
163
|
decorations.push(
|
|
148
164
|
Decoration.node(range.from, range.to, {
|
|
149
165
|
class: spec.className,
|
|
@@ -172,8 +188,12 @@ export function buildDecorations(
|
|
|
172
188
|
activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
|
|
173
189
|
workflowCandidates?: readonly WorkflowCandidateRange[],
|
|
174
190
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
|
|
191
|
+
activeWorkflowWorkItemId?: string | null,
|
|
192
|
+
activeWorkflowScopeIds?: readonly string[],
|
|
175
193
|
): DecorationSet {
|
|
176
194
|
const decorations: Decoration[] = [];
|
|
195
|
+
const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
|
|
196
|
+
const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
|
|
177
197
|
|
|
178
198
|
// Walk comment threads and create inline decorations
|
|
179
199
|
if (commentModel) {
|
|
@@ -251,25 +271,33 @@ export function buildDecorations(
|
|
|
251
271
|
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
|
|
252
272
|
const pmRange = buildAnchorPmRange(scope.anchor, positionMap);
|
|
253
273
|
if (!pmRange) continue;
|
|
274
|
+
const isActiveWorkItem =
|
|
275
|
+
Boolean(activeWorkflowWorkItemId) &&
|
|
276
|
+
(
|
|
277
|
+
scope.workItemId === activeWorkflowWorkItemId ||
|
|
278
|
+
activeScopeIds.has(scope.scopeId)
|
|
279
|
+
);
|
|
254
280
|
|
|
255
281
|
if (pmRange.allowInline && pmRange.from < pmRange.to) {
|
|
256
282
|
decorations.push(
|
|
257
283
|
Decoration.inline(pmRange.from, pmRange.to, {
|
|
258
|
-
class: getWorkflowInlineClass(scope),
|
|
284
|
+
class: getWorkflowInlineClass(scope, isActiveWorkItem),
|
|
259
285
|
"data-workflow-scope-id": scope.scopeId,
|
|
260
286
|
"data-workflow-scope-mode": scope.mode,
|
|
287
|
+
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
261
288
|
}),
|
|
262
289
|
);
|
|
263
290
|
}
|
|
264
291
|
|
|
265
292
|
pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
|
|
266
293
|
railKind: "scope",
|
|
267
|
-
className: getWorkflowRailClass(scope),
|
|
294
|
+
className: getWorkflowRailClass(scope, isActiveWorkItem),
|
|
268
295
|
attrs: {
|
|
269
296
|
"data-workflow-scope-id": scope.scopeId,
|
|
270
297
|
"data-workflow-scope-mode": scope.mode,
|
|
298
|
+
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
271
299
|
},
|
|
272
|
-
});
|
|
300
|
+
}, railRangeCache);
|
|
273
301
|
}
|
|
274
302
|
}
|
|
275
303
|
|
|
@@ -295,7 +323,7 @@ export function buildDecorations(
|
|
|
295
323
|
attrs: {
|
|
296
324
|
"data-workflow-candidate-id": candidate.candidateId,
|
|
297
325
|
},
|
|
298
|
-
});
|
|
326
|
+
}, railRangeCache);
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
|
|
@@ -327,7 +355,7 @@ export function buildDecorations(
|
|
|
327
355
|
attrs: {
|
|
328
356
|
"data-workflow-blocked-code": reason.code,
|
|
329
357
|
},
|
|
330
|
-
});
|
|
358
|
+
}, railRangeCache);
|
|
331
359
|
}
|
|
332
360
|
}
|
|
333
361
|
|
|
@@ -383,6 +383,8 @@ function buildTable(
|
|
|
383
383
|
}
|
|
384
384
|
rows.push(editorSchema.nodes.table_row.create(
|
|
385
385
|
{
|
|
386
|
+
gridBefore: row.gridBefore ?? 0,
|
|
387
|
+
gridAfter: row.gridAfter ?? 0,
|
|
386
388
|
height: row.height ?? null,
|
|
387
389
|
heightRule: row.heightRule ?? null,
|
|
388
390
|
isHeader: row.isHeader ?? false,
|
|
@@ -41,6 +41,8 @@ export function createSurfaceDecorationKey(input: {
|
|
|
41
41
|
workflowScopeSignature?: string;
|
|
42
42
|
workflowCandidateSignature?: string;
|
|
43
43
|
workflowBlockedSignature?: string;
|
|
44
|
+
activeWorkflowWorkItemId?: string | null;
|
|
45
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
44
46
|
}): string {
|
|
45
47
|
return JSON.stringify({
|
|
46
48
|
markupDisplay: input.markupDisplay,
|
|
@@ -51,5 +53,7 @@ export function createSurfaceDecorationKey(input: {
|
|
|
51
53
|
workflowScopeSignature: input.workflowScopeSignature ?? null,
|
|
52
54
|
workflowCandidateSignature: input.workflowCandidateSignature ?? null,
|
|
53
55
|
workflowBlockedSignature: input.workflowBlockedSignature ?? null,
|
|
56
|
+
activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
|
|
57
|
+
activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
|
|
54
58
|
});
|
|
55
59
|
}
|
|
@@ -87,6 +87,9 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
87
87
|
onOutdentTab?: () => void;
|
|
88
88
|
onInsertHardBreak?: () => void;
|
|
89
89
|
onSplitParagraph?: () => void;
|
|
90
|
+
onUndo?: () => void;
|
|
91
|
+
onRedo?: () => void;
|
|
92
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
90
93
|
onCommentActivated?: (commentId: string) => void;
|
|
91
94
|
onRevisionActivated?: (revisionId: string) => void;
|
|
92
95
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -94,6 +97,8 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
94
97
|
workflowScopes?: readonly WorkflowScope[];
|
|
95
98
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
96
99
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
100
|
+
activeWorkflowWorkItemId?: string | null;
|
|
101
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -161,8 +166,11 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
161
166
|
onInsertHardBreak: () => props.onInsertHardBreak?.(),
|
|
162
167
|
onInsertTab: () => props.onInsertTab?.(),
|
|
163
168
|
onOutdentTab: () => props.onOutdentTab?.(),
|
|
164
|
-
onUndo: () =>
|
|
165
|
-
onRedo: () =>
|
|
169
|
+
onUndo: () => props.onUndo?.(),
|
|
170
|
+
onRedo: () => props.onRedo?.(),
|
|
171
|
+
onBlockedInput: (command, message) => {
|
|
172
|
+
props.onBlockedInput?.(command, message);
|
|
173
|
+
},
|
|
166
174
|
onSelectionChange: (sel) => {
|
|
167
175
|
pendingSelectionProbeRef.current = startPerfProbe("selection");
|
|
168
176
|
props.onSelectionChange?.(
|
|
@@ -204,9 +212,11 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
204
212
|
canEdit,
|
|
205
213
|
activeCommentId: snapshot.comments.activeCommentId,
|
|
206
214
|
activeRevisionId: props.activeRevisionId,
|
|
207
|
-
workflowScopeSignature:
|
|
208
|
-
workflowCandidateSignature:
|
|
209
|
-
workflowBlockedSignature:
|
|
215
|
+
workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
|
|
216
|
+
workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
|
|
217
|
+
workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
|
|
218
|
+
activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
|
|
219
|
+
activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
|
|
210
220
|
}),
|
|
211
221
|
[
|
|
212
222
|
canEdit,
|
|
@@ -214,6 +224,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
214
224
|
props.activeRevisionId,
|
|
215
225
|
props.workflowCandidates,
|
|
216
226
|
props.workflowBlockedReasons,
|
|
227
|
+
props.activeWorkflowWorkItemId,
|
|
228
|
+
props.activeWorkflowScopeIds,
|
|
217
229
|
props.workflowScopes,
|
|
218
230
|
showTrackedChanges,
|
|
219
231
|
snapshot.comments.activeCommentId,
|
|
@@ -233,6 +245,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
233
245
|
onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
|
|
234
246
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
235
247
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
248
|
+
onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
|
|
236
249
|
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
237
250
|
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
238
251
|
isSelectionSyncSuppressed: () =>
|
|
@@ -259,6 +272,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
259
272
|
snapshot.activeStory,
|
|
260
273
|
props.workflowCandidates,
|
|
261
274
|
props.workflowBlockedReasons,
|
|
275
|
+
props.activeWorkflowWorkItemId,
|
|
276
|
+
props.activeWorkflowScopeIds,
|
|
262
277
|
);
|
|
263
278
|
view.setProps({
|
|
264
279
|
editable: () => canEdit,
|
|
@@ -273,13 +288,13 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
273
288
|
commentModel,
|
|
274
289
|
decorationBuildKey,
|
|
275
290
|
markupDisplay,
|
|
276
|
-
|
|
277
|
-
|
|
291
|
+
props.activeWorkflowScopeIds,
|
|
292
|
+
props.activeWorkflowWorkItemId,
|
|
278
293
|
props.workflowBlockedReasons,
|
|
279
294
|
props.workflowCandidates,
|
|
280
295
|
props.workflowScopes,
|
|
281
|
-
|
|
282
|
-
|
|
296
|
+
revisionModel,
|
|
297
|
+
showTrackedChanges,
|
|
283
298
|
],
|
|
284
299
|
);
|
|
285
300
|
|
|
@@ -309,6 +324,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
309
324
|
snapshot.activeStory,
|
|
310
325
|
props.workflowCandidates,
|
|
311
326
|
props.workflowBlockedReasons,
|
|
327
|
+
props.activeWorkflowWorkItemId,
|
|
328
|
+
props.activeWorkflowScopeIds,
|
|
312
329
|
);
|
|
313
330
|
recordPerfSample("pm.rebuild");
|
|
314
331
|
incrementInvalidationCounter("pm.laneA.rebuilds");
|
|
@@ -371,6 +388,20 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
371
388
|
applyDecorationProps(view, positionMap);
|
|
372
389
|
}, [applyDecorationProps, decorationBuildKey, surface]);
|
|
373
390
|
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (!activeSearchRef.current || !surface) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
applySearch(activeSearchRef.current.query, activeSearchRef.current.options);
|
|
396
|
+
}, [
|
|
397
|
+
markupDisplay,
|
|
398
|
+
props.canonicalDocument,
|
|
399
|
+
props.documentNavigation,
|
|
400
|
+
snapshot.activeStory,
|
|
401
|
+
snapshot.trackedChanges,
|
|
402
|
+
surface,
|
|
403
|
+
]);
|
|
404
|
+
|
|
374
405
|
useEffect(() => {
|
|
375
406
|
const view = viewRef.current;
|
|
376
407
|
const positionMap = positionMapRef.current;
|
|
@@ -442,7 +473,15 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
442
473
|
return getTableSelectionDescriptor(view.state);
|
|
443
474
|
},
|
|
444
475
|
}),
|
|
445
|
-
[
|
|
476
|
+
[
|
|
477
|
+
markupDisplay,
|
|
478
|
+
props.canonicalDocument,
|
|
479
|
+
props.documentNavigation,
|
|
480
|
+
snapshot.activeStory,
|
|
481
|
+
snapshot.selection,
|
|
482
|
+
snapshot.surface,
|
|
483
|
+
snapshot.trackedChanges,
|
|
484
|
+
],
|
|
446
485
|
);
|
|
447
486
|
|
|
448
487
|
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
@@ -788,6 +827,84 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
788
827
|
}
|
|
789
828
|
});
|
|
790
829
|
|
|
830
|
+
function createWorkflowScopeSignature(scopes: readonly WorkflowScope[] | undefined): string {
|
|
831
|
+
if (!scopes || scopes.length === 0) {
|
|
832
|
+
return "";
|
|
833
|
+
}
|
|
834
|
+
return scopes.map((scope) =>
|
|
835
|
+
[
|
|
836
|
+
scope.scopeId,
|
|
837
|
+
scope.mode,
|
|
838
|
+
scope.workItemId ?? "",
|
|
839
|
+
serializeAnchorSignature(scope.anchor),
|
|
840
|
+
serializeStoryTargetSignature(scope.storyTarget),
|
|
841
|
+
].join(":")
|
|
842
|
+
).join("|");
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function createWorkflowCandidateSignature(
|
|
846
|
+
candidates: readonly WorkflowCandidateRange[] | undefined,
|
|
847
|
+
): string {
|
|
848
|
+
if (!candidates || candidates.length === 0) {
|
|
849
|
+
return "";
|
|
850
|
+
}
|
|
851
|
+
return candidates.map((candidate) =>
|
|
852
|
+
[
|
|
853
|
+
candidate.candidateId,
|
|
854
|
+
serializeAnchorSignature(candidate.anchor),
|
|
855
|
+
serializeStoryTargetSignature(candidate.storyTarget),
|
|
856
|
+
candidate.source ?? "",
|
|
857
|
+
].join(":")
|
|
858
|
+
).join("|");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function createWorkflowBlockedSignature(
|
|
862
|
+
blockedReasons: readonly WorkflowBlockedCommandReason[] | undefined,
|
|
863
|
+
): string {
|
|
864
|
+
if (!blockedReasons || blockedReasons.length === 0) {
|
|
865
|
+
return "";
|
|
866
|
+
}
|
|
867
|
+
return blockedReasons.map((reason) =>
|
|
868
|
+
[
|
|
869
|
+
reason.code,
|
|
870
|
+
reason.scopeId ?? "",
|
|
871
|
+
reason.workItemId ?? "",
|
|
872
|
+
serializeAnchorSignature(reason.anchor),
|
|
873
|
+
serializeStoryTargetSignature(reason.storyTarget),
|
|
874
|
+
].join(":")
|
|
875
|
+
).join("|");
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function serializeAnchorSignature(anchor: WorkflowScope["anchor"] | WorkflowCandidateRange["anchor"] | WorkflowBlockedCommandReason["anchor"] | undefined): string {
|
|
879
|
+
if (!anchor) {
|
|
880
|
+
return "";
|
|
881
|
+
}
|
|
882
|
+
switch (anchor.kind) {
|
|
883
|
+
case "range":
|
|
884
|
+
return `range:${anchor.from}:${anchor.to}:${anchor.assoc.start}:${anchor.assoc.end}`;
|
|
885
|
+
case "node":
|
|
886
|
+
return `node:${anchor.at}:${anchor.assoc}`;
|
|
887
|
+
case "detached":
|
|
888
|
+
return `detached:${anchor.lastKnownRange.from}:${anchor.lastKnownRange.to}:${anchor.reason}`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function serializeStoryTargetSignature(storyTarget: WorkflowScope["storyTarget"] | WorkflowCandidateRange["storyTarget"] | WorkflowBlockedCommandReason["storyTarget"]): string {
|
|
893
|
+
if (!storyTarget) {
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
switch (storyTarget.kind) {
|
|
897
|
+
case "main":
|
|
898
|
+
return "main";
|
|
899
|
+
case "header":
|
|
900
|
+
case "footer":
|
|
901
|
+
return `${storyTarget.kind}:${storyTarget.relationshipId}:${storyTarget.variant}:${storyTarget.sectionIndex ?? ""}`;
|
|
902
|
+
case "footnote":
|
|
903
|
+
case "endnote":
|
|
904
|
+
return `${storyTarget.kind}:${storyTarget.noteId}`;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
791
908
|
function buildSelectionToolbarMeasurementKey(
|
|
792
909
|
selection: SelectionSnapshot,
|
|
793
910
|
activeStory: RuntimeRenderSnapshot["activeStory"],
|