@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
ZoomLevel,
|
|
40
40
|
} from "../api/public-types";
|
|
41
41
|
import { findPageForOffset } from "../runtime/document-navigation.ts";
|
|
42
|
+
import { createCanvasBackend } from "../runtime/layout/index.ts";
|
|
42
43
|
import {
|
|
43
44
|
DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
44
45
|
estimateBlockHeight,
|
|
@@ -50,6 +51,8 @@ import {
|
|
|
50
51
|
incrementInvalidationCounter,
|
|
51
52
|
recordPerfSample,
|
|
52
53
|
} from "./editor-surface/perf-probe.ts";
|
|
54
|
+
import { sliceBlocksForPage } from "./editor-surface/page-slice-util.ts";
|
|
55
|
+
import { TwPageBlockView } from "./editor-surface/tw-page-block-view.tsx";
|
|
53
56
|
import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
|
|
54
57
|
import type { SessionCapabilities } from "../runtime/session-capabilities";
|
|
55
58
|
import type {
|
|
@@ -77,11 +80,13 @@ import {
|
|
|
77
80
|
resolveChromePresetOptions,
|
|
78
81
|
resolveChromeVisibilityForPreset,
|
|
79
82
|
} from "./chrome/chrome-preset-model";
|
|
80
|
-
import { TwReviewQueueBar } from "./chrome/review-queue-bar";
|
|
81
83
|
import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
|
|
84
|
+
import { resolveSelectionAnchor } from "./chrome/tw-selection-anchor-resolver";
|
|
85
|
+
import { resolveSelectionToolPlacement } from "./chrome/tw-selection-tool-placement";
|
|
82
86
|
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
83
87
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
84
88
|
import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
89
|
+
import { TwChromeOverlay } from "./chrome-overlay";
|
|
85
90
|
|
|
86
91
|
export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
|
|
87
92
|
|
|
@@ -92,6 +97,38 @@ export interface TwReviewWorkspaceProps {
|
|
|
92
97
|
currentUserId?: string;
|
|
93
98
|
capabilities?: SessionCapabilities;
|
|
94
99
|
reviewMode?: "editing" | "review";
|
|
100
|
+
/**
|
|
101
|
+
* Runtime-owned layout facet. Optional so existing tests + host apps
|
|
102
|
+
* continue to mount the workspace without installing a facet. When
|
|
103
|
+
* supplied, the ChromeOverlay plane (scope rail, workflow dock, etc.)
|
|
104
|
+
* renders over the document column.
|
|
105
|
+
*/
|
|
106
|
+
layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
|
|
107
|
+
/**
|
|
108
|
+
* Optional shell header mounted above the formatting toolbar. Pass a
|
|
109
|
+
* pre-assembled `<TwShellHeader />` with brand / mode switcher /
|
|
110
|
+
* primaryAction, or any other ReactNode. Hosts that do not supply this
|
|
111
|
+
* get the legacy layout.
|
|
112
|
+
*/
|
|
113
|
+
shellHeader?: ReactNode;
|
|
114
|
+
/**
|
|
115
|
+
* Optional host-provided Workflow-tab override for the review rail.
|
|
116
|
+
* When unset the rail renders the built-in `TwWorkflowTab` sourced from
|
|
117
|
+
* `layoutFacet.getAllScopeRailSegments()`.
|
|
118
|
+
*/
|
|
119
|
+
reviewRailWorkflowTab?: ReactNode;
|
|
120
|
+
reviewRailWorkflowCount?: number;
|
|
121
|
+
reviewRailWorkflowScopesTitle?: string;
|
|
122
|
+
reviewRailIntelligenceEyebrow?: string;
|
|
123
|
+
/** Opt in to the editorial DOCUMENT INTELLIGENCE header + underline tab chip. */
|
|
124
|
+
reviewRailIntelligenceHeader?: boolean;
|
|
125
|
+
/** Optional SEARCH / HELP utility footer at the bottom of the rail. */
|
|
126
|
+
reviewRailFooter?: {
|
|
127
|
+
onSearch?: () => void;
|
|
128
|
+
helpHref?: string;
|
|
129
|
+
searchLabel?: string;
|
|
130
|
+
helpLabel?: string;
|
|
131
|
+
};
|
|
95
132
|
document: ReactNode;
|
|
96
133
|
workspaceMode: WorkspaceMode;
|
|
97
134
|
zoomLevel?: ZoomLevel;
|
|
@@ -114,6 +151,17 @@ export interface TwReviewWorkspaceProps {
|
|
|
114
151
|
activeSelectionTool?: ActiveSelectionToolModel | null;
|
|
115
152
|
selectionToolAnchor?: SelectionToolAnchor | null;
|
|
116
153
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
154
|
+
/**
|
|
155
|
+
* R2.3: chrome-pin change handler. When supplied, selection tools
|
|
156
|
+
* expose their detach affordance and persist pin state through to
|
|
157
|
+
* runtime ViewState (via the host's `setChromePin` action). When
|
|
158
|
+
* omitted, the detach handle is suppressed — the tool behaves as
|
|
159
|
+
* a non-pinnable anchored panel (pre-R2 behavior for most kinds).
|
|
160
|
+
*/
|
|
161
|
+
onChromePinChange?: (
|
|
162
|
+
surface: import("../api/public-types").ChromePinSurface,
|
|
163
|
+
pin: import("../api/public-types").PinState | null,
|
|
164
|
+
) => void;
|
|
117
165
|
onWorkspaceModeChange?: (value: WorkspaceMode) => void;
|
|
118
166
|
onZoomChange?: (level: ZoomLevel) => void;
|
|
119
167
|
onActiveRailTabChange?: (value: ReviewRailTab) => void;
|
|
@@ -218,6 +266,14 @@ export interface TwReviewWorkspaceProps {
|
|
|
218
266
|
onCloseStory?: () => void;
|
|
219
267
|
onOpenHeaderStory?: () => void;
|
|
220
268
|
onOpenFooterStory?: () => void;
|
|
269
|
+
/**
|
|
270
|
+
* Open a header/footer story for a specific page. Called when the user
|
|
271
|
+
* double-clicks a per-page header/footer band in the page-stack chrome.
|
|
272
|
+
* Must resolve the correct variant for that page's section and call
|
|
273
|
+
* `runtime.openStory()`.
|
|
274
|
+
*/
|
|
275
|
+
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
276
|
+
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
221
277
|
onSetParagraphIndentation?: (indentation: {
|
|
222
278
|
left?: number;
|
|
223
279
|
right?: number;
|
|
@@ -227,13 +283,37 @@ export interface TwReviewWorkspaceProps {
|
|
|
227
283
|
onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
|
|
228
284
|
onRestartNumbering?: () => void;
|
|
229
285
|
onContinueNumbering?: () => void;
|
|
286
|
+
// P6: new table ops
|
|
287
|
+
onToggleRowHeader?: () => void;
|
|
288
|
+
onToggleRowCantSplit?: () => void;
|
|
289
|
+
onDistributeColumnsEvenly?: () => void;
|
|
290
|
+
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
291
|
+
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
292
|
+
/** P6: active table context for chrome overlay grips. */
|
|
293
|
+
tableContext?: import("../api/public-types").TableStructureContextSnapshot | null;
|
|
294
|
+
/** P6: column resize committed from overlay grip → set-column-width op. */
|
|
295
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
296
|
+
/** P6: row resize committed from overlay grip → set-row-height op. */
|
|
297
|
+
onSetRowHeight?: (rowIndex: number, twips: number, rule: "auto" | "atLeast" | "exact") => void;
|
|
298
|
+
onListIndent?: () => void;
|
|
299
|
+
onListOutdent?: () => void;
|
|
230
300
|
onUpdateFields?: () => void;
|
|
231
301
|
onUpdateTableOfContents?: () => void;
|
|
232
302
|
onGoToPreviousReviewItem?: () => void;
|
|
233
303
|
onGoToNextReviewItem?: () => void;
|
|
234
304
|
onMarkSectionForReview?: () => void;
|
|
305
|
+
/** Optional: open sidebar to tracked-changes panel. When provided, the review role shows a sidebar-TC icon. */
|
|
306
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
307
|
+
/** Optional: open sidebar to comments panel. When provided, the review role shows a sidebar-comments icon. */
|
|
308
|
+
onReviewSidebarComments?: () => void;
|
|
235
309
|
onNavigateHeading?: (headingId: string) => void;
|
|
236
310
|
chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
|
|
311
|
+
/**
|
|
312
|
+
* Called when the shell-header mode tab changes or any chrome surface fires
|
|
313
|
+
* a role switch. Wire to `runtime.setEditorRole(role)` so the workspace
|
|
314
|
+
* re-renders with the new per-role action set.
|
|
315
|
+
*/
|
|
316
|
+
onEditorRoleChange?: (role: import("../api/public-types.ts").EditorRole) => void;
|
|
237
317
|
}
|
|
238
318
|
|
|
239
319
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -275,6 +355,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
275
355
|
reviewRailAvailable,
|
|
276
356
|
}),
|
|
277
357
|
);
|
|
358
|
+
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
359
|
+
// useMemo below re-executes when the render kernel emits new rects.
|
|
360
|
+
const [renderFrameRevision, setRenderFrameRevision] = useState(0);
|
|
278
361
|
const responsiveChromeSignatureRef = useRef<string | null>(null);
|
|
279
362
|
const headings = props.documentNavigation?.headings ?? [];
|
|
280
363
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
@@ -304,11 +387,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
304
387
|
),
|
|
305
388
|
[props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
|
|
306
389
|
);
|
|
307
|
-
const selectionToolbarPlacement = resolveSelectionToolbarPlacement(
|
|
308
|
-
props.selectionToolAnchor,
|
|
309
|
-
selectionToolbarRootRef.current,
|
|
310
|
-
zoomScale,
|
|
311
|
-
);
|
|
312
390
|
const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
|
|
313
391
|
const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
|
|
314
392
|
const gatedSelectionTool = useMemo(() => {
|
|
@@ -320,6 +398,40 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
320
398
|
}
|
|
321
399
|
return props.activeSelectionTool;
|
|
322
400
|
}, [props.activeSelectionTool, chromeVisibility.contextToolbars]);
|
|
401
|
+
const selectionToolbarPlacement = useMemo(() => {
|
|
402
|
+
// Prefer render-frame anchors when the layout facet is available — this
|
|
403
|
+
// keeps the tool glued to kernel coordinates across zoom, scroll, and
|
|
404
|
+
// predicted-text reconciliation (R4).
|
|
405
|
+
if (props.layoutFacet && gatedSelectionTool) {
|
|
406
|
+
const anchorRect = resolveSelectionAnchor({
|
|
407
|
+
facet: props.layoutFacet,
|
|
408
|
+
selection: viewState.selection,
|
|
409
|
+
tool: gatedSelectionTool,
|
|
410
|
+
});
|
|
411
|
+
if (anchorRect && selectionToolbarRootRef.current) {
|
|
412
|
+
const containerRect = selectionToolbarRootRef.current.getBoundingClientRect();
|
|
413
|
+
const result = resolveSelectionToolPlacement({
|
|
414
|
+
anchor: anchorRect,
|
|
415
|
+
container: { widthPx: containerRect.width, heightPx: containerRect.height },
|
|
416
|
+
});
|
|
417
|
+
if (result) return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Fall back to DOM rects for hosts that do not supply a layout facet.
|
|
421
|
+
return resolveSelectionToolbarPlacement(
|
|
422
|
+
props.selectionToolAnchor,
|
|
423
|
+
selectionToolbarRootRef.current,
|
|
424
|
+
zoomScale,
|
|
425
|
+
);
|
|
426
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
427
|
+
}, [
|
|
428
|
+
props.layoutFacet,
|
|
429
|
+
props.selectionToolAnchor,
|
|
430
|
+
gatedSelectionTool,
|
|
431
|
+
viewState.selection,
|
|
432
|
+
zoomScale,
|
|
433
|
+
renderFrameRevision,
|
|
434
|
+
]);
|
|
323
435
|
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
324
436
|
const pageShellMetrics = useMemo(
|
|
325
437
|
() => buildPageShellMetrics(snapshot.pageLayout),
|
|
@@ -344,6 +456,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
344
456
|
}),
|
|
345
457
|
[reviewRailAvailable, reviewRailOpen, viewportWidth],
|
|
346
458
|
);
|
|
459
|
+
const hasSidebarPanelAccess = Boolean(
|
|
460
|
+
props.onReviewSidebarTrackedChanges || props.onReviewSidebarComments,
|
|
461
|
+
);
|
|
347
462
|
const scopedChromePolicy = useMemo(
|
|
348
463
|
() =>
|
|
349
464
|
resolveScopedChromePolicy({
|
|
@@ -353,14 +468,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
353
468
|
interactionGuardSnapshot: props.interactionGuardSnapshot,
|
|
354
469
|
workflowScopeSnapshot: props.workflowScopeSnapshot,
|
|
355
470
|
activeListContext: props.activeListContext,
|
|
471
|
+
role: viewState.editorRole,
|
|
472
|
+
hasSidebarPanelAccess,
|
|
356
473
|
}),
|
|
357
474
|
[
|
|
358
475
|
caps,
|
|
359
476
|
chromePreset,
|
|
477
|
+
hasSidebarPanelAccess,
|
|
360
478
|
props.activeListContext,
|
|
361
479
|
props.interactionGuardSnapshot,
|
|
362
480
|
props.workflowScopeSnapshot,
|
|
363
481
|
responsiveChrome.isNarrow,
|
|
482
|
+
viewState.editorRole,
|
|
364
483
|
],
|
|
365
484
|
);
|
|
366
485
|
const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
|
|
@@ -402,6 +521,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
402
521
|
};
|
|
403
522
|
}, []);
|
|
404
523
|
|
|
524
|
+
// Subscribe to layout facet events so chrome re-projects on zoom changes
|
|
525
|
+
// and after incremental relayout (R4).
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!props.layoutFacet) return;
|
|
528
|
+
const unsub = props.layoutFacet.subscribe((event) => {
|
|
529
|
+
if (event.kind === "zoom_changed" || event.kind === "render_frame_ready") {
|
|
530
|
+
setRenderFrameRevision((n) => n + 1);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return unsub;
|
|
534
|
+
}, [props.layoutFacet]);
|
|
535
|
+
|
|
405
536
|
useEffect(() => {
|
|
406
537
|
const responsiveSignature = `${reviewRailAvailable ? "1" : "0"}:${isNarrowChromeViewport(viewportWidth) ? "n" : "d"}`;
|
|
407
538
|
if (responsiveChromeSignatureRef.current === responsiveSignature) {
|
|
@@ -448,9 +579,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
448
579
|
};
|
|
449
580
|
}, [responsiveChrome.showDrawerReviewRail]);
|
|
450
581
|
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (!props.layoutFacet) return;
|
|
584
|
+
const facet = props.layoutFacet;
|
|
585
|
+
void document.fonts.ready.then(() => {
|
|
586
|
+
facet.swapMeasurementProvider(createCanvasBackend());
|
|
587
|
+
});
|
|
588
|
+
}, [props.layoutFacet]);
|
|
589
|
+
|
|
451
590
|
return (
|
|
452
591
|
<Tooltip.Provider delayDuration={400}>
|
|
453
592
|
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
593
|
+
{props.shellHeader}
|
|
454
594
|
{chromeVisibility.toolbar ? (
|
|
455
595
|
<div className="px-3 pt-3">
|
|
456
596
|
<ChromePresetToolbar
|
|
@@ -561,24 +701,56 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
561
701
|
dismissSelectionToolbar();
|
|
562
702
|
props.onShowTrackedChangesChange(show);
|
|
563
703
|
}}
|
|
704
|
+
role={viewState.editorRole}
|
|
705
|
+
reviewQueue={props.reviewQueue}
|
|
706
|
+
markupDisplay={markupDisplay}
|
|
707
|
+
onReviewSidebarTrackedChanges={props.onReviewSidebarTrackedChanges
|
|
708
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarTrackedChanges)
|
|
709
|
+
: undefined}
|
|
710
|
+
onReviewSidebarComments={props.onReviewSidebarComments
|
|
711
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarComments)
|
|
712
|
+
: undefined}
|
|
713
|
+
onMarkScopePosture={props.onMarkSectionForReview
|
|
714
|
+
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
715
|
+
: undefined}
|
|
716
|
+
onReviewPrev={props.onGoToPreviousReviewItem
|
|
717
|
+
? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
|
|
718
|
+
: undefined}
|
|
719
|
+
onReviewNext={props.onGoToNextReviewItem
|
|
720
|
+
? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
|
|
721
|
+
: undefined}
|
|
722
|
+
onReviewAccept={(() => {
|
|
723
|
+
const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
|
|
724
|
+
if (active?.kind !== "change" || !props.onAcceptRevision) {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
// ReviewQueueItem.itemId for a "change" entry is the
|
|
728
|
+
// revision id (set by the runtime review-queue projection).
|
|
729
|
+
const revisionId = active.itemId;
|
|
730
|
+
return () => {
|
|
731
|
+
dismissSelectionToolbar();
|
|
732
|
+
props.onAcceptRevision?.(revisionId);
|
|
733
|
+
};
|
|
734
|
+
})()}
|
|
735
|
+
onReviewReject={(() => {
|
|
736
|
+
const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
|
|
737
|
+
if (active?.kind !== "change" || !props.onRejectRevision) {
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
const revisionId = active.itemId;
|
|
741
|
+
return () => {
|
|
742
|
+
dismissSelectionToolbar();
|
|
743
|
+
props.onRejectRevision?.(revisionId);
|
|
744
|
+
};
|
|
745
|
+
})()}
|
|
746
|
+
chromePins={viewState.chromePins}
|
|
747
|
+
onChromePinChange={props.onChromePinChange}
|
|
564
748
|
/>
|
|
565
749
|
</div>
|
|
566
750
|
) : null}
|
|
567
751
|
|
|
568
|
-
{
|
|
569
|
-
|
|
570
|
-
queue={props.reviewQueue}
|
|
571
|
-
onPrevious={props.onGoToPreviousReviewItem
|
|
572
|
-
? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
|
|
573
|
-
: undefined}
|
|
574
|
-
onNext={props.onGoToNextReviewItem
|
|
575
|
-
? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
|
|
576
|
-
: undefined}
|
|
577
|
-
onMarkSection={chromeOptions.showSectionTagAction && props.onMarkSectionForReview
|
|
578
|
-
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
579
|
-
: undefined}
|
|
580
|
-
/>
|
|
581
|
-
) : null}
|
|
752
|
+
{/* Legacy TwReviewQueueBar is suppressed — review role's action region
|
|
753
|
+
now owns queue prev/next + counts inline in the top toolbar. */}
|
|
582
754
|
|
|
583
755
|
{chromeVisibility.alerts ? <TwAlertBanner
|
|
584
756
|
snapshot={snapshot}
|
|
@@ -883,6 +1055,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
883
1055
|
onSetImageFrame={props.onSetImageFrame}
|
|
884
1056
|
onRestartNumbering={props.onRestartNumbering}
|
|
885
1057
|
onContinueNumbering={props.onContinueNumbering}
|
|
1058
|
+
onToggleRowHeader={props.onToggleRowHeader}
|
|
1059
|
+
onToggleRowCantSplit={props.onToggleRowCantSplit}
|
|
1060
|
+
onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
|
|
1061
|
+
onSetTableAlignment={props.onSetTableAlignment}
|
|
1062
|
+
onSetCellVerticalAlign={props.onSetCellVerticalAlign}
|
|
1063
|
+
chromePins={viewState.chromePins}
|
|
1064
|
+
onChromePinChange={props.onChromePinChange}
|
|
886
1065
|
/>
|
|
887
1066
|
) : null}
|
|
888
1067
|
<div
|
|
@@ -949,8 +1128,24 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
949
1128
|
style={pageChromeModel.pageBorderStyle}
|
|
950
1129
|
/>
|
|
951
1130
|
) : null}
|
|
952
|
-
<div className={isPageWorkspace ? "relative z-10" :
|
|
1131
|
+
<div className={isPageWorkspace ? "relative z-10" : "relative"}>
|
|
1132
|
+
{/* Page chrome (frame borders, header/footer bands,
|
|
1133
|
+
page-number labels, inter-page separators) is
|
|
1134
|
+
rendered as in-flow widget decorations inside
|
|
1135
|
+
the PM surface itself — see
|
|
1136
|
+
`pm-page-break-decorations.ts`. That keeps the
|
|
1137
|
+
chrome perfectly aligned with PM content without
|
|
1138
|
+
any absolute-positioned overlay that would drift
|
|
1139
|
+
relative to the browser's line layout. */}
|
|
953
1140
|
{props.document}
|
|
1141
|
+
{props.layoutFacet ? (
|
|
1142
|
+
<TwChromeOverlay
|
|
1143
|
+
facet={props.layoutFacet}
|
|
1144
|
+
tableContext={props.tableContext}
|
|
1145
|
+
onSetColumnWidth={props.onSetColumnWidth}
|
|
1146
|
+
onSetRowHeight={props.onSetRowHeight}
|
|
1147
|
+
/>
|
|
1148
|
+
) : null}
|
|
954
1149
|
</div>
|
|
955
1150
|
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
956
1151
|
<div
|
|
@@ -975,6 +1170,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
975
1170
|
</div>
|
|
976
1171
|
</div>
|
|
977
1172
|
</div>
|
|
1173
|
+
{/* Pages 2..N in page mode are now rendered by TwPageStackChrome
|
|
1174
|
+
as absolute overlays above the single flowing PM surface.
|
|
1175
|
+
The PM surface holds all editable content; page frames draw
|
|
1176
|
+
borders, header/footer bands, and per-page numbers on top. */}
|
|
978
1177
|
</div>
|
|
979
1178
|
|
|
980
1179
|
{chromeVisibility.statusBar ? (
|
|
@@ -1021,6 +1220,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1021
1220
|
onRejectRevision={props.onRejectRevision}
|
|
1022
1221
|
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
1023
1222
|
onRejectAllChanges={props.onRejectAllChanges}
|
|
1223
|
+
scopeRailSegments={props.layoutFacet?.getAllScopeRailSegments?.() ?? []}
|
|
1224
|
+
workflowTab={props.reviewRailWorkflowTab}
|
|
1225
|
+
workflowCount={props.reviewRailWorkflowCount}
|
|
1226
|
+
workflowScopesTitle={props.reviewRailWorkflowScopesTitle}
|
|
1227
|
+
intelligenceEyebrow={props.reviewRailIntelligenceEyebrow}
|
|
1228
|
+
intelligenceHeader={props.reviewRailIntelligenceHeader}
|
|
1229
|
+
railFooter={props.reviewRailFooter}
|
|
1024
1230
|
/> : null}
|
|
1025
1231
|
|
|
1026
1232
|
{responsiveChrome.showDrawerReviewRail ? (
|
|
@@ -1062,6 +1268,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1062
1268
|
onRejectRevision={props.onRejectRevision}
|
|
1063
1269
|
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
1064
1270
|
onRejectAllChanges={props.onRejectAllChanges}
|
|
1271
|
+
scopeRailSegments={props.layoutFacet?.getAllScopeRailSegments?.() ?? []}
|
|
1272
|
+
workflowTab={props.reviewRailWorkflowTab}
|
|
1273
|
+
workflowCount={props.reviewRailWorkflowCount}
|
|
1274
|
+
workflowScopesTitle={props.reviewRailWorkflowScopesTitle}
|
|
1275
|
+
intelligenceEyebrow={props.reviewRailIntelligenceEyebrow}
|
|
1276
|
+
intelligenceHeader={props.reviewRailIntelligenceHeader}
|
|
1277
|
+
railFooter={props.reviewRailFooter}
|
|
1065
1278
|
/>
|
|
1066
1279
|
</div>
|
|
1067
1280
|
</div>
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import * as Y from "yjs";
|
|
2
|
-
|
|
3
|
-
import type { DocumentRuntime, DocumentRuntimeEvent, Unsubscribe } from "./document-runtime.ts";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Serialised shapes stored inside the Y.Maps
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
interface YCommentThread {
|
|
10
|
-
commentId: string;
|
|
11
|
-
status: "open" | "resolved" | "detached";
|
|
12
|
-
anchor: { kind: string; [key: string]: unknown };
|
|
13
|
-
createdAt: string;
|
|
14
|
-
createdBy: string;
|
|
15
|
-
authorId: string;
|
|
16
|
-
body: string;
|
|
17
|
-
entries: Array<{
|
|
18
|
-
entryId: string;
|
|
19
|
-
authorId: string;
|
|
20
|
-
body: string;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
}>;
|
|
23
|
-
resolvedAt?: string;
|
|
24
|
-
resolvedBy?: string;
|
|
25
|
-
warningIds: string[];
|
|
26
|
-
sourceClientId: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface YRevisionAction {
|
|
30
|
-
changeId: string;
|
|
31
|
-
action: "accept" | "reject";
|
|
32
|
-
sourceClientId: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Public API
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
export interface CollabReviewSyncHandle {
|
|
40
|
-
destroy(): void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function createCollabReviewSync(
|
|
44
|
-
ydoc: Y.Doc,
|
|
45
|
-
runtime: DocumentRuntime,
|
|
46
|
-
): CollabReviewSyncHandle {
|
|
47
|
-
const yComments = ydoc.getMap<YCommentThread>("comments");
|
|
48
|
-
const yRevisionActions = ydoc.getMap<YRevisionAction>("revisionActions");
|
|
49
|
-
const clientId = ydoc.clientID;
|
|
50
|
-
|
|
51
|
-
let suppressLocalEvents = false;
|
|
52
|
-
|
|
53
|
-
// --- Local → Yjs ---------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
const unsubEvents: Unsubscribe = runtime.subscribeToEvents((event) => {
|
|
56
|
-
if (suppressLocalEvents) return;
|
|
57
|
-
|
|
58
|
-
switch (event.type) {
|
|
59
|
-
case "comment_added":
|
|
60
|
-
pushCommentToYjs(event.commentId);
|
|
61
|
-
break;
|
|
62
|
-
case "comment_resolved":
|
|
63
|
-
syncCommentFieldToYjs(event.commentId);
|
|
64
|
-
break;
|
|
65
|
-
case "change_accepted":
|
|
66
|
-
yRevisionActions.set(revisionActionKey(event.changeId, "accept"), {
|
|
67
|
-
changeId: event.changeId,
|
|
68
|
-
action: "accept",
|
|
69
|
-
sourceClientId: clientId,
|
|
70
|
-
});
|
|
71
|
-
break;
|
|
72
|
-
case "change_rejected":
|
|
73
|
-
yRevisionActions.set(revisionActionKey(event.changeId, "reject"), {
|
|
74
|
-
changeId: event.changeId,
|
|
75
|
-
action: "reject",
|
|
76
|
-
sourceClientId: clientId,
|
|
77
|
-
});
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
function pushCommentToYjs(commentId: string): void {
|
|
83
|
-
const thread = runtime
|
|
84
|
-
.getRenderSnapshot()
|
|
85
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
86
|
-
if (!thread) return;
|
|
87
|
-
|
|
88
|
-
yComments.set(commentId, {
|
|
89
|
-
commentId: thread.commentId,
|
|
90
|
-
status: thread.status,
|
|
91
|
-
anchor: thread.anchor,
|
|
92
|
-
createdAt: thread.createdAt,
|
|
93
|
-
createdBy: thread.createdBy,
|
|
94
|
-
authorId: thread.createdBy,
|
|
95
|
-
body: thread.entries[0]?.body ?? "",
|
|
96
|
-
entries: thread.entries.map((e) => ({
|
|
97
|
-
entryId: e.entryId,
|
|
98
|
-
authorId: e.authorId,
|
|
99
|
-
body: e.body,
|
|
100
|
-
createdAt: e.createdAt,
|
|
101
|
-
})),
|
|
102
|
-
resolvedAt: thread.resolvedAt,
|
|
103
|
-
resolvedBy: thread.resolvedBy,
|
|
104
|
-
warningIds: [],
|
|
105
|
-
sourceClientId: clientId,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function syncCommentFieldToYjs(commentId: string): void {
|
|
110
|
-
const existing = yComments.get(commentId);
|
|
111
|
-
const thread = runtime
|
|
112
|
-
.getRenderSnapshot()
|
|
113
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
114
|
-
if (!existing || !thread) return;
|
|
115
|
-
|
|
116
|
-
yComments.set(commentId, {
|
|
117
|
-
...existing,
|
|
118
|
-
status: thread.status,
|
|
119
|
-
entries: thread.entries.map((e) => ({
|
|
120
|
-
entryId: e.entryId,
|
|
121
|
-
authorId: e.authorId,
|
|
122
|
-
body: e.body,
|
|
123
|
-
createdAt: e.createdAt,
|
|
124
|
-
})),
|
|
125
|
-
resolvedAt: thread.resolvedAt,
|
|
126
|
-
resolvedBy: thread.resolvedBy,
|
|
127
|
-
sourceClientId: clientId,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// --- Yjs → Local ---------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
function onCommentMapChange(event: Y.YMapEvent<YCommentThread>): void {
|
|
134
|
-
suppressLocalEvents = true;
|
|
135
|
-
try {
|
|
136
|
-
for (const [commentId, change] of event.changes.keys) {
|
|
137
|
-
const entry = yComments.get(commentId);
|
|
138
|
-
if (!entry || entry.sourceClientId === clientId) continue;
|
|
139
|
-
|
|
140
|
-
const existing = runtime
|
|
141
|
-
.getRenderSnapshot()
|
|
142
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
143
|
-
|
|
144
|
-
if (change.action === "add" && !existing) {
|
|
145
|
-
runtime.dispatch({
|
|
146
|
-
type: "comment.add",
|
|
147
|
-
comment: {
|
|
148
|
-
commentId: entry.commentId,
|
|
149
|
-
status: entry.status,
|
|
150
|
-
anchor: entry.anchor as never,
|
|
151
|
-
createdAt: entry.createdAt,
|
|
152
|
-
createdBy: entry.createdBy,
|
|
153
|
-
authorId: entry.authorId,
|
|
154
|
-
body: entry.body,
|
|
155
|
-
entries: entry.entries,
|
|
156
|
-
warningIds: entry.warningIds,
|
|
157
|
-
isResolved: entry.status === "resolved",
|
|
158
|
-
metadata: { source: "runtime" },
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
} else if (change.action === "update" && existing) {
|
|
162
|
-
if (entry.status === "resolved" && existing.status !== "resolved") {
|
|
163
|
-
runtime.dispatch({
|
|
164
|
-
type: "comment.resolve",
|
|
165
|
-
commentId,
|
|
166
|
-
resolvedBy: entry.resolvedBy,
|
|
167
|
-
});
|
|
168
|
-
} else if (entry.status === "open" && existing.status === "resolved") {
|
|
169
|
-
runtime.dispatch({ type: "comment.reopen", commentId });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const localEntryCount = existing.entries.length;
|
|
173
|
-
if (entry.entries.length > localEntryCount) {
|
|
174
|
-
for (const newEntry of entry.entries.slice(localEntryCount)) {
|
|
175
|
-
runtime.dispatch({
|
|
176
|
-
type: "comment.add-reply",
|
|
177
|
-
commentId,
|
|
178
|
-
body: newEntry.body,
|
|
179
|
-
authorId: newEntry.authorId,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} finally {
|
|
186
|
-
suppressLocalEvents = false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function onRevisionActionMapChange(event: Y.YMapEvent<YRevisionAction>): void {
|
|
191
|
-
suppressLocalEvents = true;
|
|
192
|
-
try {
|
|
193
|
-
for (const [, change] of event.changes.keys) {
|
|
194
|
-
if (change.action !== "add") continue;
|
|
195
|
-
const entries = [...yRevisionActions.entries()];
|
|
196
|
-
const latest = entries[entries.length - 1];
|
|
197
|
-
if (!latest) continue;
|
|
198
|
-
const entry = latest[1];
|
|
199
|
-
if (entry.sourceClientId === clientId) continue;
|
|
200
|
-
|
|
201
|
-
runtime.dispatch({
|
|
202
|
-
type: entry.action === "accept" ? "change.accept" : "change.reject",
|
|
203
|
-
changeId: entry.changeId,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
} finally {
|
|
207
|
-
suppressLocalEvents = false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
yComments.observe(onCommentMapChange);
|
|
212
|
-
yRevisionActions.observe(onRevisionActionMapChange);
|
|
213
|
-
|
|
214
|
-
// --- Initial sync: push existing comments to Yjs if first client ----------
|
|
215
|
-
|
|
216
|
-
const snapshot = runtime.getRenderSnapshot();
|
|
217
|
-
if (yComments.size === 0 && snapshot.comments.threads.length > 0) {
|
|
218
|
-
ydoc.transact(() => {
|
|
219
|
-
for (const thread of snapshot.comments.threads) {
|
|
220
|
-
yComments.set(thread.commentId, {
|
|
221
|
-
commentId: thread.commentId,
|
|
222
|
-
status: thread.status,
|
|
223
|
-
anchor: thread.anchor,
|
|
224
|
-
createdAt: thread.createdAt,
|
|
225
|
-
createdBy: thread.createdBy,
|
|
226
|
-
authorId: thread.createdBy,
|
|
227
|
-
body: thread.entries[0]?.body ?? "",
|
|
228
|
-
entries: thread.entries.map((e) => ({
|
|
229
|
-
entryId: e.entryId,
|
|
230
|
-
authorId: e.authorId,
|
|
231
|
-
body: e.body,
|
|
232
|
-
createdAt: e.createdAt,
|
|
233
|
-
})),
|
|
234
|
-
resolvedAt: thread.resolvedAt,
|
|
235
|
-
resolvedBy: thread.resolvedBy,
|
|
236
|
-
warningIds: [],
|
|
237
|
-
sourceClientId: clientId,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
destroy() {
|
|
245
|
-
unsubEvents();
|
|
246
|
-
yComments.unobserve(onCommentMapChange);
|
|
247
|
-
yRevisionActions.unobserve(onRevisionActionMapChange);
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function revisionActionKey(changeId: string, action: string): string {
|
|
253
|
-
return `${changeId}:${action}`;
|
|
254
|
-
}
|