From 56b8817aa0a84aedd1bd598fb2a755b0d086d7b5 Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Fri, 24 Jul 2026 14:58:36 -0700 Subject: [PATCH] fix(content-sidebar): add programmatic Resin tracking for sidebar resize Resin DOM delegation only captures click/mouseup, but the resize handle uses pointer capture, so GA shipped with zero sidebarresizehandle events. Emit recordAction on resizeStart/resizeEnd and forward resin through ContentPreview. Co-authored-by: Cursor --- .../content-preview/ContentPreview.js | 5 ++ .../__tests__/ContentPreview.test.js | 16 +++++ .../content-sidebar/ContentSidebar.js | 5 ++ src/elements/content-sidebar/Sidebar.js | 69 ++++++++++++++----- .../content-sidebar/SidebarResizeHandle.js | 10 ++- .../__tests__/ContentSidebar.test.js | 11 +++ .../content-sidebar/__tests__/Sidebar.test.js | 44 ++++++++++++ .../__tests__/SidebarResizeHandle.test.js | 15 +++- 8 files changed, 152 insertions(+), 23 deletions(-) diff --git a/src/elements/content-preview/ContentPreview.js b/src/elements/content-preview/ContentPreview.js index 3f6a6e8ec9..d3108befd1 100644 --- a/src/elements/content-preview/ContentPreview.js +++ b/src/elements/content-preview/ContentPreview.js @@ -155,6 +155,9 @@ type Props = { }, previewLibraryVersion: string, previewMode?: 'default' | 'shared_file' | 'shared_folder' | 'editable_shared_file' | 'inline_feed', + resin?: { + recordAction?: (data: Object) => void, + }, requestInterceptor?: Function, responseInterceptor?: Function, sharedLink?: string, @@ -1588,6 +1591,7 @@ class ContentPreview extends React.PureComponent { sharedLink, sharedLinkPassword, requestInterceptor, + resin, responseInterceptor, theme, }: Props = this.props; @@ -1725,6 +1729,7 @@ class ContentPreview extends React.PureComponent { isDefaultOpen={isLarge || isVeryLarge} language={language} ref={this.contentSidebar} + resin={resin} sharedLink={sharedLink} sharedLinkPassword={sharedLinkPassword} requestInterceptor={requestInterceptor} diff --git a/src/elements/content-preview/__tests__/ContentPreview.test.js b/src/elements/content-preview/__tests__/ContentPreview.test.js index b5eb84d90d..a6948e4656 100644 --- a/src/elements/content-preview/__tests__/ContentPreview.test.js +++ b/src/elements/content-preview/__tests__/ContentPreview.test.js @@ -2460,6 +2460,22 @@ describe('elements/content-preview/ContentPreview', () => { expect(bodyDiv.find('.bcpr-container').exists()).toBe(true); }); + test('should pass the Resin integration to the sidebar', () => { + const resin = { recordAction: jest.fn() }; + const wrapper = getWrapper({ + fileId: '123', + hideSidebar: false, + resin, + }); + wrapper.setState({ + currentFileId: '123', + file: { id: '123', name: 'test.pdf' }, + }); + + const sidebar = wrapper.find('.bcpr-body').children().at(1); + expect(sidebar.prop('resin')).toBe(resin); + }); + test('should render sidebar components when hideSidebar is not provided (default)', () => { const wrapper = getWrapper({ fileId: '123', diff --git a/src/elements/content-sidebar/ContentSidebar.js b/src/elements/content-sidebar/ContentSidebar.js index 29e272b605..b0f0ad0360 100644 --- a/src/elements/content-sidebar/ContentSidebar.js +++ b/src/elements/content-sidebar/ContentSidebar.js @@ -110,6 +110,9 @@ type Props = { onPanelChange?: (name: string, isInitialState: boolean) => void, onVersionChange?: Function, onVersionHistoryClick?: Function, + resin?: { + recordAction?: (data: Object) => void, + }, requestInterceptor?: Function, responseInterceptor?: Function, sharedLink?: string, @@ -405,6 +408,7 @@ class ContentSidebar extends React.Component { onPanelChange, onVersionChange, onVersionHistoryClick, + resin, signSidebarProps, theme, versionsSidebarProps, @@ -453,6 +457,7 @@ class ContentSidebar extends React.Component { onPanelChange={onPanelChange} onVersionChange={onVersionChange} onVersionHistoryClick={onVersionHistoryClick} + resin={resin} signSidebarProps={signSidebarProps} theme={theme} versionsSidebarProps={versionsSidebarProps} diff --git a/src/elements/content-sidebar/Sidebar.js b/src/elements/content-sidebar/Sidebar.js index 70206dcf7d..d46f574830 100644 --- a/src/elements/content-sidebar/Sidebar.js +++ b/src/elements/content-sidebar/Sidebar.js @@ -12,34 +12,36 @@ import noop from 'lodash/noop'; import uniqueid from 'lodash/uniqueId'; import { matchPath, withRouter } from 'react-router-dom'; import type { Location, RouterHistory } from 'react-router-dom'; + +import API from '../../api'; +import type { BoxItem, User } from '../../common/types/core'; +import type { MetadataEditor } from '../../common/types/metadata'; import LoadingIndicator from '../../components/loading-indicator/LoadingIndicator'; -import LocalStore from '../../utils/LocalStore'; -import withMediaQuery from '../../components/media-query/withMediaQuery'; import { VIEW_SIZE_TYPE } from '../../components/media-query/constants'; -import SidebarNav from './SidebarNav'; -import SidebarPanels from './SidebarPanels'; -import SidebarResizeHandle from './SidebarResizeHandle'; -import SidebarUtils from './SidebarUtils'; -// $FlowFixMe TypeScript file -import ThemingStyles from '../common/theming'; +import withMediaQuery from '../../components/media-query/withMediaQuery'; +import { SIDEBAR_VIEW_ACTIVITY, SIDEBAR_VIEW_BOXAI, SIDEBAR_VIEW_DOCGEN } from '../../constants'; +import LocalStore from '../../utils/LocalStore'; import { withCurrentUser } from '../common/current-user'; import { isFeatureEnabled, withFeatureConsumer } from '../common/feature-checking'; import type { FeatureConfig } from '../common/feature-checking'; +import type { Errors } from '../common/flowTypes'; +import { SIDEBAR_NAV_TARGETS } from '../common/interactionTargets'; +// $FlowFixMe TypeScript file +import ThemingStyles from '../common/theming'; +// $FlowFixMe TypeScript file +import type { Theme } from '../common/theming'; import type { ActivitySidebarProps } from './ActivitySidebar'; +import type { BoxAISidebarProps } from './BoxAISidebar'; import type { DetailsSidebarProps } from './DetailsSidebar'; import type { DocGenSidebarProps } from './DocGenSidebar/DocGenSidebar'; -import type { MetadataSidebarProps } from './MetadataSidebar'; -import type { BoxAISidebarProps } from './BoxAISidebar'; -import type { VersionsSidebarProps } from './versions'; import type { AdditionalSidebarTab, CustomSidebarPanel } from './flowTypes'; -import type { MetadataEditor } from '../../common/types/metadata'; -import type { BoxItem, User } from '../../common/types/core'; +import type { MetadataSidebarProps } from './MetadataSidebar'; import type { SignSidebarProps } from './SidebarNavSign'; -import type { Errors } from '../common/flowTypes'; -// $FlowFixMe TypeScript file -import type { Theme } from '../common/theming'; -import { SIDEBAR_VIEW_ACTIVITY, SIDEBAR_VIEW_BOXAI, SIDEBAR_VIEW_DOCGEN } from '../../constants'; -import API from '../../api'; +import SidebarNav from './SidebarNav'; +import SidebarPanels from './SidebarPanels'; +import SidebarResizeHandle from './SidebarResizeHandle'; +import SidebarUtils from './SidebarUtils'; +import type { VersionsSidebarProps } from './versions'; type Props = { activitySidebarProps: ActivitySidebarProps, @@ -74,6 +76,9 @@ type Props = { onPanelChange?: (name: string, isInitialState: boolean) => void, onVersionChange?: Function, onVersionHistoryClick?: Function, + resin?: { + recordAction?: (data: Object) => void, + }, /** When true, enables data fetching. When false, defers data fetching. Used to prioritize preview loading. */ shouldFetchSidebarData?: boolean, signSidebarProps: SignSidebarProps, @@ -151,8 +156,33 @@ class Sidebar extends React.Component { this.setState({ width }); }; - handleResizeEnd = (width: number): void => { + handleResizeStart = (width: number): void => { + this.recordResizeAction('resizeStart', width, width); + }; + + handleResizeEnd = (width: number, startWidth: number): void => { this.store.setItem(SIDEBAR_WIDTH_KEY, width); + this.recordResizeAction('resizeEnd', width, startWidth); + }; + + recordResizeAction = (phase: 'resizeStart' | 'resizeEnd', width: number, startWidth: number): void => { + const { file, fileId, resin, viewWidth } = this.props; + + if (resin && resin.recordAction) { + resin.recordAction({ + action: 'programmatic', + component: 'preview', + data: { + phase, + startWidth, + viewWidth, + width, + }, + fileExtension: file.extension, + fileId, + target: SIDEBAR_NAV_TARGETS.RESIZE_HANDLE, + }); + } }; componentDidMount() { @@ -410,6 +440,7 @@ class Sidebar extends React.Component { minWidth={minWidth} onResize={this.handleResize} onResizeEnd={this.handleResizeEnd} + onResizeStart={this.handleResizeStart} width={currentWidth} /> )} diff --git a/src/elements/content-sidebar/SidebarResizeHandle.js b/src/elements/content-sidebar/SidebarResizeHandle.js index 3503cabef2..84cb6d4e17 100644 --- a/src/elements/content-sidebar/SidebarResizeHandle.js +++ b/src/elements/content-sidebar/SidebarResizeHandle.js @@ -12,13 +12,14 @@ type Props = { maxWidth: number, minWidth: number, onResize: (width: number) => void, - onResizeEnd?: (width: number) => void, + onResizeEnd?: (width: number, startWidth: number) => void, + onResizeStart?: (width: number) => void, width: number, }; const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max); -const SidebarResizeHandle = ({ maxWidth, minWidth, onResize, onResizeEnd, width }: Props) => { +const SidebarResizeHandle = ({ maxWidth, minWidth, onResize, onResizeEnd, onResizeStart, width }: Props) => { const startXRef = React.useRef(0); const startWidthRef = React.useRef(width); const [isDragging, setIsDragging] = React.useState(false); @@ -47,7 +48,7 @@ const SidebarResizeHandle = ({ maxWidth, minWidth, onResize, onResizeEnd, width const finalWidth = clamp(startWidthRef.current + deltaX, minWidth, maxWidth); onResize(finalWidth); if (onResizeEnd) { - onResizeEnd(finalWidth); + onResizeEnd(finalWidth, startWidthRef.current); } }, [handlePointerMove, maxWidth, minWidth, onResize, onResizeEnd], @@ -57,6 +58,9 @@ const SidebarResizeHandle = ({ maxWidth, minWidth, onResize, onResizeEnd, width event.preventDefault(); startXRef.current = event.clientX; startWidthRef.current = width; + if (onResizeStart) { + onResizeStart(width); + } setIsDragging(true); if (typeof event.currentTarget.setPointerCapture === 'function') { event.currentTarget.setPointerCapture(((event.pointerId: any): string)); diff --git a/src/elements/content-sidebar/__tests__/ContentSidebar.test.js b/src/elements/content-sidebar/__tests__/ContentSidebar.test.js index b86b0615c6..42e8517f0e 100644 --- a/src/elements/content-sidebar/__tests__/ContentSidebar.test.js +++ b/src/elements/content-sidebar/__tests__/ContentSidebar.test.js @@ -403,5 +403,16 @@ describe('elements/content-sidebar/ContentSidebar', () => { expect(wrapper.find('sidebar').prop('isLoading')).toBe(false); }); + + test('should pass the Resin integration to Sidebar', () => { + const resin = { recordAction: jest.fn() }; + const wrapper = getWrapper({ + fileId: 'test_id', + minimalFile, + resin, + }); + + expect(wrapper.find('sidebar').prop('resin')).toBe(resin); + }); }); }); diff --git a/src/elements/content-sidebar/__tests__/Sidebar.test.js b/src/elements/content-sidebar/__tests__/Sidebar.test.js index 28417c32aa..efd1f63ff2 100644 --- a/src/elements/content-sidebar/__tests__/Sidebar.test.js +++ b/src/elements/content-sidebar/__tests__/Sidebar.test.js @@ -26,6 +26,7 @@ jest.mock('../../../utils/LocalStore'); describe('elements/content-sidebar/Sidebar', () => { const file = { + extension: 'pdf', id: 'id', file_version: { id: '123', @@ -609,6 +610,49 @@ describe('elements/content-sidebar/Sidebar', () => { expect(wrapper.find('SidebarResizeHandle').prop('maxWidth')).toBe(600); }); + + test('records programmatic Resin actions when resizing starts and ends', () => { + const recordAction = jest.fn(); + LocalStore.mockImplementationOnce(() => ({ + getItem: jest.fn(() => SIDEBAR_FORCE_VALUE_OPEN), + setItem: jest.fn(), + })); + const wrapper = getWrapper({ + ...resizableProps, + fileId: file.id, + resin: { recordAction }, + }); + + wrapper.instance().handleResizeStart(400); + wrapper.instance().handleResizeEnd(550, 400); + + expect(recordAction).toHaveBeenNthCalledWith(1, { + action: 'programmatic', + component: 'preview', + data: { + phase: 'resizeStart', + startWidth: 400, + viewWidth: 1600, + width: 400, + }, + fileExtension: 'pdf', + fileId: 'id', + target: 'sidebarresizehandle', + }); + expect(recordAction).toHaveBeenNthCalledWith(2, { + action: 'programmatic', + component: 'preview', + data: { + phase: 'resizeEnd', + startWidth: 400, + viewWidth: 1600, + width: 550, + }, + fileExtension: 'pdf', + fileId: 'id', + target: 'sidebarresizehandle', + }); + }); }); describe('on panel change', () => { diff --git a/src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js b/src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js index badcdc3ea2..b196c63743 100644 --- a/src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js +++ b/src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js @@ -113,6 +113,19 @@ describe('elements/content-sidebar/SidebarResizeHandle', () => { expect(handle).not.toHaveClass('bcs-resize-handle-is-dragging'); }); + test('calls onResizeStart with the initial width on pointerdown', () => { + const onResizeStart = jest.fn(); + render(); + const handle = screen.getByTestId('sidebar-resize-handle'); + + fireEvent( + handle, + Object.assign(new MouseEvent('pointerdown', { bubbles: true, clientX: 1000 }), { pointerId: 1 }), + ); + + expect(onResizeStart).toHaveBeenCalledWith(600); + }); + test('removes window pointer listeners on unmount', () => { const removeSpy = jest.spyOn(window, 'removeEventListener'); const { unmount } = render(); @@ -138,6 +151,6 @@ describe('elements/content-sidebar/SidebarResizeHandle', () => { dispatchWindowPointer('pointerup', { clientX: 950, pointerId: 1 }); }); - expect(onResizeEnd).toHaveBeenCalledWith(450); + expect(onResizeEnd).toHaveBeenCalledWith(450, 400); }); });