Skip to content

Commit 675caca

Browse files
committed
export menu in library
1 parent 950d302 commit 675caca

File tree

2 files changed

+146
-24
lines changed

2 files changed

+146
-24
lines changed

src/components/Libraries/LibraryEntityPane.tsx

+140-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChevronLeftIcon, LockIcon, SettingsIcon, UnlockIcon } from '@chakra-ui/icons';
1+
import { ChevronDownIcon, ChevronLeftIcon, LockIcon, SettingsIcon, UnlockIcon } from '@chakra-ui/icons';
22
import {
33
Box,
44
Button,
@@ -9,6 +9,17 @@ import {
99
HStack,
1010
Icon,
1111
IconButton,
12+
Menu,
13+
MenuButton,
14+
MenuDivider,
15+
MenuGroup,
16+
MenuGroupProps,
17+
MenuItem,
18+
MenuItemOption,
19+
MenuList,
20+
MenuOptionGroup,
21+
Portal,
22+
Stack,
1223
Table,
1324
Tbody,
1425
Td,
@@ -27,8 +38,8 @@ import { BuildingLibraryIcon, ShareIcon } from '@heroicons/react/24/solid';
2738

2839
import { AppState, useStore } from '@/store';
2940
import { NumPerPageType } from '@/types';
30-
import { uniq } from 'ramda';
31-
import { useEffect, useMemo, useState } from 'react';
41+
import { curryN, uniq, values } from 'ramda';
42+
import { ReactElement, useEffect, useMemo, useState } from 'react';
3243
import { DocumentList } from './DocumentList/DocumentList';
3344
import { SimpleLink } from '@/components/SimpleLink';
3445
import { CustomInfoMessage, LoadingMessage } from '@/components/Feedbacks';
@@ -40,11 +51,16 @@ import { isBiblibSort, isSolrSort } from '@/utils/common/guards';
4051
import { normalizeSolrSort } from '@/utils/common/search';
4152
import { parseAPIError } from '@/utils/common/parseAPIError';
4253
import { LibraryIdentifier } from '@/api/biblib/types';
43-
import { IDocsEntity } from '@/api/search/types';
54+
import { Bibcode, IDocsEntity } from '@/api/search/types';
4455
import { BiblibSort, BiblibSortField } from '@/api/models';
4556
import { useEditLibraryDocuments, useGetLibraryEntity } from '@/api/biblib/libraries';
4657
import { useBigQuerySearch } from '@/api/search/search';
4758
import { getSearchParams } from '@/api/search/models';
59+
import { useSettings } from '@/lib/useSettings';
60+
import { ExportApiFormatKey } from '@/api/export/types';
61+
import { useVaultBigQuerySearch } from '@/api/vault/vault';
62+
import { useRouter } from 'next/router';
63+
import { exportFormats } from '../CitationExporter';
4864

4965
export interface ILibraryEntityPaneProps {
5066
id: LibraryIdentifier;
@@ -249,7 +265,7 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
249265
<LoadingMessage message="Loading library" />
250266
</Center>
251267
)}
252-
{errorFetchingLibs && (
268+
{!isLoadingLibs && errorFetchingLibs && (
253269
<CustomInfoMessage
254270
status={'error'}
255271
title={'Library not found'}
@@ -395,15 +411,16 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
395411
</SearchQueryLink>
396412
</Flex>
397413

398-
{canWrite && !publicView && (
414+
{canWrite && !publicView && !isLoadingDocs && (
399415
<Box style={isLoadingDocs ? { pointerEvents: 'none' } : { pointerEvents: 'auto' }} w="full">
400-
<BulkAction
416+
<BulkMenu
417+
library={id}
401418
isAllSelected={isAllSelected}
402419
isSomeSelected={isSomeSelected}
403420
onSelectAllCurrent={handleSelectAllCurrent}
404421
onClearAllCurrent={handleClearAllCurrent}
405422
onClearAll={handleClearAll}
406-
selectedCount={selected.length}
423+
selectedDocs={selected}
407424
onDeleteSelected={handleDeleteFromLibrary}
408425
/>
409426
</Box>
@@ -447,28 +464,44 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
447464
);
448465
};
449466

450-
const BulkAction = ({
467+
const BulkMenu = ({
468+
library,
451469
isAllSelected,
452470
isSomeSelected,
453471
onSelectAllCurrent,
454472
onClearAllCurrent,
455473
onClearAll,
456-
selectedCount,
474+
selectedDocs,
457475
onDeleteSelected,
458476
}: {
477+
library: string;
459478
isAllSelected: boolean;
460479
isSomeSelected: boolean;
461-
selectedCount: number;
480+
selectedDocs: string[];
462481
onSelectAllCurrent: () => void;
463482
onClearAllCurrent: () => void;
464483
onClearAll: () => void;
465484
onDeleteSelected: () => void;
466485
}) => {
486+
const { settings } = useSettings();
487+
488+
const { panel: PanelBackground } = useColorModeColors();
489+
490+
const [applyToAll, setApplyToAll] = useState(selectedDocs.length === 0);
491+
492+
useEffect(() => {
493+
setApplyToAll(selectedDocs.length === 0);
494+
}, [selectedDocs]);
495+
467496
const handleChange = () => {
468497
isAllSelected || isSomeSelected ? onClearAllCurrent() : onSelectAllCurrent();
469498
};
470499

471-
const { panel: PanelBackground } = useColorModeColors();
500+
const handleApplyOption = (value: string | string[]) => {
501+
if (typeof value === 'string') {
502+
setApplyToAll(value === 'all');
503+
}
504+
};
472505

473506
return (
474507
<Flex justifyContent="space-between" backgroundColor={PanelBackground} p={4}>
@@ -487,23 +520,108 @@ const BulkAction = ({
487520
data-testid="select-all-checkbox"
488521
/>
489522
</Tooltip>
490-
{selectedCount > 0 && (
523+
{selectedDocs.length > 0 && (
491524
<>
492-
<>{selectedCount} selected</>
525+
<>{selectedDocs.length} selected</>
493526
<Button variant="link" onClick={onClearAll}>
494527
Clear All
495528
</Button>
496529
</>
497530
)}
498531
</HStack>
499-
<Button
500-
isDisabled={selectedCount === 0}
501-
colorScheme="red"
502-
onClick={onDeleteSelected}
503-
data-testid="del-selected-btn"
504-
>
505-
Delete
506-
</Button>
532+
<Stack direction="row" order={{ base: '1', md: '2' }} wrap="wrap">
533+
<Menu>
534+
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
535+
Export
536+
</MenuButton>
537+
<Portal>
538+
<MenuList>
539+
<MenuOptionGroup value={applyToAll ? 'all' : 'selected'} type="radio" onChange={handleApplyOption}>
540+
<MenuItemOption value="all" closeOnSelect={false}>
541+
All
542+
</MenuItemOption>
543+
<MenuItemOption value="selected" isDisabled={selectedDocs.length === 0} closeOnSelect={false}>
544+
Selected
545+
</MenuItemOption>
546+
</MenuOptionGroup>
547+
<MenuDivider />
548+
{applyToAll ? (
549+
<ExportMenu library={library} defaultExportFormat={settings.defaultExportFormat} />
550+
) : (
551+
<ExportMenu library={library} docs={selectedDocs} defaultExportFormat={settings.defaultExportFormat} />
552+
)}
553+
</MenuList>
554+
</Portal>
555+
</Menu>
556+
<Button
557+
isDisabled={selectedDocs.length === 0}
558+
colorScheme="red"
559+
onClick={onDeleteSelected}
560+
data-testid="del-selected-btn"
561+
>
562+
Delete
563+
</Button>
564+
</Stack>
507565
</Flex>
508566
);
509567
};
568+
569+
const ExportMenu = (
570+
props: MenuGroupProps & { docs?: string[]; library: string; defaultExportFormat: string },
571+
): ReactElement => {
572+
const { docs, library, defaultExportFormat, ...menuGroupProps } = props;
573+
const router = useRouter();
574+
const [selected, setSelected] = useState<Bibcode[]>(null);
575+
const [route, setRoute] = useState(['', '']);
576+
577+
const { data } = useVaultBigQuerySearch(selected ?? [], { enabled: !!selected && selected.length > 0 });
578+
579+
const defaultExportFormatValue = values(exportFormats).find((f) => f.label === defaultExportFormat).value;
580+
581+
useEffect(() => {
582+
// when vault query is done, transition to the export page passing only qid
583+
if (data) {
584+
setSelected([]);
585+
void router.push(
586+
{
587+
pathname: route[0],
588+
query: { q: `docs(library/${library}`, qid: data.qid, referrer: `/user/libraries/${library}` },
589+
},
590+
{
591+
pathname: route[1],
592+
query: { q: `docs(library/${library}`, qid: data.qid, referrer: `/user/libraries/${library}` },
593+
},
594+
);
595+
}
596+
}, [data]);
597+
598+
// on route change
599+
useEffect(() => {
600+
if (route[0] !== '' && route[1] !== '') {
601+
if (docs && docs.length > 0) {
602+
// do this to trigger query request
603+
return setSelected(docs);
604+
} else {
605+
// if explore all, then just use the current query, and do not trigger vault (redirect immediately)
606+
void router.push(
607+
{ pathname: route[0], query: { q: `docs(library/${library})`, referrer: `/user/libraries/${library}` } },
608+
{ pathname: route[1], query: { q: `docs(library/${library})`, referrer: `/user/libraries/${library}` } },
609+
);
610+
}
611+
}
612+
}, [route]);
613+
614+
const handleExportItemClick = curryN(2, (format: ExportApiFormatKey) => {
615+
setRoute([`/search/exportcitation/[format]`, `/search/exportcitation/${format}`]);
616+
});
617+
618+
return (
619+
<MenuGroup {...menuGroupProps} title="EXPORT">
620+
<MenuItem onClick={handleExportItemClick(ExportApiFormatKey.bibtex)}>in BibTeX</MenuItem>
621+
<MenuItem onClick={handleExportItemClick(ExportApiFormatKey.aastex)}>in AASTeX</MenuItem>
622+
<MenuItem onClick={handleExportItemClick(ExportApiFormatKey.endnote)}>in EndNote</MenuItem>
623+
<MenuItem onClick={handleExportItemClick(ExportApiFormatKey.ris)}>in RIS</MenuItem>
624+
<MenuItem onClick={handleExportItemClick(defaultExportFormatValue as ExportApiFormatKey)}>Other Formats</MenuItem>
625+
</MenuGroup>
626+
);
627+
};

src/pages/search/exportcitation/[format].tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import { exportCitationKeys, fetchExportCitation } from '@/api/export/export';
2727
interface IExportCitationPageProps {
2828
format: ExportApiFormatKey;
2929
query: IADSApiSearchParams;
30+
referrer?: string;
3031
error?: {
3132
status?: string;
3233
message?: string;
3334
};
3435
}
3536

3637
const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
37-
const { format, query } = props;
38+
const { format, query, referrer } = props;
3839
const isClient = useIsClient();
3940

4041
// get export related user settings
@@ -80,7 +81,7 @@ const ExportCitationPage: NextPage<IExportCitationPageProps> = (props) => {
8081
</Head>
8182
<Flex direction="column">
8283
<HStack my={10}>
83-
<SimpleLink href={getSearchHref()}>
84+
<SimpleLink href={referrer ?? getSearchHref()}>
8485
<ChevronLeftIcon w={8} h={8} />
8586
</SimpleLink>
8687
<Heading as="h2" fontSize="2xl">
@@ -125,6 +126,7 @@ export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx
125126
qid = null,
126127
p,
127128
format,
129+
referrer = null,
128130
...query
129131
} = parseQueryFromUrl<{ qid: string; format: string }>(ctx.req.url, { sortPostfix: 'id asc' });
130132

@@ -134,6 +136,7 @@ export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx
134136
format,
135137
query,
136138
qid,
139+
referrer,
137140
error: 'No Records',
138141
},
139142
};
@@ -181,6 +184,7 @@ export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx
181184
props: {
182185
format: exportParams.format,
183186
query: params,
187+
referrer,
184188
dehydratedState,
185189
},
186190
};

0 commit comments

Comments
 (0)