diff --git a/.env b/.env index 9b9f3f8a0..31d08b58e 100644 --- a/.env +++ b/.env @@ -11,3 +11,5 @@ API_STAC_ENDPOINT='https://staging-stac.delta-backend.com' # Google form for feedback GOOGLE_FORM = 'https://docs.google.com/forms/d/e/1FAIpQLSfGcd3FDsM3kQIOVKjzdPn4f88hX8RZ4Qef7qBsTtDqxjTSkg/viewform?embedded=true' + +FEATURE_NEW_EXPLORATION = 'TRUE' diff --git a/app/scripts/components/common/browse-controls/constants.js b/app/scripts/components/common/browse-controls/constants.js new file mode 100644 index 000000000..87818a2fb --- /dev/null +++ b/app/scripts/components/common/browse-controls/constants.js @@ -0,0 +1,6 @@ +export const optionAll = { + id: 'all', + name: 'All' +}; + +export const minSearchLength = 3; diff --git a/app/scripts/components/common/browse-controls/index.tsx b/app/scripts/components/common/browse-controls/index.tsx index 18fb10269..96293e961 100644 --- a/app/scripts/components/common/browse-controls/index.tsx +++ b/app/scripts/components/common/browse-controls/index.tsx @@ -10,12 +10,9 @@ import { import { glsp, truncated } from '@devseed-ui/theme-provider'; import { DropMenu, DropTitle } from '@devseed-ui/dropdown'; -import { - Actions, - FilterOption, - optionAll, - useBrowserControls -} from './use-browse-controls'; +import { useFiltersWithQS } from '../catalog/controls/hooks/use-filters-with-query'; +import { optionAll } from './constants'; +import { FilterActions } from '$components/common/catalog/utils'; import DropdownScrollable from '$components/common/dropdown-scrollable'; import DropMenuItemButton from '$styles/drop-menu-item-button'; @@ -23,6 +20,11 @@ import { variableGlsp } from '$styles/variable-utils'; import SearchField from '$components/common/search-field'; import { useMediaQuery } from '$utils/use-media-query'; +export interface FilterOption { + id: string; + name: string; +} + const BrowseControlsWrapper = styled.div` display: flex; flex-flow: column; @@ -67,7 +69,7 @@ const ButtonPrefix = styled(Overline).attrs({ as: 'small' })` white-space: nowrap; `; -interface BrowseControlsProps extends ReturnType { +interface BrowseControlsProps extends ReturnType { taxonomiesOptions: Taxonomy[]; } @@ -84,20 +86,21 @@ function BrowseControls(props: BrowseControlsProps) { const filterWrapConstant = 4; const wrapTaxonomies = taxonomiesOptions.length > filterWrapConstant; // wrap list of taxonomies when more then 4 filter options - const createFilterList = (filterList: Taxonomy[]) => ( - filterList.map(({ name, values }) => ( + + const createFilterList = (filterList: Taxonomy[]) => { + return filterList.map(({ name, values }) => ( { - onAction(Actions.TAXONOMY, { key: name, value: v }); + onAction(FilterActions.TAXONOMY, { key: name, value: v }); }} size={isLargeUp ? 'large' : 'medium'} /> - )) - ); + )); + }; return ( @@ -106,8 +109,8 @@ function BrowseControls(props: BrowseControlsProps) { size={isLargeUp ? 'large' : 'medium'} placeholder='Title, description...' keepOpen={isLargeUp} - value={search ?? ''} - onChange={(v) => onAction(Actions.SEARCH, v)} + value={search} + onChange={(v) => onAction(FilterActions.SEARCH, v)} /> {createFilterList(taxonomiesOptions.slice(0, filterWrapConstant))} @@ -140,7 +143,7 @@ function DropdownOptions(props: DropdownOptionsProps) { const { size, items, currentId, onChange, prefix } = props; const currentItem = items.find((d) => d.id === currentId); - + return ( void; - -export interface FilterOption { - id: string; - name: string; -} - -export interface TaxonomyFilterOption { - taxonomyType: string; - value: string; - exclusion?: string; -} - -interface BrowseControlsHookParams { - sortOptions: FilterOption[]; -} - -export const sortDirOptions: FilterOption[] = [ - { - id: 'asc', - name: 'Ascending' - }, - { - id: 'desc', - name: 'Descending' - } -]; - -export const optionAll = { - id: 'all', - name: 'All' -}; - -export const minSearchLength = 3; - -// This hook is only used for the Stories Hub to manage browsing controls -// such as search, sort, and taxonomy filters. -export function useBrowserControls({ sortOptions }: BrowseControlsHookParams) { - // Setup Qs State to store data in the url's query string - // react-router function to get the navigation. - const navigate = useNavigate(); - const useQsState = useQsStateCreator({ - commit: navigate - }); - - const [sortField, setSortField] = useQsState.memo( - { - key: Actions.SORT_FIELD, - // If pubDate exists, default sorting to this - default: - sortOptions.find((o) => o.id === 'pubDate')?.id || sortOptions[0]?.id, - validator: sortOptions.map((d) => d.id) - }, - [sortOptions] - ); - - const [sortDir, setSortDir] = useQsState.memo( - { - key: Actions.SORT_DIR, - default: sortDirOptions[0].id, - validator: sortDirOptions.map((d) => d.id) - }, - [] - ); - - const [search, setSearch] = useQsState.memo( - { - key: Actions.SEARCH, - default: '' - }, - [] - ); - - const [taxonomies, setTaxonomies] = useQsState.memo< - Record - >( - { - key: Actions.TAXONOMY, - default: {}, - dehydrator: (v) => JSON.stringify(v), // dehydrator defines how a value is stored in the url - hydrator: (v) => (v ? JSON.parse(v) : {}) // hydrator defines how a value is read from the url - }, - [] - ); - - const onAction = useCallback( - (action, value) => { - switch (action) { - case Actions.CLEAR: - setSearch(''); - setTaxonomies({}); - break; - case Actions.SEARCH: - setSearch(value); - break; - case Actions.SORT_FIELD: - setSortField(value); - break; - case Actions.SORT_DIR: - setSortDir(value); - break; - case Actions.TAXONOMY: - { - const { key, value: val } = value; - if (val === optionAll.id) { - setTaxonomies(omit(taxonomies, key)); - } else { - setTaxonomies(set({ ...taxonomies }, key, val)); - } - } - break; - } - }, - [setSortField, setSortDir, taxonomies, setTaxonomies, setSearch] - ); - - return { - search, - sortField, - sortDir, - taxonomies, - onAction - }; -} diff --git a/app/scripts/components/common/card-sources.tsx b/app/scripts/components/common/card-sources.tsx index 55151834f..b2d59c9b0 100644 --- a/app/scripts/components/common/card-sources.tsx +++ b/app/scripts/components/common/card-sources.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { TaxonomyItem } from 'veda'; import { Link } from 'react-router-dom'; import { listReset } from '@devseed-ui/theme-provider'; -import { Actions } from '$components/common/browse-controls/use-browse-controls'; +import { FilterActions } from '$components/common//catalog/utils'; const SourcesUl = styled.ul` ${listReset()} @@ -51,7 +51,7 @@ export function CardSourcesList(props: SourcesListProps) { {sources.map((source) => (
  • ; - onAction: (action: CatalogActions, value?: any) => void; + onAction: (action: FilterActions, value?: any) => void; } const DEFAULT_SORT_OPTION = 'asc'; @@ -53,8 +53,6 @@ function CatalogContent({ const [exclusiveSourceSelected, setExclusiveSourceSelected] = useState(null); const isSelectable = selectedIds !== undefined; - const navigate = useNavigate(); - const datasetTaxonomies = generateTaxonomies(datasets); const urlTaxonomyItems = taxonomies ? Object.entries(taxonomies).map(([key, val]) => getTaxonomyByIds(key, val, datasetTaxonomies)).flat() : []; @@ -84,7 +82,7 @@ function CatalogContent({ setSelectedFilters(selectedFilters.filter((selected) => selected.id !== item.id)); } - onAction(CatalogActions.TAXONOMY_MULTISELECT, { key: item.taxonomy, value: item.id }); + onAction(FilterActions.TAXONOMY_MULTISELECT, { key: item.taxonomy, value: item.id }); }, [setSelectedFilters, selectedFilters, onAction]); const handleClearTag = useCallback((item: OptionItem) => { @@ -95,12 +93,12 @@ function CatalogContent({ const handleClearTags = useCallback(() => { setSelectedFilters([]); setExclusiveSourceSelected(null); - onAction(CatalogActions.CLEAR); + onAction(FilterActions.CLEAR_TAXONOMY); }, [onAction]); useEffect(() => { if (clearedTagItem && (selectedFilters.length == prevSelectedFilters.length - 1)) { - onAction(CatalogActions.TAXONOMY_MULTISELECT, { key: clearedTagItem.taxonomy, value: clearedTagItem.id}); + onAction(FilterActions.TAXONOMY_MULTISELECT, { key: clearedTagItem.taxonomy, value: clearedTagItem.id}); setClearedTagItem(undefined); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -108,11 +106,7 @@ function CatalogContent({ useEffect(() => { if (!selectedFilters.length) { - onAction(CatalogActions.CLEAR); - - if (!isSelectable) { - navigate(DATASETS_PATH); - } + onAction(FilterActions.CLEAR_TAXONOMY); } setExclusiveSourceSelected(null); diff --git a/app/scripts/components/common/catalog/controls/hooks/use-catalog-view.ts b/app/scripts/components/common/catalog/controls/hooks/use-catalog-view.ts deleted file mode 100644 index 91515f10c..000000000 --- a/app/scripts/components/common/catalog/controls/hooks/use-catalog-view.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useAtom } from 'jotai'; -import { useCallback } from 'react'; -import { taxonomyAtom } from '../atoms/taxonomy-atom'; -import { searchAtom } from '../atoms/search-atom'; -import { CatalogViewAction, onCatalogAction } from '../../utils'; - -export function useCatalogView() { - const [search, setSearch] = useAtom(searchAtom); - const [taxonomies, setTaxonomies] = useAtom(taxonomyAtom); - - const onAction = useCallback( - (action, value) => - onCatalogAction(action, value, taxonomies, setSearch, setTaxonomies), - [setSearch, setTaxonomies, taxonomies] - ); - - return { - search, - taxonomies, - onAction - }; -} diff --git a/app/scripts/components/common/catalog/controls/hooks/use-filters-with-query.ts b/app/scripts/components/common/catalog/controls/hooks/use-filters-with-query.ts new file mode 100644 index 000000000..46f6ab82d --- /dev/null +++ b/app/scripts/components/common/catalog/controls/hooks/use-filters-with-query.ts @@ -0,0 +1,70 @@ +import { useAtom } from 'jotai'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; +import useQsStateCreator from 'qs-state-hook'; +import { taxonomyAtom } from '../atoms/taxonomy-atom'; +import { searchAtom } from '../atoms/search-atom'; +import { FilterActions, FilterAction, onFilterAction } from '../../utils'; +interface UseFiltersWithQueryResult { + search: string; + taxonomies: Record | Record; + onAction: FilterAction +} + +// @TECH-DEBT: We have diverged ways of dealing with query parameters and need to consolidate them. +// useFiltersWithURLAtom uses URLAtoms, while useFiltersWithQS uses QS for query parameters related to filters. +// For more details on why we ended up with these two approaches, see: https://github.com/NASA-IMPACT/veda-ui/pull/1021 + +export function useFiltersWithURLAtom(): UseFiltersWithQueryResult { + const [search, setSearch] = useAtom(searchAtom); + const [taxonomies, setTaxonomies] = useAtom(taxonomyAtom); + + const onAction = useCallback( + (action, value) => + onFilterAction(action, value, taxonomies, setSearch, setTaxonomies), + [setSearch, setTaxonomies, taxonomies] + ); + + return { + search, + taxonomies, + onAction, + }; +} + +export function useFiltersWithQS(): UseFiltersWithQueryResult { + const navigate = useNavigate(); + const useQsState = useQsStateCreator({ + commit: navigate + }); + + const [search, setSearch] = useQsState.memo( + { + key: FilterActions.SEARCH, + default: '' + }, + [] + ); + + const [taxonomies, setTaxonomies] = useQsState.memo( + { + key: FilterActions.TAXONOMY, + default: {}, + dehydrator: (v) => JSON.stringify(v), // dehydrator defines how a value is stored in the url + hydrator: (v) => (v ? JSON.parse(v) : {}) // hydrator defines how a value is read from the url + }, + [] + ); + + const onAction = useCallback( + (action, value) => + onFilterAction(action, value, taxonomies, setSearch, setTaxonomies), + [setSearch, setTaxonomies, taxonomies] + ); + + return { + search: search?? '', + taxonomies: taxonomies?? {}, + onAction, + }; +} \ No newline at end of file diff --git a/app/scripts/components/common/catalog/filters-control.tsx b/app/scripts/components/common/catalog/filters-control.tsx index db3092b1c..ea9fef452 100644 --- a/app/scripts/components/common/catalog/filters-control.tsx +++ b/app/scripts/components/common/catalog/filters-control.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { Taxonomy } from 'veda'; -import { CatalogActions } from './utils'; +import { FilterActions } from './utils'; import SearchField from '$components/common/search-field'; import CheckableFilters, { OptionItem } from '$components/common/form/checkable-filter'; import { useSlidingStickyHeader, HEADER_TRANSITION_DURATION } from '$utils/use-sliding-sticky-header'; @@ -16,7 +16,7 @@ const ControlsWrapper = styled.div<{ widthValue?: string; heightValue?: string; `; interface FiltersMenuProps { - onAction: (action: CatalogActions, value: any) => void; + onAction: (action: FilterActions, value: any) => void; search?: string; taxonomiesOptions: Taxonomy[]; allSelected: OptionItem[]; @@ -103,7 +103,7 @@ export default function FiltersControl(props: FiltersMenuProps) { size='large' placeholder='Search by title, description' value={search ?? ''} - onChange={(v) => onAction(CatalogActions.SEARCH, v)} + onChange={(v) => onAction(FilterActions.SEARCH, v)} /> {taxonomiesItems.map(({ title, items }) => ( diff --git a/app/scripts/components/common/catalog/prepare-datasets.ts b/app/scripts/components/common/catalog/prepare-datasets.ts index 4d18a1ee0..835fc48cf 100644 --- a/app/scripts/components/common/catalog/prepare-datasets.ts +++ b/app/scripts/components/common/catalog/prepare-datasets.ts @@ -1,22 +1,30 @@ -import { DatasetData } from 'veda'; -import { optionAll } from '$components/common/browse-controls/use-browse-controls'; +import { DatasetData, StoryData } from 'veda'; +import { optionAll } from '$components/common/browse-controls/constants'; import { TAXONOMY_TOPICS } from '$utils/veda-data'; -const prepareDatasets = ( - data: DatasetData[], - options: { - search: string; - taxonomies: Record | null; - sortField: string | null; - sortDir: string | null; - filterLayers: boolean | null; - } -) => { +const isDatasetData = (data: DatasetData | StoryData): data is DatasetData => { + return 'layers' in data; +}; + +interface FilterOptionsType { + search: string | null; + taxonomies: Record | null; + sortField?: string | null; + sortDir?: string | null; + filterLayers?: boolean | null; +} + +export function prepareDatasets(data: DatasetData[], options: FilterOptionsType): DatasetData[]; +export function prepareDatasets(data: StoryData[], options: FilterOptionsType): StoryData[]; +export function prepareDatasets ( + data: DatasetData[] | StoryData[], + options: FilterOptionsType +) { const { sortField, sortDir, search, taxonomies, filterLayers } = options; let filtered = [...data]; // Does the free text search appear in specific fields? - if (search.length >= 3) { + if (search && search.length >= 3) { const searchLower = search.toLowerCase(); // Function to check if searchLower is included in any of the string fields const includesSearchLower = (str) => str.toLowerCase().includes(searchLower); @@ -40,7 +48,7 @@ const prepareDatasets = ( idLower.includes(searchLower) || nameLower.includes(searchLower) || descriptionLower.includes(searchLower) || - d.layers.some(layerMatchesSearch) || + (isDatasetData(d) && d.layers.some(layerMatchesSearch)) || topicsTaxonomy?.values.some((t) => includesSearchLower(t.name)) ); }); @@ -48,8 +56,8 @@ const prepareDatasets = ( if (filterLayers) filtered = filtered.map((d) => ({ ...d, - layers: d.layers.filter(layerMatchesSearch), - })); + layers: (isDatasetData(d) && d.layers.filter(layerMatchesSearch)), + })) as DatasetData[]; } taxonomies && @@ -77,6 +85,4 @@ const prepareDatasets = ( } return filtered; -}; - -export default prepareDatasets; \ No newline at end of file +} diff --git a/app/scripts/components/common/catalog/utils.test.ts b/app/scripts/components/common/catalog/utils.test.ts index 3441ac2bd..c931dffa3 100644 --- a/app/scripts/components/common/catalog/utils.test.ts +++ b/app/scripts/components/common/catalog/utils.test.ts @@ -1,7 +1,7 @@ import { omit, set } from 'lodash'; -import { CatalogActions, onCatalogAction } from './utils'; +import { FilterActions, onFilterAction } from './utils'; -describe('onCatalogAction', () => { +describe('onFilterAction', () => { let setSearchMock; let setTaxonomiesMock; let taxonomies; @@ -16,8 +16,8 @@ describe('onCatalogAction', () => { }); it('should clear search and taxonomies on CLEAR action', () => { - onCatalogAction( - CatalogActions.CLEAR, + onFilterAction( + FilterActions.CLEAR, null, taxonomies, setSearchMock, @@ -28,10 +28,35 @@ describe('onCatalogAction', () => { expect(setTaxonomiesMock).toHaveBeenCalledWith({}); }); + it('should clear only taxonomies on CLEAR_TAXONOMY action', () => { + onFilterAction( + FilterActions.CLEAR_TAXONOMY, + null, + taxonomies, + setSearchMock, + setTaxonomiesMock + ); + + expect(setTaxonomiesMock).toHaveBeenCalledWith({}); + expect(setSearchMock).not.toHaveBeenCalled(); + }); + + it('should clear only search on CLEAR_SEARCH action', () => { + onFilterAction( + FilterActions.CLEAR_SEARCH, + null, + taxonomies, + setSearchMock, + setTaxonomiesMock + ); + + expect(setSearchMock).toHaveBeenCalledWith(''); + expect(setTaxonomiesMock).not.toHaveBeenCalled(); + }); it('should set search value on SEARCH action', () => { const searchValue = 'climate'; - onCatalogAction( - CatalogActions.SEARCH, + onFilterAction( + FilterActions.SEARCH, searchValue, taxonomies, setSearchMock, @@ -43,8 +68,8 @@ describe('onCatalogAction', () => { it('should add value to Topics taxonomy on TAXONOMY_MULTISELECT action', () => { const value = { key: 'Topics', value: 'pollution' }; - onCatalogAction( - CatalogActions.TAXONOMY_MULTISELECT, + onFilterAction( + FilterActions.TAXONOMY_MULTISELECT, value, taxonomies, setSearchMock, @@ -56,10 +81,25 @@ describe('onCatalogAction', () => { ); }); + it('should overwrite the existing taxonomy value with TAXONOMY action', () => { + const value = { key: 'Topics', value: 'climate' }; + onFilterAction( + FilterActions.TAXONOMY, + value, + taxonomies, + setSearchMock, + setTaxonomiesMock + ); + + expect(setTaxonomiesMock).toHaveBeenCalledWith( + set({ ...taxonomies }, value.key, value.value) + ); + }); + it('should remove value from Topics taxonomy on TAXONOMY_MULTISELECT action', () => { const value = { key: 'Topics', value: 'climate' }; - onCatalogAction( - CatalogActions.TAXONOMY_MULTISELECT, + onFilterAction( + FilterActions.TAXONOMY_MULTISELECT, value, taxonomies, setSearchMock, @@ -74,8 +114,8 @@ describe('onCatalogAction', () => { it('should remove Topics key when last value is removed on TAXONOMY_MULTISELECT action', () => { const value = { key: 'Topics', value: 'climate' }; - onCatalogAction( - CatalogActions.TAXONOMY_MULTISELECT, + onFilterAction( + FilterActions.TAXONOMY_MULTISELECT, value, { Topics: ['climate'] }, setSearchMock, @@ -89,8 +129,8 @@ describe('onCatalogAction', () => { it('should initialize new taxonomy key on TAXONOMY_MULTISELECT action', () => { const value = { key: 'Regions', value: 'Europe' }; - onCatalogAction( - CatalogActions.TAXONOMY_MULTISELECT, + onFilterAction( + FilterActions.TAXONOMY_MULTISELECT, value, taxonomies, setSearchMock, diff --git a/app/scripts/components/common/catalog/utils.ts b/app/scripts/components/common/catalog/utils.ts index 15ed625bb..ded96f3b6 100644 --- a/app/scripts/components/common/catalog/utils.ts +++ b/app/scripts/components/common/catalog/utils.ts @@ -1,31 +1,56 @@ import { omit, set } from 'lodash'; +import { optionAll } from '$components/common/browse-controls/constants'; -export enum CatalogActions { +export enum FilterActions { + TAXONOMY_MULTISELECT = 'taxonomy_multiselect', CLEAR = 'clear', SEARCH = 'search', - TAXONOMY_MULTISELECT = 'taxonomy_multiselect' + SORT_FIELD = 'sfield', + SORT_DIR = 'sdir', + TAXONOMY = 'taxonomy', + CLEAR_TAXONOMY = 'clear_taxonomy', + CLEAR_SEARCH = 'clear_search', } -export type CatalogViewAction = (action: CatalogActions, value?: any) => void; +export type FilterAction = (action: FilterActions, value?: any) => void; -export function onCatalogAction( - action: CatalogActions, +export function onFilterAction( + action: FilterActions, value: any, taxonomies: any, setSearch: (value: string) => void, setTaxonomies: (value: any) => void ) { switch (action) { - case CatalogActions.CLEAR: { + case FilterActions.CLEAR: { setSearch(''); setTaxonomies({}); break; } - case CatalogActions.SEARCH: { + case FilterActions.SEARCH: { setSearch(value); break; } - case CatalogActions.TAXONOMY_MULTISELECT: { + case FilterActions.CLEAR_TAXONOMY: { + setTaxonomies({}); + break; + } + case FilterActions.CLEAR_SEARCH: { + setSearch(''); + break; + } + case FilterActions.TAXONOMY: { + { + const { key, value: val } = value; + if (val === optionAll.id) { + setTaxonomies(omit(taxonomies, key)); + } else { + setTaxonomies(set({ ...taxonomies }, key, val)); + } + } + break; + } + case FilterActions.TAXONOMY_MULTISELECT: { const { key, value: val } = value; if (taxonomies && key in taxonomies) { diff --git a/app/scripts/components/common/content-taxonomy.tsx b/app/scripts/components/common/content-taxonomy.tsx index bec14f610..830a7aeb0 100644 --- a/app/scripts/components/common/content-taxonomy.tsx +++ b/app/scripts/components/common/content-taxonomy.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Heading, Overline } from '@devseed-ui/typography'; -import { Actions } from './browse-controls/use-browse-controls'; +import { FilterActions } from '$components/common/catalog/utils'; import { variableGlsp } from '$styles/variable-utils'; import { Pill } from '$styles/pill'; @@ -60,7 +60,7 @@ export function ContentTaxonomy(props: ContentTaxonomyProps) { variation='achromic' key={t.id} as={Link} - to={`${linkBase}?${Actions.TAXONOMY}=${encodeURIComponent( + to={`${linkBase}?${FilterActions.TAXONOMY}=${encodeURIComponent( JSON.stringify({ [name]: [t.id] }) )}`} > diff --git a/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx b/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx index 1d4651073..c417f881f 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { useAtom } from 'jotai'; - import { DatasetData, DatasetLayer } from 'veda'; import { Modal, @@ -20,11 +19,13 @@ import { allExploreDatasets } from '../../data-utils'; import RenderModalHeader from './header'; + import ModalFooterRender from './footer'; import CatalogContent from '$components/common/catalog/catalog-content'; import { DATASETS_PATH } from '$utils/routes'; -import { CatalogViewAction, onCatalogAction } from '$components/common/catalog/utils'; +import { useFiltersWithURLAtom } from '$components/common/catalog/controls/hooks/use-filters-with-query'; +import { FilterActions } from '$components/common/catalog/utils'; const DatasetModal = styled(Modal)` z-index: ${themeVal('zIndices.modal')}; @@ -77,14 +78,20 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { const { revealed, close } = props; const [timelineDatasets, setTimelineDatasets] = useAtom(timelineDatasetsAtom); - - const [searchTerm, setSearchTerm] = useState(''); - const [taxonomies, setTaxonomies] = useState({}); - // Store a list of selected datasets and only confirm on save. const [selectedIds, setSelectedIds] = useState( timelineDatasets.map((dataset) => dataset.data.id) ); + // Use Jotai controlled atoms for query parameter manipulation on new E&A page + const {search: searchTerm, taxonomies, onAction } = useFiltersWithURLAtom(); + + useEffect(() => { + // Reset filter when modal is hidden + if(!revealed) { + onAction(FilterActions.CLEAR); + } + },[revealed, onAction]); + useEffect(() => { setSelectedIds(timelineDatasets.map((dataset) => dataset.data.id)); }, [timelineDatasets]); @@ -93,13 +100,9 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { setTimelineDatasets( reconcileDatasets(selectedIds, datasetLayers, timelineDatasets) ); + onAction(FilterActions.CLEAR); close(); - }, [close, selectedIds, timelineDatasets, setTimelineDatasets]); - - const onAction = useCallback( - (action, value) => onCatalogAction(action, value, taxonomies, setSearchTerm, setTaxonomies), - [setTaxonomies, taxonomies] - ); + }, [close, selectedIds, timelineDatasets, setTimelineDatasets, onAction]); return ( d!.data); @@ -67,94 +65,24 @@ const BrowseFoldHeader = styled(FoldHeader)` const FoldWithTopMargin = styled(Fold)` margin-top: ${glsp()}; `; - -const sortOptions = [ - { id: 'name', name: 'Name' }, - { id: 'pubDate', name: 'Date' } -]; - -const prepareStories = ( - data: StoryData[], - options: { - search: string; - taxonomies: Record | null; - sortField: string | null; - sortDir: string | null; - } -) => { - const { sortField, sortDir, search, taxonomies } = options; - - let filtered = [...data]; - - // Does the free text search appear in specific fields? - if (search.length >= 3) { - const searchLower = search.toLowerCase(); - filtered = filtered.filter((d) => { - const topicsTaxonomy = d.taxonomy.find((t) => t.name === TAXONOMY_TOPICS); - return ( - d.name.toLowerCase().includes(searchLower) || - d.description.toLowerCase().includes(searchLower) || - topicsTaxonomy?.values.some((t) => - t.name.toLowerCase().includes(searchLower) - ) - ); - }); - } - - taxonomies && - Object.entries(taxonomies).forEach(([name, value]) => { - if (value !== optionAll.id) { - filtered = filtered.filter((d) => - d.taxonomy.some( - (t) => t.name === name && t.values.some((v) => v.id === value) - ) - ); - } - }); - - sortField && - /* eslint-disable-next-line fp/no-mutating-methods */ - filtered.sort((a, b) => { - if (!a[sortField]) return Infinity; - - return a[sortField]?.localeCompare(b[sortField]); - }); - - // In the case of the date, ordering is reversed. - if (sortField === 'pubDate') { - /* eslint-disable-next-line fp/no-mutating-methods */ - filtered.reverse(); - } - - if (sortDir === 'desc') { - /* eslint-disable-next-line fp/no-mutating-methods */ - filtered.reverse(); - } - - return filtered; -}; - + function StoriesHub() { - const controlVars = useBrowserControls({ - sortOptions - }); + const controlVars = useFiltersWithQS(); - const { taxonomies, sortField, sortDir, onAction } = controlVars; - const search = controlVars.search ?? ''; + const { search, taxonomies, onAction } = controlVars; + const displayStories = useMemo( () => - prepareStories(allStories, { + prepareDatasets(allStories, { search, - taxonomies, - sortField, - sortDir + taxonomies }), - [search, taxonomies, sortField, sortDir] + [search, taxonomies] ); - + const isFiltering = !!( - (taxonomies && Object.keys(taxonomies).length) || + (taxonomies && Object.keys(taxonomies).length )|| search ); @@ -186,7 +114,9 @@ function StoriesHub() { Browse @@ -224,7 +154,7 @@ function StoriesHub() { sources={getTaxonomy(d, TAXONOMY_SOURCE)?.values} rootPath={STORIES_PATH} onSourceClick={(id) => { - onAction(Actions.TAXONOMY, { + onAction(FilterActions.TAXONOMY_MULTISELECT, { key: TAXONOMY_SOURCE, value: id }); @@ -232,17 +162,9 @@ function StoriesHub() { }} /> + {!isNaN(pubDate.getTime()) && ( - { - e.preventDefault(); - onAction(Actions.SORT_FIELD, 'pubDate'); - browseControlsHeaderRef.current?.scrollIntoView(); - }} - > - )} } @@ -276,7 +198,7 @@ function StoriesHub() {