Fix listing related eslint PropTypes violations
This commit is contained in:
@@ -123,7 +123,10 @@ FormField.propTypes = {
|
||||
field: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
values: PropTypes.object,
|
||||
tip: PropTypes.string,
|
||||
tip: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.string,
|
||||
]),
|
||||
label: PropTypes.string,
|
||||
fields: PropTypes.array,
|
||||
description: PropTypes.string,
|
||||
|
@@ -26,6 +26,11 @@ class Form extends React.Component {
|
||||
id: '',
|
||||
onSubmit: undefined,
|
||||
automationId: '',
|
||||
messages: {
|
||||
onUpdate: () => { /* no-op */ },
|
||||
onCreate: () => { /* no-op */ },
|
||||
},
|
||||
endpoint: undefined,
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -67,6 +72,7 @@ class Form extends React.Component {
|
||||
loadItem = (id) => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
if (!this.props.endpoint) return;
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: this.props.endpoint,
|
||||
@@ -120,6 +126,8 @@ class Form extends React.Component {
|
||||
item.id = this.props.params.id;
|
||||
}
|
||||
|
||||
if (!this.props.endpoint) return;
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: this.props.endpoint,
|
||||
@@ -257,14 +265,14 @@ Form.propTypes = {
|
||||
}).isRequired,
|
||||
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
endpoint: PropTypes.string.isRequired,
|
||||
endpoint: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.object),
|
||||
messages: PropTypes.shape({
|
||||
onUpdate: PropTypes.func,
|
||||
onCreate: PropTypes.func,
|
||||
}).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
children: PropTypes.element,
|
||||
children: PropTypes.array, // eslint-disable-line react/forbid-prop-types
|
||||
id: PropTypes.string,
|
||||
automationId: PropTypes.string,
|
||||
beforeFormContent: PropTypes.func,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingBulkActions extends React.Component {
|
||||
state = {
|
||||
@@ -7,20 +8,16 @@ class ListingBulkActions extends React.Component {
|
||||
extra: false,
|
||||
};
|
||||
|
||||
handleChangeAction = (e) => {
|
||||
this.setState({
|
||||
action: e.target.value,
|
||||
extra: false,
|
||||
}, () => {
|
||||
const action = this.getSelectedAction();
|
||||
getSelectedAction = () => {
|
||||
const selectedAction = this.action.value;
|
||||
if (selectedAction.length > 0) {
|
||||
const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
|
||||
|
||||
// action on select callback
|
||||
if (action !== null && action.onSelect !== undefined) {
|
||||
this.setState({
|
||||
extra: action.onSelect(e),
|
||||
});
|
||||
if (action.length > 0) {
|
||||
return action[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
handleApplyAction = (e) => {
|
||||
@@ -60,16 +57,20 @@ class ListingBulkActions extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
getSelectedAction = () => {
|
||||
const selectedAction = this.action.value;
|
||||
if (selectedAction.length > 0) {
|
||||
const action = this.props.bulk_actions.filter(act => (act.name === selectedAction));
|
||||
handleChangeAction = (e) => {
|
||||
this.setState({
|
||||
action: e.target.value,
|
||||
extra: false,
|
||||
}, () => {
|
||||
const action = this.getSelectedAction();
|
||||
|
||||
if (action.length > 0) {
|
||||
return action[0];
|
||||
// action on select callback
|
||||
if (action !== null && action.onSelect !== undefined) {
|
||||
this.setState({
|
||||
extra: action.onSelect(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -113,4 +114,14 @@ class ListingBulkActions extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ListingBulkActions.propTypes = {
|
||||
bulk_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onBulkAction: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ListingBulkActions;
|
||||
|
@@ -1,34 +1,9 @@
|
||||
import React from 'react';
|
||||
import jQuery from 'jquery';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingFilters extends React.Component {
|
||||
handleFilterAction = () => {
|
||||
const filters = {};
|
||||
this.getAvailableFilters().forEach((filter, i) => {
|
||||
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
|
||||
});
|
||||
if (this.props.onBeforeSelectFilter) {
|
||||
this.props.onBeforeSelectFilter(filters);
|
||||
}
|
||||
return this.props.onSelectFilter(filters);
|
||||
};
|
||||
|
||||
handleEmptyTrash = () => {
|
||||
return this.props.onEmptyTrash();
|
||||
};
|
||||
|
||||
getAvailableFilters = () => {
|
||||
const filters = this.props.filters;
|
||||
return Object.keys(filters).filter(filter => !(
|
||||
filters[filter].length === 0
|
||||
|| (
|
||||
filters[filter].length === 1
|
||||
&& !filters[filter][0].value
|
||||
)
|
||||
));
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
const selectedFilters = this.props.filter;
|
||||
this.getAvailableFilters().forEach(
|
||||
@@ -42,6 +17,30 @@ class ListingFilters extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getAvailableFilters = () => {
|
||||
const filters = this.props.filters;
|
||||
return Object.keys(filters).filter(filter => !(
|
||||
filters[filter].length === 0
|
||||
|| (
|
||||
filters[filter].length === 1
|
||||
&& !filters[filter][0].value
|
||||
)
|
||||
));
|
||||
};
|
||||
|
||||
handleEmptyTrash = () => this.props.onEmptyTrash();
|
||||
|
||||
handleFilterAction = () => {
|
||||
const filters = {};
|
||||
this.getAvailableFilters().forEach((filter, i) => {
|
||||
filters[this[`filter-${i}`].name] = this[`filter-${i}`].value;
|
||||
});
|
||||
if (this.props.onBeforeSelectFilter) {
|
||||
this.props.onBeforeSelectFilter(filters);
|
||||
}
|
||||
return this.props.onSelectFilter(filters);
|
||||
};
|
||||
|
||||
render() {
|
||||
const filters = this.props.filters;
|
||||
const availableFilters = this.getAvailableFilters()
|
||||
@@ -96,4 +95,20 @@ class ListingFilters extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ListingFilters.propTypes = {
|
||||
filters: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
]).isRequired,
|
||||
onEmptyTrash: PropTypes.func.isRequired,
|
||||
onBeforeSelectFilter: PropTypes.func,
|
||||
onSelectFilter: PropTypes.func.isRequired,
|
||||
filter: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ListingFilters.defaultProps = {
|
||||
onBeforeSelectFilter: undefined,
|
||||
};
|
||||
|
||||
export default ListingFilters;
|
||||
|
@@ -1,13 +1,10 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListingColumn from './listing_column.jsx';
|
||||
|
||||
class ListingHeader extends React.Component {
|
||||
handleSelectItems = () => {
|
||||
return this.props.onSelectItems(
|
||||
this.toggle.checked
|
||||
);
|
||||
};
|
||||
handleSelectItems = () => this.props.onSelectItems(this.toggle.checked);
|
||||
|
||||
render() {
|
||||
const columns = this.props.columns.map((column, index) => {
|
||||
@@ -57,47 +54,23 @@ class ListingHeader extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class ListingColumn extends React.Component {
|
||||
handleSort = () => {
|
||||
const sortBy = this.props.column.name;
|
||||
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
|
||||
this.props.onSort(sortBy, sortOrder);
|
||||
};
|
||||
ListingHeader.propTypes = {
|
||||
onSelectItems: PropTypes.func.isRequired,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object),
|
||||
sort_by: PropTypes.string,
|
||||
sort_order: PropTypes.string,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = classNames(
|
||||
'manage-column',
|
||||
{ 'column-primary': this.props.column.is_primary },
|
||||
{ sortable: this.props.column.sortable },
|
||||
this.props.column.sorted,
|
||||
{ sorted: (this.props.sort_by === this.props.column.name) }
|
||||
);
|
||||
let label;
|
||||
|
||||
if (this.props.column.sortable === true) {
|
||||
label = (
|
||||
<a
|
||||
onClick={this.handleSort}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span>{ this.props.column.label }</span>
|
||||
<span className="sorting-indicator" />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
label = this.props.column.label;
|
||||
}
|
||||
return (
|
||||
<th
|
||||
role="columnheader"
|
||||
className={classes}
|
||||
id={this.props.column.name}
|
||||
scope="col"
|
||||
width={this.props.column.width || null}
|
||||
>{label}</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
ListingHeader.defaultProps = {
|
||||
columns: [],
|
||||
sort_by: undefined,
|
||||
sort_order: 'desc',
|
||||
};
|
||||
|
||||
module.exports = ListingHeader;
|
||||
|
@@ -1,312 +1,76 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import jQuery from 'jquery';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import _ from 'underscore';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ListingBulkActions from 'listing/bulk_actions.jsx';
|
||||
import ListingHeader from 'listing/header.jsx';
|
||||
import ListingPages from 'listing/pages.jsx';
|
||||
import ListingSearch from 'listing/search.jsx';
|
||||
import ListingGroups from 'listing/groups.jsx';
|
||||
import ListingFilters from 'listing/filters.jsx';
|
||||
import ListingItems from 'listing/listing_items.jsx';
|
||||
|
||||
class ListingItem extends React.Component {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
handleSelectItem = (e) => {
|
||||
this.props.onSelectItem(
|
||||
parseInt(e.target.value, 10),
|
||||
e.target.checked
|
||||
);
|
||||
|
||||
return !e.target.checked;
|
||||
};
|
||||
|
||||
handleRestoreItem = (id) => {
|
||||
this.props.onRestoreItem(id);
|
||||
};
|
||||
|
||||
handleTrashItem = (id) => {
|
||||
this.props.onTrashItem(id);
|
||||
};
|
||||
|
||||
handleDeleteItem = (id) => {
|
||||
this.props.onDeleteItem(id);
|
||||
};
|
||||
|
||||
handleToggleItem = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
};
|
||||
|
||||
render() {
|
||||
let checkbox = false;
|
||||
|
||||
if (this.props.is_selectable === true) {
|
||||
checkbox = (
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
|
||||
`Select ${this.props.item[this.props.columns[0].name]}`
|
||||
}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={this.props.item.id}
|
||||
checked={
|
||||
this.props.item.selected || this.props.selection === 'all'
|
||||
}
|
||||
onChange={this.handleSelectItem}
|
||||
disabled={this.props.selection === 'all'}
|
||||
id={`listing-row-checkbox-${this.props.item.id}`}
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
const customActions = this.props.item_actions;
|
||||
let itemActions = false;
|
||||
|
||||
if (customActions.length > 0) {
|
||||
let isFirst = true;
|
||||
itemActions = customActions
|
||||
.filter(action => action.display === undefined || action.display(this.props.item))
|
||||
.map((action, index) => {
|
||||
let customAction = null;
|
||||
|
||||
if (action.name === 'trash') {
|
||||
customAction = (
|
||||
<span key={`action-${action.name}`} className="trash">
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleTrashItem(this.props.item.id)}
|
||||
>
|
||||
{MailPoet.I18n.t('moveToTrash')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
} else if (action.refresh) {
|
||||
customAction = (
|
||||
<span
|
||||
onClick={this.props.onRefreshItems}
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
role="button"
|
||||
tabIndex={index}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else if (action.link) {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={
|
||||
(action.onClick !== undefined)
|
||||
? () => action.onClick(this.props.item, this.props.onRefreshItems)
|
||||
: false
|
||||
}
|
||||
>{ action.label }</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (customAction !== null && isFirst === true) {
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return customAction;
|
||||
});
|
||||
} else {
|
||||
itemActions = (
|
||||
<span className="edit">
|
||||
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let actions;
|
||||
|
||||
if (this.props.group === 'trash') {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
<span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleRestoreItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('restore')}</a>
|
||||
</span>
|
||||
{ ' | ' }
|
||||
<span className="delete">
|
||||
<a
|
||||
className="submitdelete"
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleDeleteItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('deletePermanently')}</a>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
{ itemActions }
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
|
||||
|
||||
return (
|
||||
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
|
||||
{ checkbox }
|
||||
{ this.props.onRenderItem(this.props.item, actions) }
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListingItems extends React.Component {
|
||||
render() {
|
||||
if (this.props.items.length === 0) {
|
||||
let message;
|
||||
if (this.props.loading === true) {
|
||||
message = (this.props.messages.onLoadingItems
|
||||
&& this.props.messages.onLoadingItems(this.props.group))
|
||||
|| MailPoet.I18n.t('loadingItems');
|
||||
} else {
|
||||
message = (this.props.messages.onNoItemsFound
|
||||
&& this.props.messages.onNoItemsFound(this.props.group))
|
||||
|| MailPoet.I18n.t('noItemsFound');
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className="no-items">
|
||||
<td
|
||||
colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
className="colspanchange"
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
const selectAllClasses = classNames(
|
||||
'mailpoet_select_all',
|
||||
{ mailpoet_hidden: (
|
||||
this.props.selection === false
|
||||
|| (this.props.count <= this.props.limit)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className={selectAllClasses}>
|
||||
<td colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
>
|
||||
{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLabel')
|
||||
: MailPoet.I18n.t('selectedAllLabel').replace(
|
||||
'%d',
|
||||
this.props.count.toLocaleString()
|
||||
)
|
||||
}
|
||||
|
||||
<a
|
||||
onClick={this.props.onSelectAll}
|
||||
href="javascript:;"
|
||||
>{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLink')
|
||||
: MailPoet.I18n.t('clearSelection')
|
||||
}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.props.items.map((item) => {
|
||||
const renderItem = item;
|
||||
renderItem.id = parseInt(item.id, 10);
|
||||
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
|
||||
let key = `item-${renderItem.id}-${item.id}`;
|
||||
if (typeof this.props.getListingItemKey === 'function') {
|
||||
key = this.props.getListingItemKey(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListingItem
|
||||
columns={this.props.columns}
|
||||
onSelectItem={this.props.onSelectItem}
|
||||
onRenderItem={this.props.onRenderItem}
|
||||
onDeleteItem={this.props.onDeleteItem}
|
||||
onRestoreItem={this.props.onRestoreItem}
|
||||
onTrashItem={this.props.onTrashItem}
|
||||
onRefreshItems={this.props.onRefreshItems}
|
||||
selection={this.props.selection}
|
||||
is_selectable={this.props.is_selectable}
|
||||
item_actions={this.props.item_actions}
|
||||
group={this.props.group}
|
||||
key={key}
|
||||
item={renderItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Listing = createReactClass({
|
||||
const Listing = createReactClass({ // eslint-disable-line react/prefer-es6-class
|
||||
displayName: 'Listing',
|
||||
|
||||
/* eslint-disable react/require-default-props */
|
||||
propTypes: {
|
||||
limit: PropTypes.number,
|
||||
sort_by: PropTypes.string,
|
||||
sort_order: PropTypes.string,
|
||||
params: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
auto_refresh: PropTypes.bool,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}),
|
||||
base_url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
endpoint: PropTypes.string.isRequired,
|
||||
afterGetItems: PropTypes.func,
|
||||
messages: PropTypes.shape({
|
||||
onRestore: PropTypes.func,
|
||||
onTrash: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
}),
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object),
|
||||
bulk_actions: PropTypes.arrayOf(PropTypes.object),
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object),
|
||||
search: PropTypes.bool,
|
||||
groups: PropTypes.bool,
|
||||
renderExtraActions: PropTypes.func,
|
||||
onBeforeSelectFilter: PropTypes.func,
|
||||
getListingItemKey: PropTypes.func,
|
||||
},
|
||||
/* eslint-enable react/require-default-props */
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: () => ({
|
||||
limit: 10,
|
||||
sort_by: null,
|
||||
sort_order: undefined,
|
||||
auto_refresh: false,
|
||||
location: undefined,
|
||||
base_url: '',
|
||||
type: undefined,
|
||||
afterGetItems: undefined,
|
||||
messages: undefined,
|
||||
columns: [],
|
||||
bulk_actions: [],
|
||||
item_actions: [],
|
||||
search: false,
|
||||
groups: false,
|
||||
renderExtraActions: undefined,
|
||||
onBeforeSelectFilter: undefined,
|
||||
getListingItemKey: undefined,
|
||||
}),
|
||||
|
||||
getInitialState: function getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -327,64 +91,25 @@ const Listing = createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
getParam: function getParam(param) {
|
||||
const regex = /(.*)\[(.*)\]/;
|
||||
const matches = regex.exec(param);
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.isComponentMounted = true;
|
||||
const params = this.props.params || {};
|
||||
this.initWithParams(params);
|
||||
|
||||
initWithParams: function initWithParams(params) {
|
||||
const state = this.getInitialState();
|
||||
// check for url params
|
||||
if (params.splat) {
|
||||
params.splat.split('/').forEach((param) => {
|
||||
const [key, value] = this.getParam(param);
|
||||
const filters = {};
|
||||
switch (key) {
|
||||
case 'filter':
|
||||
value.split('&').forEach((pair) => {
|
||||
const [k, v] = pair.split('=');
|
||||
filters[k] = v;
|
||||
});
|
||||
|
||||
state.filter = filters;
|
||||
break;
|
||||
default:
|
||||
state[key] = value;
|
||||
}
|
||||
if (this.props.auto_refresh) {
|
||||
jQuery(document).on('heartbeat-tick.mailpoet', () => {
|
||||
this.getItems();
|
||||
});
|
||||
}
|
||||
|
||||
// limit per page
|
||||
if (this.props.limit !== undefined) {
|
||||
state.limit = Math.abs(Number(this.props.limit));
|
||||
}
|
||||
|
||||
// sort by
|
||||
if (state.sort_by === null && this.props.sort_by !== undefined) {
|
||||
state.sort_by = this.props.sort_by;
|
||||
}
|
||||
|
||||
// sort order
|
||||
if (state.sort_order === null && this.props.sort_order !== undefined) {
|
||||
state.sort_order = this.props.sort_order;
|
||||
}
|
||||
|
||||
this.setState(state, () => {
|
||||
this.getItems();
|
||||
});
|
||||
},
|
||||
|
||||
getParams: function getParams() {
|
||||
// get all route parameters (without the "splat")
|
||||
const params = _.omit(this.props.params, 'splat');
|
||||
// TODO:
|
||||
// find a way to set the "type" in the routes definition
|
||||
// so that it appears in `this.props.params`
|
||||
if (this.props.type) {
|
||||
params.type = this.props.type;
|
||||
}
|
||||
return params;
|
||||
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
|
||||
const params = nextProps.params || {};
|
||||
this.initWithParams(params);
|
||||
},
|
||||
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
this.isComponentMounted = false;
|
||||
},
|
||||
|
||||
setParams: function setParams() {
|
||||
@@ -451,25 +176,22 @@ const Listing = createReactClass({
|
||||
return ret;
|
||||
},
|
||||
|
||||
componentDidMount: function componentDidMount() {
|
||||
this.isComponentMounted = true;
|
||||
const params = this.props.params || {};
|
||||
this.initWithParams(params);
|
||||
|
||||
if (this.props.auto_refresh) {
|
||||
jQuery(document).on('heartbeat-tick.mailpoet', () => {
|
||||
this.getItems();
|
||||
});
|
||||
getParams: function getParams() {
|
||||
// get all route parameters (without the "splat")
|
||||
const params = _.omit(this.props.params, 'splat');
|
||||
// TODO:
|
||||
// find a way to set the "type" in the routes definition
|
||||
// so that it appears in `this.props.params`
|
||||
if (this.props.type) {
|
||||
params.type = this.props.type;
|
||||
}
|
||||
return params;
|
||||
},
|
||||
|
||||
componentWillUnmount: function componentWillUnmount() {
|
||||
this.isComponentMounted = false;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
|
||||
const params = nextProps.params || {};
|
||||
this.initWithParams(params);
|
||||
getParam: function getParam(param) {
|
||||
const regex = /(.*)\[(.*)\]/;
|
||||
const matches = regex.exec(param);
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
getItems: function getItems() {
|
||||
@@ -525,6 +247,48 @@ const Listing = createReactClass({
|
||||
});
|
||||
},
|
||||
|
||||
initWithParams: function initWithParams(params) {
|
||||
const state = this.getInitialState();
|
||||
// check for url params
|
||||
if (params.splat) {
|
||||
params.splat.split('/').forEach((param) => {
|
||||
const [key, value] = this.getParam(param);
|
||||
const filters = {};
|
||||
switch (key) {
|
||||
case 'filter':
|
||||
value.split('&').forEach((pair) => {
|
||||
const [k, v] = pair.split('=');
|
||||
filters[k] = v;
|
||||
});
|
||||
|
||||
state.filter = filters;
|
||||
break;
|
||||
default:
|
||||
state[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// limit per page
|
||||
if (this.props.limit !== undefined) {
|
||||
state.limit = Math.abs(Number(this.props.limit));
|
||||
}
|
||||
|
||||
// sort by
|
||||
if (state.sort_by === null && this.props.sort_by !== undefined) {
|
||||
state.sort_by = this.props.sort_by;
|
||||
}
|
||||
|
||||
// sort order
|
||||
if (state.sort_order === null && this.props.sort_order !== undefined) {
|
||||
state.sort_order = this.props.sort_order;
|
||||
}
|
||||
|
||||
this.setState(state, () => {
|
||||
this.getItems();
|
||||
});
|
||||
},
|
||||
|
||||
handleRestoreItem: function handleRestoreItem(id) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
68
assets/js/src/listing/listing_column.jsx
Normal file
68
assets/js/src/listing/listing_column.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingColumn extends React.Component {
|
||||
handleSort = () => {
|
||||
const sortBy = this.props.column.name;
|
||||
const sortOrder = (this.props.column.sorted === 'asc') ? 'desc' : 'asc';
|
||||
this.props.onSort(sortBy, sortOrder);
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = classNames(
|
||||
'manage-column',
|
||||
{ 'column-primary': this.props.column.is_primary },
|
||||
{ sortable: this.props.column.sortable },
|
||||
this.props.column.sorted,
|
||||
{ sorted: (this.props.sort_by === this.props.column.name) }
|
||||
);
|
||||
let label;
|
||||
|
||||
if (this.props.column.sortable === true) {
|
||||
label = (
|
||||
<a
|
||||
onClick={this.handleSort}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span>{ this.props.column.label }</span>
|
||||
<span className="sorting-indicator" />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
label = this.props.column.label;
|
||||
}
|
||||
return (
|
||||
<th
|
||||
role="columnheader"
|
||||
className={classes}
|
||||
id={this.props.column.name}
|
||||
scope="col"
|
||||
width={this.props.column.width || null}
|
||||
>{label}</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingColumn.propTypes = {
|
||||
column: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
sorted: PropTypes.string,
|
||||
is_primary: PropTypes.bool,
|
||||
sortable: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
}).isRequired,
|
||||
sort_by: PropTypes.string,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ListingColumn.defaultProps = {
|
||||
sort_by: undefined,
|
||||
};
|
||||
|
||||
module.exports = ListingColumn;
|
215
assets/js/src/listing/listing_item.jsx
Normal file
215
assets/js/src/listing/listing_item.jsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MailPoet from 'mailpoet';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class ListingItem extends React.Component {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
handleSelectItem = (e) => {
|
||||
this.props.onSelectItem(
|
||||
parseInt(e.target.value, 10),
|
||||
e.target.checked
|
||||
);
|
||||
|
||||
return !e.target.checked;
|
||||
};
|
||||
|
||||
handleRestoreItem = (id) => {
|
||||
this.props.onRestoreItem(id);
|
||||
};
|
||||
|
||||
handleTrashItem = (id) => {
|
||||
this.props.onTrashItem(id);
|
||||
};
|
||||
|
||||
handleDeleteItem = (id) => {
|
||||
this.props.onDeleteItem(id);
|
||||
};
|
||||
|
||||
handleToggleItem = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
};
|
||||
|
||||
render() {
|
||||
let checkbox = false;
|
||||
|
||||
if (this.props.is_selectable === true) {
|
||||
checkbox = (
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text" htmlFor={`listing-row-checkbox-${this.props.item.id}`}>{
|
||||
`Select ${this.props.item[this.props.columns[0].name]}`
|
||||
}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={this.props.item.id}
|
||||
checked={
|
||||
this.props.item.selected || this.props.selection === 'all'
|
||||
}
|
||||
onChange={this.handleSelectItem}
|
||||
disabled={this.props.selection === 'all'}
|
||||
id={`listing-row-checkbox-${this.props.item.id}`}
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
const customActions = this.props.item_actions;
|
||||
let itemActions = false;
|
||||
|
||||
if (customActions.length > 0) {
|
||||
let isFirst = true;
|
||||
itemActions = customActions
|
||||
.filter(action => action.display === undefined || action.display(this.props.item))
|
||||
.map((action, index) => {
|
||||
let customAction = null;
|
||||
|
||||
if (action.name === 'trash') {
|
||||
customAction = (
|
||||
<span key={`action-${action.name}`} className="trash">
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleTrashItem(this.props.item.id)}
|
||||
>
|
||||
{MailPoet.I18n.t('moveToTrash')}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
} else if (action.refresh) {
|
||||
customAction = (
|
||||
<span
|
||||
onClick={this.props.onRefreshItems}
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
role="button"
|
||||
tabIndex={index}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else if (action.link) {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
{ action.link(this.props.item) }
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
customAction = (
|
||||
<span
|
||||
key={`action-${action.name}`}
|
||||
className={action.name}
|
||||
>
|
||||
{(!isFirst) ? ' | ' : ''}
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={
|
||||
(action.onClick !== undefined)
|
||||
? () => action.onClick(this.props.item, this.props.onRefreshItems)
|
||||
: false
|
||||
}
|
||||
>{ action.label }</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (customAction !== null && isFirst === true) {
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return customAction;
|
||||
});
|
||||
} else {
|
||||
itemActions = (
|
||||
<span className="edit">
|
||||
<Link to={`/edit/${this.props.item.id}`}>{MailPoet.I18n.t('edit')}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let actions;
|
||||
|
||||
if (this.props.group === 'trash') {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
<span>
|
||||
<a
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleRestoreItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('restore')}</a>
|
||||
</span>
|
||||
{ ' | ' }
|
||||
<span className="delete">
|
||||
<a
|
||||
className="submitdelete"
|
||||
href="javascript:;"
|
||||
onClick={() => this.handleDeleteItem(this.props.item.id)}
|
||||
>{MailPoet.I18n.t('deletePermanently')}</a>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
actions = (
|
||||
<div>
|
||||
<div className="row-actions">
|
||||
{ itemActions }
|
||||
</div>
|
||||
<button
|
||||
onClick={() => this.handleToggleItem(this.props.item.id)}
|
||||
className="toggle-row"
|
||||
type="button"
|
||||
>
|
||||
<span className="screen-reader-text">{MailPoet.I18n.t('showMoreDetails')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rowClasses = classNames({ 'is-expanded': this.state.expanded });
|
||||
|
||||
return (
|
||||
<tr className={rowClasses} data-automation-id={`listing_item_${this.props.item.id}`}>
|
||||
{ checkbox }
|
||||
{ this.props.onRenderItem(this.props.item, actions) }
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingItem.propTypes = {
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
onRestoreItem: PropTypes.func.isRequired,
|
||||
onTrashItem: PropTypes.func.isRequired,
|
||||
onDeleteItem: PropTypes.func.isRequired,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onRefreshItems: PropTypes.func.isRequired,
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
module.exports = ListingItem;
|
140
assets/js/src/listing/listing_items.jsx
Normal file
140
assets/js/src/listing/listing_items.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import ListingItem from 'listing/listing_item.jsx';
|
||||
|
||||
class ListingItems extends React.Component { // eslint-disable-line react/prefer-stateless-function, max-len
|
||||
render() {
|
||||
if (this.props.items.length === 0) {
|
||||
let message;
|
||||
if (this.props.loading === true) {
|
||||
message = (this.props.messages.onLoadingItems
|
||||
&& this.props.messages.onLoadingItems(this.props.group))
|
||||
|| MailPoet.I18n.t('loadingItems');
|
||||
} else {
|
||||
message = (this.props.messages.onNoItemsFound
|
||||
&& this.props.messages.onNoItemsFound(this.props.group))
|
||||
|| MailPoet.I18n.t('noItemsFound');
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className="no-items">
|
||||
<td
|
||||
colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
className="colspanchange"
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
const selectAllClasses = classNames(
|
||||
'mailpoet_select_all',
|
||||
{ mailpoet_hidden: (
|
||||
this.props.selection === false
|
||||
|| (this.props.count <= this.props.limit)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<tr className={selectAllClasses}>
|
||||
<td colSpan={
|
||||
this.props.columns.length
|
||||
+ (this.props.is_selectable ? 1 : 0)
|
||||
}
|
||||
>
|
||||
{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLabel')
|
||||
: MailPoet.I18n.t('selectedAllLabel').replace(
|
||||
'%d',
|
||||
this.props.count.toLocaleString()
|
||||
)
|
||||
}
|
||||
|
||||
<a
|
||||
onClick={this.props.onSelectAll}
|
||||
href="javascript:;"
|
||||
>{
|
||||
(this.props.selection !== 'all')
|
||||
? MailPoet.I18n.t('selectAllLink')
|
||||
: MailPoet.I18n.t('clearSelection')
|
||||
}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.props.items.map((item) => {
|
||||
const renderItem = item;
|
||||
renderItem.id = parseInt(item.id, 10);
|
||||
renderItem.selected = (this.props.selected_ids.indexOf(renderItem.id) !== -1);
|
||||
let key = `item-${renderItem.id}-${item.id}`;
|
||||
if (typeof this.props.getListingItemKey === 'function') {
|
||||
key = this.props.getListingItemKey(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListingItem
|
||||
columns={this.props.columns}
|
||||
onSelectItem={this.props.onSelectItem}
|
||||
onRenderItem={this.props.onRenderItem}
|
||||
onDeleteItem={this.props.onDeleteItem}
|
||||
onRestoreItem={this.props.onRestoreItem}
|
||||
onTrashItem={this.props.onTrashItem}
|
||||
onRefreshItems={this.props.onRefreshItems}
|
||||
selection={this.props.selection}
|
||||
is_selectable={this.props.is_selectable}
|
||||
item_actions={this.props.item_actions}
|
||||
group={this.props.group}
|
||||
key={key}
|
||||
item={renderItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListingItems.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
messages: PropTypes.shape({
|
||||
onLoadingItems: PropTypes.func,
|
||||
onNoItemsFound: PropTypes.func,
|
||||
}).isRequired,
|
||||
group: PropTypes.string.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
is_selectable: PropTypes.bool.isRequired,
|
||||
selection: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
onSelectAll: PropTypes.func.isRequired,
|
||||
selected_ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
getListingItemKey: PropTypes.func,
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
onRenderItem: PropTypes.func.isRequired,
|
||||
onDeleteItem: PropTypes.func.isRequired,
|
||||
onRestoreItem: PropTypes.func.isRequired,
|
||||
onTrashItem: PropTypes.func.isRequired,
|
||||
onRefreshItems: PropTypes.func.isRequired,
|
||||
item_actions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
};
|
||||
|
||||
ListingItems.defaultProps = {
|
||||
getListingItemKey: undefined,
|
||||
};
|
||||
|
||||
module.exports = ListingItems;
|
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ListingPages extends React.Component {
|
||||
state = {
|
||||
@@ -35,9 +36,7 @@ class ListingPages extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
constrainPage = (page) => {
|
||||
return Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
|
||||
};
|
||||
getLastPage = () => Math.ceil(this.props.count / this.props.limit);
|
||||
|
||||
handleSetManualPage = (e) => {
|
||||
if (e.which === 13) {
|
||||
@@ -55,9 +54,7 @@ class ListingPages extends React.Component {
|
||||
this.setPage(e.target.value);
|
||||
};
|
||||
|
||||
getLastPage = () => {
|
||||
return Math.ceil(this.props.count / this.props.limit);
|
||||
};
|
||||
constrainPage = page => Math.min(Math.max(1, Math.abs(Number(page))), this.getLastPage());
|
||||
|
||||
render() {
|
||||
if (this.props.count === 0) {
|
||||
@@ -193,4 +190,14 @@ class ListingPages extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ListingPages.propTypes = {
|
||||
onSetPage: PropTypes.func.isRequired,
|
||||
page: PropTypes.oneOfType([
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
]).isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
module.exports = ListingPages;
|
||||
|
Reference in New Issue
Block a user