Skip to content

Commit 0bb39ff

Browse files
committed
feat(content-explorer): Add initial filter values to Metadata View
1 parent 0adee80 commit 0bb39ff

3 files changed

Lines changed: 59 additions & 58 deletions

File tree

src/elements/content-explorer/MetadataViewContainer.tsx

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,56 @@
11
import * as React from 'react';
2+
import type { EnumType, FloatType, MetadataFormFieldValue, RangeType } from '@box/metadata-filter';
23
import { MetadataView, type MetadataViewProps } from '@box/metadata-view';
3-
import { FloatType, MetadataFormFieldValue, RangeType } from '@box/metadata-filter';
44

5-
import type { MetadataTemplate } from '../../common/types/metadata';
65
import type { Collection } from '../../common/types/core';
6+
import type { MetadataTemplate } from '../../common/types/metadata';
77

8-
// Public-friendly metadata value shape (array value for enum type, range/float objects stay the same)
9-
export type MetadataFormFieldValuePublic = string[] | RangeType | FloatType;
8+
// Public-friendly version of MetadataFormFieldValue from @box/metadata-filter
9+
// (string[] for enum type, range/float objects stay the same)
10+
type EnumToStringArray<T> = T extends EnumType ? string[] : T;
11+
type ExternalMetadataFormFieldValue = EnumToStringArray<MetadataFormFieldValue>;
1012

11-
export type FilterValuesPublic = Record<
13+
type ExternalFilterValues = Record<
1214
string,
1315
{
14-
value: MetadataFormFieldValuePublic;
16+
value: ExternalMetadataFormFieldValue;
1517
}
1618
>;
1719

18-
export type ActionBarProps = Omit<
20+
type ActionBarProps = Omit<
1921
MetadataViewProps['actionBarProps'],
2022
'initialFilterValues' | 'onFilterSubmit' | 'filterGroups'
2123
> & {
22-
initialFilterValues?: FilterValuesPublic;
23-
onFilterSubmit?: (filterValues: FilterValuesPublic) => void;
24+
initialFilterValues?: ExternalFilterValues;
25+
onFilterSubmit?: (filterValues: ExternalFilterValues) => void;
2426
};
2527

28+
function transformInitialFilterValuesToInternal(
29+
publicValues?: ExternalFilterValues,
30+
): Record<string, { value: MetadataFormFieldValue }> | undefined {
31+
if (!publicValues) return undefined;
32+
33+
return Object.entries(publicValues).reduce<Record<string, { value: MetadataFormFieldValue }>>(
34+
(acc, [key, { value }]) => {
35+
acc[key] = Array.isArray(value) ? { value: { enum: value } } : { value };
36+
return acc;
37+
},
38+
{},
39+
);
40+
}
41+
42+
function transformInternalFieldsToPublic(
43+
fields: Record<string, { value: MetadataFormFieldValue }>,
44+
): ExternalFilterValues {
45+
return Object.entries(fields).reduce<ExternalFilterValues>((acc, [key, { value }]) => {
46+
acc[key] =
47+
'enum' in value && Array.isArray(value.enum)
48+
? { value: value.enum }
49+
: { value: value as RangeType | FloatType };
50+
return acc;
51+
}, {});
52+
}
53+
2654
export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'items' | 'actionBarProps'> {
2755
actionBarProps?: ActionBarProps;
2856
currentCollection: Collection;
@@ -37,6 +65,7 @@ const MetadataViewContainer = ({
3765
...rest
3866
}: MetadataViewContainerProps) => {
3967
const { items = [] } = currentCollection;
68+
const { initialFilterValues: initialFilterValuesProp, onFilterSubmit: onFilterSubmitProp } = actionBarProps ?? {};
4069

4170
const filterGroups = React.useMemo(
4271
() => [
@@ -58,41 +87,19 @@ const MetadataViewContainer = ({
5887
);
5988

6089
// Transform initial filter values to internal field format
61-
const initialFilterValues = React.useMemo(() => {
62-
const filterValues = actionBarProps?.initialFilterValues;
63-
if (!filterValues) return undefined;
64-
65-
const transformed: Record<string, { value: MetadataFormFieldValue }> = {};
66-
Object.entries(filterValues).forEach(([key, filterValue]) => {
67-
const { value } = filterValue;
68-
if (Array.isArray(value)) {
69-
// Convert customer-friendly array to internal enum shape
70-
transformed[key] = { value: { enum: value } };
71-
} else {
72-
// Keep range/float as-is
73-
transformed[key] = { value };
74-
}
75-
});
76-
return transformed;
77-
}, [actionBarProps?.initialFilterValues]);
90+
const initialFilterValues = React.useMemo(
91+
() => transformInitialFilterValuesToInternal(initialFilterValuesProp),
92+
[initialFilterValuesProp],
93+
);
7894

7995
// Transform field values to public-friendly format
8096
const onFilterSubmit = React.useCallback(
8197
(fields: Record<string, { value: MetadataFormFieldValue }>) => {
82-
if (!actionBarProps?.onFilterSubmit) return;
83-
84-
const transformed: Record<string, { value: MetadataFormFieldValuePublic }> = {};
85-
Object.entries(fields).forEach(([key, filterValue]) => {
86-
const { value } = filterValue;
87-
if (value && typeof value === 'object' && 'enum' in value && Array.isArray(value.enum)) {
88-
transformed[key] = { value: value.enum };
89-
} else {
90-
transformed[key] = { value: value as RangeType | FloatType };
91-
}
92-
});
93-
actionBarProps.onFilterSubmit(transformed);
98+
if (!onFilterSubmitProp) return;
99+
const transformed = transformInternalFieldsToPublic(fields);
100+
onFilterSubmitProp(transformed);
94101
},
95-
[actionBarProps],
102+
[onFilterSubmitProp],
96103
);
97104

98105
const transformedActionBarProps = React.useMemo(() => {

src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as React from 'react';
2-
import userEvent from '@testing-library/user-event';
3-
import { render, screen, waitFor, within } from '../../../test-utils/testing-library';
4-
import MetadataViewContainer, { MetadataViewContainerProps } from '../MetadataViewContainer';
2+
53
import type { Collection } from '../../../common/types/core';
64
import type { MetadataTemplate, MetadataTemplateField } from '../../../common/types/metadata';
5+
import { render, screen, userEvent, waitFor, within } from '../../../test-utils/testing-library';
6+
import MetadataViewContainer, { MetadataViewContainerProps } from '../MetadataViewContainer';
77

88
describe('elements/content-explorer/MetadataViewContainer', () => {
99
const mockItems = [
@@ -19,7 +19,7 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
1919
type: 'string',
2020
},
2121
{
22-
id: 'field1',
22+
id: 'field2',
2323
key: 'industry',
2424
displayName: 'Industry',
2525
type: 'enum',
@@ -103,14 +103,11 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
103103

104104
renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit } });
105105

106-
const roleChip = screen.getByRole('button', { name: /Contact Role/ });
107-
await userEvent.click(roleChip);
108-
let menu = screen.getByRole('menu');
109-
await userEvent.click(within(menu).getByRole('menuitemcheckbox', { name: 'Developer' }));
106+
await userEvent().click(screen.getByRole('button', { name: /Contact Role/ }));
107+
await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Developer' }));
110108
// Re-open the chip to select a second value (menu closes after submit)
111-
await userEvent.click(roleChip);
112-
menu = screen.getByRole('menu');
113-
await userEvent.click(within(menu).getByRole('menuitemcheckbox', { name: 'Marketing' }));
109+
await userEvent().click(screen.getByRole('button', { name: /Contact Role/ }));
110+
await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Marketing' }));
114111

115112
await waitFor(() => expect(onFilterSubmit).toHaveBeenCalledTimes(2));
116113
const firstCall = onFilterSubmit.mock.calls[0][0];

src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,16 @@ export const metadataViewV2WithInitialFilterValues: Story = {
150150
},
151151
},
152152
play: async ({ canvas }) => {
153-
// Wait for content to render
153+
// Wait for chips to update with initial values
154154
await waitFor(() => {
155-
expect(canvas.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
155+
expect(canvas.getByRole('button', { name: /Industry/i })).toHaveTextContent(/\(1\)/);
156156
});
157-
// Chips should reflect initial counts
158-
const industryChip = canvas.getByRole('button', { name: /Industry/i });
159-
await waitFor(() => expect(industryChip).toHaveTextContent(/\(1\)/));
160-
157+
// Other chips should reflect initialized values
161158
const contactRoleChip = canvas.getByRole('button', { name: /Contact Role/i });
162-
await waitFor(() => expect(contactRoleChip).toHaveTextContent(/\(3\)/));
159+
expect(contactRoleChip).toHaveTextContent(/\(3\)/);
163160

164161
const fileTypeChip = canvas.getByRole('button', { name: /Box Note/i });
165-
await waitFor(() => expect(fileTypeChip).toHaveTextContent(/\+2/));
162+
expect(fileTypeChip).toHaveTextContent(/\+2/);
166163
},
167164
};
168165

0 commit comments

Comments
 (0)