Commit 7f5f6754 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Continue to implement the edit user metadata dialog.

parent 20edd70d
...@@ -15,51 +15,17 @@ import ReactJson from 'react-json-view' ...@@ -15,51 +15,17 @@ import ReactJson from 'react-json-view'
import Autosuggest from 'react-autosuggest' import Autosuggest from 'react-autosuggest'
import match from 'autosuggest-highlight/match' import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse' import parse from 'autosuggest-highlight/parse'
import deburr from 'lodash/deburr' import { compose } from 'recompose'
import { withApi } from './api'
// TODO replace with the actual authors
const suggestions = [ class SuggestionsTextFieldUnstyled extends React.Component {
{ label: 'Afghanistan' },
{ label: 'Aland Islands' },
{ label: 'Albania' },
{ label: 'Algeria' },
{ label: 'American Samoa' },
{ label: 'Andorra' },
{ label: 'Angola' },
{ label: 'Anguilla' },
{ label: 'Antarctica' },
{ label: 'Antigua and Barbuda' },
{ label: 'Argentina' },
{ label: 'Armenia' },
{ label: 'Aruba' },
{ label: 'Australia' },
{ label: 'Austria' },
{ label: 'Azerbaijan' },
{ label: 'Bahamas' },
{ label: 'Bahrain' },
{ label: 'Bangladesh' },
{ label: 'Barbados' },
{ label: 'Belarus' },
{ label: 'Belgium' },
{ label: 'Belize' },
{ label: 'Benin' },
{ label: 'Bermuda' },
{ label: 'Bhutan' },
{ label: 'Bolivia, Plurinational State of' },
{ label: 'Bonaire, Sint Eustatius and Saba' },
{ label: 'Bosnia and Herzegovina' },
{ label: 'Botswana' },
{ label: 'Bouvet Island' },
{ label: 'Brazil' },
{ label: 'British Indian Ocean Territory' },
{ label: 'Brunei Darussalam' }
]
class AuthorTextFieldUnstyled extends React.Component {
static propTypes = { static propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
suggestions: PropTypes.func.isRequired,
suggestionValue: PropTypes.func.isRequired,
suggestionRendered: PropTypes.func.isRequired
} }
static styles = theme => ({ static styles = theme => ({
...@@ -79,8 +45,37 @@ class AuthorTextFieldUnstyled extends React.Component { ...@@ -79,8 +45,37 @@ class AuthorTextFieldUnstyled extends React.Component {
} }
}) })
constructor(props) {
super(props)
this.lastRequestId = null
}
loadSuggestions(value) {
if (this.state.isLoading) {
return
}
if (this.lastRequestId !== null) {
clearTimeout(this.lastRequestId)
}
this.setState({
isLoading: true
})
this.lastRequestId = setTimeout(() => {
this.props.suggestions(value).then(suggestions => {
this.setState({
isLoading: false,
suggestions: suggestions
})
})
}, 1000)
}
state = { state = {
suggestions: [], suggestions: [],
isLoading: false,
anchorEl: null anchorEl: null
} }
...@@ -105,8 +100,9 @@ class AuthorTextFieldUnstyled extends React.Component { ...@@ -105,8 +100,9 @@ class AuthorTextFieldUnstyled extends React.Component {
} }
renderSuggestion(suggestion, { query, isHighlighted }) { renderSuggestion(suggestion, { query, isHighlighted }) {
const matches = match(suggestion.label, query) suggestion = this.props.suggestionRendered(suggestion)
const parts = parse(suggestion.label, matches) const matches = match(suggestion, query)
const parts = parse(suggestion, matches)
return ( return (
<MenuItem selected={isHighlighted} component="div"> <MenuItem selected={isHighlighted} component="div">
...@@ -121,35 +117,12 @@ class AuthorTextFieldUnstyled extends React.Component { ...@@ -121,35 +117,12 @@ class AuthorTextFieldUnstyled extends React.Component {
) )
} }
getSuggestions(value) {
const inputValue = deburr(value.trim()).toLowerCase()
const inputLength = inputValue.length
let count = 0
return inputLength === 0
? []
: suggestions.filter(suggestion => {
const keep =
count < 5 && suggestion.label.slice(0, inputLength).toLowerCase() === inputValue
if (keep) {
count += 1
}
return keep
})
}
getSuggestionValue(suggestion) {
return suggestion.label
}
render() { render() {
const { classes, onChange, value, ...props } = this.props const { classes, onChange, value, suggestions, suggestionValue, suggestionRendered, ...props } = this.props
const { suggestions, anchorEl } = this.state const { anchorEl } = this.state
const handleSuggestionsFetchRequested = ({ value }) => { const handleSuggestionsFetchRequested = ({ value }) => {
this.setState({suggestions: this.getSuggestions(value)}) this.loadSuggestions(value)
} }
const handleSuggestionsClearRequested = () => { const handleSuggestionsClearRequested = () => {
...@@ -162,10 +135,10 @@ class AuthorTextFieldUnstyled extends React.Component { ...@@ -162,10 +135,10 @@ class AuthorTextFieldUnstyled extends React.Component {
const autosuggestProps = { const autosuggestProps = {
renderInputComponent: this.renderInputComponent.bind(this), renderInputComponent: this.renderInputComponent.bind(this),
suggestions, suggestions: this.state.suggestions,
onSuggestionsFetchRequested: handleSuggestionsFetchRequested, onSuggestionsFetchRequested: handleSuggestionsFetchRequested,
onSuggestionsClearRequested: handleSuggestionsClearRequested, onSuggestionsClearRequested: handleSuggestionsClearRequested,
getSuggestionValue: this.getSuggestionValue.bind(this), getSuggestionValue: suggestionValue,
renderSuggestion: this.renderSuggestion.bind(this) renderSuggestion: this.renderSuggestion.bind(this)
} }
...@@ -206,7 +179,7 @@ class AuthorTextFieldUnstyled extends React.Component { ...@@ -206,7 +179,7 @@ class AuthorTextFieldUnstyled extends React.Component {
} }
} }
const AuthorTextField = withStyles(AuthorTextFieldUnstyled.styles)(AuthorTextFieldUnstyled) const SuggestionsTextField = withStyles(SuggestionsTextFieldUnstyled.styles)(SuggestionsTextFieldUnstyled)
var urlPattern = new RegExp('^(https?:\\/\\/)?' + // protocol var urlPattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|' + // domain name '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|' + // domain name
...@@ -216,7 +189,7 @@ var urlPattern = new RegExp('^(https?:\\/\\/)?' + // protocol ...@@ -216,7 +189,7 @@ var urlPattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator '(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
function isURL(str) { function isURL(str) {
return urlPattern.test(str) return str === '' || urlPattern.test(str.trim())
} }
class ListTextInputUnstyled extends React.Component { class ListTextInputUnstyled extends React.Component {
...@@ -308,55 +281,113 @@ class ListTextInputUnstyled extends React.Component { ...@@ -308,55 +281,113 @@ class ListTextInputUnstyled extends React.Component {
const ListTextInput = withStyles(ListTextInputUnstyled.styles)(ListTextInputUnstyled) const ListTextInput = withStyles(ListTextInputUnstyled.styles)(ListTextInputUnstyled)
class AuthorsListTextInput extends React.Component { class SuggestionsListTextInput extends React.Component {
render() { render() {
return <ListTextInput component={AuthorTextField} {...this.props} /> return <ListTextInput component={SuggestionsTextField} {...this.props} />
} }
} }
class EditUserMetadataDialog extends React.Component { class EditUserMetadataDialogUnstyled extends React.Component {
static propTypes = { static propTypes = {
query: PropTypes.object classes: PropTypes.object.isRequired,
total: PropTypes.number,
example: PropTypes.object,
buttonProps: PropTypes.object,
api: PropTypes.object.isRequired
}
static styles = theme => ({
dialog: {
width: '100%'
}
})
constructor(props) {
super(props)
this.handleButtonClick = this.handleButtonClick.bind(this)
} }
state = { state = {
open: false, open: false,
comment: 'This is the existing comment and it is very long. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', editData: {
references: ['http://reference1', 'http://reference2'], comment: '',
coAuthors: ['Scheidgen, Markus'], references: [],
sharedWith: [], coAuthors: [],
datasets: [], sharedWith: [],
withEmbargo: true datasets: [],
withEmbargo: true
}
}
update() {
const { example } = this.props
const editData = {
comment: example.comment || '',
references: example.references || [],
coAuthors: example.authors.filter(author => author.user_id !== example.uploader.user_id).map(author => author.email),
sharedWith: example.owners.filter(author => author.user_id !== example.uploader.user_id).map(author => author.email),
datasets: (example.datasets || []).map(ds => ds.name),
withEmbargo: example.with_embargo
}
this.setState({editData: editData})
}
componentDidMount() {
this.update()
} }
handleChange(key, value) { componentDidUpdate(prevProps) {
this.setState({[key]: value}) if (prevProps.example.calc_id !== this.props.example.calc_id) {
this.update()
}
}
handleButtonClick() {
const { open } = this.state
if (!open) {
this.update()
}
this.setState({open: !open})
} }
render() { render() {
const { query, ...buttonProps } = this.props const { classes, buttonProps, total, api } = this.props
const { open } = this.state const { open } = this.state
const close = () => this.setState({open: false}) const close = () => this.setState({open: false})
const handleChange = (key, value) => {
this.setState({editData: {...this.state.editData, [key]: value}})
}
const value = key => this.state.editData[key]
const userSuggestions = query => {
return api.getUsers(query)
.then(result => result.users)
.catch((err) => {
console.log(err)
return []
})
}
return ( return (
<React.Fragment> <React.Fragment>
<Tooltip title="Edit user metadata"> <Tooltip title="Edit user metadata">
<IconButton {...buttonProps} onClick={() => this.setState({open: true})}> <IconButton {...(buttonProps || {})} onClick={this.handleButtonClick}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog open={open} onClose={close} disableBackdropClick disableEscapeKeyDown> <Dialog classes={{paper: classes.dialog}} open={open} onClose={close} disableBackdropClick disableEscapeKeyDown>
<DialogTitle>Edit the user metadata of X entries</DialogTitle> <DialogTitle>Edit the user metadata of {total} entries</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
To subscribe to this website, please enter your email address here. We will send updates TODO better text
occasionally.
</DialogContentText> </DialogContentText>
<TextField <TextField
id="comment" id="comment"
label="Comment" label="Comment"
value={this.state.comment} value={value('comment')}
onChange={event => this.handleChange('comment', event.target.value)} onChange={event => handleChange('comment', event.target.value)}
margin="normal" margin="normal"
multiline multiline
fullWidth fullWidth
...@@ -366,45 +397,66 @@ class EditUserMetadataDialog extends React.Component { ...@@ -366,45 +397,66 @@ class EditUserMetadataDialog extends React.Component {
label="References" label="References"
errorLabel="References must be valid URLs" errorLabel="References must be valid URLs"
placeholder="Add a URL reference" placeholder="Add a URL reference"
values={this.state.references} values={value('references')}
onChange={values => this.handleChange('references', values)} onChange={values => handleChange('references', values)}
validate={isURL} validate={isURL}
fullWidth fullWidth
/> />
<AuthorsListTextInput <SuggestionsListTextInput
suggestions={userSuggestions}
suggestionValue={v => v.email}
suggestionRendered={v => `${v.name} (${v.email})`}
id="coAuthors" id="coAuthors"
label="Co-authors" label="Co-authors"
placeholder="Add a co-author by name" placeholder="Add a co-author by name"
values={this.state.coAuthors} values={value('coAuthors')}
onChange={values => this.handleChange('coAuthors', values)} onChange={values => handleChange('coAuthors', values)}
fullWidth fullWidth
/> />
<AuthorsListTextInput <SuggestionsListTextInput
suggestions={userSuggestions}
suggestionValue={v => v.email}
suggestionRendered={v => `${v.name} (${v.email})`}
id="sharedWith" id="sharedWith"
label="Shared with" label="Shared with"
placeholder="Add a user by name to share with" placeholder="Add a user by name to share with"
values={this.state.sharedWith} values={value('sharedWith')}
onChange={values => this.handleChange('sharedWith', values)} onChange={values => handleChange('sharedWith', values)}
fullWidth fullWidth
/> />
<ListTextInput <SuggestionsListTextInput
suggestions={prefix => {
console.log(prefix)
return api.getDatasets(prefix)
.then(result => result.results.map(ds => ds.name))
.catch((err) => {
console.log(err)
return []
})
}}
suggestionValue={v => v}
suggestionRendered={v => v}
id="datasets" id="datasets"
label="Datasets" label="Datasets"
placeholder="Add a dataset" placeholder="Add a dataset"
values={this.state.datasets} values={value('datasets')}
onChange={values => this.handleChange('datasets', values)} onChange={values => handleChange('datasets', values)}
fullWidth fullWidth
/> />
</DialogContent> </DialogContent>
<DialogContent> <DialogContent>
<ReactJson src={this.props.query} enableClipboard={false} collapsed={0} /> <ReactJson
src={this.state.editData}
enableClipboard={false}
collapsed={0}
/>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={close} color="primary"> <Button onClick={close} color="primary">
Cancel Cancel
</Button> </Button>
<Button onClick={close} color="primary"> <Button onClick={close} color="primary">
Subscribe Submit
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
...@@ -413,4 +465,4 @@ class EditUserMetadataDialog extends React.Component { ...@@ -413,4 +465,4 @@ class EditUserMetadataDialog extends React.Component {
} }
} }
export default EditUserMetadataDialog export default compose(withApi(false, false), withStyles(EditUserMetadataDialogUnstyled.styles))(EditUserMetadataDialogUnstyled)
...@@ -326,6 +326,33 @@ class Api { ...@@ -326,6 +326,33 @@ class Api {
.finally(this.onFinishLoading) .finally(this.onFinishLoading)
} }
async getDatasets(prefix) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.datasets.list_datasets({prefix: prefix}))
.catch(handleApiError)
.then(response => response.body)
.finally(this.onFinishLoading)
}
async getUsers(query) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.auth.get_users({query: query}))
.catch(handleApiError)
.then(response => response.body)
.finally(this.onFinishLoading)
}
async quantities_search(search) {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.repo.quantities_search(search))
.catch(handleApiError)
.then(response => response.body)
.finally(this.onFinishLoading)
}
async deleteUpload(uploadId) { async deleteUpload(uploadId) {
this.onStartLoading() this.onStartLoading()
return this.swagger() return this.swagger()
......
...@@ -208,7 +208,7 @@ class EntryListUnstyled extends React.Component { ...@@ -208,7 +208,7 @@ class EntryListUnstyled extends React.Component {
renderEntryActions(row) { renderEntryActions(row) {
return <React.Fragment> return <React.Fragment>
<EditUserMetadataDialog query={{calc_id: row.calc_id}}/> <EditUserMetadataDialog example={row} total={1} />
<Tooltip title="Download raw files"> <Tooltip title="Download raw files">
<IconButton> <IconButton>
<DownloadIcon /> <DownloadIcon />
...@@ -225,6 +225,7 @@ class EntryListUnstyled extends React.Component { ...@@ -225,6 +225,7 @@ class EntryListUnstyled extends React.Component {
render() { render() {
const { classes, data, order, order_by, page, per_page, domain, editable } = this.props const { classes, data, order, order_by, page, per_page, domain, editable } = this.props
const { results, pagination: { total } } = data const { results, pagination: { total } } = data
const { selected } = this.state
const columns = { const columns = {
...domain.searchResultColumns, ...domain.searchResultColumns,
...@@ -243,8 +244,12 @@ class EntryListUnstyled extends React.Component { ...@@ -243,8 +244,12 @@ class EntryListUnstyled extends React.Component {
onChangeRowsPerPage={this.handleChangeRowsPerPage} onChangeRowsPerPage={this.handleChangeRowsPerPage}
/> />
const example = selected ? data.results.find(d => d.calc_id === selected[0]) : data.results[0]
const selectActions = editable ? <React.Fragment> const selectActions = editable ? <React.Fragment>
<EditUserMetadataDialog color="primary" query={this.selectionQuery()}/> <EditUserMetadataDialog
buttonProps={{color: 'primary'}}
example={example} total={total}
/>
<Tooltip title="Download raw files"> <Tooltip title="Download raw files">
<IconButton color="primary"> <IconButton color="primary">
<DownloadIcon /> <DownloadIcon />
......
...@@ -220,6 +220,31 @@ class AuthResource(Resource): ...@@ -220,6 +220,31 @@ class AuthResource(Resource):
abort(401, 'The authenticated user does not exist') abort(401, 'The authenticated user does not exist')
users_model = api.model('UsersModel', {
'users': fields.Nested(api.model('UserModel', {
'name': fields.String(description='The full name of the user as presented in the UI.'),
'user_id': fields.String(description='The unique user UUID.'),
'email': fields.String(description='The email.')
}))
})
users_parser = api.parser()