1
- import { ChevronLeftIcon , LockIcon , SettingsIcon , UnlockIcon } from '@chakra-ui/icons' ;
1
+ import { ChevronDownIcon , ChevronLeftIcon , LockIcon , SettingsIcon , UnlockIcon } from '@chakra-ui/icons' ;
2
2
import {
3
3
Box ,
4
4
Button ,
@@ -9,6 +9,17 @@ import {
9
9
HStack ,
10
10
Icon ,
11
11
IconButton ,
12
+ Menu ,
13
+ MenuButton ,
14
+ MenuDivider ,
15
+ MenuGroup ,
16
+ MenuGroupProps ,
17
+ MenuItem ,
18
+ MenuItemOption ,
19
+ MenuList ,
20
+ MenuOptionGroup ,
21
+ Portal ,
22
+ Stack ,
12
23
Table ,
13
24
Tbody ,
14
25
Td ,
@@ -27,8 +38,8 @@ import { BuildingLibraryIcon, ShareIcon } from '@heroicons/react/24/solid';
27
38
28
39
import { AppState , useStore } from '@/store' ;
29
40
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' ;
32
43
import { DocumentList } from './DocumentList/DocumentList' ;
33
44
import { SimpleLink } from '@/components/SimpleLink' ;
34
45
import { CustomInfoMessage , LoadingMessage } from '@/components/Feedbacks' ;
@@ -40,11 +51,16 @@ import { isBiblibSort, isSolrSort } from '@/utils/common/guards';
40
51
import { normalizeSolrSort } from '@/utils/common/search' ;
41
52
import { parseAPIError } from '@/utils/common/parseAPIError' ;
42
53
import { LibraryIdentifier } from '@/api/biblib/types' ;
43
- import { IDocsEntity } from '@/api/search/types' ;
54
+ import { Bibcode , IDocsEntity } from '@/api/search/types' ;
44
55
import { BiblibSort , BiblibSortField } from '@/api/models' ;
45
56
import { useEditLibraryDocuments , useGetLibraryEntity } from '@/api/biblib/libraries' ;
46
57
import { useBigQuerySearch } from '@/api/search/search' ;
47
58
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' ;
48
64
49
65
export interface ILibraryEntityPaneProps {
50
66
id : LibraryIdentifier ;
@@ -249,7 +265,7 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
249
265
< LoadingMessage message = "Loading library" />
250
266
</ Center >
251
267
) }
252
- { errorFetchingLibs && (
268
+ { ! isLoadingLibs && errorFetchingLibs && (
253
269
< CustomInfoMessage
254
270
status = { 'error' }
255
271
title = { 'Library not found' }
@@ -395,15 +411,16 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
395
411
</ SearchQueryLink >
396
412
</ Flex >
397
413
398
- { canWrite && ! publicView && (
414
+ { canWrite && ! publicView && ! isLoadingDocs && (
399
415
< Box style = { isLoadingDocs ? { pointerEvents : 'none' } : { pointerEvents : 'auto' } } w = "full" >
400
- < BulkAction
416
+ < BulkMenu
417
+ library = { id }
401
418
isAllSelected = { isAllSelected }
402
419
isSomeSelected = { isSomeSelected }
403
420
onSelectAllCurrent = { handleSelectAllCurrent }
404
421
onClearAllCurrent = { handleClearAllCurrent }
405
422
onClearAll = { handleClearAll }
406
- selectedCount = { selected . length }
423
+ selectedDocs = { selected }
407
424
onDeleteSelected = { handleDeleteFromLibrary }
408
425
/>
409
426
</ Box >
@@ -447,28 +464,44 @@ export const LibraryEntityPane = ({ id, publicView }: ILibraryEntityPaneProps) =
447
464
) ;
448
465
} ;
449
466
450
- const BulkAction = ( {
467
+ const BulkMenu = ( {
468
+ library,
451
469
isAllSelected,
452
470
isSomeSelected,
453
471
onSelectAllCurrent,
454
472
onClearAllCurrent,
455
473
onClearAll,
456
- selectedCount ,
474
+ selectedDocs ,
457
475
onDeleteSelected,
458
476
} : {
477
+ library : string ;
459
478
isAllSelected : boolean ;
460
479
isSomeSelected : boolean ;
461
- selectedCount : number ;
480
+ selectedDocs : string [ ] ;
462
481
onSelectAllCurrent : ( ) => void ;
463
482
onClearAllCurrent : ( ) => void ;
464
483
onClearAll : ( ) => void ;
465
484
onDeleteSelected : ( ) => void ;
466
485
} ) => {
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
+
467
496
const handleChange = ( ) => {
468
497
isAllSelected || isSomeSelected ? onClearAllCurrent ( ) : onSelectAllCurrent ( ) ;
469
498
} ;
470
499
471
- const { panel : PanelBackground } = useColorModeColors ( ) ;
500
+ const handleApplyOption = ( value : string | string [ ] ) => {
501
+ if ( typeof value === 'string' ) {
502
+ setApplyToAll ( value === 'all' ) ;
503
+ }
504
+ } ;
472
505
473
506
return (
474
507
< Flex justifyContent = "space-between" backgroundColor = { PanelBackground } p = { 4 } >
@@ -487,23 +520,108 @@ const BulkAction = ({
487
520
data-testid = "select-all-checkbox"
488
521
/>
489
522
</ Tooltip >
490
- { selectedCount > 0 && (
523
+ { selectedDocs . length > 0 && (
491
524
< >
492
- < > { selectedCount } selected</ >
525
+ < > { selectedDocs . length } selected</ >
493
526
< Button variant = "link" onClick = { onClearAll } >
494
527
Clear All
495
528
</ Button >
496
529
</ >
497
530
) }
498
531
</ 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 >
507
565
</ Flex >
508
566
) ;
509
567
} ;
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
+ } ;
0 commit comments