Skip to content

Commit 540a957

Browse files
fix(metadataeditor): portal dropdown to preview container (#4273)
* fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container * fix(metadataeditor): portal metadata editor sidebar to preview container --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 23b27f6 commit 540a957

File tree

4 files changed

+270
-71
lines changed

4 files changed

+270
-71
lines changed

src/elements/content-preview/ContentPreview.js

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import Internationalize from '../common/Internationalize';
2626
import AsyncLoad from '../common/async-load';
2727
// $FlowFixMe TypeScript file
2828
import ThemingStyles from '../common/theming';
29+
// $FlowFixMe TypeScript file
30+
import PreviewContext from './PreviewContext';
2931
import TokenService from '../../utils/TokenService';
3032
import { isInputElement, focus } from '../../utils/dom';
3133
import { getTypedFileId } from '../../utils/file';
@@ -212,6 +214,10 @@ class ContentPreview extends React.PureComponent<Props, State> {
212214
// Defines a generic type for ContentSidebar, since an import would interfere with code splitting
213215
contentSidebar: { current: null | { refresh: Function } } = React.createRef();
214216

217+
previewBodyRef = React.createRef<HTMLDivElement>();
218+
219+
previewContextValue = { previewBodyRef: this.previewBodyRef };
220+
215221
previewContainer: ?HTMLDivElement;
216222

217223
mouseMoveTimeoutID: TimeoutID;
@@ -1328,82 +1334,88 @@ class ContentPreview extends React.PureComponent<Props, State> {
13281334
return (
13291335
<Internationalize language={language} messages={messages}>
13301336
<APIContext.Provider value={(this.api: API)}>
1331-
<Providers hasProviders={hasProviders}>
1332-
<div
1333-
id={this.id}
1334-
className={styleClassName}
1335-
ref={measureRef}
1336-
onKeyDown={this.onKeyDown}
1337-
tabIndex={0}
1338-
>
1339-
<ThemingStyles theme={theme} />
1340-
{hasHeader && (
1341-
<PreviewHeader
1342-
file={file}
1343-
logoUrl={logoUrl}
1344-
token={token}
1345-
onClose={onHeaderClose}
1346-
onPrint={this.print}
1347-
canDownload={this.canDownload()}
1348-
canPrint={canPrint}
1349-
onDownload={this.download}
1350-
contentAnswersProps={contentAnswersProps}
1351-
contentOpenWithProps={contentOpenWithProps}
1352-
canAnnotate={this.canAnnotate()}
1353-
selectedVersion={selectedVersion}
1354-
/>
1355-
)}
1356-
<div className="bcpr-body">
1357-
<div className="bcpr-container" onMouseMove={this.onMouseMove} ref={this.containerRef}>
1337+
<PreviewContext.Provider value={this.previewContextValue}>
1338+
<Providers hasProviders={hasProviders}>
1339+
<div
1340+
id={this.id}
1341+
className={styleClassName}
1342+
ref={measureRef}
1343+
onKeyDown={this.onKeyDown}
1344+
tabIndex={0}
1345+
>
1346+
<ThemingStyles theme={theme} />
1347+
{hasHeader && (
1348+
<PreviewHeader
1349+
file={file}
1350+
logoUrl={logoUrl}
1351+
token={token}
1352+
onClose={onHeaderClose}
1353+
onPrint={this.print}
1354+
canDownload={this.canDownload()}
1355+
canPrint={canPrint}
1356+
onDownload={this.download}
1357+
contentAnswersProps={contentAnswersProps}
1358+
contentOpenWithProps={contentOpenWithProps}
1359+
canAnnotate={this.canAnnotate()}
1360+
selectedVersion={selectedVersion}
1361+
/>
1362+
)}
1363+
<div className="bcpr-body" ref={this.previewBodyRef}>
1364+
<div
1365+
className="bcpr-container"
1366+
onMouseMove={this.onMouseMove}
1367+
ref={this.containerRef}
1368+
>
1369+
{file && (
1370+
<Measure bounds onResize={this.onResize}>
1371+
{({ measureRef: previewRef }) => (
1372+
<div ref={previewRef} className="bcpr-content" />
1373+
)}
1374+
</Measure>
1375+
)}
1376+
<PreviewMask
1377+
errorCode={errorCode}
1378+
extension={currentExtension}
1379+
isLoading={isLoading}
1380+
/>
1381+
<PreviewNavigation
1382+
collection={collection}
1383+
currentIndex={this.getFileIndex()}
1384+
onNavigateLeft={this.navigateLeft}
1385+
onNavigateRight={this.navigateRight}
1386+
/>
1387+
</div>
13581388
{file && (
1359-
<Measure bounds onResize={this.onResize}>
1360-
{({ measureRef: previewRef }) => (
1361-
<div ref={previewRef} className="bcpr-content" />
1362-
)}
1363-
</Measure>
1389+
<LoadableSidebar
1390+
{...contentSidebarProps}
1391+
apiHost={apiHost}
1392+
token={token}
1393+
cache={this.api.getCache()}
1394+
fileId={currentFileId}
1395+
getPreview={this.getPreview}
1396+
getViewer={this.getViewer}
1397+
history={history}
1398+
isDefaultOpen={isLarge || isVeryLarge}
1399+
language={language}
1400+
ref={this.contentSidebar}
1401+
sharedLink={sharedLink}
1402+
sharedLinkPassword={sharedLinkPassword}
1403+
requestInterceptor={requestInterceptor}
1404+
responseInterceptor={responseInterceptor}
1405+
onAnnotationSelect={this.handleAnnotationSelect}
1406+
onVersionChange={this.onVersionChange}
1407+
/>
13641408
)}
1365-
<PreviewMask
1366-
errorCode={errorCode}
1367-
extension={currentExtension}
1368-
isLoading={isLoading}
1369-
/>
1370-
<PreviewNavigation
1371-
collection={collection}
1372-
currentIndex={this.getFileIndex()}
1373-
onNavigateLeft={this.navigateLeft}
1374-
onNavigateRight={this.navigateRight}
1375-
/>
13761409
</div>
1377-
{file && (
1378-
<LoadableSidebar
1379-
{...contentSidebarProps}
1380-
apiHost={apiHost}
1381-
token={token}
1382-
cache={this.api.getCache()}
1383-
fileId={currentFileId}
1384-
getPreview={this.getPreview}
1385-
getViewer={this.getViewer}
1386-
history={history}
1387-
isDefaultOpen={isLarge || isVeryLarge}
1388-
language={language}
1389-
ref={this.contentSidebar}
1390-
sharedLink={sharedLink}
1391-
sharedLinkPassword={sharedLinkPassword}
1392-
requestInterceptor={requestInterceptor}
1393-
responseInterceptor={responseInterceptor}
1394-
onAnnotationSelect={this.handleAnnotationSelect}
1395-
onVersionChange={this.onVersionChange}
1410+
{isReloadNotificationVisible && (
1411+
<ReloadNotification
1412+
onClose={this.closeReloadNotification}
1413+
onClick={this.loadFileFromStage}
13961414
/>
13971415
)}
13981416
</div>
1399-
{isReloadNotificationVisible && (
1400-
<ReloadNotification
1401-
onClose={this.closeReloadNotification}
1402-
onClick={this.loadFileFromStage}
1403-
/>
1404-
)}
1405-
</div>
1406-
</Providers>
1417+
</Providers>
1418+
</PreviewContext.Provider>
14071419
</APIContext.Provider>
14081420
</Internationalize>
14091421
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
3+
export interface PreviewContextType {
4+
previewBodyRef: React.RefObject<HTMLDivElement>;
5+
}
6+
7+
const PreviewContext = React.createContext<PreviewContextType | null>(null);
8+
9+
PreviewContext.displayName = 'PreviewContext';
10+
export default PreviewContext;
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { userEvent, within } from 'storybook/test';
2+
import { http, HttpResponse } from 'msw';
3+
4+
import { DEFAULT_HOSTNAME_API } from '../../../../constants';
5+
import { mockEventRequest, mockFileRequest, mockUserRequest } from '../../../common/__mocks__/mockRequests';
6+
import ContentPreview from '../../ContentPreview';
7+
8+
const apiV2Path = `${DEFAULT_HOSTNAME_API}/2.0`;
9+
const fileIdWithMetadata = '415542803939';
10+
11+
// Mock file with metadata permissions
12+
const mockFileWithMetadata = {
13+
url: `${apiV2Path}/files/${fileIdWithMetadata}`,
14+
response: {
15+
type: 'file',
16+
id: fileIdWithMetadata,
17+
etag: '3',
18+
extension: 'pdf',
19+
name: 'Test Document.pdf',
20+
permissions: {
21+
can_download: true,
22+
can_preview: true,
23+
can_upload: true,
24+
can_comment: true,
25+
can_rename: true,
26+
can_delete: true,
27+
can_share: true,
28+
can_set_share_access: true,
29+
can_invite_collaborator: true,
30+
can_annotate: true,
31+
can_view_annotations_all: true,
32+
can_view_annotations_self: true,
33+
can_create_annotations: true,
34+
can_view_annotations: true,
35+
},
36+
},
37+
};
38+
39+
// Mock metadata template with very long dropdown options
40+
const mockMetadataTemplateWithLongOptions = {
41+
url: `${apiV2Path}/metadata_templates/enterprise`,
42+
response: {
43+
limit: 1000,
44+
entries: [
45+
{
46+
id: 'long-dropdown-template-id',
47+
type: 'metadata_template',
48+
templateKey: 'longDropdownTemplate',
49+
scope: 'enterprise_173733877',
50+
displayName: 'Long Dropdown Test Template',
51+
hidden: false,
52+
copyInstanceOnItemCopy: false,
53+
fields: [
54+
{
55+
id: 'long-dropdown-field-id',
56+
type: 'enum',
57+
key: 'longDropdownField',
58+
displayName: 'Department Selection',
59+
hidden: false,
60+
description: 'Select your department from the dropdown',
61+
options: [
62+
{
63+
id: 'option-1',
64+
key: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
65+
},
66+
{
67+
id: 'option-2',
68+
key: 'Marketing - Digital Campaigns - Social Media Content Strategy Team Beta Division',
69+
},
70+
{
71+
id: 'option-3',
72+
key: 'Human Resources - Talent Acquisition - Employee Experience Team Gamma Division',
73+
},
74+
{
75+
id: 'option-4',
76+
key: 'Finance - Accounting - Budget Planning - Financial Analysis Team Delta Division',
77+
},
78+
{
79+
id: 'option-5',
80+
key: 'Operations - Supply Chain - Logistics - Vendor Management Team Epsilon Division',
81+
},
82+
{
83+
id: 'option-6',
84+
key: 'Legal - Compliance - Regulatory Affairs - Contract Management Team Zeta Division',
85+
},
86+
],
87+
},
88+
],
89+
},
90+
],
91+
},
92+
};
93+
94+
// Mock metadata instance for the file
95+
const mockMetadataInstances = {
96+
url: `${apiV2Path}/files/${fileIdWithMetadata}/metadata`,
97+
response: {
98+
entries: [
99+
{
100+
$id: 'long-dropdown-instance-id',
101+
$version: 1,
102+
$type: 'longDropdownTemplate-template-type',
103+
$parent: `file_${fileIdWithMetadata}`,
104+
$typeVersion: 1,
105+
$template: 'longDropdownTemplate',
106+
$scope: 'enterprise_173733877',
107+
$templateKey: 'longDropdownTemplate',
108+
longDropdownField: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
109+
$canEdit: true,
110+
},
111+
],
112+
limit: 100,
113+
},
114+
};
115+
116+
const mockGlobalMetadataTemplates = {
117+
url: `${apiV2Path}/metadata_templates/global`,
118+
response: {
119+
entries: [],
120+
},
121+
};
122+
123+
export const metadataDropdownPositioning = {
124+
parameters: {
125+
msw: {
126+
handlers: [
127+
http.get(mockFileWithMetadata.url, () => HttpResponse.json(mockFileWithMetadata.response)),
128+
http.get(mockMetadataTemplateWithLongOptions.url, () =>
129+
HttpResponse.json(mockMetadataTemplateWithLongOptions.response),
130+
),
131+
http.get(mockMetadataInstances.url, () => HttpResponse.json(mockMetadataInstances.response)),
132+
http.get(mockGlobalMetadataTemplates.url, () =>
133+
HttpResponse.json(mockGlobalMetadataTemplates.response),
134+
),
135+
http.get(mockFileRequest.url, () => HttpResponse.json(mockFileRequest.response)),
136+
http.get(mockUserRequest.url, () => HttpResponse.json(mockUserRequest.response)),
137+
http.get(mockEventRequest.url, () => HttpResponse.json(mockEventRequest.response)),
138+
],
139+
},
140+
},
141+
play: async ({ canvasElement }) => {
142+
const canvas = within(canvasElement);
143+
144+
const editButton = await canvas.findByRole('button', { name: 'Edit Long Dropdown Test Template' });
145+
await userEvent.click(editButton);
146+
147+
const dropdownField = await canvas.findByRole('combobox', { name: 'Department Selection' });
148+
await userEvent.click(dropdownField);
149+
150+
await canvas.findByRole('option', {
151+
name: 'Engineering - Software Development - Frontend React TypeScript Team Alpha Division',
152+
});
153+
},
154+
};
155+
156+
export default {
157+
title: 'Elements/ContentPreview/tests/visual/Metadata',
158+
component: ContentPreview,
159+
args: {
160+
fileId: fileIdWithMetadata,
161+
hasHeader: true,
162+
contentSidebarProps: {
163+
hasMetadata: true,
164+
metadataSidebarProps: {
165+
isFeatureEnabled: true,
166+
},
167+
features: {
168+
'metadata.redesign.enabled': true,
169+
},
170+
},
171+
},
172+
};

src/elements/content-sidebar/MetadataInstanceEditor.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
type MetadataTemplateInstance,
66
} from '@box/metadata-editor';
77
import { TaxonomyOptionsFetcher } from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js';
8-
import React from 'react';
8+
import React, { useContext } from 'react';
9+
import PreviewContext, { type PreviewContextType } from '../content-preview/PreviewContext';
910
import {
1011
ERROR_CODE_METADATA_AUTOFILL_TIMEOUT,
1112
ERROR_CODE_UNKNOWN,
@@ -51,6 +52,9 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
5152
template,
5253
isAdvancedExtractAgentEnabled = false,
5354
}) => {
55+
const previewContext: PreviewContextType | null = useContext(PreviewContext);
56+
const customRef = previewContext?.previewBodyRef?.current;
57+
5458
return (
5559
<MetadataInstanceForm
5660
// TODO investigate if this property should be optional and by default false
@@ -71,6 +75,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
7175
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
7276
taxonomyOptionsFetcher={taxonomyOptionsFetcher}
7377
isAdvancedExtractAgentEnabled={isAdvancedExtractAgentEnabled}
78+
customRef={customRef}
7479
/>
7580
);
7681
};

0 commit comments

Comments
 (0)