From 3bd2b7583ec9e09b06c7e8253617c30a6e635615 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Wed, 10 Mar 2021 14:54:52 +0100 Subject: [PATCH 01/19] chg: [js:bootstrap-helper] Made submission modal more explicit --- .../genericElements/Form/submitButton.php | 2 +- .../IndexTable/Fields/actions.php | 2 +- .../IndexTable/Fields/alignments.php | 4 +- .../ListTopBar/element_simple.php | 6 +- .../ListTopBar/group_search.php | 6 +- .../SingleViews/Fields/alignmentField.php | 6 +- .../genericElements/side_menu_scaffold.php | 2 +- webroot/js/bootstrap-helper.js | 127 +++++++++++++----- 8 files changed, 106 insertions(+), 49 deletions(-) diff --git a/templates/element/genericElements/Form/submitButton.php b/templates/element/genericElements/Form/submitButton.php index 9a2a82c..134e857 100644 --- a/templates/element/genericElements/Form/submitButton.php +++ b/templates/element/genericElements/Form/submitButton.php @@ -3,7 +3,7 @@ echo sprintf( '%s', sprintf( - '', + '', '#form-' . h($formRandomValue), __('Submit') ) diff --git a/templates/element/genericElements/IndexTable/Fields/actions.php b/templates/element/genericElements/IndexTable/Fields/actions.php index fd03628..e099d41 100644 --- a/templates/element/genericElements/IndexTable/Fields/actions.php +++ b/templates/element/genericElements/IndexTable/Fields/actions.php @@ -87,7 +87,7 @@ $action['open_modal'] ); $reload_url = !empty($action['reload_url']) ? $action['reload_url'] : $this->Url->build(['action' => 'index']); - $action['onclick'] = sprintf('UI.openModalFromURL(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue); + $action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue); } echo sprintf( ' ', diff --git a/templates/element/genericElements/IndexTable/Fields/alignments.php b/templates/element/genericElements/IndexTable/Fields/alignments.php index 611f990..f761c2a 100644 --- a/templates/element/genericElements/IndexTable/Fields/alignments.php +++ b/templates/element/genericElements/IndexTable/Fields/alignments.php @@ -14,7 +14,7 @@ if ($field['scope'] === 'individuals') { h($alignment['organisation']['name']) ), !$canRemove ? '' : sprintf( - "UI.openModalFromURL(%s);", + "UI.submissionModalForIndex(%s);", sprintf( "'/alignments/delete/%s'", h($alignment['id']) @@ -34,7 +34,7 @@ if ($field['scope'] === 'individuals') { h($alignment['individual']['email']) ), !$canRemove ? '' : sprintf( - "UI.openModalFromURL(%s);", + "UI.submissionModalForIndex(%s);", sprintf( "'/alignments/delete/%s'", h($alignment['id']) diff --git a/templates/element/genericElements/ListTopBar/element_simple.php b/templates/element/genericElements/ListTopBar/element_simple.php index 6747eb1..a14976f 100644 --- a/templates/element/genericElements/ListTopBar/element_simple.php +++ b/templates/element/genericElements/ListTopBar/element_simple.php @@ -71,12 +71,8 @@ \ No newline at end of file diff --git a/templates/element/genericElements/ListTopBar/group_search.php b/templates/element/genericElements/ListTopBar/group_search.php index 8e85f39..f405640 100644 --- a/templates/element/genericElements/ListTopBar/group_search.php +++ b/templates/element/genericElements/ListTopBar/group_search.php @@ -144,11 +144,7 @@ } function openFilteringModal(clicked, url, reloadUrl, tableId) { - const loadingOverlay = new OverlayFactory(clicked); - loadingOverlay.show() - UI.openModalFromURL(url, reloadUrl, tableId).finally(() => { - loadingOverlay.hide() - }) + UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, tableId)) } }); diff --git a/templates/element/genericElements/SingleViews/Fields/alignmentField.php b/templates/element/genericElements/SingleViews/Fields/alignmentField.php index eb922b9..50e9162 100644 --- a/templates/element/genericElements/SingleViews/Fields/alignmentField.php +++ b/templates/element/genericElements/SingleViews/Fields/alignmentField.php @@ -20,7 +20,7 @@ if ($field['scope'] === 'individuals') { h($alignment['organisation']['name']) ), sprintf( - "UI.openModalFromURL(%s);", + "UI.submissionModalForSinglePage(%s);", sprintf( "'/alignments/delete/%s'", $alignment['id'] @@ -40,7 +40,7 @@ if ($field['scope'] === 'individuals') { h($alignment['individual']['email']) ), sprintf( - "UI.openModalFromURL(%s);", + "UI.submissionModalForSinglePage(%s);", sprintf( "'/alignments/delete/%s'", $alignment['id'] @@ -53,7 +53,7 @@ echo sprintf( '
%s
', $alignments, sprintf( - "UI.openModalFromURL('/alignments/add/%s/%s');", + "UI.submissionModalForSinglePage('/alignments/add/%s/%s');", h($field['scope']), h($extracted['id']) ), diff --git a/templates/element/genericElements/side_menu_scaffold.php b/templates/element/genericElements/side_menu_scaffold.php index bd6ab03..5edb5e2 100644 --- a/templates/element/genericElements/side_menu_scaffold.php +++ b/templates/element/genericElements/side_menu_scaffold.php @@ -35,7 +35,7 @@ if (isset($menu[$metaGroup])) { } $active = ($scope === $this->request->getParam('controller') && $action === $this->request->getParam('action')); if (!empty($data['popup'])) { - $link_template = '%s'; + $link_template = '%s'; } else { $link_template = '%s'; } diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 26c6ecb..ec61568 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -25,13 +25,13 @@ class UIFactory { } /** - * Create and display a modal where the modal's content is fetched from the provided URL. + * Create and display a modal where the modal's content is fetched from the provided URL. Link an AJAXApi to the submission button * @param {string} url - The URL from which the modal's content should be fetched * @param {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that handles successful form submission * @param {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that handles form submissions errors and validation errors. * @return {Promise} Promise object resolving to the ModalFactory object */ - modalFromURL(url, POSTSuccessCallback, POSTFailCallback) { + submissionModal(url, POSTSuccessCallback, POSTFailCallback) { return AJAXApi.quickFetchURL(url).then((modalHTML) => { const theModal = new ModalFactory({ rawHtml: modalHTML, @@ -46,45 +46,110 @@ class UIFactory { } /** - * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the table after a successful operation and handles displayOnSuccess option + * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the single page view after a successful operation. + * Supports `displayOnSuccess` option to show another modal after the submission * @param {string} url - The URL from which the modal's content should be fetched - * @param {string} reloadUrl - The URL from which the data should be fetched after confirming - * @param {string} tableId - The table ID which should be reloaded on success + * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming + * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success * @return {Promise} Promise object resolving to the ModalFactory object */ - openModalFromURL(url, reloadUrl=false, tableId=false) { - return UI.modalFromURL(url, (data) => { - let reloaded = false - if (reloadUrl === false || tableId === false) { // Try to get information from the DOM - let $elligibleTable = $('table.table') - let currentModel = location.pathname.split('/')[1] - if ($elligibleTable.length == 1 && currentModel.length > 0) { - let $container = $elligibleTable.closest('div[id^="table-container-"]') - if ($container.length == 1) { - UI.reload(`/${currentModel}/index`, $container, $elligibleTable) - reloaded = true - } else { - $container = $elligibleTable.closest('div[id^="single-view-table-container-"]') - if ($container.length == 1) { - UI.reload(location.pathname, $container, $elligibleTable) - reloaded = true - } - } - } + submissionModalForSinglePage(url, reloadUrl=false, $table=false) { + let $statusNode, $reloadedElement + if (reloadUrl === false) { + reloadUrl = location.pathname + } + if ($table === false) { // Try to get information from the DOM + const $elligibleTable = $('table[id^="single-view-table-"]') + const $container = $elligibleTable.closest('div[id^="single-view-table-container-"]') + $reloadedElement = $container + $statusNode = $elligibleTable + } else { + if ($table instanceof jQuery) { + $reloadedElement = $table + $statusNode = $table.find('table[id^="single-view-table-"]') } else { - UI.reload(reloadUrl, $(`#table-container-${tableId}`), $(`#table-container-${tableId} table.table`)) - reloaded = true + $reloadedElement = $(`single-view-table-container-${$table}`) + $statusNode = $(`single-view-table-${$table}`) } + } + if ($reloadedElement.length == 0) { + UI.Toaster({ + variant: 'danger', + title: 'Could not find element to be reloaded', + body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' + }) + return + } + return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); + } + + /** + * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation. + * Supports `displayOnSuccess` option to show another modal after the submission + * @param {string} url - The URL from which the modal's content should be fetched + * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming + * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success + * @return {Promise} Promise object resolving to the ModalFactory object + */ + submissionModalForIndex(url, reloadUrl=false, $table=false) { + let $statusNode, $reloadedElement + if (reloadUrl === false) { + const currentModel = location.pathname.split('/')[1] + if (currentModel.length > 0) { + reloadUrl = `/${currentModel}/index` + } else { + UI.Toaster({ + variant: 'danger', + title: 'Could not find URL for the reload', + body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' + }) + return + } + } + if ($table === false) { // Try to get information from the DOM + const $elligibleTable = $('table.table') + const $container = $elligibleTable.closest('div[id^="table-container-"]') + $reloadedElement = $container + $statusNode = $elligibleTable + } else { + if ($table instanceof jQuery) { + $reloadedElement = $table + $statusNode = $table.find('table.table') + } else { + $reloadedElement = $(`#table-container-${$table}`) + $statusNode = $(`#table-container-${$table} table.table`) + } + } + if ($reloadedElement.length == 0) { + UI.Toaster({ + variant: 'danger', + title: 'Could not find element to be reloaded', + body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' + }) + return + } + return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); + } + + /** + * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation. + * Supports `displayOnSuccess` option to show another modal after the submission + * @param {string} url - The URL from which the modal's content should be fetched + * @param {string} reloadUrl - The URL from which the data should be fetched after confirming + * @param {(jQuery|string)} $reloadedElement - The element which should be reloaded on success + * @param {(jQuery|string)} [$statusNode=null] - A reference to a HTML node on which the loading animation should be displayed. If not provided, $container will be used + * @return {Promise} Promise object resolving to the ModalFactory object + */ + submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode=null) { + const successCallback = function (data) { + UI.reload(reloadUrl, $reloadedElement, $statusNode) if (data.additionalData !== undefined && data.additionalData.displayOnSuccess !== undefined) { UI.modal({ rawHtml: data.additionalData.displayOnSuccess }) - } else { - if (!reloaded) { - location.reload() - } } - }) + } + return UI.submissionModal(url, successCallback) } /** From a08dfc743416b0a1b440c0d45e29d735497c94dc Mon Sep 17 00:00:00 2001 From: mokaddem Date: Wed, 10 Mar 2021 15:37:19 +0100 Subject: [PATCH 02/19] fix: [js:bootstrap-helper] Coorectly call toasts and documentation precisions --- webroot/js/bootstrap-helper.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index ec61568..8562522 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -38,7 +38,7 @@ class UIFactory { POSTSuccessCallback: POSTSuccessCallback !== undefined ? POSTSuccessCallback : () => {}, POSTFailCallback: POSTFailCallback !== undefined ? POSTFailCallback : (errorMessage) => {}, }); - theModal.makeModal(modalHTML) + theModal.makeModal() theModal.show() theModal.$modal.data('modalObject', theModal) return theModal @@ -73,7 +73,7 @@ class UIFactory { } } if ($reloadedElement.length == 0) { - UI.Toaster({ + UI.toast({ variant: 'danger', title: 'Could not find element to be reloaded', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' @@ -98,7 +98,7 @@ class UIFactory { if (currentModel.length > 0) { reloadUrl = `/${currentModel}/index` } else { - UI.Toaster({ + UI.toast({ variant: 'danger', title: 'Could not find URL for the reload', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' @@ -121,7 +121,7 @@ class UIFactory { } } if ($reloadedElement.length == 0) { - UI.Toaster({ + UI.toast({ variant: 'danger', title: 'Could not find element to be reloaded', body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.' @@ -320,7 +320,7 @@ class Toaster { if (options.body !== false || options.bodyHtml !== false) { var $toastBody if (options.bodyHtml !== false) { - $toastBody = $('
').html(options.mutedHtml) + $toastBody = $('
').html(options.bodyHtml) } else { $toastBody = $('
').text(options.body) } @@ -338,8 +338,15 @@ class ModalFactory { */ constructor(options) { this.options = Object.assign({}, ModalFactory.defaultOptions, options) - if (this.options.rawHtml && options.POSTSuccessCallback !== undefined) { - this.attachSubmitButtonListener = true + if (options.POSTSuccessCallback !== undefined) { + if (this.options.rawHtml) { + this.attachSubmitButtonListener = true + } else { + UI.toast({ + variant: 'danger', + bodyHtml: 'POSTSuccessCallback can only be used in conjuction with the rawHtml option. Instead, use the promise instead returned by the API call in APIConfirm.' + }) + } } if (options.type === undefined && options.cancel !== undefined) { this.options.type = 'confirm' @@ -414,14 +421,14 @@ class ModalFactory { * @property {string} confirmText - The text to be placed in the confirm button * @property {string} cancelText - The text to be placed in the cancel button * @property {boolean} closeManually - If true, the modal will be closed automatically whenever a footer's button is pressed - * @property {boolean} closeOnSuccess - If true, the modal will be closed if the $FILL_ME operation is successful + * @property {boolean} closeOnSuccess - If true, the modal will be closed if the operation is successful * @property {ModalFactory~confirm} confirm - The callback that should be called if the user confirm the modal * @property {ModalFactory~cancel} cancel - The callback that should be called if the user cancel the modal * @property {ModalFactory~APIConfirm} APIConfirm - The callback that should be called if the user confirm the modal. Behaves like the confirm option but provides an AJAXApi object that can be used to issue requests * @property {ModalFactory~APIError} APIError - The callback called if the APIConfirm callback fails. * @property {ModalFactory~shownCallback} shownCallback - The callback that should be called whenever the modal is shown * @property {ModalFactory~hiddenCallback} hiddenCallback - The callback that should be called whenever the modal is hiddenAPIConfirm - * @property {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that should be called if the POST operation has been a success + * @property {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that should be called if the POST operation has been a success. Works in confunction with the `rawHtml` * @property {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that should be called if the POST operation has been a failure (Either the request failed or the form validation did not pass) */ static defaultOptions = { From 5e7811ccaf147dfbf29d830c755ccd01842b82cc Mon Sep 17 00:00:00 2001 From: mokaddem Date: Thu, 11 Mar 2021 09:18:12 +0100 Subject: [PATCH 03/19] chg: [genericTemplates:filters] Slightly improved UI --- templates/genericTemplates/filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index 44c1b63..83290e9 100644 --- a/templates/genericTemplates/filters.php +++ b/templates/genericTemplates/filters.php @@ -15,7 +15,7 @@ $filteringForm = $this->Bootstrap->table( [ 'labelHtml' => sprintf('%s %s', __('Value'), - sprintf('', __('Supports strict match and LIKE match with the `%` character. Example: `%.com`')) + sprintf('', __('Supports strict match and LIKE match with the `%` character. Example: `%.com`')) ) ], __('Action') From 010d0896a978fc651e6c34e622911bf8ae67cadf Mon Sep 17 00:00:00 2001 From: mokaddem Date: Thu, 11 Mar 2021 09:20:03 +0100 Subject: [PATCH 04/19] new: [js:api-helper] Added postData function --- webroot/js/api-helper.js | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 282b1b4..65683a5 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -134,6 +134,18 @@ class AJAXApi { return tmpApi.postForm(form, dataToMerge, constAlteredOptions.skipRequestHooks) } + /** + * @param {string} url - The URL to on which to execute the POST + * @param {Object} [data={}] - The data to be posted + * @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions + * @return {Promise} Promise object resolving to the result of the POST operation + */ + static async quickPostData(url, data={}, options={}) { + const constAlteredOptions = Object.assign({}, {}, options) + const tmpApi = new AJAXApi(constAlteredOptions) + return tmpApi.postData(url, data, constAlteredOptions.skipRequestHooks) + } + /** * @param {string} url - The URL from which to fetch the form * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form @@ -258,6 +270,62 @@ class AJAXApi { return toReturn } + /** + * @param {string} url - The URL to fetch + * @param {Object} dataToPost - data to be posted + * @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped + * @param {boolean} [skipFeedback=false] - Pass this value to the AJAXApi.provideFeedback function + * @return {Promise} Promise object resolving to the result of the POST + */ + async postData(url, dataToPost, skipRequestHooks=false, skipFeedback=false) { + if (!skipRequestHooks) { + this.beforeRequest() + } + let toReturn + try { + let formData = new FormData() + formData = AJAXApi.mergeFormData(formData, dataToPost) + let requestConfig = AJAXApi.genericRequestConfigPOST + let options = { + ...requestConfig, + body: formData, + }; + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`Network response was not ok. \`${response.statusText}\``) + } + const data = await response.json() + if (data.success) { + this.provideFeedback({ + variant: 'success', + body: data.message + }, false, skipFeedback); + toReturn = data; + } else { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: data.message + }, true, skipFeedback); + feedbackShown = true + this.injectFormValidationFeedback(form, data.errors) + toReturn = Promise.reject(data.errors); + } + } catch (error) { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: error.message + }, true, skipFeedback); + toReturn = Promise.reject(error); + } finally { + if (!skipRequestHooks) { + this.afterRequest() + } + } + return toReturn + } + /** * @param {HTMLFormElement} form - The form to be posted * @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form From 77fe4e650569e44944404c4c84302ee68d437604 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Mon, 15 Mar 2021 22:47:13 +0100 Subject: [PATCH 05/19] new: [inbox] First version of Inbox system and requestProcessors - WiP --- .../GenericRequestProcessor.php | 113 ++++++++++++++++ .../UserRequestProcessor.php | 122 ++++++++++++++++++ src/Controller/AppController.php | 7 + src/Controller/Component/ACLComponent.php | 23 ++++ src/Controller/Component/CRUDComponent.php | 4 +- .../Component/RestResponseComponent.php | 5 +- src/Controller/EncryptionKeysController.php | 2 +- src/Controller/InboxController.php | 101 +++++++++++++++ src/Controller/UsersController.php | 20 +++ src/Model/Entity/Inbox.php | 11 ++ src/Model/Table/EncryptionKeysTable.php | 14 +- src/Model/Table/InboxTable.php | 116 +++++++++++++++++ src/Model/Table/IndividualsTable.php | 2 +- src/Model/Table/OrganisationsTable.php | 2 +- src/View/Helper/BootstrapHelper.php | 7 +- templates/EncryptionKeys/add.php | 6 +- templates/EncryptionKeys/index.php | 2 +- templates/Inbox/index.php | 91 +++++++++++++ templates/Inbox/view.php | 50 +++++++ templates/Instance/migration_index.php | 2 +- .../RequestProcessors/User/registration.php | 122 ++++++++++++++++++ .../genericElements/Form/genericForm.php | 18 +++ .../IndexTable/Fields/owner.php | 2 +- .../IndexTable/Fields/user.php | 11 ++ .../ListTopBar/group_search.php | 9 +- .../genericElements/side_menu_scaffold.php | 2 +- templates/genericTemplates/delete.php | 2 +- templates/genericTemplates/filters.php | 2 +- webroot/js/api-helper.js | 3 +- webroot/js/bootstrap-helper.js | 31 ++++- webroot/js/main.js | 11 ++ 31 files changed, 881 insertions(+), 32 deletions(-) create mode 100644 libraries/RequestProcessors/GenericRequestProcessor.php create mode 100644 libraries/RequestProcessors/UserRequestProcessor.php create mode 100644 src/Controller/InboxController.php create mode 100644 src/Model/Entity/Inbox.php create mode 100644 src/Model/Table/InboxTable.php create mode 100644 templates/Inbox/index.php create mode 100644 templates/Inbox/view.php create mode 100644 templates/RequestProcessors/User/registration.php create mode 100644 templates/element/genericElements/IndexTable/Fields/user.php diff --git a/libraries/RequestProcessors/GenericRequestProcessor.php b/libraries/RequestProcessors/GenericRequestProcessor.php new file mode 100644 index 0000000..614de69 --- /dev/null +++ b/libraries/RequestProcessors/GenericRequestProcessor.php @@ -0,0 +1,113 @@ +Inbox = TableRegistry::getTableLocator()->get('Inbox'); + if ($registerActions) { + $this->registerActionInProcessor(); + } + $processingTemplatePath = $this->getProcessingTemplatePath(); + $file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath); + if ($file->exists()) { + $this->processingTemplate = $processingTemplatePath; + } + $file->close(); + } + + private function getProcessingTemplatePath() + { + $class = str_replace('RequestProcessor', '', get_parent_class($this)); + $action = strtolower(str_replace('Processor', '', get_class($this))); + return sprintf('%s/%s.php', + $class, + $action + ); + } + + public function getProcessingTemplate() + { + if ($this->processingTemplate == '/genericTemplates/confirm') { + return '/genericTemplates/confirm'; + } + return DS . 'RequestProcessors' . DS . str_replace('.php', '', $this->processingTemplate); + } + + protected function generateRequest($requestData) + { + $request = $this->Inbox->newEmptyEntity(); + $request = $this->Inbox->patchEntity($request, $requestData); + if ($request->getErrors()) { + throw new MethodNotAllowed(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1); + } + return $request; + } + + protected function validateRequestData($requestData) + { + $errors = []; + if (!isset($requestData['data'])) { + $errors[] = __('No request data provided'); + } + $validator = new Validator(); + if (method_exists($this, 'addValidatorRules')) { + $validator = $this->addValidatorRules($validator); + $errors = $validator->validate($requestData['data']); + } + if (!empty($errors)) { + throw new Exception('Error while validating request data. ' . json_encode($errors), 1); + } + } + + protected function registerActionInProcessor() + { + foreach ($this->registeredActions as $i => $action) { + $className = "{$action}Processor"; + $reflection = new ReflectionClass($className); + if ($reflection->isAbstract() || $reflection->isInterface()) { + throw new Exception(__('Cannot create instance of %s, as it is abstract or is an interface')); + } + $this->{$action} = $reflection->newInstance(); + } + } + + public function checkLoading() + { + return 'Assimilation successful!'; + } + + protected function setViewVariablesConfirmModal($controller, $id, $title='', $question='', $actionName='') + { + $controller->set('title', !empty($title) ? $title : __('Process request {0}', $id)); + $controller->set('question', !empty($question) ? $question : __('Confirm request {0}', $id)); + $controller->set('actionName', !empty($actionName) ? $actionName : __('Confirm')); + $controller->set('path', ['controller' => 'inbox', 'action' => 'process', $id]); + } + + public function create($requestData) + { + $request = $this->generateRequest($requestData); + $savedRequest = $this->Inbox->save($request); + if ($savedRequest !== false) { + // log here + } + } +} diff --git a/libraries/RequestProcessors/UserRequestProcessor.php b/libraries/RequestProcessors/UserRequestProcessor.php new file mode 100644 index 0000000..7c8c3bc --- /dev/null +++ b/libraries/RequestProcessors/UserRequestProcessor.php @@ -0,0 +1,122 @@ +scope; + $requestData['action'] = $this->action; + $requestData['description'] = $this->description; + parent::create($requestData); + } +} + +class RegistrationProcessor extends UserRequestProcessor implements GenericProcessorActionI { + protected $action = 'Registration'; + protected $description; + + public function __construct() { + parent::__construct(); + $this->description = __('Handle user account for this cerebrate instance'); + $this->Users = TableRegistry::getTableLocator()->get('Users'); + } + + protected function addValidatorRules($validator) + { + return $validator + ->requirePresence('username') + ->notEmpty('name', 'A username must be provided.') + ->requirePresence('email') + ->add('email', 'validFormat', [ + 'rule' => 'email', + 'message' => 'E-mail must be valid' + ]) + ->requirePresence('first_name') + ->notEmpty('name', 'A first name must be provided.') + ->requirePresence('last_name') + ->notEmpty('name', 'A last name must be provided.'); + } + + public function create($requestData) { + $this->validateRequestData($requestData); + $requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']); + parent::create($requestData); + } + + public function setViewVariables($controller, $request) + { + $dropdownData = [ + 'role' => $this->Users->Roles->find('list', [ + 'sort' => ['name' => 'asc'] + ]), + 'individual' => [-1 => __('-- New individual --')] + $this->Users->Individuals->find('list', [ + 'sort' => ['email' => 'asc'] + ])->toArray() + ]; + $individualEntity = $this->Users->Individuals->newEntity([ + 'email' => !empty($request['data']['email']) ? $request['data']['email'] : '', + 'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '', + 'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '', + 'position' => !empty($request['data']['position']) ? $request['data']['position'] : '', + ]); + $userEntity = $this->Users->newEntity([ + 'individual_id' => -1, + 'username' => !empty($request['data']['username']) ? $request['data']['username'] : '', + 'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '', + 'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '', + ]); + $controller->set('individualEntity', $individualEntity); + $controller->set('userEntity', $userEntity); + $controller->set(compact('dropdownData')); + } + + public function process($id, $serverRequest) + { + $data = $serverRequest->getData(); + if ($data['individual_id'] == -1) { + $individual = $this->Users->Individuals->newEntity([ + 'uuid' => $data['uuid'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'position' => $data['position'], + ]); + $individual = $this->Users->Individuals->save($individual); + } else { + $individual = $this->Users->Individuals->get($data['individual_id']); + } + $user = $this->Users->newEntity([ + 'individual_id' => $individual->id, + 'username' => $data['username'], + 'password' => '~PASSWORD_TO_BE_REPLACED~', + 'role_id' => $data['role_id'], + 'disabled' => $data['disabled'], + ]); + $user = $this->Users->save($user); + return [ + 'data' => $user, + 'success' => $user !== false, + 'message' => $user !== false ? __('User `{0}` created', $user->username) : __('Could not create user `{0}`.', $user->username), + 'errors' => $user->getErrors() + ]; + } + + public function discard($id) + { + parent::discard($id); + } +} \ No newline at end of file diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index e713f50..dd619ea 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -107,6 +107,13 @@ class AppController extends Controller } else if ($this->ParamHandler->isRest()) { throw new MethodNotAllowedException(__('Invalid user credentials.')); } + + // if ($this->request->getParam('action') === 'index') { + // $this->Security->setConfig('validatePost', false); + // } + $this->Security->setConfig('unlockedActions', ['index']); + $this->Security->setConfig('validatePost', false); + $this->ACL->checkAccess(); $this->set('menu', $this->ACL->getMenu()); $this->set('ajax', $this->request->is('ajax')); diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index e7ff59f..dcd7f31 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -642,6 +642,29 @@ class ACLComponent extends Component ] ] ], + 'Inbox' => [ + 'label' => __('Inbox'), + 'url' => '/inbox/index', + 'children' => [ + 'index' => [ + 'url' => '/inbox/index', + 'label' => __('Inbox') + ], + 'view' => [ + 'url' => '/inbox/view/{{id}}', + 'label' => __('View Meta Template'), + 'actions' => ['delete', 'edit', 'view'], + 'skipTopMenu' => 1 + ], + 'delete' => [ + 'url' => '/inbox/delete/{{id}}', + 'label' => __('Delete Meta Template'), + 'actions' => ['delete', 'edit', 'view'], + 'skipTopMenu' => 1, + 'popup' => 1 + ] + ] + ], 'MetaTemplates' => [ 'label' => __('Meta Field Templates'), 'url' => '/metaTemplates/index', diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index e64470d..5b08801 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -145,7 +145,7 @@ class CRUDComponent extends Component $message = __( '{0} could not be added.{1}', $this->ObjectAlias, - empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) + empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { @@ -485,7 +485,7 @@ class CRUDComponent extends Component if (is_bool($contextFromField)) { $contextFromFieldText = sprintf('%s: %s', $field, $contextFromField ? 'true' : 'false'); } else { - $contextFromFieldText = $contextFromField; + $contextFromFieldText = sprintf('%s: %s', $field, $contextFromField); } $filteringContexts[] = [ 'label' => Inflector::humanize($contextFromFieldText), diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index ef9bf53..055558f 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -423,11 +423,12 @@ class RestResponseComponent extends Component public function ajaxSuccessResponse($ObjectAlias, $action, $entity, $message, $additionalData=[]) { $action = $this->__dissectAdminRouting($action); + $entity = is_array($entity) ? $entity : $entity->toArray(); $response = [ 'success' => true, 'message' => $message, - 'data' => $entity->toArray(), - 'url' => $this->__generateURL($action, $ObjectAlias, $entity->id) + 'data' => $entity, + 'url' => !empty($entity['id']) ? $this->__generateURL($action, $ObjectAlias, $entity['id']) : '' ]; if (!empty($additionalData)) { $response['additionalData'] = $additionalData; diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index afcc611..a9bf699 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -18,7 +18,7 @@ class EncryptionKeysController extends AppController { $this->CRUD->index([ 'quickFilters' => ['encryption_key'], - 'filters' => ['owner_type', 'organisation_id', 'individual_id', 'encryption_key'], + 'filters' => ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'], 'contextFilters' => [ 'fields' => [ 'type' diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php new file mode 100644 index 0000000..dd00a5c --- /dev/null +++ b/src/Controller/InboxController.php @@ -0,0 +1,101 @@ +set('metaGroup', 'Administration'); + } + + + public function index() + { + $this->CRUD->index([ + 'filters' => $this->filters, + 'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]], + 'contextFilters' => [ + 'fields' => [ + 'scope', + 'action', + ] + ], + 'contain' => ['Users'] + ]); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + + public function filtering() + { + $this->CRUD->filtering(); + } + + // public function add() + // { + // $this->CRUD->add(); + // $responsePayload = $this->CRUD->getResponsePayload(); + // if (!empty($responsePayload)) { + // return $responsePayload; + // } + // } + + public function view($id) + { + $this->CRUD->view($id); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + + public function delete($id) + { + $this->CRUD->delete($id); + $responsePayload = $this->CRUD->getResponsePayload(); + if (!empty($responsePayload)) { + return $responsePayload; + } + } + + public function process($id) + { + $request = $this->Inbox->get($id); + $scope = $request->scope; + $action = $request->action; + $processor = $this->Inbox->getRequestProcessor($scope, $action); + if ($this->request->is('post')) { + $processResult = $processor->process($id, $this->request); + if ($processResult['success']) { + $message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} processed.', $id); + $response = $this->RestResponse->ajaxSuccessResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message); + } else { + $message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} could not be processed.', $id); + $response = $this->RestResponse->ajaxFailResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']); + } + return $response; + } else { + $processor->setViewVariables($this, $request); + $processingTemplate = $processor->getProcessingTemplate(); + $this->set('request', $request); + $this->viewBuilder()->setLayout('ajax'); + $this->render($processingTemplate); + } + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index db11453..742e247 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Controller\AppController; use Cake\Utility\Hash; use Cake\Utility\Text; +use Cake\ORM\TableRegistry; use \Cake\Database\Expression\QueryExpression; class UsersController extends AppController @@ -134,4 +135,23 @@ class UsersController extends AppController return $this->redirect(['controller' => 'Users', 'action' => 'login']); } } + + public function register() + { + $this->Inbox = TableRegistry::getTableLocator()->get('Inbox'); + $processor = $this->Inbox->getRequestProcessor('User', 'Registration'); + $data = [ + 'origin' => '127.0.0.1', + 'comment' => 'Hi there!, please create an account', + 'data' => [ + 'username' => 'foobar', + 'email' => 'foobar@admin.test', + 'first_name' => 'foo', + 'last_name' => 'bar', + ], + ]; + $processor->create($data); + $this->Flash->success(__('Entry created')); + return $this->redirect(['controller' => 'Inbox', 'action' => 'index']); + } } diff --git a/src/Model/Entity/Inbox.php b/src/Model/Entity/Inbox.php new file mode 100644 index 0000000..a8f546e --- /dev/null +++ b/src/Model/Entity/Inbox.php @@ -0,0 +1,11 @@ + 'owner_id', - 'conditions' => ['owner_type' => 'individual'] + 'conditions' => ['owner_model' => 'individual'] ] ); $this->belongsTo( 'Organisations', [ 'foreignKey' => 'owner_id', - 'conditions' => ['owner_type' => 'organisation'] + 'conditions' => ['owner_model' => 'organisation'] ] ); $this->setDisplayField('encryption_key'); @@ -34,13 +34,13 @@ class EncryptionKeysTable extends AppTable public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options) { if (empty($data['owner_id'])) { - if (empty($data['owner_type'])) { + if (empty($data['owner_model'])) { return false; } - if (empty($data[$data['owner_type'] . '_id'])) { + if (empty($data[$data['owner_model'] . '_id'])) { return false; } - $data['owner_id'] = $data[$data['owner_type'] . '_id']; + $data['owner_id'] = $data[$data['owner_model'] . '_id']; } } @@ -50,8 +50,8 @@ class EncryptionKeysTable extends AppTable ->notEmptyString('type') ->notEmptyString('encryption_key') ->notEmptyString('owner_id') - ->notEmptyString('owner_type') - ->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_type'], 'create'); + ->notEmptyString('owner_model') + ->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_model'], 'create'); return $validator; } } diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php new file mode 100644 index 0000000..0a04b9a --- /dev/null +++ b/src/Model/Table/InboxTable.php @@ -0,0 +1,116 @@ +addBehavior('UUID'); + $this->addBehavior('Timestamp', [ + 'events' => [ + 'Model.beforeSave' => [ + 'created' => 'new' + ] + ] + ]); + + $this->belongsTo('Users'); + $this->setDisplayField('title'); + } + + protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface + { + $schema->setColumnType('data', 'json'); + + return $schema; + } + + public function validationDefault(Validator $validator): Validator + { + $validator + ->notEmptyString('scope') + ->notEmptyString('action') + ->notEmptyString('title') + ->notEmptyString('origin') + ->datetime('created') + + ->requirePresence([ + 'scope' => ['message' => __('The field `scope` is required')], + 'action' => ['message' => __('The field `action` is required')], + 'title' => ['message' => __('The field `title` is required')], + 'origin' => ['message' => __('The field `origin` is required')], + ], 'create'); + return $validator; + } + + public function buildRules(RulesChecker $rules): RulesChecker + { + $rules->add($rules->existsIn('user_id', 'Users'), [ + 'message' => 'The provided `user_id` does not exist' + ]); + + return $rules; + } + + public function getRequestProcessor($name, $action=null) + { + if (!isset($this->requestProcessors)) { + $this->loadRequestProcessors(); + } + if (isset($this->requestProcessors[$name])) { + if (is_null($action)) { + return $this->requestProcessors[$name]; + } else if (!empty($this->requestProcessors[$name]->{$action})) { + return $this->requestProcessors[$name]->{$action}; + } else { + throw new \Exception(__('Processor {0}.{1} not found', $name, $action)); + } + } + throw new \Exception(__('Processor not found'), 1); + } + + private function loadRequestProcessors() + { + $processorDir = new Folder($this->processorsDirectory); + $processorFiles = $processorDir->find('.*RequestProcessor\.php', true); + foreach ($processorFiles as $processorFile) { + if ($processorFile == 'GenericRequestProcessor.php') { + continue; + } + $processorMainClassName = str_replace('.php', '', $processorFile); + $processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile); + $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName); + if ($processorMainClass !== false) { + $this->requestProcessors[$processorMainClassNameShort] = $processorMainClass; + } + } + } + + private function getProcessorClass($filePath, $processorMainClassName) + { + require_once($filePath); + $reflection = new \ReflectionClass($processorMainClassName); + $processorMainClass = $reflection->newInstance(true); + if ($processorMainClass->checkLoading() === 'Assimilation successful!') { + return $processorMainClass; + } + try { + } catch (Exception $e) { + return false; + } + } +} diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 1611a5f..d0df577 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -25,7 +25,7 @@ class IndividualsTable extends AppTable 'EncryptionKeys', [ 'foreignKey' => 'owner_id', - 'conditions' => ['owner_type' => 'individual'] + 'conditions' => ['owner_model' => 'individual'] ] ); $this->hasOne( diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index 446d3fa..c23ee7b 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -30,7 +30,7 @@ class OrganisationsTable extends AppTable [ 'dependent' => true, 'foreignKey' => 'owner_id', - 'conditions' => ['owner_type' => 'organisation'] + 'conditions' => ['owner_model' => 'organisation'] ] ); $this->hasMany( diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 2309d87..8f77a5d 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -822,7 +822,7 @@ class BoostrapModal extends BootstrapGeneric { 'variant' => 'primary', 'text' => __('Ok'), 'params' => [ - 'data-dismiss' => 'modal', + 'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal', 'onclick' => $this->options['confirmFunction'] ] ]))->button(); @@ -848,8 +848,9 @@ class BoostrapModal extends BootstrapGeneric { 'variant' => $variant, 'text' => h($this->options['confirmText']), 'params' => [ - 'data-dismiss' => 'modal', - 'onclick' => $this->options['confirmFunction'] + 'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal', + // 'onclick' => sprintf('(function(clicked) { %s.finally( () => { $(clicked).closest(\'.modal\').data(\'modalObject\').hide() }) }(this))', $this->options['confirmFunction']) + 'onclick' => sprintf('closeModalOnFunctionCompletion(this, function(clicked) { return %s })', $this->options['confirmFunction']) ] ]))->button(); return $buttonCancel . $buttonConfirm; diff --git a/templates/EncryptionKeys/add.php b/templates/EncryptionKeys/add.php index 6a80c39..3f1800e 100644 --- a/templates/EncryptionKeys/add.php +++ b/templates/EncryptionKeys/add.php @@ -6,7 +6,7 @@ echo $this->element('genericElements/Form/genericForm', [ 'model' => 'Organisations', 'fields' => [ [ - 'field' => 'owner_type', + 'field' => 'owner_model', 'label' => __('Owner type'), 'options' => array_combine(array_keys($dropdownData), array_keys($dropdownData)), 'type' => 'dropdown' @@ -17,7 +17,7 @@ echo $this->element('genericElements/Form/genericForm', [ 'options' => $dropdownData['organisation'] ?? [], 'type' => 'dropdown', 'stateDependence' => [ - 'source' => '#owner_type-field', + 'source' => '#owner_model-field', 'option' => 'organisation' ] ], @@ -27,7 +27,7 @@ echo $this->element('genericElements/Form/genericForm', [ 'options' => $dropdownData['individual'] ?? [], 'type' => 'dropdown', 'stateDependence' => [ - 'source' => '#owner_type-field', + 'source' => '#owner_model-field', 'option' => 'individual' ] ], diff --git a/templates/EncryptionKeys/index.php b/templates/EncryptionKeys/index.php index fed2abd..5017319 100644 --- a/templates/EncryptionKeys/index.php +++ b/templates/EncryptionKeys/index.php @@ -45,7 +45,7 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'name' => __('Owner'), 'data_path' => 'owner_id', - 'owner_type_path' => 'owner_type', + 'owner_model_path' => 'owner_model', 'element' => 'owner' ], [ diff --git a/templates/Inbox/index.php b/templates/Inbox/index.php new file mode 100644 index 0000000..e43d0e1 --- /dev/null +++ b/templates/Inbox/index.php @@ -0,0 +1,91 @@ +Html->scriptBlock(sprintf( + 'var csrfToken = %s;', + json_encode($this->request->getAttribute('csrfToken')) +)); +echo $this->element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'children' => [ + [ + 'type' => 'context_filters', + 'context_filters' => !empty($filteringContexts) ? $filteringContexts : [] + ], + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'value', + 'allowFilering' => true + ] + ] + ], + 'fields' => [ + [ + 'name' => '#', + 'sort' => 'id', + 'data_path' => 'id', + ], + [ + 'name' => 'created', + 'sort' => 'created', + 'data_path' => 'created', + 'element' => 'datetime' + ], + [ + 'name' => 'scope', + 'sort' => 'scope', + 'data_path' => 'scope', + ], + [ + 'name' => 'action', + 'sort' => 'action', + 'data_path' => 'action', + ], + [ + 'name' => 'title', + 'sort' => 'title', + 'data_path' => 'title', + ], + [ + 'name' => 'origin', + 'sort' => 'origin', + 'data_path' => 'origin', + ], + [ + 'name' => 'user', + 'sort' => 'user_id', + 'data_path' => 'user', + 'element' => 'user' + ], + [ + 'name' => 'description', + 'sort' => 'description', + 'data_path' => 'description', + ], + [ + 'name' => 'comment', + 'sort' => 'comment', + 'data_path' => 'comment', + ], + ], + 'title' => __('Inbox'), + 'description' => __('A list of requests to be manually processed'), + 'actions' => [ + [ + 'open_modal' => '/inbox/process/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'eye' + ], + [ + 'open_modal' => '/individuals/delete/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'trash' + ], + ] + ] +]); +echo ''; +?> diff --git a/templates/Inbox/view.php b/templates/Inbox/view.php new file mode 100644 index 0000000..89ccc8e --- /dev/null +++ b/templates/Inbox/view.php @@ -0,0 +1,50 @@ +element( + '/genericElements/SingleViews/single_view', + [ + 'data' => $entity, + 'fields' => [ + [ + 'key' => __('ID'), + 'path' => 'id' + ], + [ + 'key' => 'created', + 'path' => 'created', + ], + [ + 'key' => 'scope', + 'path' => 'scope', + ], + [ + 'key' => 'action', + 'path' => 'action', + ], + [ + 'key' => 'title', + 'path' => 'title', + ], + [ + 'key' => 'origin', + 'path' => 'origin', + ], + [ + 'key' => 'user_id', + 'path' => 'user_id', + ], + [ + 'key' => 'description', + 'path' => 'description', + ], + [ + 'key' => 'comment', + 'path' => 'comment', + ], + [ + 'key' => 'data', + 'path' => 'data', + ], + ], + 'children' => [] + ] +); diff --git a/templates/Instance/migration_index.php b/templates/Instance/migration_index.php index b554dd1..f2bef2e 100644 --- a/templates/Instance/migration_index.php +++ b/templates/Instance/migration_index.php @@ -51,7 +51,7 @@ function runAllUpdate() { type: 'confirm-success', confirmText: '', APIConfirm: (tmpApi) => { - tmpApi.fetchAndPostForm(url, {}).then(() => { + return tmpApi.fetchAndPostForm(url, {}).then(() => { location.reload() }) }, diff --git a/templates/RequestProcessors/User/registration.php b/templates/RequestProcessors/User/registration.php new file mode 100644 index 0000000..28e84e0 --- /dev/null +++ b/templates/RequestProcessors/User/registration.php @@ -0,0 +1,122 @@ +element('genericElements/Form/genericForm', [ + 'entity' => $individualEntity, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'description' => __('Create individual'), + 'model' => 'Individual', + 'fields' => [ + [ + 'field' => 'email', + 'autocomplete' => 'off' + ], + [ + 'field' => 'uuid', + 'label' => 'UUID', + 'type' => 'uuid', + 'autocomplete' => 'off' + ], + [ + 'field' => 'first_name', + 'autocomplete' => 'off' + ], + [ + 'field' => 'last_name', + 'autocomplete' => 'off' + ], + [ + 'field' => 'position', + 'autocomplete' => 'off' + ], + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] + ]); + + $formUser = $this->element('genericElements/Form/genericForm', [ + 'entity' => $userEntity, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'description' => __('Create user account'), + 'model' => 'User', + 'fields' => [ + [ + 'field' => 'individual_id', + 'type' => 'dropdown', + 'label' => __('Associated individual'), + 'options' => $dropdownData['individual'], + ], + [ + 'field' => 'username', + 'autocomplete' => 'off', + ], + [ + 'field' => 'role_id', + 'type' => 'dropdown', + 'label' => __('Role'), + 'options' => $dropdownData['role'] + ], + [ + 'field' => 'disabled', + 'type' => 'checkbox', + 'label' => 'Disable' + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] + ]); + + echo $this->Bootstrap->modal([ + 'title' => __('Register user'), + 'size' => 'lg', + 'type' => 'confirm', + 'bodyHtml' => sprintf('
%s
%s
', + $formIndividual, + $formUser + ), + 'confirmText' => __('Submit'), + 'confirmFunction' => 'submitRegistration(clicked)' + ]); +?> + + + diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index 6e642ff..9e3ba2e 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -115,6 +115,24 @@ 'actionButton' => $this->element('genericElements/Form/submitButton', $submitButtonData), 'class' => 'modal-lg' ]); + } else if (!empty($raw)) { + echo sprintf( + '%s%s%s%s%s%s', + empty($data['description']) ? '' : sprintf( + '
%s
', + $data['description'] + ), + $ajaxFlashMessage, + $formCreate, + $fieldsString, + empty($metaTemplateString) ? '' : $this->element( + 'genericElements/accordion_scaffold', [ + 'body' => $metaTemplateString, + 'title' => 'Meta fields' + ] + ), + $formEnd + ); } else { echo sprintf( '%s

%s

%s%s%s%s%s%s%s%s%s', diff --git a/templates/element/genericElements/IndexTable/Fields/owner.php b/templates/element/genericElements/IndexTable/Fields/owner.php index 17d491f..aa16e41 100644 --- a/templates/element/genericElements/IndexTable/Fields/owner.php +++ b/templates/element/genericElements/IndexTable/Fields/owner.php @@ -1,5 +1,5 @@ Hash->extract($row, $field['owner_type_path'])[0]; + $type = $this->Hash->extract($row, $field['owner_model_path'])[0]; $owner = $row[$type]; $types = [ 'individual' => [ diff --git a/templates/element/genericElements/IndexTable/Fields/user.php b/templates/element/genericElements/IndexTable/Fields/user.php new file mode 100644 index 0000000..25504fc --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/user.php @@ -0,0 +1,11 @@ +Hash->extract($row, 'user.id')[0]; + $userName = $this->Hash->extract($row, 'user.username')[0]; + echo $this->Html->link( + h($userName), + ['controller' => 'users', 'action' => 'view', $userId] + ); + } + +?> diff --git a/templates/element/genericElements/ListTopBar/group_search.php b/templates/element/genericElements/ListTopBar/group_search.php index f405640..4359a43 100644 --- a/templates/element/genericElements/ListTopBar/group_search.php +++ b/templates/element/genericElements/ListTopBar/group_search.php @@ -12,6 +12,7 @@ * - id: element ID for the input field - defaults to quickFilterField */ if (!isset($data['requirement']) || $data['requirement']) { + $filterEffective = !empty($quickFilter); // No filters will be picked up, thus rendering the filtering useless $filteringButton = ''; if (!empty($data['allowFilering'])) { $activeFilters = !empty($activeFilters) ? $activeFilters : []; @@ -32,9 +33,10 @@ $filteringButton = $this->Bootstrap->button($buttonConfig); } $button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf( - '
%s
', + '
%s
', empty($data['data']) ? '' : h($data['data']), h($tableRandomValue), + $filterEffective ? '' : 'disabled="disabled"', empty($data['fa-icon']) ? '' : sprintf('', h($data['fa-icon'])), empty($data['button']) ? '' : h($data['button']), $filteringButton @@ -43,13 +45,14 @@ $button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel'])); } $input = sprintf( - '', + '', h($tableRandomValue), empty($data['placeholder']) ? '' : h($data['placeholder']), empty($data['placeholder']) ? '' : h($data['placeholder']), empty($data['id']) ? 'quickFilterField' : h($data['id']), empty($data['searchKey']) ? 'searchall' : h($data['searchKey']), - empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value']) + empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value']), + $filterEffective ? '' : 'disabled="disabled"' ); echo sprintf( '
%s%s
', diff --git a/templates/element/genericElements/side_menu_scaffold.php b/templates/element/genericElements/side_menu_scaffold.php index 5edb5e2..9e8eab5 100644 --- a/templates/element/genericElements/side_menu_scaffold.php +++ b/templates/element/genericElements/side_menu_scaffold.php @@ -35,7 +35,7 @@ if (isset($menu[$metaGroup])) { } $active = ($scope === $this->request->getParam('controller') && $action === $this->request->getParam('action')); if (!empty($data['popup'])) { - $link_template = '%s'; + $link_template = '%s'; } else { $link_template = '%s'; } diff --git a/templates/genericTemplates/delete.php b/templates/genericTemplates/delete.php index 7da0be7..8e008bd 100644 --- a/templates/genericTemplates/delete.php +++ b/templates/genericTemplates/delete.php @@ -17,7 +17,7 @@ Form->postLink( 'Delete', (empty($postLinkParameters) ? ['action' => 'delete', $id] : $postLinkParameters), - ['class' => 'btn btn-primary button-execute', 'id' => 'submitButton'] + ['class' => 'btn btn-danger button-execute', 'id' => 'submitButton'] ) ?> diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php index 83290e9..1e28173 100644 --- a/templates/genericTemplates/filters.php +++ b/templates/genericTemplates/filters.php @@ -30,7 +30,7 @@ echo $this->Bootstrap->modal([ 'type' => 'confirm', 'bodyHtml' => $filteringForm, 'confirmText' => __('Filter'), - 'confirmFunction' => 'filterIndex(this)' + 'confirmFunction' => 'filterIndex(clicked)' ]); ?> diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 65683a5..5d6e552 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -286,6 +286,7 @@ class AJAXApi { let formData = new FormData() formData = AJAXApi.mergeFormData(formData, dataToPost) let requestConfig = AJAXApi.genericRequestConfigPOST + requestConfig.headers.append('AUTHORIZATION', '~HACKY-HACK~') let options = { ...requestConfig, body: formData, @@ -307,8 +308,6 @@ class AJAXApi { title: 'There has been a problem with the operation', body: data.message }, true, skipFeedback); - feedbackShown = true - this.injectFormValidationFeedback(form, data.errors) toReturn = Promise.reject(data.errors); } } catch (error) { diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 8562522..e9e063b 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -131,6 +131,35 @@ class UIFactory { return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode); } + /** + * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation. + * Supports `displayOnSuccess` option to show another modal after the submission + * @param {string} url - The URL from which the modal's content should be fetched + * @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming + * @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success + * @return {Promise} Promise object resolving to the ModalFactory object + */ + submissionModalAutoGuess(url, reloadUrl=false, $table=false) { + let currentAction = location.pathname.split('/')[2] + currentAction += 'cdsc' + if (currentAction !== undefined) { + if (currentAction === 'index') { + return UI.submissionModalForIndex(url, reloadUrl, $table) + } else if (currentAction === 'view') { + return UI.submissionModalForSinglePage(url, reloadUrl, $table) + } + } + const successCallback = () => { + UI.toast({ + variant: 'danger', + title: 'Could not reload the page', + body: 'Reloading the page manually is advised.' + }) + } + return UI.submissionModal(url, successCallback) + } + + /** * Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation. * Supports `displayOnSuccess` option to show another modal after the submission @@ -322,7 +351,7 @@ class Toaster { if (options.bodyHtml !== false) { $toastBody = $('
').html(options.bodyHtml) } else { - $toastBody = $('
').text(options.body) + $toastBody = $('
').append($('
').text(options.body)) } $toast.append($toastBody) } diff --git a/webroot/js/main.js b/webroot/js/main.js index 3e05d3a..3bc56d4 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -66,6 +66,17 @@ function attachTestConnectionResultHtml(result, $container) { return $testResultDiv } +function closeModalOnFunctionCompletion(clicked, fun) { + const result = fun(clicked) + if (result === undefined) { + $(clicked).closest('.modal').data('modalObject').hide() + } else { + result.finally( () => { + $(clicked).closest('.modal').data('modalObject').hide() + }) + } +} + var UI $(document).ready(() => { if (typeof UIFactory !== "undefined") { From 1729d4b6ede44315f3b4022916e6119e25bc04f7 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Tue, 16 Mar 2021 08:45:37 +0100 Subject: [PATCH 06/19] chg: [requestProcessor:user-registration] Slightly improved UI --- .../UserRequestProcessor.php | 10 +-- .../RequestProcessors/User/registration.php | 86 ++++++++++--------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/libraries/RequestProcessors/UserRequestProcessor.php b/libraries/RequestProcessors/UserRequestProcessor.php index 7c8c3bc..8cb805c 100644 --- a/libraries/RequestProcessors/UserRequestProcessor.php +++ b/libraries/RequestProcessors/UserRequestProcessor.php @@ -38,17 +38,13 @@ class RegistrationProcessor extends UserRequestProcessor implements GenericProce protected function addValidatorRules($validator) { return $validator - ->requirePresence('username') - ->notEmpty('name', 'A username must be provided.') - ->requirePresence('email') + ->notEmpty('username', 'A username must be provided.') ->add('email', 'validFormat', [ 'rule' => 'email', 'message' => 'E-mail must be valid' ]) - ->requirePresence('first_name') - ->notEmpty('name', 'A first name must be provided.') - ->requirePresence('last_name') - ->notEmpty('name', 'A last name must be provided.'); + ->notEmpty('first_name', 'A first name must be provided') + ->notEmpty('last_name', 'A last name must be provided'); } public function create($requestData) { diff --git a/templates/RequestProcessors/User/registration.php b/templates/RequestProcessors/User/registration.php index 28e84e0..7981add 100644 --- a/templates/RequestProcessors/User/registration.php +++ b/templates/RequestProcessors/User/registration.php @@ -1,4 +1,40 @@ element('genericElements/Form/genericForm', [ + 'entity' => $userEntity, + 'ajax' => false, + 'raw' => true, + 'data' => [ + 'description' => __('Create user account'), + 'model' => 'User', + 'fields' => [ + [ + 'field' => 'individual_id', + 'type' => 'dropdown', + 'label' => __('Associated individual'), + 'options' => $dropdownData['individual'], + ], + [ + 'field' => 'username', + 'autocomplete' => 'off', + ], + [ + 'field' => 'role_id', + 'type' => 'dropdown', + 'label' => __('Role'), + 'options' => $dropdownData['role'] + ], + [ + 'field' => 'disabled', + 'type' => 'checkbox', + 'label' => 'Disable' + ] + ], + 'submit' => [ + 'action' => $this->request->getParam('action') + ] + ] + ]); + $formIndividual = $this->element('genericElements/Form/genericForm', [ 'entity' => $individualEntity, 'ajax' => false, @@ -36,51 +72,15 @@ ] ]); - $formUser = $this->element('genericElements/Form/genericForm', [ - 'entity' => $userEntity, - 'ajax' => false, - 'raw' => true, - 'data' => [ - 'description' => __('Create user account'), - 'model' => 'User', - 'fields' => [ - [ - 'field' => 'individual_id', - 'type' => 'dropdown', - 'label' => __('Associated individual'), - 'options' => $dropdownData['individual'], - ], - [ - 'field' => 'username', - 'autocomplete' => 'off', - ], - [ - 'field' => 'role_id', - 'type' => 'dropdown', - 'label' => __('Role'), - 'options' => $dropdownData['role'] - ], - [ - 'field' => 'disabled', - 'type' => 'checkbox', - 'label' => 'Disable' - ] - ], - 'submit' => [ - 'action' => $this->request->getParam('action') - ] - ] - ]); - echo $this->Bootstrap->modal([ 'title' => __('Register user'), 'size' => 'lg', 'type' => 'confirm', - 'bodyHtml' => sprintf('
%s
%s
', - $formIndividual, - $formUser + 'bodyHtml' => sprintf('
%s
%s
', + $formUser, + $formIndividual ), - 'confirmText' => __('Submit'), + 'confirmText' => __('Create user'), 'confirmFunction' => 'submitRegistration(clicked)' ]); ?> @@ -120,3 +120,9 @@ }, {}) } + + \ No newline at end of file From 414ac9a59fc7daa1999b8ab80c20dc3d711711f1 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Thu, 18 Mar 2021 08:51:11 +0100 Subject: [PATCH 07/19] chg: [requestProcessor] Refactoring code organisation --- .../GenericRequestProcessor.php | 16 +++- .../UserRequestProcessor.php | 9 +-- src/Controller/InboxController.php | 54 +++++++++---- src/Controller/UsersController.php | 4 +- src/Model/Table/InboxTable.php | 51 ------------ src/Model/Table/RequestProcessorTable.php | 81 +++++++++++++++++++ templates/genericTemplates/index_simple.php | 12 +++ 7 files changed, 149 insertions(+), 78 deletions(-) create mode 100644 src/Model/Table/RequestProcessorTable.php create mode 100644 templates/genericTemplates/index_simple.php diff --git a/libraries/RequestProcessors/GenericRequestProcessor.php b/libraries/RequestProcessors/GenericRequestProcessor.php index 614de69..8f1893d 100644 --- a/libraries/RequestProcessors/GenericRequestProcessor.php +++ b/libraries/RequestProcessors/GenericRequestProcessor.php @@ -14,7 +14,7 @@ interface GenericProcessorActionI class GenericRequestProcessor { - public $Inbox; + protected $Inbox; protected $registeredActions = []; protected $validator; private $processingTemplate = '/genericTemplates/confirm'; @@ -56,7 +56,7 @@ class GenericRequestProcessor $request = $this->Inbox->newEmptyEntity(); $request = $this->Inbox->patchEntity($request, $requestData); if ($request->getErrors()) { - throw new MethodNotAllowed(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1); + throw new Exception(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1); } return $request; } @@ -103,6 +103,18 @@ class GenericRequestProcessor } public function create($requestData) + { + $requestData['scope'] = $this->scope; + $requestData['action'] = $this->action; + $requestData['description'] = $this->description; + $request = $this->generateRequest($requestData); + $savedRequest = $this->Inbox->save($request); + if ($savedRequest !== false) { + // log here + } + } + + public function discard($requestData) { $request = $this->generateRequest($requestData); $savedRequest = $this->Inbox->save($request); diff --git a/libraries/RequestProcessors/UserRequestProcessor.php b/libraries/RequestProcessors/UserRequestProcessor.php index 8cb805c..230e2fe 100644 --- a/libraries/RequestProcessors/UserRequestProcessor.php +++ b/libraries/RequestProcessors/UserRequestProcessor.php @@ -6,8 +6,8 @@ require_once(ROOT . DS . 'libraries' . DS . 'RequestProcessors' . DS . 'GenericR class UserRequestProcessor extends GenericRequestProcessor { protected $scope = 'User'; - protected $action = 'overridden-in-processor-action'; - protected $description = 'overridden-in-processor-action'; + protected $action = 'not-specified'; //overriden when extending + protected $description = ''; // overriden when extending protected $registeredActions = [ 'Registration' ]; @@ -18,15 +18,12 @@ class UserRequestProcessor extends GenericRequestProcessor public function create($requestData) { - $requestData['scope'] = $this->scope; - $requestData['action'] = $this->action; - $requestData['description'] = $this->description; parent::create($requestData); } } class RegistrationProcessor extends UserRequestProcessor implements GenericProcessorActionI { - protected $action = 'Registration'; + public $action = 'Registration'; protected $description; public function __construct() { diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index dd00a5c..92f8dab 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -3,10 +3,11 @@ namespace App\Controller; use App\Controller\AppController; -use Cake\Utility\Hash; -use Cake\Utility\Text; use Cake\Database\Expression\QueryExpression; use Cake\Event\EventInterface; +use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; +use Cake\Utility\Text; use Cake\Http\Exception\NotFoundException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\ForbiddenException; @@ -47,15 +48,6 @@ class InboxController extends AppController $this->CRUD->filtering(); } - // public function add() - // { - // $this->CRUD->add(); - // $responsePayload = $this->CRUD->getResponsePayload(); - // if (!empty($responsePayload)) { - // return $responsePayload; - // } - // } - public function view($id) { $this->CRUD->view($id); @@ -79,7 +71,8 @@ class InboxController extends AppController $request = $this->Inbox->get($id); $scope = $request->scope; $action = $request->action; - $processor = $this->Inbox->getRequestProcessor($scope, $action); + $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); + $processor = $this->requestProcessor->getProcessor($scope, $action); if ($this->request->is('post')) { $processResult = $processor->process($id, $this->request); if ($processResult['success']) { @@ -91,11 +84,38 @@ class InboxController extends AppController } return $response; } else { - $processor->setViewVariables($this, $request); - $processingTemplate = $processor->getProcessingTemplate(); - $this->set('request', $request); - $this->viewBuilder()->setLayout('ajax'); - $this->render($processingTemplate); + $this->requestProcessor->render($this, $processor, $request); } } + + public function listProcessors() + { + $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); + $requestProcessors = $this->requestProcessor->listProcessors(); + if ($this->ParamHandler->isRest()) { + return $this->RestResponse->viewData($requestProcessors, 'json'); + } + $data = []; + foreach ($requestProcessors as $scope => $processors) { + foreach ($processors as $processor) { + $data[] = [ + 'scope' => $scope, + 'action' => $processor->action + ]; + } + } + $this->set('title', 'Available request processors'); + $this->set('fields', [ + [ + 'name' => 'Processor scope', + 'data_path' => 'scope', + ], + [ + 'name' => 'Processor action', + 'data_path' => 'action', + ] + ]); + $this->set('data', $data); + $this->render('/genericTemplates/index_simple'); + } } diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 742e247..f5a4597 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -138,8 +138,8 @@ class UsersController extends AppController public function register() { - $this->Inbox = TableRegistry::getTableLocator()->get('Inbox'); - $processor = $this->Inbox->getRequestProcessor('User', 'Registration'); + $this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor'); + $processor = $this->requestProcessor->getProcessor('User', 'Registration'); $data = [ 'origin' => '127.0.0.1', 'comment' => 'Hi there!, please create an account', diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php index 0a04b9a..93a7f22 100644 --- a/src/Model/Table/InboxTable.php +++ b/src/Model/Table/InboxTable.php @@ -4,7 +4,6 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\Database\Schema\TableSchemaInterface; use Cake\Database\Type; -use Cake\Filesystem\Folder; use Cake\ORM\Table; use Cake\ORM\RulesChecker; use Cake\Validation\Validator; @@ -13,8 +12,6 @@ Type::map('json', 'Cake\Database\Type\JsonType'); class InboxTable extends AppTable { - private $processorsDirectory = ROOT . '/libraries/RequestProcessors'; - private $requestProcessors; public function initialize(array $config): void { @@ -65,52 +62,4 @@ class InboxTable extends AppTable return $rules; } - - public function getRequestProcessor($name, $action=null) - { - if (!isset($this->requestProcessors)) { - $this->loadRequestProcessors(); - } - if (isset($this->requestProcessors[$name])) { - if (is_null($action)) { - return $this->requestProcessors[$name]; - } else if (!empty($this->requestProcessors[$name]->{$action})) { - return $this->requestProcessors[$name]->{$action}; - } else { - throw new \Exception(__('Processor {0}.{1} not found', $name, $action)); - } - } - throw new \Exception(__('Processor not found'), 1); - } - - private function loadRequestProcessors() - { - $processorDir = new Folder($this->processorsDirectory); - $processorFiles = $processorDir->find('.*RequestProcessor\.php', true); - foreach ($processorFiles as $processorFile) { - if ($processorFile == 'GenericRequestProcessor.php') { - continue; - } - $processorMainClassName = str_replace('.php', '', $processorFile); - $processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile); - $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName); - if ($processorMainClass !== false) { - $this->requestProcessors[$processorMainClassNameShort] = $processorMainClass; - } - } - } - - private function getProcessorClass($filePath, $processorMainClassName) - { - require_once($filePath); - $reflection = new \ReflectionClass($processorMainClassName); - $processorMainClass = $reflection->newInstance(true); - if ($processorMainClass->checkLoading() === 'Assimilation successful!') { - return $processorMainClass; - } - try { - } catch (Exception $e) { - return false; - } - } } diff --git a/src/Model/Table/RequestProcessorTable.php b/src/Model/Table/RequestProcessorTable.php new file mode 100644 index 0000000..25485a7 --- /dev/null +++ b/src/Model/Table/RequestProcessorTable.php @@ -0,0 +1,81 @@ +loadProcessors(); + } + + public function getProcessor($scope, $action=null) + { + if (isset($this->requestProcessors[$scope])) { + if (is_null($action)) { + return $this->requestProcessors[$scope]; + } else if (!empty($this->requestProcessors[$scope]->{$action})) { + return $this->requestProcessors[$scope]->{$action}; + } else { + throw new \Exception(__('Processor {0}.{1} not found', $scope, $action)); + } + } + throw new \Exception(__('Processor not found'), 1); + } + + public function render($controller, $processor, $request=[]) + { + $processor->setViewVariables($controller, $request); + $controller->set('request', $request); + $controller->viewBuilder()->setLayout('ajax'); + $processingTemplate = $processor->getProcessingTemplate(); + $controller->render($processingTemplate); + } + + public function listProcessors($scope=null) + { + if (is_null($scope)) { + return $this->requestProcessors; + } else { + if (isset($this->requestProcessors[$scope])) { + return $this->requestProcessors[$scope]; + } else { + throw new \Exception(__('Processors for {0} not found', $scope)); + } + } + } + + private function loadProcessors() + { + $processorDir = new Folder($this->processorsDirectory); + $processorFiles = $processorDir->find('.*RequestProcessor\.php', true); + foreach ($processorFiles as $processorFile) { + if ($processorFile == 'GenericRequestProcessor.php') { + continue; + } + $processorMainClassName = str_replace('.php', '', $processorFile); + $processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile); + $processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName); + if ($processorMainClass !== false) { + $this->requestProcessors[$processorMainClassNameShort] = $processorMainClass; + } + } + } + + private function getProcessorClass($filePath, $processorMainClassName) + { + require_once($filePath); + $reflection = new \ReflectionClass($processorMainClassName); + $processorMainClass = $reflection->newInstance(true); + if ($processorMainClass->checkLoading() === 'Assimilation successful!') { + return $processorMainClass; + } + } +} diff --git a/templates/genericTemplates/index_simple.php b/templates/genericTemplates/index_simple.php new file mode 100644 index 0000000..d0f2c0b --- /dev/null +++ b/templates/genericTemplates/index_simple.php @@ -0,0 +1,12 @@ +element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'skip_pagination' => true, + 'data' => !empty($data) ? $data : [], + 'top_bar' => [], + 'fields' => !empty($fields) ? $fields : [], + 'title' => !empty($title) ? h($title) : __('Index'), + 'description' => !empty($description) ? h($description) : '', + 'actions' => !empty($actions) ? $actions : [] + ], +]); \ No newline at end of file From 0a1294bbeed88dc3ee91c7addce42e8c3f5e190e Mon Sep 17 00:00:00 2001 From: mokaddem Date: Thu, 18 Mar 2021 09:26:01 +0100 Subject: [PATCH 08/19] chg: [inbox] Improved layouts --- src/Controller/InboxController.php | 3 +++ templates/Inbox/index.php | 11 ++++++++--- templates/Inbox/view.php | 1 + .../genericElements/SingleViews/Fields/jsonField.php | 3 +++ templates/genericTemplates/delete.php | 10 ++++++++-- 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 templates/element/genericElements/SingleViews/Fields/jsonField.php diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 92f8dab..8179c64 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -59,6 +59,9 @@ class InboxController extends AppController public function delete($id) { + $this->set('deletionTitle', __('Discard request')); + $this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id)); + $this->set('deletionConfirm', __('Discard')); $this->CRUD->delete($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/templates/Inbox/index.php b/templates/Inbox/index.php index e43d0e1..fd550a8 100644 --- a/templates/Inbox/index.php +++ b/templates/Inbox/index.php @@ -75,12 +75,17 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'description' => __('A list of requests to be manually processed'), 'actions' => [ [ - 'open_modal' => '/inbox/process/[onclick_params_data_path]', - 'modal_params_data_path' => 'id', + 'url' => '/inbox/view', + 'url_params_data_paths' => ['id'], 'icon' => 'eye' ], [ - 'open_modal' => '/individuals/delete/[onclick_params_data_path]', + 'open_modal' => '/inbox/process/[onclick_params_data_path]', + 'modal_params_data_path' => 'id', + 'icon' => 'cogs' + ], + [ + 'open_modal' => '/inbox/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', 'icon' => 'trash' ], diff --git a/templates/Inbox/view.php b/templates/Inbox/view.php index 89ccc8e..36e0dfc 100644 --- a/templates/Inbox/view.php +++ b/templates/Inbox/view.php @@ -43,6 +43,7 @@ echo $this->element( [ 'key' => 'data', 'path' => 'data', + 'type' => 'json' ], ], 'children' => [] diff --git a/templates/element/genericElements/SingleViews/Fields/jsonField.php b/templates/element/genericElements/SingleViews/Fields/jsonField.php new file mode 100644 index 0000000..9e6a687 --- /dev/null +++ b/templates/element/genericElements/SingleViews/Fields/jsonField.php @@ -0,0 +1,3 @@ +%s', h(json_encode($value, JSON_PRETTY_PRINT))); diff --git a/templates/genericTemplates/delete.php b/templates/genericTemplates/delete.php index 8e008bd..6b07d6a 100644 --- a/templates/genericTemplates/delete.php +++ b/templates/genericTemplates/delete.php @@ -1,7 +1,13 @@