Skip to content
Open
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
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,31 +125,31 @@
"@babel/template": "^7.24.7",
"@babel/types": "^7.24.7",
"@box/activity-feed": "^3.0.0",
"@box/blueprint-web": "^16.20.5",
"@box/blueprint-web-assets": "^5.7.12",
"@box/box-ai-agent-selector": "^1.39.21",
"@box/blueprint-web": "^17.2.4",
"@box/blueprint-web-assets": "^5.9.0",
"@box/box-ai-agent-selector": "^2.2.31",
"@box/box-ai-content-answers": "^1.43.22",
"@box/box-item-type-selector": "^1.39.21",
"@box/cldr-data": "^34.2.0",
"@box/collaboration-popover": "^2.2.21",
"@box/combobox-with-api": "^1.42.22",
"@box/combobox-with-api": "^2.2.34",
"@box/content-field": "^1.40.23",
"@box/copy-input": "^1.42.16",
"@box/frontend": "^11.0.1",
"@box/item-icon": "^3.2.0",
"@box/languages": "^1.0.0",
"@box/metadata-editor": "^2.4.19",
"@box/metadata-editor": "^2.5.12",
"@box/metadata-filter": "^1.80.23",
"@box/metadata-taxonomy-picker": "^3.3.1",
"@box/metadata-template-browser": "^2.1.7",
"@box/metadata-taxonomy-picker": "^3.3.14",
"@box/metadata-template-browser": "^2.3.14",
"@box/metadata-view": "^1.53.26",
"@box/react-virtualized": "^9.22.3-rc-box.10",
"@box/readable-time": "^2.2.21",
"@box/threaded-annotations": "^4.8.7",
"@box/types": "^2.1.8",
"@box/unified-share-modal": "^2.15.16",
"@box/uploads-manager": "^2.4.0",
"@box/user-selector": "^2.2.22",
"@box/user-selector": "^3.0.10",
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
"@chromatic-com/storybook": "^5.2.1",
"@commitlint/cli": "^19.8.0",
Expand Down Expand Up @@ -297,29 +297,29 @@
},
"peerDependencies": {
"@box/activity-feed": "^3.0.0",
"@box/blueprint-web": "^16.20.5",
"@box/blueprint-web-assets": "^5.7.12",
"@box/box-ai-agent-selector": "^1.39.21",
"@box/blueprint-web": "^17.2.4",
"@box/blueprint-web-assets": "^5.9.0",
"@box/box-ai-agent-selector": "^2.2.31",
"@box/box-ai-content-answers": "^1.43.22",
"@box/box-item-type-selector": "^1.39.21",
"@box/cldr-data": ">=34.2.0",
"@box/collaboration-popover": "^2.2.21",
"@box/combobox-with-api": "^1.42.22",
"@box/combobox-with-api": "^2.2.34",
"@box/content-field": "^1.40.23",
"@box/copy-input": "^1.42.16",
"@box/item-icon": "^3.2.0",
"@box/metadata-editor": "^2.2.11",
"@box/metadata-editor": "^2.5.12",
"@box/metadata-filter": "^1.80.23",
"@box/metadata-taxonomy-picker": "^3.1.8",
"@box/metadata-template-browser": "^2.1.7",
"@box/metadata-taxonomy-picker": "^3.3.14",
"@box/metadata-template-browser": "^2.3.14",
"@box/metadata-view": "^1.53.26",
"@box/react-virtualized": "^9.22.3-rc-box.10",
"@box/readable-time": "^2.2.21",
"@box/threaded-annotations": "^4.8.7",
"@box/types": "^2.1.8",
"@box/unified-share-modal": "^2.15.16",
"@box/uploads-manager": "^2.4.0",
"@box/user-selector": "^2.2.22",
"@box/user-selector": "^3.0.10",
"@hapi/address": "^2.1.4",
"@tanstack/react-virtual": "^3.13.12",
"axios": "^0.33.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 @@ -6,6 +6,7 @@ import {
FIELD_TYPE_MULTISELECT,
FIELD_TYPE_STRING,
FIELD_TYPE_TAXONOMY,
FIELD_TYPE_USER,
} from '../../features/metadata-instance-fields/constants';
import type { SkillCards } from './skills';

Expand All @@ -15,7 +16,8 @@ type MetadataFieldType =
| typeof FIELD_TYPE_FLOAT
| typeof FIELD_TYPE_MULTISELECT
| typeof FIELD_TYPE_STRING
| typeof FIELD_TYPE_TAXONOMY;
| typeof FIELD_TYPE_TAXONOMY
| typeof FIELD_TYPE_USER;

type MetadataTemplateFieldOption = {
id?: string,
Expand Down
13 changes: 13 additions & 0 deletions src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
CreateTaxonomyItemsService,
TaxonomyOptionsFetcher,
} from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-taxonomy-field/types.js';
import type {
FetchAvatarUrls,
FetchUsers,
} from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-user-field/types.js';
import React, { useContext } from 'react';
import PreviewContext, { type PreviewContextType } from '../content-preview/PreviewContext';
import {
Expand All @@ -20,13 +24,16 @@ import {
export interface MetadataInstanceEditorProps {
areAiSuggestionsAvailable: boolean;
errorCode?: ERROR_CODE_METADATA_AUTOFILL_TIMEOUT | ERROR_CODE_METADATA_PRECONDITION_FAILED | ERROR_CODE_UNKNOWN;
fetchAvatarUrls?: FetchAvatarUrls;
fetchUsers?: FetchUsers;
isBetaLanguageEnabled: boolean;
isBoxAiSuggestionsEnabled: boolean;
isDeleteButtonDisabled: boolean;
isDeleteConfirmationModalCheckboxEnabled: boolean;
isLargeFile: boolean;
isMetadataMultiLevelTaxonomyFieldEnabled: boolean;
isMetadataTaxonomyPickerEnabled?: boolean;
isMetadataUserFieldEnabled?: boolean;
isUnsavedChangesModalOpen: boolean;
onCancel: () => void;
onDelete: (metadataInstance: MetadataTemplateInstance) => void;
Expand All @@ -49,13 +56,16 @@ export interface MetadataInstanceEditorProps {
const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
areAiSuggestionsAvailable,
errorCode,
fetchAvatarUrls,
fetchUsers,
isBetaLanguageEnabled,
isBoxAiSuggestionsEnabled,
isDeleteButtonDisabled,
isDeleteConfirmationModalCheckboxEnabled,
isLargeFile,
isMetadataMultiLevelTaxonomyFieldEnabled,
isMetadataTaxonomyPickerEnabled = false,
isMetadataUserFieldEnabled = false,
isUnsavedChangesModalOpen,
onCancel,
onDelete,
Expand All @@ -82,6 +92,9 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
// TODO investigate if this property should be optional and by default false
isMultilevelTaxonomyFieldEnabled={isMetadataMultiLevelTaxonomyFieldEnabled}
isTaxonomyPickerEnabled={isMetadataTaxonomyPickerEnabled}
isUserFieldEnabled={isMetadataUserFieldEnabled}
fetchAvatarUrls={fetchAvatarUrls}
fetchUsers={fetchUsers}
createTaxonomyItemsService={createTaxonomyItemsService}
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
errorCode={errorCode}
Expand Down
4 changes: 4 additions & 0 deletions src/elements/content-sidebar/MetadataSidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type ExternalProps = {
onWarningModalDiscard?: () => void,
onWarningModalClose?: () => void,
trackEvent?: (eventName: string, data?: { [string]: any }) => void,
// Redesign path only — optional host overrides for the metadata user field fetchers.
// Canonical types (FetchUsers / FetchAvatarUrls) live in @box/metadata-editor (TypeScript).
fetchUsers?: (inputValue: string) => Promise<Array<Object>>,
fetchAvatarUrls?: (userContacts: Array<Object>) => Promise<{ [key: string]: string }>,
};

type PropsWithoutContext = {
Expand Down
22 changes: 22 additions & 0 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
type MetadataTemplateInstance,
} from '@box/metadata-editor';
import { TreeQueryInput } from '@box/combobox-with-api';
import type {
FetchAvatarUrls,
FetchUsers,
} from '@box/metadata-editor/lib/components/metadata-editor-fields/components/metadata-user-field/types.js';

import type { GetPreviewForMetadataReturnType } from './types/BoxAISidebarTypes';
import API from '../../api';
Expand Down Expand Up @@ -49,6 +53,7 @@ import {
metadataTaxonomyNodeAncestorsFetcher,
type TaxonomyFieldConfig,
} from './fetchers/metadataTaxonomyFetcher';
import { createFetchAvatarUrls, createFetchUsers } from './fetchers/metadataUserFetcher';
import { useMetadataSidebarFilteredTemplates } from './hooks/useMetadataSidebarFilteredTemplates';
import useMetadataFieldSelection from './hooks/useMetadataFieldSelection';
import useMetadataSidebarUnsavedChangesGuard from './hooks/useMetadataSidebarUnsavedChangesGuard';
Expand All @@ -60,6 +65,10 @@ mark(MARK_NAME_JS_READY);
export interface ExternalProps {
isFeatureEnabled: boolean;
getStructuredTextRep?: (fileId: string, accessToken: string) => Promise<string>;
/** Custom user/group search fetcher (e.g. a session-authenticated contacts endpoint); defaults to enterprise /users + /groups search. */
fetchUsers?: FetchUsers;
/** Custom avatar URL resolver; defaults to /users/:id/avatar with an access token. */
fetchAvatarUrls?: FetchAvatarUrls;
}

interface PropsWithoutContext extends ExternalProps {
Expand Down Expand Up @@ -102,6 +111,8 @@ function MetadataSidebarRedesign({
elementId,
fileExtension,
fileId,
fetchAvatarUrls: fetchAvatarUrlsOverride,
fetchUsers: fetchUsersOverride,
filteredTemplateIds = [],
getPreview,
history,
Expand All @@ -121,6 +132,7 @@ function MetadataSidebarRedesign({
const isBetaLanguageEnabled: boolean = useFeatureEnabled('metadata.betaLanguage.enabled');
const isMetadataMultiLevelTaxonomyFieldEnabled: boolean = useFeatureEnabled('metadata.multilevelTaxonomy.enabled');
const isMetadataTaxonomyPickerEnabled: boolean = useFeatureEnabled('metadata.taxonomyPicker.enabled');
const isMetadataUserFieldEnabled: boolean = useFeatureEnabled('metadata.userField.enabled');
const isAdvancedExtractAgentEnabled: boolean = useFeatureEnabled('metadata.extractAdvancedAgents.enabled');
const isDeleteConfirmationModalCheckboxEnabled: boolean = useFeatureEnabled(
'metadata.deleteConfirmationModalCheckbox.enabled',
Expand Down Expand Up @@ -354,6 +366,13 @@ function MetadataSidebarRedesign({
[api, fileId, resolveTaxonomyFieldConfig],
);

// Hosts can inject their own user-field fetchers (e.g. a session-authenticated contacts
// endpoint); without overrides the sidebar uses the default BUIE API implementations.
const defaultFetchUsers = useMemo(() => createFetchUsers(api, fileId), [api, fileId]);
const defaultFetchAvatarUrls = useMemo(() => createFetchAvatarUrls(api, fileId), [api, fileId]);
const fetchUsers = fetchUsersOverride ?? defaultFetchUsers;
const fetchAvatarUrls = fetchAvatarUrlsOverride ?? defaultFetchAvatarUrls;

useEffect(() => {
if (createSessionRequest && fileId && !isSessionInitiated.current) {
isSessionInitiated.current = true;
Expand Down Expand Up @@ -391,13 +410,16 @@ function MetadataSidebarRedesign({
isMetadataTaxonomyPickerEnabled ? taxonomyItemsServiceCreator : undefined
}
errorCode={extractErrorCode}
fetchAvatarUrls={fetchAvatarUrls}
fetchUsers={fetchUsers}
isBetaLanguageEnabled={isBetaLanguageEnabled}
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isDeleteConfirmationModalCheckboxEnabled={isDeleteConfirmationModalCheckboxEnabled}
isLargeFile={isLargeFile}
isMetadataMultiLevelTaxonomyFieldEnabled={isMetadataMultiLevelTaxonomyFieldEnabled}
isMetadataTaxonomyPickerEnabled={isMetadataTaxonomyPickerEnabled}
isMetadataUserFieldEnabled={isMetadataUserFieldEnabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={handleCancel}
onDelete={handleDeleteInstance}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import type { MetadataTemplateInstance } from '@box/metadata-editor';
import { render } from '../../../test-utils/testing-library';
import MetadataInstanceEditor, { type MetadataInstanceEditorProps } from '../MetadataInstanceEditor';

const mockMetadataInstanceForm = jest.fn<null, [Record<string, unknown>]>(() => null);

// Stubbing MetadataInstanceForm lets us inspect the props handed to @box/metadata-editor
// without rendering the real (possibly yarn-linked) package form tree.
jest.mock('@box/metadata-editor', () => ({
__esModule: true,
MetadataInstanceForm: (props: Record<string, unknown>) => mockMetadataInstanceForm(props),
}));

describe('MetadataInstanceEditor user field wiring', () => {
const template = {
id: 'template-id',
displayName: 'Template Name',
fields: [],
scope: 'global',
templateKey: 'customTemplate',
type: 'metadata_template',
hidden: false,
canEdit: true,
} as unknown as MetadataTemplateInstance;

const defaultProps: MetadataInstanceEditorProps = {
areAiSuggestionsAvailable: true,
isBetaLanguageEnabled: false,
isBoxAiSuggestionsEnabled: true,
isDeleteButtonDisabled: false,
isDeleteConfirmationModalCheckboxEnabled: false,
isLargeFile: false,
isMetadataMultiLevelTaxonomyFieldEnabled: false,
isUnsavedChangesModalOpen: false,
onCancel: jest.fn(),
onDelete: jest.fn(),
onDiscardUnsavedChanges: jest.fn(),
onSubmit: jest.fn(),
setIsUnsavedChangesModalOpen: jest.fn(),
taxonomyOptionsFetcher: jest.fn(),
template,
};

const lastFormProps = (): Record<string, unknown> => {
const { calls } = mockMetadataInstanceForm.mock;
const [firstArg] = calls[calls.length - 1];
return firstArg;
};

beforeEach(() => {
mockMetadataInstanceForm.mockClear();
});

test('disables the user field by default', () => {
render(<MetadataInstanceEditor {...defaultProps} />);

expect(lastFormProps()).toEqual(expect.objectContaining({ isUserFieldEnabled: false }));
});

test('passes isUserFieldEnabled, fetchUsers and fetchAvatarUrls through to MetadataInstanceForm', () => {
const fetchUsers = jest.fn();
const fetchAvatarUrls = jest.fn();

render(
<MetadataInstanceEditor
{...defaultProps}
fetchAvatarUrls={fetchAvatarUrls}
fetchUsers={fetchUsers}
isMetadataUserFieldEnabled
/>,
);

expect(lastFormProps()).toEqual(
expect.objectContaining({
fetchAvatarUrls,
fetchUsers,
isUserFieldEnabled: true,
}),
);
});
});
Loading
Loading