diff --git a/package.json b/package.json index 7025e716d..aa65ef3c4 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,13 @@ "@babel/preset-react": "^7.25.9", "@babel/preset-typescript": "^7.25.9", "@box/frontend": "^10.0.0", - "@box/blueprint-web": "^14.27.0", - "@box/blueprint-web-assets": "^4.116.6", - "@box/collaboration-popover": "^1.61.30", + "@box/blueprint-web": "^14.30.0", + "@box/blueprint-web-assets": "^4.117.2", + "@box/collaboration-popover": "^1.62.3", "@box/languages": "^1.1.0", - "@box/readable-time": "^1.40.30", - "@box/threaded-annotations": "^1.86.0", - "@box/user-selector": "^1.75.30", + "@box/readable-time": "^1.41.3", + "@box/threaded-annotations": "^1.89.1", + "@box/user-selector": "^1.76.3", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.2.0", @@ -128,12 +128,12 @@ "worker-farm": "^1.7.0" }, "peerDependencies": { - "@box/blueprint-web": "^14.27.0", - "@box/blueprint-web-assets": "^4.116.6", - "@box/collaboration-popover": "^1.61.30", - "@box/readable-time": "^1.40.30", - "@box/threaded-annotations": "^1.86.0", - "@box/user-selector": "^1.75.30" + "@box/blueprint-web": "^14.30.0", + "@box/blueprint-web-assets": "^4.117.2", + "@box/collaboration-popover": "^1.62.3", + "@box/readable-time": "^1.41.3", + "@box/threaded-annotations": "^1.89.1", + "@box/user-selector": "^1.76.3" }, "scripts": { "build": "yarn setup && yarn build:prod:dist", diff --git a/src/@types/model.ts b/src/@types/model.ts index 49ca4caa2..1bfaece72 100644 --- a/src/@types/model.ts +++ b/src/@types/model.ts @@ -86,6 +86,13 @@ export interface Rect extends Position { width: number; } +export interface ReplyPermissions { + can_delete?: boolean; + can_edit?: boolean; + can_reply?: boolean; + can_resolve?: boolean; +} + export interface Reply { created_at: string; created_by: User; @@ -95,6 +102,7 @@ export interface Reply { id: string; type: string; }; + permissions?: ReplyPermissions; type: 'reply'; } diff --git a/src/BoxAnnotations.ts b/src/BoxAnnotations.ts index d5770dea9..0cfe8e792 100644 --- a/src/BoxAnnotations.ts +++ b/src/BoxAnnotations.ts @@ -16,6 +16,7 @@ type Annotator = { type AnnotationsOptions = { features: Features; intl: IntlOptions; + onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void; }; export type Features = { diff --git a/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts b/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts index ea0f3de07..08cc0a047 100644 --- a/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts +++ b/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts @@ -133,16 +133,31 @@ describe('threadedAnnotationsAdapters', () => { }); }); - test('should set default permissions for replies', () => { + test('should default permissions to false when reply has no permissions field', () => { const result = replyToTextMessage(mockReply); expect(result.permissions).toEqual({ canDelete: false, canEdit: false, - canReply: true, + canReply: false, canResolve: false, }); }); + + test('should map reply permissions from backend payload, forcing canEdit false', () => { + const reply: Reply = { + ...mockReply, + permissions: { can_delete: true, can_edit: true, can_reply: true, can_resolve: true }, + }; + const result = replyToTextMessage(reply); + + expect(result.permissions).toEqual({ + canDelete: true, + canEdit: false, + canReply: true, + canResolve: true, + }); + }); }); describe('annotationToMessages', () => { @@ -162,22 +177,19 @@ describe('threadedAnnotationsAdapters', () => { expect(result).toEqual([]); }); - test('should include description as first message', () => { + test('should include description as first message using annotation-level author and permissions', () => { const annotation: Annotation = { ...baseAnnotation, - description: { - created_at: '2026-01-01T00:00:00Z', - created_by: { id: '1', login: 'user@box.com', name: 'User', type: 'user' }, - id: 'desc-1', - message: 'Root message', - parent: { id: 'ann-1', type: 'annotation' }, - type: 'reply', - }, + description: { message: 'Root message' } as unknown as Reply, }; const result = annotationToMessages(annotation); expect(result).toHaveLength(1); - expect(result[0].id).toBe('desc-1'); + expect(result[0].id).toBe('ann-1'); + expect(result[0].author.name).toBe('User'); + expect(result[0].author.email).toBe('user@box.com'); + expect(result[0].author.id).toBe(1); + expect(result[0].createdAt).toBe(new Date('2026-01-01T00:00:00Z').getTime()); expect(result[0].permissions.canDelete).toBe(true); expect(result[0].permissions.canEdit).toBe(true); }); @@ -185,14 +197,7 @@ describe('threadedAnnotationsAdapters', () => { test('should include description and replies in order', () => { const annotation: Annotation = { ...baseAnnotation, - description: { - created_at: '2026-01-01T00:00:00Z', - created_by: { id: '1', login: 'user@box.com', name: 'User', type: 'user' }, - id: 'desc-1', - message: 'Root', - parent: { id: 'ann-1', type: 'annotation' }, - type: 'reply', - }, + description: { message: 'Root' } as unknown as Reply, replies: [ { created_at: '2026-01-02T00:00:00Z', @@ -207,42 +212,12 @@ describe('threadedAnnotationsAdapters', () => { const result = annotationToMessages(annotation); expect(result).toHaveLength(2); - expect(result[0].id).toBe('desc-1'); - expect(result[1].id).toBe('reply-1'); - }); - - test('should fall back to annotation fields when description is sparse', () => { - const annotation: Annotation = { - ...baseAnnotation, - description: { - message: 'Sparse description', - } as unknown as Reply, - }; - const result = annotationToMessages(annotation); - - expect(result).toHaveLength(1); expect(result[0].id).toBe('ann-1'); + expect(result[1].id).toBe('reply-1'); expect(result[0].author.name).toBe('User'); - expect(result[0].author.email).toBe('user@box.com'); - expect(result[0].author.id).toBe(1); - expect(result[0].createdAt).toBe(new Date('2026-01-01T00:00:00Z').getTime()); + expect(result[1].author.name).toBe('Other'); }); - test('should handle description with missing created_by gracefully', () => { - const annotation: Annotation = { - ...baseAnnotation, - description: { - id: 'desc-1', - message: 'Test', - created_at: '2026-01-01T00:00:00Z', - } as unknown as Reply, - }; - const result = annotationToMessages(annotation); - - expect(result).toHaveLength(1); - expect(result[0].author.name).toBe('User'); - expect(result[0].author.email).toBe('user@box.com'); - }); }); describe('collaboratorToUserContact', () => { diff --git a/src/adapters/threadedAnnotationsAdapters.ts b/src/adapters/threadedAnnotationsAdapters.ts index 5cd1e7c6b..47e3a0434 100644 --- a/src/adapters/threadedAnnotationsAdapters.ts +++ b/src/adapters/threadedAnnotationsAdapters.ts @@ -82,35 +82,28 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({ id: reply.id, message: deserializeMentionMarkup(reply.message), permissions: { - canDelete: false, + canDelete: reply.permissions?.can_delete ?? false, canEdit: false, - canReply: true, - canResolve: false, + canReply: reply.permissions?.can_reply ?? false, + canResolve: reply.permissions?.can_resolve ?? false, }, }); -/** - * Converts an Annotation's description (root message) to a TextMessageType. - * The root message gets resolve permission from the annotation's permissions. - * Falls back to annotation-level fields when the description object is sparse - * (the list endpoint returns description with only { message }). - */ -const descriptionToTextMessage = ( - annotation: Annotation, - reply: Reply, -): TextMessageTypeV2 => ({ +// The root message shares the annotation's author and permissions; description +// comes back sparse ({ message } only) from the list endpoint. +const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 => ({ author: { - email: (reply.created_by ?? annotation.created_by)?.login ?? '', - id: parseInt((reply.created_by ?? annotation.created_by)?.id ?? '0', 10), - name: (reply.created_by ?? annotation.created_by)?.name ?? '', + email: annotation.created_by?.login ?? '', + id: parseInt(annotation.created_by?.id ?? '0', 10), + name: annotation.created_by?.name ?? '', }, - createdAt: new Date(reply.created_at ?? annotation.created_at).getTime(), - id: reply.id ?? annotation.id, - message: deserializeMentionMarkup(reply.message), + createdAt: new Date(annotation.created_at).getTime(), + id: annotation.id, + message: deserializeMentionMarkup(annotation.description?.message ?? ''), permissions: { canDelete: annotation.permissions?.can_delete ?? false, canEdit: annotation.permissions?.can_edit ?? false, - canReply: true, + canReply: annotation.permissions?.can_reply ?? false, canResolve: annotation.permissions?.can_resolve ?? false, }, }); @@ -123,7 +116,7 @@ export const annotationToMessages = (annotation: Annotation): TextMessageTypeV2[ const messages: TextMessageTypeV2[] = []; if (annotation.description) { - messages.push(descriptionToTextMessage(annotation, annotation.description)); + messages.push(descriptionToTextMessage(annotation)); } if (annotation.replies) { diff --git a/src/common/AnnotationCallbacksContext.ts b/src/common/AnnotationCallbacksContext.ts new file mode 100644 index 000000000..637c2a2e8 --- /dev/null +++ b/src/common/AnnotationCallbacksContext.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +export type AnnotationCallbacks = { + onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void; +}; + +export default createContext({}); diff --git a/src/common/BaseAnnotator.ts b/src/common/BaseAnnotator.ts index 4e1a31325..f5e74fb1f 100644 --- a/src/common/BaseAnnotator.ts +++ b/src/common/BaseAnnotator.ts @@ -44,6 +44,7 @@ export type Options = { initialViewMode?: ViewMode; intl: IntlOptions; locale?: string; + onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void; token: string; }; @@ -70,6 +71,8 @@ export default class BaseAnnotator extends EventEmitter { intl: IntlShape; + onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void; + store: store.AppStore; constructor({ @@ -82,6 +85,7 @@ export default class BaseAnnotator extends EventEmitter { initialMode, initialViewMode = 'annotations', intl, + onCopyLink, token, }: Options) { super(); @@ -111,6 +115,7 @@ export default class BaseAnnotator extends EventEmitter { this.container = container; this.features = features; this.intl = i18n.createIntlProvider(intl); + this.onCopyLink = onCopyLink; this.store = store.createStore(initialState, { api: new API({ apiHost, token }), }); diff --git a/src/common/BaseManager.ts b/src/common/BaseManager.ts index 9f31669c4..2ffcf02c8 100644 --- a/src/common/BaseManager.ts +++ b/src/common/BaseManager.ts @@ -1,6 +1,7 @@ import * as ReactDOM from 'react-dom/client'; import { IntlShape } from 'react-intl'; import { Store } from 'redux'; +import { AnnotationCallbacks } from './AnnotationCallbacksContext'; import { applyResinTags } from '../utils/resin'; import { TARGET_TYPE } from '../constants'; @@ -14,6 +15,7 @@ export type Options = { }; export type Props = { + callbacks?: AnnotationCallbacks; intl: IntlShape; store: Store; }; diff --git a/src/common/__tests__/BaseAnnotator-test.ts b/src/common/__tests__/BaseAnnotator-test.ts index fcf77949a..408bedd4a 100644 --- a/src/common/__tests__/BaseAnnotator-test.ts +++ b/src/common/__tests__/BaseAnnotator-test.ts @@ -153,6 +153,19 @@ describe('BaseAnnotator', () => { expect(annotator.features).toEqual(features); }); + test('should store onCopyLink callback when provided', () => { + const onCopyLink = jest.fn(); + initAnnotator({ onCopyLink }); + + expect(annotator.onCopyLink).toBe(onCopyLink); + }); + + test('should leave onCopyLink undefined when not provided', () => { + initAnnotator(); + + expect(annotator.onCopyLink).toBeUndefined(); + }); + test('should set initial mode and color', () => { initAnnotator({ initialColor: '#000', initialMode: Mode.REGION }); diff --git a/src/common/withProviders.tsx b/src/common/withProviders.tsx index f59975017..5a78cd972 100644 --- a/src/common/withProviders.tsx +++ b/src/common/withProviders.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; import { IntlShape, RawIntlProvider } from 'react-intl'; import { Provider as StoreProvider } from 'react-redux'; import { Store } from 'redux'; +import AnnotationCallbacksContext, { AnnotationCallbacks } from './AnnotationCallbacksContext'; type WrapperProps = { + callbacks?: AnnotationCallbacks; intl: IntlShape; store: Store; }; @@ -11,11 +13,13 @@ type WrapperProps = { type WrapperReturn

= React.FC

; export default function withProviders

(WrappedComponent: React.ComponentType

): WrapperReturn

{ - return function RootProvider({ intl, store, ...rest }: P & WrapperProps): JSX.Element { + return function RootProvider({ callbacks, intl, store, ...rest }: P & WrapperProps): JSX.Element { return ( - + + + ); diff --git a/src/components/Popups/PopupV2.tsx b/src/components/Popups/PopupV2.tsx index e5c38ab72..81b3a3d31 100644 --- a/src/components/Popups/PopupV2.tsx +++ b/src/components/Popups/PopupV2.tsx @@ -17,6 +17,7 @@ import type { JSONContent } from '@tiptap/core'; import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap'; import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters'; +import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext'; import { createReplyAction, deleteAnnotationAction, @@ -24,7 +25,7 @@ import { updateAnnotationAction, } from '../../store/annotations/actions'; import { getAnnotation } from '../../store/annotations/selectors'; -import { getApiHost, getToken } from '../../store/options'; +import { getApiHost, getFileVersionId, getToken } from '../../store/options'; import { fetchCollaboratorsAction } from '../../store/users/actions'; import type { AppState, AppThunkDispatch } from '../../store/types'; @@ -80,12 +81,21 @@ const fetchAvatarBlob = async (apiHost: string, token: string, userId: string): const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => { const intl = useIntl(); const dispatch = useDispatch(); + const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext); const popupRef = React.useRef(null); const popperRef = React.useRef(); const optionsRef = React.useRef>(getPopupOptions()); const apiHost = useSelector(getApiHost); + const fileVersionId = useSelector(getFileVersionId); const token = useSelector(getToken); + const onCopyLink = React.useMemo( + () => + consumerOnCopyLink && annotationId && fileVersionId + ? () => consumerOnCopyLink({ annotationId, fileVersionId }) + : undefined, + [consumerOnCopyLink, annotationId, fileVersionId], + ); const annotation = useSelector((state: AppState) => annotationId ? getAnnotation(state, annotationId) : undefined, ); @@ -235,6 +245,16 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J [annotationId, dispatch, onSubmit], ); + const handleEdit = React.useCallback( + async (id: string, content: JSONContent | null): Promise => { + if (!annotationId || id !== annotationId) return; + const doc = createDocumentNode(content); + const { text } = serializeMentionMarkup(doc); + await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } })); + }, + [annotationId, dispatch], + ); + const handleThreadDelete = React.useCallback( async (): Promise => { if (annotationId) { @@ -293,7 +313,9 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J isResolved={isResolved} messages={threadMessages} onAvatarClick={noop} + onCopyLink={onCopyLink} onDelete={noop} + onEdit={handleEdit} onPost={handlePost} onResolve={handleResolve} onThreadDelete={handleThreadDelete} diff --git a/src/components/Popups/__tests__/PopupV2-test.tsx b/src/components/Popups/__tests__/PopupV2-test.tsx index cb070e294..57e19eab2 100644 --- a/src/components/Popups/__tests__/PopupV2-test.tsx +++ b/src/components/Popups/__tests__/PopupV2-test.tsx @@ -1,8 +1,11 @@ import React from 'react'; import { act, render, screen } from '@testing-library/react'; import { useDispatch, useSelector } from 'react-redux'; +import type { ThreadedAnnotationsPropsV2 } from '@box/threaded-annotations'; +import AnnotationCallbacksContext from '../../../common/AnnotationCallbacksContext'; import PopupV2, { Props } from '../PopupV2'; -import { getApiHost, getToken } from '../../../store/options'; +import { updateAnnotationAction } from '../../../store/annotations/actions'; +import { getApiHost, getFileVersionId, getToken } from '../../../store/options'; jest.mock('react-redux', () => ({ useDispatch: jest.fn(), @@ -27,6 +30,8 @@ jest.mock('@box/blueprint-web', () => ({ TooltipProvider: ({ children }: { children: React.ReactNode }) => children, })); +let lastThreadedAnnotationsProps: Partial = {}; + jest.mock('@box/threaded-annotations', () => { const ReactMock = jest.requireActual('react'); return { @@ -37,17 +42,20 @@ jest.mock('@box/threaded-annotations', () => { 'data-testid': 'message-editor-v2', 'data-is-first-annotation': String(props.isFirstAnnotation), }), - ThreadedAnnotationsV2: (props: Record) => - ReactMock.createElement('div', { + ThreadedAnnotationsV2: (props: Partial) => { + lastThreadedAnnotationsProps = props; + return ReactMock.createElement('div', { 'data-testid': 'threaded-annotations-v2', 'data-is-annotations': String(props.isAnnotations), - 'data-messages-count': String((props.messages as unknown[])?.length ?? 0), + 'data-messages-count': String(props.messages?.length ?? 0), + 'data-has-on-edit': String(typeof props.onEdit === 'function'), 'data-has-on-post': String(typeof props.onPost === 'function'), 'data-has-on-resolve': String(typeof props.onResolve === 'function'), 'data-has-on-thread-delete': String(typeof props.onThreadDelete === 'function'), 'data-has-on-unresolve': String(typeof props.onUnresolve === 'function'), - }), - serializeMentionMarkup: jest.fn().mockReturnValue({ hasMention: false, text: '' }), + }); + }, + serializeMentionMarkup: jest.fn().mockReturnValue({ hasMention: false, text: 'serialized text' }), }; }); @@ -72,6 +80,7 @@ const mockUseSelector = useSelector as jest.MockedFunction; const mockSelectorValues = (annotation?: unknown): void => { mockUseSelector.mockImplementation(selector => { if (selector === getApiHost) return 'https://api.box.com'; + if (selector === getFileVersionId) return 'fv-1'; if (selector === getToken) return 'test-token'; return annotation; }); @@ -119,6 +128,7 @@ describe('PopupV2', () => { }; beforeEach(() => { + lastThreadedAnnotationsProps = {}; mockUseDispatch.mockReturnValue(mockDispatch); mockFetch.mockResolvedValue({ blob: () => Promise.resolve(new Blob(['avatar'])), @@ -214,12 +224,73 @@ describe('PopupV2', () => { await flushPromises(); const thread = screen.getByTestId('threaded-annotations-v2'); + expect(thread.getAttribute('data-has-on-edit')).toBe('true'); expect(thread.getAttribute('data-has-on-post')).toBe('true'); expect(thread.getAttribute('data-has-on-resolve')).toBe('true'); expect(thread.getAttribute('data-has-on-thread-delete')).toBe('true'); expect(thread.getAttribute('data-has-on-unresolve')).toBe('true'); }); + test('should dispatch updateAnnotationAction when editing the root message', async () => { + render(); + await flushPromises(); + + await lastThreadedAnnotationsProps.onEdit?.('annotation-1', { type: 'doc', content: [] }); + + expect(updateAnnotationAction).toHaveBeenCalledWith({ + annotationId: 'annotation-1', + payload: { message: 'serialized text' }, + }); + }); + + test('should not dispatch updateAnnotationAction when editing a reply', async () => { + render(); + await flushPromises(); + + await lastThreadedAnnotationsProps.onEdit?.('reply-1', { type: 'doc', content: [] }); + + expect(updateAnnotationAction).not.toHaveBeenCalled(); + }); + + test('should invoke context onCopyLink with the root annotationId and fileVersionId regardless of clicked message id', async () => { + const onCopyLink = jest.fn(); + render( + + + , + ); + await flushPromises(); + + (lastThreadedAnnotationsProps.onCopyLink as (id: string) => void)('reply-1'); + + expect(onCopyLink).toHaveBeenCalledWith({ annotationId: 'annotation-1', fileVersionId: 'fv-1' }); + }); + + test('should leave onCopyLink undefined when fileVersionId is missing from the store', async () => { + const onCopyLink = jest.fn(); + mockUseSelector.mockImplementation(selector => { + if (selector === getApiHost) return 'https://api.box.com'; + if (selector === getFileVersionId) return null; + if (selector === getToken) return 'test-token'; + return mockAnnotation; + }); + render( + + + , + ); + await flushPromises(); + + expect(lastThreadedAnnotationsProps.onCopyLink).toBeUndefined(); + }); + + test('should leave onCopyLink undefined when no context value is provided', async () => { + render(); + await flushPromises(); + + expect(lastThreadedAnnotationsProps.onCopyLink).toBeUndefined(); + }); + test('should set popupThreadV2 as resin component', async () => { render(); await flushPromises(); diff --git a/src/document/DocumentAnnotator.ts b/src/document/DocumentAnnotator.ts index cd01b7fa1..16b99b216 100644 --- a/src/document/DocumentAnnotator.ts +++ b/src/document/DocumentAnnotator.ts @@ -224,6 +224,7 @@ export default class DocumentAnnotator extends BaseAnnotator { } manager.render({ + callbacks: { onCopyLink: this.onCopyLink }, intl: this.intl, store: this.store, }); diff --git a/src/document/__tests__/DocumentAnnotator-test.ts b/src/document/__tests__/DocumentAnnotator-test.ts index e3830893e..0099ee69e 100644 --- a/src/document/__tests__/DocumentAnnotator-test.ts +++ b/src/document/__tests__/DocumentAnnotator-test.ts @@ -327,6 +327,7 @@ describe('DocumentAnnotator', () => { expect(annotator.getPageManagers).toHaveBeenCalledWith(pageEl); expect(annotator.getPageNumber).toHaveBeenCalledWith(pageEl); expect(mockManager.render).toHaveBeenCalledWith({ + callbacks: { onCopyLink: undefined }, intl: annotator.intl, store: expect.any(Object), }); diff --git a/src/image/ImageAnnotator.ts b/src/image/ImageAnnotator.ts index 1bd6c2291..190f58eba 100644 --- a/src/image/ImageAnnotator.ts +++ b/src/image/ImageAnnotator.ts @@ -124,6 +124,7 @@ export default class ImageAnnotator extends BaseAnnotator { }); manager.render({ + callbacks: { onCopyLink: this.onCopyLink }, intl: this.intl, store: this.store, }); diff --git a/src/image/__tests__/ImageAnnotator-test.ts b/src/image/__tests__/ImageAnnotator-test.ts index 94f23d1e9..dacf87948 100644 --- a/src/image/__tests__/ImageAnnotator-test.ts +++ b/src/image/__tests__/ImageAnnotator-test.ts @@ -202,6 +202,7 @@ describe('ImageAnnotator', () => { expect(annotator.getManagers).toHaveBeenCalled(); expect(mockManager.render).toHaveBeenCalledWith({ + callbacks: { onCopyLink: undefined }, intl: annotator.intl, store: expect.any(Object), }); diff --git a/src/media/MediaAnnotator.ts b/src/media/MediaAnnotator.ts index 7d74c98ac..60207a330 100644 --- a/src/media/MediaAnnotator.ts +++ b/src/media/MediaAnnotator.ts @@ -108,6 +108,7 @@ export default class MediaAnnotator extends BaseAnnotator { width: `${referenceEl.offsetWidth}px`, }); manager.render({ + callbacks: { onCopyLink: this.onCopyLink }, intl: this.intl, store: this.store, }); diff --git a/src/media/__tests__/MediaAnnotator-test.ts b/src/media/__tests__/MediaAnnotator-test.ts index dda9c18cd..4c34484a8 100644 --- a/src/media/__tests__/MediaAnnotator-test.ts +++ b/src/media/__tests__/MediaAnnotator-test.ts @@ -114,6 +114,7 @@ describe('MediaAnnotator', () => { expect(annotator.getManagers).toHaveBeenCalled(); expect(mockManager.render).toHaveBeenCalledWith({ + callbacks: { onCopyLink: undefined }, intl: annotator.intl, store: expect.any(Object), }); diff --git a/src/popup/PopupManager.tsx b/src/popup/PopupManager.tsx index 322c739af..09ba4e3f2 100644 --- a/src/popup/PopupManager.tsx +++ b/src/popup/PopupManager.tsx @@ -20,16 +20,18 @@ export default class PopupManager extends BaseManager { this.reactEl.dataset.testid = 'ba-Layer--popup'; } - render(props: Props): void { + render({ callbacks, intl, store }: Props): void { if (!this.root) { this.root = ReactDOM.createRoot(this.reactEl); } this.root.render( , ); diff --git a/src/popup/__tests__/PopupManager-test.tsx b/src/popup/__tests__/PopupManager-test.tsx index 29048b58f..3933a0766 100644 --- a/src/popup/__tests__/PopupManager-test.tsx +++ b/src/popup/__tests__/PopupManager-test.tsx @@ -67,5 +67,16 @@ describe('PopupManager', () => { expect(root.render).toHaveBeenCalled(); }); + + test('should forward callbacks to PopupContainer', () => { + const wrapper = getWrapper(); + const root = createRoot(rootEl); + const onCopyLink = jest.fn(); + + wrapper.render({ callbacks: { onCopyLink }, intl, store: createStore() }); + + const element = (root.render as jest.Mock).mock.calls[0][0]; + expect(element.props.callbacks).toEqual({ onCopyLink }); + }); }); }); diff --git a/yarn.lock b/yarn.lock index c6b840b2d..4d935d7a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1027,19 +1027,19 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@box/blueprint-web-assets@^4.116.6": - version "4.116.6" - resolved "https://registry.yarnpkg.com/@box/blueprint-web-assets/-/blueprint-web-assets-4.116.6.tgz#9475300589e7467dcb76c197b85f2a06c346f018" - integrity sha512-RAA4MEjVGzXgPouyVtu6PSB5p/wvGId3gho2Aw0vPz4pB4pUV71W3gZzu+y+rOAAnayTeodGqjq9jlB2/ET3PQ== +"@box/blueprint-web-assets@^4.117.2": + version "4.117.2" + resolved "https://registry.yarnpkg.com/@box/blueprint-web-assets/-/blueprint-web-assets-4.117.2.tgz#f3793b0ca111baa67eec5f6d1d12e7db8b505c75" + integrity sha512-9PbdGk8R6qkhdcYSRRtTuynSKPfOT0MxEPP256u2IWYv/y4Nmp5HB9dyGJKbOLKLSJsf88Fo3tfHKEYeFZKKfg== -"@box/blueprint-web@^14.27.0": - version "14.27.0" - resolved "https://registry.yarnpkg.com/@box/blueprint-web/-/blueprint-web-14.27.0.tgz#a93f91dcca9c66c6b5372af887bbbd715607ae16" - integrity sha512-DMZJL/DVG4YFJ8hnEjmLCY3fOg2b/WQ+O5pmAmBpguYJuPOv/6Ce8mpw9IfhmupZDgQGssx1lUdgJPdsVpniXg== +"@box/blueprint-web@^14.30.0": + version "14.30.0" + resolved "https://registry.yarnpkg.com/@box/blueprint-web/-/blueprint-web-14.30.0.tgz#c0dac3b29e6dcd3b6a827387eadea78113639bef" + integrity sha512-x0gJPInCmPxPn/hvdKcH0+3i/SDIC97hhnnnXKthryq3VTgWDz0alexcsPvfubmLlszmMuaIuVeihekYpwFlHg== dependencies: "@ariakit/react" "0.4.21" "@ariakit/react-core" "0.4.21" - "@box/blueprint-web-assets" "^4.116.6" + "@box/blueprint-web-assets" "^4.117.2" "@internationalized/date" "^3.12.0" "@radix-ui/react-accordion" "1.1.2" "@radix-ui/react-checkbox" "1.0.4" @@ -1068,10 +1068,10 @@ react-aria-components "1.16.0" type-fest "^3.2.0" -"@box/collaboration-popover@^1.61.30": - version "1.61.30" - resolved "https://registry.yarnpkg.com/@box/collaboration-popover/-/collaboration-popover-1.61.30.tgz#269a82fabd9b574e673f4cbf408b003e62e1dfa7" - integrity sha512-vdbJCDEOhy8OstSAbDG8DDxRyYDtc8cAOr0WKV74hrK0QPHIEy3nY68Gah9iyvDe3iIJQ7WTo8JkacBRoONKvw== +"@box/collaboration-popover@^1.62.3": + version "1.62.3" + resolved "https://registry.yarnpkg.com/@box/collaboration-popover/-/collaboration-popover-1.62.3.tgz#b1cea34df5df771373af57ad0a27dc85a830a3e6" + integrity sha512-eu71Hra6p4QPwaVjQbvR36oLbyukIGSt+nEVvSQzM3pnL+KwD05JJEn2OFnzrPaQUQgUV0bhYzBRFvOADzLS4w== "@box/frontend@^10.0.0": version "10.0.0" @@ -1087,15 +1087,15 @@ resolved "https://registry.yarnpkg.com/@box/languages/-/languages-1.1.0.tgz#58bef2d4166d344aa316919e5dc09c7149ab801f" integrity sha512-nGP1D5mNPwKajbl6i0NERXvHzK56HNjrRnkJlhbVl62IlpgCJtayEpRPWWAbSAC4NFyku03KieavaaXnWalx+A== -"@box/readable-time@^1.40.30": - version "1.40.30" - resolved "https://registry.yarnpkg.com/@box/readable-time/-/readable-time-1.40.30.tgz#dc9a8a4b146532b191b519a2300b21713a6dca6d" - integrity sha512-uvMvH4Bl2e/6jNP/MadvckvWSgHuc3xnZD59fhZtdRYlYgi/e+eaGRHjOYJm0shDoMEDBnUeyHbEZpbkq0yu9g== +"@box/readable-time@^1.41.3": + version "1.41.3" + resolved "https://registry.yarnpkg.com/@box/readable-time/-/readable-time-1.41.3.tgz#423d19e3bb97b08f233a3a1543586dc619b953ff" + integrity sha512-6igs7l8gxXcG8f3fQ+uSl+KE+d3Z+rFU2oyWITw7WoswzQn0w3shezKa27vLT/7Rxa22yxzzkJXUEPZ/j7gIHw== -"@box/threaded-annotations@^1.86.0": - version "1.86.0" - resolved "https://registry.yarnpkg.com/@box/threaded-annotations/-/threaded-annotations-1.86.0.tgz#405e26d2741abfc0862444dea4d792f46cab7e5a" - integrity sha512-0b2xdKCLPfEodG5tLuTe/ErmdHSGWEUR9XDP9DDDrurolRh2eH8lmf/3GznJlFSmuUUUr5xhQTJEos6ZBfB1Nw== +"@box/threaded-annotations@^1.89.1": + version "1.89.1" + resolved "https://registry.yarnpkg.com/@box/threaded-annotations/-/threaded-annotations-1.89.1.tgz#c777560d04150fa4c4039fbeef11edcd59fb2c47" + integrity sha512-vqWhinWEriUjzw1iqVA6/PYN8FI2RBDWaOqIzS5qRgLldr/prwbyjTLGnORvYWPQ0Hsx+1dXjuR2KjCh6pFZBA== dependencies: "@tanstack/react-virtual" "^3.10.8" "@tiptap/core" "2.0.2" @@ -1109,10 +1109,10 @@ "@tiptap/suggestion" "2.0.2" uuid "^9.0.1" -"@box/user-selector@^1.75.30": - version "1.75.30" - resolved "https://registry.yarnpkg.com/@box/user-selector/-/user-selector-1.75.30.tgz#a6e634aa37420cb0d75b18736483c4ff9f2623f8" - integrity sha512-bAfONk/fWvKwxBkDKd2f/j4/T8hW9++9tzv6Xoay9ool0Q4rTeRrJFXpdZ75WUXd+Grt/kQxqS9SZEz+bWKIcA== +"@box/user-selector@^1.76.3": + version "1.76.3" + resolved "https://registry.yarnpkg.com/@box/user-selector/-/user-selector-1.76.3.tgz#9c8d63c00e6be4fb47fe7072b293ace4bd410fca" + integrity sha512-rwLQeF4WqDc2JXptv/ALCe5w1Mw26/8eNssRofqPI400D0KrZXBTmf8ykQ5rk00quNbaKp0GLHT7gSiGIiokFA== "@cfaester/enzyme-adapter-react-18@^0.8.0": version "0.8.0"