Skip to content

Commit

Permalink
Merge pull request #566 from IQSS/feat/562-add-collection-facets
Browse files Browse the repository at this point in the history
Collection Page Facets Filters
  • Loading branch information
ofahimIQSS authored Jan 3, 2025
2 parents d1b4cc3 + 50a172a commit 9170126
Show file tree
Hide file tree
Showing 35 changed files with 1,002 additions and 125 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"@faker-js/faker": "7.6.0",
"@iqss/dataverse-client-javascript": "2.0.0-alpha.10",
"@iqss/dataverse-client-javascript": "2.0.0-alpha.11",
"@iqss/dataverse-design-system": "*",
"@istanbuljs/nyc-config-typescript": "1.0.2",
"@tanstack/react-table": "8.9.2",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/en/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"error": "There was an error publishing your collection."
},
"publishedAlert": "Your collection is now public.",
"addFacetFilter": "Add {{labelName}} facet filter",
"removeSelectedFacet": "Remove {{labelName}} facet filter",
"share": {
"shareCollection": "Share Collection",
"helpText": "Share this collection on your favorite social media networks."
Expand Down
2 changes: 2 additions & 0 deletions public/locales/en/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"cancel": "Cancel",
"close": "Close",
"continue": "Continue",
"more": "More...",
"less": "Less...",
"share": "Share",
"pageNumberNotFound": {
"heading": "Page Number Not Found",
Expand Down
12 changes: 12 additions & 0 deletions src/collection/domain/models/CollectionItemSubset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@ import { FileItemTypePreview } from '../../../files/domain/models/FileItemTypePr

export interface CollectionItemSubset {
items: CollectionItem[]
facets: CollectionItemsFacet[]
totalItemCount: number
}

export type CollectionItem =
| CollectionItemTypePreview
| DatasetItemTypePreview
| FileItemTypePreview

export interface CollectionItemsFacet {
name: string
friendlyName: string
labels: CollectionItemsFacetLabel[]
}

interface CollectionItemsFacetLabel {
name: string
count: number
}
8 changes: 8 additions & 0 deletions src/collection/domain/models/CollectionItemsQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum CollectionItemsQueryParams {
SORT = 'sort',
ORDER = 'order',
START = 'start',
TYPES = 'types',
QUERY = 'q',
FILTER_QUERIES = 'fqs'
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class CollectionJSDataverseRepository implements CollectionRepository {

return {
items: collectionItemsPreviewsMapped,
facets: jsCollectionItemSubset.facets,
totalItemCount: jsCollectionItemSubset.totalItemCount
}
})
Expand Down
2 changes: 0 additions & 2 deletions src/sections/Route.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export const RouteWithParams = {
export enum QueryParamKey {
VERSION = 'version',
PERSISTENT_ID = 'persistentId',
QUERY = 'q',
COLLECTION_ITEM_TYPES = 'types',
PAGE = 'page',
COLLECTION_ID = 'collectionId'
}
22 changes: 16 additions & 6 deletions src/sections/collection/CollectionHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Collection } from '@/collection/domain/models/Collection'
import { CollectionItemsQueryParams } from '@/collection/domain/models/CollectionItemsQueryParams'
import { FilterQuery } from '@/collection/domain/models/CollectionSearchCriteria'
import { CollectionItemType } from '@/collection/domain/models/CollectionItemType'
import { QueryParamKey } from '../Route.enum'
import { Collection } from '@/collection/domain/models/Collection'
import { UpwardHierarchyNode } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode'

export class CollectionHelper {
Expand All @@ -9,11 +10,11 @@ export class CollectionHelper {
? parseInt(searchParams.get('page') as string, 10)
: 1

const searchQuery = searchParams.get(QueryParamKey.QUERY)
? decodeURIComponent(searchParams.get(QueryParamKey.QUERY) as string)
const searchQuery = searchParams.get(CollectionItemsQueryParams.QUERY)
? decodeURIComponent(searchParams.get(CollectionItemsQueryParams.QUERY) as string)
: undefined

const typesParam = searchParams.get(QueryParamKey.COLLECTION_ITEM_TYPES) ?? undefined
const typesParam = searchParams.get(CollectionItemsQueryParams.TYPES) ?? undefined

const typesQuery = typesParam
?.split(',')
Expand All @@ -22,7 +23,16 @@ export class CollectionHelper {
Object.values(CollectionItemType).includes(type as CollectionItemType)
) as CollectionItemType[]

return { pageQuery, searchQuery, typesQuery }
const filtersParam = searchParams.get(CollectionItemsQueryParams.FILTER_QUERIES) ?? undefined

const filtersQuery: FilterQuery[] | undefined = filtersParam
? (filtersParam
?.split(',')
.map((filterQuery) => decodeURIComponent(filterQuery))
.filter((decodedFilter) => /^[^:]+:[^:]+$/.test(decodedFilter)) as FilterQuery[])
: undefined

return { pageQuery, searchQuery, typesQuery, filtersQuery }
}

static isRootCollection(collectionHierarchy: Collection['hierarchy']) {
Expand Down
165 changes: 125 additions & 40 deletions src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { useEffect, useRef, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Stack } from '@iqss/dataverse-design-system'
import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository'
import { CollectionItemsPaginationInfo } from '@/collection/domain/models/CollectionItemsPaginationInfo'
import { CollectionSearchCriteria } from '@/collection/domain/models/CollectionSearchCriteria'
import {
CollectionSearchCriteria,
FilterQuery
} from '@/collection/domain/models/CollectionSearchCriteria'
import { CollectionItemType } from '@/collection/domain/models/CollectionItemType'
import { CollectionItemsQueryParams } from '@/collection/domain/models/CollectionItemsQueryParams'
import { useGetAccumulatedItems } from './useGetAccumulatedItems'
import { UseCollectionQueryParamsReturnType } from '../useGetCollectionQueryParams'
import { useLoadMoreOnPopStateEvent } from './useLoadMoreOnPopStateEvent'
import { useLoading } from '@/sections/loading/LoadingContext'
import { QueryParamKey } from '../../Route.enum'
import { CollectionHelper } from '../CollectionHelper'
import { FilterPanel } from './filter-panel/FilterPanel'
import { ItemsList } from './items-list/ItemsList'
import { SearchPanel } from './search-panel/SearchPanel'
import { ItemTypeChange } from './filter-panel/type-filters/TypeFilters'
import { RemoveAddFacetFilter } from './filter-panel/facets-filters/FacetFilterGroup'
import { SelectedFacets } from './selected-facets/SelectedFacets'
import styles from './CollectionItemsPanel.module.scss'

interface CollectionItemsPanelProps {
Expand All @@ -30,7 +36,8 @@ interface CollectionItemsPanelProps {
* 2. When the user scrolls to the bottom of the list and there are more items to load
* 3. When the user submits a search query in the search panel
* 4. When the user changes the item types in the filter panel
* 5. When the user navigates back and forward in the browser
* 5. When the user selects or removes a facet filter
* 6. When the user navigates back and forward in the browser
*
* It initializes the search criteria with the query params in the URL.
* By default if no query params are present in the URL, the search query is empty and the item types are COLLECTION and DATASET.
Expand All @@ -51,7 +58,10 @@ export const CollectionItemsPanel = ({
// This object will update every time we update a query param in the URL with the setSearchParams setter
const currentSearchCriteria = new CollectionSearchCriteria(
collectionQueryParams.searchQuery,
collectionQueryParams.typesQuery || [CollectionItemType.COLLECTION, CollectionItemType.DATASET]
collectionQueryParams.typesQuery || [CollectionItemType.COLLECTION, CollectionItemType.DATASET],
undefined,
undefined,
collectionQueryParams.filtersQuery
)

const [paginationInfo, setPaginationInfo] = useState<CollectionItemsPaginationInfo>(
Expand All @@ -62,6 +72,7 @@ export const CollectionItemsPanel = ({
const {
isLoadingItems,
accumulatedItems,
facets,
totalAvailable,
hasNextPage,
error,
Expand Down Expand Up @@ -97,20 +108,23 @@ export const CollectionItemsPanel = ({
if (searchValue === '') {
// Update the URL without the search value, keep other querys
setSearchParams((currentSearchParams) => {
currentSearchParams.delete(QueryParamKey.QUERY)
currentSearchParams.delete(CollectionItemsQueryParams.QUERY)
return currentSearchParams
})
} else {
// Update the URL with the search value ,keep other querys and include all item types always
setSearchParams((currentSearchParams) => ({
...currentSearchParams,
[QueryParamKey.COLLECTION_ITEM_TYPES]: [
CollectionItemType.COLLECTION,
CollectionItemType.DATASET,
CollectionItemType.FILE
].join(','),
[QueryParamKey.QUERY]: searchValue
}))
setSearchParams((currentSearchParams) => {
currentSearchParams.set(
CollectionItemsQueryParams.TYPES,
[CollectionItemType.COLLECTION, CollectionItemType.DATASET, CollectionItemType.FILE].join(
','
)
)

currentSearchParams.set(CollectionItemsQueryParams.QUERY, searchValue)

return currentSearchParams
})
}

// WHEN SEARCHING, WE RESET THE PAGINATION INFO AND KEEP ALL ITEM TYPES!!
Expand All @@ -137,24 +151,73 @@ export const CollectionItemsPanel = ({
(itemType) => itemType !== type
)

// KEEP SEARCH VALUE IF EXISTS
itemsListContainerRef.current?.scrollTo({ top: 0 })

const resetPaginationInfo = new CollectionItemsPaginationInfo()
setPaginationInfo(resetPaginationInfo)

// Update the URL with the new item types, keep other querys and include the search value if exists
setSearchParams((currentSearchParams) => ({
...currentSearchParams,
[QueryParamKey.COLLECTION_ITEM_TYPES]: newItemsTypes.join(','),
...(currentSearchCriteria.searchText && {
[QueryParamKey.QUERY]: currentSearchCriteria.searchText
})
}))
// Update the URL with the new item types, keep other querys
setSearchParams((currentSearchParams) => {
currentSearchParams.set(CollectionItemsQueryParams.TYPES, newItemsTypes.join(','))

return currentSearchParams
})

const newCollectionSearchCriteria = new CollectionSearchCriteria(
currentSearchCriteria.searchText,
newItemsTypes
newItemsTypes,
undefined,
undefined,
currentSearchCriteria.filterQueries
)

const totalItemsCount = await loadMore(resetPaginationInfo, newCollectionSearchCriteria, true)

if (totalItemsCount !== undefined) {
const paginationInfoUpdated = resetPaginationInfo.withTotal(totalItemsCount)
setPaginationInfo(paginationInfoUpdated)
}
}

const handleFacetChange = async (filterQuery: FilterQuery, removeOrAdd: RemoveAddFacetFilter) => {
const newFilterQueries =
removeOrAdd === RemoveAddFacetFilter.ADD
? [
...new Set([
...(currentSearchCriteria?.filterQueries ?? /* istanbul ignore next */ []),
filterQuery
])
]
: (currentSearchCriteria.filterQueries ?? /* istanbul ignore next */ []).filter(
(fQuery) => fQuery !== filterQuery
)

itemsListContainerRef.current?.scrollTo({ top: 0 })

const resetPaginationInfo = new CollectionItemsPaginationInfo()
setPaginationInfo(resetPaginationInfo)

const newFilterQueriesWithFacetValueEncoded = newFilterQueries.map((fq) => {
const [facetName, facetValue] = fq.split(':')
return `${facetName}:${encodeURIComponent(facetValue)}`
})

// Update the URL with the new facets, keep other querys and include the search value if exists
setSearchParams((currentSearchParams) => {
currentSearchParams.set(
CollectionItemsQueryParams.FILTER_QUERIES,
newFilterQueriesWithFacetValueEncoded.join(',')
)

return currentSearchParams
})

const newCollectionSearchCriteria = new CollectionSearchCriteria(
currentSearchCriteria.searchText,
currentSearchCriteria.itemTypes,
undefined,
undefined,
newFilterQueries
)

const totalItemsCount = await loadMore(resetPaginationInfo, newCollectionSearchCriteria, true)
Expand All @@ -171,7 +234,10 @@ export const CollectionItemsPanel = ({

const newCollectionSearchCriteria = new CollectionSearchCriteria(
collectionQueryParams.searchQuery,
collectionQueryParams.typesQuery
collectionQueryParams.typesQuery,
undefined,
undefined,
collectionQueryParams.filtersQuery
)

const newPaginationInfo = new CollectionItemsPaginationInfo()
Expand All @@ -183,6 +249,9 @@ export const CollectionItemsPanel = ({
}
}

const showSelectedFacets =
currentSearchCriteria.filterQueries && currentSearchCriteria.filterQueries.length > 0

useEffect(() => {
setIsLoading(isLoadingItems)
}, [isLoadingItems, setIsLoading])
Expand All @@ -202,24 +271,40 @@ export const CollectionItemsPanel = ({
<FilterPanel
currentItemTypes={currentSearchCriteria.itemTypes}
onItemTypesChange={handleItemsTypeChange}
currentFilterQueries={currentSearchCriteria.filterQueries}
facets={facets}
onFacetChange={handleFacetChange}
isLoadingCollectionItems={isLoadingItems}
/>

<ItemsList
parentCollectionAlias={collectionId}
items={accumulatedItems}
error={error}
accumulatedCount={accumulatedCount}
isLoadingItems={isLoadingItems}
areItemsAvailable={areItemsAvailable}
hasNextPage={hasNextPage}
isEmptyItems={isEmptyItems}
hasSearchValue={currentSearchCriteria.hasSearchText()}
itemsTypesSelected={currentSearchCriteria.itemTypes as CollectionItemType[]}
paginationInfo={paginationInfo}
onBottomReach={handleLoadMoreOnBottomReach}
ref={itemsListContainerRef}
/>
<Stack direction="vertical" gap={2}>
{showSelectedFacets && facets.length > 0 && (
<SelectedFacets
onRemoveFacet={(filterQuery: FilterQuery) =>
handleFacetChange(filterQuery, RemoveAddFacetFilter.REMOVE)
}
selectedFilterQueries={currentSearchCriteria.filterQueries}
isLoadingCollectionItems={isLoadingItems}
/>
)}

<ItemsList
parentCollectionAlias={collectionId}
items={accumulatedItems}
error={error}
accumulatedCount={accumulatedCount}
isLoadingItems={isLoadingItems}
areItemsAvailable={areItemsAvailable}
hasNextPage={hasNextPage}
isEmptyItems={isEmptyItems}
hasSearchValue={currentSearchCriteria.hasSearchText()}
itemsTypesSelected={currentSearchCriteria.itemTypes as CollectionItemType[]}
filterQueriesSelected={currentSearchCriteria.filterQueries ?? []}
paginationInfo={paginationInfo}
onBottomReach={handleLoadMoreOnBottomReach}
ref={itemsListContainerRef}
/>
</Stack>
</div>
</section>
)
Expand Down
Loading

0 comments on commit 9170126

Please sign in to comment.