|
| 1 | +import React from 'react' |
| 2 | +import PropTypes from 'prop-types' |
| 3 | +import withStyles from '@mui/styles/withStyles' |
| 4 | +import Button from '@mui/material/Button' |
| 5 | +import Paper from '@mui/material/Paper' |
| 6 | +import { stateToUrl } from '../../helpers/helpers' |
| 7 | +import { Alert, AlertTitle, FormControl, FormHelperText, InputLabel, MenuItem, Select, TextField } from '@mui/material' |
| 8 | +import intl from 'react-intl-universal' |
| 9 | +import parse from 'html-react-parser' |
| 10 | + |
| 11 | +const styles = theme => ({ |
| 12 | + root: { |
| 13 | + height: 'calc(100% - 72px)', |
| 14 | + width: '100%', |
| 15 | + display: 'flex', |
| 16 | + alignItems: 'center', |
| 17 | + justifyContent: 'center', |
| 18 | + borderTop: '1px solid rgba(224, 224, 224, 1);', |
| 19 | + flexDirection: 'column' |
| 20 | + }, |
| 21 | + link: { |
| 22 | + textDecoration: 'none' |
| 23 | + }, |
| 24 | + button: { |
| 25 | + margin: theme.spacing(3) |
| 26 | + }, |
| 27 | + rightIcon: { |
| 28 | + marginLeft: theme.spacing(1) |
| 29 | + }, |
| 30 | + alert: { |
| 31 | + margin: theme.spacing(1), |
| 32 | + width: '75%', |
| 33 | + boxSizing: 'border-box' |
| 34 | + }, |
| 35 | + linkContainer: { |
| 36 | + margin: theme.spacing(1), |
| 37 | + display: 'flex', |
| 38 | + alignItems: 'center', |
| 39 | + flexDirection: 'column', |
| 40 | + width: '75%' |
| 41 | + }, |
| 42 | + linkField: { |
| 43 | + width: '100%', |
| 44 | + textOverflow: 'ellipsis' |
| 45 | + } |
| 46 | +}) |
| 47 | + |
| 48 | +/** |
| 49 | + * A component for creating a link to a specific search. |
| 50 | + */ |
| 51 | +class ExportLink extends React.Component { |
| 52 | + constructor (props) { |
| 53 | + super(props) |
| 54 | + this.state = { |
| 55 | + downloadLink: '', |
| 56 | + selectedView: 'table' |
| 57 | + } |
| 58 | + this.handleViewChange = this.handleViewChange.bind(this) |
| 59 | + } |
| 60 | + |
| 61 | + componentDidMount = () => { |
| 62 | + this.setState({ downloadLink: this.createDownloadLink() }) |
| 63 | + } |
| 64 | + |
| 65 | + componentDidUpdate = prevProps => { |
| 66 | + // check if filters have changed |
| 67 | + if (prevProps.facetUpdateID !== this.props.facetUpdateID) { |
| 68 | + this.setState({ downloadLink: this.createDownloadLink() }) |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + createDownloadLink = () => { |
| 73 | + const params = stateToUrl({ |
| 74 | + facetClass: this.props.facetClass, |
| 75 | + facets: this.props.facets |
| 76 | + }) |
| 77 | + |
| 78 | + const constraints = params.constraints ? params.constraints : [] |
| 79 | + const mappedConstraints = [] |
| 80 | + // go through constraints: |
| 81 | + for (const constraint of constraints) { |
| 82 | + const facetId = constraint.facetID |
| 83 | + const filterType = constraint.filterType |
| 84 | + let value |
| 85 | + |
| 86 | + switch (constraint.filterType) { |
| 87 | + case 'uriFilter': |
| 88 | + // go through each object (can have multiple) |
| 89 | + for (const [k, v] of Object.entries(this.props.facets[constraint.facetID].uriFilter)) { |
| 90 | + value = { |
| 91 | + path: v.path, |
| 92 | + node: { |
| 93 | + id: k, |
| 94 | + prefLabel: v.node.prefLabel, |
| 95 | + instanceCount: v.node.instanceCount, |
| 96 | + ...(v.node.parent && { parent: v.node.parent }), |
| 97 | + ...(v.node.children && { children: v.node.children.map(child => ({ id: child.id, prefLabel: child.prefLabel, instanceCount: child.instanceCount })) }) |
| 98 | + } |
| 99 | + } |
| 100 | + mappedConstraints.push({ facetId: facetId, filterType: filterType, value: value }) |
| 101 | + } |
| 102 | + break |
| 103 | + case 'textFilter': |
| 104 | + value = this.props.facets[constraint.facetID].textFilter |
| 105 | + mappedConstraints.push({ facetId: facetId, filterType: filterType, value: value }) |
| 106 | + break |
| 107 | + case 'dateFilter': |
| 108 | + case 'timespanFilter': |
| 109 | + case 'directTimespanFilter': |
| 110 | + value = [this.props.facets[constraint.facetID].timespanFilter.start, this.props.facets[constraint.facetID].timespanFilter.end] |
| 111 | + mappedConstraints.push({ facetId: facetId, filterType: filterType, value: value }) |
| 112 | + break |
| 113 | + case 'dateNoTimespanFilter': |
| 114 | + value = [this.props.facets[constraint.facetID].dateNoTimespanFilter.start, this.props.facets[constraint.facetID].dateNoTimespanFilter.end] |
| 115 | + mappedConstraints.push({ facetId: facetId, filterType: filterType, value: value }) |
| 116 | + break |
| 117 | + case 'integerFilter': |
| 118 | + case 'integerFilterRange': |
| 119 | + value = [this.props.facets[constraint.facetID].integerFilter.start, this.props.facets[constraint.facetID].integerFilter.end] |
| 120 | + mappedConstraints.push({ facetId: facetId, filterType: filterType, value: value }) |
| 121 | + break |
| 122 | + default: |
| 123 | + break |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + const pageNumber = this.props.data.page === -1 ? 0 : this.props.data.page |
| 128 | + const pageNumberString = this.state.selectedView === 'table' ? `page=${pageNumber}` : '' |
| 129 | + const constraintsString = mappedConstraints.length > 0 ? `constraints=${encodeURIComponent(JSON.stringify(mappedConstraints))}` : '' |
| 130 | + const separator = (pageNumberString.length > 0 && constraintsString.length > 0) ? '&' : '' |
| 131 | + const queryString = (pageNumberString.length > 0 || constraintsString.length > 0) ? `?${pageNumberString}${separator}${constraintsString}` : '' |
| 132 | + return `${this.props.rootUrl}/${this.props.facetClass}/faceted-search/${this.state.selectedView}${queryString}` |
| 133 | + } |
| 134 | + |
| 135 | + updateDownloadLink = () => { |
| 136 | + this.setState({ downloadLink: this.createDownloadLink() }) |
| 137 | + } |
| 138 | + |
| 139 | + handleViewChange (event) { |
| 140 | + this.setState({ selectedView: event.target.value }, () => { this.updateDownloadLink() }) |
| 141 | + } |
| 142 | + |
| 143 | + render = () => { |
| 144 | + const { classes } = this.props |
| 145 | + const fullLink = window.location.origin + this.state.downloadLink |
| 146 | + const fieldLabel = intl.get('exportLink.fieldLabel') ? intl.get('exportLink.fieldLabel') : 'Generated link (read-only)' |
| 147 | + const infoBody = intl.getHTML('exportLink.infoBody') ? intl.getHTML('exportLink.infoBody') : 'You can share the query you made by using the link below. The link is generated based on what you have selected for different facets and will open the search view of this perpsective with those choices on the selected tab. You can change the opened tab to any of the supported ones using the dropdown menu below. If you make additional choices while on this page, the link will be automatically updated to include those.' |
| 148 | + const warningTitle = intl.get('exportLink.warningTitle') ? intl.get('exportLink.warningTitle') : 'Generated link might be too long for some browsers' |
| 149 | + const warningBody = intl.getHTML('exportLink.warningBody') ? intl.getHTML('exportLink.warningBody') : parse('The current length of the generated link is more than 2,000 characters. Browsers have different limits for the maximum lengths of links they can handle. <strong>This link might not work on all browsers</strong> — you can reduce the length of the link by deselecting some facet options.') |
| 150 | + const errorTitle = intl.get('exportLink.errorTitle') ? intl.get('exportLink.errorTitle') : 'Generated link is too long' |
| 151 | + const errorBody = intl.getHTML('exportLink.errorBody') ? intl.getHTML('exportLink.errorBody') : parse('The current length of the generated link is more than 15,800 characters. <strong>The server will refuse to handle requests that go over certain length limits</strong> — you can reduce the length of the link by deselecting some facet options.') |
| 152 | + const copyLinkToClipboard = intl.get('exportLink.copyLinkToClipboard') ? intl.get('exportLink.copyLinkToClipboard') : 'Copy link to clipboard' |
| 153 | + |
| 154 | + const acceptedComponentTypes = ['ApexCharts', 'ApexChartsDouble', 'LeafletMap', 'Deck'] |
| 155 | + const defaultTab = this.props.perspectiveConfig.resultClasses[this.props.perspectiveConfig.id].paginatedResultsConfig |
| 156 | + const suitableResultClasses = Object.values(this.props.perspectiveConfig.resultClasses).filter(resultClass => acceptedComponentTypes.includes(resultClass.component)) |
| 157 | + return ( |
| 158 | + <Paper square className={classes.root}> |
| 159 | + <Alert severity='info' className={classes.alert}>{infoBody}</Alert> |
| 160 | + {fullLink.length > 2000 && fullLink.length <= 15800 ? (<Alert severity='warning' className={classes.alert}><AlertTitle>{warningTitle}</AlertTitle>{warningBody}</Alert>) : ''} |
| 161 | + {fullLink.length > 15800 ? (<Alert severity='error' className={classes.alert}><AlertTitle>{errorTitle}</AlertTitle>{errorBody}</Alert>) : ''} |
| 162 | + <div className={classes.linkContainer}> |
| 163 | + <FormControl sx={{ m: 1, minWidth: 120 }}> |
| 164 | + <InputLabel id='view-select-helper-label'>{intl.get('exportLink.viewLabel')}</InputLabel> |
| 165 | + <Select |
| 166 | + labelId='view-select-helper-label' |
| 167 | + value={this.state.selectedView} |
| 168 | + label={intl.get('exportLink.viewLabel')} |
| 169 | + onChange={this.handleViewChange} |
| 170 | + > |
| 171 | + <MenuItem value={defaultTab.tabPath} default>{intl.get(`tabs.${defaultTab.tabPath}`) ? intl.get(`tabs.${defaultTab.tabPath}`) : defaultTab.tabPath}</MenuItem> |
| 172 | + {suitableResultClasses.map(resultClass => <MenuItem value={resultClass.tabPath} key={resultClass.tabPath}>{intl.get(`tabs.${resultClass.tabPath}`) ? intl.get(`tabs.${resultClass.tabPath}`) : resultClass.tabPath}</MenuItem>)} |
| 173 | + </Select> |
| 174 | + <FormHelperText>{intl.get('exportLink.viewInstructions')}</FormHelperText> |
| 175 | + </FormControl> |
| 176 | + <TextField className={classes.linkField} label={fieldLabel} value={fullLink} InputProps={{ readOnly: true }} variant='filled' size='small' disabled={fullLink.length > 15800} /> |
| 177 | + <Button variant='contained' color='primary' className={classes.button} onClick={() => navigator.clipboard.writeText(fullLink)} disabled={fullLink.length > 15800}> |
| 178 | + {copyLinkToClipboard} |
| 179 | + </Button> |
| 180 | + </div> |
| 181 | + </Paper> |
| 182 | + ) |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +ExportLink.propTypes = { |
| 187 | + classes: PropTypes.object.isRequired, |
| 188 | + resultClass: PropTypes.string.isRequired, |
| 189 | + facetClass: PropTypes.string.isRequired, |
| 190 | + facets: PropTypes.object.isRequired, |
| 191 | + facetUpdateID: PropTypes.number.isRequired, |
| 192 | + rootUrl: PropTypes.string.isRequired, |
| 193 | + data: PropTypes.object.isRequired |
| 194 | +} |
| 195 | + |
| 196 | +export const ExportLinkComponent = ExportLink |
| 197 | + |
| 198 | +export default withStyles(styles)(ExportLink) |
0 commit comments