@beyondwork/docx-react-component 1.0.30 → 1.0.32
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 +6 -0
- package/package.json +1 -1
- package/src/api/public-types.ts +16 -1
- package/src/api/session-state.ts +2 -0
- package/src/io/docx-session.ts +16 -3
- package/src/io/ooxml/parse-footnotes.ts +23 -33
- package/src/io/ooxml/parse-headers-footers.ts +20 -21
- package/src/io/ooxml/workflow-payload.ts +311 -8
- package/src/model/snapshot.ts +113 -1
- package/src/runtime/document-runtime.ts +207 -33
- package/src/runtime/surface-projection.ts +156 -7
- package/src/ui/WordReviewEditor.tsx +13 -5
- package/src/ui/editor-surface-controller.tsx +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +4 -1
- package/src/ui/headless/selection-tool-types.ts +1 -2
- package/src/ui/workflow-surface-blocked-rails.ts +19 -1
- package/src/ui-tailwind/chrome/responsive-chrome.ts +46 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +4 -4
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +5 -5
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +4 -4
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +14 -9
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +4 -5
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +5 -5
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +6 -6
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +9 -9
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +173 -124
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +88 -14
- package/src/ui-tailwind/editor-surface/pm-schema.ts +29 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -1
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +3 -3
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +20 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +9 -1
- package/src/ui-tailwind/theme/editor-theme.css +8 -0
- package/src/ui-tailwind/toolbar/toolbar-layout.ts +47 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +367 -22
- package/src/ui-tailwind/tw-review-workspace.tsx +131 -4
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
StyleCatalogSnapshot,
|
|
5
4
|
TableOperationCapabilitySnapshot,
|
|
6
5
|
TableStructureContextSnapshot,
|
|
7
6
|
} from "../../api/public-types";
|
|
@@ -10,7 +9,7 @@ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-edi
|
|
|
10
9
|
export interface TwTableContextToolbarProps {
|
|
11
10
|
disabled: boolean;
|
|
12
11
|
tableContext: TableStructureContextSnapshot | null;
|
|
13
|
-
tableStyles:
|
|
12
|
+
tableStyles: Array<{ styleId: string; displayName: string }>;
|
|
14
13
|
onSetTableStyle?: (styleId: string) => void;
|
|
15
14
|
onAddRowBefore?: () => void;
|
|
16
15
|
onAddRowAfter?: () => void;
|
|
@@ -34,138 +33,188 @@ const CELL_COLORS = [
|
|
|
34
33
|
] as const;
|
|
35
34
|
|
|
36
35
|
export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
36
|
+
const tableContext = props.tableContext;
|
|
37
|
+
const tableSizeLabel = tableContext ? `${tableContext.rowCount} x ${tableContext.columnCount}` : null;
|
|
38
|
+
const selectionLabel = tableContext
|
|
39
|
+
? tableContext.selectedCellCount > 1
|
|
40
|
+
? `${tableContext.selectedCellCount} cells`
|
|
41
|
+
: `R${tableContext.currentCell.rowIndex + 1} C${tableContext.currentCell.columnIndex + 1}`
|
|
42
|
+
: null;
|
|
43
|
+
|
|
37
44
|
return (
|
|
38
45
|
<div
|
|
39
46
|
data-testid="table-context-toolbar"
|
|
40
|
-
className="flex flex-wrap items-
|
|
47
|
+
className="flex max-w-[min(30rem,calc(100vw-1.5rem))] flex-wrap items-start gap-1.5 rounded-lg border border-border bg-canvas px-2.5 py-1.5 shadow-sm"
|
|
41
48
|
>
|
|
42
|
-
<span className="text-[
|
|
49
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
43
50
|
Table
|
|
44
51
|
</span>
|
|
52
|
+
{tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
|
|
53
|
+
{selectionLabel ? <ToolbarBadge>{selectionLabel}</ToolbarBadge> : null}
|
|
54
|
+
{tableContext?.currentCell.isHeader ? <ToolbarBadge tone="accent">Header row</ToolbarBadge> : null}
|
|
55
|
+
|
|
56
|
+
<ToolbarSection label="Style">
|
|
57
|
+
<select
|
|
58
|
+
aria-label="Table style"
|
|
59
|
+
className="h-7 min-w-[9rem] rounded-md border border-border bg-canvas px-2 text-[11px] text-primary disabled:opacity-40"
|
|
60
|
+
disabled={
|
|
61
|
+
props.disabled ||
|
|
62
|
+
props.tableStyles.length === 0 ||
|
|
63
|
+
!props.onSetTableStyle ||
|
|
64
|
+
!tableContext?.operations.setTableStyle.enabled
|
|
65
|
+
}
|
|
66
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
67
|
+
onChange={(event) => props.onSetTableStyle?.(event.target.value)}
|
|
68
|
+
value={tableContext?.currentStyleId ?? ""}
|
|
69
|
+
title={tableContext?.operations.setTableStyle.reason}
|
|
70
|
+
>
|
|
71
|
+
<option value="" disabled>Table style</option>
|
|
72
|
+
{props.tableStyles.map((style) => (
|
|
73
|
+
<option key={style.styleId} value={style.styleId}>
|
|
74
|
+
{style.displayName}
|
|
75
|
+
</option>
|
|
76
|
+
))}
|
|
77
|
+
</select>
|
|
78
|
+
</ToolbarSection>
|
|
79
|
+
|
|
80
|
+
<ToolbarSection label="Rows">
|
|
81
|
+
<ToolbarButton
|
|
82
|
+
ariaLabel="Add row above"
|
|
83
|
+
capability={tableContext?.operations.addRowBefore}
|
|
84
|
+
disabled={props.disabled}
|
|
85
|
+
onClick={props.onAddRowBefore}
|
|
86
|
+
>
|
|
87
|
+
Above
|
|
88
|
+
</ToolbarButton>
|
|
89
|
+
<ToolbarButton
|
|
90
|
+
ariaLabel="Add row below"
|
|
91
|
+
capability={tableContext?.operations.addRowAfter}
|
|
92
|
+
disabled={props.disabled}
|
|
93
|
+
onClick={props.onAddRowAfter}
|
|
94
|
+
>
|
|
95
|
+
Below
|
|
96
|
+
</ToolbarButton>
|
|
97
|
+
<ToolbarButton
|
|
98
|
+
ariaLabel="Delete row"
|
|
99
|
+
capability={tableContext?.operations.deleteRow}
|
|
100
|
+
disabled={props.disabled}
|
|
101
|
+
onClick={props.onDeleteRow}
|
|
102
|
+
>
|
|
103
|
+
Delete row
|
|
104
|
+
</ToolbarButton>
|
|
105
|
+
</ToolbarSection>
|
|
45
106
|
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
107
|
+
<ToolbarSection label="Columns">
|
|
108
|
+
<ToolbarButton
|
|
109
|
+
ariaLabel="Add column left"
|
|
110
|
+
capability={tableContext?.operations.addColumnBefore}
|
|
111
|
+
disabled={props.disabled}
|
|
112
|
+
onClick={props.onAddColumnBefore}
|
|
113
|
+
>
|
|
114
|
+
Left
|
|
115
|
+
</ToolbarButton>
|
|
116
|
+
<ToolbarButton
|
|
117
|
+
ariaLabel="Add column right"
|
|
118
|
+
capability={tableContext?.operations.addColumnAfter}
|
|
119
|
+
disabled={props.disabled}
|
|
120
|
+
onClick={props.onAddColumnAfter}
|
|
121
|
+
>
|
|
122
|
+
Right
|
|
123
|
+
</ToolbarButton>
|
|
124
|
+
<ToolbarButton
|
|
125
|
+
ariaLabel="Delete column"
|
|
126
|
+
capability={tableContext?.operations.deleteColumn}
|
|
127
|
+
disabled={props.disabled}
|
|
128
|
+
onClick={props.onDeleteColumn}
|
|
129
|
+
>
|
|
130
|
+
Delete column
|
|
131
|
+
</ToolbarButton>
|
|
132
|
+
</ToolbarSection>
|
|
51
133
|
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
props.disabled
|
|
57
|
-
props.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</option>
|
|
71
|
-
))}
|
|
72
|
-
</select>
|
|
134
|
+
<ToolbarSection label="Cells">
|
|
135
|
+
<ToolbarButton
|
|
136
|
+
ariaLabel="Merge cells"
|
|
137
|
+
capability={tableContext?.operations.mergeCells}
|
|
138
|
+
disabled={props.disabled}
|
|
139
|
+
onClick={props.onMergeCells}
|
|
140
|
+
>
|
|
141
|
+
Merge
|
|
142
|
+
</ToolbarButton>
|
|
143
|
+
<ToolbarButton
|
|
144
|
+
ariaLabel="Split cell"
|
|
145
|
+
capability={tableContext?.operations.splitCell}
|
|
146
|
+
disabled={props.disabled}
|
|
147
|
+
onClick={props.onSplitCell}
|
|
148
|
+
>
|
|
149
|
+
Split
|
|
150
|
+
</ToolbarButton>
|
|
151
|
+
</ToolbarSection>
|
|
73
152
|
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>
|
|
96
|
-
Delete row
|
|
97
|
-
</ToolbarButton>
|
|
98
|
-
<ToolbarButton
|
|
99
|
-
ariaLabel="Add column left"
|
|
100
|
-
capability={props.tableContext?.operations.addColumnBefore}
|
|
101
|
-
disabled={props.disabled}
|
|
102
|
-
onClick={props.onAddColumnBefore}
|
|
103
|
-
>
|
|
104
|
-
Column left
|
|
105
|
-
</ToolbarButton>
|
|
106
|
-
<ToolbarButton
|
|
107
|
-
ariaLabel="Add column right"
|
|
108
|
-
capability={props.tableContext?.operations.addColumnAfter}
|
|
109
|
-
disabled={props.disabled}
|
|
110
|
-
onClick={props.onAddColumnAfter}
|
|
111
|
-
>
|
|
112
|
-
Column right
|
|
113
|
-
</ToolbarButton>
|
|
114
|
-
<ToolbarButton
|
|
115
|
-
ariaLabel="Delete column"
|
|
116
|
-
capability={props.tableContext?.operations.deleteColumn}
|
|
117
|
-
disabled={props.disabled}
|
|
118
|
-
onClick={props.onDeleteColumn}
|
|
119
|
-
>
|
|
120
|
-
Delete column
|
|
121
|
-
</ToolbarButton>
|
|
122
|
-
<ToolbarButton
|
|
123
|
-
ariaLabel="Merge cells"
|
|
124
|
-
capability={props.tableContext?.operations.mergeCells}
|
|
125
|
-
disabled={props.disabled}
|
|
126
|
-
onClick={props.onMergeCells}
|
|
127
|
-
>
|
|
128
|
-
Merge
|
|
129
|
-
</ToolbarButton>
|
|
130
|
-
<ToolbarButton
|
|
131
|
-
ariaLabel="Split cell"
|
|
132
|
-
capability={props.tableContext?.operations.splitCell}
|
|
133
|
-
disabled={props.disabled}
|
|
134
|
-
onClick={props.onSplitCell}
|
|
135
|
-
>
|
|
136
|
-
Split
|
|
137
|
-
</ToolbarButton>
|
|
153
|
+
<ToolbarSection label="Fill">
|
|
154
|
+
<div className="flex items-center gap-1">
|
|
155
|
+
{CELL_COLORS.map((color) => (
|
|
156
|
+
<button
|
|
157
|
+
key={color}
|
|
158
|
+
type="button"
|
|
159
|
+
aria-label={`Set cell fill ${color}`}
|
|
160
|
+
disabled={
|
|
161
|
+
props.disabled ||
|
|
162
|
+
!props.onSetCellBackground ||
|
|
163
|
+
!tableContext?.operations.setCellBackground.enabled
|
|
164
|
+
}
|
|
165
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
166
|
+
onClick={() => props.onSetCellBackground?.(color)}
|
|
167
|
+
className="h-5 w-5 rounded border border-border disabled:opacity-40"
|
|
168
|
+
style={{ backgroundColor: color }}
|
|
169
|
+
title={tableContext?.operations.setCellBackground.reason}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</ToolbarSection>
|
|
138
174
|
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
<ToolbarSection label="Table">
|
|
176
|
+
<ToolbarButton
|
|
177
|
+
ariaLabel="Delete table"
|
|
178
|
+
capability={tableContext?.operations.deleteTable}
|
|
179
|
+
danger
|
|
180
|
+
disabled={props.disabled}
|
|
181
|
+
onClick={props.onDeleteTable}
|
|
182
|
+
>
|
|
183
|
+
Delete table
|
|
184
|
+
</ToolbarButton>
|
|
185
|
+
</ToolbarSection>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ToolbarBadge(props: {
|
|
191
|
+
children: React.ReactNode;
|
|
192
|
+
tone?: "neutral" | "accent";
|
|
193
|
+
}) {
|
|
194
|
+
return (
|
|
195
|
+
<span
|
|
196
|
+
className={[
|
|
197
|
+
"rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-[0.08em]",
|
|
198
|
+
props.tone === "accent"
|
|
199
|
+
? "bg-accent-soft text-accent"
|
|
200
|
+
: "bg-surface text-secondary",
|
|
201
|
+
].join(" ")}
|
|
202
|
+
>
|
|
203
|
+
{props.children}
|
|
204
|
+
</span>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
>
|
|
167
|
-
|
|
168
|
-
</
|
|
208
|
+
function ToolbarSection(props: {
|
|
209
|
+
label: string;
|
|
210
|
+
children: React.ReactNode;
|
|
211
|
+
}) {
|
|
212
|
+
return (
|
|
213
|
+
<div className="flex flex-wrap items-center gap-1 rounded-md bg-surface/60 px-1.5 py-1 ring-1 ring-border/35">
|
|
214
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-tertiary">
|
|
215
|
+
{props.label}
|
|
216
|
+
</span>
|
|
217
|
+
<div className="flex flex-wrap items-center gap-1">{props.children}</div>
|
|
169
218
|
</div>
|
|
170
219
|
);
|
|
171
220
|
}
|
|
@@ -188,7 +237,7 @@ function ToolbarButton(props: {
|
|
|
188
237
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
189
238
|
onClick={props.onClick}
|
|
190
239
|
title={title}
|
|
191
|
-
className={`inline-flex h-
|
|
240
|
+
className={`inline-flex h-7 items-center rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
192
241
|
props.danger
|
|
193
242
|
? "text-danger hover:bg-danger/10"
|
|
194
243
|
: "text-primary hover:bg-surface"
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
EditorStoryTarget,
|
|
10
10
|
WorkflowBlockedCommandReason,
|
|
11
11
|
WorkflowCandidateRange,
|
|
12
|
+
WorkflowLockedZone,
|
|
12
13
|
WorkflowMetadataMarkup,
|
|
13
14
|
WorkflowScope,
|
|
14
15
|
} from "../../api/public-types";
|
|
@@ -81,16 +82,16 @@ function getWorkflowMetadataInlineClass(): string {
|
|
|
81
82
|
|
|
82
83
|
function getWorkflowBlockedInlineClass(reason: WorkflowBlockedCommandReason): string {
|
|
83
84
|
if (reason.code === "workflow_blocked_import") {
|
|
84
|
-
return "wre-workflow-inline wre-workflow-inline-blocked-import";
|
|
85
|
+
return "wre-workflow-inline wre-workflow-inline-locked-zone wre-workflow-inline-blocked-import";
|
|
85
86
|
}
|
|
86
|
-
return "wre-workflow-inline wre-workflow-inline-preserve-only";
|
|
87
|
+
return "wre-workflow-inline wre-workflow-inline-locked-zone wre-workflow-inline-preserve-only";
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function getWorkflowBlockedRailClass(reason: WorkflowBlockedCommandReason): string {
|
|
90
91
|
if (reason.code === "workflow_blocked_import") {
|
|
91
|
-
return "wre-workflow-rail wre-workflow-rail-blocked-import";
|
|
92
|
+
return "wre-workflow-rail wre-workflow-rail-locked-zone wre-workflow-rail-blocked-import";
|
|
92
93
|
}
|
|
93
|
-
return "wre-workflow-rail wre-workflow-rail-preserve-only";
|
|
94
|
+
return "wre-workflow-rail wre-workflow-rail-locked-zone wre-workflow-rail-preserve-only";
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
function hasBlockChildren(node: PMNode): boolean {
|
|
@@ -172,6 +173,56 @@ function buildAnchorPmRange(
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
function collectLockedPmRanges(
|
|
177
|
+
lockedZones: readonly WorkflowLockedZone[] | undefined,
|
|
178
|
+
activeStory: EditorStoryTarget,
|
|
179
|
+
positionMap: PositionMap,
|
|
180
|
+
): Array<{ from: number; to: number; zone: WorkflowLockedZone }> {
|
|
181
|
+
if (!lockedZones || lockedZones.length === 0) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const ranges: Array<{ from: number; to: number; zone: WorkflowLockedZone }> = [];
|
|
185
|
+
for (const zone of lockedZones) {
|
|
186
|
+
const zoneStoryTarget = zone.storyTarget ?? MAIN_STORY_TARGET;
|
|
187
|
+
if (!storyTargetsEqual(zoneStoryTarget, activeStory)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const pmRange = buildAnchorPmRange(zone.anchor, positionMap);
|
|
191
|
+
if (!pmRange || !pmRange.allowInline || pmRange.from >= pmRange.to) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
ranges.push({ from: pmRange.from, to: pmRange.to, zone });
|
|
195
|
+
}
|
|
196
|
+
return ranges;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function subtractInlineOverlaps(
|
|
200
|
+
baseRange: { from: number; to: number },
|
|
201
|
+
blockedRanges: Array<{ from: number; to: number }>,
|
|
202
|
+
): Array<{ from: number; to: number }> {
|
|
203
|
+
let segments = [baseRange];
|
|
204
|
+
for (const blockedRange of blockedRanges) {
|
|
205
|
+
const nextSegments: Array<{ from: number; to: number }> = [];
|
|
206
|
+
for (const segment of segments) {
|
|
207
|
+
if (blockedRange.to <= segment.from || blockedRange.from >= segment.to) {
|
|
208
|
+
nextSegments.push(segment);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (blockedRange.from > segment.from) {
|
|
212
|
+
nextSegments.push({ from: segment.from, to: blockedRange.from });
|
|
213
|
+
}
|
|
214
|
+
if (blockedRange.to < segment.to) {
|
|
215
|
+
nextSegments.push({ from: blockedRange.to, to: segment.to });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
segments = nextSegments;
|
|
219
|
+
if (segments.length === 0) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return segments.filter((segment) => segment.from < segment.to);
|
|
224
|
+
}
|
|
225
|
+
|
|
175
226
|
function pushRailDecorations(
|
|
176
227
|
decorations: Decoration[],
|
|
177
228
|
doc: PMNode,
|
|
@@ -214,6 +265,7 @@ export function buildDecorations(
|
|
|
214
265
|
activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
|
|
215
266
|
workflowCandidates?: readonly WorkflowCandidateRange[],
|
|
216
267
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
|
|
268
|
+
workflowLockedZones?: readonly WorkflowLockedZone[],
|
|
217
269
|
activeWorkflowWorkItemId?: string | null,
|
|
218
270
|
activeWorkflowScopeIds?: readonly string[],
|
|
219
271
|
workflowMetadata?: readonly WorkflowMetadataMarkup[],
|
|
@@ -221,6 +273,7 @@ export function buildDecorations(
|
|
|
221
273
|
const decorations: Decoration[] = [];
|
|
222
274
|
const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
|
|
223
275
|
const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
|
|
276
|
+
const lockedPmRanges = collectLockedPmRanges(workflowLockedZones, activeStory, positionMap);
|
|
224
277
|
|
|
225
278
|
// Walk comment threads and create inline decorations
|
|
226
279
|
if (commentModel) {
|
|
@@ -307,15 +360,21 @@ export function buildDecorations(
|
|
|
307
360
|
);
|
|
308
361
|
|
|
309
362
|
if (pmRange.allowInline && pmRange.from < pmRange.to) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"data-workflow-scope-id": scope.scopeId,
|
|
314
|
-
"data-workflow-scope-mode": scope.mode,
|
|
315
|
-
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
316
|
-
...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
|
|
317
|
-
}),
|
|
363
|
+
const visibleScopeSegments = subtractInlineOverlaps(
|
|
364
|
+
{ from: pmRange.from, to: pmRange.to },
|
|
365
|
+
lockedPmRanges.filter((range) => range.to > pmRange.from && range.from < pmRange.to),
|
|
318
366
|
);
|
|
367
|
+
for (const visibleSegment of visibleScopeSegments) {
|
|
368
|
+
decorations.push(
|
|
369
|
+
Decoration.inline(visibleSegment.from, visibleSegment.to, {
|
|
370
|
+
class: getWorkflowInlineClass(scope, isActiveWorkItem, isSelectionZone),
|
|
371
|
+
"data-workflow-scope-id": scope.scopeId,
|
|
372
|
+
"data-workflow-scope-mode": scope.mode,
|
|
373
|
+
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
374
|
+
...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
319
378
|
}
|
|
320
379
|
|
|
321
380
|
pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
|
|
@@ -375,8 +434,19 @@ export function buildDecorations(
|
|
|
375
434
|
}
|
|
376
435
|
}
|
|
377
436
|
|
|
378
|
-
if (workflowBlockedReasons) {
|
|
379
|
-
|
|
437
|
+
if ((workflowLockedZones && workflowLockedZones.length > 0) || workflowBlockedReasons) {
|
|
438
|
+
const blockedReasonsToRender = workflowLockedZones && workflowLockedZones.length > 0
|
|
439
|
+
? workflowLockedZones.map((zone) => ({
|
|
440
|
+
code: zone.code,
|
|
441
|
+
message: zone.detail,
|
|
442
|
+
anchor: zone.anchor,
|
|
443
|
+
storyTarget: zone.storyTarget,
|
|
444
|
+
fragmentId: zone.fragmentId,
|
|
445
|
+
label: zone.label,
|
|
446
|
+
detail: zone.detail,
|
|
447
|
+
}))
|
|
448
|
+
: workflowBlockedReasons ?? [];
|
|
449
|
+
for (const reason of blockedReasonsToRender) {
|
|
380
450
|
if (
|
|
381
451
|
reason.code !== "workflow_preserve_only" &&
|
|
382
452
|
reason.code !== "workflow_blocked_import"
|
|
@@ -393,6 +463,8 @@ export function buildDecorations(
|
|
|
393
463
|
Decoration.inline(pmRange.from, pmRange.to, {
|
|
394
464
|
class: getWorkflowBlockedInlineClass(reason),
|
|
395
465
|
"data-workflow-blocked-code": reason.code,
|
|
466
|
+
...(reason.fragmentId ? { "data-workflow-fragment-id": reason.fragmentId } : {}),
|
|
467
|
+
"data-workflow-zone": "locked",
|
|
396
468
|
}),
|
|
397
469
|
);
|
|
398
470
|
}
|
|
@@ -402,6 +474,8 @@ export function buildDecorations(
|
|
|
402
474
|
className: getWorkflowBlockedRailClass(reason),
|
|
403
475
|
attrs: {
|
|
404
476
|
"data-workflow-blocked-code": reason.code,
|
|
477
|
+
...(reason.fragmentId ? { "data-workflow-fragment-id": reason.fragmentId } : {}),
|
|
478
|
+
"data-workflow-zone": "locked",
|
|
405
479
|
},
|
|
406
480
|
}, railRangeCache);
|
|
407
481
|
}
|
|
@@ -489,6 +489,7 @@ export const editorSchema = new Schema({
|
|
|
489
489
|
label: { default: "Locked" },
|
|
490
490
|
detail: { default: "" },
|
|
491
491
|
presentation: { default: "inline-chip" },
|
|
492
|
+
displayText: { default: null },
|
|
492
493
|
},
|
|
493
494
|
toDOM(node) {
|
|
494
495
|
const presentation = node.attrs.presentation as string;
|
|
@@ -505,6 +506,34 @@ export const editorSchema = new Schema({
|
|
|
505
506
|
},
|
|
506
507
|
];
|
|
507
508
|
}
|
|
509
|
+
if (presentation === "text-box") {
|
|
510
|
+
return [
|
|
511
|
+
"span",
|
|
512
|
+
{
|
|
513
|
+
class: "mx-0.5 inline-flex max-w-full whitespace-pre-wrap rounded border border-slate-300 bg-slate-50 px-2 py-1 align-top text-sm leading-snug text-slate-700 shadow-sm",
|
|
514
|
+
"data-node-type": "opaque_inline",
|
|
515
|
+
"data-inline-presentation": "text-box",
|
|
516
|
+
contenteditable: "false",
|
|
517
|
+
title: node.attrs.detail as string,
|
|
518
|
+
"aria-label": node.attrs.label as string,
|
|
519
|
+
},
|
|
520
|
+
(node.attrs.displayText as string | null) ?? (node.attrs.label as string),
|
|
521
|
+
];
|
|
522
|
+
}
|
|
523
|
+
if (presentation === "checkbox") {
|
|
524
|
+
return [
|
|
525
|
+
"span",
|
|
526
|
+
{
|
|
527
|
+
class: "mx-0.5 inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-slate-300 bg-white px-1 align-text-bottom text-sm leading-none text-slate-700",
|
|
528
|
+
"data-node-type": "opaque_inline",
|
|
529
|
+
"data-inline-presentation": "checkbox",
|
|
530
|
+
contenteditable: "false",
|
|
531
|
+
title: node.attrs.detail as string,
|
|
532
|
+
"aria-label": node.attrs.label as string,
|
|
533
|
+
},
|
|
534
|
+
(node.attrs.displayText as string | null) ?? "☐",
|
|
535
|
+
];
|
|
536
|
+
}
|
|
508
537
|
return [
|
|
509
538
|
"span",
|
|
510
539
|
{
|
|
@@ -34,7 +34,7 @@ export function createPMStateFromSnapshot(
|
|
|
34
34
|
selection: SelectionSnapshot,
|
|
35
35
|
plugins: Plugin[],
|
|
36
36
|
mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
|
|
37
|
-
showUnsupportedObjectPreviews =
|
|
37
|
+
showUnsupportedObjectPreviews = false,
|
|
38
38
|
): PMStateResult {
|
|
39
39
|
const doc = buildPMDoc(surface, mediaPreviews, showUnsupportedObjectPreviews);
|
|
40
40
|
const positionMap = buildPositionMap(surface);
|
|
@@ -508,6 +508,17 @@ function buildOpaqueInlineOrComplexAtom(
|
|
|
508
508
|
const label = segment.label;
|
|
509
509
|
const detail = segment.detail;
|
|
510
510
|
|
|
511
|
+
if (segment.presentation === "text-box" || segment.presentation === "checkbox") {
|
|
512
|
+
return editorSchema.nodes.opaque_inline.create({
|
|
513
|
+
fragmentId: segment.fragmentId,
|
|
514
|
+
warningId: segment.warningId,
|
|
515
|
+
label,
|
|
516
|
+
detail,
|
|
517
|
+
presentation: segment.presentation,
|
|
518
|
+
displayText: segment.displayText ?? null,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
511
522
|
if (showUnsupportedObjectPreviews && label === "Embedded chart") {
|
|
512
523
|
return editorSchema.nodes.chart_atom.create({ detail });
|
|
513
524
|
}
|
|
@@ -548,6 +559,7 @@ function buildOpaqueInlineOrComplexAtom(
|
|
|
548
559
|
label,
|
|
549
560
|
detail,
|
|
550
561
|
presentation: segment.presentation ?? "inline-chip",
|
|
562
|
+
displayText: segment.displayText ?? null,
|
|
551
563
|
});
|
|
552
564
|
}
|
|
553
565
|
|
|
@@ -30,7 +30,7 @@ export function createSurfaceDocumentBuildKey(input: {
|
|
|
30
30
|
: getSurfaceIdentity(input.surface),
|
|
31
31
|
activeStory: input.activeStory,
|
|
32
32
|
mediaPreviewKey: input.mediaPreviewKey,
|
|
33
|
-
showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ??
|
|
33
|
+
showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? false,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -43,10 +43,10 @@ export function createSurfaceDecorationKey(input: {
|
|
|
43
43
|
workflowScopeSignature?: string;
|
|
44
44
|
workflowCandidateSignature?: string;
|
|
45
45
|
workflowBlockedSignature?: string;
|
|
46
|
+
workflowLockedZoneSignature?: string;
|
|
46
47
|
workflowMetadataSignature?: string;
|
|
47
48
|
activeWorkflowWorkItemId?: string | null;
|
|
48
49
|
activeWorkflowScopeIds?: readonly string[];
|
|
49
|
-
suggestionsEnabled?: boolean;
|
|
50
50
|
}): string {
|
|
51
51
|
return JSON.stringify({
|
|
52
52
|
markupDisplay: input.markupDisplay,
|
|
@@ -57,9 +57,9 @@ export function createSurfaceDecorationKey(input: {
|
|
|
57
57
|
workflowScopeSignature: input.workflowScopeSignature ?? null,
|
|
58
58
|
workflowCandidateSignature: input.workflowCandidateSignature ?? null,
|
|
59
59
|
workflowBlockedSignature: input.workflowBlockedSignature ?? null,
|
|
60
|
+
workflowLockedZoneSignature: input.workflowLockedZoneSignature ?? null,
|
|
60
61
|
workflowMetadataSignature: input.workflowMetadataSignature ?? null,
|
|
61
62
|
activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
|
|
62
63
|
activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
|
|
63
|
-
suggestionsEnabled: input.suggestionsEnabled ?? false,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -106,6 +106,26 @@ export function TwInlineToken(props: TwInlineTokenProps) {
|
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
if (segment.presentation === "checkbox") {
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
tabIndex={-1}
|
|
114
|
+
onMouseDown={(e) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
props.onSelectionChange?.(createSelectionSnapshot(segment.from, segment.to));
|
|
117
|
+
}}
|
|
118
|
+
className={`inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-slate-300 bg-white px-1 text-sm leading-none text-slate-700 ${commentClass} ${selected ? "ring-1 ring-accent/30" : ""} ${focusRingClass}`}
|
|
119
|
+
title={segment.detail}
|
|
120
|
+
data-inline-presentation="checkbox"
|
|
121
|
+
>
|
|
122
|
+
{renderTwCaret(selection, segment.from)}
|
|
123
|
+
{segment.displayText ?? "☐"}
|
|
124
|
+
{renderTwCaret(selection, segment.to)}
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
return (
|
|
110
130
|
<button
|
|
111
131
|
type="button"
|