diff --git a/assets/js/src/form/fields/field.jsx b/assets/js/src/form/fields/field.jsx index 5d7c1773c7..a1117d08ab 100644 --- a/assets/js/src/form/fields/field.jsx +++ b/assets/js/src/form/fields/field.jsx @@ -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, diff --git a/assets/js/src/form/form.jsx b/assets/js/src/form/form.jsx index 01043ebcf7..856978f560 100644 --- a/assets/js/src/form/form.jsx +++ b/assets/js/src/form/form.jsx @@ -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, diff --git a/assets/js/src/listing/bulk_actions.jsx b/assets/js/src/listing/bulk_actions.jsx index 6687e77350..a23c07a6d1 100644 --- a/assets/js/src/listing/bulk_actions.jsx +++ b/assets/js/src/listing/bulk_actions.jsx @@ -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; diff --git a/assets/js/src/listing/filters.jsx b/assets/js/src/listing/filters.jsx index d7e4bc2623..aeeea368c3 100644 --- a/assets/js/src/listing/filters.jsx +++ b/assets/js/src/listing/filters.jsx @@ -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; diff --git a/assets/js/src/listing/header.jsx b/assets/js/src/listing/header.jsx index 2da2a5f28e..4acb03de28 100644 --- a/assets/js/src/listing/header.jsx +++ b/assets/js/src/listing/header.jsx @@ -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 = ( - - { this.props.column.label } - - - ); - } else { - label = this.props.column.label; - } - return ( - {label} - ); - } -} +ListingHeader.defaultProps = { + columns: [], + sort_by: undefined, + sort_order: 'desc', +}; module.exports = ListingHeader; diff --git a/assets/js/src/listing/listing.jsx b/assets/js/src/listing/listing.jsx index c757f27267..36210a1831 100644 --- a/assets/js/src/listing/listing.jsx +++ b/assets/js/src/listing/listing.jsx @@ -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 = ( - - - - - ); - } - - 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 = ( - - {(!isFirst) ? ' | ' : ''} - this.handleTrashItem(this.props.item.id)} - > - {MailPoet.I18n.t('moveToTrash')} - - - ); - } else if (action.refresh) { - customAction = ( - - {(!isFirst) ? ' | ' : ''} - { action.link(this.props.item) } - - ); - } else if (action.link) { - customAction = ( - - {(!isFirst) ? ' | ' : ''} - { action.link(this.props.item) } - - ); - } else { - customAction = ( - - {(!isFirst) ? ' | ' : ''} - action.onClick(this.props.item, this.props.onRefreshItems) - : false - } - >{ action.label } - - ); - } - - if (customAction !== null && isFirst === true) { - isFirst = false; - } - - return customAction; - }); - } else { - itemActions = ( - - {MailPoet.I18n.t('edit')} - - ); - } - - let actions; - - if (this.props.group === 'trash') { - actions = ( -
-
- - this.handleRestoreItem(this.props.item.id)} - >{MailPoet.I18n.t('restore')} - - { ' | ' } - - this.handleDeleteItem(this.props.item.id)} - >{MailPoet.I18n.t('deletePermanently')} - -
- -
- ); - } else { - actions = ( -
-
- { itemActions } -
- -
- ); - } - - const rowClasses = classNames({ 'is-expanded': this.state.expanded }); - - return ( - - { checkbox } - { this.props.onRenderItem(this.props.item, actions) } - - ); - } -} - -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 ( - - - - {message} - - - - ); - } - const selectAllClasses = classNames( - 'mailpoet_select_all', - { mailpoet_hidden: ( - this.props.selection === false - || (this.props.count <= this.props.limit) - ), - } - ); - - return ( - - - - { - (this.props.selection !== 'all') - ? MailPoet.I18n.t('selectAllLabel') - : MailPoet.I18n.t('selectedAllLabel').replace( - '%d', - this.props.count.toLocaleString() - ) - } -   - { - (this.props.selection !== 'all') - ? MailPoet.I18n.t('selectAllLink') - : MailPoet.I18n.t('clearSelection') - } - - - - {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 ( - - ); - })} - - ); - } -} - -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, diff --git a/assets/js/src/listing/listing_column.jsx b/assets/js/src/listing/listing_column.jsx new file mode 100644 index 0000000000..cfe86a994e --- /dev/null +++ b/assets/js/src/listing/listing_column.jsx @@ -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 = ( + + { this.props.column.label } + + + ); + } else { + label = this.props.column.label; + } + return ( + {label} + ); + } +} + +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; diff --git a/assets/js/src/listing/listing_item.jsx b/assets/js/src/listing/listing_item.jsx new file mode 100644 index 0000000000..c1d479d4d4 --- /dev/null +++ b/assets/js/src/listing/listing_item.jsx @@ -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 = ( + + + + + ); + } + + 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 = ( + + {(!isFirst) ? ' | ' : ''} + this.handleTrashItem(this.props.item.id)} + > + {MailPoet.I18n.t('moveToTrash')} + + + ); + } else if (action.refresh) { + customAction = ( + + {(!isFirst) ? ' | ' : ''} + { action.link(this.props.item) } + + ); + } else if (action.link) { + customAction = ( + + {(!isFirst) ? ' | ' : ''} + { action.link(this.props.item) } + + ); + } else { + customAction = ( + + {(!isFirst) ? ' | ' : ''} + action.onClick(this.props.item, this.props.onRefreshItems) + : false + } + >{ action.label } + + ); + } + + if (customAction !== null && isFirst === true) { + isFirst = false; + } + + return customAction; + }); + } else { + itemActions = ( + + {MailPoet.I18n.t('edit')} + + ); + } + + let actions; + + if (this.props.group === 'trash') { + actions = ( +
+
+ + this.handleRestoreItem(this.props.item.id)} + >{MailPoet.I18n.t('restore')} + + { ' | ' } + + this.handleDeleteItem(this.props.item.id)} + >{MailPoet.I18n.t('deletePermanently')} + +
+ +
+ ); + } else { + actions = ( +
+
+ { itemActions } +
+ +
+ ); + } + + const rowClasses = classNames({ 'is-expanded': this.state.expanded }); + + return ( + + { checkbox } + { this.props.onRenderItem(this.props.item, actions) } + + ); + } +} + +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; diff --git a/assets/js/src/listing/listing_items.jsx b/assets/js/src/listing/listing_items.jsx new file mode 100644 index 0000000000..ad95b51ce1 --- /dev/null +++ b/assets/js/src/listing/listing_items.jsx @@ -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 ( + + + + {message} + + + + ); + } + const selectAllClasses = classNames( + 'mailpoet_select_all', + { mailpoet_hidden: ( + this.props.selection === false + || (this.props.count <= this.props.limit) + ), + } + ); + + return ( + + + + { + (this.props.selection !== 'all') + ? MailPoet.I18n.t('selectAllLabel') + : MailPoet.I18n.t('selectedAllLabel').replace( + '%d', + this.props.count.toLocaleString() + ) + } +   + { + (this.props.selection !== 'all') + ? MailPoet.I18n.t('selectAllLink') + : MailPoet.I18n.t('clearSelection') + } + + + + {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 ( + + ); + })} + + ); + } +} + +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; diff --git a/assets/js/src/listing/pages.jsx b/assets/js/src/listing/pages.jsx index ece96ebbc5..ccfdf29216 100644 --- a/assets/js/src/listing/pages.jsx +++ b/assets/js/src/listing/pages.jsx @@ -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;