Skip to content
Merged
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
28 changes: 26 additions & 2 deletions src/api/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ class Metadata extends File {
return `${this.getMetadataCacheKey(id)}_classification`;
}

/**
* Creates a key for the metadata template schema cache
*
* @param {string} templateKey - template key
* @return {string} key
*/
getMetadataTemplateSchemaCacheKey(templateKey: string): string {
return `${CACHE_PREFIX_METADATA}template_schema_${templateKey}`;
}

/**
* API URL for metadata
*
Expand Down Expand Up @@ -337,9 +347,23 @@ class Metadata extends File {
* @param {string} templateKey - template key
* @return {Promise} Promise object of metadata template
*/
getSchemaByTemplateKey(templateKey: string): Promise<MetadataTemplateSchemaResponse> {
async getSchemaByTemplateKey(templateKey: string): Promise<MetadataTemplateSchemaResponse> {
const cache: APICache = this.getCache();
const key = this.getMetadataTemplateSchemaCacheKey(templateKey);

// Return cached value if it exists
if (cache.has(key)) {
return cache.get(key);
}

// Fetch from API if not cached
const url = this.getMetadataTemplateSchemaUrl(templateKey);
return this.xhr.get({ url });
const response = await this.xhr.get({ url });

// Cache the response
cache.set(key, response);

return response;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/elements/content-explorer/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const Content = ({
isLoading={percentLoaded !== 100}
hasError={view === VIEW_ERROR}
metadataTemplate={metadataTemplate}
onSortChange={onSortChange}
{...metadataViewProps}
/>
)}
Expand Down
6 changes: 3 additions & 3 deletions src/elements/content-explorer/ContentExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import throttle from 'lodash/throttle';
import uniqueid from 'lodash/uniqueId';
import { TooltipProvider } from '@box/blueprint-web';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { Selection } from 'react-aria-components';
import type { Key, Selection } from 'react-aria-components';

import CreateFolderDialog from '../common/create-folder-dialog';
import UploadDialog from '../common/upload-dialog';
Expand Down Expand Up @@ -153,7 +153,7 @@ export interface ContentExplorerProps {
rootFolderId?: string;
sharedLink?: string;
sharedLinkPassword?: string;
sortBy?: SortBy;
sortBy?: SortBy | Key;
sortDirection?: SortDirection;
staticHost?: string;
staticPath?: string;
Expand Down Expand Up @@ -896,7 +896,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
* @param {string} sortDirection - sort direction
* @return {void}
*/
sort = (sortBy: SortBy, sortDirection: SortDirection) => {
sort = (sortBy: SortBy | Key, sortDirection: SortDirection) => {
const {
currentCollection: { id },
view,
Expand Down
62 changes: 61 additions & 1 deletion src/elements/content-explorer/MetadataViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from 'react';
import type { EnumType, FloatType, MetadataFormFieldValue, RangeType } from '@box/metadata-filter';
import { MetadataView, type MetadataViewProps } from '@box/metadata-view';
import { type Key } from '@react-types/shared';

import { SortDescriptor } from 'react-aria-components';
import type { Collection } from '../../common/types/core';
import type { MetadataTemplate } from '../../common/types/metadata';

Expand All @@ -25,6 +27,21 @@ type ActionBarProps = Omit<
onFilterSubmit?: (filterValues: ExternalFilterValues) => void;
};

/**
* Helper function to trim metadataFieldNamePrefix from column names
* For example: 'metadata.enterprise_1515946.mdViewTemplate1.industry' -> 'industry'
*/
function trimMetadataFieldPrefix(column: string): string {
// Check if the column starts with 'metadata.' and contains at least 2 dots
if (column.startsWith('metadata.') && column.split('.').length >= 3) {
// Split by dots and take everything after the first 3 parts
// metadata.enterprise_1515946.mdViewTemplate1.industry -> industry
const parts = column.split('.');
return parts.slice(3).join('.');
}
return column;
}

function transformInitialFilterValuesToInternal(
publicValues?: ExternalFilterValues,
): Record<string, { value: MetadataFormFieldValue }> | undefined {
Expand Down Expand Up @@ -55,13 +72,16 @@ export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'ite
actionBarProps?: ActionBarProps;
currentCollection: Collection;
metadataTemplate: MetadataTemplate;
/* Internally controlled onSortChange prop for the MetadataView component. */
onSortChange?: (sortBy: Key, sortDirection: string) => void;
}

const MetadataViewContainer = ({
actionBarProps,
columns,
currentCollection,
metadataTemplate,
onSortChange: onSortChangeInternal,
...rest
}: MetadataViewContainerProps) => {
const { items = [] } = currentCollection;
Expand Down Expand Up @@ -111,7 +131,47 @@ const MetadataViewContainer = ({
};
}, [actionBarProps, initialFilterValues, onFilterSubmit, filterGroups]);

return <MetadataView actionBarProps={transformedActionBarProps} columns={columns} items={items} {...rest} />;
// Extract the original tableProps.onSortChange from rest
const { tableProps, ...otherRest } = rest;
const onSortChangeExternal = tableProps?.onSortChange;

// Create a wrapper function that calls both. The wrapper function should follow the signature of onSortChange from RAC
const handleSortChange = React.useCallback(
({ column, direction }: SortDescriptor) => {
// Call the internal onSortChange first
// API accepts asc/desc "https://developer.box.com/reference/post-metadata-queries-execute-read/"
if (onSortChangeInternal) {
const trimmedColumn = trimMetadataFieldPrefix(String(column));
onSortChangeInternal(trimmedColumn, direction === 'ascending' ? 'ASC' : 'DESC');
}

// Then call the original customer-provided onSortChange if it exists
// Accepts "ascending" / "descending" (https://react-spectrum.adobe.com/react-aria/Table.html)
if (onSortChangeExternal) {
onSortChangeExternal({
column,
direction,
});
}
},
[onSortChangeInternal, onSortChangeExternal],
);

// Create new tableProps with our wrapper function
const newTableProps = {
...tableProps,
onSortChange: handleSortChange,
};

return (
<MetadataView
actionBarProps={transformedActionBarProps}
columns={columns}
items={items}
tableProps={newTableProps}
{...otherRest}
/>
);
};

export default MetadataViewContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -454,15 +454,15 @@ describe('elements/content-explorer/ContentExplorer', () => {
textValue: 'Name',
id: 'name',
type: 'string' as const,
allowSorting: true,
allowsSorting: true,
minWidth: 150,
maxWidth: 150,
},
...mockSchema.fields.map(field => ({
textValue: field.displayName,
id: `${metadataFieldNamePrefix}.${field.key}`,
type: field.type as MetadataFieldType,
allowSorting: true,
allowsSorting: true,
minWidth: 150,
maxWidth: 150,
})),
Expand Down Expand Up @@ -506,6 +506,40 @@ describe('elements/content-explorer/ContentExplorer', () => {
expect(screen.getByRole('button', { name: 'Metadata' })).toBeInTheDocument();
});

test('should call both internal and user onSortChange callbacks when sorting by a metadata field', async () => {
const mockOnSortChangeInternal = jest.fn();
const mockOnSortChangeExternal = jest.fn();

renderComponent({
...metadataViewV2ElementProps,
metadataViewProps: {
...metadataViewV2ElementProps.metadataViewProps,
onSortChange: mockOnSortChangeInternal, // Internal callback - receives trimmed column name
tableProps: {
...metadataViewV2ElementProps.metadataViewProps.tableProps,
onSortChange: mockOnSortChangeExternal, // User callback - receives full column ID
},
},
});

const industryHeader = await screen.findByRole('columnheader', { name: 'Industry' });
expect(industryHeader).toBeInTheDocument();

const firstRow = await screen.findByRole('row', { name: /Child 2/i });
expect(firstRow).toBeInTheDocument();

await userEvent.click(industryHeader);

// Internal callback gets trimmed version for API calls
expect(mockOnSortChangeInternal).toHaveBeenCalledWith('industry', 'ASC');

// User callback gets full column ID with direction
expect(mockOnSortChangeExternal).toHaveBeenCalledWith({
column: 'metadata.enterprise_0.templateName.industry',
direction: 'ascending',
});
});

test('should call onClick when bulk item action is clicked', async () => {
let mockOnClickArg;
const mockOnClick = jest.fn(arg => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { http, HttpResponse } from 'msw';
import { Download, SignMeOthers } from '@box/blueprint-web-assets/icons/Fill/index';
import { Sign } from '@box/blueprint-web-assets/icons/Line';
import { expect, fn, userEvent, waitFor, within, screen } from 'storybook/test';

import noop from 'lodash/noop';
import orderBy from 'lodash/orderBy';

import ContentExplorer from '../../ContentExplorer';
import { DEFAULT_HOSTNAME_API } from '../../../../constants';
Expand Down Expand Up @@ -138,17 +140,16 @@ export const metadataViewV2: Story = {
args: metadataViewV2ElementProps,
};

// @TODO Assert that rows are actually sorted in a different order, once handleSortChange is implemented
export const metadataViewV2SortsFromHeader: Story = {
args: metadataViewV2ElementProps,
play: async ({ canvas }) => {
await waitFor(() => {
expect(canvas.getByRole('row', { name: /Industry/i })).toBeInTheDocument();
});
const industryHeader = await canvas.findByRole('columnheader', { name: 'Industry' });
expect(industryHeader).toBeInTheDocument();

const firstRow = canvas.getByRole('row', { name: /Industry/i });
const industryHeader = within(firstRow).getByRole('columnheader', { name: 'Industry' });
userEvent.click(industryHeader);
const firstRow = await canvas.findByRole('row', { name: /Child 2/i });
expect(firstRow).toBeInTheDocument();

await userEvent.click(industryHeader);
},
};

Expand Down Expand Up @@ -248,7 +249,21 @@ const meta: Meta<typeof ContentExplorer> = {
parameters: {
msw: {
handlers: [
http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, () => {
// Note that the Metadata API backend normally handles the sorting. The mocks below simulate the sorting for specific cases, but may not 100% accurately reflect the backend behavior.
http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, async ({ request }) => {
const body = await request.clone().json();
const orderByDirection = body.order_by[0].direction;
const orderByFieldKey = body.order_by[0].field_key;

// Hardcoded case for sorting by industry
if (orderByFieldKey === `industry` && orderByDirection === 'ASC') {
const sortedMetadata = orderBy(
mockMetadata.entries,
'metadata.enterprise_0.templateName.industry',
'asc',
);
return HttpResponse.json({ ...mockMetadata, entries: sortedMetadata });
}
return HttpResponse.json(mockMetadata);
}),
http.get(`${DEFAULT_HOSTNAME_API}/2.0/metadata_templates/enterprise/templateName/schema`, () => {
Expand Down