From 97b6ed8cbf92c3449229d64abcd2d042db73cc53 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Fri, 4 Dec 2020 16:08:11 +0100 Subject: [PATCH 01/82] new: [helper] Added simple bootstrap navigation helper --- src/View/Helper/BootstrapHelper.php | 298 ++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 src/View/Helper/BootstrapHelper.php diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php new file mode 100644 index 0000000..b44c0f4 --- /dev/null +++ b/src/View/Helper/BootstrapHelper.php @@ -0,0 +1,298 @@ +Bootstrap->Tabs([ + * 'pills' => true, + * 'card' => true, + * 'data' => [ + * 'navs' => [ + * 'tab1', + * ['text' => 'tab2', 'active' => true], + * ['html' => 'tab3', 'disabled' => true], + * ], + * 'content' => [ + * 'body1', + * 'body2', + * '~body3~' + * ] + * ] + * ]); + */ + +namespace App\View\Helper; + +use Cake\View\Helper; +use Cake\Utility\Security; +use InvalidArgumentException; + +class BootstrapHelper extends Helper +{ + public function tabs($options) + { + $bsTabs = new BootstrapTabs($options); + return $bsTabs->tabs(); + } +} + +class BootstrapTabs extends Helper +{ + private $defaultOptions = [ + 'nav-fill' => false, + 'nav-justify' => false, + 'pills' => false, + 'vertical' => false, + 'vertical-size' => 3, + 'card' => false, + 'nav-class' => [], + 'nav-item-class' => [], + 'content-class' => [], + 'data' => [ + 'navs' => [], + 'content' => [], + ], + ]; + + private $allowedOptionValues = [ + 'nav-justify' => [false, 'center', 'end'], + ]; + + private $options = null; + private $bsClasses = null; + + function __construct($options) { + $this->processOptions($options); + } + + public function tabs() + { + return $this->genTabs(); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->data = $this->options['data']; + $this->checkOptionValidity(); + $this->bsClasses = [ + 'nav' => [], + 'nav-item' => $this->options['nav-item-class'], + + ]; + + if (!empty($this->options['nav-justify'])) { + $this->bsClasses['nav'][] = 'justify-content-' . $this->options['nav-justify']; + } + + if ($this->options['pills']) { + $this->bsClasses['nav'][] = 'nav-pills'; + if ($this->options['vertical']) { + $this->bsClasses['nav'][] = 'flex-column'; + } + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-pills'; + } + } else { + $this->bsClasses['nav'][] = 'nav-tabs'; + if ($this->options['card']) { + $this->bsClasses['nav'][] = 'card-header-tabs'; + } + } + + if ($this->options['nav-fill']) { + $this->bsClasses['nav'][] = 'nav-fill'; + } + if ($this->options['nav-justify']) { + $this->bsClasses['nav'][] = 'nav-justify'; + } + + $activeTab = 0; + foreach ($this->data['navs'] as $i => $nav) { + if (!is_array($nav)) { + $this->data['navs'][$i] = ['text' => $nav]; + } + if (!isset($this->data['navs'][$i]['id'])) { + $this->data['navs'][$i]['id'] = 't-' . Security::randomString(8); + } + if (!empty($nav['active'])) { + $activeTab = $i; + } + } + $this->data['navs'][$activeTab]['active'] = true; + + $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + } + + private function checkOptionValidity() + { + foreach ($this->allowedOptionValues as $option => $values) { + if (!isset($this->options[$option])) { + throw new InvalidArgumentException(__('Option `{0}` should have a value', $option)); + } + if (!in_array($this->options[$option], $values)) { + throw new InvalidArgumentException(__('Option `{0}` is not a valid option for `{1}`. Accepted values: {2}', json_encode($this->options[$option]), $option, json_encode($values))); + } + } + if (empty($this->data['navs'])) { + throw new InvalidArgumentException(__('No navigation data provided')); + } + if ($this->options['card'] && $this->options['vertical']) { + throw new InvalidArgumentException(__('`card` option can only be used on horizontal mode')); + } + } + + private function genTabs() + { + $html = ''; + if ($this->options['vertical']) { + $html .= $this->genVerticalTabs(); + } else { + $html .= $this->genHorizontalTabs(); + } + return $html; + } + + private function genHorizontalTabs() + { + $html = ''; + if ($this->options['card']) { + $html .= $this->genNode('div', ['class' => ['card']]); + $html .= $this->genNode('div', ['class' => ['card-header']]); + } + $html .= $this->genNav(); + if ($this->options['card']) { + $html .= ''; + $html .= $this->genNode('div', ['class' => ['card-body']]); + } + $html .= $this->genContent(); + if ($this->options['card']) { + $html .= ''; + } + return $html; + } + + private function genVerticalTabs() + { + $html = sprintf('
', $this->genHTMLParams(['class' => 'row'])); + $html .= sprintf('
', $this->genHTMLParams(['class' => 'col-' . $this->options['vertical-size']])); + $html .= $this->genNav(); + $html .= '
'; + $html .= sprintf('
', $this->genHTMLParams(['class' => 'col-' . (12 - $this->options['vertical-size'])])); + $html .= $this->genContent(); + $html .= '
'; + $html .= '
'; + return $html; + } + + private function genNav() + { + $ulParams = [ + 'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']), + 'role' => 'tablist', + ]; + $html = sprintf(''; + return $html; + } + + private function genNavItem($navItem) + { + $liParams = [ + 'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']), + 'role' => 'presentation', + ]; + $aParams = [ + 'class' => array_merge( + ['nav-link'], + [!empty($navItem['active']) ? 'active' : ''], + [!empty($navItem['disabled']) ? 'disabled' : ''] + ), + 'data-toggle' => $this->options['pills'] ? 'pill' : 'tab', + 'id' => $navItem['id'] . '-tab', + 'href' => '#' . $navItem['id'], + 'aria-controls' => $navItem['id'], + 'aria-selected' => !empty($navItem['active']), + 'role' => 'tab', + ]; + $html = sprintf('
  • ', $this->genHTMLParams($liParams)); + $html .= sprintf('', $this->genHTMLParams($aParams)); + if (!empty($navItem['html'])) { + $html .= $navItem['html']; + } else { + $html .= h($navItem['text']); + } + $html .= '
  • '; + return $html; + } + + private function genContent() + { + $divParams = [ + 'class' => array_merge(['tab-content'], $this->options['content-class']), + ]; + $html = sprintf('
    ', $this->genHTMLParams($divParams)); + foreach ($this->data['content'] as $i => $content) { + $navItem = $this->data['navs'][$i]; + $html .= $this->genContentItem($navItem, $content); + } + $html .= ''; + return $html; + } + + private function genContentItem($navItem, $content) + { + $divParams = [ + 'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']), + 'role' => 'tabpanel', + 'id' => $navItem['id'], + 'aria-labelledby' => $navItem['id'] . '-tab' + ]; + $html = sprintf('
    ', $this->genHTMLParams($divParams)); + $html .= $content; + $html .= '
    '; + return $html; + } + + private function genNode($node, $params) + { + return sprintf('<%s %s>', $node, $this->genHTMLParams($params)); + } + + private function genHTMLParams($params) + { + $html = ''; + foreach ($params as $k => $v) { + $html .= $this->genHTMLParam($k, $v) . ' '; + } + return $html; + } + + private function genHTMLParam($paramName, $values) + { + if (!is_array($values)) { + $values = [$values]; + } + return sprintf('%s="%s"', $paramName, implode(' ', $values)); + } +} + From 14509edef81087d61e4cfc14a23ede6a4ffc64a1 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Mon, 7 Dec 2020 09:52:35 +0100 Subject: [PATCH 02/82] chg: [metaTemplate] Moved to namespaced meta fields based on their template - WiP --- src/Controller/Component/CRUDComponent.php | 45 +++++++++++-------- src/Model/Table/MetaFieldsTable.php | 6 ++- src/View/Helper/BootstrapHelper.php | 7 +++ templates/Organisations/add.php | 3 +- .../genericElements/Form/genericForm.php | 23 ++++------ .../Form/metaTemplateScaffold.php | 26 +++++++++++ 6 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 templates/element/genericElements/Form/metaTemplateScaffold.php diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 3378ad2..5df6396 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -48,7 +48,7 @@ class CRUDComponent extends Component private function getMetaTemplates() { - $metaFields = []; + $metaTemplates = []; if (!empty($this->Table->metaFields)) { $metaQuery = $this->MetaTemplates->find(); $metaQuery->where([ @@ -57,13 +57,8 @@ class CRUDComponent extends Component ]); $metaQuery->contain(['MetaTemplateFields']); $metaTemplates = $metaQuery->all(); - foreach ($metaTemplates as $metaTemplate) { - foreach ($metaTemplate->meta_template_fields as $field) { - $metaFields[$field['field']] = $field; - } - } } - $this->Controller->set('metaFields', $metaFields); + $this->Controller->set('metaTemplates', $metaTemplates); return true; } @@ -136,18 +131,30 @@ class CRUDComponent extends Component private function saveMetaFields($id, $input) { - foreach ($input['metaFields'] as $metaField => $values) { - if (!is_array($values)) { - $values = [$values]; + foreach ($input['metaFields'] as $templateID => $metaFields) { + $metaTemplates = $this->MetaTemplates->find()->where([ + 'id' => $templateID, + 'enabled' => 1 + ])->contain(['MetaTemplateFields'])->first(); + $fieldNameToId = []; + foreach ($metaTemplates->meta_template_fields as $i => $metaTemplateField) { + $fieldNameToId[$metaTemplateField->field] = $metaTemplateField->id; } - foreach ($values as $value) { - if ($value !== '') { - $temp = $this->MetaFields->newEmptyEntity(); - $temp->field = $metaField; - $temp->value = $value; - $temp->scope = $this->Table->metaFields; - $temp->parent_id = $id; - $this->MetaFields->save($temp); + foreach ($metaFields as $metaField => $values) { + if (!is_array($values)) { + $values = [$values]; + } + foreach ($values as $value) { + if ($value !== '') { + $temp = $this->MetaFields->newEmptyEntity(); + $temp->field = $metaField; + $temp->value = $value; + $temp->scope = $this->Table->metaFields; + $temp->parent_id = $id; + $temp->meta_template_id = $templateID; + $temp->meta_template_field_id = $fieldNameToId[$metaField]; + $this->MetaFields->save($temp); + } } } } @@ -227,7 +234,7 @@ class CRUDComponent extends Component return $data; } $query = $this->MetaFields->find(); - $query->where(['scope' => $this->Table->metaFields, 'parent_id' => $id]); + $query->where(['MetaFields.scope' => $this->Table->metaFields, 'MetaFields.parent_id' => $id]); $metaFields = $query->all(); $data['metaFields'] = []; foreach($metaFields as $metaField) { diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php index e0abcab..765c8dd 100644 --- a/src/Model/Table/MetaFieldsTable.php +++ b/src/Model/Table/MetaFieldsTable.php @@ -13,6 +13,8 @@ class MetaFieldsTable extends AppTable parent::initialize($config); $this->addBehavior('UUID'); $this->setDisplayField('field'); + $this->hasOne('MetaTemplates'); + $this->belongsTo('MetaTemplateFields'); } public function validationDefault(Validator $validator): Validator @@ -22,7 +24,9 @@ class MetaFieldsTable extends AppTable ->notEmptyString('field') ->notEmptyString('uuid') ->notEmptyString('value') - ->requirePresence(['scope', 'field', 'value', 'uuid'], 'create'); + ->notEmptyString('meta_template_id') + ->notEmptyString('meta_template_field_id') + ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create'); return $validator; } } diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index b44c0f4..a451959 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -138,6 +138,13 @@ class BootstrapTabs extends Helper $this->data['navs'][$activeTab]['active'] = true; $this->options['vertical-size'] = $this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11 ? 3 : $this->options['vertical-size']; + + if (!is_array($this->options['nav-class'])) { + $this->options['nav-class'] = [$this->options['nav-class']]; + } + if (!is_array($this->options['content-class'])) { + $this->options['content-class'] = [$this->options['content-class']]; + } } private function checkOptionValidity() diff --git a/templates/Organisations/add.php b/templates/Organisations/add.php index 37fefe7..654e367 100644 --- a/templates/Organisations/add.php +++ b/templates/Organisations/add.php @@ -29,7 +29,8 @@ 'field' => 'type' ) ), - 'metaFields' => empty($metaFields) ? [] : $metaFields, + // 'metaFields' => empty($metaFields) ? [] : $metaFields, + 'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates, 'submit' => array( 'action' => $this->request->getParam('action') ) diff --git a/templates/element/genericElements/Form/genericForm.php b/templates/element/genericElements/Form/genericForm.php index ed73cf1..864aca7 100644 --- a/templates/element/genericElements/Form/genericForm.php +++ b/templates/element/genericElements/Form/genericForm.php @@ -62,19 +62,12 @@ ); } } - $metaFieldString = ''; - if (!empty($data['metaFields'])) { - foreach ($data['metaFields'] as $metaField) { - $metaField['label'] = \Cake\Utility\Inflector::humanize($metaField['field']); - $metaField['field'] = 'metaFields.' . $metaField['field']; - $metaFieldString .= $this->element( - 'genericElements/Form/fieldScaffold', [ - 'fieldData' => $metaField->toArray(), - 'form' => $this->Form - ] - ); - } - } + $metaTemplateString = $this->element( + 'genericElements/Form/metaTemplateScaffold', [ + 'metaTemplatesData' => $data['metaTemplates'], + 'form' => $this->Form, + ] + ); $submitButtonData = ['model' => $modelForForm, 'formRandomValue' => $formRandomValue]; if (!empty($data['submit'])) { $submitButtonData = array_merge($submitButtonData, $data['submit']); @@ -104,9 +97,9 @@ $ajaxFlashMessage, $formCreate, $fieldsString, - empty($metaFieldString) ? '' : $this->element( + empty($metaTemplateString) ? '' : $this->element( 'genericElements/accordion_scaffold', [ - 'body' => $metaFieldString, + 'body' => $metaTemplateString, 'title' => 'Meta fields' ] ), diff --git a/templates/element/genericElements/Form/metaTemplateScaffold.php b/templates/element/genericElements/Form/metaTemplateScaffold.php new file mode 100644 index 0000000..cf32d35 --- /dev/null +++ b/templates/element/genericElements/Form/metaTemplateScaffold.php @@ -0,0 +1,26 @@ + $metaTemplate) { + $tabData['navs'][$i] = [ + 'text' => $metaTemplate->name + ]; + $fieldsHtml = ''; + foreach ($metaTemplate->meta_template_fields as $metaField) { + $metaField->label = Inflector::humanize($metaField->field); + $metaField->field = sprintf('%s.%s.%s', 'metaFields', $metaTemplate->id, $metaField->field); + $fieldsHtml .= $this->element( + 'genericElements/Form/fieldScaffold', [ + 'fieldData' => $metaField->toArray(), + 'form' => $this->Form + ] + ); + } + $tabData['content'][$i] = $fieldsHtml; +} +echo $this->Bootstrap->Tabs([ + 'pills' => true, + 'data' => $tabData, + 'nav-class' => ['pb-1'] +]); \ No newline at end of file From 8ea8bf641818abd43cefa8fbb7b93daf29edbceb Mon Sep 17 00:00:00 2001 From: mokaddem Date: Mon, 7 Dec 2020 14:15:59 +0100 Subject: [PATCH 03/82] new: [webroot] Added bootstrap toaster --- templates/layout/default.php | 3 + webroot/css/bootstrap-additional.css | 68 +++++++++++++++++++ webroot/js/bootstrap-helper.js | 97 ++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 webroot/css/bootstrap-additional.css create mode 100644 webroot/js/bootstrap-helper.js diff --git a/templates/layout/default.php b/templates/layout/default.php index c5284b9..4c69c61 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -39,9 +39,11 @@ $cakeDescription = 'Cerebrate'; Html->script('popper.min.js') ?> Html->script('bootstrap.bundle.js') ?> Html->script('main.js') ?> + Html->script('bootstrap-helper.js') ?> fetch('meta') ?> fetch('css') ?> fetch('script') ?> + Html->css('bootstrap-additional.css') ?> Html->meta('favicon.ico', '/img/favicon.ico', ['type' => 'icon']); ?> @@ -63,5 +65,6 @@ $cakeDescription = 'Cerebrate';
    +
    diff --git a/webroot/css/bootstrap-additional.css b/webroot/css/bootstrap-additional.css new file mode 100644 index 0000000..981324c --- /dev/null +++ b/webroot/css/bootstrap-additional.css @@ -0,0 +1,68 @@ +/* Toast */ +.toast { + min-width: 250px; +} +.toast-primary { + color: #004085; + background-color: #cce5ff; + border-color: #b8daff; +} +.toast-primary strong { + color: #002752; +} +.toast-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; +} +.toast-secondary strong { + color: #202326; +} +.toast-success { + color: #155724 !important; + background-color: #d4edda !important; + border-color: #c3e6cb !important; +} +.toast-success strong { + color: #0b2e13; +} +.toast-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} +.toast-info strong { + color: #062c33; +} +.toast-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; +} +.toast-warning strong { + color: #533f03; +} +.toast-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} +.toast-danger strong { + color: #491217; +} +.toast-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; +} +.toast-light strong { + color: #686868; +} +.toast-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; +} +.toast-dark strong { + color: #040505; +} diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js new file mode 100644 index 0000000..60c4a5b --- /dev/null +++ b/webroot/js/bootstrap-helper.js @@ -0,0 +1,97 @@ +function showToast(options) { + var theToast = new Toaster(options) + theToast.makeToast() + theToast.show() + return theToast.$toast +} + +class Toaster { + constructor(options) { + this.options = Object.assign({}, Toaster.defaultOptions, options) + this.bsToastOptions = { + autohide: this.options.autohide, + delay: this.options.delay, + } + } + + static defaultOptions = { + title: false, + muted: false, + body: false, + variant: 'default', + autohide: true, + delay: 5000, + titleHtml: false, + mutedHtml: false, + bodyHtml: false, + closeButton: true, + } + + makeToast() { + if (this.isValid()) { + this.$toast = Toaster.buildToast(this.options) + $('#mainToastContainer').append(this.$toast) + } + } + + show() { + if (this.isValid()) { + var that = this + this.$toast.toast(this.bsToastOptions) + .toast('show') + .on('hidden.bs.toast', function () { + that.removeToast() + }) + } + } + + removeToast() { + this.$toast.remove(); + } + + isValid() { + return this.options.title !== false || this.options.muted !== false || this.options.body !== false + } + + static buildToast(options) { + var $toast = $('