diff --git a/src/components/AllAuthorsModal/AllAuthorsModal.tsx b/src/components/AllAuthorsModal/AllAuthorsModal.tsx index c6225f5fb..d603ebf12 100644 --- a/src/components/AllAuthorsModal/AllAuthorsModal.tsx +++ b/src/components/AllAuthorsModal/AllAuthorsModal.tsx @@ -49,6 +49,11 @@ import { sendGTMEvent } from '@next/third-parties/google'; import { unwrapStringValue } from '@/utils/common/formatters'; import { useGetAffiliations } from '@/api/search/search'; import { IADSApiSearchParams, IDocsEntity } from '@/api/search/types'; +import { useSettings } from '@/lib/useSettings'; +import { solrDefaultSortDirection, SolrSort } from '@/api/models'; +import { normalizeSolrSort, parseQueryFromUrl } from '@/utils/common/search'; +import { APP_DEFAULTS } from '@/config'; +import DOMPurify from 'isomorphic-dompurify'; export interface IAllAuthorsModalProps { bibcode: IDocsEntity['bibcode']; @@ -66,6 +71,13 @@ export const AllAuthorsModal = ({ bibcode, label }: IAllAuthorsModalProps): Reac const initialRef = useRef(null); const toast = useToast(); const router = useRouter(); + const { settings } = useSettings({ suspense: false }); + + // resolve the active sort: prefer whatever sort is in the URL, fall back to user's stored preference + const preferredSortField = settings?.preferredSearchSort ?? APP_DEFAULTS.PREFERRED_SEARCH_SORT; + const preferredSort = normalizeSolrSort([`${preferredSortField} ${solrDefaultSortDirection[preferredSortField]}`]); + const hasSortParam = router.asPath.includes('?') && new URLSearchParams(router.asPath.split('?')[1]).has('sort'); + const activeSort = hasSortParam ? parseQueryFromUrl(router.asPath).sort : preferredSort; // on history change (or url params), close the modal useEffect(() => { @@ -117,7 +129,7 @@ export const AllAuthorsModal = ({ bibcode, label }: IAllAuthorsModalProps): Reac fontWeight="bold" as={MathJax} dangerouslySetInnerHTML={{ - __html: unwrapStringValue(data.docs[0].title), + __html: DOMPurify.sanitize(unwrapStringValue(data.docs[0].title)), }} /> @@ -129,7 +141,14 @@ export const AllAuthorsModal = ({ bibcode, label }: IAllAuthorsModalProps): Reac )} - {isSuccess && } + {isSuccess && ( + + )} @@ -137,190 +156,192 @@ export const AllAuthorsModal = ({ bibcode, label }: IAllAuthorsModalProps): Reac ); }; -const createQuery = (type: 'author' | 'orcid', value: string): IADSApiSearchParams => { - return { q: `${type}:"${value}"`, sort: type === 'author' ? ['score desc'] : ['date desc'] }; +const createQuery = (type: 'author' | 'orcid', value: string, sort?: SolrSort[]): IADSApiSearchParams => { + const defaultSort: SolrSort[] = type === 'author' ? ['score desc'] : ['date desc']; + return { q: `${type}:"${value}"`, sort: sort ?? defaultSort }; }; -const AuthorsTable = forwardRef void }>( - ({ doc, onSearchClear }, ref) => { - // process doc (extracts author information) - const authors = useGetAuthors({ doc }); - const [list, setList] = useState(authors); - const [searchVal, setSearchVal] = useState(''); - const debSearchVal = useDebounce(searchVal, 500); +const AuthorsTable = forwardRef< + HTMLInputElement, + { doc: IDocsEntity; onSearchClear: () => void; activeSort: SolrSort[] } +>(({ doc, onSearchClear, activeSort }, ref) => { + // process doc (extracts author information) + const authors = useGetAuthors({ doc }); + const [list, setList] = useState(authors); + const [searchVal, setSearchVal] = useState(''); + const debSearchVal = useDebounce(searchVal, 500); - // fill list with authors when it finishes loading - useEffect(() => setList(authors), [authors]); + // fill list with authors when it finishes loading + useEffect(() => setList(authors), [authors]); - // filter list when searchval changes - useEffect( - () => - setList( - debSearchVal === '' - ? authors - : matchSorter(authors, debSearchVal, { - keys: ['1', '2'], - threshold: matchSorter.rankings.CONTAINS, - }), - ), - [debSearchVal, authors], - ); - - const [{ start, end }, setPagination] = useState({ start: 0, end: 10 }); + // filter list when searchval changes + useEffect( + () => + setList( + debSearchVal === '' + ? authors + : matchSorter(authors, debSearchVal, { + keys: ['1', '2'], + threshold: matchSorter.rankings.CONTAINS, + }), + ), + [debSearchVal, authors], + ); - const { getPaginationProps } = usePagination({ - numFound: list.length, - onStateChange: (pagination) => { - if (pagination.startIndex !== start || pagination.endIndex !== end) { - setPagination({ - start: pagination.startIndex, - end: pagination.endIndex, - }); - } - }, - }); + const [{ start, end }, setPagination] = useState({ start: 0, end: 10 }); - // update search val on input change - const handleInputChange: ChangeEventHandler = (e) => { - setSearchVal(e.currentTarget.value); - }; + const { getPaginationProps } = usePagination({ + numFound: list.length, + onStateChange: (pagination) => { + if (pagination.startIndex !== start || pagination.endIndex !== end) { + setPagination({ + start: pagination.startIndex, + end: pagination.endIndex, + }); + } + }, + }); - // clear input and update list - const handleInputClear = () => { - setSearchVal(''); - setList(authors); - onSearchClear(); - }; + // update search val on input change + const handleInputChange: ChangeEventHandler = (e) => { + setSearchVal(e.currentTarget.value); + }; - // handle the download button - const handleDownloadClick: MouseEventHandler = () => { - const csvBlob = new Blob( - [ - authors.reduce( - (acc, [a = '', b = '', c = '', d = '']) => `${acc}"${a}","${b}","${c}","${d}"\n`, - 'position,name,affiliation,orcid\n', - ), - ], - { type: 'text/csv;charset=utf-8' }, - ); - saveAs(csvBlob, `${doc.bibcode}-authors.csv`); - sendGTMEvent({ - event: 'author_list_export', - }); - }; + // clear input and update list + const handleInputClear = () => { + setSearchVal(''); + setList(authors); + onSearchClear(); + }; - const renderRows = () => { - return ( - <> - {list.slice(start, end).map((item, index) => { - const [position, author, aff, orcid] = item; - return ( - - - {position}. - - - {typeof author === 'string' && ( - - <>{author} - - )} - - - {typeof orcid === 'string' && ( - - - - )} - - - - - - ); - })} - - ); - }; + // handle the download button + const handleDownloadClick: MouseEventHandler = () => { + const csvBlob = new Blob( + [ + authors.reduce( + (acc, [a = '', b = '', c = '', d = '']) => `${acc}"${a}","${b}","${c}","${d}"\n`, + 'position,name,affiliation,orcid\n', + ), + ], + { type: 'text/csv;charset=utf-8' }, + ); + saveAs(csvBlob, `${doc.bibcode}-authors.csv`); + sendGTMEvent({ + event: 'author_list_export', + }); + }; + const renderRows = () => { return ( - + {list.slice(start, end).map((item, index) => { + const [position, author, aff, orcid] = item; + return ( + + + {position}. + + + {typeof author === 'string' && ( + + <>{author} + + )} + + + {typeof orcid === 'string' && ( + + + + )} + + + + + + ); + })} + + ); + }; + + return ( + + - - - - - } - variant="unstyled" - aria-label="clear" - size="sm" - hidden={searchVal.length <= 0} - onClick={handleInputClear} - /> - - - + + + } - size="md" - ml="4" - onClick={handleDownloadClick} - aria-label="Download list as CSV file" + icon={} + variant="unstyled" + aria-label="clear" + size="sm" + hidden={searchVal.length <= 0} + onClick={handleInputClear} /> - - - - {list.length > 0 && ( - <> - - - - - - - - - - {renderRows()} -
NameORCiDAffliation
- - )} - -
-
- ); - }, -); + + + + } + size="md" + ml="4" + onClick={handleDownloadClick} + aria-label="Download list as CSV file" + /> + + + + {list.length > 0 && ( + <> + + + + + + + + + + {renderRows()} +
NameORCiDAffliation
+ + )} + +
+
+ ); +}); AuthorsTable.displayName = 'AllAuthorsModal';