diff --git a/ui/src/actions/holdingpen.ts b/ui/src/actions/holdingpen.ts index f618828b5..870e1cf11 100644 --- a/ui/src/actions/holdingpen.ts +++ b/ui/src/actions/holdingpen.ts @@ -181,7 +181,12 @@ function resetQuery() { }; } -type QueryParams = { page: number; size: number; [key: string]: any }; +type QueryParams = { + page: number; + size?: number; + ordering?: string; + [key: string]: any; +}; export function searchQueryUpdate( query: QueryParams ): (dispatch: ActionCreator) => Promise { @@ -208,7 +213,8 @@ export function fetchSearchResults(): ( const currentQuery = getState()?.holdingpen?.get('query')?.toJS() || {}; const resolveQuery = `${BACKOFFICE_SEARCH_API}/?${ Object.entries(currentQuery) - ?.map(([key, value]: [string, any]) => `${key}=${value}`) + .filter(([_, value]) => value != null) + .map(([key, value]: [string, any]) => `${key}=${value}`) .join('&') || '' }`; diff --git a/ui/src/common/components/Logo/LogoHoldingpen.tsx b/ui/src/common/components/Logo/LogoHoldingpen.tsx index 8acbcbfcb..99f3728da 100644 --- a/ui/src/common/components/Logo/LogoHoldingpen.tsx +++ b/ui/src/common/components/Logo/LogoHoldingpen.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; import './Logo.less'; -import { HOLDINGPEN } from '../../routes'; +import { HOLDINGPEN_NEW } from '../../routes'; import { ReactComponent as LogoSvg } from './logo-holdingpen.svg'; const LogoHoldingpen = () => ( - + ); diff --git a/ui/src/holdingpen-new/__tests__/__snapshots__/index.test.tsx.snap b/ui/src/holdingpen-new/__tests__/__snapshots__/index.test.tsx.snap index 125482b62..003552f59 100644 --- a/ui/src/holdingpen-new/__tests__/__snapshots__/index.test.tsx.snap +++ b/ui/src/holdingpen-new/__tests__/__snapshots__/index.test.tsx.snap @@ -73,7 +73,7 @@ exports[`Holdingpen renders initial state 1`] = ` class="ant-breadcrumb-link" > Dashboard @@ -328,7 +328,7 @@ exports[`Holdingpen renders initial state 1`] = `

View all @@ -364,7 +364,7 @@ exports[`Holdingpen renders initial state 1`] = `

View all @@ -400,7 +400,7 @@ exports[`Holdingpen renders initial state 1`] = `

View all diff --git a/ui/src/holdingpen-new/components/SearchFilters.tsx b/ui/src/holdingpen-new/components/SearchFilters.tsx new file mode 100644 index 000000000..aee68d4b2 --- /dev/null +++ b/ui/src/holdingpen-new/components/SearchFilters.tsx @@ -0,0 +1,182 @@ +import React, { useState } from 'react'; +import { Col, Card, Select, Row, Checkbox } from 'antd'; +import { connect, RootStateOrAny } from 'react-redux'; +import { Action, ActionCreator } from 'redux'; +import { Map } from 'immutable'; +import classNames from 'classnames'; + +import LoadingOrChildren from '../../common/components/LoadingOrChildren'; +import UnclickableTag from '../../common/components/UnclickableTag'; +import { searchQueryReset, searchQueryUpdate } from '../../actions/holdingpen'; +import { COLLECTIONS, getIcon } from '../utils/utils'; +import LinkLikeButton from '../../common/components/LinkLikeButton/LinkLikeButton'; + +interface SearchFiltersProps { + dispatch: ActionCreator; + loading: boolean; + query: Map; + facets: any; + count: any; +} + +const FilterOption: React.FC<{ + label: string; + options: Map; + selectedKey: string | null; + onSelect: (key: string | null) => void; + renderLabel: (key: string) => React.ReactNode; +}> = ({ label, options, selectedKey, onSelect, renderLabel }) => { + if (!options?.size) return null; + + return ( + <> + +

{label}

+
+ {options.map((option: Map) => { + const key = option.get('key'); + const isChecked = selectedKey === key; + return ( + + + onSelect(isChecked ? null : key)} + > + {renderLabel(key)} + + + + {option.get('doc_count')} + + + ); + })} + + ); +}; + +const SearchFilters: React.FC = ({ + dispatch, + loading, + query, + facets, + count, +}) => { + const [selectedFilters, setSelectedFilters] = useState({ + status: query?.get('status'), + workflow_type: query?.get('workflow_type'), + }); + + const updateFilters = (key: string, value: string | null) => { + setSelectedFilters((prevFilters) => ({ ...prevFilters, [key]: value })); + dispatch( + searchQueryUpdate({ + ...query.toJS(), + page: 1, + [key]: value, + }) + ); + }; + + return ( + + + +

Results per page

+ + dispatch( + searchQueryUpdate({ + page: 1, + size: query.get('size'), + ordering: value, + }) + ) + } + /> + + {count > 0 && ( + <> + updateFilters('workflow_type', key)} + renderLabel={(key) => ( + {COLLECTIONS[key]} + )} + /> + + updateFilters('status', key)} + renderLabel={(key) => ( + + {getIcon(key)} + {key} + + )} + /> + + )} + + + { + setSelectedFilters({ status: null, workflow_type: null }); + dispatch(searchQueryReset()); + }} + > + Reset filters + + +
+
+ + ); +}; + +const mapStateToProps = (state: RootStateOrAny) => ({ + loading: state.holdingpen.get('loading'), + query: state.holdingpen.get('query'), + facets: state.holdingpen.get('facets'), + count: state.holdingpen.get('totalResults'), +}); + +export default connect(mapStateToProps)(SearchFilters); diff --git a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.less b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.less index 9d5fd660e..2f97e460b 100644 --- a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.less +++ b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.less @@ -99,6 +99,9 @@ background-color: #f5f5f5; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; } +.bg-running { + background-color: #c2c5c7; +} .font-white { color: white !important; diff --git a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx index b62d36dc2..1c6cb76b7 100644 --- a/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/DashboardPageContainer/DashboardPageContainer.tsx @@ -1,12 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useEffect } from 'react'; import classNames from 'classnames'; -import { - WarningOutlined, - CheckOutlined, - HourglassOutlined, - LoadingOutlined, -} from '@ant-design/icons'; import { Card, Input, Select, Tabs } from 'antd'; import { Link } from 'react-router-dom'; import { push } from 'connected-react-router'; @@ -24,6 +18,7 @@ import { } from '../../../actions/holdingpen'; import EmptyOrChildren from '../../../common/components/EmptyOrChildren'; import LoadingOrChildren from '../../../common/components/LoadingOrChildren'; +import { COLLECTIONS, getIcon } from '../../utils/utils'; interface DashboardPageContainerProps { dispatch: ActionCreator; @@ -35,12 +30,6 @@ const TEXT_CENTER: Record = { textAlign: 'center', }; -const COLLECTIONS: Record = { - AUTHOR_CREATE: 'new authors', - AUTHOR_UPDATE: 'author updates', - HEP_CREATE: 'new literature submissions', -}; - const { Option } = Select; const { Search } = Input; @@ -60,21 +49,6 @@ const selectBefore = ( ); -const getIcon = (status: string) => { - switch (status?.toLowerCase()) { - case 'approval': - return ; - case 'error': - return ; - case 'completed': - return ; - case 'running': - return ; - default: - return null; - } -}; - const DashboardPageContainer: React.FC = ({ dispatch, facets, @@ -117,16 +91,14 @@ const DashboardPageContainer: React.FC = ({ {type?.get('doc_count')}

{ dispatch( searchQueryUpdate({ page: 1, size: 10, - workflow_name: type?.get('key'), + workflow_type: type?.get('key'), }) ); }} @@ -144,16 +116,14 @@ const DashboardPageContainer: React.FC = ({ {(type?.getIn(['status', 'buckets']) as List)?.map( (status: any) => ( { dispatch( searchQueryUpdate({ page: 1, size: 10, - workflow_name: type?.get('key'), + workflow_type: type?.get('key'), status: status?.get('key'), }) ); @@ -188,7 +158,7 @@ const DashboardPageContainer: React.FC = ({ className="__DashboardPageContainer__" data-testid="holdingpen-dashboard-page" > - +

Search Holdingpen

diff --git a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.less b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.less index 74816159a..e2fd21e6a 100644 --- a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.less +++ b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.less @@ -4,7 +4,6 @@ .facet-category { font-size: 1rem; - margin-top: 1rem; font-weight: 600; } @@ -12,3 +11,17 @@ margin-right: 0; } } + +.waiting, +.running { + color: #7b898a; +} +.error { + color: #e74c3c; +} +.approval { + color: #cfa90e; +} +.completed { + color: #16a085; +} diff --git a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx index 749f95cfd..8a54aad53 100644 --- a/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/SearchPageContainer/SearchPageContainer.tsx @@ -1,26 +1,24 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useEffect } from 'react'; -import { Row, Col, Card, Checkbox, Select } from 'antd'; +import { Row, Col } from 'antd'; import { connect, RootStateOrAny } from 'react-redux'; import { Action, ActionCreator } from 'redux'; import { List, Map } from 'immutable'; import './SearchPageContainer.less'; -import { facets } from '../../mocks/mockSearchData'; import Breadcrumbs from '../../components/Breadcrumbs'; -import LoadingOrChildren from '../../../common/components/LoadingOrChildren'; import { SEARCH_PAGE_GUTTER } from '../../../common/constants'; import SearchResults from '../../../common/components/SearchResults'; import NumberOfResults from '../../../common/components/NumberOfResults'; import SearchPagination from '../../../common/components/SearchPagination'; import PublicationsSelectAllContainer from '../../../authors/containers/PublicationsSelectAllContainer'; -import UnclickableTag from '../../../common/components/UnclickableTag'; import AuthorResultItem from '../../components/AuthorResultItem'; import { fetchSearchResults, searchQueryUpdate, } from '../../../actions/holdingpen'; import EmptyOrChildren from '../../../common/components/EmptyOrChildren'; +import SearchFilters from '../../components/SearchFilters'; interface SearchPageContainerProps { dispatch: ActionCreator; @@ -37,7 +35,6 @@ const renderResultItem = (item: Map) => { const SearchPageContainer: React.FC = ({ dispatch, results, - loading, totalResults, query, }) => { @@ -52,75 +49,10 @@ const SearchPageContainer: React.FC = ({ > - - <> - - - -

Results per page

- - {facets.map( - (facet: { - category: string; - filters: { name: string; doc_count: number }[]; - }) => ( -
- -

- Filter by {facet.category} -

-
- {facet.filters.map((filter) => ( - - - - {filter.name} - - - - - {filter.doc_count} - - - - ))} -
- ) - )} -
-
- - + + + + <> @@ -155,18 +87,20 @@ const SearchPageContainer: React.FC = ({
- dispatch(searchQueryUpdate({ page, size })) + dispatch(searchQueryUpdate({ ...query.toJS(), page, size })) } onSizeChange={(_page, size) => - dispatch(searchQueryUpdate({ page: 1, size })) + dispatch( + searchQueryUpdate({ ...query.toJS(), page: 1, size }) + ) } page={query?.get('page') || 1} total={totalResults} pageSize={query?.get('size') || 10} /> - - -
+ +
+
); diff --git a/ui/src/holdingpen-new/mocks/mockSearchData.ts b/ui/src/holdingpen-new/mocks/mockSearchData.ts deleted file mode 100644 index fddd774e8..000000000 --- a/ui/src/holdingpen-new/mocks/mockSearchData.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { List, Map } from 'immutable'; - -export const data = List([ - Map({ - id: 123456, - display_name: 'Doe, John Marc', - name: Map({ title: 'John Marc Doe' }), - preferred_name: 'Johnny Marc Doe', - status: 'active', - orcid: '0000-0000-0000-0000', - author_profile: Map({ - url: 'https://inspirehep.net/authors/1072974', - value: 'J.D.Siemieniuk.1', - }), - institutions: List([ - Map({ - name: 'University of Toronto', - rank: 1, - start_date: '2019', - end_date: '-', - current: 'true', - }), - Map({ - name: 'Sunnybrook Health Sciences Centre', - rank: 2, - start_date: '2007', - end_date: '2019', - current: 'false', - }), - Map({ - name: 'University of Warsaw', - rank: 3, - start_date: '1998', - end_date: '2007', - current: 'false', - }), - ]), - projects: List([ - Map({ - name: 'Project A', - start_date: '2019', - end_date: '-', - current: 'true', - }), - Map({ - name: 'Project B', - start_date: '2007', - end_date: '2019', - current: 'false', - }), - Map({ - name: 'Project C', - start_date: '1998', - end_date: '2007', - current: 'false', - }), - ]), - subject_areas: List([ - Map({ - term: 'hep-ex', - }), - Map({ - term: 'hep-ph', - }), - ]), - advisors: List([ - Map({ - name: 'Giri, Anjan', - position: 'PhD', - }), - Map({ - name: 'Gugri, Injan', - position: 'PhD', - }), - ]), - }), - Map({ - title: 'Introduction to Quantum Mechanics', - id: 1, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: ['quantum', 'mechanics', 'physics'], - authors: List([ - Map({ name: 'John Smith', affiliation: 'University X' }), - Map({ name: 'Emily Johnson', affiliation: 'University Y' }), - ]), - date: '2024-04-02', - publisher: 'Science Publishers Inc.', - status: 'completed', - }), - Map({ - title: 'Advancements in Artificial Intelligence', - id: 2, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: [ - 'artificial intelligence', - 'machine learning', - 'neural networks', - ], - authors: List([ - Map({ name: 'Michael Brown', affiliation: 'Tech University' }), - Map({ name: 'Sophia Martinez', affiliation: 'AI Research Institute' }), - ]), - date: '2024-04-02', - publisher: 'Tech Publishing Group', - status: 'awaiting decision', - }), - Map({ - title: 'Introduction to Quantum Mechanics', - id: 3, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: ['quantum', 'mechanics', 'physics'], - authors: List([ - Map({ name: 'John Smith', affiliation: 'University X' }), - Map({ name: 'Emily Johnson', affiliation: 'University Y' }), - ]), - date: '2024-04-02', - publisher: 'Science Publishers Inc.', - status: 'error', - }), - Map({ - title: 'Advancements in Artificial Intelligence', - id: 4, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: [ - 'artificial intelligence', - 'machine learning', - 'neural networks', - ], - authors: List([ - Map({ name: 'Michael Brown', affiliation: 'Tech University' }), - Map({ name: 'Sophia Martinez', affiliation: 'AI Research Institute' }), - ]), - date: '2024-04-02', - publisher: 'Tech Publishing Group', - status: 'preparing', - }), - Map({ - title: 'Introduction to Quantum Mechanics', - id: 5, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: ['quantum', 'mechanics', 'physics'], - authors: List([ - Map({ name: 'John Smith', affiliation: 'University X' }), - Map({ name: 'Emily Johnson', affiliation: 'University Y' }), - ]), - date: '2024-04-02', - publisher: 'Science Publishers Inc.', - status: 'awaiting decision', - }), - Map({ - title: 'Advancements in Artificial Intelligence', - id: 6, - abstract: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec varius massa. Sed eleifend, ipsum at dignissim sollicitudin, ante odio laoreet nulla, at convallis purus ex sit amet dolor.', - keywords: [ - 'artificial intelligence', - 'machine learning', - 'neural networks', - ], - authors: List([ - Map({ name: 'Michael Brown', affiliation: 'Tech University' }), - Map({ name: 'Sophia Martinez', affiliation: 'AI Research Institute' }), - ]), - date: '2024-04-02', - publisher: 'Tech Publishing Group', - status: 'running', - }), -]); - -export const facets = [ - { - category: 'status', - filters: [ - { name: 'complete', doc_count: 10 }, - { name: 'awaiting decision', doc_count: 23 }, - { name: 'error', doc_count: 23 }, - { name: 'preparing', doc_count: 23 }, - { name: 'running', doc_count: 23 }, - ], - }, - { - category: 'source', - filters: [ - { name: 'arXiv', doc_count: 10 }, - { name: 'CDS', doc_count: 23 }, - { name: 'APS', doc_count: 67 }, - ], - }, - { category: 'collection', filters: [{ name: 'HEP', doc_count: 54 }] }, - { - category: 'subject', - filters: [ - { name: 'physics', doc_count: 10 }, - { name: 'quantum physics', doc_count: 23 }, - { name: 'astrophysics', doc_count: 23 }, - { name: 'other', doc_count: 23 }, - ], - }, - { - category: 'decision', - filters: [ - { name: 'core', doc_count: 10 }, - { name: 'non core', doc_count: 23 }, - { name: 'rejected', doc_count: 67 }, - ], - }, - { - category: 'pending action', - filters: [ - { name: 'merging', doc_count: 10 }, - { name: 'matching', doc_count: 23 }, - { name: 'core selection', doc_count: 67 }, - ], - }, - { - category: 'journal', - filters: [ - { name: 'Phys.Rev.C4', doc_count: 10 }, - { name: 'Eur.Phys.J.C2', doc_count: 23 }, - { name: 'Phys.Rev.A', doc_count: 67 }, - ], - }, -]; diff --git a/ui/src/holdingpen-new/utils/utils.ts b/ui/src/holdingpen-new/utils/utils.ts deleted file mode 100644 index e9ac74d80..000000000 --- a/ui/src/holdingpen-new/utils/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import storage from '../../common/storage'; -import { - BACKOFFICE_API, - BACKOFFICE_LOGIN, - HOLDINGPEN_LOGIN_NEW, -} from '../../common/routes'; - -export const refreshToken = async () => { - try { - const res = await fetch(`${BACKOFFICE_LOGIN}refresh/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - refresh: storage.getSync('holdingpen.refreshToken'), - }), - }); - - if (!res.ok) { - throw new Error('Failed to refresh token'); - } - - const data = await res.json(); - storage.set('holdingpen.token', data.access); - return data.access; - } catch (error) { - window.location.assign(HOLDINGPEN_LOGIN_NEW); - } - - return null; -}; diff --git a/ui/src/holdingpen-new/utils/utils.tsx b/ui/src/holdingpen-new/utils/utils.tsx new file mode 100644 index 000000000..7291317f0 --- /dev/null +++ b/ui/src/holdingpen-new/utils/utils.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + WarningOutlined, + CheckOutlined, + HourglassOutlined, + LoadingOutlined, +} from '@ant-design/icons'; + +import storage from '../../common/storage'; +import { BACKOFFICE_LOGIN, HOLDINGPEN_LOGIN_NEW } from '../../common/routes'; + +export const COLLECTIONS: Record = { + AUTHOR_CREATE: 'new authors', + AUTHOR_UPDATE: 'author updates', + HEP_CREATE: 'new literature submissions', +}; + +export const getIcon = (status: string) => { + switch (status?.toLowerCase()) { + case 'approval': + return ; + case 'error': + return ; + case 'completed': + return ; + case 'running': + return ; + default: + return null; + } +}; + +export const refreshToken = async () => { + try { + const res = await fetch(`${BACKOFFICE_LOGIN}refresh/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refresh: storage.getSync('holdingpen.refreshToken'), + }), + }); + + if (!res.ok) { + throw new Error('Failed to refresh token'); + } + + const data = await res.json(); + storage.set('holdingpen.token', data.access); + return data.access; + } catch (error) { + window.location.assign(HOLDINGPEN_LOGIN_NEW); + } + + return null; +}; diff --git a/ui/src/reducers/holdingpen.js b/ui/src/reducers/holdingpen.js index 86ac2637b..1009dc51b 100644 --- a/ui/src/reducers/holdingpen.js +++ b/ui/src/reducers/holdingpen.js @@ -19,7 +19,7 @@ import { export const initialState = fromJS({ loggedIn: false, - query: { page: 1, size: 10 }, + query: { page: 1, size: 10, ordering: '-_updated_at' }, searchResults: [], totalResults: 0, loading: false, @@ -28,7 +28,7 @@ export const initialState = fromJS({ actionInProgress: false, }); -const HOLDINGPENReducer = (state = initialState, action) => { +const HoldingpenReducer = (state = initialState, action) => { switch (action.type) { case HOLDINGPEN_LOGIN_ERROR: case HOLDINGPEN_LOGOUT_SUCCESS: @@ -74,4 +74,4 @@ const HOLDINGPENReducer = (state = initialState, action) => { } }; -export default HOLDINGPENReducer; +export default HoldingpenReducer;