UNPKG

keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

558 lines (521 loc) 15 kB
/** * The list view is a paginated table of all items in the list. It can show a * variety of information about the individual items in columns. */ import React from 'react'; // import { findDOMNode } from 'react-dom'; // TODO re-implement focus when ready import numeral from 'numeral'; import { connect } from 'react-redux'; import { BlankState, Center, Container, Glyph, GlyphButton, Pagination, Spinner, } from '../../elemental'; import ListFilters from './components/Filtering/ListFilters'; import ListHeaderTitle from './components/ListHeaderTitle'; import ListHeaderToolbar from './components/ListHeaderToolbar'; import ListManagement from './components/ListManagement'; import ConfirmationDialog from '../../shared/ConfirmationDialog'; import CreateForm from '../../shared/CreateForm'; import FlashMessages from '../../shared/FlashMessages'; import ItemsTable from './components/ItemsTable/ItemsTable'; import UpdateForm from './components/UpdateForm'; import { plural as pluralize } from '../../../utils/string'; import { listsByPath } from '../../../utils/lists'; import { checkForQueryChange } from '../../../utils/queryParams'; import { deleteItems, setActiveSearch, setActiveSort, setCurrentPage, selectList, clearCachedQuery, } from './actions'; import { deleteItem, } from '../Item/actions'; const ESC_KEY_CODE = 27; const ListView = React.createClass({ contextTypes: { router: React.PropTypes.object.isRequired, }, getInitialState () { return { confirmationDialog: { isOpen: false, }, checkedItems: {}, constrainTableWidth: true, manageMode: false, showCreateForm: false, showUpdateForm: false, }; }, componentWillMount () { // When we directly navigate to a list without coming from another client // side routed page before, we need to initialize the list and parse // possibly specified query parameters this.props.dispatch(selectList(this.props.params.listId)); const isNoCreate = this.props.lists.data[this.props.params.listId].nocreate; const shouldOpenCreate = this.props.location.search === '?create'; this.setState({ showCreateForm: (shouldOpenCreate && !isNoCreate) || Keystone.createFormErrors, }); }, componentWillReceiveProps (nextProps) { // We've opened a new list from the client side routing, so initialize // again with the new list id const isReady = this.props.lists.ready && nextProps.lists.ready; if (isReady && checkForQueryChange(nextProps, this.props)) { this.props.dispatch(selectList(nextProps.params.listId)); } }, componentWillUnmount () { this.props.dispatch(clearCachedQuery()); }, // ============================== // HEADER // ============================== // Called when a new item is created onCreate (item) { // Hide the create form this.toggleCreateModal(false); // Redirect to newly created item path const list = this.props.currentList; this.context.router.push(`${Keystone.adminPath}/${list.path}/${item.id}`); }, createAutocreate () { const list = this.props.currentList; list.createItem(null, (err, data) => { if (err) { // TODO Proper error handling alert('Something went wrong, please try again!'); console.log(err); } else { this.context.router.push(`${Keystone.adminPath}/${list.path}/${data.id}`); } }); }, updateSearch (e) { this.props.dispatch(setActiveSearch(e.target.value)); }, handleSearchClear () { this.props.dispatch(setActiveSearch('')); // TODO re-implement focus when ready // findDOMNode(this.refs.listSearchInput).focus(); }, handleSearchKey (e) { // clear on esc if (e.which === ESC_KEY_CODE) { this.handleSearchClear(); } }, handlePageSelect (i) { // If the current page index is the same as the index we are intending to pass to redux, bail out. if (i === this.props.lists.page.index) return; return this.props.dispatch(setCurrentPage(i)); }, toggleManageMode (filter = !this.state.manageMode) { this.setState({ manageMode: filter, checkedItems: {}, }); }, toggleUpdateModal (filter = !this.state.showUpdateForm) { this.setState({ showUpdateForm: filter, }); }, massUpdate () { // TODO: Implement update multi-item console.log('Update ALL the things!'); }, massDelete () { const { checkedItems } = this.state; const list = this.props.currentList; const itemCount = pluralize(checkedItems, ('* ' + list.singular.toLowerCase()), ('* ' + list.plural.toLowerCase())); const itemIds = Object.keys(checkedItems); this.setState({ confirmationDialog: { isOpen: true, label: 'Delete', body: ( <div> Are you sure you want to delete {itemCount}? <br /> <br /> This cannot be undone. </div> ), onConfirmation: () => { this.props.dispatch(deleteItems(itemIds)); this.toggleManageMode(); this.removeConfirmationDialog(); }, }, }); }, handleManagementSelect (selection) { if (selection === 'all') this.checkAllItems(); if (selection === 'none') this.uncheckAllTableItems(); if (selection === 'visible') this.checkAllTableItems(); return false; }, renderConfirmationDialog () { const props = this.state.confirmationDialog; return ( <ConfirmationDialog confirmationLabel={props.label} isOpen={props.isOpen} onCancel={this.removeConfirmationDialog} onConfirmation={props.onConfirmation} > {props.body} </ConfirmationDialog> ); }, renderManagement () { const { checkedItems, manageMode, selectAllItemsLoading } = this.state; const { currentList } = this.props; return ( <ListManagement checkedItemCount={Object.keys(checkedItems).length} handleDelete={this.massDelete} handleSelect={this.handleManagementSelect} handleToggle={() => this.toggleManageMode(!manageMode)} isOpen={manageMode} itemCount={this.props.items.count} itemsPerPage={this.props.lists.page.size} nodelete={currentList.nodelete} noedit={currentList.noedit} selectAllItemsLoading={selectAllItemsLoading} /> ); }, renderPagination () { const items = this.props.items; if (this.state.manageMode || !items.count) return; const list = this.props.currentList; const currentPage = this.props.lists.page.index; const pageSize = this.props.lists.page.size; return ( <Pagination currentPage={currentPage} onPageSelect={this.handlePageSelect} pageSize={pageSize} plural={list.plural} singular={list.singular} style={{ marginBottom: 0 }} total={items.count} limit={10} /> ); }, renderHeader () { const items = this.props.items; const { autocreate, nocreate, plural, singular } = this.props.currentList; return ( <Container style={{ paddingTop: '2em' }}> <ListHeaderTitle activeSort={this.props.active.sort} availableColumns={this.props.currentList.columns} handleSortSelect={this.handleSortSelect} title={` ${numeral(items.count).format()} ${pluralize(items.count, ' ' + singular, ' ' + plural)} `} /> <ListHeaderToolbar // common dispatch={this.props.dispatch} list={listsByPath[this.props.params.listId]} // expand expandIsActive={!this.state.constrainTableWidth} expandOnClick={this.toggleTableWidth} // create createIsAvailable={!nocreate} createListName={singular} createOnClick={autocreate ? this.createAutocreate : this.openCreateModal} // search searchHandleChange={this.updateSearch} searchHandleClear={this.handleSearchClear} searchHandleKeyup={this.handleSearchKey} searchValue={this.props.active.search} // filters filtersActive={this.props.active.filters} filtersAvailable={this.props.currentList.columns.filter((col) => ( col.field && col.field.hasFilterMethod) || col.type === 'heading' )} // columns columnsActive={this.props.active.columns} columnsAvailable={this.props.currentList.columns} /> <ListFilters dispatch={this.props.dispatch} filters={this.props.active.filters} /> </Container> ); }, // ============================== // TABLE // ============================== checkTableItem (item, e) { e.preventDefault(); const newCheckedItems = { ...this.state.checkedItems }; const itemId = item.id; if (this.state.checkedItems[itemId]) { delete newCheckedItems[itemId]; } else { newCheckedItems[itemId] = true; } this.setState({ checkedItems: newCheckedItems, }); }, checkAllTableItems () { const checkedItems = {}; this.props.items.results.forEach(item => { checkedItems[item.id] = true; }); this.setState({ checkedItems: checkedItems, }); }, checkAllItems () { const checkedItems = { ...this.state.checkedItems }; // Just in case this API call takes a long time, we'll update the select all button with // a spinner. this.setState({ selectAllItemsLoading: true }); var self = this; this.props.currentList.loadItems({ expandRelationshipFilters: false, filters: {} }, function (err, data) { data.results.forEach(item => { checkedItems[item.id] = true; }); self.setState({ checkedItems: checkedItems, selectAllItemsLoading: false, }); }); }, uncheckAllTableItems () { this.setState({ checkedItems: {}, }); }, deleteTableItem (item, e) { if (e.altKey) { this.props.dispatch(deleteItem(item.id)); return; } e.preventDefault(); this.setState({ confirmationDialog: { isOpen: true, label: 'Delete', body: ( <div> Are you sure you want to delete <strong>{item.name}</strong>? <br /> <br /> This cannot be undone. </div> ), onConfirmation: () => { this.props.dispatch(deleteItem(item.id)); this.removeConfirmationDialog(); }, }, }); }, removeConfirmationDialog () { this.setState({ confirmationDialog: { isOpen: false, }, }); }, toggleTableWidth () { this.setState({ constrainTableWidth: !this.state.constrainTableWidth, }); }, // ============================== // COMMON // ============================== handleSortSelect (path, inverted) { if (inverted) path = '-' + path; this.props.dispatch(setActiveSort(path)); }, toggleCreateModal (visible) { this.setState({ showCreateForm: visible, }); }, openCreateModal () { this.toggleCreateModal(true); }, closeCreateModal () { this.toggleCreateModal(false); }, showBlankState () { return !this.props.loading && !this.props.items.results.length && !this.props.active.search && !this.props.active.filters.length; }, renderBlankState () { const { currentList } = this.props; if (!this.showBlankState()) return null; // create and nav directly to the item view, or open the create modal const onClick = currentList.autocreate ? this.createAutocreate : this.openCreateModal; // display the button if create allowed const button = !currentList.nocreate ? ( <GlyphButton color="success" glyph="plus" position="left" onClick={onClick} data-e2e-list-create-button="no-results"> Create {currentList.singular} </GlyphButton> ) : null; return ( <Container> {(this.props.error) ? ( <FlashMessages messages={{ error: [{ title: "There is a problem with the network, we're trying to reconnect...", }] }} /> ) : null} <BlankState heading={`No ${this.props.currentList.plural.toLowerCase()} found...`} style={{ marginTop: 40 }}> {button} </BlankState> </Container> ); }, renderActiveState () { if (this.showBlankState()) return null; const containerStyle = { transition: 'max-width 160ms ease-out', msTransition: 'max-width 160ms ease-out', MozTransition: 'max-width 160ms ease-out', WebkitTransition: 'max-width 160ms ease-out', }; if (!this.state.constrainTableWidth) { containerStyle.maxWidth = '100%'; } return ( <div> {this.renderHeader()} <Container> <div style={{ height: 35, marginBottom: '1em', marginTop: '1em' }}> {this.renderManagement()} {this.renderPagination()} <span style={{ clear: 'both', display: 'table' }} /> </div> </Container> <Container style={containerStyle}> {(this.props.error) ? ( <FlashMessages messages={{ error: [{ title: "There is a problem with the network, we're trying to reconnect..", }] }} /> ) : null} {(this.props.loading) ? ( <Center height="50vh"> <Spinner /> </Center> ) : ( <div> <ItemsTable activeSort={this.props.active.sort} checkedItems={this.state.checkedItems} checkTableItem={this.checkTableItem} columns={this.props.active.columns} deleteTableItem={this.deleteTableItem} handleSortSelect={this.handleSortSelect} items={this.props.items} list={this.props.currentList} manageMode={this.state.manageMode} rowAlert={this.props.rowAlert} currentPage={this.props.lists.page.index} pageSize={this.props.lists.page.size} drag={this.props.lists.drag} dispatch={this.props.dispatch} /> {this.renderNoSearchResults()} </div> )} </Container> </div> ); }, renderNoSearchResults () { if (this.props.items.results.length) return null; let matching = this.props.active.search; if (this.props.active.filters.length) { matching += (matching ? ' and ' : '') + pluralize(this.props.active.filters.length, '* filter', '* filters'); } matching = matching ? ' found matching ' + matching : '.'; return ( <BlankState style={{ marginTop: 20, marginBottom: 20 }}> <Glyph name="search" size="medium" style={{ marginBottom: 20 }} /> <h2 style={{ color: 'inherit' }}> No {this.props.currentList.plural.toLowerCase()}{matching} </h2> </BlankState> ); }, render () { if (!this.props.ready) { return ( <Center height="50vh" data-screen-id="list"> <Spinner /> </Center> ); } return ( <div data-screen-id="list"> {this.renderBlankState()} {this.renderActiveState()} <CreateForm err={Keystone.createFormErrors} isOpen={this.state.showCreateForm} list={this.props.currentList} onCancel={this.closeCreateModal} onCreate={this.onCreate} /> <UpdateForm isOpen={this.state.showUpdateForm} itemIds={Object.keys(this.state.checkedItems)} list={this.props.currentList} onCancel={() => this.toggleUpdateModal(false)} /> {this.renderConfirmationDialog()} </div> ); }, }); module.exports = connect((state) => { return { lists: state.lists, loading: state.lists.loading, error: state.lists.error, currentList: state.lists.currentList, items: state.lists.items, page: state.lists.page, ready: state.lists.ready, rowAlert: state.lists.rowAlert, active: state.active, }; })(ListView);