Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@
"@box/frontend": "^11.0.1",
"@box/item-icon": "^3.2.0",
"@box/languages": "^1.0.0",
"@box/metadata-editor": "^2.2.11",
"@box/metadata-editor": "^2.2.28",
"@box/metadata-filter": "^1.80.23",
"@box/metadata-taxonomy-picker": "^3.1.8",
"@box/metadata-taxonomy-picker": "^3.1.25",
"@box/metadata-template-browser": "^2.1.7",
"@box/metadata-view": "^1.53.26",
"@box/react-virtualized": "^9.22.3-rc-box.10",
Expand Down
11 changes: 10 additions & 1 deletion src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
type MetadataTemplateField,
type MetadataTemplateInstance,
} from '@box/metadata-editor';
import { TaxonomyOptionsFetcher } from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js';
import {
CreateTaxonomyItemsService,
TaxonomyOptionsFetcher,
} from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js';
import React, { useContext } from 'react';
import PreviewContext, { type PreviewContextType } from '../content-preview/PreviewContext';
import {
Expand All @@ -23,6 +26,7 @@ export interface MetadataInstanceEditorProps {
isDeleteConfirmationModalCheckboxEnabled: boolean;
isLargeFile: boolean;
isMetadataMultiLevelTaxonomyFieldEnabled: boolean;
isMetadataTaxonomyPickerEnabled?: boolean;
isUnsavedChangesModalOpen: boolean;
onCancel: () => void;
onDelete: (metadataInstance: MetadataTemplateInstance) => void;
Expand All @@ -32,6 +36,7 @@ export interface MetadataInstanceEditorProps {
setIsUnsavedChangesModalOpen: (isUnsavedChangesModalOpen: boolean) => void;
shouldShowOnlyReviewFields?: boolean;
taxonomyOptionsFetcher: TaxonomyOptionsFetcher;
createTaxonomyItemsService?: CreateTaxonomyItemsService;
template: MetadataTemplateInstance;
isAdvancedExtractAgentEnabled?: boolean;
isConfidenceScoreReviewEnabled?: boolean;
Expand All @@ -50,6 +55,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
isDeleteConfirmationModalCheckboxEnabled,
isLargeFile,
isMetadataMultiLevelTaxonomyFieldEnabled,
isMetadataTaxonomyPickerEnabled = false,
isUnsavedChangesModalOpen,
onCancel,
onDelete,
Expand All @@ -59,6 +65,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
setIsUnsavedChangesModalOpen,
shouldShowOnlyReviewFields = false,
taxonomyOptionsFetcher,
createTaxonomyItemsService,
template,
isAdvancedExtractAgentEnabled = false,
isConfidenceScoreReviewEnabled = false,
Expand All @@ -74,6 +81,8 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
<MetadataInstanceForm
// TODO investigate if this property should be optional and by default false
isMultilevelTaxonomyFieldEnabled={isMetadataMultiLevelTaxonomyFieldEnabled}
isTaxonomyPickerEnabled={isMetadataTaxonomyPickerEnabled}
createTaxonomyItemsService={createTaxonomyItemsService}
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
errorCode={errorCode}
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
Expand Down
33 changes: 31 additions & 2 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @file Redesigned Metadata sidebar component
* @author Box
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import flow from 'lodash/flow';
import { FormattedMessage, useIntl } from 'react-intl';
import { withRouter, RouteComponentProps } from 'react-router-dom';
Expand Down Expand Up @@ -43,7 +43,12 @@ import './MetadataSidebarRedesign.scss';
import MetadataInstanceEditor from './MetadataInstanceEditor';
import { convertTemplateToTemplateInstance } from './utils/convertTemplateToTemplateInstance';
import { isExtensionSupportedForMetadataSuggestions } from './utils/isExtensionSupportedForMetadataSuggestions';
import { metadataTaxonomyFetcher, metadataTaxonomyNodeAncestorsFetcher } from './fetchers/metadataTaxonomyFetcher';
import {
createTaxonomyItemsService,
metadataTaxonomyFetcher,
metadataTaxonomyNodeAncestorsFetcher,
type TaxonomyFieldConfig,
} from './fetchers/metadataTaxonomyFetcher';
import { useMetadataSidebarFilteredTemplates } from './hooks/useMetadataSidebarFilteredTemplates';
import useMetadataFieldSelection from './hooks/useMetadataFieldSelection';
import useMetadataSidebarUnsavedChangesGuard from './hooks/useMetadataSidebarUnsavedChangesGuard';
Expand Down Expand Up @@ -115,6 +120,7 @@ function MetadataSidebarRedesign({
const isBoxAiSuggestionsEnabled: boolean = useFeatureEnabled('metadata.aiSuggestions.enabled');
const isBetaLanguageEnabled: boolean = useFeatureEnabled('metadata.betaLanguage.enabled');
const isMetadataMultiLevelTaxonomyFieldEnabled: boolean = useFeatureEnabled('metadata.multilevelTaxonomy.enabled');
const isMetadataTaxonomyPickerEnabled: boolean = useFeatureEnabled('metadata.taxonomyPicker.enabled');
const isAdvancedExtractAgentEnabled: boolean = useFeatureEnabled('metadata.extractAdvancedAgents.enabled');
const isDeleteConfirmationModalCheckboxEnabled: boolean = useFeatureEnabled(
'metadata.deleteConfirmationModalCheckbox.enabled',
Expand Down Expand Up @@ -329,6 +335,25 @@ function MetadataSidebarRedesign({
[api, fileId],
);

const resolveTaxonomyFieldConfig = useCallback(
(_templateKey: string, fieldKey: string): TaxonomyFieldConfig | undefined => {
const field = editingTemplate?.fields?.find(f => f.key === fieldKey && f.type === 'taxonomy');
if (!field) {
return undefined;
}
return {
levels: field.levels,
selectableLevels: field.optionsRules?.selectableLevels,
};
},
[editingTemplate],
);

const taxonomyItemsServiceCreator = useMemo(
() => createTaxonomyItemsService(api, fileId, resolveTaxonomyFieldConfig),
[api, fileId, resolveTaxonomyFieldConfig],
);

useEffect(() => {
if (createSessionRequest && fileId && !isSessionInitiated.current) {
isSessionInitiated.current = true;
Expand Down Expand Up @@ -362,13 +387,17 @@ function MetadataSidebarRedesign({
{editingTemplate && (
<MetadataInstanceEditor
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
createTaxonomyItemsService={
isMetadataTaxonomyPickerEnabled ? taxonomyItemsServiceCreator : undefined
}
errorCode={extractErrorCode}
isBetaLanguageEnabled={isBetaLanguageEnabled}
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isDeleteConfirmationModalCheckboxEnabled={isDeleteConfirmationModalCheckboxEnabled}
isLargeFile={isLargeFile}
isMetadataMultiLevelTaxonomyFieldEnabled={isMetadataMultiLevelTaxonomyFieldEnabled}
isMetadataTaxonomyPickerEnabled={isMetadataTaxonomyPickerEnabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={handleCancel}
onDelete={handleDeleteInstance}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React from 'react';
import { userEvent } from '@testing-library/user-event';
import { createMemoryHistory } from 'history';
import type { RouteComponentProps } from 'react-router-dom';
import type { MetadataTemplate, MetadataTemplateInstance } from '@box/metadata-editor';
import { screen, render } from '../../../test-utils/testing-library';
import {
MetadataSidebarRedesignComponent as MetadataSidebarRedesign,
type MetadataSidebarRedesignProps,
} from '../MetadataSidebarRedesign';
import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher';
import useMetadataFieldSelection from '../hooks/useMetadataFieldSelection';
import type { MetadataInstanceEditorProps } from '../MetadataInstanceEditor';

jest.mock('../hooks/useSidebarMetadataFetcher');
jest.mock('../hooks/useMetadataFieldSelection');

// Stubbing the editor lets us inspect the props the sidebar hands to it
// without pulling the full @box/metadata-editor form tree into the test.
const mockEditor = jest.fn<null, [MetadataInstanceEditorProps]>(() => null);
jest.mock('../MetadataInstanceEditor', () => ({
__esModule: true,
default: (props: MetadataInstanceEditorProps) => mockEditor(props),
}));

const mockUseSidebarMetadataFetcher = useSidebarMetadataFetcher as jest.MockedFunction<
typeof useSidebarMetadataFetcher
>;
const mockUseMetadataFieldSelection = useMetadataFieldSelection as jest.MockedFunction<
typeof useMetadataFieldSelection
>;

const mockFile = { id: '123', permissions: { can_upload: true } };
const mockTemplates: MetadataTemplate[] = [
{
id: 'metadata_template_custom_1',
scope: 'global',
templateKey: 'properties',
hidden: false,
fields: [],
type: 'metadata_template',
},
];

const taxonomyTemplateInstance = {
canEdit: true,
displayName: 'Taxonomy Template',
fields: [
{
id: 'field-1',
type: 'taxonomy',
key: 'geography',
displayName: 'Geography',
hidden: false,
levels: [
{ level: 1, displayName: 'Region', description: '' },
{ level: 2, displayName: 'Country', description: '' },
],
optionsRules: { selectableLevels: [1, 2] },
},
],
hidden: false,
id: 'taxonomy_instance',
scope: 'global',
templateKey: 'taxonomyTemplate',
type: 'metadata_template',
} as unknown as MetadataTemplateInstance;

const api = { options: { token: jest.fn().mockResolvedValue({ read: 'r', write: 'w' }) } };

const renderSidebar = (features: Record<string, boolean> = {}) => {
const history = createMemoryHistory({ initialEntries: ['/metadata'] });
const routerProps = {
history,
location: history.location,
match: { params: {}, isExact: true, path: '/metadata', url: '/metadata' },
} as unknown as RouteComponentProps;

const props = {
api,
elementId: 'element-1',
fileExtension: 'pdf',
fileId: 'file-id-1',
filteredTemplateIds: [],
getPreview: jest.fn().mockReturnValue({}),
isFeatureEnabled: true,
onError: jest.fn(),
onSuccess: jest.fn(),
...routerProps,
} satisfies MetadataSidebarRedesignProps;

return render(<MetadataSidebarRedesign {...props} />, { wrapperProps: { features } });
};

describe('MetadataSidebarRedesign taxonomy picker wiring', () => {
beforeEach(() => {
mockEditor.mockClear();

mockUseMetadataFieldSelection.mockReturnValue({
selectedMetadataFieldId: null,
handleSelectMetadataField: jest.fn(),
});

mockUseSidebarMetadataFetcher.mockReturnValue({
clearExtractError: jest.fn(),
extractSuggestions: jest.fn(),
handleCreateMetadataInstance: jest.fn(),
handleDeleteMetadataInstance: jest.fn(),
handleUpdateMetadataInstance: jest.fn(),
templateInstances: [taxonomyTemplateInstance],
templates: mockTemplates,
errorMessage: null,
status: STATUS.SUCCESS,
file: mockFile,
extractErrorCode: null,
});
});

const enterEditMode = async () => {
const editButton = await screen.findByRole('button', { name: /Edit Taxonomy Template/i });
await userEvent.click(editButton);
};

const lastEditorProps = (): MetadataInstanceEditorProps => {
const { calls } = mockEditor.mock;
const [firstArg] = calls[calls.length - 1];
return firstArg;
};

test('passes createTaxonomyItemsService to the editor when the picker flag is on', async () => {
renderSidebar({ 'metadata.taxonomyPicker.enabled': true });
await enterEditMode();

const editorProps = lastEditorProps();
expect(editorProps.isMetadataTaxonomyPickerEnabled).toBe(true);
expect(editorProps.createTaxonomyItemsService).toEqual(expect.any(Function));
});

test('omits createTaxonomyItemsService when the picker flag is off', async () => {
renderSidebar({ 'metadata.taxonomyPicker.enabled': false });
await enterEditMode();

const editorProps = lastEditorProps();
expect(editorProps.isMetadataTaxonomyPickerEnabled).toBe(false);
expect(editorProps.createTaxonomyItemsService).toBeUndefined();
});

test('service factory resolves field config from the editing template', async () => {
renderSidebar({ 'metadata.taxonomyPicker.enabled': true });
await enterEditMode();

const editorProps = lastEditorProps();
// The service factory calls resolveField(templateKey, fieldKey); creating
// the service must not throw when the taxonomy field is in the template.
expect(() =>
editorProps.createTaxonomyItemsService?.({
scope: 'global',
templateKey: 'taxonomyTemplate',
fieldKey: 'geography',
}),
).not.toThrow();
});
});
Loading
Loading