From 621cf5ab29ea7af357c54398b6c2ba7b7d2022bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Sun, 6 Jul 2025 18:40:30 -0300 Subject: [PATCH 01/24] refactor: rename search panel to search input --- .../my-data-section/MyDataItemsPanel.tsx | 4 +- .../CollectionItemsPanel.tsx | 4 +- .../search-input/SearchInput.module.scss | 9 ++++ .../SearchInput.tsx} | 49 +++++++++---------- .../search-panel/SearchPanel.module.scss | 26 ---------- ...rchPanel.spec.tsx => SearchInput.spec.tsx} | 10 ++-- 6 files changed, 40 insertions(+), 62 deletions(-) create mode 100644 src/sections/collection/collection-items-panel/search-input/SearchInput.module.scss rename src/sections/collection/collection-items-panel/{search-panel/SearchPanel.tsx => search-input/SearchInput.tsx} (50%) delete mode 100644 src/sections/collection/collection-items-panel/search-panel/SearchPanel.module.scss rename tests/component/sections/collection/collection-items-panel/{SearchPanel.spec.tsx => SearchInput.spec.tsx} (91%) diff --git a/src/sections/account/my-data-section/MyDataItemsPanel.tsx b/src/sections/account/my-data-section/MyDataItemsPanel.tsx index 3600e90b0..fda792047 100644 --- a/src/sections/account/my-data-section/MyDataItemsPanel.tsx +++ b/src/sections/account/my-data-section/MyDataItemsPanel.tsx @@ -10,7 +10,7 @@ import { ItemsListType } from '@/sections/collection/collection-items-panel/items-list/ItemsList' import { MyDataFilterPanel } from '@/sections/account/my-data-section/my-data-filter-panel/MyDataFilterPanel' -import { SearchPanel } from '@/sections/collection/collection-items-panel/search-panel/SearchPanel' +import { SearchInput } from '@/sections/collection/collection-items-panel/search-input/SearchInput' import { ItemTypeChange } from '@/sections/collection/collection-items-panel/filter-panel/type-filters/TypeFilters' import { MyDataSearchCriteria } from '@/sections/account/my-data-section/MyDataSearchCriteria' import { useGetMyDataAccumulatedItems } from '@/sections/account/my-data-section/useGetMyDataAccumulatedItems' @@ -248,7 +248,7 @@ export const MyDataItemsPanel = ({ collectionRepository }: MyDataItemsPanelProps
-
- void placeholderText: string } -export const SearchPanel = ({ +export const SearchInput = ({ currentSearchValue = '', isLoadingCollectionItems, onSubmitSearch, placeholderText -}: SearchPanelProps) => { +}: SearchInputProps) => { const { t } = useTranslation('shared') const [searchValue, setSearchValue] = useState(currentSearchValue) @@ -38,28 +38,23 @@ export const SearchPanel = ({ }, [currentSearchValue]) return ( -
-
- - - */} -
+
+ + + + @@ -73,14 +82,18 @@ export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps) {/* Datasets Metadata blocks */} - {metadataBlocks.map((metadataBlock) => { + {searchableMetadataBlockFields.map((metadataBlock) => { + if (Object.keys(metadataBlock.metadataFields).length === 0) { + return null // Skip empty metadata blocks + } + return ( {`${t('datasets')}: ${metadataBlock.displayName}`} - + ) @@ -94,6 +107,10 @@ export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps) + + ) diff --git a/src/sections/advanced-search/advanced-search-form/DatasetsMetadataSearchFields.tsx b/src/sections/advanced-search/advanced-search-form/DatasetsMetadataSearchFields.tsx deleted file mode 100644 index e00102e2d..000000000 --- a/src/sections/advanced-search/advanced-search-form/DatasetsMetadataSearchFields.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const DatasetsMetadataSearchFields = () => { - return ( -
-

holo

-
- ) -} diff --git a/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx b/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx new file mode 100644 index 000000000..470f8473d --- /dev/null +++ b/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx @@ -0,0 +1,115 @@ +import { useTranslation } from 'react-i18next' +import { Controller, useFormContext } from 'react-hook-form' +import { Col, Form } from '@iqss/dataverse-design-system' +import { + MetadataField, + TypeClassMetadataFieldOptions +} from '@/metadata-block-info/domain/models/MetadataBlockInfo' + +interface MetadataBlockSearchFieldsProps { + metadataFields: Record +} + +export const MetadataBlockSearchFields = ({ metadataFields }: MetadataBlockSearchFieldsProps) => { + console.log(metadataFields) + return ( + <> + {Object.entries(metadataFields).map(([fieldKey, fieldInfo]) => { + const isControlledVocabulary = + fieldInfo.typeClass === TypeClassMetadataFieldOptions.ControlledVocabulary + + if (isControlledVocabulary) { + return + } + + return + })} + + ) +} + +interface TextFieldProps { + fieldInfo: MetadataField +} + +const TextField = ({ fieldInfo }: TextFieldProps) => { + const { t } = useTranslation('advancedSearch', { keyPrefix: 'datasets' }) + const { control } = useFormContext() + + return ( + + + {fieldInfo.displayName} + + ( + + + {error?.message} + + )} + /> + + ) +} + +interface VocabularyMultipleFieldProps { + fieldInfo: MetadataField +} + +const VocabularyMultipleField = ({ fieldInfo }: VocabularyMultipleFieldProps) => { + const { t } = useTranslation('advancedSearch', { keyPrefix: 'datasets' }) + const { control } = useFormContext() + + return ( + + + {fieldInfo.displayName} + + ( + + + {error?.message} + + )} + /> + + ) +} From 6421e54d813bd544f695960de3531d9863a0fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 9 Jul 2025 23:51:26 -0300 Subject: [PATCH 08/24] feat: construct search query in progress --- .../advanced-search/AdvancedSearchHelper.ts | 209 +++++++++++++++++- .../AdvancedSearchForm.tsx | 29 ++- .../MetadataBlockSearchFields.tsx | 1 - 3 files changed, 231 insertions(+), 8 deletions(-) diff --git a/src/sections/advanced-search/AdvancedSearchHelper.ts b/src/sections/advanced-search/AdvancedSearchHelper.ts index d04146c59..c5c8a4d6a 100644 --- a/src/sections/advanced-search/AdvancedSearchHelper.ts +++ b/src/sections/advanced-search/AdvancedSearchHelper.ts @@ -2,7 +2,11 @@ import { MetadataBlockInfo, MetadataField } from '@/metadata-block-info/domain/models/MetadataBlockInfo' -// import { SearchFields } from '@/search/domain/models/SearchFields' +import { AdvancedSearchFormData } from './advanced-search-form/AdvancedSearchForm' +import { SearchFields } from '@/search/domain/models/SearchFields' + +// TODO:ME - create constructDatasetQuery +// TODO:ME - create function to transform collectionPageQuery to AdvancedSearchFormData export class AdvancedSearchHelper { public static filterSearchableMetadataBlockFields( @@ -37,4 +41,207 @@ export class AdvancedSearchHelper { } }) } + + public static getFormDefaultValues( + metadataBlocks: MetadataBlockInfo[], + collectionPageQuery: string | null + ): AdvancedSearchFormData { + // console.log('collectionPageQuery: ', collectionPageQuery) + return { + collections: { + [SearchFields.DATAVERSE_NAME]: '', + [SearchFields.DATAVERSE_ALIAS]: '', + [SearchFields.DATAVERSE_AFFILIATION]: '', + [SearchFields.DATAVERSE_DESCRIPTION]: '', + [SearchFields.DATAVERSE_SUBJECT]: [] + }, + datasets: { + astroFacility: 'Something here' + }, + files: { + [SearchFields.FILE_NAME]: '', + [SearchFields.FILE_DESCRIPTION]: '', + [SearchFields.FILE_TYPE_SEARCHABLE]: '', + [SearchFields.FILE_PERSISTENT_ID]: '', + [SearchFields.VARIABLE_NAME]: '', + [SearchFields.VARIABLE_LABEL]: '', + [SearchFields.FILE_TAG_SEARCHABLE]: '' + } + } + } + + public static constructSearchQuery(formData: AdvancedSearchFormData): string { + const collectionQuery = this.constructCollectionQuery(formData.collections) + + const fileQuery = this.constructFileQuery(formData.files) + + const queries: string[] = [] + + if (collectionQuery) { + queries.push(collectionQuery) + } + + if (fileQuery) { + queries.push(fileQuery) + } + + const query = this.constructQuery(queries, false, false) + + return query + } + + private static constructCollectionQuery(fields: { + [SearchFields.DATAVERSE_NAME]: string + [SearchFields.DATAVERSE_ALIAS]: string + [SearchFields.DATAVERSE_AFFILIATION]: string + [SearchFields.DATAVERSE_DESCRIPTION]: string + [SearchFields.DATAVERSE_SUBJECT]: string[] + }): string { + const queryStrings: string[] = [] + + if (fields[SearchFields.DATAVERSE_NAME]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.DATAVERSE_NAME, + fields[SearchFields.DATAVERSE_NAME].trim() + ) + ) + } + + if (fields[SearchFields.DATAVERSE_ALIAS]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.DATAVERSE_ALIAS, + fields[SearchFields.DATAVERSE_ALIAS].trim() + ) + ) + } + + if (fields[SearchFields.DATAVERSE_AFFILIATION]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.DATAVERSE_AFFILIATION, + fields[SearchFields.DATAVERSE_AFFILIATION].trim() + ) + ) + } + + if (fields[SearchFields.DATAVERSE_DESCRIPTION]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.DATAVERSE_DESCRIPTION, + fields[SearchFields.DATAVERSE_DESCRIPTION].trim() + ) + ) + } + + if (fields[SearchFields.DATAVERSE_SUBJECT].length > 0) { + const subjectQueries = fields[SearchFields.DATAVERSE_SUBJECT].map( + (value) => `${SearchFields.DATAVERSE_SUBJECT}:"${value}"` + ) + queryStrings.push(this.constructQuery(subjectQueries, false)) + } + + return this.constructQuery(queryStrings, true) + } + + private static constructFileQuery(fields: { + [SearchFields.FILE_NAME]: string + [SearchFields.FILE_DESCRIPTION]: string + [SearchFields.FILE_TYPE_SEARCHABLE]: string + [SearchFields.FILE_PERSISTENT_ID]: string + [SearchFields.VARIABLE_NAME]: string + [SearchFields.VARIABLE_LABEL]: string + [SearchFields.FILE_TAG_SEARCHABLE]: string + }): string { + const queryStrings: string[] = [] + + if (fields[SearchFields.FILE_NAME]?.trim()) { + queryStrings.push( + this.constructFieldQuery(SearchFields.FILE_NAME, fields[SearchFields.FILE_NAME].trim()) + ) + } + + if (fields[SearchFields.FILE_DESCRIPTION]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.FILE_DESCRIPTION, + fields[SearchFields.FILE_DESCRIPTION].trim() + ) + ) + } + + if (fields[SearchFields.FILE_TYPE_SEARCHABLE]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.FILE_TYPE_SEARCHABLE, + fields[SearchFields.FILE_TYPE_SEARCHABLE].trim() + ) + ) + } + + if (fields[SearchFields.FILE_PERSISTENT_ID]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.FILE_PERSISTENT_ID, + fields[SearchFields.FILE_PERSISTENT_ID].trim() + ) + ) + } + + if (fields[SearchFields.VARIABLE_NAME]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.VARIABLE_NAME, + fields[SearchFields.VARIABLE_NAME].trim() + ) + ) + } + + if (fields[SearchFields.VARIABLE_LABEL]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.VARIABLE_LABEL, + fields[SearchFields.VARIABLE_LABEL].trim() + ) + ) + } + + if (fields[SearchFields.FILE_TAG_SEARCHABLE]?.trim()) { + queryStrings.push( + this.constructFieldQuery( + SearchFields.FILE_TAG_SEARCHABLE, + fields[SearchFields.FILE_TAG_SEARCHABLE].trim() + ) + ) + } + + return this.constructQuery(queryStrings, true) + } + + private static constructQuery( + queryStrings: string[], + isAnd: boolean, + surroundWithParens = true + ): string { + const nonEmpty = queryStrings.filter((str) => str.trim() !== '') + + if (nonEmpty.length === 0) return '' + + const combined = nonEmpty.join(isAnd ? ' AND ' : ' OR ') + + return surroundWithParens && nonEmpty.length > 1 ? `(${combined})` : combined + } + + private static constructFieldQuery(field: string, value: string): string { + if (!value.trim()) return '' + const words = value.trim().split(' ') + + if (words.length === 1) { + return `${field}:${words[0]}` + } + const joinedWords = words.map((word) => `${field}:${word}`).join(' ') + + return `(${joinedWords})` + } } diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 562592834..7c212de17 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -16,12 +16,30 @@ interface AdvancedSearchFormProps { metadataBlocks: MetadataBlockInfo[] } +export interface AdvancedSearchFormData { + collections: { + [SearchFields.DATAVERSE_NAME]: string + [SearchFields.DATAVERSE_ALIAS]: string + [SearchFields.DATAVERSE_AFFILIATION]: string + [SearchFields.DATAVERSE_DESCRIPTION]: string + [SearchFields.DATAVERSE_SUBJECT]: string[] + } + datasets: Record + files: { + [SearchFields.FILE_NAME]: string + [SearchFields.FILE_DESCRIPTION]: string + [SearchFields.FILE_TYPE_SEARCHABLE]: string + [SearchFields.FILE_PERSISTENT_ID]: string + [SearchFields.VARIABLE_NAME]: string + [SearchFields.VARIABLE_LABEL]: string + [SearchFields.FILE_TAG_SEARCHABLE]: string + } +} + export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps) => { const { t } = useTranslation('shared') - { - /* */ - } - const formMethods = useForm({ + + const formMethods = useForm({ mode: 'onChange', defaultValues: { collections: { @@ -63,8 +81,7 @@ export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps)
{ - console.log(data) - console.log(Object.keys(data.datasets).length) + AdvancedSearchHelper.constructSearchQuery(data) })} noValidate={true}> -
diff --git a/src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts b/src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts index b5d6bab9e..d9f6d6251 100644 --- a/src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts +++ b/src/sections/shared/form/DatasetMetadataForm/MetadataFieldsHelper.ts @@ -414,7 +414,7 @@ export class MetadataFieldsHelper { } private static replaceDotWithSlash = (str: string) => str.replace(/\./g, '/') - private static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.') + public static replaceSlashWithDot = (str: string) => str.replace(/\//g, '.') /* * To define the field name that will be used to register the field in the form From 4d1e83d861a4d189dfd83dd1046513945cdcd948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 10 Jul 2025 16:44:21 -0300 Subject: [PATCH 11/24] feat: empty form default values --- .../advanced-search/AdvancedSearch.tsx | 8 ++- .../advanced-search/AdvancedSearchHelper.ts | 48 ++++++------- .../AdvancedSearchForm.tsx | 68 ++++++++----------- 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/sections/advanced-search/AdvancedSearch.tsx b/src/sections/advanced-search/AdvancedSearch.tsx index 7c9b88022..25a502a37 100644 --- a/src/sections/advanced-search/AdvancedSearch.tsx +++ b/src/sections/advanced-search/AdvancedSearch.tsx @@ -12,6 +12,7 @@ import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine' import { AdvancedSearchForm } from './advanced-search-form/AdvancedSearchForm' import { MetadataFieldsHelper } from '../shared/form/DatasetMetadataForm/MetadataFieldsHelper' +import { AdvancedSearchHelper } from './AdvancedSearchHelper' interface AdvancedSearchProps { collectionId: string @@ -69,7 +70,7 @@ export const AdvancedSearch = ({ const normalizedMetadataBlocksInfo = MetadataFieldsHelper.replaceMetadataBlocksInfoDotNamesKeysWithSlash(metadataBlocksInfo) - // TODO:ME - Encapsulate form to define defaultValues based on metadata blocks and collectionPageQuery, follow JSF convention for URL + const formDefaultValues = AdvancedSearchHelper.getFormDefaultValues(normalizedMetadataBlocksInfo) return (
@@ -84,7 +85,10 @@ export const AdvancedSearch = ({ - +
) } diff --git a/src/sections/advanced-search/AdvancedSearchHelper.ts b/src/sections/advanced-search/AdvancedSearchHelper.ts index 877e5223f..12110136a 100644 --- a/src/sections/advanced-search/AdvancedSearchHelper.ts +++ b/src/sections/advanced-search/AdvancedSearchHelper.ts @@ -1,8 +1,13 @@ import { MetadataBlockInfo, - MetadataField + MetadataField, + TypeClassMetadataFieldOptions } from '@/metadata-block-info/domain/models/MetadataBlockInfo' -import { AdvancedSearchFormData } from './advanced-search-form/AdvancedSearchForm' +import { + AdvancedSearchFormData, + CollectionsFields, + FilesFields +} from './advanced-search-form/AdvancedSearchForm' import { SearchFields } from '@/search/domain/models/SearchFields' import { MetadataFieldsHelper } from '../shared/form/DatasetMetadataForm/MetadataFieldsHelper' @@ -42,11 +47,20 @@ export class AdvancedSearchHelper { }) } - public static getFormDefaultValues( - metadataBlocks: MetadataBlockInfo[], - collectionPageQuery: string | null - ): AdvancedSearchFormData { - // console.log('collectionPageQuery: ', collectionPageQuery) + public static getFormDefaultValues(metadataBlocks: MetadataBlockInfo[]): AdvancedSearchFormData { + const searchableMetadataBlockFields = this.filterSearchableMetadataBlockFields(metadataBlocks) + + const flattenedMetadataFields: Record = {} + + for (const block of searchableMetadataBlockFields) { + for (const field of Object.values(block.metadataFields)) { + const isControlledVocabulary = + field.typeClass === TypeClassMetadataFieldOptions.ControlledVocabulary + + flattenedMetadataFields[field.name] = isControlledVocabulary ? [] : '' + } + } + return { collections: { [SearchFields.DATAVERSE_NAME]: '', @@ -56,7 +70,7 @@ export class AdvancedSearchHelper { [SearchFields.DATAVERSE_SUBJECT]: [] }, datasets: { - astroFacility: 'Something here' + ...flattenedMetadataFields }, files: { [SearchFields.FILE_NAME]: '', @@ -94,13 +108,7 @@ export class AdvancedSearchHelper { return query } - private static constructCollectionQuery(fields: { - [SearchFields.DATAVERSE_NAME]: string - [SearchFields.DATAVERSE_ALIAS]: string - [SearchFields.DATAVERSE_AFFILIATION]: string - [SearchFields.DATAVERSE_DESCRIPTION]: string - [SearchFields.DATAVERSE_SUBJECT]: string[] - }): string { + private static constructCollectionQuery(fields: CollectionsFields): string { const queryStrings: string[] = [] if (fields[SearchFields.DATAVERSE_NAME]?.trim()) { @@ -149,15 +157,7 @@ export class AdvancedSearchHelper { return this.constructQuery(queryStrings, true) } - private static constructFileQuery(fields: { - [SearchFields.FILE_NAME]: string - [SearchFields.FILE_DESCRIPTION]: string - [SearchFields.FILE_TYPE_SEARCHABLE]: string - [SearchFields.FILE_PERSISTENT_ID]: string - [SearchFields.VARIABLE_NAME]: string - [SearchFields.VARIABLE_LABEL]: string - [SearchFields.FILE_TAG_SEARCHABLE]: string - }): string { + private static constructFileQuery(fields: FilesFields): string { const queryStrings: string[] = [] if (fields[SearchFields.FILE_NAME]?.trim()) { diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 46b339881..c42746caa 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -13,55 +13,45 @@ import { FilesSearchFields } from './FilesSearchFields' import { AdvancedSearchHelper } from '../AdvancedSearchHelper' interface AdvancedSearchFormProps { + formDefaultValues: AdvancedSearchFormData metadataBlocks: MetadataBlockInfo[] } export interface AdvancedSearchFormData { - collections: { - [SearchFields.DATAVERSE_NAME]: string - [SearchFields.DATAVERSE_ALIAS]: string - [SearchFields.DATAVERSE_AFFILIATION]: string - [SearchFields.DATAVERSE_DESCRIPTION]: string - [SearchFields.DATAVERSE_SUBJECT]: string[] - } - datasets: Record - files: { - [SearchFields.FILE_NAME]: string - [SearchFields.FILE_DESCRIPTION]: string - [SearchFields.FILE_TYPE_SEARCHABLE]: string - [SearchFields.FILE_PERSISTENT_ID]: string - [SearchFields.VARIABLE_NAME]: string - [SearchFields.VARIABLE_LABEL]: string - [SearchFields.FILE_TAG_SEARCHABLE]: string - } + collections: CollectionsFields + datasets: DatasetsFields + files: FilesFields } -export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps) => { +export interface CollectionsFields { + [SearchFields.DATAVERSE_NAME]: string + [SearchFields.DATAVERSE_ALIAS]: string + [SearchFields.DATAVERSE_AFFILIATION]: string + [SearchFields.DATAVERSE_DESCRIPTION]: string + [SearchFields.DATAVERSE_SUBJECT]: string[] +} + +export type DatasetsFields = Record + +export type FilesFields = { + [SearchFields.FILE_NAME]: string + [SearchFields.FILE_DESCRIPTION]: string + [SearchFields.FILE_TYPE_SEARCHABLE]: string + [SearchFields.FILE_PERSISTENT_ID]: string + [SearchFields.VARIABLE_NAME]: string + [SearchFields.VARIABLE_LABEL]: string + [SearchFields.FILE_TAG_SEARCHABLE]: string +} + +export const AdvancedSearchForm = ({ + formDefaultValues, + metadataBlocks +}: AdvancedSearchFormProps) => { const { t } = useTranslation('shared') const formMethods = useForm({ mode: 'onChange', - defaultValues: { - collections: { - [SearchFields.DATAVERSE_NAME]: '', - [SearchFields.DATAVERSE_ALIAS]: '', - [SearchFields.DATAVERSE_AFFILIATION]: '', - [SearchFields.DATAVERSE_DESCRIPTION]: '', - [SearchFields.DATAVERSE_SUBJECT]: [] - }, - datasets: { - astroFacility: '' - }, - files: { - [SearchFields.FILE_NAME]: '', - [SearchFields.FILE_DESCRIPTION]: '', - [SearchFields.FILE_TYPE_SEARCHABLE]: '', - [SearchFields.FILE_PERSISTENT_ID]: '', - [SearchFields.VARIABLE_NAME]: '', - [SearchFields.VARIABLE_LABEL]: '', - [SearchFields.FILE_TAG_SEARCHABLE]: '' - } - } + defaultValues: formDefaultValues }) const subjectFieldControlledVocab: string[] = useMemo( From 896f823063c7c6636320da99c18c48f49eac7d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 10 Jul 2025 17:03:32 -0300 Subject: [PATCH 12/24] fix: remove unused location state --- src/sections/not-found-page/NotFoundPageFactory.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sections/not-found-page/NotFoundPageFactory.tsx b/src/sections/not-found-page/NotFoundPageFactory.tsx index 3e9b54756..9adf6c4e6 100644 --- a/src/sections/not-found-page/NotFoundPageFactory.tsx +++ b/src/sections/not-found-page/NotFoundPageFactory.tsx @@ -1,5 +1,4 @@ import { ReactElement } from 'react' -import { useLocation } from 'react-router-dom' import { NotFoundPage } from './NotFoundPage' export class NotFoundPageFactory { @@ -9,10 +8,5 @@ export class NotFoundPageFactory { } function NotFoundPageWithParams() { - const location = useLocation() - const locationState = location.state as { foo: boolean } | undefined - - console.log(locationState) - return } From 7ecf464b845352febd192b7d631c51132acbdec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 10 Jul 2025 17:10:12 -0300 Subject: [PATCH 13/24] feat: navigate after search --- .../advanced-search/AdvancedSearch.tsx | 1 + .../AdvancedSearchForm.tsx | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/sections/advanced-search/AdvancedSearch.tsx b/src/sections/advanced-search/AdvancedSearch.tsx index 25a502a37..12195a1a7 100644 --- a/src/sections/advanced-search/AdvancedSearch.tsx +++ b/src/sections/advanced-search/AdvancedSearch.tsx @@ -86,6 +86,7 @@ export const AdvancedSearch = ({ diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index c42746caa..2c71a0bea 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' import { FormProvider, useForm } from 'react-hook-form' import { Accordion, Button } from '@iqss/dataverse-design-system' import { @@ -7,12 +8,16 @@ import { MetadataBlockName } from '@/metadata-block-info/domain/models/MetadataBlockInfo' import { SearchFields } from '@/search/domain/models/SearchFields' +import { CollectionItemsQueryParams } from '@/collection/domain/models/CollectionItemsQueryParams' +import { Route } from '@/sections/Route.enum' +import { CollectionItemType } from '@/collection/domain/models/CollectionItemType' import { CollectionsSearchFields } from './CollectionsSearchFields' import { MetadataBlockSearchFields } from './MetadataBlockSearchFields' import { FilesSearchFields } from './FilesSearchFields' import { AdvancedSearchHelper } from '../AdvancedSearchHelper' interface AdvancedSearchFormProps { + collectionId: string formDefaultValues: AdvancedSearchFormData metadataBlocks: MetadataBlockInfo[] } @@ -44,10 +49,12 @@ export type FilesFields = { } export const AdvancedSearchForm = ({ + collectionId, formDefaultValues, metadataBlocks }: AdvancedSearchFormProps) => { const { t } = useTranslation('shared') + const navigate = useNavigate() const formMethods = useForm({ mode: 'onChange', @@ -71,7 +78,23 @@ export const AdvancedSearchForm = ({
{ - AdvancedSearchHelper.constructSearchQuery(data) + const advancedSearchQuery = AdvancedSearchHelper.constructSearchQuery(data) + const searchParams = new URLSearchParams() + searchParams.set(CollectionItemsQueryParams.QUERY, advancedSearchQuery) + searchParams.set( + CollectionItemsQueryParams.TYPES, + [ + CollectionItemType.COLLECTION, + CollectionItemType.DATASET, + CollectionItemType.FILE + ].join(',') + ) + + const collectionUrlWithQuery = `${ + Route.COLLECTIONS_BASE + }/${collectionId}?${searchParams.toString()}` + + navigate(collectionUrlWithQuery) })} noValidate={true}> From 7cfe2a7ef3152dc28f2ffbd1976e84a44c597a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 11 Jul 2025 11:11:05 -0300 Subject: [PATCH 15/24] feat: clear form button --- public/locales/en/advancedSearch.json | 1 + .../advanced-search/AdvancedSearchHelper.ts | 4 +-- .../AdvancedSearchForm.tsx | 27 +++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/public/locales/en/advancedSearch.json b/public/locales/en/advancedSearch.json index 2a1940455..c0e2a58aa 100644 --- a/public/locales/en/advancedSearch.json +++ b/public/locales/en/advancedSearch.json @@ -1,5 +1,6 @@ { "pageTitle": "Advanced Search", + "clearForm": "Clear Form", "collections": { "name": { "label": "Name", diff --git a/src/sections/advanced-search/AdvancedSearchHelper.ts b/src/sections/advanced-search/AdvancedSearchHelper.ts index a7c7442bc..c822025f7 100644 --- a/src/sections/advanced-search/AdvancedSearchHelper.ts +++ b/src/sections/advanced-search/AdvancedSearchHelper.ts @@ -3,6 +3,7 @@ import { MetadataField, TypeClassMetadataFieldOptions } from '@/metadata-block-info/domain/models/MetadataBlockInfo' +import { Utils } from '@/shared/helpers/Utils' import { AdvancedSearchFormData, CollectionsFields, @@ -10,9 +11,6 @@ import { } from './advanced-search-form/AdvancedSearchForm' import { SearchFields } from '@/search/domain/models/SearchFields' import { MetadataFieldsHelper } from '../shared/form/DatasetMetadataForm/MetadataFieldsHelper' -import { Utils } from '@/shared/helpers/Utils' - -// TODO:ME - Add clear all values button to the form export class AdvancedSearchHelper { public static previousAdvancedSearchQueryLSKey = 'previousAdvancedSearchQuery' diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 9e1b03bfe..7f61b1b63 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -1,7 +1,8 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { FormProvider, useForm } from 'react-hook-form' +import { ArrowClockwise } from 'react-bootstrap-icons' import { Accordion, Button } from '@iqss/dataverse-design-system' import { MetadataBlockInfo, @@ -54,6 +55,8 @@ export const AdvancedSearchForm = ({ metadataBlocks }: AdvancedSearchFormProps) => { const { t } = useTranslation('shared') + const { t: tAdvancedSearch } = useTranslation('advancedSearch') + const [resetKey, setResetKey] = useState(0) const navigate = useNavigate() const formMethods = useForm({ @@ -75,6 +78,12 @@ export const AdvancedSearchForm = ({ AdvancedSearchHelper.saveAdvancedSearchQueryToLocalStorage(collectionId, data) } + const handleClearForm = () => { + formMethods.reset(AdvancedSearchHelper.getFormDefaultValues(metadataBlocks, null)) + AdvancedSearchHelper.clearPreviousAdvancedSearchQueryFromLocalStorage() // Clear local storage in case there was a previous search saved. + setResetKey((prev) => prev + 1) // This is a workaround to force re-render components that depend on the form values. + } + const subjectFieldControlledVocab: string[] = useMemo( () => metadataBlocks.find((block) => block.name === MetadataBlockName.CITATION)?.metadataFields[ @@ -89,11 +98,19 @@ export const AdvancedSearchForm = ({ AdvancedSearchHelper.filterSearchableMetadataBlockFields(metadataBlocks) return ( - + - +
+ + +
Date: Fri, 11 Jul 2025 11:44:52 -0300 Subject: [PATCH 16/24] fix(Design System): fix word wrapping in options list to prevent overflow and ensure long text is displayed correctly --- packages/design-system/CHANGELOG.md | 4 ++++ .../lib/components/select-advanced/SelectAdvanced.module.scss | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index 476a900fa..580bc8a1d 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# Non Published Changes + +- **SelectAdvanced:** Fix word wrapping in options list to prevent overflow and ensure long text is displayed correctly. + # [2.0.2](https://github.com/IQSS/dataverse-frontend/compare/@iqss/dataverse-design-system@2.0.1...@iqss/dataverse-design-system@2.0.2) (2024-06-23) ### Bug Fixes diff --git a/packages/design-system/src/lib/components/select-advanced/SelectAdvanced.module.scss b/packages/design-system/src/lib/components/select-advanced/SelectAdvanced.module.scss index 957e076d4..cc86a8093 100644 --- a/packages/design-system/src/lib/components/select-advanced/SelectAdvanced.module.scss +++ b/packages/design-system/src/lib/components/select-advanced/SelectAdvanced.module.scss @@ -154,6 +154,7 @@ &__checkbox-input { display: flex; + flex-wrap: wrap; align-items: center; padding-left: 0; @@ -164,15 +165,16 @@ } label { - width: 100%; padding-left: 0.5rem; padding-block: 0.25rem; + white-space: wrap; } } } .option-item-not-multiple { margin-bottom: 0.125rem; + white-space: wrap; cursor: pointer; transition: background-color 0.1s ease-in-out; } From 6c59d9889fdba4b6853181a8d077ebd3dc3d63a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 11 Jul 2025 17:33:52 -0300 Subject: [PATCH 17/24] tests: add unit tests --- .../advanced-search/AdvancedSearchHelper.ts | 1 - .../AdvancedSearchForm.tsx | 7 +- .../advanced-search/AdvancedSearch.spec.tsx | 152 ++++++++++ .../AdvancedSearchHelper.spec.tsx | 272 ++++++++++++++++++ 4 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 tests/component/sections/advanced-search/AdvancedSearch.spec.tsx create mode 100644 tests/component/sections/advanced-search/AdvancedSearchHelper.spec.tsx diff --git a/src/sections/advanced-search/AdvancedSearchHelper.ts b/src/sections/advanced-search/AdvancedSearchHelper.ts index c822025f7..8f2503448 100644 --- a/src/sections/advanced-search/AdvancedSearchHelper.ts +++ b/src/sections/advanced-search/AdvancedSearchHelper.ts @@ -274,7 +274,6 @@ export class AdvancedSearchHelper { } private static constructFieldQuery(field: string, value: string): string { - if (!value.trim()) return '' const words = value.trim().split(' ') if (words.length === 1) { diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 7f61b1b63..d7901daa5 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -117,7 +117,7 @@ export const AdvancedSearchForm = ({ alwaysOpen={true}> {t('collections')} - + @@ -133,7 +133,8 @@ export const AdvancedSearchForm = ({ {`${t('datasets')}: ${metadataBlock.displayName}`} - + @@ -143,7 +144,7 @@ export const AdvancedSearchForm = ({ {/* Files */} {t('files')} - + diff --git a/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx b/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx new file mode 100644 index 000000000..5329e8fe1 --- /dev/null +++ b/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx @@ -0,0 +1,152 @@ +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { MetadataBlockInfoRepository } from '@/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' +import { SearchFields } from '@/search/domain/models/SearchFields' +import { AdvancedSearchFormData } from '@/sections/advanced-search/advanced-search-form/AdvancedSearchForm' +import { AdvancedSearch } from '@/sections/advanced-search/AdvancedSearch' +import { AdvancedSearchHelper } from '@/sections/advanced-search/AdvancedSearchHelper' +import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' +import { MetadataBlockInfoMother } from '@tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother' + +const collectionRepository = {} as CollectionRepository +const metadataBlockInfoRepository = {} as MetadataBlockInfoRepository + +const testCollection = CollectionMother.create({ id: 'test-collection', name: 'Test Collection' }) +const testCitationMetadataBlock = MetadataBlockInfoMother.getCitationBlock() + +describe('AdvancedSearch', () => { + beforeEach(() => { + cy.viewport('macbook-15') + collectionRepository.getById = cy.stub().resolves(testCollection) + metadataBlockInfoRepository.getByCollectionId = cy.stub().resolves([testCitationMetadataBlock]) + }) + + it('should render Datasets Citation Metadata Block fields correctly', () => { + cy.customMount( + + ) + + cy.findByTestId('advanced-search-metadata-block-citation') + .should('be.visible') + .within(() => { + // Asserting that this metadata block has the expected fields (fields with isAdvancedSearchFieldType === true) + cy.findByLabelText('Title').should('be.visible') + cy.findByLabelText('Subject').should('be.visible') + }) + }) + + it('should prefill the form with previous search data if it exists and it belongs to current collection', () => { + // Simulating a previous search data in local storage + const previousSearchData: AdvancedSearchFormData = { + collections: { + [SearchFields.DATAVERSE_NAME]: 'Some Dataverse Name', + [SearchFields.DATAVERSE_ALIAS]: 'Some Dataverse Alias', + [SearchFields.DATAVERSE_AFFILIATION]: '', + [SearchFields.DATAVERSE_DESCRIPTION]: '', + [SearchFields.DATAVERSE_SUBJECT]: ['Chemistry'] + }, + datasets: { + title: 'Some Dataset Title', + subject: [] + }, + files: { + [SearchFields.FILE_NAME]: 'Some File Name', + [SearchFields.FILE_DESCRIPTION]: '', + [SearchFields.FILE_TYPE_SEARCHABLE]: '', + [SearchFields.FILE_PERSISTENT_ID]: '', + [SearchFields.VARIABLE_NAME]: '', + [SearchFields.VARIABLE_LABEL]: '', + [SearchFields.FILE_TAG_SEARCHABLE]: '' + } + } + AdvancedSearchHelper.saveAdvancedSearchQueryToLocalStorage( + testCollection.id, + previousSearchData + ) + + cy.customMount( + + ) + + cy.findByTestId('advanced-search-collections').within(() => { + cy.findByLabelText('Name').should('be.visible').should('have.value', 'Some Dataverse Name') + + cy.findByLabelText('Identifier') + .should('be.visible') + .should('have.value', 'Some Dataverse Alias') + + cy.get('[id="collections.subject"]').click() + cy.findAllByRole('checkbox', { name: 'Chemistry' }) + .should('be.checked') + .should('have.value', 'Chemistry') + }) + + cy.findByTestId('advanced-search-metadata-block-citation') + .should('be.visible') + .within(() => { + cy.findByLabelText('Title').should('be.visible').should('have.value', 'Some Dataset Title') + }) + + cy.findByTestId('advanced-search-files').within(() => { + cy.findByLabelText('Name').should('be.visible').should('have.value', 'Some File Name') + }) + + // Clear the form test + cy.findByRole('button', { name: 'Clear Form' }).click() + + cy.findByTestId('advanced-search-collections').within(() => { + cy.findByLabelText('Name').should('be.visible').should('have.value', '') + + cy.findByLabelText('Identifier').should('be.visible').should('have.value', '') + + cy.get('[id="collections.subject"]').click() + cy.findAllByRole('checkbox', { name: 'Chemistry' }).should('not.be.checked') + }) + + cy.findByTestId('advanced-search-metadata-block-citation') + .should('be.visible') + .within(() => { + cy.findByLabelText('Title').should('be.visible').should('have.value', '') + }) + + cy.findByTestId('advanced-search-files').within(() => { + cy.findByLabelText('Name').should('be.visible').should('have.value', '') + }) + }) + + it('should show not found page if collection does not exist', () => { + collectionRepository.getById = cy.stub().resolves(null) + + cy.customMount( + + ) + + cy.findByTestId('not-found-page').should('be.visible') + }) + + it('should show error alert if there is an error fetching metadata blocks', () => { + const errorMessage = 'Error fetching metadata blocks' + metadataBlockInfoRepository.getByCollectionId = cy.stub().rejects(new Error(errorMessage)) + + cy.customMount( + + ) + + cy.findByRole('alert').should('be.visible').and('contain.text', errorMessage) + }) +}) diff --git a/tests/component/sections/advanced-search/AdvancedSearchHelper.spec.tsx b/tests/component/sections/advanced-search/AdvancedSearchHelper.spec.tsx new file mode 100644 index 000000000..cc7ab0ed3 --- /dev/null +++ b/tests/component/sections/advanced-search/AdvancedSearchHelper.spec.tsx @@ -0,0 +1,272 @@ +import { MetadataBlockInfo } from '@/metadata-block-info/domain/models/MetadataBlockInfo' +import { SearchFields } from '@/search/domain/models/SearchFields' +import { AdvancedSearchFormData } from '@/sections/advanced-search/advanced-search-form/AdvancedSearchForm' +import { AdvancedSearchHelper } from '@/sections/advanced-search/AdvancedSearchHelper' + +const testMetadataBlocks: MetadataBlockInfo[] = [ + { + id: 1, + name: 'block1', + displayName: 'Block 1', + displayOnCreate: true, + metadataFields: { + title: { + name: 'title', + displayName: 'Title', + title: 'Title', + type: 'TEXT', + watermark: '', + description: 'The main title of the Dataset', + multiple: false, + isControlledVocabulary: false, + displayFormat: '', + isRequired: true, + displayOrder: 0, + typeClass: 'primitive', + displayOnCreate: true, + isAdvancedSearchFieldType: true + }, + author: { + name: 'author', + displayName: 'Author', + title: 'Author', + type: 'NONE', + watermark: '', + description: 'The entity, e.g. a person or organization, that created the Dataset', + multiple: true, + isControlledVocabulary: false, + displayFormat: '', + isRequired: true, + displayOrder: 7, + typeClass: 'compound', + displayOnCreate: true, + isAdvancedSearchFieldType: false, + childMetadataFields: { + authorName: { + name: 'authorName', + displayName: 'Author Name', + title: 'Name', + type: 'TEXT', + watermark: '1) Family Name, Given Name or 2) Organization XYZ', + description: + "The name of the author, such as the person's name or the name of an organization", + multiple: false, + isControlledVocabulary: false, + displayFormat: '#VALUE', + isRequired: true, + displayOrder: 8, + typeClass: 'primitive', + displayOnCreate: true, + isAdvancedSearchFieldType: true + }, + authorAffiliation: { + name: 'authorAffiliation', + displayName: 'Author Affiliation', + title: 'Affiliation', + type: 'TEXT', + watermark: 'Organization XYZ', + description: + "The name of the entity affiliated with the author, e.g. an organization's name", + multiple: false, + isControlledVocabulary: false, + displayFormat: '(#VALUE)', + isRequired: false, + displayOrder: 9, + typeClass: 'primitive', + displayOnCreate: true, + isAdvancedSearchFieldType: false + } + } + }, + subject: { + name: 'subject', + displayName: 'Subject', + title: 'Subject', + type: 'TEXT', + watermark: '', + description: 'The area of study relevant to the Dataset', + multiple: true, + isControlledVocabulary: true, + controlledVocabularyValues: [ + 'Agricultural Sciences', + 'Arts and Humanities', + 'Astronomy and Astrophysics', + 'Business and Management', + 'Chemistry', + 'Computer and Information Science', + 'Earth and Environmental Sciences', + 'Engineering', + 'Law', + 'Mathematical Sciences', + 'Medicine, Health and Life Sciences', + 'Physics', + 'Social Sciences', + 'Other' + ], + displayFormat: '', + isRequired: true, + displayOrder: 19, + typeClass: 'controlledVocabulary', + displayOnCreate: true, + isAdvancedSearchFieldType: true + } + } + } +] + +describe('AdvancedSearchHelper', () => { + describe('filterSearchableMetadataBlockFields', () => { + it('should return only metadata fields with searchable fields', () => { + const result = AdvancedSearchHelper.filterSearchableMetadataBlockFields(testMetadataBlocks) + + expect(result[0].metadataFields).to.have.property('title') + expect(result[0].metadataFields).to.have.property('authorName') + expect(result[0].metadataFields).to.have.property('subject') + expect(result[0].metadataFields).to.not.have.property('author') + expect(result[0].metadataFields).to.not.have.property('authorAffiliation') + }) + }) + + describe('getFormDefaultValues', () => { + it('should return correct form default values', () => { + const result = AdvancedSearchHelper.getFormDefaultValues(testMetadataBlocks, null) + console.log(result) + expect(result).to.have.property('collections') + expect(result).to.have.property('datasets') + expect(result).to.have.property('files') + expect(result.collections).to.have.property(SearchFields.DATAVERSE_NAME) + expect(result.collections).to.have.property(SearchFields.DATAVERSE_ALIAS) + expect(result.collections).to.have.property(SearchFields.DATAVERSE_AFFILIATION) + expect(result.collections).to.have.property(SearchFields.DATAVERSE_DESCRIPTION) + expect(result.collections).to.have.property(SearchFields.DATAVERSE_SUBJECT) + expect(result.datasets).to.have.property('title') + expect(result.datasets.title).to.equal('') + expect(result.datasets).to.have.property('authorName') + expect(result.datasets.authorName).to.equal('') + expect(result.datasets).to.have.property('subject') + expect(result.datasets.subject).to.deep.equal([]) + expect(result.files).to.have.property(SearchFields.FILE_NAME) + expect(result.files).to.have.property(SearchFields.FILE_DESCRIPTION) + expect(result.files).to.have.property(SearchFields.FILE_TYPE_SEARCHABLE) + expect(result.files).to.have.property(SearchFields.FILE_PERSISTENT_ID) + expect(result.files).to.have.property(SearchFields.VARIABLE_NAME) + expect(result.files).to.have.property(SearchFields.VARIABLE_LABEL) + expect(result.files).to.have.property(SearchFields.FILE_TAG_SEARCHABLE) + }) + + it('should return correct form default values with existing search data', () => { + const existingSearchData = { + collections: { + [SearchFields.DATAVERSE_NAME]: 'Existing Dataverse Name', + [SearchFields.DATAVERSE_ALIAS]: 'Existing Dataverse Alias', + [SearchFields.DATAVERSE_AFFILIATION]: 'Existing Affiliation', + [SearchFields.DATAVERSE_DESCRIPTION]: 'Existing Description', + [SearchFields.DATAVERSE_SUBJECT]: ['Chemistry'] + }, + datasets: { + title: 'Existing Dataset Title', + authorName: '', + subject: [] + }, + files: { + [SearchFields.FILE_NAME]: 'Existing File Name', + [SearchFields.FILE_DESCRIPTION]: 'Existing File Description', + [SearchFields.FILE_TYPE_SEARCHABLE]: 'PDF', + [SearchFields.FILE_PERSISTENT_ID]: 'pid123', + [SearchFields.VARIABLE_NAME]: 'Variable1', + [SearchFields.VARIABLE_LABEL]: 'Variable Label 1', + [SearchFields.FILE_TAG_SEARCHABLE]: 'Tag1' + } + } + + const result = AdvancedSearchHelper.getFormDefaultValues( + testMetadataBlocks, + existingSearchData + ) + + expect(result.collections[SearchFields.DATAVERSE_NAME]).to.equal('Existing Dataverse Name') + expect(result.collections[SearchFields.DATAVERSE_ALIAS]).to.equal('Existing Dataverse Alias') + expect(result.collections[SearchFields.DATAVERSE_AFFILIATION]).to.equal( + 'Existing Affiliation' + ) + expect(result.collections[SearchFields.DATAVERSE_DESCRIPTION]).to.equal( + 'Existing Description' + ) + expect(result.collections[SearchFields.DATAVERSE_SUBJECT]).to.deep.equal(['Chemistry']) + expect(result.datasets.title).to.equal('Existing Dataset Title') + expect(result.datasets.authorName).to.equal('') + expect(result.datasets.subject).to.deep.equal([]) + expect(result.files[SearchFields.FILE_NAME]).to.equal('Existing File Name') + expect(result.files[SearchFields.FILE_DESCRIPTION]).to.equal('Existing File Description') + expect(result.files[SearchFields.FILE_TYPE_SEARCHABLE]).to.equal('PDF') + expect(result.files[SearchFields.FILE_PERSISTENT_ID]).to.equal('pid123') + expect(result.files[SearchFields.VARIABLE_NAME]).to.equal('Variable1') + expect(result.files[SearchFields.VARIABLE_LABEL]).to.equal('Variable Label 1') + expect(result.files[SearchFields.FILE_TAG_SEARCHABLE]).to.equal('Tag1') + }) + }) + + describe('constructSearchQuery', () => { + it('should construct a search query from form data', () => { + const formData: AdvancedSearchFormData = { + collections: { + [SearchFields.DATAVERSE_NAME]: 'Test Dataverse', + [SearchFields.DATAVERSE_ALIAS]: 'test-dataverse', + [SearchFields.DATAVERSE_AFFILIATION]: 'Test Affiliation', + [SearchFields.DATAVERSE_DESCRIPTION]: 'Test Description', + [SearchFields.DATAVERSE_SUBJECT]: ['Chemistry', 'Law'] + }, + datasets: { + title: 'Foo', + authorName: 'Bar', + subject: ['Physics', 'Engineering'] + }, + files: { + [SearchFields.FILE_NAME]: 'Test File Name', + [SearchFields.FILE_DESCRIPTION]: 'Test File Description', + [SearchFields.FILE_TYPE_SEARCHABLE]: ' ', + [SearchFields.FILE_PERSISTENT_ID]: 'pid123', + [SearchFields.VARIABLE_NAME]: 'Variable1', + [SearchFields.VARIABLE_LABEL]: 'Variable Label 1', + [SearchFields.FILE_TAG_SEARCHABLE]: 'Tag1' + } + } + + const query = AdvancedSearchHelper.constructSearchQuery(formData) + // Collections fields + expect(query).to.include( + `(${SearchFields.DATAVERSE_NAME}:Test ${SearchFields.DATAVERSE_NAME}:Dataverse)` + ) + expect(query).to.include(`${SearchFields.DATAVERSE_ALIAS}:test-dataverse`) + expect(query).to.include( + `(${SearchFields.DATAVERSE_AFFILIATION}:Test ${SearchFields.DATAVERSE_AFFILIATION}:Affiliation)` + ) + expect(query).to.include( + `(${SearchFields.DATAVERSE_DESCRIPTION}:Test ${SearchFields.DATAVERSE_DESCRIPTION}:Description)` + ) + expect(query).to.include( + `(${SearchFields.DATAVERSE_SUBJECT}:"Chemistry" OR ${SearchFields.DATAVERSE_SUBJECT}:"Law")` + ) + + // Datasets fields + expect(query).to.include('title:Foo') + expect(query).to.include('authorName:Bar') + expect(query).to.include(`(subject:"Physics" OR subject:"Engineering")`) + + // Files fields + expect(query).to.include( + `(${SearchFields.FILE_NAME}:Test ${SearchFields.FILE_NAME}:File ${SearchFields.FILE_NAME}:Name)` + ) + expect(query).to.include( + `(${SearchFields.FILE_DESCRIPTION}:Test ${SearchFields.FILE_DESCRIPTION}:File ${SearchFields.FILE_DESCRIPTION}:Description)` + ) + expect(query).not.to.include(`${SearchFields.FILE_TYPE_SEARCHABLE}`) + expect(query).to.include(`${SearchFields.FILE_PERSISTENT_ID}:pid123`) + expect(query).to.include(`${SearchFields.VARIABLE_NAME}:Variable1`) + expect(query).to.include( + `(${SearchFields.VARIABLE_LABEL}:Variable ${SearchFields.VARIABLE_LABEL}:Label ${SearchFields.VARIABLE_LABEL}:1)` + ) + expect(query).to.include(`${SearchFields.FILE_TAG_SEARCHABLE}:Tag1`) + }) + }) +}) From 7d80db7d5358dec2bb258df2938a7d23653cac4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Sun, 13 Jul 2025 22:23:38 -0300 Subject: [PATCH 18/24] refactor: remove current search from link --- .../CollectionItemsPanel.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx index 4d89f1c7b..d01231acf 100644 --- a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx +++ b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { Stack } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' @@ -306,14 +306,6 @@ export const CollectionItemsPanel = ({ setIsLoading(isLoadingItems) }, [isLoadingItems, setIsLoading]) - const advancedSearchLinkURL: string = useMemo(() => { - const searchParams = new URLSearchParams() - if (currentSearchCriteria.searchText) { - searchParams.set(CollectionItemsQueryParams.QUERY, currentSearchCriteria.searchText) - } - return `${RouteWithParams.ADVANCED_SEARCH(collectionId)}?${searchParams.toString()}` - }, [collectionId, currentSearchCriteria.searchText]) - return (
@@ -324,7 +316,9 @@ export const CollectionItemsPanel = ({ placeholderText={t('searchThisCollectionPlaceholder')} /> - + {t('advancedSearch')} From 5c37342ea5e8f9197c7989e5b6fff193be4ac661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 14 Jul 2025 15:24:37 -0300 Subject: [PATCH 19/24] feat: add story --- .../AdvancedSearch.stories.tsx | 29 +++++++++++++++++++ .../domain/models/MetadataBlockInfoMother.ts | 8 ++--- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/stories/advanced-search/AdvancedSearch.stories.tsx diff --git a/src/stories/advanced-search/AdvancedSearch.stories.tsx b/src/stories/advanced-search/AdvancedSearch.stories.tsx new file mode 100644 index 000000000..a911222a9 --- /dev/null +++ b/src/stories/advanced-search/AdvancedSearch.stories.tsx @@ -0,0 +1,29 @@ +import { AdvancedSearch } from '@/sections/advanced-search/AdvancedSearch' +import { Meta, StoryObj } from '@storybook/react' +import { WithI18next } from '../WithI18next' +import { WithLayout } from '../WithLayout' +import { CollectionMockRepository } from '../collection/CollectionMockRepository' +import { MetadataBlockInfoMockRepository } from '../shared-mock-repositories/metadata-block-info/MetadataBlockInfoMockRepository' + +const meta: Meta = { + title: 'Pages/Advanced Search', + component: AdvancedSearch, + decorators: [WithI18next, WithLayout], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + ) +} diff --git a/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts b/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts index 39f007c98..4ac184b58 100644 --- a/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts +++ b/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts @@ -711,7 +711,7 @@ export class MetadataBlockInfoMother { displayOrder: 0, typeClass: 'primitive', displayOnCreate: true, - isAdvancedSearchFieldType: false + isAdvancedSearchFieldType: true }, subtitle: { name: 'subtitle', @@ -846,7 +846,7 @@ export class MetadataBlockInfoMother { displayOrder: 8, typeClass: 'primitive', displayOnCreate: true, - isAdvancedSearchFieldType: false + isAdvancedSearchFieldType: true }, authorAffiliation: { name: 'authorAffiliation', @@ -863,7 +863,7 @@ export class MetadataBlockInfoMother { displayOrder: 9, typeClass: 'primitive', displayOnCreate: true, - isAdvancedSearchFieldType: false + isAdvancedSearchFieldType: true }, authorIdentifierScheme: { name: 'authorIdentifierScheme', @@ -1045,7 +1045,7 @@ export class MetadataBlockInfoMother { displayOrder: 19, typeClass: 'controlledVocabulary', displayOnCreate: true, - isAdvancedSearchFieldType: false + isAdvancedSearchFieldType: true }, keyword: { name: 'keyword', From e2a64dcac2b50b36593dcc7b7eaf1ca071b508cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 16 Jul 2025 08:50:24 -0300 Subject: [PATCH 20/24] feat: keep filter queries when performing advanced search --- src/router/routes.tsx | 4 ++-- .../advanced-search/AdvancedSearch.tsx | 5 ++++- .../advanced-search/AdvancedSearchFactory.tsx | 7 +++++- .../AdvancedSearchForm.tsx | 8 ++++++- .../CollectionItemsPanel.tsx | 22 +++++++++++++++---- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/router/routes.tsx b/src/router/routes.tsx index f6644d136..7331ef96f 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -117,7 +117,7 @@ const SignUpPage = lazy(() => })) ) -const AdvancedSearch = lazy(() => +const AdvancedSearchPage = lazy(() => import('../sections/advanced-search/AdvancedSearchFactory').then(({ AdvancedSearchFactory }) => ({ default: () => AdvancedSearchFactory.create() })) @@ -190,7 +190,7 @@ export const routes: RouteObject[] = [ path: Route.ADVANCED_SEARCH, element: ( }> - + ), errorElement: diff --git a/src/sections/advanced-search/AdvancedSearch.tsx b/src/sections/advanced-search/AdvancedSearch.tsx index 4ab655a76..27c35dbd1 100644 --- a/src/sections/advanced-search/AdvancedSearch.tsx +++ b/src/sections/advanced-search/AdvancedSearch.tsx @@ -21,12 +21,14 @@ interface AdvancedSearchProps { collectionId: string collectionRepository: CollectionRepository metadataBlockInfoRepository: MetadataBlockInfoRepository + collectionFilterQueries?: string } export const AdvancedSearch = ({ collectionId, collectionRepository, - metadataBlockInfoRepository + metadataBlockInfoRepository, + collectionFilterQueries }: AdvancedSearchProps) => { const { t } = useTranslation('advancedSearch') const { setIsLoading } = useLoading() @@ -108,6 +110,7 @@ export const AdvancedSearch = ({ collectionId={collectionId} formDefaultValues={formDefaultValues} metadataBlocks={normalizedMetadataBlocksInfo} + collectionFilterQueries={collectionFilterQueries} />
) diff --git a/src/sections/advanced-search/AdvancedSearchFactory.tsx b/src/sections/advanced-search/AdvancedSearchFactory.tsx index 4109ed84b..3aae7eb34 100644 --- a/src/sections/advanced-search/AdvancedSearchFactory.tsx +++ b/src/sections/advanced-search/AdvancedSearchFactory.tsx @@ -1,7 +1,8 @@ import { ReactElement } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' import { MetadataBlockInfoJSDataverseRepository } from '@/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' +import { CollectionItemsQueryParams } from '@/collection/domain/models/CollectionItemsQueryParams' import { AdvancedSearch } from './AdvancedSearch' const collectionRepository = new CollectionJSDataverseRepository() @@ -17,12 +18,16 @@ function AdvancedSearchWithSearchParams() { const { collectionId } = useParams<{ collectionId: string }>() as { collectionId: string } + const [searchParams] = useSearchParams() + const collectionPageCurrentFilterQueries = + searchParams.get(CollectionItemsQueryParams.FILTER_QUERIES) ?? undefined return ( ) } diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index d7901daa5..079cef07c 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -21,6 +21,7 @@ interface AdvancedSearchFormProps { collectionId: string formDefaultValues: AdvancedSearchFormData metadataBlocks: MetadataBlockInfo[] + collectionFilterQueries?: string } export interface AdvancedSearchFormData { @@ -52,7 +53,8 @@ export type FilesFields = { export const AdvancedSearchForm = ({ collectionId, formDefaultValues, - metadataBlocks + metadataBlocks, + collectionFilterQueries }: AdvancedSearchFormProps) => { const { t } = useTranslation('shared') const { t: tAdvancedSearch } = useTranslation('advancedSearch') @@ -72,6 +74,10 @@ export const AdvancedSearchForm = ({ CollectionItemsQueryParams.TYPES, [CollectionItemType.COLLECTION, CollectionItemType.DATASET, CollectionItemType.FILE].join(',') ) + // We navigate to the collection page with the previous filter queries if they exist + if (collectionFilterQueries) { + searchParams.set(CollectionItemsQueryParams.FILTER_QUERIES, collectionFilterQueries) + } navigate(`${Route.COLLECTIONS_BASE}/${collectionId}?${searchParams.toString()}`) diff --git a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx index d01231acf..492801de4 100644 --- a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx +++ b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { Stack } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' @@ -306,6 +306,22 @@ export const CollectionItemsPanel = ({ setIsLoading(isLoadingItems) }, [isLoadingItems, setIsLoading]) + const advancedSearchLinkURL: string = useMemo(() => { + const searchParams = new URLSearchParams() + if (currentSearchCriteria.filterQueries && currentSearchCriteria.filterQueries.length > 0) { + const filterQueriesWithFacetValueEncoded = currentSearchCriteria.filterQueries.map((fq) => { + const [facetName, facetValue] = fq.split(':') + return `${facetName}:${encodeURIComponent(facetValue)}` + }) + + searchParams.set( + CollectionItemsQueryParams.FILTER_QUERIES, + filterQueriesWithFacetValueEncoded.join(',') + ) + } + return `${RouteWithParams.ADVANCED_SEARCH(collectionId)}?${searchParams.toString()}` + }, [collectionId, currentSearchCriteria.filterQueries]) + return (
@@ -316,9 +332,7 @@ export const CollectionItemsPanel = ({ placeholderText={t('searchThisCollectionPlaceholder')} /> - + {t('advancedSearch')} From 11316d5decd0c6bb580cf9820ff97582f3c1106e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 16 Jul 2025 09:04:14 -0300 Subject: [PATCH 21/24] feat: show search if more than 10 controlled vocab values --- .../advanced-search-form/MetadataBlockSearchFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx b/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx index 3b2d3cab8..eea844e29 100644 --- a/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx +++ b/src/sections/advanced-search/advanced-search-form/MetadataBlockSearchFields.tsx @@ -99,7 +99,7 @@ const VocabularyMultipleField = ({ fieldInfo }: VocabularyMultipleFieldProps) => defaultValue={value as string[]} options={fieldInfo.controlledVocabularyValues as string[]} isMultiple={true} - isSearchable={true} + isSearchable={(fieldInfo.controlledVocabularyValues as string[])?.length > 10} onChange={onChange} isInvalid={invalid} ref={ref} From 99b3e1e9c2af3a68a137b984f3b5c383a66a197a Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 22 Jul 2025 13:58:17 -0400 Subject: [PATCH 22/24] t submit AdvancedSearchForm (to increase code coverage) --- .../advanced-search-form/AdvancedSearchForm.tsx | 2 +- .../advanced-search/AdvancedSearch.spec.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 079cef07c..194a2062e 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -156,7 +156,7 @@ export const AdvancedSearchForm = ({ - diff --git a/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx b/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx index 5329e8fe1..3eb1f0b7c 100644 --- a/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx +++ b/tests/component/sections/advanced-search/AdvancedSearch.spec.tsx @@ -149,4 +149,19 @@ describe('AdvancedSearch', () => { cy.findByRole('alert').should('be.visible').and('contain.text', errorMessage) }) + it('should submit the form ', () => { + cy.customMount( + + ) + cy.findByTestId('advanced-search-metadata-block-citation') + .should('be.visible') + .within(() => { + cy.findByLabelText('Title').should('be.visible').type('Test Dataset Title') + }) + cy.findByTestId('submit-button').click() + }) }) From 9b139d72d97f5d63d19348b401edc723e1d039b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 14 Aug 2025 08:36:04 -0300 Subject: [PATCH 23/24] fix: import metadata block name enum from dataset model --- .../advanced-search-form/AdvancedSearchForm.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx index 194a2062e..39ce3bbcf 100644 --- a/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx +++ b/src/sections/advanced-search/advanced-search-form/AdvancedSearchForm.tsx @@ -4,11 +4,9 @@ import { useNavigate } from 'react-router-dom' import { FormProvider, useForm } from 'react-hook-form' import { ArrowClockwise } from 'react-bootstrap-icons' import { Accordion, Button } from '@iqss/dataverse-design-system' -import { - MetadataBlockInfo, - MetadataBlockName -} from '@/metadata-block-info/domain/models/MetadataBlockInfo' +import { MetadataBlockInfo } from '@/metadata-block-info/domain/models/MetadataBlockInfo' import { SearchFields } from '@/search/domain/models/SearchFields' +import { MetadataBlockName } from '@/dataset/domain/models/Dataset' import { CollectionItemsQueryParams } from '@/collection/domain/models/CollectionItemsQueryParams' import { Route } from '@/sections/Route.enum' import { CollectionItemType } from '@/collection/domain/models/CollectionItemType' From 1cead0b6982d15f8863e849ae0ea959446ee1fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 20 Aug 2025 11:24:33 -0300 Subject: [PATCH 24/24] test: avoid fetching external image and use fixture image --- .../e2e/sections/dataset/Dataset.spec.tsx | 3 +- .../fixtures/images/dog-640x480.jpg | Bin 0 -> 49907 bytes .../files/FileJSDataverseRepository.spec.ts | 34 +++++++++++------- .../shared/files/FileHelper.ts | 27 +++++--------- 4 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 tests/e2e-integration/fixtures/images/dog-640x480.jpg diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index 97ef89241..307713abc 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -682,7 +682,8 @@ describe('Dataset', () => { }) it('shows the thumbnail for a file', () => { - cy.wrap(FileHelper.createImage().then((file) => DatasetHelper.createWithFiles([file]))) + FileHelper.createImage() + .then((fileData) => cy.wrap(DatasetHelper.createWithFiles([fileData]))) .its('persistentId') .then((persistentId: string) => { cy.visit(`/spa/datasets?persistentId=${persistentId}&version=${DRAFT_PARAM}`) diff --git a/tests/e2e-integration/fixtures/images/dog-640x480.jpg b/tests/e2e-integration/fixtures/images/dog-640x480.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e76f701708d69f2b421cf7e9d1d1984010f827e GIT binary patch literal 49907 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 K$A`)PZvp^n3{Oe` literal 0 HcmV?d00001 diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index 6c84bf89e..b9124e709 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -365,22 +365,30 @@ describe('File JSDataverse Repository', () => { }) }) - it('gets all the files by dataset persistentId after adding a thumbnail to the files', async () => { - const datasetResponse = await FileHelper.createImage().then((file) => - DatasetHelper.createWithFiles([file]) - ) - if (!datasetResponse.files) throw new Error('Files not found') + it('gets all the files by dataset persistentId after adding a thumbnail to the files', () => { + FileHelper.createImage() + .then((fileData) => cy.wrap(DatasetHelper.createWithFiles([fileData]))) + .then((datasetResponse) => { + expect(datasetResponse.files).to.exist + + return cy.wrap( + datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + ) + }) + .then((dataset) => { + expect(dataset).to.exist - const dataset = await datasetRepository.getByPersistentId( - datasetResponse.persistentId, - DatasetNonNumericVersion.DRAFT - ) - if (!dataset) throw new Error('Dataset not found') + if (!dataset) throw new Error('Dataset not found') - await fileRepository - .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) + return cy.wrap( + fileRepository.getAllByDatasetPersistentId(dataset.persistentId, dataset.version) + ) + }) .then((files) => { - expect(files[0].metadata.thumbnail).to.not.be.undefined + expect(files?.[0]?.metadata?.thumbnail).to.not.be.undefined }) }) diff --git a/tests/e2e-integration/shared/files/FileHelper.ts b/tests/e2e-integration/shared/files/FileHelper.ts index 44b75abf9..3dd1965ed 100644 --- a/tests/e2e-integration/shared/files/FileHelper.ts +++ b/tests/e2e-integration/shared/files/FileHelper.ts @@ -59,15 +59,11 @@ export class FileHelper extends DataverseApiHelper { } } - static async createImage(): Promise { - const fileBinary = await this.generateImgData() - if (!fileBinary) { - throw new Error('File could not be fetched') - } - return { - file: fileBinary, + static createImage(): Cypress.Chainable { + return this.generateImgData().then((blob) => ({ + file: blob, jsonData: JSON.stringify({ description: 'This is an example file' }) - } + })) } static generateCsvData(): string { @@ -94,17 +90,10 @@ export class FileHelper extends DataverseApiHelper { return faker.lorem.sentence() } - static async generateImgData(): Promise { - return await fetch('https://picsum.photos/id/237/200') - .then((response) => { - if (!response.ok) { - throw new Error('Network response was not ok') - } - return response.blob() - }) - .catch(() => { - throw new Error('Files could not be fetched') - }) + static generateImgData(): Cypress.Chainable { + return cy + .fixture('images/dog-640x480.jpg', 'binary') + .then((binary: string) => Cypress.Blob.binaryStringToBlob(binary, 'image/jpeg')) } static async download(id: number) {