Skip to content

Commit faef59e

Browse files
authored
feat(features): Add new feature metadata-based-view (#1519)
1 parent 7b341f7 commit faef59e

File tree

15 files changed

+582
-59
lines changed

15 files changed

+582
-59
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
/src/features/item-details/ @box/ui-elements
99
/src/features/access-stats/ @box/ui-elements
1010
/src/features/metadata-instance-editor/ @box/ui-elements
11+
/src/features/metadata-based-view/ @box/ui-elements
1112
/src/icons/ @box/ui-elements-extended
1213
/src/styles/ @box/ui-elements-extended
1314
/src/utils/ @box/ui-elements-extended

i18n/en-US.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ be.loadingState = Please wait while the items load...
300300
be.logo = Logo
301301
# Indicator on the footer that max items have been selected.
302302
be.max = max
303+
# Message shown when there are no items for provided metadata query.
304+
be.metadataState = There are no items in this folder.
303305
# Text for modified date with modified prefix.
304306
be.modifiedDate = Modified {date}
305307
# Text for modified date with user with modified prefix.

src/api/MetadataQuery.js

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@
44
* @author Box
55
*/
66

7+
import getProp from 'lodash/get';
8+
import omit from 'lodash/omit';
79
import Base from './Base';
8-
import type { MetadataQuery as MetadataQueryType, MetadataQueryResponse } from '../common/types/metadataQueries';
910
import { CACHE_PREFIX_METADATA_QUERY, ERROR_CODE_METADATA_QUERY } from '../constants';
11+
import { ITEM_TYPE_FILE } from '../common/constants';
12+
import type {
13+
MetadataQuery as MetadataQueryType,
14+
FlattenedMetadataQueryResponse,
15+
FlattenedMetadataQueryResponseEntry,
16+
FlattenedMetadataQueryResponseEntryMetadata,
17+
MetadataQueryResponse,
18+
MetadataQueryResponseEntry,
19+
MetadataQueryResponseEntryMetadata,
20+
} from '../common/types/metadataQueries';
1021

1122
class MetadataQuery extends Base {
1223
/**
@@ -67,12 +78,90 @@ class MetadataQuery extends Base {
6778
this.successCallback(metadataQueryData);
6879
}
6980

81+
/**
82+
* Returns the response object with entries of type 'file' only.
83+
*
84+
* @param {Object} response
85+
* @return {Object}
86+
*/
87+
filterMetdataQueryResponse = (response: MetadataQueryResponse): MetadataQueryResponse => {
88+
const { entries = [], next_marker } = response;
89+
return {
90+
entries: entries.filter(entry => getProp(entry, 'item.type') === ITEM_TYPE_FILE), // return only file items
91+
next_marker,
92+
};
93+
};
94+
95+
/**
96+
* Extracts flattened metadata from the metadata response object
97+
* @param {Object} - metadata from the query response entry
98+
* @return {Object} - flattened metadata entry without the $ fields
99+
*/
100+
flattenMetadata = (metadata: MetadataQueryResponseEntryMetadata): FlattenedMetadataQueryResponseEntryMetadata => {
101+
let flattenedMetadata = {};
102+
103+
Object.keys(metadata).forEach(scope => {
104+
Object.keys(metadata[scope]).forEach(templateKey => {
105+
const nonconformingInstance = metadata[scope][templateKey];
106+
const data = omit(nonconformingInstance, [
107+
'$id',
108+
'$parent',
109+
'$type',
110+
'$typeScope',
111+
'$typeVersion',
112+
'$version',
113+
]);
114+
115+
flattenedMetadata = {
116+
data,
117+
id: nonconformingInstance.$id,
118+
metadataTemplate: {
119+
type: 'metadata-template',
120+
templateKey,
121+
},
122+
};
123+
});
124+
});
125+
126+
return flattenedMetadata;
127+
};
128+
129+
/**
130+
* Converts metadata query response entry to a flattened one
131+
* @param {Object} - metadata query response entry
132+
* @return {Object} - flattened metadata query response entry
133+
*/
134+
flattenResponseEntry = ({ item, metadata }: MetadataQueryResponseEntry): FlattenedMetadataQueryResponseEntry => {
135+
const { id, name, size } = item;
136+
137+
return {
138+
id,
139+
metadata: this.flattenMetadata(metadata),
140+
name,
141+
size,
142+
};
143+
};
144+
145+
/**
146+
* Flattens metadata query response
147+
* @param {Object} - metadata query response object
148+
* @return {Object} - flattened metadata query response object
149+
*/
150+
flattenMetdataQueryResponse = ({ entries, next_marker }: MetadataQueryResponse): FlattenedMetadataQueryResponse => {
151+
return {
152+
items: entries.map<FlattenedMetadataQueryResponseEntry>(this.flattenResponseEntry),
153+
nextMarker: next_marker,
154+
};
155+
};
156+
70157
/**
71158
* @param {Object} response
72159
*/
73160
queryMetadataSuccessHandler = ({ data }: { data: MetadataQueryResponse }): void => {
74161
const cache: APICache = this.getCache();
75-
cache.set(this.key, data);
162+
const filteredResponse = this.filterMetdataQueryResponse(data);
163+
// Flatten the filtered metadata query response and set it in cache
164+
cache.set(this.key, this.flattenMetdataQueryResponse(filteredResponse));
76165
this.finish();
77166
};
78167

src/api/__tests__/MetadataQuery-test.js

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,104 @@ import { CACHE_PREFIX_METADATA_QUERY, ERROR_CODE_METADATA_QUERY } from '../../co
55

66
let metadataQuery;
77
let cache;
8-
const successResponse = { entries: [], next_marker: 'abc123' };
8+
const marker = 'marker_123456789';
9+
const templateKey = 'awesomeTemplateKey';
10+
const templateType = 'metadata-template';
11+
const metadataInstanceId1 = 'c614dcaa-ebdc-4c88-b242-15cad4f7b787';
12+
const metadataInstanceId2 = 'ee348ed1-9460-44f3-9c34-aa580a93efda';
13+
14+
const mockMetadataQuerySuccessResponse = {
15+
entries: [
16+
{
17+
item: {
18+
type: 'file',
19+
id: '1234',
20+
name: 'filename1.pdf',
21+
size: 10000,
22+
},
23+
metadata: {
24+
enterprise_2222: {
25+
awesomeTemplateKey: {
26+
$id: metadataInstanceId1,
27+
$parent: 'file_998877',
28+
$type: 'awesomeTemplateKey-asdlk-1234-asd1',
29+
$typeScope: 'enterprise_2222',
30+
$typeVersion: 0,
31+
$version: 0,
32+
type: 'bill', // metadata template field
33+
amount: 500, // metadata template field
34+
approved: 'yes', // metadata template field
35+
},
36+
},
37+
},
38+
},
39+
{
40+
item: {
41+
type: 'file',
42+
id: '9876',
43+
name: 'filename2.mp4',
44+
size: 389027,
45+
},
46+
metadata: {
47+
enterprise_2222: {
48+
awesomeTemplateKey: {
49+
$id: metadataInstanceId2,
50+
$parent: 'file_998877',
51+
$type: 'awesomeTemplateKey-asdlk-1234-asd1',
52+
$typeScope: 'enterprise_2222',
53+
$typeVersion: 0,
54+
$version: 0,
55+
type: 'receipt', // metadata template field
56+
amount: 2735, // metadata template field
57+
approved: 'no', // metadata template field
58+
},
59+
},
60+
},
61+
},
62+
],
63+
next_marker: marker,
64+
};
65+
66+
const flattenedMockMetadataQuerySuccessResponse = {
67+
items: [
68+
{
69+
id: '1234',
70+
metadata: {
71+
data: {
72+
type: 'bill',
73+
amount: 500,
74+
approved: 'yes',
75+
},
76+
id: metadataInstanceId1,
77+
metadataTemplate: {
78+
type: templateType,
79+
templateKey,
80+
},
81+
},
82+
name: 'filename1.pdf',
83+
size: 10000,
84+
},
85+
{
86+
id: '9876',
87+
metadata: {
88+
data: {
89+
type: 'receipt',
90+
amount: 2735,
91+
approved: 'no',
92+
},
93+
id: metadataInstanceId2,
94+
metadataTemplate: {
95+
type: templateType,
96+
templateKey,
97+
},
98+
},
99+
name: 'filename2.mp4',
100+
size: 389027,
101+
},
102+
],
103+
nextMarker: marker,
104+
};
105+
9106
const url = 'https://api.box.com/2.0/metadata_queries/execute';
10107
const mockQuery = {
11108
query: 'enteprise_1234.tempalteKey.type = :arg1',
@@ -51,15 +148,15 @@ describe('api/MetadataQuery', () => {
51148

52149
test('should return true when loaded', () => {
53150
metadataQuery.key = 'key';
54-
cache.set('key', successResponse);
151+
cache.set('key', mockMetadataQuerySuccessResponse);
55152
expect(metadataQuery.isLoaded()).toBe(true);
56153
});
57154
});
58155

59156
describe('finish()', () => {
60157
beforeEach(() => {
61158
metadataQuery.key = `${CACHE_PREFIX_METADATA_QUERY}_foo`;
62-
cache.set(metadataQuery.key, successResponse);
159+
cache.set(metadataQuery.key, mockMetadataQuerySuccessResponse);
63160
});
64161

65162
test('should not do anything if destroyed', () => {
@@ -74,7 +171,36 @@ describe('api/MetadataQuery', () => {
74171
test('should call success callback with proper collection', () => {
75172
metadataQuery.successCallback = jest.fn();
76173
metadataQuery.finish();
77-
expect(metadataQuery.successCallback).toHaveBeenCalledWith(successResponse);
174+
expect(metadataQuery.successCallback).toHaveBeenCalledWith(mockMetadataQuerySuccessResponse);
175+
});
176+
});
177+
178+
describe('filterMetdataQueryResponse()', () => {
179+
test('should return query response with entries of type file only', () => {
180+
const entries = [
181+
{ item: { type: 'file' }, metadata: {} },
182+
{ item: { type: 'folder' }, metadata: {} },
183+
{ item: { type: 'file' }, metadata: {} },
184+
{ item: { type: 'folder' }, metadata: {} },
185+
{ item: { type: 'file' }, metadata: {} },
186+
];
187+
const next_marker = 'marker_123456789';
188+
const metadataQueryResponse = {
189+
entries,
190+
next_marker,
191+
};
192+
193+
const filteredResponse = metadataQuery.filterMetdataQueryResponse(metadataQueryResponse);
194+
const isEveryEntryOfTypeFile = filteredResponse.entries.every(entry => entry.item.type === 'file');
195+
expect(isEveryEntryOfTypeFile).toBe(true);
196+
});
197+
});
198+
199+
describe('flattenMetdataQueryResponse()', () => {
200+
test('should flatten the metadata query api response successfully', () => {
201+
expect(metadataQuery.flattenMetdataQueryResponse(mockMetadataQuerySuccessResponse)).toEqual(
202+
flattenedMockMetadataQuerySuccessResponse,
203+
);
78204
});
79205
});
80206

@@ -84,9 +210,10 @@ describe('api/MetadataQuery', () => {
84210
metadataQuery.finish = jest.fn();
85211

86212
metadataQuery.queryMetadataSuccessHandler({
87-
data: successResponse,
213+
data: mockMetadataQuerySuccessResponse,
88214
});
89-
expect(cache.set).toHaveBeenCalledWith(metadataQuery.key, successResponse);
215+
216+
expect(cache.set).toHaveBeenCalledWith(metadataQuery.key, flattenedMockMetadataQuerySuccessResponse);
90217
expect(metadataQuery.finish).toHaveBeenCalled();
91218
});
92219
});
@@ -103,7 +230,7 @@ describe('api/MetadataQuery', () => {
103230
});
104231

105232
test('should make xhr call to metadata_queries/execute endpoint and call success callback', async () => {
106-
const mockAPIResponse = { data: successResponse };
233+
const mockAPIResponse = { data: mockMetadataQuerySuccessResponse };
107234

108235
metadataQuery.isDestroyed = jest.fn().mockReturnValueOnce(false);
109236
metadataQuery.xhr = {

src/common/types/metadataQueries.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// @flow
2-
type MetadataQeuryResponseEntryEnterprise = {
2+
type MetadataQueryResponseEntryEnterprise = {
33
[string]: MetadataInstanceV2,
44
};
55

6-
type MetadataQeuryResponseEntryMetadata = {
7-
[string]: MetadataQeuryResponseEntryEnterprise,
6+
type MetadataQueryResponseEntryMetadata = {
7+
[string]: MetadataQueryResponseEntryEnterprise,
88
};
99

10-
type MetadataQeuryResponseEntry = {
10+
type MetadataQueryResponseEntry = {
1111
item: BoxItem,
12-
metadata: MetadataQeuryResponseEntryMetadata,
12+
metadata: MetadataQueryResponseEntryMetadata,
1313
};
1414

1515
type MetadataQueryResponse = {
16-
entries: Array<MetadataQeuryResponseEntry>,
16+
entries: Array<MetadataQueryResponseEntry>,
1717
next_marker?: string,
1818
};
1919

@@ -31,11 +31,45 @@ type MetadataQuery = {
3131
query_params: Object,
3232
};
3333

34+
type MetadataColumnsToShow = Array<string>;
35+
36+
type FlattenedMetadataQueryResponseEntryMetadata = {
37+
data?: StringAnyMap,
38+
id?: string,
39+
metadataTemplate?: {
40+
templateKey: string,
41+
type: string,
42+
},
43+
};
44+
45+
type FlattenedMetadataQueryResponseEntry = {
46+
id: string,
47+
metadata: FlattenedMetadataQueryResponseEntryMetadata,
48+
name?: string,
49+
size?: number,
50+
};
51+
52+
type FlattenedMetadataQueryResponse = {
53+
items: Array<FlattenedMetadataQueryResponseEntry>,
54+
nextMarker?: string,
55+
};
56+
57+
type FlattenedMetadataQueryResponseCollection = {
58+
items: Array<FlattenedMetadataQueryResponseEntry>,
59+
nextMarker: string,
60+
percentLoaded: Number,
61+
};
62+
3463
export type {
64+
FlattenedMetadataQueryResponse,
65+
FlattenedMetadataQueryResponseCollection,
66+
FlattenedMetadataQueryResponseEntry,
67+
FlattenedMetadataQueryResponseEntryMetadata,
68+
MetadataColumnsToShow,
3569
MetadataQuery,
3670
MetadataQueryOrderByClause,
3771
MetadataQueryResponse,
38-
MetadataQeuryResponseEntry,
39-
MetadataQeuryResponseEntryMetadata,
40-
MetadataQeuryResponseEntryEnterprise,
72+
MetadataQueryResponseEntry,
73+
MetadataQueryResponseEntryEnterprise,
74+
MetadataQueryResponseEntryMetadata,
4175
};

0 commit comments

Comments
 (0)