Skip to content

Commit 6a64717

Browse files
authored
feat(metadata-editor): merge hydrated and detailed view results (#4503)
1 parent 3c3dba9 commit 6a64717

4 files changed

Lines changed: 253 additions & 10 deletions

File tree

src/api/Metadata.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
formatMetadataFieldValue,
2222
handleOnAbort,
2323
mapDetailedFieldToConfidenceScore,
24+
mergeDetailedAndHydratedInstances,
2425
parseTargetLocation,
2526
} from './utils';
2627
import File from './File';
@@ -391,6 +392,35 @@ class Metadata extends File {
391392
return response;
392393
}
393394
395+
/**
396+
* Fetches both detailed and hydrated metadata views and merges them so that
397+
* the result is in detailed format but with hydrated taxonomy values.
398+
*
399+
* @param {string} baseUrl - metadata API base URL
400+
* @param {string} requestId - typed file id
401+
* @return {Array} merged metadata instances
402+
*/
403+
async getDetailedInstancesWithHydratedTaxonomy(
404+
baseUrl: string,
405+
requestId: string,
406+
): Promise<Array<MetadataInstanceV2>> {
407+
try {
408+
const [detailedResponse, hydratedResponse] = await Promise.all([
409+
this.xhr.get({ url: `${baseUrl}?view=detailed`, id: requestId }),
410+
this.xhr.get({ url: `${baseUrl}?view=hydrated`, id: requestId }),
411+
]);
412+
const detailedEntries = getProp(detailedResponse, 'data.entries', []);
413+
const hydratedEntries = getProp(hydratedResponse, 'data.entries', []);
414+
return mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
415+
} catch (e) {
416+
const { status } = e;
417+
if (isUserCorrectableError(status)) {
418+
throw e;
419+
}
420+
}
421+
return [];
422+
}
423+
394424
/**
395425
* Gets metadata instances for a Box file
396426
*
@@ -407,14 +437,19 @@ class Metadata extends File {
407437
this.errorCode = ERROR_CODE_FETCH_METADATA;
408438
409439
const baseUrl = this.getMetadataUrl(id);
410-
const view = isConfidenceScoreEnabled ? 'detailed' : 'hydrated';
411-
const url = isMetadataRedesign ? `${baseUrl}?view=${view}` : baseUrl;
440+
const requestId = getTypedFileId(id);
441+
442+
if (isMetadataRedesign && isConfidenceScoreEnabled) {
443+
return this.getDetailedInstancesWithHydratedTaxonomy(baseUrl, requestId);
444+
}
445+
446+
const url = isMetadataRedesign ? `${baseUrl}?view=hydrated` : baseUrl;
412447
413448
let instances = {};
414449
try {
415450
instances = await this.xhr.get({
416451
url,
417-
id: getTypedFileId(id),
452+
id: requestId,
418453
});
419454
} catch (e) {
420455
const { status } = e;

src/api/__tests__/Metadata.test.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -713,18 +713,52 @@ describe('api/Metadata', () => {
713713
});
714714
});
715715

716-
test('should apply detailed view query string param when isMetadataRedesign and isConfidenceScoreEnabled are true', async () => {
716+
test('should make both detailed and hydrated calls when isMetadataRedesign and isConfidenceScoreEnabled are true', async () => {
717717
metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('metadata_url');
718-
metadata.xhr.get = jest.fn().mockReturnValueOnce({
719-
data: {
720-
entries: [],
721-
},
722-
});
723-
await metadata.getInstances('id', true, true);
718+
metadata.xhr.get = jest
719+
.fn()
720+
.mockResolvedValueOnce({
721+
data: {
722+
entries: [
723+
{
724+
$id: 'inst-1',
725+
region: {
726+
values: ['uuid-1'],
727+
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
728+
},
729+
},
730+
],
731+
},
732+
})
733+
.mockResolvedValueOnce({
734+
data: {
735+
entries: [
736+
{
737+
$id: 'inst-1',
738+
region: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
739+
},
740+
],
741+
},
742+
});
743+
const result = await metadata.getInstances('id', true, true);
744+
expect(metadata.xhr.get).toHaveBeenCalledTimes(2);
724745
expect(metadata.xhr.get).toHaveBeenCalledWith({
725746
url: 'metadata_url?view=detailed',
726747
id: 'file_id',
727748
});
749+
expect(metadata.xhr.get).toHaveBeenCalledWith({
750+
url: 'metadata_url?view=hydrated',
751+
id: 'file_id',
752+
});
753+
expect(result).toEqual([
754+
{
755+
$id: 'inst-1',
756+
region: {
757+
values: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
758+
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
759+
},
760+
},
761+
]);
728762
});
729763

730764
test('should not apply view query string param when isMetadataRedesign is false even if isConfidenceScoreEnabled is true', async () => {

src/api/__tests__/utils.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
handleOnAbort,
88
isDetailedFieldValue,
99
mapDetailedFieldToConfidenceScore,
10+
mergeDetailedAndHydratedInstances,
1011
parseTargetLocation,
1112
} from '../utils';
1213
import { threadedComments, threadedCommentsFormatted } from '../fixtures';
@@ -251,4 +252,142 @@ describe('api/utils', () => {
251252
expect(formatMetadataFieldValue(taxonomyField, [{ id, displayName }])).toEqual(expectedValue);
252253
});
253254
});
255+
256+
describe('mergeDetailedAndHydratedInstances()', () => {
257+
test('should replace detailed values with hydrated values for taxonomy fields', () => {
258+
const detailedEntries = [
259+
{
260+
$id: 'inst-1',
261+
$template: 'template1',
262+
$scope: 'enterprise',
263+
region: {
264+
values: ['uuid-1'],
265+
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
266+
},
267+
name: {
268+
values: 'Test Name',
269+
details: { updatedAt: 2000, updatedBy: 'user2', updatedAppId: 'app2' },
270+
},
271+
},
272+
];
273+
const hydratedEntries = [
274+
{
275+
$id: 'inst-1',
276+
$template: 'template1',
277+
$scope: 'enterprise',
278+
region: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
279+
name: 'Test Name',
280+
},
281+
];
282+
283+
const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
284+
285+
expect(result).toEqual([
286+
{
287+
$id: 'inst-1',
288+
$template: 'template1',
289+
$scope: 'enterprise',
290+
region: {
291+
values: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
292+
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
293+
},
294+
name: {
295+
values: 'Test Name',
296+
details: { updatedAt: 2000, updatedBy: 'user2', updatedAppId: 'app2' },
297+
},
298+
},
299+
]);
300+
});
301+
302+
test('should return detailed entries unchanged when no matching hydrated entry exists', () => {
303+
const detailedEntries = [
304+
{
305+
$id: 'inst-1',
306+
region: { values: ['uuid-1'], details: {} },
307+
},
308+
];
309+
const hydratedEntries = [];
310+
311+
const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
312+
313+
expect(result).toEqual(detailedEntries);
314+
});
315+
316+
test('should not modify $-prefixed system fields', () => {
317+
const detailedEntries = [
318+
{
319+
$id: 'inst-1',
320+
$scope: 'enterprise',
321+
$template: 'tmpl',
322+
field1: { values: 'val1', details: {} },
323+
},
324+
];
325+
const hydratedEntries = [
326+
{
327+
$id: 'inst-1',
328+
$scope: 'enterprise',
329+
$template: 'tmpl',
330+
field1: 'val1',
331+
},
332+
];
333+
334+
const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
335+
336+
expect(result[0].$id).toBe('inst-1');
337+
expect(result[0].$scope).toBe('enterprise');
338+
expect(result[0].$template).toBe('tmpl');
339+
});
340+
341+
test('should handle multiple instances', () => {
342+
const detailedEntries = [
343+
{
344+
$id: 'inst-1',
345+
country: { values: ['id-1'], details: { updatedAt: 100 } },
346+
},
347+
{
348+
$id: 'inst-2',
349+
city: { values: ['id-2'], details: { updatedAt: 200 } },
350+
},
351+
];
352+
const hydratedEntries = [
353+
{
354+
$id: 'inst-2',
355+
city: [{ id: 'id-2', displayName: 'Tokyo' }],
356+
},
357+
{
358+
$id: 'inst-1',
359+
country: [{ id: 'id-1', displayName: 'Japan' }],
360+
},
361+
];
362+
363+
const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
364+
365+
expect(result[0].country.values).toEqual([{ id: 'id-1', displayName: 'Japan' }]);
366+
expect(result[0].country.details).toEqual({ updatedAt: 100 });
367+
expect(result[1].city.values).toEqual([{ id: 'id-2', displayName: 'Tokyo' }]);
368+
expect(result[1].city.details).toEqual({ updatedAt: 200 });
369+
});
370+
371+
test('should skip non-detailed fields during merge', () => {
372+
const detailedEntries = [
373+
{
374+
$id: 'inst-1',
375+
plainField: 'just a string',
376+
detailedField: { values: 'val', details: {} },
377+
},
378+
];
379+
const hydratedEntries = [
380+
{
381+
$id: 'inst-1',
382+
plainField: 'just a string',
383+
detailedField: 'val',
384+
},
385+
];
386+
387+
const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
388+
389+
expect(result[0].plainField).toBe('just a string');
390+
expect(result[0].detailedField).toEqual({ values: 'val', details: {} });
391+
});
392+
});
254393
});

src/api/utils.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
MetadataConfidenceScoreData,
1212
MetadataDetailedFieldValue,
1313
MetadataFieldValue,
14+
MetadataInstanceV2,
1415
MetadataTargetLocationEntry,
1516
MetadataTemplateField,
1617
} from '../common/types/metadata';
@@ -94,6 +95,39 @@ const parseTargetLocation = (fieldValue: any): ?Array<MetadataTargetLocationEntr
9495
}
9596
};
9697

98+
const mergeDetailedAndHydratedInstances = (
99+
detailedEntries: Array<MetadataInstanceV2>,
100+
hydratedEntries: Array<MetadataInstanceV2>,
101+
): Array<MetadataInstanceV2> => {
102+
const hydratedById: { [string]: MetadataInstanceV2 } = {};
103+
hydratedEntries.forEach(entry => {
104+
hydratedById[entry.$id] = entry;
105+
});
106+
107+
return detailedEntries.map(detailedEntry => {
108+
const hydratedEntry = hydratedById[detailedEntry.$id];
109+
if (!hydratedEntry) {
110+
return detailedEntry;
111+
}
112+
113+
const merged = { ...detailedEntry };
114+
Object.keys(merged).forEach(key => {
115+
if (key.startsWith('$')) {
116+
return;
117+
}
118+
119+
if (isDetailedFieldValue(merged[key]) && key in hydratedEntry) {
120+
merged[key] = {
121+
...merged[key],
122+
values: hydratedEntry[key],
123+
};
124+
}
125+
});
126+
127+
return merged;
128+
});
129+
};
130+
97131
const handleOnAbort = (xhr: Xhr) => {
98132
xhr.abort();
99133

@@ -107,5 +141,6 @@ export {
107141
handleOnAbort,
108142
isDetailedFieldValue,
109143
mapDetailedFieldToConfidenceScore,
144+
mergeDetailedAndHydratedInstances,
110145
parseTargetLocation,
111146
};

0 commit comments

Comments
 (0)