Skip to content
Merged
152 changes: 82 additions & 70 deletions src/elements/content-preview/ContentPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import Internationalize from '../common/Internationalize';
import AsyncLoad from '../common/async-load';
// $FlowFixMe TypeScript file
import ThemingStyles from '../common/theming';
// $FlowFixMe TypeScript file
import PreviewContext from './PreviewContext';
import TokenService from '../../utils/TokenService';
import { isInputElement, focus } from '../../utils/dom';
import { getTypedFileId } from '../../utils/file';
Expand Down Expand Up @@ -212,6 +214,10 @@ class ContentPreview extends React.PureComponent<Props, State> {
// Defines a generic type for ContentSidebar, since an import would interfere with code splitting
contentSidebar: { current: null | { refresh: Function } } = React.createRef();

previewBodyRef = React.createRef<HTMLDivElement>();

previewContextValue = { previewBodyRef: this.previewBodyRef };

previewContainer: ?HTMLDivElement;

mouseMoveTimeoutID: TimeoutID;
Expand Down Expand Up @@ -1328,82 +1334,88 @@ class ContentPreview extends React.PureComponent<Props, State> {
return (
<Internationalize language={language} messages={messages}>
<APIContext.Provider value={(this.api: API)}>
<Providers hasProviders={hasProviders}>
<div
id={this.id}
className={styleClassName}
ref={measureRef}
onKeyDown={this.onKeyDown}
tabIndex={0}
>
<ThemingStyles theme={theme} />
{hasHeader && (
<PreviewHeader
file={file}
logoUrl={logoUrl}
token={token}
onClose={onHeaderClose}
onPrint={this.print}
canDownload={this.canDownload()}
canPrint={canPrint}
onDownload={this.download}
contentAnswersProps={contentAnswersProps}
contentOpenWithProps={contentOpenWithProps}
canAnnotate={this.canAnnotate()}
selectedVersion={selectedVersion}
/>
)}
<div className="bcpr-body">
<div className="bcpr-container" onMouseMove={this.onMouseMove} ref={this.containerRef}>
<PreviewContext.Provider value={this.previewContextValue}>
<Providers hasProviders={hasProviders}>
<div
id={this.id}
className={styleClassName}
ref={measureRef}
onKeyDown={this.onKeyDown}
tabIndex={0}
>
<ThemingStyles theme={theme} />
{hasHeader && (
<PreviewHeader
file={file}
logoUrl={logoUrl}
token={token}
onClose={onHeaderClose}
onPrint={this.print}
canDownload={this.canDownload()}
canPrint={canPrint}
onDownload={this.download}
contentAnswersProps={contentAnswersProps}
contentOpenWithProps={contentOpenWithProps}
canAnnotate={this.canAnnotate()}
selectedVersion={selectedVersion}
/>
)}
<div className="bcpr-body" ref={this.previewBodyRef}>
<div
className="bcpr-container"
onMouseMove={this.onMouseMove}
ref={this.containerRef}
>
{file && (
<Measure bounds onResize={this.onResize}>
{({ measureRef: previewRef }) => (
<div ref={previewRef} className="bcpr-content" />
)}
</Measure>
)}
<PreviewMask
errorCode={errorCode}
extension={currentExtension}
isLoading={isLoading}
/>
<PreviewNavigation
collection={collection}
currentIndex={this.getFileIndex()}
onNavigateLeft={this.navigateLeft}
onNavigateRight={this.navigateRight}
/>
</div>
{file && (
<Measure bounds onResize={this.onResize}>
{({ measureRef: previewRef }) => (
<div ref={previewRef} className="bcpr-content" />
)}
</Measure>
<LoadableSidebar
{...contentSidebarProps}
apiHost={apiHost}
token={token}
cache={this.api.getCache()}
fileId={currentFileId}
getPreview={this.getPreview}
getViewer={this.getViewer}
history={history}
isDefaultOpen={isLarge || isVeryLarge}
language={language}
ref={this.contentSidebar}
sharedLink={sharedLink}
sharedLinkPassword={sharedLinkPassword}
requestInterceptor={requestInterceptor}
responseInterceptor={responseInterceptor}
onAnnotationSelect={this.handleAnnotationSelect}
onVersionChange={this.onVersionChange}
/>
)}
<PreviewMask
errorCode={errorCode}
extension={currentExtension}
isLoading={isLoading}
/>
<PreviewNavigation
collection={collection}
currentIndex={this.getFileIndex()}
onNavigateLeft={this.navigateLeft}
onNavigateRight={this.navigateRight}
/>
</div>
{file && (
<LoadableSidebar
{...contentSidebarProps}
apiHost={apiHost}
token={token}
cache={this.api.getCache()}
fileId={currentFileId}
getPreview={this.getPreview}
getViewer={this.getViewer}
history={history}
isDefaultOpen={isLarge || isVeryLarge}
language={language}
ref={this.contentSidebar}
sharedLink={sharedLink}
sharedLinkPassword={sharedLinkPassword}
requestInterceptor={requestInterceptor}
responseInterceptor={responseInterceptor}
onAnnotationSelect={this.handleAnnotationSelect}
onVersionChange={this.onVersionChange}
{isReloadNotificationVisible && (
<ReloadNotification
onClose={this.closeReloadNotification}
onClick={this.loadFileFromStage}
/>
)}
</div>
{isReloadNotificationVisible && (
<ReloadNotification
onClose={this.closeReloadNotification}
onClick={this.loadFileFromStage}
/>
)}
</div>
</Providers>
</Providers>
</PreviewContext.Provider>
</APIContext.Provider>
</Internationalize>
);
Expand Down
10 changes: 10 additions & 0 deletions src/elements/content-preview/PreviewContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

export interface PreviewContextType {
previewBodyRef: React.RefObject<HTMLDivElement>;
}

const PreviewContext = React.createContext<PreviewContextType | null>(null);

PreviewContext.displayName = 'PreviewContext';
export default PreviewContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { userEvent, within } from 'storybook/test';
import { http, HttpResponse } from 'msw';

import { DEFAULT_HOSTNAME_API } from '../../../../constants';
import { mockEventRequest, mockFileRequest, mockUserRequest } from '../../../common/__mocks__/mockRequests';
import ContentPreview from '../../ContentPreview';

const apiV2Path = `${DEFAULT_HOSTNAME_API}/2.0`;
const fileIdWithMetadata = '415542803939';

// Mock file with metadata permissions
const mockFileWithMetadata = {
url: `${apiV2Path}/files/${fileIdWithMetadata}`,
response: {
type: 'file',
id: fileIdWithMetadata,
etag: '3',
extension: 'pdf',
name: 'Test Document.pdf',
permissions: {
can_download: true,
can_preview: true,
can_upload: true,
can_comment: true,
can_rename: true,
can_delete: true,
can_share: true,
can_set_share_access: true,
can_invite_collaborator: true,
can_annotate: true,
can_view_annotations_all: true,
can_view_annotations_self: true,
can_create_annotations: true,
can_view_annotations: true,
},
},
};

// Mock metadata template with very long dropdown options
const mockMetadataTemplateWithLongOptions = {
url: `${apiV2Path}/metadata_templates/enterprise`,
response: {
limit: 1000,
entries: [
{
id: 'long-dropdown-template-id',
type: 'metadata_template',
templateKey: 'longDropdownTemplate',
scope: 'enterprise_173733877',
displayName: 'Long Dropdown Test Template',
hidden: false,
copyInstanceOnItemCopy: false,
fields: [
{
id: 'long-dropdown-field-id',
type: 'enum',
key: 'longDropdownField',
displayName: 'Department Selection',
hidden: false,
description: 'Select your department from the dropdown',
options: [
{
id: 'option-1',
key: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
},
{
id: 'option-2',
key: 'Marketing - Digital Campaigns - Social Media Content Strategy Team Beta Division',
},
{
id: 'option-3',
key: 'Human Resources - Talent Acquisition - Employee Experience Team Gamma Division',
},
{
id: 'option-4',
key: 'Finance - Accounting - Budget Planning - Financial Analysis Team Delta Division',
},
{
id: 'option-5',
key: 'Operations - Supply Chain - Logistics - Vendor Management Team Epsilon Division',
},
{
id: 'option-6',
key: 'Legal - Compliance - Regulatory Affairs - Contract Management Team Zeta Division',
},
],
},
],
},
],
},
};

// Mock metadata instance for the file
const mockMetadataInstances = {
url: `${apiV2Path}/files/${fileIdWithMetadata}/metadata`,
response: {
entries: [
{
$id: 'long-dropdown-instance-id',
$version: 1,
$type: 'longDropdownTemplate-template-type',
$parent: `file_${fileIdWithMetadata}`,
$typeVersion: 1,
$template: 'longDropdownTemplate',
$scope: 'enterprise_173733877',
$templateKey: 'longDropdownTemplate',
longDropdownField: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
$canEdit: true,
},
],
limit: 100,
},
};

const mockGlobalMetadataTemplates = {
url: `${apiV2Path}/metadata_templates/global`,
response: {
entries: [],
},
};

export const metadataDropdownPositioning = {
parameters: {
msw: {
handlers: [
http.get(mockFileWithMetadata.url, () => HttpResponse.json(mockFileWithMetadata.response)),
http.get(mockMetadataTemplateWithLongOptions.url, () =>
HttpResponse.json(mockMetadataTemplateWithLongOptions.response),
),
http.get(mockMetadataInstances.url, () => HttpResponse.json(mockMetadataInstances.response)),
http.get(mockGlobalMetadataTemplates.url, () =>
HttpResponse.json(mockGlobalMetadataTemplates.response),
),
http.get(mockFileRequest.url, () => HttpResponse.json(mockFileRequest.response)),
http.get(mockUserRequest.url, () => HttpResponse.json(mockUserRequest.response)),
http.get(mockEventRequest.url, () => HttpResponse.json(mockEventRequest.response)),
],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

const editButton = await canvas.findByRole('button', { name: 'Edit Long Dropdown Test Template' });
await userEvent.click(editButton);

const dropdownField = await canvas.findByRole('combobox', { name: 'Department Selection' });
await userEvent.click(dropdownField);

await canvas.findByRole('option', {
name: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
});
},
};

export default {
title: 'Elements/ContentPreview/tests/visual/Metadata',
component: ContentPreview,
args: {
fileId: fileIdWithMetadata,
hasHeader: true,
contentSidebarProps: {
hasMetadata: true,
metadataSidebarProps: {
isFeatureEnabled: true,
},
features: {
'metadata.redesign.enabled': true,
},
},
},
};
7 changes: 6 additions & 1 deletion src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
type MetadataTemplateInstance,
} from '@box/metadata-editor';
import { TaxonomyOptionsFetcher } from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js';
import React from 'react';
import React, { useContext } from 'react';
import PreviewContext, { type PreviewContextType } from '../content-preview/PreviewContext';
import {
ERROR_CODE_METADATA_AUTOFILL_TIMEOUT,
ERROR_CODE_UNKNOWN,
Expand Down Expand Up @@ -51,6 +52,9 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
template,
isAdvancedExtractAgentEnabled = false,
}) => {
const previewContext: PreviewContextType | null = useContext(PreviewContext);
const customRef = previewContext?.previewBodyRef?.current;

return (
<MetadataInstanceForm
// TODO investigate if this property should be optional and by default false
Expand All @@ -71,6 +75,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
taxonomyOptionsFetcher={taxonomyOptionsFetcher}
isAdvancedExtractAgentEnabled={isAdvancedExtractAgentEnabled}
customRef={customRef}
/>
);
};
Expand Down