Commit 5ec2508d authored by Matija Obreza's avatar Matija Obreza

Merge branch '268-add-accessions-by-name-to-my-list' into 'master'

Resolve "Add accessions by name to my list"

Closes #268

See merge request genesys-pgr/genesys-ui!278
parents 74fcdc1a 39bbe9df
......@@ -106,6 +106,7 @@
"no": "No",
"noChanges": "No changes yet",
"noValue": "No value",
"nothingMatchesYourRequest": "Nothing matches your request",
"other": "Other",
"prettyNumber": "{{value, number}}",
"registered": "Registered",
......
......@@ -1783,6 +1783,17 @@
"zip": "ZIP",
"MCPD": "MCPD"
}
},
"c": {
"addAccessionsDialog": {
"buttonTitle": "Add multiple accessions",
"title": "Type multiple accessions",
"subTitle": "Type or paste list of accession numbers (ACCENUMB) as recorded in Genesys, separated by comma, semicolon or new line."
},
"addAccessionsForm": {
"acceNumbers": "Accession numbers",
"instCode": "Holding institute WIEWS code"
}
}
},
"common": {
......
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE, MY_LIST_CLEAR} from 'list/constants';
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_ADD_MULTIPLE, MY_LIST_ACCESSION_REMOVE, MY_LIST_CLEAR} from 'list/constants';
import { createApiCaller } from 'actions/ApiCall';
import { showSnackbar } from 'actions/snackbar';
import AccessionService from 'service/genesys/AccessionService';
const apiDecodeAcceNumbers = createApiCaller(AccessionService.uuidsFromAcceNumbers, MY_LIST_ACCESSION_ADD_MULTIPLE);
const initAccessions = (accessions: string[]) => ({
type: MY_LIST_ACCESSION_INIT,
payload: accessions,
......@@ -49,3 +54,13 @@ export const clearMyList = () => (dispatch) => {
window.localStorage.setItem('myList', JSON.stringify({count: 0, accessions: []}));
dispatch({type: MY_LIST_CLEAR});
};
export const decodeAcceNumbers = (acceNumbers: string[], instCode?: string) => (dispatch, getState) => {
return dispatch(apiDecodeAcceNumbers(acceNumbers, instCode))
.then((list) => {
if (list.length === 0) {
return dispatch(showSnackbar('common:label.nothingMatchesYourRequest'));
}
window.localStorage.setItem('myList', JSON.stringify(getState().list.public.myList));
});
};
export const MY_LIST_ACCESSION_INIT = 'MY_LIST_ACCESSION_INIT';
export const MY_LIST_ACCESSION_ADD = 'MY_LIST_ACCESSION_ADD';
export const MY_LIST_ACCESSION_ADD_MULTIPLE = 'MY_LIST_ACCESSION_ADD_MULTIPLE';
export const MY_LIST_ACCESSION_REMOVE = 'MY_LIST_ACCESSION_REMOVE';
export const MY_LIST_CLEAR = 'MY_LIST_CLEAR';
export const ADD_MULTIPLE_ACCESSIONS_FORM = 'Form/list/ADD_MULTIPLE_ACCESSIONS_FORM';
import {MY_LIST_ACCESSION_ADD, MY_LIST_ACCESSION_INIT, MY_LIST_ACCESSION_REMOVE, MY_LIST_CLEAR} from 'list/constants';
import {
MY_LIST_ACCESSION_ADD,
MY_LIST_ACCESSION_ADD_MULTIPLE,
MY_LIST_ACCESSION_INIT,
MY_LIST_ACCESSION_REMOVE,
MY_LIST_CLEAR,
} from 'list/constants';
import update from 'immutability-helper';
const INITIAL_STATE: {
......@@ -29,6 +35,17 @@ export default function listPublic(state = INITIAL_STATE, action: { type?: strin
},
});
}
case MY_LIST_ACCESSION_ADD_MULTIPLE: {
const {apiCall} = action.payload;
return apiCall.data ? update(state, {
myList: {
count: {$apply: (oldCount) => oldCount + apiCall.data.filter((uuid) => !state.myList.accessions.includes(uuid)).length},
accessions: {$push: apiCall.data.filter((uuid) => !state.myList.accessions.includes(uuid))},
},
})
: state;
}
case MY_LIST_ACCESSION_REMOVE: {
const index = state.myList.accessions.indexOf(action.payload);
return update(state, {
......
......@@ -10,6 +10,17 @@
"zip": "ZIP",
"MCPD": "MCPD"
}
},
"c": {
"addAccessionsDialog": {
"buttonTitle": "Add multiple accessions",
"title": "Type multiple accessions",
"subTitle": "Type or paste list of accession numbers (ACCENUMB) as recorded in Genesys, separated by comma, semicolon or new line."
},
"addAccessionsForm": {
"acceNumbers": "Accession numbers",
"instCode": "Holding institute WIEWS code"
}
}
},
"common": {
......
......@@ -12,6 +12,7 @@ import ActionButton from 'ui/common/buttons/ActionButton';
import navigateTo from 'actions/navigation';
import {clearMyList} from 'list/actions/public';
import PageTitle from 'ui/common/PageTitle';
import AddAccessionsDialog from './c/AddAccessionsDialog';
interface IMyListPageProps extends React.ClassAttributes<any> {
myList: any[];
......@@ -79,6 +80,7 @@ class MyListPage extends React.Component<IMyListPageProps> {
title={ `${accessions.length} ${ t('accessions.common.modelName', {count: accessions.length})}` }
buttons={
<div>
<AddAccessionsDialog/>
<ActionButton title={ t('list.public.p.browse.sendRequest') } action={ this.sendRequestAction }/>
<ActionButton title={ t('list.public.p.browse.clearMyList') } action={ this.clearListAction }/>
<ActionButton title={ `${t('common:action.download')} ${t('list.public.p.browse.zip')}` } action={ () => null }/>
......@@ -91,7 +93,13 @@ class MyListPage extends React.Component<IMyListPageProps> {
</PageContents>
</div>
) : (
<ContentHeaderWithButton title={ t('list.public.p.browse.noAccessions') } isSecondary/>
<ContentHeaderWithButton
title={ t('list.public.p.browse.noAccessions') }
buttons={
<AddAccessionsDialog/>
}
isSecondary
/>
)
}
</PageLayout>
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
// actions
import { decodeAcceNumbers } from 'list/actions/public';
// ui
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import AddAccessionsForm from './AddAccessionsForm';
interface IFtpPasswordPage extends React.ClassAttributes<any> {
variant?: 'text' | 'flat' | 'outlined' | 'contained' | 'raised' | 'fab' | 'extendedFab';
decodeAcceNumbers: (acceNumbers: string, instCode?: string) => void;
t?: any;
}
class ChangePasswordPage extends React.Component<IFtpPasswordPage> {
public state = {
open: false,
};
public render() {
const { variant = 'contained', t } = this.props;
return (
<span>
<Button disableFocusRipple variant={ variant } onClick={ this.show }>{ t('list.public.c.addAccessionsDialog.buttonTitle') }</Button>
<Dialog open={ this.state.open } onClose={ this.hide } maxWidth="md" fullWidth>
<DialogTitle>{ t('list.public.c.addAccessionsDialog.title') }</DialogTitle>
<div className="ml-20">{ t('list.public.c.addAccessionsDialog.subTitle') }</div>
<DialogContent>
<AddAccessionsForm onSubmit={ this.handleConfirm }/>
</DialogContent>
</Dialog>
</span>
);
}
private show = () => {
this.setState({ open: true });
}
private hide = () => {
this.setState({ open: false });
}
private handleConfirm = (value) => {
const {decodeAcceNumbers} = this.props;
console.log('Submitting: ', value);
if (value.acceNumbers) {
decodeAcceNumbers(value.acceNumbers, value.instCode);
}
this.setState({ open: false });
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
decodeAcceNumbers,
}, dispatch);
export default translate()(connect(null, mapDispatchToProps)(ChangePasswordPage));
import * as React from 'react';
import {translate} from 'react-i18next';
import { Field, reduxForm } from 'redux-form';
// constants
import { ADD_MULTIPLE_ACCESSIONS_FORM } from 'list/constants';
// ui
import Button from '@material-ui/core/Button';
import MultipleSelectionField from 'ui/common/forms/MultipleSelectionField';
import { TextField } from 'ui/common/text-field';
class PasswordForm extends React.Component<any, void> {
public constructor(props: any) {
super(props);
}
public render() {
const {t, error, handleSubmit, submitting} = this.props;
return (
<form onSubmit={ handleSubmit }>
<Field
name="acceNumbers"
label={ t('list.public.c.addAccessionsForm.acceNumbers') }
component={ MultipleSelectionField }
/>
<Field
name="instCode"
label={ t('list.public.c.addAccessionsForm.instCode') }
component={ TextField }
/>
{ error && <div style={ {color: 'red'} }>{ error }</div> }
<Button variant="contained" type="submit" style={ {margin: '1rem 0 1rem 0'} } disabled={ submitting }>
{ t('common:action.confirm') }
</Button>
</form>
);
}
}
export default translate()(reduxForm({
form: ADD_MULTIPLE_ACCESSIONS_FORM,
enableReinitialize: true,
})(PasswordForm));
......@@ -13,6 +13,8 @@ import AccessionAuditLog from 'model/accession/AccessionAuditLog';
import AccessionSuggestionPage from 'model/accession/AccessionSuggestionPage';
const URL_GET_BY_DOI = `/api/v1/acn/{doi}`; // UrlTemplate doesn't like the / in DOI
const URL_UUIDS_FROM_ACCE_NUMBERS = `/api/v1/acn/acce-number`;
const URL_UUID_FROM_ACCE_NUMBER = UrlTemplate.parse(`/api/v1/acn/acce-number/{acceNumber}`);
const URL_GET_BY_UUID = UrlTemplate.parse(`/api/v1/acn/{uuid}`);
const URL_GEO_JSON = `/api/v1/acn/geoJson`;
const URL_UUID_FROM_IDS = `/api/v1/acn/id`;
......@@ -95,6 +97,53 @@ class AccessionService {
}).then(({ data }) => data as any);
}
/**
* uuidsFromAcceNumbers at /api/v1/acn/acce-number
*
* @param acceNumbers acceNumbers
* @param instCode instCode
* @param xhrConfig additional xhr config
*/
public static uuidsFromAcceNumbers(acceNumbers: string[], instCode?: string, xhrConfig?: any): Promise<string[]> {
const qs = QueryString.stringify({
instCode: instCode || undefined,
}, {});
const apiUrl = URL_UUIDS_FROM_ACCE_NUMBERS + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { data: acceNumbers };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as string[]);
}
/**
* uuidFromAcceNumber at /api/v1/acn/acce-number/{acceNumber}
*
* @param acceNumber acceNumber
* @param instCode instCode
* @param xhrConfig additional xhr config
*/
public static uuidFromAcceNumber(acceNumber: string, instCode?: string, xhrConfig?: any): Promise<string> {
const qs = QueryString.stringify({
instCode: instCode || undefined,
}, {});
const apiUrl = URL_UUID_FROM_ACCE_NUMBER.expand({ acceNumber }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as string);
}
/**
* uuidFromIds at /api/v1/acn/id
*
......
import * as React from 'react';
import {translate} from 'react-i18next';
// UI
import { FormControl } from '@material-ui/core';
import Input from '@material-ui/core/Input';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import { PlusOne } from '@material-ui/icons';
import InputLabel from '@material-ui/core/InputLabel';
import Grid from '@material-ui/core/Grid';
class ValuesList extends React.Component<any> {
public state = {
renderList: [],
};
public render() {
const {input, removeByIndex} = this.props;
return (
<Grid container spacing={ 8 }>
{ input && input.value && input.value.map((renderItem, index) => (
<Grid item xs={ 12 } sm={ 6 }>
<div style={ { margin: '.2rem 0', padding: '.2rem 1rem', backgroundColor: '#e8e5e1', color: '#202222' } } key={ renderItem.value }>
{ renderItem }
<div className="font-bold float-right cursor-pointer" onClick={ () => removeByIndex(index) }>X</div>
</div>
</Grid>
)) }
</Grid>
);
}
}
class MultipleSelectionField extends React.Component<any> {
public state = {
values: [],
text: '',
};
private maybeAdd = (...newValues: string[]) => {
const values = [ ...this.state.values ];
newValues.forEach((text) => {
if (text && text.length > 0) {
if (values.indexOf(text) < 0) {
values.push(text);
}
}
});
if (!_.isEqual(values, this.state.values)) {
this.setState({
text: '',
values,
});
}
return values;
}
private maybeRemove = (...newValues: string[]) => {
const values = [ ...this.state.values ];
newValues.forEach((text) => {
if (text && text.length > 0) {
const index: number = values.indexOf(text);
if (index >= 0) {
values.splice(index, 1);
}
}
});
if (!_.isEqual(values, this.state.values)) {
this.setState({
text: '',
values,
});
}
return values;
}
private handleKeyPres = (event) => {
const { input } = this.props;
const { text } = this.state;
if (event.key === 'Enter') {
if (text && text.length > 0) {
event.preventDefault();
const values = this.maybeAdd(...text.split(/[,\n;]/));
input.onChange(values);
}
}
}
private removeByIndex = (index) => {
const { input } = this.props;
const newValues = this.maybeRemove(this.state.values[index]);
input.onChange(newValues);
}
private handleChange = (event) => {
this.setState({ ...this.state, text: event.target.value });
}
private handleAddCurrent = (event) => {
const { input } = this.props;
const { text } = this.state;
event.preventDefault();
const values = this.maybeAdd(text);
input.onChange(values);
}
private dataPasted = (e) => {
e.preventDefault();
const {input} = this.props;
const data = e.clipboardData.getData('text/plain');
const dataArr = data.split(/[,\n;]/).map((item) => item.trim());
const values = this.maybeAdd(...dataArr);
input.onChange(values);
}
public render() {
const { text } = this.state;
const {label, placeholder, input, t} = this.props;
return (
<FormControl fullWidth className="full-width">
<InputLabel>{ t(`${label}`) }</InputLabel>
<Input
className="full-width"
value={ text }
onChange={ this.handleChange }
placeholder={ placeholder ? t(`${placeholder}`) : '' }
onKeyPress={ this.handleKeyPres }
onPaste={ this.dataPasted }
onBlur={ this.handleAddCurrent }
endAdornment={
<InputAdornment position="end">
<IconButton type="button" onClick={ this.handleAddCurrent }>
<PlusOne style={ { fontSize: '1.5rem' } } />
</IconButton>
</InputAdornment>
}
/>
<ValuesList input={ input } removeByIndex={ this.removeByIndex } />
</FormControl>
);
}
}
export default translate()(MultipleSelectionField);
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment