@beyondwork/docx-react-component 1.0.31 → 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.
@@ -3,7 +3,6 @@ import type {
3
3
  CommentSidebarThreadSnapshot,
4
4
  FormattingStateSnapshot,
5
5
  InteractionGuardSnapshot,
6
- StyleCatalogSnapshot,
7
6
  TableStructureContextSnapshot,
8
7
  SuggestionsSnapshot,
9
8
  TrackedChangeEntrySnapshot,
@@ -79,7 +78,7 @@ export type StructureContextKind = "table" | "image" | "object" | "list";
79
78
  export interface StructureContextSelectionToolModel extends BaseSelectionToolModel {
80
79
  kind: "structure-context";
81
80
  structureKind: StructureContextKind;
82
- tableStyles?: StyleCatalogSnapshot["tables"];
81
+ tableStyles?: Array<{ styleId: string; displayName: string }>;
83
82
  activeTable?: TableStructureContextSnapshot | null;
84
83
  activeImage?: ActiveImageContext;
85
84
  activeObject?: ActiveObjectContext;
@@ -3,6 +3,7 @@ import type {
3
3
  RuntimeRenderSnapshot,
4
4
  SurfaceBlockSnapshot,
5
5
  WorkflowBlockedCommandReason,
6
+ WorkflowLockedZone,
6
7
  WorkflowMarkupSnapshot,
7
8
  } from "../api/public-types";
8
9
 
@@ -10,6 +11,21 @@ export function deriveVisibleWorkflowBlockedRails(
10
11
  surface: RuntimeRenderSnapshot["surface"] | undefined,
11
12
  markupSnapshot: WorkflowMarkupSnapshot | null,
12
13
  ): WorkflowBlockedCommandReason[] {
14
+ return deriveVisibleWorkflowLockedZones(surface, markupSnapshot).map((zone) => ({
15
+ code: zone.code,
16
+ message: zone.detail,
17
+ fragmentId: zone.fragmentId,
18
+ label: zone.label,
19
+ detail: zone.detail,
20
+ anchor: zone.anchor,
21
+ storyTarget: zone.storyTarget,
22
+ }));
23
+ }
24
+
25
+ export function deriveVisibleWorkflowLockedZones(
26
+ surface: RuntimeRenderSnapshot["surface"] | undefined,
27
+ markupSnapshot: WorkflowMarkupSnapshot | null,
28
+ ): WorkflowLockedZone[] {
13
29
  if (!surface || !markupSnapshot) {
14
30
  return [];
15
31
  }
@@ -22,8 +38,10 @@ export function deriveVisibleWorkflowBlockedRails(
22
38
  ),
23
39
  )
24
40
  .map((fragment) => ({
41
+ fragmentId: fragment.fragmentId,
25
42
  code: fragment.blockedReasonCode,
26
- message: fragment.detail,
43
+ label: fragment.label,
44
+ detail: fragment.detail,
27
45
  anchor: fragment.anchor,
28
46
  storyTarget: fragment.storyTarget,
29
47
  }));
@@ -52,8 +52,7 @@ export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
52
52
  return (
53
53
  <TwTableContextToolbar
54
54
  disabled={!props.model.canMutate}
55
- tableContext={props.model.activeTable ?? null}
56
- tableStyles={props.model.tableStyles ?? []}
55
+ tableContext={props.model.activeTable ?? null} tableStyles={props.model.tableStyles ?? []}
57
56
  onSetTableStyle={props.onSetTableStyle}
58
57
  onAddRowBefore={props.onAddRowBefore}
59
58
  onAddRowAfter={props.onAddRowAfter}
@@ -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: StyleCatalogSnapshot["tables"];
12
+ tableStyles: Array<{ styleId: string; displayName: string }>;
14
13
  onSetTableStyle?: (styleId: string) => void;
15
14
  onAddRowBefore?: () => void;
16
15
  onAddRowAfter?: () => void;
@@ -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
- decorations.push(
311
- Decoration.inline(pmRange.from, pmRange.to, {
312
- class: getWorkflowInlineClass(scope, isActiveWorkItem, isSelectionZone),
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
- for (const reason of workflowBlockedReasons) {
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
  {
@@ -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
 
@@ -43,6 +43,7 @@ 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[];
@@ -56,6 +57,7 @@ export function createSurfaceDecorationKey(input: {
56
57
  workflowScopeSignature: input.workflowScopeSignature ?? null,
57
58
  workflowCandidateSignature: input.workflowCandidateSignature ?? null,
58
59
  workflowBlockedSignature: input.workflowBlockedSignature ?? null,
60
+ workflowLockedZoneSignature: input.workflowLockedZoneSignature ?? null,
59
61
  workflowMetadataSignature: input.workflowMetadataSignature ?? null,
60
62
  activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
61
63
  activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
@@ -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"
@@ -18,6 +18,7 @@ import type {
18
18
  SelectionSnapshot,
19
19
  WorkflowBlockedCommandReason,
20
20
  WorkflowCandidateRange,
21
+ WorkflowLockedZone,
21
22
  WorkflowMetadataMarkup,
22
23
  WorkflowScope,
23
24
  } from "../../api/public-types";
@@ -101,6 +102,7 @@ export interface TwProseMirrorSurfaceProps {
101
102
  workflowScopes?: readonly WorkflowScope[];
102
103
  workflowCandidates?: readonly WorkflowCandidateRange[];
103
104
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
105
+ workflowLockedZones?: readonly WorkflowLockedZone[];
104
106
  activeWorkflowWorkItemId?: string | null;
105
107
  activeWorkflowScopeIds?: readonly string[];
106
108
  workflowMetadata?: readonly WorkflowMetadataMarkup[];
@@ -221,6 +223,7 @@ export const TwProseMirrorSurface = forwardRef<
221
223
  workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
222
224
  workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
223
225
  workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
226
+ workflowLockedZoneSignature: createWorkflowLockedZoneSignature(props.workflowLockedZones),
224
227
  workflowMetadataSignature: createWorkflowMetadataSignature(props.workflowMetadata),
225
228
  activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
226
229
  activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
@@ -231,6 +234,7 @@ export const TwProseMirrorSurface = forwardRef<
231
234
  props.activeRevisionId,
232
235
  props.workflowCandidates,
233
236
  props.workflowBlockedReasons,
237
+ props.workflowLockedZones,
234
238
  props.workflowMetadata,
235
239
  props.activeWorkflowWorkItemId,
236
240
  props.activeWorkflowScopeIds,
@@ -280,6 +284,7 @@ export const TwProseMirrorSurface = forwardRef<
280
284
  snapshot.activeStory,
281
285
  props.workflowCandidates,
282
286
  props.workflowBlockedReasons,
287
+ props.workflowLockedZones,
283
288
  props.activeWorkflowWorkItemId,
284
289
  props.activeWorkflowScopeIds,
285
290
  props.workflowMetadata,
@@ -300,6 +305,7 @@ export const TwProseMirrorSurface = forwardRef<
300
305
  props.activeWorkflowScopeIds,
301
306
  props.activeWorkflowWorkItemId,
302
307
  props.workflowBlockedReasons,
308
+ props.workflowLockedZones,
303
309
  props.workflowMetadata,
304
310
  props.workflowCandidates,
305
311
  props.workflowScopes,
@@ -335,6 +341,7 @@ export const TwProseMirrorSurface = forwardRef<
335
341
  snapshot.activeStory,
336
342
  props.workflowCandidates,
337
343
  props.workflowBlockedReasons,
344
+ props.workflowLockedZones,
338
345
  props.activeWorkflowWorkItemId,
339
346
  props.activeWorkflowScopeIds,
340
347
  props.workflowMetadata,
@@ -898,6 +905,24 @@ function createWorkflowBlockedSignature(
898
905
  ).join("|");
899
906
  }
900
907
 
908
+ function createWorkflowLockedZoneSignature(
909
+ lockedZones: readonly WorkflowLockedZone[] | undefined,
910
+ ): string {
911
+ if (!lockedZones || lockedZones.length === 0) {
912
+ return "";
913
+ }
914
+ return lockedZones.map((zone) =>
915
+ [
916
+ zone.fragmentId,
917
+ zone.code,
918
+ zone.label,
919
+ zone.detail,
920
+ serializeAnchorSignature(zone.anchor),
921
+ serializeStoryTargetSignature(zone.storyTarget),
922
+ ].join(":")
923
+ ).join("|");
924
+ }
925
+
901
926
  function createWorkflowMetadataSignature(
902
927
  metadata: readonly WorkflowMetadataMarkup[] | undefined,
903
928
  ): string {
@@ -922,6 +947,7 @@ function serializeAnchorSignature(
922
947
  | WorkflowScope["anchor"]
923
948
  | WorkflowCandidateRange["anchor"]
924
949
  | WorkflowBlockedCommandReason["anchor"]
950
+ | WorkflowLockedZone["anchor"]
925
951
  | WorkflowMetadataMarkup["anchor"]
926
952
  | undefined,
927
953
  ): string {
@@ -326,6 +326,10 @@
326
326
  text-underline-offset: 0.18em;
327
327
  }
328
328
 
329
+ .prosemirror-surface .ProseMirror .wre-workflow-inline-locked-zone {
330
+ border-radius: 0.35rem;
331
+ }
332
+
329
333
  .prosemirror-surface .ProseMirror .wre-workflow-rail {
330
334
  position: relative;
331
335
  padding-left: 0.875rem;
@@ -421,6 +425,10 @@
421
425
  );
422
426
  }
423
427
 
428
+ .prosemirror-surface .ProseMirror .wre-workflow-rail-locked-zone {
429
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--wre-workflow-rail-color, var(--color-danger)) 10%, transparent);
430
+ }
431
+
424
432
  .prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone {
425
433
  background: transparent;
426
434
  }