From 16df1eb872c4df31c9cbe0b57aec53028eb542dd Mon Sep 17 00:00:00 2001 From: Arlen Johnson Date: Tue, 7 May 2024 14:56:39 -0400 Subject: [PATCH 01/15] Add first-pass at Bulk Actions box. Turn on bulk actions for Groups and Group Members. --- app/resources/locales/en_US/operation.po | 3 ++ app/templates/GroupMembers/columns.inc | 12 +++--- app/templates/Groups/columns.inc | 11 ++---- app/templates/Standard/index.php | 16 +++++++- app/templates/element/bulk/bulk.php | 49 ++++++++++++++++++++++++ app/templates/element/javascript.php | 2 + app/templates/element/menuAction.php | 2 +- app/webroot/css/co-base.css | 46 ++++++++++++++++++---- app/webroot/css/co-responsive.css | 4 ++ 9 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 app/templates/element/bulk/bulk.php diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 473c81ebf..4b58a526d 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -69,6 +69,9 @@ msgstr "Assign" msgid "any" msgstr "Any" +msgid "bulk.actions" +msgstr "Bulk Actions" + msgid "cancel" msgstr "Cancel" diff --git a/app/templates/GroupMembers/columns.inc b/app/templates/GroupMembers/columns.inc index 3007190e8..9fcffd6af 100644 --- a/app/templates/GroupMembers/columns.inc +++ b/app/templates/GroupMembers/columns.inc @@ -83,13 +83,11 @@ $topLinks = [ */ ]; -/* -// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. -$bulkActions = [ - // TODO: develop bulk actions. For now, use a placeholder. - 'delete' => true -]; -*/ + +// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. +// The array should contain a list of actions to be made available. +$bulkActions = ['delete']; + $subnav = [ 'name' => 'group', diff --git a/app/templates/Groups/columns.inc b/app/templates/Groups/columns.inc index 2167470fd..436f28760 100644 --- a/app/templates/Groups/columns.inc +++ b/app/templates/Groups/columns.inc @@ -73,10 +73,7 @@ $rowActions = [ ] ]; -/* -// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. -$bulkActions = [ - // TODO: develop bulk actions. For now, use a placeholder. - 'delete' => true -]; -*/ \ No newline at end of file + +// When the $bulkActions variable exists in a columns.inc config, the "Bulk edit" switch will appear in the index. +// The array should contain a list of actions to be made available. +$bulkActions = ['delete']; diff --git a/app/templates/Standard/index.php b/app/templates/Standard/index.php index d29a64b71..2295a1572 100644 --- a/app/templates/Standard/index.php +++ b/app/templates/Standard/index.php @@ -169,14 +169,21 @@ - + + element('flash', $flashArgs); ?> + + + + + element('bulk/bulk', ['bulkActions' => $bulkActions]); ?> + - + '; print '
'; print $this->element('menuAction', $action_args); print '
'; @@ -603,6 +611,10 @@ } break; } + + if($isFirstLink && !empty($rowActions)) { + print ''; // field-actions-container + } ?> diff --git a/app/templates/element/bulk/bulk.php b/app/templates/element/bulk/bulk.php new file mode 100644 index 000000000..0e48a4529 --- /dev/null +++ b/app/templates/element/bulk/bulk.php @@ -0,0 +1,49 @@ + + +
+ + + + + + + + +
diff --git a/app/templates/element/javascript.php b/app/templates/element/javascript.php index 23c171e21..de77d2194 100644 --- a/app/templates/element/javascript.php +++ b/app/templates/element/javascript.php @@ -271,8 +271,10 @@ // Bulk edit switch $('#bulk-edit-switch').click(function() { if($("#bulk-edit-switch").is(':checked')) { + $("body").addClass('bulk-mode'); $("table.index-table").removeClass('list-mode').addClass('bulk-edit-mode'); } else { + $("body").removeClass('bulk-mode'); $("table.index-table").removeClass('bulk-edit-mode').addClass('list-mode'); } }); diff --git a/app/templates/element/menuAction.php b/app/templates/element/menuAction.php index 723141a15..1832e89d1 100644 --- a/app/templates/element/menuAction.php +++ b/app/templates/element/menuAction.php @@ -149,7 +149,7 @@ class="">
  • -
    +
    diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index 006b8be8d..9cf296af2 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -1026,12 +1026,14 @@ h2.config-subtitle { } /* INDEX ACTION COMMAND MENUS */ th.with-field-actions { - padding-left: 2.75em; + padding-left: 3em; +} +table.list-mode td.with-field-actions { + padding: 0; } -td.with-field-actions { +table.list-mode .field-actions-container { display: flex; align-items: center; - padding: 0; } .field-actions .action-menu-toggle { display: inline-block; @@ -1064,17 +1066,33 @@ a.dropdown-item.deletebutton { border-bottom: 1px solid var(--cmg-color-bg-005); } /* INDEX ACTION BULK EDIT */ +#bulk-actions { + display: none; +} +body.bulk-mode #bulk-actions { + display: flex; + gap: 1em; + align-items: center; + padding: 1em; + margin: 1em 0; + background-color: var(--cmg-color-bg-003); + border: 1px solid var(--cmg-color-bg-005); + flex-wrap: wrap; +} .field-actions.top-links #bulk-edit-switch-container { padding: 0.5em 1em; } +body.bulk-mode #bulk-actions legend { + width: auto; +} table.bulk-edit-mode a.row-link, table.bulk-edit-mode .read-only-link-container, table.bulk-edit-mode .row-link-heading, table.bulk-edit-mode .field-actions { display: none; } -table.bulk-edit-mode th.with-field-actions { - padding-left: 0.75em; +table.bulk-edit-mode .with-field-actions { + padding-left: 1em; } table.list-mode a.row-link, table.list-mode .field-actions { @@ -1084,11 +1102,19 @@ table.list-mode .bulk-action-checkbox-container { display: none; } table.index-table .form-check { - margin-bottom: 0; + margin: 0 0 0 -0.5em; min-height: unset; } +table.index-table .with-field-actions .form-check { + margin: 0; +} +.bulk-action-checkbox-container { + display: flex; + align-items: center; +} .bulk-action-checkbox-container .form-check-input { margin-right: 1em; + flex-shrink: 0; } /* PAGINATION */ #pagination { @@ -1998,10 +2024,13 @@ button, .btn, .btn:hover { border-color: var(--cmg-color-btn-bg-001); } .btn-primary, -.btn-primary:active { +.btn-primary:active, +.btn-secondary, +.btn-secondary:active { box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); } -.btn-primary:hover { +.btn-primary:hover, +.btn-secondary:hover { background-color: var(--cmg-color-btn-bg-002); color: var(--cmg-color-txt-inverse) !important; border-color: var(--cmg-color-btn-bg-002); @@ -2031,6 +2060,7 @@ html.dark-mode .btn-default:active { .btn-secondary { background-color: var(--cmg-color-btn-004); color: var(--cmg-color-txt-inverse); + border-color: var(--cmg-color-btn-004); } .btn-link { font-size: 1em; diff --git a/app/webroot/css/co-responsive.css b/app/webroot/css/co-responsive.css index 7a3e97b81..117a177f1 100644 --- a/app/webroot/css/co-responsive.css +++ b/app/webroot/css/co-responsive.css @@ -375,6 +375,10 @@ margin-top: -3.5em; float: right; } + /* BULK EDIT */ + .field-actions.top-links #bulk-edit-switch-container { + padding: 0 1em; + } /* CO CONFIGURATION DASHBOARD */ .page-title-features { display: flex; From ddf66b17e8292d32fd6bfb4a6ed58bf66520d04b Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 8 May 2024 22:15:20 +0300 Subject: [PATCH 02/15] Support bulk delete --- app/resources/locales/en_US/operation.po | 3 + app/templates/element/bulk/bulk.php | 101 +++++++++-- app/templates/element/mveaJs.php | 2 +- .../comanage/components/bulk/bulk-actions.js | 161 ++++++++++++++++++ .../js/comanage/components/common/alert.js | 98 +++++++++++ .../js/comanage/components/common/modal.js | 67 ++++++++ .../js/comanage/components/utils/helpers.js | 15 +- 7 files changed, 433 insertions(+), 14 deletions(-) create mode 100644 app/webroot/js/comanage/components/bulk/bulk-actions.js create mode 100644 app/webroot/js/comanage/components/common/alert.js create mode 100644 app/webroot/js/comanage/components/common/modal.js diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 4b58a526d..6513364ba 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -174,6 +174,9 @@ msgstr "Display records" msgid "page.goto" msgstr "Go to page" +msgid "pick" +msgstr "Pick" + msgid "previous" msgstr "Previous" diff --git a/app/templates/element/bulk/bulk.php b/app/templates/element/bulk/bulk.php index 0e48a4529..2741c0832 100644 --- a/app/templates/element/bulk/bulk.php +++ b/app/templates/element/bulk/bulk.php @@ -32,18 +32,95 @@ * @since COmanage Registry v5.0.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ - -// Get parameters -$bulkActions = $bulkActions; + +/* + * Parameters: + * $bulkActions : array, required + */ + +declare(strict_types = 1); + +use \Cake\Utility\Inflector; + +// $this->name = Models +$modelsName = $this->name; +// $tablename = models +// XXX backport to match? +$tableName = Inflector::tableize($this->name); +$controllersName = Inflector::dasherize($this->name); +$tableFK = Inflector::singularize($tableName) . "_id"; + +// Load my helper functions +$vueHelper = $this->loadHelper('Vue'); +// Get the CSRF Token in JavaScript +$token = $this->request->getAttribute('csrfToken'); + +$label = 'Apply'; + ?> -
    - - - - - - + + +
    diff --git a/app/templates/element/mveaJs.php b/app/templates/element/mveaJs.php index 7360b44e1..b7d1080db 100644 --- a/app/templates/element/mveaJs.php +++ b/app/templates/element/mveaJs.php @@ -35,7 +35,7 @@ $mveaController = Cake\Utility\Inflector::camelize($mveaType); $title = __d('controller', $mveaController, [99]); -// Get the CSRF Token in JavaScript +// Get the CSRF Token in JavaScript $token = $this->request->getAttribute('csrfToken'); // Load my helper functions $vueHelper = $this->loadHelper('Vue'); diff --git a/app/webroot/js/comanage/components/bulk/bulk-actions.js b/app/webroot/js/comanage/components/bulk/bulk-actions.js new file mode 100644 index 000000000..4dfbc65a2 --- /dev/null +++ b/app/webroot/js/comanage/components/bulk/bulk-actions.js @@ -0,0 +1,161 @@ +/** + * COmanage Registry Bulk Actions Vue.js Component + * + * Portions licensed to the University Corporation for Advanced Internet + * Development, Inc. ("UCAID") under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @link https://www.internet2.edu/comanage COmanage Project + * @package registry + * @since COmanage Registry v5.0.0 + * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +import Alert from './alert.js' +import {capitalize} from "../utils/helpers.js?t=3" + +export default { + data() { + return { + loading: false, + num: 0, + progress: 0, + ids: [], + selected: null, + failed: 0, + succeeded: 0 + } + }, + components: { + Alert + }, + props: { + label: { + type: String, + default: "Apply" + }, + legend: { + type: String, + default: "Not provided" + }, + bulkactions: { + type: Array, + default: [] + } + }, + inject: ['txt', 'api', 'app'], + methods: { + capitalize, + async allProgress(fetches, progress_cb) { + let d = 0; + progress_cb(0); + for (const p of fetches) { + p.then(()=> { + d ++; + progress_cb( (d * 100) / fetches.length ); + }); + } + return Promise.all(fetches); + }, + async run() { + this.loading = true; + // Construct the url + const urlString = window.location.protocol + + "//" + window.location.host + + this.api.webroot + + this.app.controller + "/" + + this.selected; + + this.allProgress( + this.ids.map((index, id) => { + let finalUrl = urlString + "/" + id + // AJAX Request + let request = new Request(finalUrl, { + headers: new Headers({ + 'X-Requested-With': 'XMLHttpRequest', + "Accept": "application/json", + }), + method: this.selected + }); + + return fetch(request) + }), + (prog) => this.progress = prog + ).then((responses) => { + this.failed = responses.filter((index, resp) => !resp.ok).length + this.succeeded = responses.filter((index, resp) => resp.ok).length + this.loading = false + }).catch((error) => { + this.loading = false + console.error(error.message); + }); + }, + items() { + let checked = $('.bulk-action-checkbox-container input.form-check-input[type=checkbox]:checked'); + this.ids = checked.map((index, item) => item.getAttribute('data-entity-id')) + this.num = this.ids.length; + } + }, + computed: { + calculateStyle() { + return "width: " + this.progress + "%;" + }, + disable() { + return this.ids.length == 0 || this.selected == undefined + }, + failedMessage() { + return `Failed to "${capitalize(this.selected)}" #${this.failed} records` + }, + succeededMessage() { + return `Successfully "${capitalize(this.selected)}" #${this.failed} records` + } + }, + template: ` + + + {{ this.legend }} + + + + +
    +
    {{ this.progress }}%
    +
    + ` +} \ No newline at end of file diff --git a/app/webroot/js/comanage/components/common/alert.js b/app/webroot/js/comanage/components/common/alert.js new file mode 100644 index 000000000..ae0fe33af --- /dev/null +++ b/app/webroot/js/comanage/components/common/alert.js @@ -0,0 +1,98 @@ +/** + * COmanage Registry Alert Vue.js Component + * + * Portions licensed to the University Corporation for Advanced Internet + * Development, Inc. ("UCAID") under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @link https://www.internet2.edu/comanage COmanage Project + * @package registry + * @since COmanage Registry v5.0.0 + * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +import {capitalize} from "../utils/helpers.js"; + +export default { + data() { + return { + msg: null + } + }, + props: { + message: { + type: String + }, + failed: { + type: Number + }, + succeed: { + type: Number + }, + action: { + type: String + }, + title: { + type: String, + default: null + } + }, + computed: { + getClass() { + let state = 'success' + if(this.hasFailed) { + state = 'danger' + } + + return "w-100 alert co-alert alert-" + state + }, + titleExists() { + return this.title != undefined && this.title != '' + }, + hasSucceeded() { + return this.succeed > 0 + }, + hasFailed() { + return this.failed > 0 + }, + getMessage() { + if(this.message != undefined && this.message != '') { + return this.message + } else if(this.hasFailed) { + return capitalize(this.action) + ' Failed (#' + this.failed + ')' + } else if(this.hasSucceeded) { + return capitalize(this.action) + ' Succeeded (#' + this.succeed + ')' + } + }, + display() { + return this.hasSucceeded || this.hasFailed + } + }, + template: ` +
    +
    + + report_problem + {{ this.title }} + + {{ this.getMessage }} + + + +
    +
    + ` +} \ No newline at end of file diff --git a/app/webroot/js/comanage/components/common/modal.js b/app/webroot/js/comanage/components/common/modal.js new file mode 100644 index 000000000..70481d42c --- /dev/null +++ b/app/webroot/js/comanage/components/common/modal.js @@ -0,0 +1,67 @@ +/** + * COmanage Registry Modal Vue Element + * + * Portions licensed to the University Corporation for Advanced Internet + * Development, Inc. ("UCAID") under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * UCAID licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @link https://www.internet2.edu/comanage COmanage Project + * @package registry + * @since COmanage Registry v5.0.0 + * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +export default { + props: { + id: { + type: String + }, + title: { + type: String, + default: 'Title' + }, + buttonLabel: { + type: String + } + }, + inject: ['txt'], + template: ` + + ` + } \ No newline at end of file diff --git a/app/webroot/js/comanage/components/utils/helpers.js b/app/webroot/js/comanage/components/utils/helpers.js index aeed98b1e..8881cb571 100644 --- a/app/webroot/js/comanage/components/utils/helpers.js +++ b/app/webroot/js/comanage/components/utils/helpers.js @@ -49,7 +49,20 @@ const camelize = (word) => { return word.split("_").map(word => (word[0].toUpperCase() + word.slice(1))).join('') } +// Capitalize String +const capitalize = (sentence) => { + let capitalized = [] + sentence?.split(' ').forEach(word => { + capitalized.push( + word.charAt(0).toUpperCase() + + word.slice(1).toLowerCase() + ) + }) + return capitalized.join(' ') +} + export { constructLanguageString, - camelize + camelize, + capitalize } \ No newline at end of file From 126328b71d954406facbf824fe0e55e3b942a292 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 8 May 2024 22:17:46 +0300 Subject: [PATCH 03/15] Use Modal for bulk actions --- .../comanage/components/bulk/bulk-actions.js | 74 ++++++++++++------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/app/webroot/js/comanage/components/bulk/bulk-actions.js b/app/webroot/js/comanage/components/bulk/bulk-actions.js index 4dfbc65a2..a442a9e59 100644 --- a/app/webroot/js/comanage/components/bulk/bulk-actions.js +++ b/app/webroot/js/comanage/components/bulk/bulk-actions.js @@ -24,8 +24,9 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ -import Alert from './alert.js' -import {capitalize} from "../utils/helpers.js?t=3" +import Alert from '../common/alert.js' +import Modal from '../common/modal.js' +import {capitalize} from "../utils/helpers.js" export default { data() { @@ -40,7 +41,8 @@ export default { } }, components: { - Alert + Alert, + Modal }, props: { label: { @@ -57,7 +59,18 @@ export default { } }, inject: ['txt', 'api', 'app'], + mounted: function() { + this.reload() + }, methods: { + reload() { + // Reload the view to fetch the latest changes + if($('#bulk-actions-modal').length) { + $('#bulk-actions-modal').on('hidden.bs.modal', function() { + location.reload() + }) + } + }, capitalize, async allProgress(fetches, progress_cb) { let d = 0; @@ -86,7 +99,8 @@ export default { let request = new Request(finalUrl, { headers: new Headers({ 'X-Requested-With': 'XMLHttpRequest', - "Accept": "application/json", + 'Accept': 'application/json', + 'X-CSRF-Token': this.api.token }), method: this.selected }); @@ -95,8 +109,8 @@ export default { }), (prog) => this.progress = prog ).then((responses) => { - this.failed = responses.filter((index, resp) => !resp.ok).length - this.succeeded = responses.filter((index, resp) => resp.ok).length + this.failed = responses.filter(resp => !resp.ok).length + this.succeeded = responses.filter(resp => resp.ok).length this.loading = false }).catch((error) => { this.loading = false @@ -110,28 +124,23 @@ export default { } }, computed: { + roundedProgress() { + return Math.trunc(this.progress) + }, calculateStyle() { - return "width: " + this.progress + "%;" + return "width: " + this.roundedProgress + "%;" }, disable() { return this.ids.length == 0 || this.selected == undefined }, failedMessage() { - return `Failed to "${capitalize(this.selected)}" #${this.failed} records` + return `Failed to "${capitalize(this.selected)}" #${this.failed} ${this.app.humanize}` }, succeededMessage() { - return `Successfully "${capitalize(this.selected)}" #${this.failed} records` + return `Successfully "${capitalize(this.selected)}" #${this.succeeded} ${this.app.humanize}` } }, template: ` - - {{ this.legend }}