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

Merge branch '3-entry-page' into 'master'

Resolve web application with expressjs

Closes #3

See merge request grin-global/grin-global-ui!3
parents e20d79da 01171422
import { SortDirection } from '@gringlobal/client/model/page';
export default interface IPageRequest {
page?: number; // page number
size?: number; // page size
direction?: SortDirection; // direction
properties?: string[]; // sort by properties
}
import { SortDirection } from '@gringlobal/client/model/page';
export default interface ISort {
ascending?: boolean;
descending?: boolean;
direction: SortDirection;
property: string;
}
/*
* Defined in OpenAPI as '#/definitions/Page<T>'
*/
import { IPageRequest, ISort, SortDirection } from '@gringlobal/client/model/page';
class Page<T> {
public content?: T[];
public last?: boolean;
public number?: number;
public totalElements?: number;
public totalPages?: number;
public size?: number;
public sort?: ISort[];
public static nextPage(paged?: Page<any>): IPageRequest {
const nextPage: IPageRequest = {
page: paged && (paged.number || paged.number === 0) ? paged.number + 1 : 0,
size: paged && paged.size || 50,
direction: paged && paged.sort && paged.sort[0].direction || undefined,
properties: paged && paged.sort && paged.sort[0].property ? [ paged.sort[0].property ] : undefined,
};
// console.log('Current/Next', paged, nextPage);
return nextPage;
}
public static fromQueryString(qs: any): IPageRequest {
return {
page: 0,
size: undefined,
properties: qs ? [ qs.s ] : undefined,
direction: qs ? qs.d : undefined,
};
}
public static reSort(paged: Page<any>, property?: string, direction?: SortDirection): Page<any> {
const resorted: Page<any> = {
number: undefined, // because loadMore defaults to 0
size: undefined,
sort: [ {
property: property ? property : undefined,
direction: direction ? direction : undefined,
} ],
};
console.log(`Resort for prop=${property} dir={direction}`, resorted);
return resorted;
}
public static merge(oldPaged: Page<any>, newPaged: Page<any>): Page<any> {
if (!oldPaged) {
return newPaged;
}
return newPaged ? {
content: newPaged.content && newPaged.number === 0 ? newPaged.content : (newPaged.content ? [...oldPaged.content, ...newPaged.content] : oldPaged.content),
last: newPaged.last === false ? newPaged.last : newPaged.last || oldPaged.last,
number: newPaged.number === 0 ? newPaged.number : newPaged.number || oldPaged.number,
totalElements: newPaged.totalElements === 0 ? newPaged.totalElements : newPaged.totalElements || oldPaged.totalElements,
totalPages: newPaged.totalPages === 0 ? newPaged.totalPages : newPaged.totalPages || oldPaged.totalPages,
size: newPaged.size === 0 ? newPaged.size : newPaged.size || oldPaged.size,
sort: newPaged.sort || oldPaged.sort,
} : oldPaged;
}
}
export default Page;
// TODO Remove, we're using infinite loaders
export default class Pagination<T> {
public page: number;
public size: number;
// TODO Should we be using DataOrder (below)?
public sort: string[] | string;
public dir: string[] | string;
public filter: T;
public filterCode: string;
public constructor(obj?) {
if (obj !== null && obj !== undefined) {
const props = Object.keys(obj);
for (const prop of props) { this[prop] = obj[prop] }
}
}
public equals(other: Pagination<T>): boolean {
if (other === null) {
return false;
}
if (this.page !== other.page) {
return false;
}
if (this.size !== other.size) {
return false;
}
if (this.filterCode !== other.filterCode) {
return false;
}
if (JSON.stringify(this.sort) !== JSON.stringify(other.sort)) {
return false;
}
return JSON.stringify(this.dir) === JSON.stringify(other.dir);
}
}
enum SortDirection {
ASC = 'ASC',
DESC = 'DESC',
}
export default SortDirection;
import { FilteredPage } from '@gringlobal/client/model/page';
export default class SuggestionsPage<T> extends FilteredPage<T> {
public suggestions: any;
}
export { default as FilteredPage } from '@gringlobal/client/model/page/FilteredPage';
export { default as IPageRequest } from '@gringlobal/client/model/page/IPageRequest';
export { default as ISort } from '@gringlobal/client/model/page/ISort';
export { default as Page } from '@gringlobal/client/model/page/Page';
export { default as Pagination } from '@gringlobal/client/model/page/Pagination';
export { default as SortDirection } from '@gringlobal/client/model/page/SortDirection';
import update from 'immutability-helper';
import { UPDATE_HISTORY } from '@gringlobal/client/constants/history';
const INITIAL_STATE: {
hasHistory: boolean,
lastLocation: string,
} = {
hasHistory: false,
lastLocation: '/',
};
export default (state = INITIAL_STATE, action: { type?: string, payload?: any } = { type: '', payload: {} }) => {
switch (action.type) {
case UPDATE_HISTORY: {
return update(state, {
hasHistory: { $set: true },
lastLocation: { $set: action.payload },
});
}
default:
return state;
}
}
......@@ -3,6 +3,7 @@ import { connectRouter } from 'connected-react-router';
// common reduces
import applicationConfig from '@gringlobal/client/reducer/applicationConfig';
import historyReducer from '@gringlobal/client/reducer/history';
import login from '@gringlobal/client/reducer/login';
import pageTitle from '@gringlobal/client/reducer/pageTitle';
......@@ -13,6 +14,7 @@ const coreReducers = (history?) => ({
// common reducers
applicationConfig,
history: historyReducer,
login,
pageTitle,
......
import * as UrlTemplate from 'url-template';
import * as QueryString from 'query-string';
import { axiosBackend } from '@gringlobal/client/utilities/requestUtils';
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import FilteredPage from '@gringlobal/client/model/page/FilteredPage';
import IPageRequest from '@gringlobal/client/model/page/IPageRequest';
const URL_FILTER_1 = '/api/v1/cooperator/filter';
const URL_GET_COOPERATOR = UrlTemplate.parse('/api/v1/cooperator/{id}');
/**
* Cooperator service
*
* GRIN-Global CE API
*/
class CooperatorService {
/**
* filter_1 at /api/v1/cooperator/filter
*
* @param f undefined
* @param page undefined
* @param xhrConfig additional xhr config
*/
public static filter_1(f?: string, page?: IPageRequest, xhrConfig?: any): Promise<FilteredPage<Cooperator>> {
const qs = QueryString.stringify({
f: f || undefined,
p: page.page || undefined,
l: page.size || undefined,
d: page.direction ? page.direction : undefined,
s: page.properties || undefined,
}, {});
const apiUrl = URL_FILTER_1 + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as FilteredPage<Cooperator>);
}
/**
* getCooperator at /api/v1/cooperator/{id}
*
* @param id undefined
* @param xhrConfig additional xhr config
*/
public static getCooperator(id: number, xhrConfig?: any): Promise<Cooperator> {
const apiUrl = URL_GET_COOPERATOR.expand({ id });
// 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 Cooperator);
}
}
export default CooperatorService;
......@@ -27,6 +27,9 @@ export class LoginService {
}
public static login(username: string, password: string, xhrConfig?) {
console.error('====================', axiosBackend.defaults.headers);
const form = new FormData();
form.append('username', username);
form.append('password', password);
......@@ -50,6 +53,8 @@ export class LoginService {
*/
public static logout(authToken: string, xhrConfig?): Promise<any> {
console.error('====================', axiosBackend.defaults.headers);
const apiUrl = URL_LOGOUT;
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */};
......
import { AxiosRequestConfig } from 'axios';
import * as UrlTemplate from 'url-template';
import * as QueryString from 'query-string';
// Model
import User from '@gringlobal/client/model/user/User';
import { FilteredPage, IPageRequest } from '@gringlobal/client/model/page';
// utilities
import { axiosBackend } from '@gringlobal/client/utilities/requestUtils';
const LIST_USERS_URL = '/api/user/list';
const GET_USER_URL = UrlTemplate.parse('/api/user/{uuid}');
const LIST_USERS_URL = '/api/v1/user/list';
const GET_USER_URL = UrlTemplate.parse('/api/v1/user/{uuid}');
export default class UserService {
public static listUsers(xhrConfig?: AxiosRequestConfig) {
const apiUrl = LIST_USERS_URL;
/**
* listUsers at /api/v1/user/list
*
* @param filter the user filter
* @param page the page
*/
public static listUsers(filter: string, page: IPageRequest, xhrConfig?): Promise<FilteredPage<User>> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
p: page.page || undefined,
l: page.size || 100,
d: page.direction ? page.direction : undefined,
s: page.properties && page.properties.length && page.properties || undefined,
}, {});
const apiUrl = LIST_USERS_URL + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { };
return axiosBackend.get(apiUrl, {
return axiosBackend({
...xhrConfig,
},
).then(({ data }) => data as User[])
.catch(({ error }) => error as any);
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as FilteredPage<User>);
}
public static getUser(uuid: string, xhrConfig?: AxiosRequestConfig): Promise<User> {
......
{
"p": {
"welcome":{
"title": "Grin global application"
}
"appName": "GRIN-Global CE",
"validations": {
"required": "Required"
}
}
......@@ -3,13 +3,25 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import renderRoutes from '@gringlobal/client/ui/renderRoutes';
// Actions
import { updateHistory } from '@gringlobal/client/action/history';
interface IAppProps extends React.ClassAttributes<any>, RouteComponentProps {
route?: any;
updateHistory: (location: string) => void;
}
class App extends React.Component<IAppProps, any> {
public componentDidUpdate(prevProps: IAppProps) {
const { updateHistory, location: prevLocation } = prevProps;
const { location } = this.props;
if (prevLocation !== null && location !== null) {
if (prevLocation !== location) {
updateHistory(`${prevLocation.pathname}${prevLocation.search ? prevLocation.search : ''}`);
}
}
}
public render() {
const { route: { routes } } = this.props;
return (
......@@ -23,6 +35,7 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
updateHistory,
}, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
// import CircularProgress from '@material-ui/core/CircularProgress';
import CircularProgress from '@material-ui/core/CircularProgress';
interface ILoadingProps extends React.ClassAttributes<any>, WithTranslation {
id?: string;
......@@ -30,6 +30,7 @@ class Loading extends React.Component<ILoadingProps, any> {
return noShow && !immediatelyShow ? null : (
<div className={ className } id={ id } style={ { textAlign: 'center', padding: '5rem' } }>
<CircularProgress />
<div className="font-bold uppercase">{ message || t('common:label.loadingData') }</div>
</div>
);
......
import * as React from 'react';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import withStyles from '@material-ui/core/styles/withStyles';
import { ButtonProps } from '@material-ui/core'
const styles = () => ({
/* eslint-disable */
progress: {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
}
/* eslint-enable */
});
interface IActionButtonProps extends React.ClassAttributes<any> {
title: string;
action: () => any;
style?: any;
className?: string;
sync?: boolean;
classes: any;
}
class ActionButton extends React.Component<IActionButtonProps & ButtonProps, any> {
private _isMounted: boolean = false;
public constructor(props) {
super(props);
this.state = {
running: false,
};
}
public componentDidMount() {
this._isMounted = true;
}
public componentWillUnmount() {
this._isMounted = false;
}
private runAction = () => {
const { action } = this.props;
if (! action) {
return;
}
const { running } = this.state;
if (! running) {
const r = action();
if (r instanceof Promise) {
// console.log('Action response is a promise');
this.setState({ running: true });
(r as Promise<any>).then((result) => {
console.log('ActionButton sync action completed');
return result;
}).catch((e) => {
console.log('ActionButton sync error', e);
throw e;
}).finally(() => {
if (this._isMounted) {
this.setState({ running: false });
}
});
} else {
// console.log(`Action response:`, r, typeof r, r instanceof Promise);
// No need to set state
// this.setState({ running: false });
}
} else {
console.log('ActionButton sync action is running');
}
}
public render() {
const { title, action, variant = 'contained', style, className, classes, sync = false } = this.props;
const { running } = this.state;
if (sync) {
return (
<Button disabled={ running } className={ className } style={ style } variant={ variant } onClick={ this.runAction }>
{ title }
{ running && <CircularProgress size={ 24 } className={ classes.progress }/> }
</Button>
);
} else {
return (
<Button className={ className } style={ style } variant={ variant } onClick={ action }>
{ title }
</Button>
);
}
}
}
export default withStyles(styles)(ActionButton);
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Button from '@material-ui/core/Button';
import navigateTo from '@gringlobal/client/action/navigation';
interface IBackButtonProps extends React.ClassAttributes<any>, WithTranslation {
lastPath: string;
defaultTarget: string;
defaultBackText?: string;
hasHistory: boolean;
lastLocation: string;
navigateTo: (path: string) => void;
style?: any;
itemState?: string;
}
class BackButton extends React.Component<IBackButtonProps, any> {
public constructor(props: any) {
super(props);
}
protected onGoBack = () => {
const { defaultTarget, hasHistory, lastLocation, navigateTo } = this.props;
if (hasHistory) {
navigateTo(lastLocation);
} else {
navigateTo(defaultTarget);
}
};
public render() {
const { defaultBackText, style, t } = this.props;
return (
<Button variant="contained" style={style} onClick={this.onGoBack}>
{defaultBackText || t('common:action.back')}
</Button>
);
}
}
const mapStateToProps = (state) => ({
hasHistory: state.history.hasHistory,
lastLocation: state.history.lastLocation,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
navigateTo,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(BackButton));
<
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { WithStyles, withStyles } from '@material-ui/core/styles';
import InputLabel from '@material-ui/core/InputLabel';
import MuiFormControl from '@material-ui/core/FormControl';
interface IProps extends WithStyles, WithTranslation {
fullWidth?: boolean;
required?: boolean;
label?: string;
children: any;
meta?: any;
disabled?: boolean;
disableAnimation?: boolean;
shrink?: boolean;
className?: string;
t: any;
}
const styles = (theme) => {
// console.log(theme);
return ({
helper: {
paddingTop: '0.5rem',
color: '#FF0000',
},
});
};
const FormControl = ({ fullWidth = false, required = false, label = null, children, meta = {}, disableAnimation = false, disabled = false, shrink, classes, t }: IProps) => {
// console.log('Meta', meta);
const { error, warning } = meta;
return (