Commit e18b6892 authored by Viacheslav Pavlov's avatar Viacheslav Pavlov
Browse files

Extract AccessionTable, add table for subset/dataset list page

parent 103dcd72
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { AccessionService } from '@genesys-pgr/client/service';
import AccessionFilter from '@genesys-pgr/client/model/accession/AccessionFilter';
import Accession from '@genesys-pgr/client/model/accession/Accession';
import FilteredPage, { IPageRequest } from '@genesys-pgr/client/model/FilteredPage';
import Pagination from 'ui/common/Pagination';
import { AccessionFilters } from './AccessionFilters';
import { withTranslation, WithTranslation } from 'react-i18next';
import { canAddToCart, LocalStorageCart } from 'utilities';
import { WithConfig } from 'config/config';
import Loading from 'ui/common/Loading';
// hook
import useAccessionPageLoad from 'accession/hook/useAccessionPageLoad';
// ui
import PageTitle from 'ui/common/PageTitle';
import { CountryName } from 'ui/common/CountryName';
// util
import {parseLocationSearch} from "utilities/filterUtil";
interface AccessionListPageState {
filter: AccessionFilter;
accessions: FilteredPage<Accession, AccessionFilter>;
selected: string[];
cartItems: string[];
isAllSelected: boolean;
}
import AccessionTable from 'accession/c/AccessionTable';
import { AccessionFilters } from 'accession/AccessionFilters';
import { LocalStorageCart } from 'utilities';
import { parseLocationSearch } from 'utilities/filterUtil';
interface AccessionListPageProps {
location: any;
location: Location;
}
class AccessionListPage extends React.Component<AccessionListPageProps & WithTranslation & WithConfig, AccessionListPageState> {
public constructor(props) {
super(props);
this.state = {
filter: this.props.appConfig.filter,
accessions: null,
selected: [],
isAllSelected: false,
cartItems: LocalStorageCart.getCartItemsLS(),
}
}
public componentDidMount() {
const { current, filterCode } = parseLocationSearch(this.props.location);
this.loadData(filterCode || this.state.filter, current ? { page: current } : {});
if (typeof window !== 'undefined') {
window.addEventListener('storage', this.handleLocalStorageUpdate);
this.receiveData();
}
}
public componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('storage', this.handleLocalStorageUpdate);
}
}
private handleLocalStorageUpdate = (e: StorageEvent) => {
if (e.key === LocalStorageCart.LS_KEY) {
this.receiveData();
}
};
private receiveData = () => {
this.setState({ cartItems: LocalStorageCart.getCartItemsLS() });
};
public componentDidUpdate(prevProps) {
const { current: currentPage, filterCode } = parseLocationSearch(this.props.location);
const { current: prevPage, filterCode: prevFilterCode } = parseLocationSearch(prevProps.location);
if (prevPage !== undefined && currentPage === 0 && prevFilterCode !== undefined && filterCode === undefined) {
// console.log('did update, reset filter);
this.loadData(this.props.appConfig.filter, {});
}
}
private loadData = (filter: string | AccessionFilter, pageR: IPageRequest): Promise<any> => {
return AccessionService
.list(filter, pageR)
.then((data) => {
// console.log('accessions: ', data);
this.setState({ accessions: data, filter: data.filter });
return data;
})
.catch((e) => {
console.log('Api call failed: ', e);
})
.finally(() => {
this.setState({ isAllSelected: false, selected: [] });
});
};
private applyFilter = (filter: AccessionFilter) => {
this.loadData({ ...this.state.filter, ...filter }, {});
};
private toggleRowSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const { selected } = this.state;
const { uuid } = (e.currentTarget as HTMLElement).dataset;
const updated = selected.filter((selectedUuid) => selectedUuid !== uuid);
if (updated.length === selected.length) {
updated.push(uuid);
}
this.setState({ selected: updated });
};
private addSelectedToCart = () => {
LocalStorageCart.addToCart(this.state.selected);
this.setState({ selected: [], isAllSelected: false, cartItems: LocalStorageCart.getCartItemsLS() });
};
private addToCart = (e: React.MouseEvent<HTMLButtonElement>) => {
LocalStorageCart.addToCart(e.currentTarget.getAttribute('data-uuid'));
this.setState( { cartItems: LocalStorageCart.getCartItemsLS() } )
};
private removeFromCart = (e: React.MouseEvent<HTMLButtonElement>) => {
LocalStorageCart.removeFromCart(e.currentTarget.getAttribute('data-uuid'));
this.setState( { cartItems: LocalStorageCart.getCartItemsLS() } )
}
private renderCartButton = (accession, i) => {
const { t } = this.props;
const { cartItems } = this.state;
if (!canAddToCart(accession)) {
return <span>{t('list.notAvailable')}</span>
}
if ( cartItems.includes(accession.uuid)) {
return <button
type="button"
name={ `button-remove-${accession.uuid}-${i}` }
data-uuid={ accession.uuid }
onClick={ this.removeFromCart }
className="btn btn-primary"
>
{ t('cart.removeFromCart') }
</button>
}
return <button
type="button"
name={ `button-add-${accession.uuid}-${i}` }
data-uuid={ accession.uuid }
onClick={ this.addToCart }
className="btn btn-primary"
>
{ t('cart.addToCart') }
</button>
}
private onToggleAll = () => {
this.setState((prevState) => ({
isAllSelected: !prevState.isAllSelected,
selected: !prevState.isAllSelected ? prevState.accessions.content.map((acc) => canAddToCart(acc) ? acc.uuid : null).filter((uuid) => uuid && uuid) : [],
}));
};
public render() {
const { accessions, selected, isAllSelected } = this.state;
const { t, appConfig: { shoppingCart } } = this.props;
const selectedUUIDs = new Set();
selected.forEach((uuid) => selectedUUIDs.add(uuid));
if (accessions === null) {
return (
<>
<PageTitle title={ t('pagetitle.accessionList') }/>
<Loading/>
</>
);
} else {
return (
<>
<PageTitle title={ t('pagetitle.accessionList') }/>
<h1 className="d-flex justify-content-between align-items-center">
{ t('estimatedNumberOfItems', { count: accessions.totalElements, what: t('accession.model', { count: accessions.totalElements }) }) }
{ selected.length !== 0 &&
<button type="button" className="btn btn-primary" onClick={ this.addSelectedToCart }>
{ t('cart.addToCart') }
</button>
}
</h1>
<AccessionFilters filter={ this.state.filter } applyFilter={ this.applyFilter } key={ `filters-${accessions.filterCode}` }/>
<Pagination loadData={ this.loadData } paged={ accessions }>
<table className="table table-striped">
<thead className="thead-dark">
<tr>
{ shoppingCart.enabled && (
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ this.onToggleAll }
className="align-middle"
/>
</th>
) }
<th>{ t(['accession.crop', '_.crop']) }</th>
<th>{ t('accession.acceNumb') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.taxonomy.scientificName') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
<th>{ t('accession.sampStat') }</th>
{ shoppingCart.enabled && ( <th>{ t('list.availability') }</th> ) }
</tr>
</thead>
<tbody>
{ accessions.content.map((a, i) => (
<tr key={ a.id } className={ a.historic ? 'table-historical' : selectedUUIDs.has(a.uuid) ? 'table-primary' : '' }>
{ shoppingCart.enabled && (
<td>
{ canAddToCart(a) &&
<input
type="checkbox"
name={ `checkbox-${a.uuid}-${i}` }
data-uuid={ a.uuid }
checked={ selectedUUIDs.has(a.uuid) }
onChange={ this.toggleRowSelect }
className="align-middle"
/>
}
</td>
) }
<td>{ a.crop && a.crop.name || a.cropName }</td>
<td><Link to={ `/a/${a.uuid}` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName && <b>{ a.accessionName }</b> }</td>
<td><span dangerouslySetInnerHTML={ { __html: a.taxonomy.taxonNameHtml } } /></td>
<td>{ a.countryOfOrigin && (<CountryName code3={ a.countryOfOrigin.code3 } /> || a.countryOfOrigin.name) }</td>
<td>{ a.sampStat && t(`accession.sampleStatus.${a.sampStat}`) }</td>
{ shoppingCart.enabled &&
<td>
{ this.renderCartButton(a, i) }
</td>
}
</tr>
)) }
</tbody>
</table>
</Pagination>
</>
);
}
}
const AccessionListPage = ({location}: AccessionListPageProps) => {
// util
const { t } = useTranslation()
// memo
const initialFilterCode = React.useMemo(() => parseLocationSearch(location).filterCode, [location]);
// custom
const { accessions, filter, applyFilter, loadData } = useAccessionPageLoad({initialFilterCode});
// state
const [selectedAccessions, setSelectedAccessions] = React.useState<string[]>([]);
// callback
const addSelectedToCart = React.useCallback(() => {
LocalStorageCart.addToCart(selectedAccessions);
}, [selectedAccessions])
return (
<>
<PageTitle title={ t('pagetitle.accessionList') }/>
<h1 className="d-flex justify-content-between align-items-center">
{ t('estimatedNumberOfItems', {
count: accessions?.totalElements,
what: t('accession.model', { count: accessions?.totalElements })
}) }
{ selectedAccessions?.length !== 0 &&
<button type="button" className="btn btn-primary" onClick={ addSelectedToCart }>
{ t('cart.addToCart') }
</button>
}
</h1>
<AccessionFilters
filter={ filter }
applyFilter={ applyFilter }
key={ `filters-${ accessions?.filterCode }` }
/>
<AccessionTable accessions={ accessions } loadData={ loadData } onSelectedModified={setSelectedAccessions}/>
</>
);
}
const mapStateToProps = (state) => ({
appConfig: state.appConfig.config,
});
export default connect(mapStateToProps)(withTranslation()(AccessionListPage));
export default AccessionListPage;
import React from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// hook
import useAccessionCart from 'accession/hook/useAccessionCart';
// model
import Accession from '@genesys-pgr/client/model/accession/Accession';
import FilteredPage, { IPageRequest } from '@genesys-pgr/client/model/FilteredPage';
import AccessionFilter from '@genesys-pgr/client/model/accession/AccessionFilter';
// util
import { canAddToCart } from 'utilities';
// ui
import Pagination from 'ui/common/Pagination';
import { CountryName } from 'ui/common/CountryName';
import Loading from 'ui/common/Loading';
interface IAccessionTable {
accessions: FilteredPage<Accession, AccessionFilter>;
loadData: (filter: string | AccessionFilter, pageR: IPageRequest) => Promise<FilteredPage<Accession, AccessionFilter>>
onSelectedModified?: (selected: string[]) => void;
noRedirect?: boolean
}
const AccessionTable = ({ accessions, loadData, onSelectedModified, noRedirect }: IAccessionTable) => {
// util
const { t } = useTranslation();
// custom
const { shoppingCartEnabled, addToCart, removeFromCart, cartItems } = useAccessionCart();
// state
const [{ isAllSelected, selectedUUIDs }, setSelected]
= React.useState<{ isAllSelected: boolean, selectedUUIDs: string[] }>({ isAllSelected: false, selectedUUIDs: [] });
// callback
const loadDataInternal = React.useCallback((filter: string | AccessionFilter, pageR: IPageRequest) => {
return loadData(filter, pageR)
.then(() => setSelected({selectedUUIDs: [], isAllSelected: false}));
}, [loadData]);
const toggleRowSelect = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { uuid } = (e.currentTarget as HTMLElement).dataset;
const updated = selectedUUIDs.filter((selectedUuid) => selectedUuid !== uuid);
if (updated.length === selectedUUIDs.length) {
updated.push(uuid);
}
setSelected({ selectedUUIDs: updated, isAllSelected: updated.length === accessions?.content?.length });
}, [accessions?.content?.length, selectedUUIDs]);
const onToggleAll = React.useCallback(() => {
setSelected({
selectedUUIDs: isAllSelected
? []
: accessions?.content.filter((a) => canAddToCart(a) && a.uuid).map((a) => a.uuid),
isAllSelected: !isAllSelected,
});
}, [accessions, isAllSelected]);
React.useEffect(() => {
if (onSelectedModified) {
onSelectedModified(selectedUUIDs);
}
}, [onSelectedModified, selectedUUIDs, isAllSelected]);
if (!accessions) {
return <Loading/>;
}
return (
<>
<Pagination loadData={ loadDataInternal } paged={ accessions } noRedirect={noRedirect}>
<table className="table table-striped">
<thead className="thead-dark">
<tr>
{ shoppingCartEnabled && (
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ onToggleAll }
className="align-middle"
/>
</th>
) }
<th>{ t(['accession.crop', '_.crop']) }</th>
<th>{ t('accession.acceNumb') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.taxonomy.scientificName') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
<th>{ t('accession.sampStat') }</th>
{ shoppingCartEnabled && (<th>{ t('list.availability') }</th>) }
</tr>
</thead>
<tbody>
{ accessions.content
.map((a) => ({
...a,
canAdd: canAddToCart(a),
included: selectedUUIDs.includes(a.uuid),
inCart: cartItems.includes(a.uuid)
}))
.map((a, i) => (
<tr key={ a.id }
className={ a.historic ? 'table-historical' : a.included ? 'table-primary' : '' }>
{ shoppingCartEnabled && (
<td>
{ a.canAdd &&
<input
type="checkbox"
name={ `checkbox-${ a.uuid }-${ i }` }
data-uuid={ a.uuid }
checked={ a.included }
onChange={ toggleRowSelect }
className="align-middle"
/>
}
</td>
) }
<td>{ a.crop && a.crop.name || a.cropName }</td>
<td><Link to={ `/a/${ a.uuid }` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName && <b>{ a.accessionName }</b> }</td>
<td><span dangerouslySetInnerHTML={ { __html: a.taxonomy.taxonNameHtml } }/></td>
<td>{ a.countryOfOrigin && (
<CountryName code3={ a.countryOfOrigin.code3 }/> || a.countryOfOrigin.name) }</td>
<td>{ a.sampStat && t(`accession.sampleStatus.${ a.sampStat }`) }</td>
{ shoppingCartEnabled &&
<td>
<button
type="button"
name={ `button-add-${ a.uuid }-${ i }` }
data-uuid={ a.uuid }
onClick={ a.inCart ? removeFromCart : addToCart }
className="btn btn-primary"
>
{ a.inCart ? t('cart.removeFromCart') : t('cart.addToCart') }
</button>
</td>
}
</tr>
)) }
</tbody>
</table>
</Pagination>
</>
);
};
export default AccessionTable;
import * as React from 'react';
import { LocalStorageCart } from 'utilities';
import { useSelector } from 'react-redux';
import { Config } from 'config/config';
interface UseAccessionCart {
addToCart: (e: React.MouseEvent<HTMLButtonElement>) => void;
removeFromCart: (e: React.MouseEvent<HTMLButtonElement>) => void;
cartItems: string[];
shoppingCartEnabled: boolean;
}
const useAccessionCart = (): UseAccessionCart => {
// state
const [cartItems, setCartItems] = React.useState<string[]>([]);
// redux
const appConfig: Config = useSelector((state: any) => state.appConfig.config);
// memo
const shoppingCartEnabled = React.useMemo(() => appConfig?.shoppingCart?.enabled, [appConfig]);
// callback
const addToCart = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
LocalStorageCart.addToCart(e.currentTarget.getAttribute('data-uuid'));
setCartItems(LocalStorageCart.getCartItemsLS);
}, []);
const removeFromCart = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
LocalStorageCart.removeFromCart(e.currentTarget.getAttribute('data-uuid'));
setCartItems(LocalStorageCart.getCartItemsLS);
}, [])
const handleLocalStorageUpdate = React.useCallback((e: StorageEvent) => {
if (e.key === LocalStorageCart.LS_KEY) {
setCartItems(LocalStorageCart.getCartItemsLS());
}
}, []);
// effect
React.useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('storage', handleLocalStorageUpdate);
setCartItems(LocalStorageCart.getCartItemsLS());
}
return () => {
if (typeof window !== 'undefined') {
window.removeEventListener('storage', handleLocalStorageUpdate);
}
}
}, [handleLocalStorageUpdate])
return {
addToCart,
removeFromCart,
cartItems,
shoppingCartEnabled,
};
};
export default useAccessionCart;
import * as React from 'react';
import AccessionFilter from '@genesys-pgr/client/model/accession/AccessionFilter';
import { IPageRequest } from '@genesys-pgr/client/model/Page';
import { AccessionService } from '@genesys-pgr/client/service';
import FilteredPage from '@genesys-pgr/client/model/FilteredPage';
import Accession from '@genesys-pgr/client/model/accession/Accession';
interface UseAccessionPageLoadProps {
initialFilter?: AccessionFilter;
initialFilterCode?: string;
}
interface UseAccessionPageLoad {
accessions: FilteredPage<Accession, AccessionFilter>;
filter: AccessionFilter;
loadData: (filter: string | AccessionFilter, pageR: IPageRequest) => Promise<FilteredPage<Accession, AccessionFilter>>;
applyFilter: (filter: string | AccessionFilter) => void;
}
const useAccessionPageLoad = ({ initialFilter, initialFilterCode }: UseAccessionPageLoadProps): UseAccessionPageLoad => {
// state
const [accessions, setAccessions] = React.useState<FilteredPage<Accession, AccessionFilter>>();
const [filter, setFilter] = React.useState<AccessionFilter>(initialFilter);
// callback
const loadData = React.useCallback((filter: string | AccessionFilter, pageR: IPageRequest) => {
return AccessionService
.list(filter, pageR)
.then((data) => {
console.log('accessions: ', data);
setFilter(data.filter);
setAccessions(data);
return data;
})
.catch((e) => {
console.log('Api call failed: ', e);
return null;
});
}, []);
const applyFilter = React.useCallback((newFilter) => {
loadData({ ...filter, ...newFilter }, {});
}, [filter, loadData]);
// effect
React.useEffect(() => {
loadData(filter || initialFilterCode, {});
}, [initialFilterCode]);
return {
accessions,
filter: filter || {},
loadData,
applyFilter,
}
};
export default useAccessionPageLoad;