Skip to content

Commit b68cd70

Browse files
authored
feat(content-explorer): Add initial filter values to Metadata View (#4225)
* feat(content-explorer): Add initial filter values * feat(content-explorer): Add initial filter values * feat(content-explorer): Add initial filter values to Metadata View * feat(content-explorer): Add initial filter values to Metadata View
1 parent f969170 commit b68cd70

5 files changed

Lines changed: 172 additions & 42 deletions

File tree

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@
128128
"@box/blueprint-web-assets": "4.61.5",
129129
"@box/box-ai-agent-selector": "^0.52.0",
130130
"@box/box-ai-content-answers": "^0.124.1",
131-
"@box/box-item-type-selector": "^0.61.12",
131+
"@box/box-item-type-selector": "^0.63.12",
132132
"@box/cldr-data": "^34.2.0",
133133
"@box/combobox-with-api": "^0.34.9",
134134
"@box/frontend": "^11.0.1",
135-
"@box/item-icon": "^0.17.0",
135+
"@box/item-icon": "^0.17.15",
136136
"@box/languages": "^1.0.0",
137137
"@box/metadata-editor": "^0.122.12",
138-
"@box/metadata-filter": "^1.16.12",
139-
"@box/metadata-view": "^0.29.4",
138+
"@box/metadata-filter": "^1.19.2",
139+
"@box/metadata-view": "^0.41.2",
140140
"@box/react-virtualized": "^9.22.3-rc-box.10",
141141
"@box/types": "^0.2.1",
142142
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
@@ -297,13 +297,13 @@
297297
"@box/blueprint-web-assets": "4.61.5",
298298
"@box/box-ai-agent-selector": "^0.52.0",
299299
"@box/box-ai-content-answers": "^0.124.1",
300-
"@box/box-item-type-selector": "^0.61.12",
300+
"@box/box-item-type-selector": "^0.63.12",
301301
"@box/cldr-data": ">=34.2.0",
302302
"@box/combobox-with-api": "^0.34.9",
303-
"@box/item-icon": "^0.17.0",
303+
"@box/item-icon": "^0.17.15",
304304
"@box/metadata-editor": "^0.122.12",
305-
"@box/metadata-filter": "^1.16.12",
306-
"@box/metadata-view": "^0.29.4",
305+
"@box/metadata-filter": "^1.19.2",
306+
"@box/metadata-view": "^0.41.2",
307307
"@box/react-virtualized": "^9.22.3-rc-box.10",
308308
"@box/types": "^0.2.1",
309309
"@hapi/address": "^2.1.4",

src/elements/content-explorer/MetadataViewContainer.tsx

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,58 @@
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 type { MetadataTemplate } from '../../common/types/metadata';
4+
45
import type { Collection } from '../../common/types/core';
6+
import type { MetadataTemplate } from '../../common/types/metadata';
7+
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>;
12+
13+
type ExternalFilterValues = Record<
14+
string,
15+
{
16+
value: ExternalMetadataFormFieldValue;
17+
}
18+
>;
519

6-
export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'items'> {
20+
type ActionBarProps = Omit<
21+
MetadataViewProps['actionBarProps'],
22+
'initialFilterValues' | 'onFilterSubmit' | 'filterGroups'
23+
> & {
24+
initialFilterValues?: ExternalFilterValues;
25+
onFilterSubmit?: (filterValues: ExternalFilterValues) => void;
26+
};
27+
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+
54+
export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'items' | 'actionBarProps'> {
55+
actionBarProps?: ActionBarProps;
756
currentCollection: Collection;
857
metadataTemplate: MetadataTemplate;
958
}
@@ -16,6 +65,7 @@ const MetadataViewContainer = ({
1665
...rest
1766
}: MetadataViewContainerProps) => {
1867
const { items = [] } = currentCollection;
68+
const { initialFilterValues: initialFilterValuesProp, onFilterSubmit: onFilterSubmitProp } = actionBarProps ?? {};
1969

2070
const filterGroups = React.useMemo(
2171
() => [
@@ -36,17 +86,32 @@ const MetadataViewContainer = ({
3686
[metadataTemplate],
3787
);
3888

39-
return (
40-
<MetadataView
41-
actionBarProps={{
42-
...actionBarProps,
43-
filterGroups,
44-
}}
45-
columns={columns}
46-
items={items}
47-
{...rest}
48-
/>
89+
// Transform initial filter values to internal field format
90+
const initialFilterValues = React.useMemo(
91+
() => transformInitialFilterValuesToInternal(initialFilterValuesProp),
92+
[initialFilterValuesProp],
4993
);
94+
95+
// Transform field values to public-friendly format
96+
const onFilterSubmit = React.useCallback(
97+
(fields: Record<string, { value: MetadataFormFieldValue }>) => {
98+
if (!onFilterSubmitProp) return;
99+
const transformed = transformInternalFieldsToPublic(fields);
100+
onFilterSubmitProp(transformed);
101+
},
102+
[onFilterSubmitProp],
103+
);
104+
105+
const transformedActionBarProps = React.useMemo(() => {
106+
return {
107+
...actionBarProps,
108+
initialFilterValues,
109+
onFilterSubmit,
110+
filterGroups,
111+
};
112+
}, [actionBarProps, initialFilterValues, onFilterSubmit, filterGroups]);
113+
114+
return <MetadataView actionBarProps={transformedActionBarProps} columns={columns} items={items} {...rest} />;
50115
};
51116

52117
export default MetadataViewContainer;

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

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

78
describe('elements/content-explorer/MetadataViewContainer', () => {
89
const mockItems = [
@@ -18,7 +19,7 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
1819
type: 'string',
1920
},
2021
{
21-
id: 'field1',
22+
id: 'field2',
2223
key: 'industry',
2324
displayName: 'Industry',
2425
type: 'enum',
@@ -80,4 +81,38 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
8081
expect(screen.getByText('File 1.txt')).toBeInTheDocument();
8182
expect(screen.getByText('File 2.pdf')).toBeInTheDocument();
8283
});
84+
85+
test('should pass values as string[] on submit', async () => {
86+
const onFilterSubmit = jest.fn();
87+
const template: MetadataTemplate = {
88+
...mockMetadataTemplate,
89+
fields: [
90+
{
91+
id: 'ms1',
92+
key: 'role',
93+
displayName: 'Contact Role',
94+
type: 'multiSelect',
95+
options: [
96+
{ id: 'r1', key: 'Developer' },
97+
{ id: 'r2', key: 'Marketing' },
98+
{ id: 'r3', key: 'Sales' },
99+
],
100+
},
101+
],
102+
};
103+
104+
renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit } });
105+
106+
await userEvent().click(screen.getByRole('button', { name: /Contact Role/ }));
107+
await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Developer' }));
108+
// Re-open the chip to select a second value (menu closes after submit)
109+
await userEvent().click(screen.getByRole('button', { name: /Contact Role/ }));
110+
await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Marketing' }));
111+
112+
await waitFor(() => expect(onFilterSubmit).toHaveBeenCalledTimes(2));
113+
const firstCall = onFilterSubmit.mock.calls[0][0];
114+
const secondCall = onFilterSubmit.mock.calls[1][0];
115+
expect(firstCall['role-filter'].value).toEqual(['Developer']);
116+
expect(secondCall['role-filter'].value).toEqual(['Developer', 'Marketing']);
117+
});
83118
});

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { http, HttpResponse } from 'msw';
21
import type { Meta, StoryObj } from '@storybook/react';
2+
import { http, HttpResponse } from 'msw';
3+
import { expect, userEvent, waitFor, within } from 'storybook/test';
34
import { Download, SignMeOthers } from '@box/blueprint-web-assets/icons/Fill/index';
45
import { Sign } from '@box/blueprint-web-assets/icons/Line';
5-
import { expect, userEvent, waitFor, within } from 'storybook/test';
66
import noop from 'lodash/noop';
7+
78
import ContentExplorer from '../../ContentExplorer';
89
import { DEFAULT_HOSTNAME_API } from '../../../../constants';
910
import { mockMetadata, mockSchema } from '../../../common/__mocks__/mockMetadata';
@@ -126,13 +127,42 @@ export const metadataViewV2WithCustomActions: Story = {
126127
await waitFor(() => {
127128
expect(canvas.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
128129
});
129-
130130
const firstRow = canvas.getByRole('row', { name: /Child 2/i });
131131
const ellipsesButton = within(firstRow).getByRole('button', { name: 'Action menu' });
132132
userEvent.click(ellipsesButton);
133133
},
134134
};
135135

136+
const initialFilterActionBarProps = {
137+
initialFilterValues: {
138+
'industry-filter': { value: ['Legal'] },
139+
'mimetype-filter': { value: ['boxnoteType', 'documentType', 'threedType'] },
140+
'role-filter': { value: ['Developer', 'Business Owner', 'Marketing'] },
141+
},
142+
};
143+
144+
export const metadataViewV2WithInitialFilterValues: Story = {
145+
args: {
146+
...metadataViewV2ElementProps,
147+
metadataViewProps: {
148+
columns,
149+
actionBarProps: initialFilterActionBarProps,
150+
},
151+
},
152+
play: async ({ canvas }) => {
153+
// Wait for chips to update with initial values
154+
await waitFor(() => {
155+
expect(canvas.getByRole('button', { name: /Industry/i })).toHaveTextContent(/\(1\)/);
156+
});
157+
// Other chips should reflect initialized values
158+
const contactRoleChip = canvas.getByRole('button', { name: /Contact Role/i });
159+
expect(contactRoleChip).toHaveTextContent(/\(3\)/);
160+
161+
const fileTypeChip = canvas.getByRole('button', { name: /Box Note/i });
162+
expect(fileTypeChip).toHaveTextContent(/\+2/);
163+
},
164+
};
165+
136166
const meta: Meta<typeof ContentExplorer> = {
137167
title: 'Elements/ContentExplorer/tests/MetadataView/visual',
138168
component: ContentExplorer,

yarn.lock

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,10 +1475,10 @@
14751475
resolved "https://registry.yarnpkg.com/@box/box-ai-content-answers/-/box-ai-content-answers-0.124.5.tgz#664a34d8338569be6801c1433295f858d8f054ad"
14761476
integrity sha512-0p1j2JW9ig0rhM4tiOyEFN6dbiHa7wuX8PmKTjPbgU5MQDibGSbNeZS7IxZlJLYs2BmJ+IncUrPN+zlnsfaRgw==
14771477

1478-
"@box/box-item-type-selector@^0.61.12":
1479-
version "0.61.16"
1480-
resolved "https://registry.yarnpkg.com/@box/box-item-type-selector/-/box-item-type-selector-0.61.16.tgz#476ba5d58e177f9967a064077273d0d690b887f0"
1481-
integrity sha512-0wiiqX4/x6K7lX7QfbQYxW3ELTV+9rYdRxdV2IZqE+3fzrGiSgucYtRLkyfeqsBRUFDQSfYRXHA+sE4ZdTb/fA==
1478+
"@box/box-item-type-selector@^0.63.12":
1479+
version "0.63.13"
1480+
resolved "https://registry.yarnpkg.com/@box/box-item-type-selector/-/box-item-type-selector-0.63.13.tgz#08999f0149f9222f8d7f00db20f7951c59b6da97"
1481+
integrity sha512-c3N2zTPfZqYhLgQOHEo5kE/TTrXi9BDEaPQrqb2ctsySdRCzkwLk6DfVhGFyXLQeIl0zZLhMXomSMKSxryG1cA==
14821482

14831483
"@box/cldr-data@^34.2.0":
14841484
version "34.8.0"
@@ -1502,10 +1502,10 @@
15021502
rimraf "^3.0.0"
15031503
semver "^7.4.0"
15041504

1505-
"@box/item-icon@^0.17.0":
1506-
version "0.17.0"
1507-
resolved "https://registry.yarnpkg.com/@box/item-icon/-/item-icon-0.17.0.tgz#2e92c62046ce0d35c6d510ae1f2d2a36b31d72ad"
1508-
integrity sha512-Pj4XUYMnMfjZ72ZPMxmbt//juidqArdbfyxxbXEI+yKH8hsO4jr6Z2RGAUmSepweWkxYHLm5gDzTEJwBC0csHQ==
1505+
"@box/item-icon@^0.17.15":
1506+
version "0.17.16"
1507+
resolved "https://registry.yarnpkg.com/@box/item-icon/-/item-icon-0.17.16.tgz#bd883e33a5f75e8153a9236edbf30674a1dc69c2"
1508+
integrity sha512-8uNEvBuXHdoRonmQUe5lCyyIkzFIY7Fhcb2WD9yhfgRU4wHS/rqWZ6JHicGUkpT7GPNssEwuWU44Fytqp8lHvw==
15091509

15101510
"@box/languages@^1.0.0":
15111511
version "1.1.2"
@@ -1517,15 +1517,15 @@
15171517
resolved "https://registry.yarnpkg.com/@box/metadata-editor/-/metadata-editor-0.122.16.tgz#d120602dcc39a6b4cd6f441ddd5fdc4b2f2862a4"
15181518
integrity sha512-fy43Z9fr6+t0hO+tlVDX4A890K0mos78GqHeZp9fGDH+lQsjHBHz3DDvcbyfmkCrf3Dbg15wrvBO0Pa4FaCSvw==
15191519

1520-
"@box/metadata-filter@^1.16.12":
1521-
version "1.18.0"
1522-
resolved "https://registry.yarnpkg.com/@box/metadata-filter/-/metadata-filter-1.18.0.tgz#c6e2a69f1ce9919063243fb451a97b4fc78ed019"
1523-
integrity sha512-us2njX6ade6iYeJekaerUv67d+yAob+o14ouYkfHzRFjLyfxDxB6ycTy/0jHwXLQYREXSZfIu6L+INEgbT2VAA==
1520+
"@box/metadata-filter@^1.19.2":
1521+
version "1.19.3"
1522+
resolved "https://registry.yarnpkg.com/@box/metadata-filter/-/metadata-filter-1.19.3.tgz#87364bea4cbb1417866e65639f3b1e137a6d9b6a"
1523+
integrity sha512-5cSY8yLW7S1zsiqBHAuKkHjcyHFBuBUBHGTnYigV0eKyLH4Dm9ozjon23P3Z9HXVB5IMHwTM3I9TRDFAZuP7vw==
15241524

1525-
"@box/metadata-view@^0.29.4":
1526-
version "0.29.4"
1527-
resolved "https://registry.yarnpkg.com/@box/metadata-view/-/metadata-view-0.29.4.tgz#03c42c0e32e9fc9895a5b56bc4a97daafeec8ca9"
1528-
integrity sha512-0CPQ7PE6uiW4hO3EOfpyLu1nD0u4UXwCMYsdHjQTHSeqgpmT8sx7ZlZ/7or+bwlekMqijdJ2CqbJhz3WsIlIfQ==
1525+
"@box/metadata-view@^0.41.2":
1526+
version "0.41.3"
1527+
resolved "https://registry.yarnpkg.com/@box/metadata-view/-/metadata-view-0.41.3.tgz#95a4d8322d02c13172fb0be681e74e17f8fe90dc"
1528+
integrity sha512-7ZqUrx4YmfmwXeDoPhSpLnL8xxVBkZ3Hlw4gpfpCw8IPHdT/nYTFml1GW7DO5d43jICf3foD08wwksW9IeB7/A==
15291529

15301530
"@box/react-virtualized@^9.22.3-rc-box.10":
15311531
version "9.22.3-rc-box.10"

0 commit comments

Comments
 (0)