16
16
17
17
import React from 'react' ;
18
18
import PropTypes from 'prop-types' ;
19
+ import { computed } from 'mobx' ;
20
+ import { inject , observer } from 'mobx-react' ;
21
+ import { Link } from 'react-router' ;
19
22
import {
20
23
Dropdown ,
21
24
Icon ,
22
- message
25
+ message ,
26
+ Button ,
27
+ Modal
23
28
} from 'antd' ;
24
- import Menu , { MenuItem } from 'rc-menu' ;
29
+ import Menu , { MenuItem , Divider } from 'rc-menu' ;
25
30
import FileSaver from 'file-saver' ;
26
31
import ExportConfigurationModal from './configuration-modal' ;
27
32
import { getSortingPayload } from '../../utilities' ;
@@ -34,8 +39,14 @@ import {
34
39
CloudPath ,
35
40
MountPath
36
41
} from '../../utilities/document-columns' ;
42
+ import roleModel from '../../../../../utils/roleModel' ;
37
43
import FacetedSearchExport from '../../../../../models/search/faceted-search-export' ;
44
+ // eslint-disable-next-line max-len
45
+ import FacetedSearchExportTemplatesSave from '../../../../../models/search/faceted-search-export-templates-save' ;
46
+ // eslint-disable-next-line max-len
47
+ import FacetedSearchExportTemplates from '../../../../../models/search/faceted-search-export-templates' ;
38
48
import checkBlob from '../../../../../utils/check-blob' ;
49
+ import displayDate from '../../../../../utils/displayDate' ;
39
50
40
51
const exportVOColumns = {
41
52
[ Name . key ] : 'includeName' ,
@@ -47,9 +58,33 @@ const exportVOColumns = {
47
58
[ MountPath . key ] : 'includeMountPath'
48
59
} ;
49
60
50
- function ExportMenu ( { onExport, onConfigure} ) {
61
+ function parseExportTemplates ( rawTemplates = { } ) {
62
+ return Object . entries ( rawTemplates ) . map ( ( [ key , template ] ) => ( {
63
+ ...template ,
64
+ key
65
+ } ) ) ;
66
+ }
67
+
68
+ function checkTemplatePermissions ( userInfo , template ) {
69
+ if ( ! userInfo ) {
70
+ return false ;
71
+ }
72
+ const { userName, groups = [ ] } = userInfo ;
73
+ return template . permissions . some ( permission => {
74
+ if ( permission . principal ) {
75
+ return permission . name === userName ;
76
+ }
77
+ return roleModel . userHasRole (
78
+ userInfo ,
79
+ ( permission . name || '' ) . toUpperCase ( )
80
+ ) || groups . includes ( permission . nzme ) ;
81
+ } ) ;
82
+ }
83
+
84
+ function ExportMenu ( { onExport, onExportTemplate, onConfigure, templates} ) {
51
85
const handle = ( { key} ) => {
52
- switch ( key ) {
86
+ const [ exportType , exportKey ] = key . split ( '|' ) ;
87
+ switch ( exportType ) {
53
88
case 'export' :
54
89
if ( typeof onExport === 'function' ) {
55
90
onExport ( ) ;
@@ -60,10 +95,24 @@ function ExportMenu ({onExport, onConfigure}) {
60
95
onConfigure ( ) ;
61
96
}
62
97
break ;
98
+ case 'template' :
99
+ if ( typeof onExportTemplate === 'function' ) {
100
+ const currentTemplate = templates . find ( ( { key} ) => key === exportKey ) ;
101
+ onExportTemplate ( currentTemplate ) ;
102
+ }
103
+ break ;
63
104
default :
64
105
break ;
65
106
}
66
107
} ;
108
+ const templatesSection = templates . length ? [
109
+ < Divider key = "divider" /> ,
110
+ ...templates . map ( template => (
111
+ < MenuItem key = { `template|${ template . key } ` } >
112
+ { template [ 'friendly_name' ] || template . key }
113
+ </ MenuItem >
114
+ ) )
115
+ ] : [ ] ;
67
116
return (
68
117
< Menu
69
118
onClick = { handle }
@@ -78,17 +127,64 @@ function ExportMenu ({onExport, onConfigure}) {
78
127
< Icon type = "bars" style = { { marginRight : 10 } } />
79
128
Custom configuration
80
129
</ MenuItem >
130
+ { templatesSection }
81
131
</ Menu >
82
132
) ;
83
133
}
84
134
135
+ @inject ( 'preferences' , 'authenticatedUserInfo' , 'dataStorages' )
136
+ @observer
85
137
class ExportButton extends React . Component {
86
138
state = {
87
139
pending : false ,
88
140
modalVisible : false ,
89
141
dropdownVisible : false
90
142
} ;
91
143
144
+ @computed
145
+ get exportTemplates ( ) {
146
+ const { preferences} = this . props ;
147
+ if ( preferences ?. loaded ) {
148
+ return preferences . searchExportTemplates ;
149
+ }
150
+ return undefined ;
151
+ }
152
+
153
+ @computed
154
+ get isAdmin ( ) {
155
+ const { authenticatedUserInfo} = this . props ;
156
+ if ( authenticatedUserInfo . loaded ) {
157
+ return authenticatedUserInfo . value . admin ;
158
+ }
159
+ return false ;
160
+ }
161
+
162
+ getFacetedSearchExportPayload = ( configuration ) => {
163
+ const {
164
+ advanced,
165
+ columns = [ ] ,
166
+ query : currentQuery ,
167
+ filters = { } ,
168
+ sorting = [ ] ,
169
+ facets = [ ]
170
+ } = this . props ;
171
+ const columnsToExport = configuration && configuration . length
172
+ ? columns . filter ( ( aColumn ) => configuration . includes ( aColumn . key ) )
173
+ : columns . slice ( ) ;
174
+ const keys = columnsToExport . map ( ( column ) => column . key ) ;
175
+ const metadataFields = keys . filter ( ( key ) => ! exportVOColumns [ key ] ) ;
176
+ return {
177
+ query : ! advanced && currentQuery
178
+ ? `*${ currentQuery } *`
179
+ : ( currentQuery || '*' ) ,
180
+ filters,
181
+ sorts : getSortingPayload ( sorting ) ,
182
+ metadataFields,
183
+ facets : facets . map ( ( facet ) => facet . name ) ,
184
+ highlight : false
185
+ } ;
186
+ } ;
187
+
92
188
openModal = ( ) => {
93
189
this . setState ( {
94
190
modalVisible : true ,
@@ -118,39 +214,22 @@ class ExportButton extends React.Component {
118
214
this . setState ( state , ( ) => resolve ( ) ) ) ;
119
215
const hide = message . loading ( 'Exporting...' , 0 ) ;
120
216
await setStateAwaited ( { pending : true } ) ;
121
- const {
122
- advanced,
123
- columns = [ ] ,
124
- query : currentQuery ,
125
- filters = { } ,
126
- sorting = [ ] ,
127
- facets = [ ]
128
- } = this . props ;
217
+ const { columns = [ ] } = this . props ;
129
218
const columnsToExport = configuration && configuration . length
130
219
? columns . filter ( ( aColumn ) => configuration . includes ( aColumn . key ) )
131
220
: columns . slice ( ) ;
132
221
try {
133
222
const keys = columnsToExport . map ( ( column ) => column . key ) ;
134
- const metadataFields = keys . filter ( ( key ) => ! exportVOColumns [ key ] ) ;
135
223
const facetedSearchExportVO = Object . entries ( exportVOColumns )
136
224
. reduce ( ( acc , [ key , exportVOKey ] ) => ( {
137
225
...acc ,
138
226
[ exportVOKey ] : ! ! keys . includes ( key )
139
227
} ) , { delimiter : ',' } ) ;
140
228
const csvFileName = 'export.csv' ;
141
229
const payload = {
142
- csvFileName : 'export.csv' ,
230
+ csvFileName,
143
231
facetedSearchExportVO,
144
- facetedSearchRequest : {
145
- query : ! advanced && currentQuery
146
- ? `*${ currentQuery } *`
147
- : ( currentQuery || '*' ) ,
148
- filters,
149
- sorts : getSortingPayload ( sorting ) ,
150
- metadataFields,
151
- facets : facets . map ( ( facet ) => facet . name ) ,
152
- highlight : false
153
- }
232
+ facetedSearchRequest : this . getFacetedSearchExportPayload ( configuration )
154
233
} ;
155
234
const request = new FacetedSearchExport ( ) ;
156
235
await request . send ( payload ) ;
@@ -175,6 +254,89 @@ class ExportButton extends React.Component {
175
254
176
255
onDefaultExport = ( ) => this . onExport ( ) ;
177
256
257
+ onExportTemplate = async ( template , payload ) => {
258
+ const downloadExport = async ( template ) => {
259
+ const request = new FacetedSearchExportTemplates ( template . key ) ;
260
+ await request . send ( payload ) ;
261
+ if ( request . error ) {
262
+ return message . error ( request . error , 5 ) ;
263
+ }
264
+ if ( request . value instanceof Blob ) {
265
+ const error = await checkBlob ( request . value , 'Error exporting search results' ) ;
266
+ if ( error ) {
267
+ return message . error ( error . message , 5 ) ;
268
+ }
269
+ const fileName = `${ template . key } -${ displayDate ( Date . now ( ) , 'YYYY-MM-DD HH:mm:ss' ) } .xls` ;
270
+ FileSaver . saveAs ( request . value , fileName ) ;
271
+ }
272
+ } ;
273
+ const saveExport = async ( template , payload ) => {
274
+ const request = new FacetedSearchExportTemplatesSave ( template . key ) ;
275
+ await request . send ( payload ) ;
276
+ if ( request . error ) {
277
+ return message . error ( request . error , 5 ) ;
278
+ }
279
+ this . setState ( { savedExport : request . value } ) ;
280
+ } ;
281
+ const uploadToBucket = ! ! template . save_to ;
282
+ this . closeModal ( ) ;
283
+ const setStateAwaited = ( state ) =>
284
+ new Promise ( ( resolve ) =>
285
+ this . setState ( state , ( ) => resolve ( ) ) ) ;
286
+ const hide = message . loading ( uploadToBucket ? 'Exporting...' : 'Downloading...' , 0 ) ;
287
+ await setStateAwaited ( { pending : true } ) ;
288
+ try {
289
+ const payload = this . getFacetedSearchExportPayload ( ) ;
290
+ if ( uploadToBucket ) {
291
+ await saveExport ( template , payload ) ;
292
+ } else {
293
+ await downloadExport ( template , payload ) ;
294
+ }
295
+ } catch ( e ) {
296
+ message . error ( e . message , 5 ) ;
297
+ } finally {
298
+ hide ( ) ;
299
+ this . setState ( {
300
+ pending : false
301
+ } ) ;
302
+ }
303
+ } ;
304
+
305
+ closeExportResultModal = ( ) => this . setState ( { savedExport : undefined } ) ;
306
+
307
+ openExportResultModal = template => this . setState ( { savedExport : template } ) ;
308
+
309
+ renderSavedExportContent = ( ) => {
310
+ const { savedExport} = this . state ;
311
+ const getStorageById = ( id ) => {
312
+ const {
313
+ dataStorages
314
+ } = this . props ;
315
+ if ( dataStorages . loaded ) {
316
+ return ( dataStorages . value || [ ] ) . find ( d => Number ( d . id ) === Number ( id ) ) ;
317
+ }
318
+ return undefined ;
319
+ } ;
320
+ if ( ! savedExport ) {
321
+ return null ;
322
+ }
323
+ const storage = getStorageById ( savedExport . storageId ) ;
324
+ const path = savedExport . storagePath . split ( '/' ) ;
325
+ const file = path . pop ( ) ;
326
+ const folder = path . join ( '/' ) ;
327
+ return (
328
+ < div style = { { paddingRight : 25 } } >
329
+ Data exported to
330
+ < Link
331
+ style = { { marginLeft : 5 } }
332
+ to = { `storage/${ savedExport . storageId } ?path=${ folder } ` }
333
+ >
334
+ { `${ storage ?. name || savedExport . storageId } /${ folder } /${ file } ` }
335
+ </ Link > .
336
+ </ div >
337
+ ) ;
338
+ } ;
339
+
178
340
render ( ) {
179
341
const {
180
342
columns,
@@ -187,12 +349,25 @@ class ExportButton extends React.Component {
187
349
modalVisible,
188
350
dropdownVisible
189
351
} = this . state ;
352
+ const templates = parseExportTemplates ( this . exportTemplates )
353
+ . filter ( template => {
354
+ if ( ! template . permissions || this . isAdmin ) {
355
+ return true ;
356
+ }
357
+ if ( ! this . props . authenticatedUserInfo . loaded ) {
358
+ return false ;
359
+ }
360
+ return checkTemplatePermissions ( this . props . authenticatedUserInfo . value , template ) ;
361
+ } ) ;
190
362
return (
191
363
< Dropdown . Button
192
364
overlay = { (
193
365
< ExportMenu
194
366
onExport = { this . onDefaultExport }
367
+ onExportTemplate = { this . onExportTemplate }
195
368
onConfigure = { this . onConfigure }
369
+ templates = { templates }
370
+ storages = { this . storages }
196
371
/>
197
372
) }
198
373
className = { className }
@@ -212,6 +387,25 @@ class ExportButton extends React.Component {
212
387
onExport = { this . onExport }
213
388
columns = { columns }
214
389
/>
390
+ < Modal
391
+ title = { null }
392
+ visible = { ! ! this . state . savedExport }
393
+ onCancel = { this . closeExportResultModal }
394
+ width = "fit-content"
395
+ style = { {
396
+ minWidth : '30vw' ,
397
+ maxWidth : '80vw'
398
+ } }
399
+ footer = {
400
+ < Button
401
+ type = "primary"
402
+ onClick = { this . closeExportResultModal } >
403
+ OK
404
+ </ Button >
405
+ }
406
+ >
407
+ { this . renderSavedExportContent ( ) }
408
+ </ Modal >
215
409
</ Dropdown . Button >
216
410
) ;
217
411
}
0 commit comments