diff --git a/src/elements/content-explorer/ContentExplorer.tsx b/src/elements/content-explorer/ContentExplorer.tsx index 6b6b6941c7..df4c1ca762 100644 --- a/src/elements/content-explorer/ContentExplorer.tsx +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -34,7 +34,7 @@ import Content from './Content'; import { isThumbnailAvailable } from '../common/utils'; import { isFocusableElement, isInputElement, focus } from '../../utils/dom'; import { FILE_SHARED_LINK_FIELDS_TO_FETCH } from '../../utils/fields'; -import CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH from './constants'; +import { CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH } from './constants'; import LocalStore from '../../utils/LocalStore'; import { withFeatureConsumer, diff --git a/src/elements/content-explorer/MetadataQueryBuilder.ts b/src/elements/content-explorer/MetadataQueryBuilder.ts index 7b1ff4ae6b..0f80cc62db 100644 --- a/src/elements/content-explorer/MetadataQueryBuilder.ts +++ b/src/elements/content-explorer/MetadataQueryBuilder.ts @@ -1,4 +1,6 @@ +import { BoxItemSelection } from '@box/box-item-type-selector'; import isNil from 'lodash/isNil'; +import { mapFileTypes } from './utils'; type QueryResult = { queryParams: { [key: string]: number | Date | string }; @@ -115,9 +117,7 @@ export const getSelectFilter = (filterValue: string[], fieldKey: string, argInde return { queryParams: multiSelectQueryParams, queries: [ - `(${fieldKey === 'mimetype-filter' ? 'item.extension' : fieldKey} HASANY (${Object.keys( - multiSelectQueryParams, - ) + `(${fieldKey} HASANY (${Object.keys(multiSelectQueryParams) .map(argKey => `:${argKey}`) .join(', ')}))`, ], @@ -134,26 +134,61 @@ export const getMimeTypeFilter = (filterValue: string[], fieldKey: string, argIn }; } - let currentArgIndex = argIndexStart; + // Use mapFileTypes to get the correct extensions and handle special cases + const mappedExtensions = mapFileTypes(filterValue as BoxItemSelection); + if (mappedExtensions.length === 0) { + return { + queryParams: {}, + queries: [], + keysGenerated: 0, + }; + } - const multiSelectQueryParams = Object.fromEntries( - filterValue.map(value => { - currentArgIndex += 1; - // the item-type-selector is returning the extensions with the suffix 'Type', so we remove it for the query - return [ - generateArgKey(fieldKey, currentArgIndex), - String(value.endsWith('Type') ? value.slice(0, -4) : value), - ]; - }), - ); + let currentArgIndex = argIndexStart; + const queryParams: { [key: string]: number | Date | string } = {}; + const queries: string[] = []; + + // Handle specific extensions and folder type + const extensions: string[] = []; + let hasFolder = false; + + for (const extension of mappedExtensions) { + if (extension === 'folder') { + if (!hasFolder) { + currentArgIndex += 1; + const folderArgKey = generateArgKey('mime_folderType', currentArgIndex); + queryParams[folderArgKey] = 'folder'; + queries.push(`(item.type = :${folderArgKey})`); + hasFolder = true; + } + } else { + extensions.push(extension); + } + } - return { - queryParams: multiSelectQueryParams, - queries: [ - `(item.extension IN (${Object.keys(multiSelectQueryParams) + // Handle extensions in batch if any exist + if (extensions.length > 0) { + const extensionQueryParams = Object.fromEntries( + extensions.map(extension => { + currentArgIndex += 1; + return [generateArgKey(fieldKey, currentArgIndex), extension]; + }), + ); + + Object.assign(queryParams, extensionQueryParams); + queries.push( + `(item.extension IN (${Object.keys(extensionQueryParams) .map(argKey => `:${argKey}`) .join(', ')}))`, - ], + ); + } + + // Combine queries with OR if multiple exist + const finalQueries = queries.length > 1 ? [`(${queries.join(' OR ')})`] : queries; + + return { + queryParams, + queries: finalQueries, keysGenerated: currentArgIndex - argIndexStart, }; }; diff --git a/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts b/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts index 9dc5733d98..773c7e596c 100644 --- a/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +++ b/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts @@ -673,14 +673,20 @@ describe('elements/content-explorer/MetadataQueryAPIHelper', () => { const filters = { 'mimetype-filter': { fieldType: 'enum', - value: ['pdf', 'doc'], + value: ['pdfType', 'documentType'], }, }; const result = metadataQueryAPIHelper.buildMetadataQueryParams(filters); - expect(result.query).toBe('(item.extension IN (:arg_mimetype_filter_1, :arg_mimetype_filter_2))'); + expect(result.query).toBe( + '(item.extension IN (:arg_mimetype_filter_1, :arg_mimetype_filter_2, :arg_mimetype_filter_3, :arg_mimetype_filter_4, :arg_mimetype_filter_5, :arg_mimetype_filter_6))', + ); expect(result.queryParams.arg_mimetype_filter_1).toBe('pdf'); expect(result.queryParams.arg_mimetype_filter_2).toBe('doc'); + expect(result.queryParams.arg_mimetype_filter_3).toBe('docx'); + expect(result.queryParams.arg_mimetype_filter_4).toBe('gdoc'); + expect(result.queryParams.arg_mimetype_filter_5).toBe('rtf'); + expect(result.queryParams.arg_mimetype_filter_6).toBe('txt'); }); test('should handle multiple filters of different types', () => { diff --git a/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts b/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts index 4a70ee1117..9707f42be8 100644 --- a/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts +++ b/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts @@ -228,19 +228,6 @@ describe('elements/content-explorer/MetadataQueryBuilder', () => { }); }); - test('should handle mimetype-filter field key specially', () => { - const filterValue = ['pdf', 'doc']; - const result = getSelectFilter(filterValue, 'mimetype-filter', 0); - expect(result).toEqual({ - queryParams: { - arg_mimetype_filter_1: 'pdf', - arg_mimetype_filter_2: 'doc', - }, - queries: ['(item.extension HASANY (:arg_mimetype_filter_1, :arg_mimetype_filter_2))'], - keysGenerated: 2, - }); - }); - test('should handle single value array', () => { const filterValue = ['single_value']; const result = getSelectFilter(filterValue, 'field_name', 0); @@ -309,44 +296,51 @@ describe('elements/content-explorer/MetadataQueryBuilder', () => { }); describe('getMimeTypeFilter', () => { - test('should generate mime type filter and remove "Type" suffix', () => { - const filterValue = ['pdfType', 'docType', 'txtType']; + test('should generate mime type filter using mapFileTypes', () => { + const filterValue = ['pdfType', 'documentType']; const result = getMimeTypeFilter(filterValue, 'mimetype', 0); expect(result).toEqual({ queryParams: { arg_mimetype_1: 'pdf', arg_mimetype_2: 'doc', - arg_mimetype_3: 'txt', + arg_mimetype_3: 'docx', + arg_mimetype_4: 'gdoc', + arg_mimetype_5: 'rtf', + arg_mimetype_6: 'txt', }, - queries: ['(item.extension IN (:arg_mimetype_1, :arg_mimetype_2, :arg_mimetype_3))'], - keysGenerated: 3, + queries: [ + '(item.extension IN (:arg_mimetype_1, :arg_mimetype_2, :arg_mimetype_3, :arg_mimetype_4, :arg_mimetype_5, :arg_mimetype_6))', + ], + keysGenerated: 6, }); }); - test('should handle values without "Type" suffix', () => { + test('should handle values that are not in FILE_FOLDER_TYPES_MAP', () => { const filterValue = ['pdf', 'doc']; const result = getMimeTypeFilter(filterValue, 'mimetype', 0); expect(result).toEqual({ - queryParams: { - arg_mimetype_1: 'pdf', - arg_mimetype_2: 'doc', - }, - queries: ['(item.extension IN (:arg_mimetype_1, :arg_mimetype_2))'], - keysGenerated: 2, + queryParams: {}, + queries: [], + keysGenerated: 0, }); }); - test('should handle mixed values with and without "Type" suffix', () => { - const filterValue = ['pdfType', 'doc', 'txtType']; + test('should handle mixed valid and invalid values', () => { + const filterValue = ['pdfType', 'doc', 'documentType']; const result = getMimeTypeFilter(filterValue, 'mimetype', 0); expect(result).toEqual({ queryParams: { arg_mimetype_1: 'pdf', arg_mimetype_2: 'doc', - arg_mimetype_3: 'txt', + arg_mimetype_3: 'docx', + arg_mimetype_4: 'gdoc', + arg_mimetype_5: 'rtf', + arg_mimetype_6: 'txt', }, - queries: ['(item.extension IN (:arg_mimetype_1, :arg_mimetype_2, :arg_mimetype_3))'], - keysGenerated: 3, + queries: [ + '(item.extension IN (:arg_mimetype_1, :arg_mimetype_2, :arg_mimetype_3, :arg_mimetype_4, :arg_mimetype_5, :arg_mimetype_6))', + ], + keysGenerated: 6, }); }); @@ -360,16 +354,13 @@ describe('elements/content-explorer/MetadataQueryBuilder', () => { }); }); - test('should handle numeric values converted to strings', () => { + test('should handle numeric values that are not in FILE_FOLDER_TYPES_MAP', () => { const filterValue = ['123', '456']; const result = getMimeTypeFilter(filterValue, 'mimetype', 0); expect(result).toEqual({ - queryParams: { - arg_mimetype_1: '123', - arg_mimetype_2: '456', - }, - queries: ['(item.extension IN (:arg_mimetype_1, :arg_mimetype_2))'], - keysGenerated: 2, + queryParams: {}, + queries: [], + keysGenerated: 0, }); }); @@ -402,15 +393,140 @@ describe('elements/content-explorer/MetadataQueryBuilder', () => { }); test('should handle field names with special characters', () => { - const filterValue = ['pdfType', 'docType']; + const filterValue = ['pdfType', 'documentType']; const result = getMimeTypeFilter(filterValue, 'mime-type.with/special_chars', 0); expect(result).toEqual({ queryParams: { arg_mime_type_with_special_chars_1: 'pdf', arg_mime_type_with_special_chars_2: 'doc', + arg_mime_type_with_special_chars_3: 'docx', + arg_mime_type_with_special_chars_4: 'gdoc', + arg_mime_type_with_special_chars_5: 'rtf', + arg_mime_type_with_special_chars_6: 'txt', + }, + queries: [ + '(item.extension IN (:arg_mime_type_with_special_chars_1, :arg_mime_type_with_special_chars_2, :arg_mime_type_with_special_chars_3, :arg_mime_type_with_special_chars_4, :arg_mime_type_with_special_chars_5, :arg_mime_type_with_special_chars_6))', + ], + keysGenerated: 6, + }); + }); + + // New tests for folderType functionality + test('should handle folderType only', () => { + const filterValue = ['folderType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + }, + queries: ['(item.type = :arg_mime_folderType_1)'], + keysGenerated: 1, + }); + }); + + test('should handle folderType with file types', () => { + const filterValue = ['folderType', 'pdfType', 'documentType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + arg_mimetype_2: 'pdf', + arg_mimetype_3: 'doc', + arg_mimetype_4: 'docx', + arg_mimetype_5: 'gdoc', + arg_mimetype_6: 'rtf', + arg_mimetype_7: 'txt', + }, + queries: [ + '((item.type = :arg_mime_folderType_1) OR (item.extension IN (:arg_mimetype_2, :arg_mimetype_3, :arg_mimetype_4, :arg_mimetype_5, :arg_mimetype_6, :arg_mimetype_7)))', + ], + keysGenerated: 7, + }); + }); + + test('should handle folderType with mixed valid and invalid file types', () => { + const filterValue = ['folderType', 'pdfType', 'doc', 'documentType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + arg_mimetype_2: 'pdf', + arg_mimetype_3: 'doc', + arg_mimetype_4: 'docx', + arg_mimetype_5: 'gdoc', + arg_mimetype_6: 'rtf', + arg_mimetype_7: 'txt', + }, + queries: [ + '((item.type = :arg_mime_folderType_1) OR (item.extension IN (:arg_mimetype_2, :arg_mimetype_3, :arg_mimetype_4, :arg_mimetype_5, :arg_mimetype_6, :arg_mimetype_7)))', + ], + keysGenerated: 7, + }); + }); + + test('should handle folderType with single file type', () => { + const filterValue = ['folderType', 'pdfType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + arg_mimetype_2: 'pdf', + }, + queries: ['((item.type = :arg_mime_folderType_1) OR (item.extension IN (:arg_mimetype_2)))'], + keysGenerated: 2, + }); + }); + + test('should handle folderType with file types using correct arg index', () => { + const filterValue = ['folderType', 'pdfType', 'documentType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 5); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_6: 'folder', + arg_mimetype_7: 'pdf', + arg_mimetype_8: 'doc', + arg_mimetype_9: 'docx', + arg_mimetype_10: 'gdoc', + arg_mimetype_11: 'rtf', + arg_mimetype_12: 'txt', + }, + queries: [ + '((item.type = :arg_mime_folderType_6) OR (item.extension IN (:arg_mimetype_7, :arg_mimetype_8, :arg_mimetype_9, :arg_mimetype_10, :arg_mimetype_11, :arg_mimetype_12)))', + ], + keysGenerated: 7, + }); + }); + + test('should handle multiple folderType entries (should only process one)', () => { + const filterValue = ['folderType', 'pdfType', 'folderType', 'documentType']; + const result = getMimeTypeFilter(filterValue, 'mimetype', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + arg_mimetype_2: 'pdf', + arg_mimetype_3: 'doc', + arg_mimetype_4: 'docx', + arg_mimetype_5: 'gdoc', + arg_mimetype_6: 'rtf', + arg_mimetype_7: 'txt', + }, + queries: [ + '((item.type = :arg_mime_folderType_1) OR (item.extension IN (:arg_mimetype_2, :arg_mimetype_3, :arg_mimetype_4, :arg_mimetype_5, :arg_mimetype_6, :arg_mimetype_7)))', + ], + keysGenerated: 7, + }); + }); + + test('should handle folderType with field names containing special characters', () => { + const filterValue = ['folderType', 'pdfType']; + const result = getMimeTypeFilter(filterValue, 'mime-type.with/special_chars', 0); + expect(result).toEqual({ + queryParams: { + arg_mime_folderType_1: 'folder', + arg_mime_type_with_special_chars_2: 'pdf', }, queries: [ - '(item.extension IN (:arg_mime_type_with_special_chars_1, :arg_mime_type_with_special_chars_2))', + '((item.type = :arg_mime_folderType_1) OR (item.extension IN (:arg_mime_type_with_special_chars_2)))', ], keysGenerated: 2, }); diff --git a/src/elements/content-explorer/constants.ts b/src/elements/content-explorer/constants.ts index e325568a33..21f969f0a1 100644 --- a/src/elements/content-explorer/constants.ts +++ b/src/elements/content-explorer/constants.ts @@ -2,7 +2,7 @@ import { FIELD_FILE_VERSION, FIELD_SHA1, FIELD_SHARED_LINK, FIELD_WATERMARK_INFO import { FOLDER_FIELDS_TO_FETCH } from '../../utils/fields'; // Fields needed for Content Explorer folder requests -const CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH = [ +export const CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH = [ ...FOLDER_FIELDS_TO_FETCH, FIELD_FILE_VERSION, FIELD_SHA1, @@ -10,4 +10,41 @@ const CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH = [ FIELD_WATERMARK_INFO, ]; -export default CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH; +export const NON_FOLDER_FILE_TYPES_MAP = new Map([ + ['boxnoteType', ['boxnote']], + ['boxcanvasType', ['boxcanvas']], + ['pdfType', ['pdf']], + ['documentType', ['doc', 'docx', 'gdoc', 'rtf', 'txt']], + ['spreadsheetType', ['xls', 'xlsx', 'xlsm', 'csv', 'gsheet']], + ['presentationType', ['ppt', 'pptx', 'odp']], + ['imageType', ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff']], + ['audioType', ['mp3', 'm4a', 'm4p', 'wav', 'mid', 'wma']], + [ + 'videoType', + [ + 'mp4', + 'mpeg', + 'mpg', + 'wmv', + '3g2', + '3gp', + 'avi', + 'm2v', + 'm4v', + 'mkv', + 'mov', + 'ogg', + 'mts', + 'qt', + 'ts', + 'flv', + 'rm', + ], + ], + ['drawingType', ['dwg', 'dxf']], + ['threedType', ['obj', 'fbx', 'stl', 'amf', 'iges']], +]); + +export const FILE_FOLDER_TYPES_MAP = new Map(NON_FOLDER_FILE_TYPES_MAP).set('folderType', ['folder']); + +export const NON_FOLDER_FILE_TYPES = Array.from(NON_FOLDER_FILE_TYPES_MAP.keys()); diff --git a/src/elements/content-explorer/utils.ts b/src/elements/content-explorer/utils.ts index 07991feb4b..723bbbce9b 100644 --- a/src/elements/content-explorer/utils.ts +++ b/src/elements/content-explorer/utils.ts @@ -11,9 +11,11 @@ import { } from '@box/metadata-editor'; import type { MetadataFieldType } from '@box/metadata-view'; import type { Selection } from 'react-aria-components'; +import { BoxItemSelection } from '@box/box-item-type-selector'; import type { BoxItem, Collection } from '../../common/types/core'; import messages from '../common/messages'; +import { FILE_FOLDER_TYPES_MAP, NON_FOLDER_FILE_TYPES } from './constants'; // Specific type for metadata field value in the item // Note: Item doesn't have field value in metadata object if that field is not set, so the value will be undefined in this case @@ -193,3 +195,18 @@ export function useTemplateInstance(metadataTemplate: MetadataTemplate, selected type, }; } + +export const mapFileTypes = (selectedFileTypes: BoxItemSelection) => { + const selectedFileTypesSet = new Set(selectedFileTypes); + + const areAllNonFolderFileTypesSelected = NON_FOLDER_FILE_TYPES.every(key => selectedFileTypesSet.has(key)); + + if (areAllNonFolderFileTypesSelected) { + if (selectedFileTypes.includes('folderType')) { + return []; + } + return ['file']; + } + + return selectedFileTypes.map(fileType => FILE_FOLDER_TYPES_MAP.get(fileType as string) || []).flat(); +};