Vulcan/packages/vulcan-core/lib/modules/components/Datatable.jsx
2018-11-29 14:56:24 +01:00

464 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { registerComponent, getCollection, Utils } from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import withCurrentUser from '../containers/withCurrentUser.js';
import withComponents from '../containers/withComponents';
import withMulti from '../containers/withMulti.js';
import { FormattedMessage, intlShape } from 'meteor/vulcan:i18n';
import { getFieldValue } from './Card.jsx';
import _sortBy from 'lodash/sortBy';
/*
Datatable Component
*/
// see: http://stackoverflow.com/questions/1909441/jquery-keyup-delay
const delay = (function(){
var timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})();
const getColumnName = column => (
typeof column === 'string'
? column
: column.label || column.name
);
class Datatable extends PureComponent {
constructor() {
super();
this.updateQuery = this.updateQuery.bind(this);
this.state = {
value: '',
query: '',
currentSort: {}
};
}
toggleSort = column => {
let currentSort;
if (!this.state.currentSort[column]) {
currentSort = { [column] : 1 };
} else if (this.state.currentSort[column] === 1) {
currentSort = { [column] : -1 };
} else {
currentSort = {};
}
this.setState({ currentSort });
}
updateQuery(e) {
e.persist();
e.preventDefault();
this.setState({
value: e.target.value
});
delay(() => {
this.setState({
query: e.target.value
});
}, 700 );
}
render() {
const { Components } = this.props;
if (this.props.data) { // static JSON datatable
return <Components.DatatableContents Components={Components} columns={Object.keys(this.props.data[0])} {...this.props} results={this.props.data} showEdit={false} showNew={false} />;
} else { // dynamic datatable with data loading
const collection = this.props.collection || getCollection(this.props.collectionName);
const options = {
collection,
...this.props.options
};
const DatatableWithMulti = withMulti(options)(Components.DatatableContents);
const canInsert = collection.options && collection.options.mutations && collection.options.mutations.new && collection.options.mutations.new.check(this.props.currentUser);
// add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;
// see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782
// this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays {}. So we test on the length of keys for this object.
const orderBy = Object.keys(this.state.currentSort).length == 0 ? {} : { ...this.state.currentSort, _id: -1 };
return (
<Components.DatatableLayout Components={Components} collectionName={collection.options.collectionName}>
<Components.DatatableAbove Components={Components} {...this.props} collection={collection} canInsert={canInsert} value={this.state.value} updateQuery={this.updateQuery} />
<DatatableWithMulti Components={Components} {...this.props} collection={collection} terms={{query: this.state.query, orderBy: orderBy }} currentUser={this.props.currentUser} toggleSort={this.toggleSort} currentSort={this.state.currentSort}/>
</Components.DatatableLayout>
);
}
}
}
Datatable.propTypes = {
title: PropTypes.string,
collection: PropTypes.object,
columns: PropTypes.array,
data: PropTypes.array,
options: PropTypes.object,
showEdit: PropTypes.bool,
showNew: PropTypes.bool,
showSearch: PropTypes.bool,
newFormOptions: PropTypes.object,
editFormOptions: PropTypes.object,
emptyState: PropTypes.object,
Components: PropTypes.object.isRequired,
};
Datatable.defaultProps = {
showNew: true,
showEdit: true,
showSearch: true,
};
registerComponent({ name: 'Datatable', component: Datatable, hocs: [withCurrentUser, withComponents] });
export default Datatable;
const DatatableLayout = ({ collectionName, children }) => (
<div className={`datatable datatable-${collectionName}`}>
{children}
</div>
);
registerComponent({ name: 'DatatableLayout', component: DatatableLayout });
/*
DatatableAbove Component
*/
const DatatableAbove = (props, { intl }) => {
const { collection, currentUser, showSearch, showNew, canInsert,
value, updateQuery, options, newFormOptions, Components } = props;
return (
<Components.DatatableAboveLayout>
{showSearch && (
<Components.DatatableAboveSearchInput
className="datatable-search form-control"
placeholder={`${intl.formatMessage({ id: 'datatable.search', defaultMessage: 'Search' })}…`}
type="text"
name="datatableSearchQuery"
value={value}
onChange={updateQuery}
/>
)}
{showNew && canInsert && <Components.NewButton collection={collection} currentUser={currentUser} mutationFragmentName={options && options.fragmentName} {...newFormOptions}/>}
</Components.DatatableAboveLayout>
);
};
DatatableAbove.contextTypes = {
intl: intlShape,
};
DatatableAbove.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableAbove', DatatableAbove);
const DatatableAboveSearchInput = (props) => (
<input
{...props}
/>
);
registerComponent({ name: 'DatatableAboveSearchInput', component: DatatableAboveSearchInput });
const DatatableAboveLayout = ({ children }) => (
<div className="datatable-above">
{children}
</div>
);
registerComponent({ name: 'DatatableAboveLayout', component: DatatableAboveLayout });
/*
DatatableHeader Component
*/
const DatatableHeader = ({ collection, column, toggleSort, currentSort, Components }, { intl }) => {
const columnName = getColumnName(column);
if (collection) {
const schema = collection.simpleSchema()._schema;
/*
use either:
1. the column name translation
2. the column name label in the schema (if the column name matches a schema field)
3. the raw column name.
*/
const defaultMessage = schema[columnName] ? schema[columnName].label : Utils.camelToSpaces(columnName);
const formattedLabel = intl.formatMessage({ id: `${collection._name}.${columnName}`, defaultMessage });
// if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use column.name
const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;
return column.sortable
? <Components.DatatableSorter name={sortPropertyName} label={formattedLabel} toggleSort={toggleSort} currentSort={currentSort} sortable={column.sortable}/>
: <Components.DatatableHeaderCellLayout>{formattedLabel}</Components.DatatableHeaderCellLayout>;
} else {
const formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
return (
<Components.DatatableHeaderCellLayout
className={`datatable-th-${columnName.toLowerCase().replace(/\s/g, '-')}`}
>
{formattedLabel}
</Components.DatatableHeaderCellLayout>
);
}
};
DatatableHeader.contextTypes = {
intl: intlShape
};
DatatableHeader.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableHeader', DatatableHeader);
const DatatableHeaderCellLayout = ({ children, ...otherProps }) => (
<th {...otherProps}>{children}</th>
);
registerComponent({ name: 'DatatableHeaderCellLayout', component: DatatableHeaderCellLayout });
const SortNone = () =>
<svg width='16' height='16' viewBox='0 0 438 438' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M25.7368 247.243H280.263C303.149 247.243 314.592 274.958 298.444 291.116L171.18 418.456C161.128 428.515 144.872 428.515 134.926 418.456L7.55631 291.116C-8.59221 274.958 2.85078 247.243 25.7368 247.243ZM298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z' transform='translate(66 6)' fill='#000' fillOpacity='0.2' />
</svg>;
const SortDesc = () =>
<svg width="16" height="16" viewBox="0 0 438 438" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.7368 0H280.263C303.149 0 314.592 27.7151 298.444 43.8734L171.18 171.213C161.128 181.272 144.872 181.272 134.926 171.213L7.55631 43.8734C-8.59221 27.7151 2.85078 0 25.7368 0Z" transform="translate(66 253.243)" fill="black" fillOpacity="0.7"/>
<path d="M171.18 7.54408L298.444 134.884C314.592 151.042 303.149 178.757 280.263 178.757H25.7368C2.85078 178.757 -8.59221 151.042 7.55631 134.884L134.926 7.54408C144.872 -2.51469 161.128 -2.51469 171.18 7.54408Z" transform="translate(66 6)" fill="black" fillOpacity="0.2"/>
</svg>;
const SortAsc = () =>
<svg width="16" height="16" viewBox="0 0 438 438" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M298.444 134.884L171.18 7.54408C161.128 -2.51469 144.872 -2.51469 134.926 7.54408L7.55631 134.884C-8.59221 151.042 2.85078 178.757 25.7368 178.757H280.263C303.149 178.757 314.592 151.042 298.444 134.884Z" transform="translate(66 6)" fill="black" fillOpacity="0.7"/>
<path d="M280.263 0H25.7368C2.85078 0 -8.59221 27.7151 7.55631 43.8734L134.926 171.213C144.872 181.272 161.128 181.272 171.18 171.213L298.444 43.8734C314.592 27.7151 303.149 0 280.263 0Z" transform="translate(66 253.243)" fill="black" fillOpacity="0.2"/>
</svg>;
const DatatableSorter = ({ name, label, toggleSort, currentSort }) =>
<th>
<div className="datatable-sorter" onClick={() => {toggleSort(name);}}>
<span className="datatable-sorter-label">{label}</span>
<span className="sort-icon">
{!currentSort[name] ? (
<SortNone/>
) : currentSort[name] === 1 ? (
<SortDesc/>
) : (
<SortAsc/>
)
}
</span>
</div>
</th>;
registerComponent('DatatableSorter', DatatableSorter);
/*
DatatableContents Component
*/
const DatatableContents = (props) => {
// if no columns are provided, default to using keys of first array item
const { title, collection, results, columns, loading, loadMore,
count, totalCount, networkStatus, showEdit, currentUser, emptyState,
toggleSort, currentSort,
Components } = props;
if (loading) {
return <div className="datatable-list datatable-list-loading"><Components.Loading /></div>;
} else if (!results || !results.length) {
return emptyState || null;
}
const isLoadingMore = networkStatus === 2;
const hasMore = totalCount > results.length;
const sortedColumns = _sortBy(columns, column => column.order);
return (
<Components.DatatableContentsLayout>
{title && <Components.DatatableTitle title={title}/>}
<Components.DatatableContentsInnerLayout>
<Components.DatatableContentsHeadLayout>
{
sortedColumns
.map((column, index) => (
<Components.DatatableHeader
Components={Components}
key={index} collection={collection} column={column}
toggleSort={toggleSort} currentSort={currentSort} />)
)
}
{showEdit ? <th><FormattedMessage id="datatable.edit" /></th> : null}
</Components.DatatableContentsHeadLayout>
<Components.DatatableContentsBodyLayout>
{results.map((document, index) => <Components.DatatableRow {...props} collection={collection} columns={columns} document={document} key={index} showEdit={showEdit} currentUser={currentUser} />)}
</Components.DatatableContentsBodyLayout>
</Components.DatatableContentsInnerLayout>
{hasMore &&
<Components.DatatableContentsMoreLayout>
{isLoadingMore
? <Components.Loading />
: (
<Components.DatatableLoadMoreButton onClick={e => { e.preventDefault(); loadMore(); }}>
Load More ({count}/{totalCount})
</Components.DatatableLoadMoreButton>
)
}
</Components.DatatableContentsMoreLayout>
}
</Components.DatatableContentsLayout>
);
};
DatatableContents.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableContents', DatatableContents);
const DatatableContentsLayout = ({ children }) => (
<div className="datatable-list">
{children}
</div>
);
registerComponent({ name: 'DatatableContentsLayout', component: DatatableContentsLayout });
const DatatableContentsInnerLayout = ({ children }) => (
<table className="table">
{children}
</table>
);
registerComponent({ name: 'DatatableContentsInnerLayout', component: DatatableContentsInnerLayout });
const DatatableContentsHeadLayout = ({ children }) => (
<thead>
<tr>
{children}
</tr>
</thead>
);
registerComponent({ name: 'DatatableContentsHeadLayout', component: DatatableContentsHeadLayout });
const DatatableContentsBodyLayout = ({ children }) => (
<tbody>{children}</tbody>
);
registerComponent({ name: 'DatatableContentsBodyLayout', component: DatatableContentsBodyLayout });
const DatatableContentsMoreLayout = ({ children }) => (
<div className="datatable-list-load-more">
{children}
</div>
);
registerComponent({ name: 'DatatableContentsMoreLayout', component: DatatableContentsMoreLayout });
const DatatableLoadMoreButton = ({ count, totalCount, Components, children, ...otherProps }) => (
<Components.Button variant="primary" {...otherProps}>{children}</Components.Button>
);
registerComponent({ name: 'DatatableLoadMoreButton', component: DatatableLoadMoreButton });
/*
DatatableTitle Component
*/
const DatatableTitle = ({ title }) =>
<div className="datatable-title">{title}</div>;
registerComponent('DatatableTitle', DatatableTitle);
/*
DatatableRow Component
*/
const DatatableRow = (props, { intl }) => {
const { collection, columns, document, showEdit,
currentUser, options, editFormOptions, rowClass, Components } = props;
const canEdit = collection && collection.options && collection.options.mutations && collection.options.mutations.edit && collection.options.mutations.edit.check(currentUser, document);
const row = typeof rowClass === 'function' ? rowClass(document) : rowClass || '';
const modalProps = { title: <code>{document._id}</code> };
const sortedColumns = _sortBy(columns, column => column.order);
return (
<Components.DatatableRowLayout className={`datatable-item ${row}`}>
{
sortedColumns
.map((column, index) => (
<Components.DatatableCell
key={index}
Components={Components}
column={column} document={document}
currentUser={currentUser} />
))}
{showEdit && canEdit ?
<Components.DatatableCellLayout>
<Components.EditButton collection={collection} documentId={document._id} currentUser={currentUser} mutationFragmentName={options && options.fragmentName} modalProps={modalProps} {...editFormOptions}/>
</Components.DatatableCellLayout>
: null}
</Components.DatatableRowLayout>
);
};
DatatableRow.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableRow', DatatableRow);
DatatableRow.contextTypes = {
intl: intlShape
};
const DatatableRowLayout = ({ children, ...otherProps }) => (
<tr {...otherProps}>
{children}
</tr>
);
registerComponent({ name: 'DatatableRowLayout', component: DatatableRowLayout });
/*
DatatableCell Component
*/
const DatatableCell = ({ column, document, currentUser, Components }) => {
const Component = column.component
|| column.componentName && Components[column.componentName]
|| Components.DatatableDefaultCell;
const columnName = getColumnName(column);
return (
<Components.DatatableCellLayout className={`datatable-item-${columnName.toLowerCase().replace(/\s/g, '-')}`}>
<Component column={column} document={document} currentUser={currentUser} />
</Components.DatatableCellLayout>
);
};
DatatableCell.propTypes = {
Components: PropTypes.object.isRequired
};
registerComponent('DatatableCell', DatatableCell);
const DatatableCellLayout = ({ children, ...otherProps }) => (
<td {...otherProps}>{children}</td>
);
registerComponent({ name: 'DatatableCellLayout', component: DatatableCellLayout });
/*
DatatableDefaultCell Component
*/
const DatatableDefaultCell = ({ column, document }) =>
<div>{typeof column === 'string' ? getFieldValue(document[column]) : getFieldValue(document[column.name])}</div>;
registerComponent('DatatableDefaultCell', DatatableDefaultCell);