From 5f0d784b065acd2060f4dea437f194ca600e8659 Mon Sep 17 00:00:00 2001 From: davidmacp Date: Tue, 12 Mar 2019 16:34:03 +1300 Subject: [PATCH 01/22] fix: update duplicate filter labels --- .../workspace/collections/1.0/cloud/collection.articles.json | 4 ++-- .../api/workspace/collections/1.0/cloud/collection.pages.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/api/workspace/collections/1.0/cloud/collection.articles.json b/test/api/workspace/collections/1.0/cloud/collection.articles.json index e195bab5d..0587cf6bb 100644 --- a/test/api/workspace/collections/1.0/cloud/collection.articles.json +++ b/test/api/workspace/collections/1.0/cloud/collection.articles.json @@ -258,7 +258,7 @@ "metaTitle": { "type": "String", "required": false, - "label": "Title", + "label": "metaTitle", "publish": { "section": "Meta", "placement": "main", @@ -271,7 +271,7 @@ "metaDescription": { "type": "String", "required": false, - "label": "Description", + "label": "metaDescription", "publish": { "section": "Meta", "placement": "main", diff --git a/test/api/workspace/collections/1.0/cloud/collection.pages.json b/test/api/workspace/collections/1.0/cloud/collection.pages.json index 6e000b19d..eac9f0259 100644 --- a/test/api/workspace/collections/1.0/cloud/collection.pages.json +++ b/test/api/workspace/collections/1.0/cloud/collection.pages.json @@ -84,7 +84,7 @@ "metaTitle": { "type": "String", "required": false, - "label": "Title", + "label": "metaTitle", "publish": { "section": "Meta", "placement": "main", @@ -97,7 +97,7 @@ "metaDescription": { "type": "String", "required": false, - "label": "Description", + "label": "metaDescription", "publish": { "section": "Meta", "placement": "main", From a1727861367d7b27edcf3bed9baf081cb24bf6ab Mon Sep 17 00:00:00 2001 From: davidmacp Date: Wed, 13 Mar 2019 14:38:25 +1300 Subject: [PATCH 02/22] Update collection.articles.json --- .../api/workspace/collections/1.0/cloud/collection.articles.json | 1 - 1 file changed, 1 deletion(-) diff --git a/test/api/workspace/collections/1.0/cloud/collection.articles.json b/test/api/workspace/collections/1.0/cloud/collection.articles.json index 0587cf6bb..444f9c74b 100644 --- a/test/api/workspace/collections/1.0/cloud/collection.articles.json +++ b/test/api/workspace/collections/1.0/cloud/collection.articles.json @@ -50,7 +50,6 @@ "publish": { "section": "Details", "placement": "sidebar", - "readonly": true, "display": { "edit": true, "list": true From 0b00c2b909fb96a0b1af64f50a66b44981eff6b8 Mon Sep 17 00:00:00 2001 From: davidmacp Date: Thu, 14 Mar 2019 11:06:00 +1300 Subject: [PATCH 03/22] test: create Add Filter test --- test/functional/bootstrap.js | 57 ++++++++++++++++++- .../articles/04_article_filter_test.js | 26 +++++++++ test/functional/pages/Article.js | 49 +++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 test/functional/features/articles/04_article_filter_test.js diff --git a/test/functional/bootstrap.js b/test/functional/bootstrap.js index 311ff0ca0..9109e1838 100644 --- a/test/functional/bootstrap.js +++ b/test/functional/bootstrap.js @@ -8,7 +8,7 @@ const myTest = new Testbed({ }) class Bootstrap { - run () { + run() { return new Promise(async (resolve, reject) => { // Create authors await myTest.addData({ @@ -99,7 +99,7 @@ class Bootstrap { // Create articles await myTest.addData({ collection: 'articles', - count: 5, + count: 4, database: 'cloud', fields: { author: { @@ -120,6 +120,10 @@ class Bootstrap { collection: 'network-services' } }, + published: { + format: '{{random.boolean}}', + transform: (value) => value === "true" ? true : false + }, 'sub-category': { reference: { collection: 'sub-categories' @@ -137,9 +141,56 @@ class Bootstrap { version: '1.0' }) + // Create filter articles + await myTest.addData({ + cleanup: false, + collection: 'articles', + count: 3, + database: 'cloud', + fields: { + author: { + reference: { + collection: 'team' + } + }, + body: { + format: '{{lorem.paragraph}}' + }, + category: { + reference: { + collection: 'categories' + } + }, + 'network-service': { + reference: { + collection: 'network-services' + } + }, + published: { + format: '{{random.boolean}}', + transform: (value) => value === "true" ? true : false + }, + 'sub-category': { + reference: { + collection: 'sub-categories' + } + }, + title: { + format: '{{random.word(2)}}', + transform: (value) => 'DADI ' + value + }, + 'web-service': { + reference: { + collection: 'web-services' + } + }, + }, + version: '1.0' + }) + return resolve() }) } } -module.exports = Bootstrap +module.exports = Bootstrap \ No newline at end of file diff --git a/test/functional/features/articles/04_article_filter_test.js b/test/functional/features/articles/04_article_filter_test.js new file mode 100644 index 000000000..e1a1f372c --- /dev/null +++ b/test/functional/features/articles/04_article_filter_test.js @@ -0,0 +1,26 @@ +Feature('Articles Page Filter - @smoke') + +BeforeSuite(async (articlePage, loginPage) => { + await articlePage.deleteDocument('This Is A New Article') + await articlePage.deleteDocument('This Article Is Updated') + await articlePage.deleteDocument('Rich Text') + await loginPage.deleteUser('filter') + await loginPage.addUser('filter', '123456', ['collection:cloud_articles', + 'collection:cloud_team', + 'collection:cloud_categories', + 'collection:cloud_sub-categories', + 'collection:cloud_web-services', + 'collection:cloud_network-services' + ]) + await loginPage.createSession('filter', '123456', '/articles') +}) + +AfterSuite(async (I, loginPage) => { + await I.clearCookie('accessToken') + await loginPage.deleteUser('filter') +}) + +Scenario('Test Filter', async (articlePage) => { + await articlePage.validateArticlePage() + await articlePage.filterArticle() +}) \ No newline at end of file diff --git a/test/functional/pages/Article.js b/test/functional/pages/Article.js index 8728eab91..534dc9c64 100644 --- a/test/functional/pages/Article.js +++ b/test/functional/pages/Article.js @@ -102,7 +102,18 @@ module.exports = { 'data-field-name': 'body' }).find('ul').as('Bullet Point Text')), linkField: (locate('input[class*="RichEditor__link-input"]').as('Link Field')), - linkSave: (locate('button[class*="RichEditor__link-control"]').withText('Save').as('Save Link Button')) + linkSave: (locate('button[class*="RichEditor__link-control"]').withText('Save').as('Save Link Button')), + filterButton: (locate('button[class*="DocumentFilters__button"]').as('Filter Button')), + filterForm: (locate('form[class*="DocumentFilters__tooltip"]').as('Add Filter Form')), + filterField: (locate('select[class*="DocumentFilters__tooltip-dropdown-left"]').as('Filter Field')), + filterOperator: (locate('select[class*="DocumentFilters__tooltip-dropdown-right"]').as('Filter Operator')), + filterValue: (locate('input[class*="FieldString__filter-input"]').as('Search Value')), + addFilter: (locate('button[class*="DocumentFilters__tooltip"]').withText('Add filter').as('Add Filter Button')), + updateFilter: (locate('button[class*="DocumentFilters__tooltip"]').withText('Update filter').as('Update Filter Button')), + filterWrapper: (locate('div[class*="DocumentFilters__filter-wrapper"]').as('Filtered Detail')), + titles: (locate('//table/tbody/tr/td[2]').as('Article Titles')), + published: (locate('//table/tbody/tr/td[4]').as('Published?')), + filterClose: (locate('button[class*="DocumentFilters__filter-close"]').as('Filter Close Button')) }, async validateArticlePage() { @@ -258,6 +269,42 @@ module.exports = { I.click(this.locators.articleLink) }, + async filterArticle() { + let originalArticles = await I.grabNumberOfVisibleElements(this.locators.articleRows) + let articlePublished = await I.grabTextFrom(this.locators.published) + let yesPublish = await articlePublished.filter((article) => article === 'Yes') + let noPublish = await articlePublished.filter((article) => article === 'No') + let numberYes = yesPublish.length + let numberNo = noPublish.length + I.click(this.locators.filterButton) + I.seeElement(this.locators.filterForm) + I.seeElement(this.locators.filterField) + I.seeElement(this.locators.filterOperator) + I.fillField(this.locators.filterValue, 'DADI') + I.click(this.locators.addFilter) + I.seeElement(this.locators.filterWrapper) + let articles = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(articles, 3) + let articleTitles = await I.grabTextFrom(this.locators.titles) + I.click(this.locators.filterWrapper) + I.selectOption(this.locators.filterOperator, 'is') + I.fillField(this.locators.filterValue, articleTitles[1]) + I.click(this.locators.updateFilter) + let updatedArticles = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(updatedArticles, 1) + I.click(this.locators.filterClose) + let newTotal = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(originalArticles, newTotal) + I.click(this.locators.filterButton) + I.seeElement(this.locators.filterForm) + I.selectOption(this.locators.filterField, 'Published') + I.seeElement(this.locators.filterOperator) + I.click(this.locators.addFilter) + I.seeElement(this.locators.filterWrapper) + let publishedNo = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(publishedNo, numberNo) + }, + async deleteArticle() { let total = await I.grabTextFrom(this.locators.totalArticles) I.click(this.locators.checkArticle) From bcfb3c47fdfca88d60eca2b635dffd05cd60c147 Mon Sep 17 00:00:00 2001 From: davidmacp Date: Mon, 18 Mar 2019 09:14:28 +1300 Subject: [PATCH 04/22] test: add DateTime Filter test --- test/functional/bootstrap.js | 14 ++++++++++++++ test/functional/pages/Article.js | 31 ++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/test/functional/bootstrap.js b/test/functional/bootstrap.js index 9109e1838..4bc673a49 100644 --- a/test/functional/bootstrap.js +++ b/test/functional/bootstrap.js @@ -124,6 +124,13 @@ class Bootstrap { format: '{{random.boolean}}', transform: (value) => value === "true" ? true : false }, + publishedAt: { + format: '{{date.past}}', + transform: (value) => { + let date = new Date(value) + return date + } + }, 'sub-category': { reference: { collection: 'sub-categories' @@ -170,6 +177,13 @@ class Bootstrap { format: '{{random.boolean}}', transform: (value) => value === "true" ? true : false }, + publishedAt: { + format: '{{date.past}}', + transform: (value) => { + let date = new Date(value) + return date + } + }, 'sub-category': { reference: { collection: 'sub-categories' diff --git a/test/functional/pages/Article.js b/test/functional/pages/Article.js index 534dc9c64..10798d039 100644 --- a/test/functional/pages/Article.js +++ b/test/functional/pages/Article.js @@ -4,6 +4,8 @@ const { assert, expect } = require('chai') +const moment = require('moment') +const _ = require('lodash') let I @@ -112,8 +114,11 @@ module.exports = { updateFilter: (locate('button[class*="DocumentFilters__tooltip"]').withText('Update filter').as('Update Filter Button')), filterWrapper: (locate('div[class*="DocumentFilters__filter-wrapper"]').as('Filtered Detail')), titles: (locate('//table/tbody/tr/td[2]').as('Article Titles')), + dateTime: (locate('//table/tbody/tr/td[3]').as('Date & Time')), published: (locate('//table/tbody/tr/td[4]').as('Published?')), - filterClose: (locate('button[class*="DocumentFilters__filter-close"]').as('Filter Close Button')) + filterClose: (locate('button[class*="DocumentFilters__filter-close"]').as('Filter Close Button')), + filterValueSelect: (locate('select[class*="DropdownNative__dropdown-text-small"]').withText('No').as('Filter Value Select')), + dateTimeValue: (locate('input[class*="FieldDateTime__filter-input"]').as('Date Time Filter Field')) }, async validateArticlePage() { @@ -271,6 +276,12 @@ module.exports = { async filterArticle() { let originalArticles = await I.grabNumberOfVisibleElements(this.locators.articleRows) + let articleDateTimes = await I.grabTextFrom(this.locators.dateTime) + let randomNum = _.random(0,10) + let pastDateFilter = moment(new Date(), 'YYYY/MM/DD').subtract(randomNum, 'months') + pastDateFilter = pastDateFilter.format('YYYY/MM/DD 09:00') + let datesToFilter = await articleDateTimes.filter((datetime) => datetime < pastDateFilter) + let dateFilter = datesToFilter.length let articlePublished = await I.grabTextFrom(this.locators.published) let yesPublish = await articlePublished.filter((article) => article === 'Yes') let noPublish = await articlePublished.filter((article) => article === 'No') @@ -303,6 +314,24 @@ module.exports = { I.seeElement(this.locators.filterWrapper) let publishedNo = await I.grabNumberOfVisibleElements(this.locators.articleRows) I.seeNumbersAreEqual(publishedNo, numberNo) + I.click(this.locators.filterWrapper) + I.selectOption(this.locators.filterValueSelect, 'Yes') + I.click(this.locators.updateFilter) + let publishedYes = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(publishedYes, numberYes) + I.click(this.locators.filterClose) + newTotal = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(originalArticles, newTotal) + I.click(this.locators.filterButton) + I.seeElement(this.locators.filterForm) + I.selectOption(this.locators.filterField, 'Date & Time') + I.seeElement(this.locators.filterOperator) + I.fillField(this.locators.dateTimeValue, pastDateFilter) + I.click(this.locators.filterOperator) + I.click(this.locators.addFilter) + I.seeElement(this.locators.filterWrapper) + let datesBefore = await I.grabNumberOfVisibleElements(this.locators.articleRows) + I.seeNumbersAreEqual(datesBefore, dateFilter) }, async deleteArticle() { From 0a27e6d7ce46b7484375c4f8730fae3ead5e1d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 20 Mar 2019 11:43:10 +0000 Subject: [PATCH 05/22] feat: add filters to reference select view --- frontend/views/ReferenceSelectView/ReferenceSelectView.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx b/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx index 53314943b..d66f84d0d 100644 --- a/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx +++ b/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx @@ -248,6 +248,7 @@ class ReferenceSelectView extends Component { Date: Wed, 20 Mar 2019 16:16:19 +0000 Subject: [PATCH 06/22] fix: fix issue with document selection across pages --- .../DocumentGridList/DocumentGridList.jsx | 7 +- .../DocumentTableList/DocumentTableList.jsx | 16 ++-- frontend/components/Table/Table.jsx | 4 +- .../containers/DocumentList/DocumentList.jsx | 86 +++++++++++-------- .../DocumentListView/DocumentListView.jsx | 31 ++++--- .../views/MediaListView/MediaListView.jsx | 33 ++++--- .../ReferenceSelectView.jsx | 10 +-- 7 files changed, 96 insertions(+), 91 deletions(-) diff --git a/frontend/components/DocumentGridList/DocumentGridList.jsx b/frontend/components/DocumentGridList/DocumentGridList.jsx index ea6949c03..f0a2f25a9 100644 --- a/frontend/components/DocumentGridList/DocumentGridList.jsx +++ b/frontend/components/DocumentGridList/DocumentGridList.jsx @@ -73,7 +73,7 @@ export default class DocumentGridList extends Component { /** * A hash map of the indices of the currently selected documents. */ - selectedDocuments: proptypes.array, + selectedDocuments: proptypes.obj, /** * The maximum number of documents that can be selected. @@ -174,10 +174,9 @@ export default class DocumentGridList extends Component { style={`width: ${100 / numberOfColumns}%`} > {column.map((item, index) => { - let isSelected = selectedDocuments[index] === true - let onSelect = this.handleItemSelect.bind(this, index) + const onSelect = this.handleItemSelect.bind(this, index) - return onRenderCard(item, onSelect, isSelected) + return onRenderCard(item, onSelect, Boolean(selectedDocuments[index])) })} ))} diff --git a/frontend/components/DocumentTableList/DocumentTableList.jsx b/frontend/components/DocumentTableList/DocumentTableList.jsx index e4751695c..1fc16a6ca 100644 --- a/frontend/components/DocumentTableList/DocumentTableList.jsx +++ b/frontend/components/DocumentTableList/DocumentTableList.jsx @@ -68,7 +68,7 @@ export default class DocumentTableList extends Component { /** * A hash map of the indices of the currently selected documents. */ - selectedDocuments: proptypes.array, + selectedDocuments: proptypes.object, /** * The maximum number of documents that can be selected. @@ -83,14 +83,15 @@ export default class DocumentTableList extends Component { getSelectedRows() { const {documents, selectedDocuments} = this.props - - return documents.reduce((selectedRows, item, index) => { + const selectedRows = documents.reduce((selectedRows, item, index) => { if (selectedDocuments[item._id]) { selectedRows[index] = true } - return selectedDocuments + return selectedRows }, {}) + + return selectedRows } handleRowRender(listableFields, value, data, column, index) { @@ -149,16 +150,13 @@ export default class DocumentTableList extends Component { render() { const { collection, - config, documents, fields: fieldsToDisplay = [], - onBuildBaseUrl, onSelect, order, - referencedField, + selectedDocuments, sort } = this.props - const selectedRows = this.getSelectedRows() const collectionFields = (collection && collection.fields) || {} const listableFields = Object.keys(collectionFields).reduce((fields, fieldName) => { if (fieldsToDisplay.includes(fieldName)) { @@ -184,7 +182,7 @@ export default class DocumentTableList extends Component { onRender={this.handleRowRender.bind(this, listableFields)} onSelect={onSelect} onSort={this.handleTableSort.bind(this)} - selectedRows={selectedRows} + selectedRows={selectedDocuments} selectLimit={Infinity} sortable={true} sortBy={sort} diff --git a/frontend/components/Table/Table.jsx b/frontend/components/Table/Table.jsx index 080156bf9..d03697796 100644 --- a/frontend/components/Table/Table.jsx +++ b/frontend/components/Table/Table.jsx @@ -163,7 +163,6 @@ export default class Table extends Component { selectedRows } = this.props const selectedRowsIndices = Object.keys(selectedRows).filter(index => selectedRows[index]) - let head = [] let body = [] @@ -192,8 +191,7 @@ export default class Table extends Component { head.push(child) } else { const rowIndex = tableHasHead ? index - 1 : index - const rowIsSelected = selectedRows[rowIndex] === true - const numberOfRows = tableHasHead ? children.length - 1 : children.length + const rowIsSelected = Boolean(selectedRows[rowIndex]) const selectionExhausted = selectedRowsIndices.length >= selectLimit childAttributes.onSelect = this.handleRowSelect.bind(this) diff --git a/frontend/containers/DocumentList/DocumentList.jsx b/frontend/containers/DocumentList/DocumentList.jsx index cbbabb50c..ac16b8366 100644 --- a/frontend/containers/DocumentList/DocumentList.jsx +++ b/frontend/containers/DocumentList/DocumentList.jsx @@ -156,25 +156,35 @@ class DocumentList extends Component { state } = this.props const {app, api, documents} = state - const previousDocuments = prevProps.state.documents - - // If we are have just loaded a list of documents for a nested document, - // let's update the selection with the value of the reference field, if - // it is in view. - if (referencedField && previousDocuments.isLoading && !documents.isLoading) { - let document = Object.assign( + const { + isDeleting, + isLoading, + isSaving, + selected: selection + } = documents + const { + isDeleting: wasDeleting, + isLoading: wasLoading, + isSaving: wasSaving + } = prevProps.state.documents + + // If we have just loaded a list of documents for a nested document, let's + // see if the list of selected documents needs to be updated. + if (referencedField && wasLoading && !isLoading) { + const document = Object.assign( {}, state.document.remote, state.document.local ) - let referencedValues = document[referencedField] - let referencedIds = (Array.isArray(referencedValues) - ? referencedValues.map(value => value._id) - : [referencedValues && referencedValues._id] + const referenceValue = (Array.isArray(document[referencedField]) + ? document[referencedField] + : [document[referencedField]] ).filter(Boolean) - if (referencedIds.length > 0) { - actions.setDocumentSelection(referencedIds) + // If the referenced value has a set value and the current selection does + // not reflect it, we must update the selection. + if (referenceValue.length > 0 && selection.length === 0) { + actions.setDocumentSelection(referenceValue) } } @@ -182,8 +192,8 @@ class DocumentList extends Component { const {path: previousCollectionPath} = prevProps.collection || {} const {search} = state.router.locationBeforeTransitions const {search: previousSearch} = prevProps.state.router.locationBeforeTransitions - const hasJustDeleted = previousDocuments.isDeleting && !documents.isDeleting - const hasJustSaved = previousDocuments.isSaving && !documents.isSaving + const hasJustDeleted = wasDeleting && !isDeleting + const hasJustSaved = wasSaving && !isSaving const resourceIsTheSame = collectionPath === previousCollectionPath && referencedField === prevProps.referencedField && page === prevProps.page && @@ -193,7 +203,7 @@ class DocumentList extends Component { !app.config || api.apis.length === 0 || !collection || - documents.isLoading || + isLoading || (!hasJustDeleted && !hasJustSaved && documents.list && resourceIsTheSame) ) { return @@ -342,9 +352,9 @@ class DocumentList extends Component { ) } - const items = documents.list.results + const {results: listItems} = documents.list - if (items.length === 0) { + if (listItems.length === 0) { if (typeof onRenderEmptyDocumentList !== 'function') { return null } @@ -365,34 +375,40 @@ class DocumentList extends Component { // The first one contains all selected documents, mapping their IDs to a // `true` Boolean. The second one contains all selected documents that are // currently into view, mapping their index to a `true` Boolean. - let selectedDocuments = {} - let selectedDocumentsInView = documents.selected.reduce((result, id, index) => { - let matchingDocumentIndex = items.findIndex(item => item._id === id) + const selectedDocuments = {} + const selectedDocumentsInView = documents.selected + .reduce((result, selectedDocument) => { + const {_id: id} = selectedDocument + const matchingDocumentIndex = listItems.findIndex(item => { + return item._id === id + }) - if (matchingDocumentIndex !== -1) { - result[matchingDocumentIndex] = true - } + if (matchingDocumentIndex !== -1) { + result[matchingDocumentIndex] = selectedDocument + } - selectedDocuments[id] = true + selectedDocuments[id] = selectedDocument - return result - }, {}) + return result + }, {}) // The new selection is formed by merging the new selection hash with any // previously selected documents that are not in view (i.e. are on a // different page). - let onSelectFn = selectedIndexes => { - let newSelection = Object.assign({}, selectedDocuments) + const onSelectFn = newSelection => { + Object.keys(newSelection).forEach(index => { + const document = listItems[index] - items.forEach((item, index) => { - newSelection[item._id] = Boolean(selectedIndexes[index]) + selectedDocuments[document._id] = newSelection[index] + ? document + : undefined }) // Converting a new selection hash to the array format that the store // is expecting. - let newSelectionArray = Object.keys(newSelection).filter(id => { - return Boolean(newSelection[id]) - }) + const newSelectionArray = Object.keys(selectedDocuments) + .filter(id => Boolean(selectedDocuments[id])) + .map(id => selectedDocuments[id]) actions.setDocumentSelection(newSelectionArray) } @@ -403,7 +419,7 @@ class DocumentList extends Component { collectionParent, config, documentId, - documents: items, + documents: listItems, onBuildBaseUrl, onSelect: onSelectFn, order, diff --git a/frontend/views/DocumentListView/DocumentListView.jsx b/frontend/views/DocumentListView/DocumentListView.jsx index db16f5d47..ca7e653c7 100644 --- a/frontend/views/DocumentListView/DocumentListView.jsx +++ b/frontend/views/DocumentListView/DocumentListView.jsx @@ -62,20 +62,7 @@ class DocumentListView extends Component { } } - handleBulkActionApply(actionType) { - const {state} = this.props - - switch (actionType) { - case BULK_ACTIONS.DELETE: - this.handleDocumentDelete(state.documents.selected) - break - - default: - return - } - } - - handleDocumentDelete(ids) { + delete(ids) { const {actions, state} = this.props const { currentApi: api, @@ -89,6 +76,22 @@ class DocumentListView extends Component { }) } + handleBulkActionApply(actionType) { + const {state} = this.props + + switch (actionType) { + case BULK_ACTIONS.DELETE: + const ids = state.documents.selected.map(document => document._id) + + this.delete(ids) + + break + + default: + return + } + } + handleEmptyDocumentList() { const { filter, diff --git a/frontend/views/MediaListView/MediaListView.jsx b/frontend/views/MediaListView/MediaListView.jsx index ed75a5f69..0543e7562 100644 --- a/frontend/views/MediaListView/MediaListView.jsx +++ b/frontend/views/MediaListView/MediaListView.jsx @@ -28,7 +28,7 @@ const BULK_ACTIONS = { class MediaListView extends Component { componentDidUpdate(prevProps, prevState) { const {actions, state} = this.props - const {isDeleting, list} = state.documents + const {isDeleting} = state.documents const wasDeleting = prevProps.state.documents.isDeleting // Have we just deleted some documents? @@ -43,6 +43,16 @@ class MediaListView extends Component { } } + delete(ids) { + const {actions, state} = this.props + const api = state.api.apis[0] + + actions.deleteMedia({ + api, + ids + }) + } + handleBuildBaseUrl({ page }) { @@ -59,25 +69,13 @@ class MediaListView extends Component { const {state} = this.props if (actionType === BULK_ACTIONS.DELETE) { - this.handleDelete(state.documents.selected) - } - } + const ids = state.documents.selected.map(document => document._id) - handleDelete(ids) { - const {actions, state} = this.props - const api = state.api.apis[0] - - actions.deleteMedia({ - api, - ids - }) + this.delete(ids) + } } handleEmptyDocumentList() { - const { - referencedField - } = this.props - return ( { - return list.results.find(document => document._id === documentId) - }).filter(Boolean) + const {selected} = state.documents + const selectedDocuments = selected.filter(Boolean) let update = { [referencedField]: selectedDocuments @@ -158,10 +155,7 @@ class ReferenceSelectView extends Component { render() { const { - collection, documentId, - filter, - group, onBuildBaseUrl, order, page, From 78c630463b27ad46d6286c666a69798d63a94160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 20 Mar 2019 16:43:05 +0000 Subject: [PATCH 07/22] fix: remove filters on media reference select view --- frontend/views/ReferenceSelectView/ReferenceSelectView.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx b/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx index fe5c2d533..62922e20e 100644 --- a/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx +++ b/frontend/views/ReferenceSelectView/ReferenceSelectView.jsx @@ -242,7 +242,7 @@ class ReferenceSelectView extends Component { Date: Thu, 21 Mar 2019 15:54:20 +0000 Subject: [PATCH 08/22] feat: add button for selected docs in list view --- .../DocumentListToolbar.css | 20 ++++++ .../DocumentListToolbar.jsx | 32 ++++++++-- .../DocumentFilters/DocumentFilters.jsx | 63 ++++++++++++------- .../containers/DocumentList/DocumentList.jsx | 49 ++++++++++----- .../DocumentListView/DocumentListView.css | 3 - .../DocumentListView/DocumentListView.jsx | 18 +++--- .../ReferenceSelectView.jsx | 16 ++++- 7 files changed, 146 insertions(+), 55 deletions(-) delete mode 100644 frontend/views/DocumentListView/DocumentListView.css diff --git a/frontend/components/DocumentListToolbar/DocumentListToolbar.css b/frontend/components/DocumentListToolbar/DocumentListToolbar.css index 0cba0a526..491cde920 100644 --- a/frontend/components/DocumentListToolbar/DocumentListToolbar.css +++ b/frontend/components/DocumentListToolbar/DocumentListToolbar.css @@ -61,3 +61,23 @@ display: none; } } + +.selection-counter { + padding-left: 8px; + visibility: hidden; +} + +.selection-counter-visible { + visibility: visible; +} + +.selection-counter-button { + border-bottom: 1px solid #eee; + cursor: pointer; + outline: 0; + padding: 0 1px; +} + +.selection-counter:hover .selection-counter-button { + border-bottom-color: var(--theme-colour-data, #4A91FF); +} \ No newline at end of file diff --git a/frontend/components/DocumentListToolbar/DocumentListToolbar.jsx b/frontend/components/DocumentListToolbar/DocumentListToolbar.jsx index 3bf68a1a3..3b6c4a8cf 100644 --- a/frontend/components/DocumentListToolbar/DocumentListToolbar.jsx +++ b/frontend/components/DocumentListToolbar/DocumentListToolbar.jsx @@ -31,9 +31,19 @@ export default class DocumentListToolbar extends Component { documentsMetada: proptypes.object, /** - * A callback to be used to obtain the URL for a given page. + * A callback used to obtain the URL for a given page. */ - onBuildPageUrl: proptypes.func + onBuildPageUrl: proptypes.func, + + /** + * The list of selected documents. + */ + selectedDocuments: proptypes.array, + + /** + * The URL for limiting the document list to only selected documents. + */ + showSelectedDocumentsUrl: proptypes.string } goToPage(value) { @@ -61,7 +71,9 @@ export default class DocumentListToolbar extends Component { const { children, documentsMetadata: metadata, - onBuildPageUrl + onBuildPageUrl, + selectedDocuments = [], + showSelectedDocumentsUrl } = this.props if (!metadata) return null @@ -75,15 +87,27 @@ export default class DocumentListToolbar extends Component { return result }, {}) + const selectionCounter = new Style(styles, 'selection-counter') + .addIf('selection-counter-visible', selectedDocuments.length > 0) return ( {metadata.totalCount > 1 && (
- Showing {`${metadata.offset + 1}-${Math.min(metadata.offset + metadata.limit, metadata.totalCount)} `} of {metadata.totalCount} + + + ( + + {selectedDocuments.length} selected + + ) +
)} diff --git a/frontend/containers/DocumentFilters/DocumentFilters.jsx b/frontend/containers/DocumentFilters/DocumentFilters.jsx index 726b3732d..7038ddd18 100644 --- a/frontend/containers/DocumentFilters/DocumentFilters.jsx +++ b/frontend/containers/DocumentFilters/DocumentFilters.jsx @@ -54,29 +54,14 @@ class DocumentFilters extends Component { this.outsideTooltipHandler = this.handleClick.bind(this) } - componentDidMount() { - window.addEventListener('click', this.outsideTooltipHandler) - } - - componentDidUpdate(oldProps) { - const {collection = {}} = this.props - const {collection: oldCollection = {}} = oldProps - - // If we have navigated to a different collection, we should reset the - // state o the search bar. - if (collection.path !== oldCollection.path) { - this.setState({...this.defaultState}) - } - } - - componentWillUnmount() { - window.removeEventListener('click', this.outsideTooltipHandler) - } - - buildFiltersArray(filtersObject) { - if (!filtersObject) return [] + buildFiltersArray(filtersObject = {}) { + const filtersArray = Object.keys(filtersObject).map(field => { + if (field === '$selected') { + return { + FilterList: () => this.renderSelectedFilter(filtersObject[field]) + } + } - let filtersArray = Object.keys(filtersObject).map(field => { const fieldComponent = this.getFieldComponent(field) || {} const { filterList: FilterList, @@ -104,6 +89,25 @@ class DocumentFilters extends Component { return filtersArray } + componentDidMount() { + window.addEventListener('click', this.outsideTooltipHandler) + } + + componentDidUpdate(oldProps) { + const {collection = {}} = this.props + const {collection: oldCollection = {}} = oldProps + + // If we have navigated to a different collection, we should reset the + // state o the search bar. + if (collection.path !== oldCollection.path) { + this.setState({...this.defaultState}) + } + } + + componentWillUnmount() { + window.removeEventListener('click', this.outsideTooltipHandler) + } + getFieldComponent(fieldName) { const {collection} = this.props const fieldSchema = fieldName && collection.fields[fieldName] @@ -119,6 +123,8 @@ class DocumentFilters extends Component { const {collection} = this.props const fieldSchema = collection.fields[fieldName] + if (!fieldSchema) return null + return fieldSchema.label || fieldName } @@ -404,10 +410,11 @@ class DocumentFilters extends Component { selectedFilterOperator, selectedFilterValue } = this.state + const fieldName = this.getFieldName(field) const fieldTypeHasFilterListComponent = typeof FilterList === 'function' - const nodeField = ( + const nodeField = fieldName && ( - {this.getFieldName(field)} + {fieldName} ) const nodeOperator = ( @@ -550,6 +557,14 @@ class DocumentFilters extends Component { ) } + + renderSelectedFilter(value) { + const message = `is ${value === false ? 'not ' : ''} selected` + + return ( + {message} + ) + } } export default connectHelper( diff --git a/frontend/containers/DocumentList/DocumentList.jsx b/frontend/containers/DocumentList/DocumentList.jsx index ac16b8366..010872f3a 100644 --- a/frontend/containers/DocumentList/DocumentList.jsx +++ b/frontend/containers/DocumentList/DocumentList.jsx @@ -4,6 +4,7 @@ import {h, Component} from 'preact' import proptypes from 'proptypes' import {connect} from 'preact-redux' import {bindActionCreators} from 'redux' +import {batchActions} from 'lib/redux' import {route} from '@dadi/preact-router' import {Keyboard} from 'lib/keyboard' @@ -218,19 +219,19 @@ class DocumentList extends Component { collection, state } = this.props - - let currentCollection = collection || {} - let nextCollection = nextProps.collection || {} - - // This is required to recover from an error. If the document list has - // errored and we're about to navigate to a different collection, we - // clear the error state by setting the status to IDLE and let the - // container fetch again. - if ( - state.documents.remoteError && - currentCollection.path !== nextCollection.path - ) { - actions.setDocumentListStatus(Constants.STATUS_IDLE) + const currentCollection = collection || {} + const nextCollection = nextProps.collection || {} + + if (currentCollection.path !== nextCollection.path) { + actions.setDocumentSelection([]) + + // This is required to recover from an error. If the document list has + // errored and we're about to navigate to a different collection, we + // clear the error state by setting the status to IDLE and let the + // container fetch again. + if (state.documents.remoteError) { + actions.setDocumentListStatus(Constants.STATUS_IDLE) + } } } @@ -275,14 +276,30 @@ class DocumentList extends Component { return } - let count = (collection.settings && collection.settings.count) + if (filters.$selected === true) { + const {selected: selectedDocuments} = state.documents + const newDocumentList = { + results: selectedDocuments, + metadata: { + limit: selectedDocuments.length, + offset: 0, + page: 1, + totalCount: selectedDocuments.length, + totalPages: 1 + } + } + + return actions.setDocumentList(newDocumentList) + } + + const count = (collection.settings && collection.settings.count) || 20 // This is the object we'll send to the `fetchDocuments` action. If we're // dealing with a reference field select, we'll pass this object to any // existing `beforeReferenceSelect` hook so that field components have the // chance to modify the criteria used to retrieve documents from the API. - let fetchObject = { + const fetchObject = { api, collection, count, @@ -296,7 +313,7 @@ class DocumentList extends Component { sortOrder: order } - actions.fetchDocuments(fetchObject) + return actions.fetchDocuments(fetchObject) } render() { diff --git a/frontend/views/DocumentListView/DocumentListView.css b/frontend/views/DocumentListView/DocumentListView.css deleted file mode 100644 index b21841dd7..000000000 --- a/frontend/views/DocumentListView/DocumentListView.css +++ /dev/null @@ -1,3 +0,0 @@ -.bulk-action-select { - margin-right: 10px; -} \ No newline at end of file diff --git a/frontend/views/DocumentListView/DocumentListView.jsx b/frontend/views/DocumentListView/DocumentListView.jsx index ca7e653c7..14c64dc18 100644 --- a/frontend/views/DocumentListView/DocumentListView.jsx +++ b/frontend/views/DocumentListView/DocumentListView.jsx @@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux' import {connectHelper, setPageTitle} from 'lib/util' import {getVisibleFields} from 'lib/fields' import {route} from '@dadi/preact-router' -import {URLParams} from 'lib/util/urlParams' import * as appActions from 'actions/appActions' import * as Constants from 'lib/constants' @@ -21,8 +20,6 @@ import Header from 'containers/Header/Header' import HeroMessage from 'components/HeroMessage/HeroMessage' import Main from 'components/Main/Main' import Page from 'components/Page/Page' -import Style from 'lib/Style' -import styles from './DocumentListView.css' const BULK_ACTIONS = { DELETE: 'BULK_ACTIONS_DELETE' @@ -131,16 +128,13 @@ class DocumentListView extends Component { render() { const { - collection, documentId, - group, onBuildBaseUrl, order, page, sort, state } = this.props - const {bulkActionSelected} = this.state const { currentApi, currentCollection, @@ -159,7 +153,6 @@ class DocumentListView extends Component { viewType: 'list' }) ).concat(Constants.DEFAULT_FIELDS) - const actions = { [BULK_ACTIONS.DELETE]: { confirmationMessage: @@ -171,6 +164,15 @@ class DocumentListView extends Component { label: `Delete ${selectedDocuments.length ? ' (' + selectedDocuments.length + ')' : ''}` } } + const showSelectedDocumentsUrl = onBuildBaseUrl.call(this, { + search: { + ...search, + filter: { + ...search.filter, + $selected: true + } + } + }) return ( @@ -215,6 +217,8 @@ class DocumentListView extends Component { onBuildPageUrl={page => onBuildBaseUrl.call(this, { page })} + selectedDocuments={selectedDocuments} + showSelectedDocumentsUrl={showSelectedDocumentsUrl} >