From 79e34052c82f331824d20544009a1bfaf4a69a55 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Thu, 10 Dec 2020 15:18:02 +0100 Subject: [PATCH] new: [app] Lots of new helpers for views, js and genericElements --- src/Controller/Component/CRUDComponent.php | 22 +- .../Component/RestResponseComponent.php | 4 +- src/Controller/IndividualsController.php | 2 + src/Controller/MetaTemplatesController.php | 4 +- src/View/Helper/HashHelper.php | 5 + src/View/Helper/StringFromPathHelper.php | 45 +++ templates/MetaTemplates/index.php | 52 ++- templates/MetaTemplates/view.php | 10 + .../genericElements/Form/genericForm.php | 7 +- .../IndexTable/Fields/toggle.php | 115 +++--- .../genericElements/accordion_scaffold.php | 2 +- templates/genericTemplates/toggle.php | 2 +- templates/layout/default.php | 2 + webroot/js/api-helper.js | 180 +++++++++ webroot/js/bootstrap-helper.js | 348 +++++++++++++++++- webroot/js/main.js | 18 + 16 files changed, 723 insertions(+), 95 deletions(-) create mode 100644 src/View/Helper/StringFromPathHelper.php create mode 100644 webroot/js/api-helper.js diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 0ad84be..d847a10 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -198,7 +198,8 @@ class CRUDComponent extends Component $patchEntityParams['fields'] = $params['fields']; } $data = $this->Table->patchEntity($data, $input, $patchEntityParams); - if ($this->Table->save($data)) { + $savedData = $this->Table->save($data); + if ($savedData !== false) { $message = __('{0} updated.', $this->ObjectAlias); if (!empty($input['metaFields'])) { $this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $data->id]); @@ -206,6 +207,8 @@ class CRUDComponent extends Component } if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message); } else { $this->Controller->Flash->success($message); if (empty($params['redirect'])) { @@ -222,7 +225,8 @@ class CRUDComponent extends Component empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) ); if ($this->Controller->ParamHandler->isRest()) { - + } else if ($this->Controller->ParamHandler->isAjax()) { + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $data, $message, $validationMessage); } else { $this->Controller->Flash->error($message); } @@ -410,18 +414,18 @@ class CRUDComponent extends Component $data = $this->Table->get($id, $params); if ($this->request->is(['post', 'put'])) { $data->{$fieldName} = !$data->{$fieldName}; - $data = $this->Table->save($data); - if ($data !== false) { - $message = __('{0}\'s `{1}` field: {2}. (ID: {3})', - $this->ObjectAlias, + $savedData = $this->Table->save($data); + if ($savedData !== false) { + $message = __('{0} field {1}. (ID: {2} {3})', $fieldName, $data->{$fieldName} ? __('enabled') : __('disabled'), - $data->id, + Inflector::humanize($this->ObjectAlias), + $data->id ); if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $data, $message); + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $savedData, $message); } else { $this->Controller->Flash->success($message); if (empty($params['redirect'])) { @@ -439,7 +443,7 @@ class CRUDComponent extends Component ); if ($this->Controller->ParamHandler->isRest()) { } else if ($this->Controller->ParamHandler->isAjax()) { - $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $data, $message); + $this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationMessage); } else { $this->Controller->Flash->error($message); if (empty($params['redirect'])) { diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php index 4fa5f21..94b6efc 100644 --- a/src/Controller/Component/RestResponseComponent.php +++ b/src/Controller/Component/RestResponseComponent.php @@ -426,17 +426,19 @@ class RestResponseComponent extends Component $response = [ 'success' => true, 'message' => $message, + 'data' => $entity->toArray(), 'url' => $this->__generateURL($action, $ObjectAlias, $entity->id) ]; return $this->viewData($response); } - public function ajaxFailResponse($ObjectAlias, $action, $entity, $message) + public function ajaxFailResponse($ObjectAlias, $action, $entity, $message, $errors = []) { $action = $this->__dissectAdminRouting($action); $response = [ 'success' => false, 'message' => $message, + 'errors' => $errors, 'url' => $this->__generateURL($action, $ObjectAlias, $entity->id) ]; return $this->viewData($response); diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 2e7ba75..7e6fe17 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -49,6 +49,8 @@ class IndividualsController extends AppController $this->CRUD->edit($id); if ($this->ParamHandler->isRest()) { return $this->restResponsePayload; + } else if($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) { + return $this->ajaxResponsePayload; } $this->set('metaGroup', 'ContactDB'); $this->render('add'); diff --git a/src/Controller/MetaTemplatesController.php b/src/Controller/MetaTemplatesController.php index f46b3cb..b702c2c 100644 --- a/src/Controller/MetaTemplatesController.php +++ b/src/Controller/MetaTemplatesController.php @@ -70,9 +70,9 @@ class MetaTemplatesController extends AppController $this->set('metaGroup', 'Administration'); } - public function toggle($id) + public function toggle($id, $fieldName = 'enabled') { - $this->CRUD->toggle($id); + $this->CRUD->toggle($id, $fieldName); if ($this->ParamHandler->isRest()) { return $this->restResponsePayload; } else if($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) { diff --git a/src/View/Helper/HashHelper.php b/src/View/Helper/HashHelper.php index 5a2cbef..c926a69 100644 --- a/src/View/Helper/HashHelper.php +++ b/src/View/Helper/HashHelper.php @@ -11,4 +11,9 @@ class HashHelper extends Helper { return Hash::extract($target, $extraction_string); } + + public function get($target, $extraction_string) + { + return Hash::get($target, $extraction_string); + } } diff --git a/src/View/Helper/StringFromPathHelper.php b/src/View/Helper/StringFromPathHelper.php new file mode 100644 index 0000000..2cd0d37 --- /dev/null +++ b/src/View/Helper/StringFromPathHelper.php @@ -0,0 +1,45 @@ + true, + 'highlight' => false, + ]; + + public function buildStringFromDataPath(String $str, $data=[], array $dataPaths=[], array $options=[]) + { + $options = array_merge($this->defaultOptions, $options); + if (!empty($dataPaths)) { + $extractedVars = []; + foreach ($dataPaths as $i => $dataPath) { + if (is_array($dataPath)) { + $varValue = ''; + if (!empty($dataPath['datapath'])) { + $varValue = Hash::get($data, $dataPath['datapath']); + } else if (!empty($dataPath['raw'])) { + $varValue = $dataPath['raw']; + } + $extractedVars[] = $varValue; + } else { + $extractedVars[] = Hash::get($data, $dataPath); + } + } + foreach ($extractedVars as $i => $value) { + $value = $options['sanitize'] ? h($value) : $value; + $value = $options['highlight'] ? "${value}" : $value; + $str = str_replace( + "{{{$i}}}", + $value, + $str + ); + } + } + return $str; + } +} diff --git a/templates/MetaTemplates/index.php b/templates/MetaTemplates/index.php index 24c9983..0f70d5d 100644 --- a/templates/MetaTemplates/index.php +++ b/templates/MetaTemplates/index.php @@ -28,12 +28,52 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'enabled', 'data_path' => 'enabled', 'element' => 'toggle', - 'url' => '/metaTemplates/toggle', - 'url_params_data_paths' => ['id'], - 'toggle_requirement' => [ - 'function' => function($row, $options) { - return true; - } + 'url' => '/metaTemplates/toggle/{{0}}', + 'url_params_vars' => ['id'], + 'toggle_data' => [ + 'requirement' => [ + 'function' => function($row, $options) { + return true; + } + ] + ] + ], + [ + 'name' => 'Default', + 'sort' => 'is_default', + 'data_path' => 'is_default', + 'element' => 'toggle', + 'url' => '/metaTemplates/toggle/{{0}}/{{1}}', + 'url_params_vars' => [['datapath' => 'id'], ['raw' => 'is_default']], + 'toggle_data' => [ + 'requirement' => [ + 'function' => function($row, $options) { + return true; + } + ], + 'confirm' => [ + 'enable' => [ + 'titleHtml' => __('Make {{0}} the default template?'), + 'titleHtml_vars' => ['name'], + 'bodyHtml' => $this->Html->nestedList([ + __('Only one template per scope can be set as the default template'), + __('Current scope: {{0}}'), + ]), + 'bodyHtml_vars' => ['scope'], + 'type' => 'confirm-warning', + 'confirmText' => __('Yes, set as default') + ], + 'disable' => [ + 'titleHtml' => __('Remove {{0}} as the default template?'), + 'titleHtml_vars' => ['name'], + 'bodyHtml' => $this->Html->nestedList([ + __('Current scope: {{0}}'), + ]), + 'bodyHtml_vars' => ['scope'], + 'type' => 'confirm-warning', + 'confirmText' => __('Yes, do not set as default') + ] + ] ] ], [ diff --git a/templates/MetaTemplates/view.php b/templates/MetaTemplates/view.php index d8a282e..902949b 100644 --- a/templates/MetaTemplates/view.php +++ b/templates/MetaTemplates/view.php @@ -20,6 +20,16 @@ echo $this->element( 'key' => __('Description'), 'path' => 'description' ], + [ + 'key' => __('Enabled'), + 'path' => 'enabled', + 'type' => 'boolean' + ], + [ + 'key' => __('is_default'), + 'path' => 'is_default', + 'type' => 'boolean' + ], [ 'key' => __('Version'), 'path' => 'version' diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index 75aedd1..2217ca7 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -122,10 +122,11 @@ $data['description'] ), $fieldsString, - empty($metaFieldString) ? '' : $this->element( + empty($metaTemplateString) ? '' : $this->element( 'genericElements/accordion_scaffold', [ - 'body' => $metaFieldString, - 'title' => 'Meta fields' + 'body' => $metaTemplateString, + 'title' => 'Meta fields', + 'class' => 'mb-2' ] ), $this->element('genericElements/Form/submitButton', $submitButtonData), diff --git a/templates/element/genericElements/IndexTable/Fields/toggle.php b/templates/element/genericElements/IndexTable/Fields/toggle.php index cb19b5b..146b3bd 100644 --- a/templates/element/genericElements/IndexTable/Fields/toggle.php +++ b/templates/element/genericElements/IndexTable/Fields/toggle.php @@ -6,82 +6,85 @@ * to fetch it. * */ - $data = $this->Hash->extract($row, $field['data_path']); + $data = $this->Hash->get($row, $field['data_path']); $seed = rand(); $checkboxId = 'GenericToggle-' . $seed; $tempboxId = 'TempBox-' . $seed; $requirementMet = true; - if (isset($field['toggle_requirement'])) { - if (isset($field['toggle_requirement']['options']['datapath'])) { - foreach ($field['toggle_requirement']['options']['datapath'] as $name => $path) { - $field['toggle_requirement']['options']['datapath'][$name] = empty($this->Hash->extract($row, $path)[0]) ? null : $this->Hash->extract($row, $path)[0]; + if (isset($field['toggle_data']['requirement'])) { + if (isset($field['toggle_data']['requirement']['options']['datapath'])) { + foreach ($field['toggle_data']['requirement']['options']['datapath'] as $name => $path) { + $field['toggle_data']['requirement']['options']['datapath'][$name] = empty($this->Hash->extract($row, $path)[0]) ? null : $this->Hash->extract($row, $path)[0]; } } - $options = isset($field['toggle_requirement']['options']) ? $field['toggle_requirement']['options'] : array(); - $requirementMet = $field['toggle_requirement']['function']($row, $options); + $options = isset($field['toggle_data']['requirement']['options']) ? $field['toggle_data']['requirement']['options'] : array(); + $requirementMet = $field['toggle_data']['requirement']['function']($row, $options); } echo sprintf( - '', + '', $checkboxId, - empty($data[0]) ? '' : 'checked', + empty($data) ? '' : 'checked', $requirementMet ? '' : 'disabled="disabled"', $tempboxId ); + + // inject title and body vars into their placeholder + if (!empty($field['toggle_data']['confirm'])) { + $availableConfirmOptions = ['enable', 'disable']; + $confirmOptions = $field['toggle_data']['confirm']; + foreach ($availableConfirmOptions as $optionType) { + $availableType = ['title', 'titleHtml', 'body', 'bodyHtml']; + foreach ($availableType as $varType) { + if (!isset($confirmOptions[$optionType][$varType])) { + continue; + } + $confirmOptions[$optionType][$varType] = $this->StringFromPath->buildStringFromDataPath( + $confirmOptions[$optionType][$varType], + $row, + $confirmOptions[$optionType][$varType . '_vars'], + ['highlight' => true] + ); + } + } + } + $url = $this->StringFromPath->buildStringFromDataPath($field['url'], $row, $field['url_params_vars']); ?> \ No newline at end of file diff --git a/templates/element/genericElements/accordion_scaffold.php b/templates/element/genericElements/accordion_scaffold.php index 9b07aab..b5b8fd6 100644 --- a/templates/element/genericElements/accordion_scaffold.php +++ b/templates/element/genericElements/accordion_scaffold.php @@ -1,7 +1,7 @@ -
+
diff --git a/templates/genericTemplates/toggle.php b/templates/genericTemplates/toggle.php index ae98999..426249c 100644 --- a/templates/genericTemplates/toggle.php +++ b/templates/genericTemplates/toggle.php @@ -1 +1 @@ -Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?> \ No newline at end of file +Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?> \ No newline at end of file diff --git a/templates/layout/default.php b/templates/layout/default.php index 4c69c61..d42f2cd 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -40,6 +40,7 @@ $cakeDescription = 'Cerebrate'; Html->script('bootstrap.bundle.js') ?> Html->script('main.js') ?> Html->script('bootstrap-helper.js') ?> + Html->script('api-helper.js') ?> fetch('meta') ?> fetch('css') ?> fetch('script') ?> @@ -66,5 +67,6 @@ $cakeDescription = 'Cerebrate';
+
diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js new file mode 100644 index 0000000..37a54bd --- /dev/null +++ b/webroot/js/api-helper.js @@ -0,0 +1,180 @@ +class AJAXApi { + static genericRequestHeaders = new Headers({ + 'X-Requested-With': 'XMLHttpRequest' + }); + static genericRequestConfigGET = { + headers: AJAXApi.genericRequestHeaders + } + static genericRequestConfigPOST = { + headers: AJAXApi.genericRequestHeaders, + redirect: 'manual', + method: 'POST', + } + + static defaultOptions = { + showToast: true, + statusNode: false + } + options = {} + loadingOverlay = false + + constructor(options) { + this.mergeOptions(AJAXApi.defaultOptions) + this.mergeOptions(options) + } + + provideFeedback(options) { + if (this.options.showToast) { + UI.toast(options) + } else { + console.error(options.body) + } + } + + mergeOptions(newOptions) { + this.options = Object.assign({}, this.options, newOptions) + } + + static mergeFormData(formData, dataToMerge) { + for (const [fieldName, value] of Object.entries(dataToMerge)) { + formData.set(fieldName, value) + } + return formData + } + + async fetchURL(url, skipRequestHooks=false) { + if (!skipRequestHooks) { + this.beforeRequest() + } + let toReturn + try { + const response = await fetch(url, AJAXApi.genericRequestConfigGET); + if (!response.ok) { + throw new Error('Network response was not ok') + } + const data = await response.text(); + toReturn = data; + } catch (error) { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: error + }); + toReturn = Promise.reject(error); + } finally { + if (!skipRequestHooks) { + this.afterRequest() + } + } + return toReturn + } + + async fetchForm(url, skipRequestHooks=false) { + if (!skipRequestHooks) { + this.beforeRequest() + } + let toReturn + try { + const response = await fetch(url, AJAXApi.genericRequestConfigGET); + if (!response.ok) { + throw new Error('Network response was not ok') + } + const formHtml = await response.text(); + let tmpNode = document.createElement("div"); + tmpNode.innerHTML = formHtml; + let form = tmpNode.getElementsByTagName('form'); + if (form.length == 0) { + throw new Error('The server did not return a form element') + } + toReturn = form[0]; + } catch (error) { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: error + }); + toReturn = Promise.reject(error); + } finally { + if (!skipRequestHooks) { + this.afterRequest() + } + } + return toReturn + } + + async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false) { + if (!skipRequestHooks) { + this.beforeRequest() + } + let toReturn + try { + const form = await this.fetchForm(url, true); + try { + let formData = new FormData(form) + formData = AJAXApi.mergeFormData(formData, dataToMerge) + let options = { + ...AJAXApi.genericRequestConfigPOST, + body: formData, + }; + const response = await fetch(form.action, options); + if (!response.ok) { + throw new Error('Network response was not ok') + } + const data = await response.json(); + if (data.success) { + this.provideFeedback({ + variant: 'success', + body: data.message + }); + toReturn = data; + } else { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: data.errors + }); + toReturn = Promise.reject(error); + } + } catch (error) { + this.provideFeedback({ + variant: 'danger', + title: 'There has been a problem with the operation', + body: error + }); + toReturn = Promise.reject(error); + } + } catch (error) { + toReturn = Promise.reject(error); + } finally { + if (!skipRequestHooks) { + this.afterRequest() + } + } + return toReturn + } + + beforeRequest() { + if (this.options.statusNode !== false) { + this.toggleLoading(true) + } + } + + afterRequest() { + if (this.options.statusNode !== false) { + this.toggleLoading(false) + } + } + + toggleLoading(loading) { + if (this.loadingOverlay === false) { + this.loadingOverlay = new OverlayFactory({node: this.options.statusNode}); + } + if (loading) { + this.loadingOverlay.show() + } else { + this.loadingOverlay.hide() + + } + } +} + diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js index 60c4a5b..0290091 100644 --- a/webroot/js/bootstrap-helper.js +++ b/webroot/js/bootstrap-helper.js @@ -1,8 +1,18 @@ -function showToast(options) { - var theToast = new Toaster(options) - theToast.makeToast() - theToast.show() - return theToast.$toast + +class UIFactory { + toast(options) { + const theToast = new Toaster(options); + theToast.makeToast() + theToast.show() + return theToast + } + + modal (options) { + const theModal = new ModalFactory(options); + theModal.makeModal() + theModal.show() + return theModal + } } class Toaster { @@ -15,6 +25,7 @@ class Toaster { } static defaultOptions = { + id: false, title: false, muted: false, body: false, @@ -50,28 +61,31 @@ class Toaster { } isValid() { - return this.options.title !== false || this.options.muted !== false || this.options.body !== false + return this.options.title !== false || this.options.muted !== false || this.options.body !== false || this.options.titleHtml !== false || this.options.mutedHtml !== false || this.options.bodyHtml !== false } static buildToast(options) { var $toast = $('