Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/super-editor/src/ui/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,32 @@ describe('ui.comments — actions route through editor.doc.*', () => {
ui.destroy();
});

it('createFromCapture accepts a Document API SelectionInfo (the pending-event shape) without a cast', () => {
const { superdoc, mocks } = makeStubs();
const ui = createSuperDocUI({ superdoc });

// Shape of `pendingSelection` on a pending `comments-update` event:
// a SelectionInfo (target + text), NOT a full SelectionCapture (no
// selectionTarget / quotedText). The widened CommentAnchorCapture
// input accepts it directly — no cast here, so this also pins the
// type widening at compile time.
const pendingSelection = {
empty: false,
target: { kind: 'text' as const, segments: [{ blockId: 'p1', range: { start: 0, end: 4 } }] },
activeMarks: [],
activeCommentIds: [],
activeChangeIds: [],
text: 'word',
};

const receipt = ui.comments.createFromCapture(pendingSelection, { text: 'from pending' });

expect(receipt.success).toBe(true);
expect(mocks.create).toHaveBeenCalledWith({ target: pendingSelection.target, text: 'from pending' });

ui.destroy();
});

it('reply forwards to comments.create with parentCommentId set and no target', () => {
const { superdoc, mocks } = makeStubs();
const ui = createSuperDocUI({ superdoc });
Expand Down
1 change: 1 addition & 0 deletions packages/super-editor/src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export type {
UIToolbarCommandState,

// Comments
CommentAnchorCapture,
CommentsHandle,
CommentsSlice,

Expand Down
20 changes: 19 additions & 1 deletion packages/super-editor/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,19 @@ export interface SelectionAnchorRectOptions {
*/
export type SelectionCapture = SelectionSlice;

/**
* The minimal anchor `ui.comments.createFromCapture` needs: anything
* carrying a positional `target`. Both a full {@link SelectionCapture}
* (from `ui.selection.capture()`) and the Document API
* {@link import('@superdoc/document-api').SelectionInfo} satisfy it —
* the latter is what a pending `comments-update` event exposes as
* `pendingSelection`, so consumers can forward it straight to
* `createFromCapture` without first reconstructing a `SelectionCapture`.
*/
export type CommentAnchorCapture = {
target: import('@superdoc/document-api').TextTarget | null;
};

/**
* Aggregate toolbar handle exposed on `ui.toolbar`. Compatible with
* `HeadlessToolbarController` from `superdoc/headless-toolbar` so the
Expand Down Expand Up @@ -1670,8 +1683,13 @@ export interface CommentsHandle {
* pass it here. Routes through `editor.doc.comments.create` with
* the captured `target`. Returns a `NO_OP` receipt when the capture
* lacks a positional target.
*
* Accepts any {@link CommentAnchorCapture} (only `target` is read),
* so the `pendingSelection` from a pending `comments-update` event —
* a Document API `SelectionInfo` — can be passed directly without
* reconstructing a full `SelectionCapture`.
*/
createFromCapture(capture: SelectionCapture, input: { text: string }): import('@superdoc/document-api').Receipt;
createFromCapture(capture: CommentAnchorCapture, input: { text: string }): import('@superdoc/document-api').Receipt;
/**
* Post a reply to an existing thread. Routes through
* `editor.doc.comments.create({ parentCommentId, text })`; the
Expand Down
14 changes: 14 additions & 0 deletions packages/superdoc/src/core/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
FontsResolvedPayload,
ListDefinitionsPayload,
ProofingProvider,
SelectionInfo,
User,
} from '@superdoc/super-editor';

Expand Down Expand Up @@ -1416,6 +1417,19 @@ export interface SuperDocCommentsUpdatePayload {
comment?: Comment;
/** Per-field change set when the update is a mutation. */
changes?: Array<{ key: string; commentId: string; fileId?: string | null }>;
/**
* The Document API selection snapshot captured at the moment a
* `'pending'` comment was started, before the pending mark is
* inserted (which clears the live DOM selection). Present only on the
* `'pending'` event. Forward it straight to
* `ui.comments.createFromCapture(pendingSelection, { text })` to build
* the comment from a custom composer — it removes the need to track
* the selection yourself ahead of the floating-bubble click. A
* non-text or empty selection still yields a `SelectionInfo` whose
* `target` is `null`; the field itself is `null` only when there's no
* active editor.
*/
pendingSelection?: SelectionInfo | null;
}

export interface EditorTransactionEvent {
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/public/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type {
CommandHandle,
CommandsHandle,
CommentAddress,
CommentAnchorCapture,
CommentInfo,
CommentsHandle,
CommentsListQuery,
Expand Down
10 changes: 9 additions & 1 deletion packages/superdoc/src/stores/comments-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,15 @@ export const useCommentsStore = defineStore('comments', () => {
};

const showAddComment = (superdoc, targetClientY = null) => {
const event = { type: COMMENT_EVENTS.PENDING };
// Snapshot the selection BEFORE `insertComment('pending')` below adds
// the pending mark and the floating-bubble click clears the live DOM
// selection. This is the Document API `SelectionInfo` (carries
// `target`); consumers forward it to `ui.comments.createFromCapture`
// instead of tracking the selection themselves. A non-text/empty
// selection still yields a `SelectionInfo` with `target: null`; the
// value is `null` only when there's no active editor / selection API.
const pendingSelection = superdoc.activeEditor?.doc?.selection?.current?.({ includeText: true }) ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate pendingSelection to SuperEditor selections

When the comment tool is invoked from a PDF selection (activeSelection.source === 'pdf' in the shared toolbar flow), superdoc.activeEditor can still point at a DOCX editor, so this call captures that editor's stale Document API selection and emits it as pendingSelection for the PDF pending comment. Consumers following the new guidance and passing that payload to ui.comments.createFromCapture would then create/anchor a DOCX comment instead of treating the PDF comment as having no Document API text target; gate this snapshot on the active selection/source (or document id) and emit null outside SuperEditor text selections.

Useful? React with 👍 / 👎.

const event = { type: COMMENT_EVENTS.PENDING, pendingSelection };
superdoc.emit('comments-update', event);

const selection = { ...superdocStore.activeSelection };
Expand Down
78 changes: 78 additions & 0 deletions packages/superdoc/src/stores/comments-store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,84 @@ describe('comments-store', () => {
expect(setActiveCommentSpy).toHaveBeenCalledWith({ commentId: null });
});

describe('showAddComment pendingSelection (IT-1113)', () => {
const makePendingSuperdoc = (selectionInfo) => {
const insertComment = vi.fn();
const current = vi.fn(() => selectionInfo);
return {
superdoc: {
emit: vi.fn(),
config: { isInternal: false },
activeEditor: {
doc: { selection: { current } },
commands: { insertComment },
},
},
insertComment,
current,
};
};

it('emits the pre-mutation Document API selection snapshot on the pending event', () => {
const selectionInfo = {
empty: false,
target: { kind: 'text', segments: [{ blockId: 'b1', range: { start: 0, end: 5 } }] },
activeMarks: [],
activeCommentIds: [],
activeChangeIds: [],
text: 'hello',
};
const { superdoc, current } = makePendingSuperdoc(selectionInfo);

store.showAddComment(superdoc);

expect(current).toHaveBeenCalledWith({ includeText: true });
expect(superdoc.emit).toHaveBeenCalledWith('comments-update', {
type: comments_module_events.PENDING,
pendingSelection: selectionInfo,
});
});

it('emits the pending event before inserting the pending mark', () => {
const { superdoc, insertComment } = makePendingSuperdoc({
empty: false,
target: { kind: 'text', segments: [{ blockId: 'b1', range: { start: 0, end: 1 } }] },
activeMarks: [],
activeCommentIds: [],
activeChangeIds: [],
});

store.showAddComment(superdoc);

// The snapshot is taken (and emitted) before the pending mark is
// inserted — otherwise the captured target could shift.
expect(insertComment).toHaveBeenCalled();
expect(superdoc.emit.mock.invocationCallOrder[0]).toBeLessThan(insertComment.mock.invocationCallOrder[0]);
});

it('carries a null-target snapshot when the selection has no addressable target', () => {
const selectionInfo = { empty: true, target: null, activeMarks: [], activeCommentIds: [], activeChangeIds: [] };
const { superdoc } = makePendingSuperdoc(selectionInfo);

store.showAddComment(superdoc);

const [, payload] = superdoc.emit.mock.calls[0];
expect(payload.type).toBe(comments_module_events.PENDING);
expect(payload.pendingSelection).toEqual(selectionInfo);
expect(payload.pendingSelection.target).toBeNull();
});

it('emits pendingSelection null when there is no active editor', () => {
const superdoc = { emit: vi.fn(), config: { isInternal: false }, activeEditor: undefined };

store.showAddComment(superdoc);

const [, payload] = superdoc.emit.mock.calls[0];
expect(payload.type).toBe(comments_module_events.PENDING);
expect(payload.pendingSelection).toBeNull();
});
});

it('preserves the active floating instance when it belongs to the activated thread', () => {
const setActiveCommentSpy = vi.fn();
const superdoc = {
Expand Down
1 change: 1 addition & 0 deletions packages/superdoc/src/ui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
type CommandHandle,
type CommandsHandle,
type CommentAddress,
type CommentAnchorCapture,
type CommentInfo,
type CommentsHandle,
type CommentsListQuery,
Expand Down
Loading