canner
Version:
Build CMS in few lines of code for different data sources
396 lines (366 loc) • 10.8 kB
JavaScript
// @flow
import * as React from 'react';
import {get} from 'lodash';
import DefaultToolbarLayout from './toolbarlayout';
import Pagination from './pagination';
import Sort from './sort';
import Filter from './filter/index';
import Actions from './actions';
import isObject from 'lodash/isObject';
import type {Query} from '../../query';
import type RefId from 'canner-ref-id';
type Args = {
pagination: {
first?: number,
after?: string
},
where?: Object,
orderBy?: string,
updateQuery: Function,
}
type Props = {
children: React.Node,
updateQuery: Function,
parseConnectionToNormal: Function,
getValue: Function,
refId: RefId,
keyName: string,
defaultValue: Function,
children: React.Element<*>,
toolbar: {
async: boolean,
actions?: {
component?: React.ComponentType<*>,
export?: {
fields?: Array<Object>,
title?: string,
filename?: string
},
import?: {},
filter?: {}
},
sorter?: {
component?: React.ComponentType<*>,
[string]: *
},
pagination?: {
component?: React.ComponentType<*>,
[string]: *
},
filter?: {
component?: React.ComponentType<*>,
filters?: Array<Object>,
[string]: *
},
toolbarLayout?: {
component?: React.ComponentType<*>,
[string]: *
}
},
query: Query,
refId: RefId,
args: Args,
items: Object,
originRootValue: Object,
onChange?: Function
}
type State = {
originRootValue: any,
sort: any,
filter: any,
pagination: any,
current: number,
displayedFilterIndexs: Array<number>
}
export default class Toolbar extends React.PureComponent<Props, State> {
async: boolean;
constructor(props: Props) {
super(props);
const {args, originRootValue} = props;
this.async = props.toolbar && props.toolbar.async;
// $FlowFixMe
const permanentFilter = (props.toolbar && props.toolbar.filter && props.toolbar.filter.permanentFilter) || {};
this.state = {
originRootValue,
sort: parseOrder(args.orderBy),
filter: {...parseWhere(args.where || {}), ...parseWhere(permanentFilter)},
pagination: parsePagination(args),
current: 1,
displayedFilterIndexs: []
};
}
static getDerivedStateFromProps(nextProps: Props) {
const {originRootValue, args, toolbar} = nextProps;
if (toolbar && !toolbar.async) {
return {
originRootValue
};
}
// $FlowFixMe
const permanentFilter = (toolbar.filter && toolbar.filter.permanentFilter) || {};
return {
originRootValue,
sort: parseOrder(args.orderBy),
filter: {...parseWhere(args.where || {}), ...parseWhere(permanentFilter)},
pagination: parsePagination(args)
};
}
changeOrder = ({orderField, orderType}: {orderField: string, orderType: string}) => {
const {updateQuery, refId, args} = this.props;
if (this.async) {
if (orderField) {
updateQuery(refId.getPathArr(), {first: 10, orderBy: `${orderField}_${orderType}`, where: args.where});
} else {
updateQuery(refId.getPathArr(), {first: 10, where: args.where});
}
} else {
if (orderField) {
this.setState({
sort: {
orderField,
orderType
}
});
} else {
this.setState({
sort: {}
});
}
}
}
changeFilter = (where: Object) => {
const {updateQuery, refId, args, toolbar} = this.props;
let permanentFilter = {}
if (toolbar && toolbar.filter && toolbar.filter.permanentFilter) {
permanentFilter = toolbar.filter.permanentFilter || {};
}
if (this.async) {
updateQuery(refId.getPathArr(), {
first: 10,
orderBy: args.orderBy,
// $FlowFixMe
where: {...processWhere(where), ...permanentFilter}
});
} else {
this.setState({
// $FlowFixMe
filter: {...where}
});
}
}
changePage = (page: number) => {
this.setState({
current: page
});
}
nextPage = () => {
const {updateQuery, args, refId, keyName} = this.props;
const {originRootValue, pagination} = this.state;
if (get(originRootValue, [keyName, 'pageInfo', 'hasNextPage'])) {
const endCursor = get(originRootValue, [keyName, 'pageInfo', 'endCursor'], null);
const after = endCursor || originRootValue[keyName].edges.slice(-1)[0].cursor; // the last one
updateQuery(refId.getPathArr(), {
...args,
first: pagination.first || 10,
after,
last: undefined,
before: undefined
});
}
}
prevPage = () => {
const {updateQuery, args, refId, keyName} = this.props;
const {originRootValue, pagination} = this.state;
if (get(originRootValue, [keyName, 'pageInfo', 'hasPreviousPage'])) {
const startCursor = get(originRootValue, [keyName, 'pageInfo', 'startCursor'], null);
const before = startCursor || get(originRootValue, [keyName, 'edges', 0, 'cursor']); // the first one
updateQuery(refId.getPathArr(), {
...args,
last: pagination.last || 10,
before,
after: undefined,
first: undefined
});
}
}
changeSize = (size: number) => {
const {updateQuery, args, refId} = this.props;
const pagination = parsePagination(args);
if (pagination.last) {
updateQuery(refId.getPathArr(), {
...args,
last: size
});
} else {
updateQuery(refId.getPathArr(), {
...args,
first: size
});
}
}
addFilter = (index: number) => {
this.setState({
displayedFilterIndexs: this.state.displayedFilterIndexs.concat(index)
});
}
deleteFilter = (index: number) => {
this.setState({
displayedFilterIndexs: this.state.displayedFilterIndexs.filter(i => i !== index)
});
}
render() {
const {
children,
toolbar = {},
args,
refId,
items,
defaultValue,
parseConnectionToNormal,
getValue,
query,
keyName,
request,
deploy
} = this.props;
let {originRootValue, current, displayedFilterIndexs} = this.state;
const {sorter, pagination, filter, toolbarLayout, actions} = toolbar;
const ToolbarLayout = toolbarLayout && toolbarLayout.component ? toolbarLayout.component : DefaultToolbarLayout;
const SortComponent = sorter && sorter.component ? sorter.component : Sort;
const FilterComponent = filter && filter.component ? filter.component : Filter;
const PaginationComponent = pagination && pagination.component ? pagination.component : Pagination;
const ActionsComponent = actions && actions.component ? actions.component : Actions;
const {orderField, orderType} = parseOrder(args.orderBy);
const where = parseWhere(args.where || {});
const {first, last} = parsePagination(args);
let total = 0;
const rootValue = parseConnectionToNormal(originRootValue);
const value = getValue(originRootValue, refId.getPathArr());
return <ToolbarLayout
Actions={actions && toolbar.async ? <ActionsComponent
{...actions}
async={toolbar.async}
filters={filter && filter.filters || []}
displayedFilters={displayedFilterIndexs}
addFilter={this.addFilter}
query={query}
keyName={keyName}
value={rootValue[keyName]}
items={items.items}
request={request}
deploy={deploy}
/> : <div />}
Sort={sorter && toolbar.async ? <SortComponent
{...sorter}
async={toolbar.async}
defaultField={sorter.defaultField}
options={sorter.options || []}
changeOrder={this.changeOrder}
orderField={orderField}
orderType={orderType}
items={items}
/> : null}
Pagination={pagination && toolbar.async ? <PaginationComponent
{...pagination}
async={toolbar.async}
hasNextPage={get(value, ['pageInfo', 'hasNextPage'])}
hasPreviousPage={get(value, ['pageInfo', 'hasPreviousPage'])}
nextPage={this.nextPage}
prevPage={this.prevPage}
changeSize={this.changeSize}
size={first || last}
current={current}
changePage={this.changePage}
total={total}
/> : null}
Filter={filter && toolbar.async ? <FilterComponent
async={toolbar.async}
{...filter}
displayedFilters={displayedFilterIndexs}
items={items}
where={where}
changeFilter={this.changeFilter}
deleteFilter={this.deleteFilter}
/> : null}
>
{React.cloneElement(children, {
rootValue,
value: value ? get(value, 'edges', []).map(item => item.node) : defaultValue('array'),
showPagination: toolbar && !toolbar.async
})}
</ToolbarLayout>
}
}
export function parseOrder(orderBy: ?string): {orderField: string | null, orderType: 'ASC' | 'DESC'} {
if (typeof orderBy === 'string') {
const [orderField, orderType] = orderBy.split('_');
if (orderType !== 'ASC' && orderType !== 'DESC') {
return {orderField, orderType: 'ASC'};
}
return {orderField, orderType};
}
return {
orderField: null,
orderType: 'ASC'
};
}
export function parsePagination(args: Object = {}) {
return {
first: args.first,
after: args.after,
last: args.last,
before: args.before
}
}
export function parseWhere(where: Object) {
return Object.keys(where).reduce((result: Object, key: string) => {
const v = where[key];
const type = typeof v;
const [field, op] = key.split('_');
if (type === 'string') {
result[field] = {[op || 'eq']: v};
}
if (type === 'boolean') {
result[field] = {[op || 'eq']: v};
}
if (type === 'number') {
result[field] = {[op || 'eq']: v};
}
if (type === 'object') {
result[field] = parseWhere(v);
}
return result;
}, {});
}
export function processWhere(where: Object) {
return Object.keys(where).reduce((result: Object, key: string) => {
const v = where[key];
if (isEnd(v)) {
const {op, value} = parseOpAndValue(v);
result[`${key}_${op}`] = value;
} else {
result[key] = processWhere(v);
}
return result;
}, {});
}
function isEnd(v: Object) {
if (!isObject(v)) {
return false;
}
const keys = Object.keys(v);
const value = v[keys[0]];
return keys.length === 1 &&
['lt', 'lte', 'gt', 'gte', 'eq', 'contains'].indexOf(keys[0]) !== -1 &&
(typeof value === 'string' ||
typeof value === 'boolean' ||
typeof value === 'number');
}
function parseOpAndValue(v: Object) {
const op = Object.keys(v)[0];
const value = v[op];
return {
op,
value
}
}