Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(taxonomy): added metadata taxonomy field support #3710

Merged
merged 9 commits into from
Oct 15, 2024
4 changes: 2 additions & 2 deletions src/api/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -1111,10 +1111,10 @@ class Metadata extends File {
}

const url = this.getMetadataOptionsUrl(scope, templateKey, fieldKey);
const { marker, searchInput, signal } = options;
const { marker, searchInput: query_text, signal } = options;
const params = {
...(marker ? { marker } : {}),
...(searchInput ? { searchInput } : {}),
...(query_text ? { query_text } : {}),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but should this be camel cased?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional as the query string param is required in snake case (ie /metadata?query_text=search)

...(level || level === 0 ? { level } : {}),
};

Expand Down
2 changes: 1 addition & 1 deletion src/api/__tests__/Metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2929,7 +2929,7 @@ describe('api/Metadata', () => {
id: 'file_id',
params: {
marker: 'current_marker',
searchInput: 'search_term',
query_text: 'search_term',
level: 0,
},
});
Expand Down
4 changes: 3 additions & 1 deletion src/common/types/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FIELD_TYPE_FLOAT,
FIELD_TYPE_MULTISELECT,
FIELD_TYPE_STRING,
FIELD_TYPE_TAXONOMY,
} from '../../features/metadata-instance-fields/constants';
import type { SkillCards } from './skills';

Expand All @@ -13,7 +14,8 @@ type MetadataFieldType =
| typeof FIELD_TYPE_ENUM
| typeof FIELD_TYPE_FLOAT
| typeof FIELD_TYPE_MULTISELECT
| typeof FIELD_TYPE_STRING;
| typeof FIELD_TYPE_STRING
| typeof FIELD_TYPE_TAXONOMY;

type MetadataTemplateFieldOption = {
id?: string,
Expand Down
13 changes: 10 additions & 3 deletions src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import {
type MetadataTemplateInstance,
type FetcherResponse,
type BaseOptionType,
type PaginationQueryInput,
} from '@box/metadata-editor';
import React from 'react';

const noopTaxonomyFetcher = () => Promise.resolve({ options: [] } satisfies FetcherResponse<BaseOptionType>);

export interface MetadataInstanceEditorProps {
areAiSuggestionsAvailable: boolean;
isBoxAiSuggestionsEnabled: boolean;
Expand All @@ -20,6 +19,13 @@ export interface MetadataInstanceEditorProps {
onDiscardUnsavedChanges: () => void;
onSubmit: (values: FormValues, operations: JSONPatchOperations) => Promise<void>;
setIsUnsavedChangesModalOpen: (isUnsavedChangesModalOpen: boolean) => void;
taxonomyOptionsFetcher: (
scope: string,
templateKey: string,
fieldKey: string,
level: number,
options: PaginationQueryInput,
) => Promise<FetcherResponse<BaseOptionType>>;
template: MetadataTemplateInstance;
}

Expand All @@ -33,6 +39,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
onDiscardUnsavedChanges,
onSubmit,
setIsUnsavedChangesModalOpen,
taxonomyOptionsFetcher,
template,
}) => {
return (
Expand All @@ -47,7 +54,7 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
onSubmit={onSubmit}
selectedTemplateInstance={template}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
taxonomyOptionsFetcher={noopTaxonomyFetcher}
taxonomyOptionsFetcher={taxonomyOptionsFetcher}
/>
);
};
Expand Down
11 changes: 11 additions & 0 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type JSONPatchOperations,
type MetadataTemplate,
type MetadataTemplateInstance,
type PaginationQueryInput,
} from '@box/metadata-editor';

import API from '../../api';
Expand All @@ -35,6 +36,7 @@ import './MetadataSidebarRedesign.scss';
import MetadataInstanceEditor from './MetadataInstanceEditor';
import { convertTemplateToTemplateInstance } from './utils/convertTemplateToTemplateInstance';
import { isExtensionSupportedForMetadataSuggestions } from './utils/isExtensionSupportedForMetadataSuggestions';
import { metadataTaxonomyFetcher } from './fetchers/metadataTaxonomyFetcher';

const MARK_NAME_JS_READY = `${ORIGIN_METADATA_SIDEBAR_REDESIGN}_${EVENT_JS_READY}`;

Expand Down Expand Up @@ -169,6 +171,14 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna
const showList = !showEditor && templateInstances.length > 0 && !editingTemplate;
const areAiSuggestionsAvailable = isExtensionSupportedForMetadataSuggestions(file?.extension ?? '');

const taxonomyOptionsFetcher = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i don't think the async is needed since we're not directly using await. the function will still be a promise since metadataTaxonomyFetcher is async

scope: string,
templateKey: string,
fieldKey: string,
level: number,
options: PaginationQueryInput,
) => metadataTaxonomyFetcher(api, fileId, scope, templateKey, fieldKey, level, options);

return (
<SidebarContent
actions={metadataDropdown}
Expand Down Expand Up @@ -198,6 +208,7 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna
onDiscardUnsavedChanges={handleDiscardUnsavedChanges}
onSubmit={handleSubmit}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
taxonomyOptionsFetcher={taxonomyOptionsFetcher}
template={editingTemplate}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React from 'react';
import { AutofillContextProvider, type MetadataTemplateInstance } from '@box/metadata-editor';
import {
AutofillContextProvider,
type MetadataTemplateField,
type MetadataTemplateInstance,
} from '@box/metadata-editor';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@box/blueprint-web';
import { IntlProvider } from 'react-intl';
Expand Down Expand Up @@ -75,6 +79,7 @@ describe('MetadataInstanceEditor', () => {
onDiscardUnsavedChanges: mockOnDiscardUnsavedChanges,
onSubmit: jest.fn(),
setIsUnsavedChangesModalOpen: mockSetIsUnsavedChangesModalOpen,
taxonomyOptionsFetcher: jest.fn(),
TylerGauntlett marked this conversation as resolved.
Show resolved Hide resolved
template: mockMetadataTemplate,
};

Expand Down Expand Up @@ -153,4 +158,44 @@ describe('MetadataInstanceEditor', () => {

expect(mockOnDiscardUnsavedChanges).toHaveBeenCalled();
});

test('should call taxonomyOptionsFetcher on metadata taxonomy field search', async () => {
const taxonomyField: MetadataTemplateField = {
type: 'taxonomy',
key: 'States',
displayName: 'States',
description: 'State locations',
hidden: false,
id: '2',
taxonomyKey: 'geography',
taxonomyId: '1',
optionsRules: {
multiSelect: true,
selectableLevels: [1],
},
};

const template: MetadataTemplateInstance = {
...mockCustomMetadataTemplateWithField,
fields: [...mockCustomMetadataTemplateWithField.fields, taxonomyField],
};

const props: MetadataInstanceEditorProps = {
...defaultProps,
template,
};

const { getByRole } = renderWithAutofill(<MetadataInstanceEditor {...props} />);
const combobox = getByRole('combobox', { name: 'States' });

await userEvent.type(combobox, 'A');

expect(props.taxonomyOptionsFetcher).toHaveBeenCalledWith(
template.scope,
template.templateKey,
taxonomyField.key,
taxonomyField.optionsRules.selectableLevels[0],
{ marker: null, searchInput: 'A', signal: expect.anything() },
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { PaginationQueryInput } from '@box/metadata-editor';
import { metadataTaxonomyFetcher } from '../fetchers/metadataTaxonomyFetcher';
import type API from '../../../api';

describe('metadataTaxonomyFetcher', () => {
let apiMock: jest.Mocked<API>;
const fileId = '12345';
const scope = 'global';
const templateKey = 'template_123';
const fieldKey = 'field_abc';
const level = 1;
const options: PaginationQueryInput = { marker: 'marker_1' };

beforeEach(() => {
apiMock = {
getMetadataAPI: jest.fn().mockReturnValue({
getMetadataOptions: jest.fn(),
}),
};
});

test('should fetch metadata options and return formatted data', async () => {
const mockMetadataOptions = {
entries: [
{ id: 'opt1', display_name: 'Option 1' },
{ id: 'opt2', display_name: 'Option 2' },
],
};

apiMock.getMetadataAPI(false).getMetadataOptions.mockResolvedValue(mockMetadataOptions);

const result = await metadataTaxonomyFetcher(apiMock, fileId, scope, templateKey, fieldKey, level, options);

const expectedResult = {
options: [
{ value: 'opt1', displayValue: 'Option 1' },
{ value: 'opt2', displayValue: 'Option 2' },
],
marker: 'marker_1',
};

expect(apiMock.getMetadataAPI).toHaveBeenCalledWith(false);
expect(apiMock.getMetadataAPI(false).getMetadataOptions).toHaveBeenCalledWith(
fileId,
scope,
templateKey,
fieldKey,
level,
options,
);
expect(result).toEqual(expectedResult);
});

test('should handle empty entries array', async () => {
const mockMetadataOptions = {
entries: [],
};

apiMock.getMetadataAPI(false).getMetadataOptions.mockResolvedValue(mockMetadataOptions);

const result = await metadataTaxonomyFetcher(apiMock, fileId, scope, templateKey, fieldKey, level, options);

const expectedResult = {
options: [],
marker: 'marker_1',
};

expect(result).toEqual(expectedResult);
});

test('should set marker to null if not provided in options', async () => {
const mockMetadataOptions = {
entries: [{ id: 'opt1', display_name: 'Option 1' }],
};

apiMock.getMetadataAPI(false).getMetadataOptions.mockResolvedValue(mockMetadataOptions);

const result = await metadataTaxonomyFetcher(apiMock, fileId, scope, templateKey, fieldKey, level, {});

const expectedResult = {
options: [{ value: 'opt1', displayValue: 'Option 1' }],
marker: null,
};

expect(result).toEqual(expectedResult);
});

test('should throw an error if getMetadataOptions fails', async () => {
const error = new Error('API Error');
apiMock.getMetadataAPI(false).getMetadataOptions.mockRejectedValue(error);

await expect(
metadataTaxonomyFetcher(apiMock, fileId, scope, templateKey, fieldKey, level, options),
).rejects.toThrow('API Error');
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this repo favors utils folder path

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { PaginationQueryInput } from '@box/metadata-editor';
import type API from '../../../api';
import type { MetadataOptionEntry } from '../../../common/types/metadata';

export const metadataTaxonomyFetcher = async (
api: API,
fileId: string,
scope: string,
templateKey: string,
fieldKey: string,
level: number,
options: PaginationQueryInput,
) => {
const metadataOptions = await api
.getMetadataAPI(false)
.getMetadataOptions(fileId, scope, templateKey, fieldKey, level, options);
const { marker = null } = options;

return {
options: metadataOptions.entries.map((metadataOption: MetadataOptionEntry) => ({
value: metadataOption.id,
displayValue: metadataOption.display_name,
})),
marker,
};
};
1 change: 1 addition & 0 deletions src/features/metadata-instance-fields/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const FIELD_TYPE_INTEGER: 'integer' = 'integer';
export const FIELD_TYPE_FLOAT: 'float' = 'float';
export const FIELD_TYPE_MULTISELECT: 'multiSelect' = 'multiSelect';
export const FIELD_TYPE_STRING: 'string' = 'string';
export const FIELD_TYPE_TAXONOMY: 'taxonomy' = 'taxonomy';
Loading