Commit ef4d3114 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '14-configuring-features' into 'master'

Resolve "Configuring features"

Closes #10 and #14

See merge request !12
parents b4ffa2fd 083460e9
......@@ -34,7 +34,7 @@
"error",
{
"types": {
// "object": false
"object": "false"
}
}
],
......@@ -120,7 +120,7 @@
"import/no-internal-modules": "off",
"import/order": "off",
"indent": [2, 2, {"SwitchCase": 1}],
// "no-restricted-imports": ["error", {"patterns": ["../*"]}],
"no-restricted-imports": ["error", {"patterns": ["../*"]}],
"max-classes-per-file": [
"error",
10
......
export class Config {
public apiUrl: string;
public clientId: string;
public clientKey: string;
public filter: Record<string, any>;
public constructor(config: Config) {
this.apiUrl = config.apiUrl;
this.clientId = config.clientId;
this.clientKey = config.clientKey;
this.filter = config.filter;
}
}
export const DefaultConfig = new Config( {
apiUrl: 'https://api.sandbox.genesys-pgr.org',
clientId: 'clientid@genesys',
clientKey: 'changeme',
filter: {},
})
import { showGenesysUI } from 'genesys';
import { Config } from 'config/config';
import {
showGenesysUI,
// showOverview, // for testing
} from 'genesys';
const queryLang = document.location.search && document.location.search.substr(1) || undefined;
showGenesysUI(document.getElementById('genesys'), {
const genesysConfig: Config = {
apiUrl: 'http://localhost:8080',
clientId: 'defaultclient@localhost',
clientKey: 'changeme',
apiUrl: 'http://localhost:8080',
filter: { institute: { code: [ 'COL003', 'BEL084', 'ETH013' ] } },
}, queryLang);
language: queryLang,
shoppingCart: {
enabled: true,
},
};
showGenesysUI(document.getElementById('genesys'), genesysConfig);
// showOverview(document.getElementById('genesys'), genesysConfig);
import React from 'react';
import { connect } from 'react-redux';
import { AccessionService } from '@genesys/client/service';
import { Property } from 'ui/common/Property';
import AccessionDetails from '@genesys/client/model/accession/AccessionDetails';
import { WithTranslation, withTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
import { WithConfig } from 'config/config';
interface IAccessionDetailsPageState {
accession: AccessionDetails;
......@@ -13,10 +15,9 @@ interface IAccessionDetailsPageState {
interface IAccessionDetailsPage {
match: any;
apiUrl: string;
}
class AccessionDetailsPage extends React.Component<IAccessionDetailsPage & WithTranslation, IAccessionDetailsPageState> {
class AccessionDetailsPage extends React.Component<IAccessionDetailsPage & WithTranslation & WithConfig, IAccessionDetailsPageState> {
public constructor(props) {
super(props);
}
......@@ -91,7 +92,7 @@ class AccessionDetailsPage extends React.Component<IAccessionDetailsPage & WithT
public render() {
const { accession, cartItems } = this.state;
const { apiUrl, t } = this.props;
const { t, appConfig: { apiUrl, shoppingCart } } = this.props;
let propertyIndex = 0;
......@@ -200,25 +201,31 @@ class AccessionDetailsPage extends React.Component<IAccessionDetailsPage & WithT
)) }
</>
}
<div className="pt-4">
{cartItems.includes(this.props.match.params.uuid) ?
<button
type="button"
name={ `button-remove-${accession.details.uuid}` }
data-uuid={ accession.details.uuid }
className="btn btn-primary"
onClick={ this.removeFromCart }
>
{ t('cart.removeFromCart') }
</button>
:
this.renderAddToCart()
}
</div>
{ shoppingCart.enabled &&
<div className="pt-4">
{ cartItems.includes(this.props.match.params.uuid) ?
<button
type="button"
name={ `button-remove-${accession.details.uuid}` }
data-uuid={ accession.details.uuid }
className="btn btn-primary"
onClick={ this.removeFromCart }
>
{ t('cart.removeFromCart') }
</button>
:
this.renderAddToCart()
}
</div>
}
</>
);
}
};
}
export default withTranslation()(AccessionDetailsPage);
const mapStateToProps = (state) => ({
appConfig: state.appConfig.config,
});
export default connect(mapStateToProps)(withTranslation()(AccessionDetailsPage));
import React from 'react';
import { Link } from 'react-router-dom';
import { parse } from 'query-string';
import { connect } from 'react-redux';
import { AccessionService } from '@genesys/client/service';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
......@@ -10,6 +11,7 @@ import Pagination from 'ui/common/Pagination';
import { AccessionFilters } from './AccessionFilters';
import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
import { WithConfig } from 'config/config';
interface IAccessionListPageState {
filter: AccessionFilter;
......@@ -20,16 +22,15 @@ interface IAccessionListPageState {
}
interface IAccessionListPageProps {
filter: AccessionFilter;
location: any;
}
class AccessionListPage extends React.Component<IAccessionListPageProps & WithTranslation, IAccessionListPageState> {
class AccessionListPage extends React.Component<IAccessionListPageProps & WithTranslation & WithConfig, IAccessionListPageState> {
public constructor(props) {
super(props);
this.state = {
filter: this.props.filter,
filter: this.props.appConfig.filter,
accessions: null,
selected: [],
isAllSelected: false,
......@@ -68,7 +69,7 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
if (prevPage !== undefined && currentPage === 0 && prevFilterCode !== undefined && filterCode === undefined) {
// console.log('did update, reset filter);
this.loadData(this.props.filter, {});
this.loadData(this.props.appConfig.filter, {});
}
}
......@@ -163,7 +164,7 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
public render() {
const { accessions, selected, isAllSelected } = this.state;
const { t } = this.props;
const { t, appConfig: { shoppingCart } } = this.props;
const selectedUUIDs = new Set();
selected.forEach((uuid) => selectedUUIDs.add(uuid));
......@@ -187,48 +188,54 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
<table className="table table-striped">
<thead className="thead-dark">
<tr>
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ this.onToggleAll }
className="align-middle"
/>
</th>
{ shoppingCart.enabled && (
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ this.onToggleAll }
className="align-middle"
/>
</th>
) }
<th>{ t('accession.crop') }</th>
<th>{ t('accession.acceNumb') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.taxonomy') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
<th>{ t('accession.sampStat') }</th>
<th>{ t('list.availability') }</th>
{ shoppingCart.enabled && ( <th>{ t('list.availability') }</th> ) }
</tr>
</thead>
<tbody>
{ accessions.content.map((a, i) => (
<tr key={ a.id } className={ selectedUUIDs.has(a.uuid) ? 'table-primary' : '' }>
<td>
{this.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>
{ shoppingCart.enabled && (
<td>
{ this.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.cropName }</td>
<td><Link to={ `/a/${a.uuid}` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName }</td>
<td><span dangerouslySetInnerHTML={ { __html: a.taxonomy.taxonNameHtml } } /></td>
<td>{ a.countryOfOrigin && a.countryOfOrigin.name }</td>
<td>{ a.sampStat && t(`accession.sampleStatus.${a.sampStat}`) }</td>
<td>
{this.renderCartButton(a, i)}
</td>
{ shoppingCart.enabled &&
<td>
{ this.renderCartButton(a, i) }
</td>
}
</tr>
)) }
</tbody>
......@@ -240,4 +247,8 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
};
}
export default withTranslation()(AccessionListPage);
const mapStateToProps = (state) => ({
appConfig: state.appConfig.config,
});
export default connect(mapStateToProps)(withTranslation()(AccessionListPage));
......@@ -3,39 +3,25 @@ import { withTranslation, WithTranslation } from 'react-i18next';
// model
import AccessionOverview from '@genesys/client/model/accession/AccessionOverview';
import PropertiesCard from 'ui/common/PropertiesCard';
import { VocabularyService } from '@genesys/client/service';
import { connect } from 'react-redux';
interface IAccessionOverviewPageState {
countryCodes: object;
}
interface IAccessionOverviewPageProps extends React.ClassAttributes<any> {
overview: AccessionOverview;
countryCodes: Record<string, string>;
}
class AccessionOverviewSection extends React.Component<IAccessionOverviewPageProps & WithTranslation, IAccessionOverviewPageState> {
class AccessionOverviewSection extends React.Component<IAccessionOverviewPageProps & WithTranslation> {
public constructor(props) {
super(props);
this.state = {
countryCodes: null,
}
}
private overviewKeys = ['institute.code', 'institute.country.code3', 'cropName', 'crop.shortName', 'sampStat', 'taxonomy.genus', 'taxonomy.genusSpecies',
'taxonomy.grinTaxonomySpecies.name', 'taxonomy.currentTaxonomySpecies.name',
'countryOfOrigin.code3', 'donorCode', 'mlsStatus', 'available', 'duplSite', 'sgsv', 'storage', 'breederCode', 'aegis'];
public async componentDidMount() {
const codes = await VocabularyService.decode3166Alpha3Terms(this.props.i18n.language);
this.setState({ countryCodes: codes })
}
public render() {
const { overview, t } = this.props;
const { countryCodes } = this.state;
const { overview, t, countryCodes } = this.props;
if (!overview) {
return null;
......@@ -206,4 +192,8 @@ class AccessionOverviewSection extends React.Component<IAccessionOverviewPagePro
}
}
export default withTranslation()(AccessionOverviewSection);
const mapStateToProps = (state) => ({
countryCodes: state.decoding.countryCodes,
});
export default connect(mapStateToProps)(withTranslation()(AccessionOverviewSection));
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
// Models
import { AccessionService } from '@genesys/client/service';
......@@ -63,4 +64,8 @@ class BrowsePage extends React.Component<IOverviewPageProps, any> {
}
}
export default withTranslation()(BrowsePage);
const mapStateToProps = (state) => ({
filter: state.appConfig.config.filter,
});
export default connect(mapStateToProps)(withTranslation()(BrowsePage));
export interface WithConfig {
appConfig: Config;
}
export class Config {
public apiUrl?: string;
public clientId: string;
public clientKey: string;
public filter?: Record<string, any>;
// module config
public language?: string = 'en';
public accession?: BaseFeatureConfig;
public shoppingCart?: BaseFeatureConfig;
public constructor(config: Config) {
this.apiUrl = config.apiUrl || defaultConfig.apiUrl;
this.clientId = config.clientId;
this.clientKey = config.clientKey;
this.language = config.language || defaultConfig.language;
this.filter = config.filter || defaultConfig.filter;
// Merge feature config
this.accession = { ...defaultConfig.accession, ...config.accession };
this.shoppingCart = { ...defaultConfig.shoppingCart, ...config.shoppingCart };
// console.log('Source and merged configuration', config, this);
}
}
export class BaseFeatureConfig {
public enabled: boolean;
}
export const defaultConfig: Partial<Config> = {
apiUrl: 'https://api.sandbox.genesys-pgr.org',
filter: {},
language: 'en',
accession: { enabled: true },
shoppingCart: { enabled: false },
};
import { RECEIVE_APP_CONFIG } from 'core/constants/appConfig';
import { Config } from 'config/config';
export const setConfig = (config: Config) => ({
type: RECEIVE_APP_CONFIG,
payload: config,
});
import { VocabularyService } from '@genesys/client/service';
import { RECEIVE_COUNTRY_CODES_DECODED } from 'core/constants/decoding';
export const getCountryCodes = (lang: string) => (dispatch) => {
return VocabularyService
.decode3166Alpha3Terms(lang)
.then((codes) => {
dispatch({
type: RECEIVE_COUNTRY_CODES_DECODED,
payload: codes,
});
})
.catch((e) => {
console.log('Loading country codes decoding failed: ', e);
})
};
export const RECEIVE_APP_CONFIG = 'core/appConfig/RECEIVE';
export const RECEIVE_COUNTRY_CODES_DECODED = 'core/decoding/RECEIVE';
import update from 'immutability-helper';
import { RECEIVE_APP_CONFIG } from 'core/constants/appConfig';
import { Config } from 'config/config';
const INITIAL_STATE: {
config: Config,
} = {
config: null,
};
export default (state = INITIAL_STATE, action: { type?: string, payload?: any } = { type: '', payload: {} }) => {
switch (action.type) {
case RECEIVE_APP_CONFIG: {
const config = new Config(action.payload);
return update(state, {
config: { $set: config },
});
}
default:
return state;
}
}
import update from 'immutability-helper';
import { RECEIVE_COUNTRY_CODES_DECODED } from 'core/constants/decoding';
const INITIAL_STATE: {
countryCodes: Record<string, string>,
} = {
countryCodes: null,
};
export default (state = INITIAL_STATE, action: { type?: string, payload?: any } = { type: '', payload: {} }) => {
switch (action.type) {
case RECEIVE_COUNTRY_CODES_DECODED: {
return update(state, {
countryCodes: { $set: action.payload },
});
}
default:
return state;
}
}
import { combineReducers } from 'redux';
import appConfig from 'core/reducer/appConfig';
import decoding from './decoding';
const rootReducer = () => (combineReducers({
appConfig,
decoding,
}));
export default rootReducer;
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import initI18n from './i18n';
import { applyMiddleware, compose, createStore } from 'redux';
import { log } from '@genesys/client/utilities/debug';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
// import * as cookies from 'es-cookie';
import { reconfigureServiceAxios, LoginService } from '@genesys/client/service';
import App from 'ui/core/App';
import ApiAccessError from 'ui/core/ApiAccessError';
import { Config, DefaultConfig } from '../config/config';
import { Config } from 'config/config';
import rootReducer from 'core/reducer';
import { setConfig } from 'core/actions/appConfig';
import OverviewPage from 'accession/OverviewPage';
// declare const window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any, initialLanguage: any, initialI18nStore: any, localeMapping: any };
declare const window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any };
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const initialState = {};
const store = composeEnhancers(applyMiddleware(thunk))(createStore)(rootReducer(), initialState);
// const AUTH_COOKIE = 'GENESYS_AUTH';
console.log('Loading Genesys UI library');
......@@ -43,16 +51,19 @@ function checkAccessTokens(apiUrl: string, clientId: string, clientSecret: strin
return appLogin;
};
export function showGenesysUI(holdingNode: HTMLElement, config: Config = DefaultConfig, language: string = 'en') {
const { apiUrl, clientId, clientKey, filter } = config;
export function showGenesysUI(holdingNode: HTMLElement, config: Config) {
const { apiUrl, clientId, clientKey, language } = config;
reconfigureServiceAxios({ apiUrl });
initI18n(language);
store.dispatch(setConfig(config));
checkAccessTokens(apiUrl, clientId, clientKey).then( (result) => {
ReactDOM.render(
<App filter={ filter } apiUrl={ apiUrl } />,
<Provider store={ store }>
<App/>
</Provider>,
holdingNode,
);
}).catch((err) => {
......@@ -65,16 +76,19 @@ export function showGenesysUI(holdingNode: HTMLElement, config: Config = Default
}
export function showOverview(holdingNode: HTMLElement, config: Config = DefaultConfig, language: string = 'en') {
const { apiUrl, clientId, clientKey, filter } = config;
export function showOverview(holdingNode: HTMLElement, config: Config) {
const { apiUrl, clientId, clientKey, language } = config;
reconfigureServiceAxios({ apiUrl });
initI18n(language);
store.dispatch(setConfig(config));
checkAccessTokens(apiUrl, clientId, clientKey).then( (result) => {
ReactDOM.render(
<OverviewPage filter={ filter } />,
<Provider store={ store }>
<OverviewPage />
</Provider>,
holdingNode,
);
}).catch((err) => {
......
import React from 'react';
import { connect } from 'react-redux';