From 15a2410120d16e26660042d052a42ae3a7417752 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Mon, 22 Feb 2021 15:47:30 +0100 Subject: [PATCH 1/7] new: [helpers:bootstrap] Added support of alert --- src/View/Helper/BootstrapHelper.php | 156 ++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 34 deletions(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 47fe34f..c35b1ba 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -52,9 +52,56 @@ class BootstrapHelper extends Helper $bsTabs = new BootstrapTabs($options); return $bsTabs->tabs(); } + + public function alert($options) + { + $bsAlert = new BoostrapAlert($options); + return $bsAlert->alert(); + } } -class BootstrapTabs extends Helper +class BootstrapGeneric +{ + public static $variants = ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent']; + protected $allowedOptionValues = []; + protected $options = []; + + protected 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))); + } + } + } + + protected static function genNode($node, $params) + { + return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params)); + } + + protected static function genHTMLParams($params) + { + $html = ''; + foreach ($params as $k => $v) { + $html .= BootstrapGeneric::genHTMLParam($k, $v) . ' '; + } + return $html; + } + + protected static function genHTMLParam($paramName, $values) + { + if (!is_array($values)) { + $values = [$values]; + } + return sprintf('%s="%s"', $paramName, implode(' ', $values)); + } +} + +class BootstrapTabs extends BootstrapGeneric { private $defaultOptions = [ 'fill' => false, @@ -73,17 +120,14 @@ class BootstrapTabs extends Helper 'content' => [], ], ]; - - private $allowedOptionValues = [ - 'justify' => [false, 'center', 'end'], - 'body-variant' => ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent', ''], - 'header-variant' => ['primary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'], - ]; - - private $options = null; private $bsClasses = null; function __construct($options) { + $this->allowedOptionValues = [ + 'justify' => [false, 'center', 'end'], + 'body-variant' => array_merge(BootstrapGeneric::$variants, ['']), + 'header-variant' => BootstrapGeneric::$variants, + ]; $this->processOptions($options); } @@ -97,6 +141,9 @@ class BootstrapTabs extends Helper $this->options = array_merge($this->defaultOptions, $options); $this->data = $this->options['data']; $this->checkOptionValidity(); + if (empty($this->data['navs'])) { + throw new InvalidArgumentException(__('No navigation data provided')); + } $this->bsClasses = [ 'nav' => [], 'nav-item' => $this->options['nav-item-class'], @@ -162,21 +209,6 @@ class BootstrapTabs extends Helper } } - 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')); - } - } - private function genTabs() { $html = ''; @@ -287,27 +319,83 @@ class BootstrapTabs extends Helper $html .= ''; return $html; } +} - private function genNode($node, $params) - { - return sprintf('<%s %s>', $node, $this->genHTMLParams($params)); +class BoostrapAlert extends BootstrapGeneric { + private $defaultOptions = [ + 'text' => '', + 'html' => null, + 'dismissible' => true, + 'variant' => 'primary', + 'fade' => true + ]; + + private $bsClasses = null; + + function __construct($options) { + $this->allowedOptionValues = [ + 'variant' => BootstrapGeneric::$variants, + ]; + $this->processOptions($options); } - private function genHTMLParams($params) + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function alert() + { + return $this->genAlert(); + } + + private function genAlert() + { + $html = BootstrapGeneric::genNode('div', [ + 'class' => [ + 'alert', + "alert-{$this->options['variant']}", + $this->options['dismissible'] ? 'alert-dismissible' : '', + $this->options['fade'] ? 'fade show' : '', + ], + 'role' => "alert" + ]); + + $html .= $this->genContent(); + $html .= $this->genCloseButton(); + $html .= ''; + return $html; + } + + private function genCloseButton() { $html = ''; - foreach ($params as $k => $v) { - $html .= $this->genHTMLParam($k, $v) . ' '; + if ($this->options['dismissible']) { + $html .= BootstrapGeneric::genNode('button', [ + 'type' => 'button', + 'class' => 'close', + 'data-dismiss' => 'alert', + 'arial-label' => 'close' + ]); + $html .= BootstrapGeneric::genNode('span', [ + 'arial-hidden' => 'true' + ]); + $html .= '×'; + $html .= ''; } return $html; } - private function genHTMLParam($paramName, $values) + private function genContent() { - if (!is_array($values)) { - $values = [$values]; + $html = ''; + if (!is_null($this->options['html'])) { + $html .= $this->options['html']; + } else { + $html .= h($this->options['text']); } - return sprintf('%s="%s"', $paramName, implode(' ', $values)); + return $html; } } From 51399903bd3dc8c5b6edcdd1cf1b57d47439ee77 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Mon, 22 Feb 2021 16:38:55 +0100 Subject: [PATCH 2/7] new: [helpers:bootstrap] Added support of table --- src/View/Helper/BootstrapHelper.php | 135 +++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index c35b1ba..1daee1e 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -58,6 +58,12 @@ class BootstrapHelper extends Helper $bsAlert = new BoostrapAlert($options); return $bsAlert->alert(); } + + public function table($options, $data) + { + $bsTable = new BoostrapTable($options, $data); + return $bsTable->table(); + } } class BootstrapGeneric @@ -78,7 +84,7 @@ class BootstrapGeneric } } - protected static function genNode($node, $params) + protected static function genNode($node, $params=[]) { return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params)); } @@ -382,7 +388,7 @@ class BoostrapAlert extends BootstrapGeneric { 'arial-hidden' => 'true' ]); $html .= '×'; - $html .= ''; + $html .= ''; } return $html; } @@ -399,3 +405,128 @@ class BoostrapAlert extends BootstrapGeneric { } } +class BoostrapTable extends BootstrapGeneric { + private $defaultOptions = [ + 'striped' => true, + 'bordered' => true, + 'borderless' => false, + 'hover' => true, + 'small' => false, + 'variant' => '', + 'tableClass' => [], + 'headerClass' => [], + 'bodyClass' => [], + ]; + + private $bsClasses = null; + + function __construct($options, $data) { + $this->allowedOptionValues = [ + 'variant' => array_merge(BootstrapGeneric::$variants, ['']) + ]; + $this->processOptions($options); + $this->fields = $data['fields']; + $this->items = $data['items']; + $this->caption = !empty($data['caption']) ? $data['caption'] : ''; + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + } + + public function table() + { + return $this->genTable(); + } + + private function genTable() + { + $html = BootstrapGeneric::genNode('table', [ + 'class' => [ + 'table', + "table-{$this->options['variant']}", + $this->options['striped'] ? 'table-striped' : '', + $this->options['bordered'] ? 'table-bordered' : '', + $this->options['borderless'] ? 'table-borderless' : '', + $this->options['hover'] ? 'table-hover' : '', + $this->options['small'] ? 'table-sm' : '', + !empty($this->options['variant']) ? "table-{$this->options['variant']}" : '', + !empty($this->options['tableClass']) ? $this->options['tableClass'] : '' + ], + ]); + + $html .= $this->genCaption(); + $html .= $this->genHeader(); + $html .= $this->genBody(); + + $html .= ''; + return $html; + } + + private function genHeader() + { + $head = BootstrapGeneric::genNode('thead', [ + 'class' => [ + !empty($this->options['headerClass']) ? $this->options['headerClass'] : '' + ], + ]); + $head .= BootstrapGeneric::genNode('tr'); + foreach ($this->fields as $i => $field) { + if (is_array($field)) { + $label = !empty($field['label']) ? $field['label'] : Inflector::humanize($field['key']); + } else { + $label = Inflector::humanize($field); + } + $head .= BootstrapGeneric::genNode('th', [ + ]); + $head .= h($label); + $head .= ''; + } + $head .= ''; + $head .= ''; + return $head; + } + + private function genBody() + { + $body = BootstrapGeneric::genNode('tbody', [ + 'class' => [ + !empty($this->options['bodyClass']) ? $this->options['bodyClass'] : '' + ], + ]); + $body .= BootstrapGeneric::genNode('tr'); + foreach ($this->items as $i => $row) { + if (array_keys($row) !== range(0, count($row) - 1)) { // associative array + foreach ($this->fields as $i => $field) { + if (is_array($field)) { + $key = $field['key']; + } else { + $key = $field; + } + $cellValue = $row[$key]; + $body .= BootstrapGeneric::genNode('td', [ + ]); + $body .= h($cellValue); + $body .= ''; + } + } else { + foreach ($row as $cellValue) { + $body .= BootstrapGeneric::genNode('td', [ + ]); + $body .= h($cellValue); + $body .= ''; + } + } + } + $body .= ''; + $body .= '{$this->caption}"; + } +} \ No newline at end of file From ebb388ae60a6b58c98121a568aed18eeaed00cfd Mon Sep 17 00:00:00 2001 From: mokaddem Date: Tue, 23 Feb 2021 07:59:26 +0100 Subject: [PATCH 3/7] fix: [helpers:bootstrap] Correctly closes tr tag --- src/View/Helper/BootstrapHelper.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 1daee1e..5067e1d 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -496,8 +496,12 @@ class BoostrapTable extends BootstrapGeneric { !empty($this->options['bodyClass']) ? $this->options['bodyClass'] : '' ], ]); - $body .= BootstrapGeneric::genNode('tr'); foreach ($this->items as $i => $row) { + $body .= BootstrapGeneric::genNode('tr',[ + 'class' => [ + !empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : '' + ] + ]); if (array_keys($row) !== range(0, count($row) - 1)) { // associative array foreach ($this->fields as $i => $field) { if (is_array($field)) { @@ -519,8 +523,8 @@ class BoostrapTable extends BootstrapGeneric { $body .= ''; } } + $body .= ''; } - $body .= ''; $body .= ' Date: Tue, 23 Feb 2021 08:45:35 +0100 Subject: [PATCH 4/7] chg: [helpers:bootstrap] General improvements --- src/View/Helper/BootstrapHelper.php | 118 +++++++++++++--------------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 5067e1d..e0bb28c 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -84,11 +84,21 @@ class BootstrapGeneric } } - protected static function genNode($node, $params=[]) + protected static function genNode($node, $params=[], $content="") + { + return sprintf('<%s %s>%s', $node, BootstrapGeneric::genHTMLParams($params), $content, $node); + } + + protected static function openNode($node, $params=[]) { return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params)); } + protected static function closeNode($node) + { + return sprintf('', $node); + } + protected static function genHTMLParams($params) { $html = ''; @@ -230,55 +240,55 @@ class BootstrapTabs extends BootstrapGeneric { $html = ''; if ($this->options['card']) { - $html .= $this->genNode('div', ['class' => array_merge(['card'], ["border-{$this->options['header-border-variant']}"])]); - $html .= $this->genNode('div', ['class' => array_merge(['card-header'], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}"])]); + $html .= $this->openNode('div', ['class' => array_merge(['card'], ["border-{$this->options['header-border-variant']}"])]); + $html .= $this->openNode('div', ['class' => array_merge(['card-header'], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}"])]); } $html .= $this->genNav(); if ($this->options['card']) { - $html .= ''; - $html .= $this->genNode('div', ['class' => array_merge(['card-body'], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); + $html .= $this->closeNode('div'); + $html .= $this->openNode('div', ['class' => array_merge(['card-body'], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); } $html .= $this->genContent(); if ($this->options['card']) { - $html .= ''; - $html .= ''; + $html .= $this->closeNode('div'); + $html .= $this->closeNode('div'); } return $html; } private function genVerticalTabs() { - $html = $this->genNode('div', ['class' => array_merge(['row', ($this->options['card'] ? 'card flex-row' : '')], ["border-{$this->options['header-border-variant']}"])]); - $html .= $this->genNode('div', ['class' => array_merge(['col-' . $this->options['vertical-size'], ($this->options['card'] ? 'card-header border-right' : '')], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}", "border-{$this->options['header-border-variant']}"])]); + $html = $this->openNode('div', ['class' => array_merge(['row', ($this->options['card'] ? 'card flex-row' : '')], ["border-{$this->options['header-border-variant']}"])]); + $html .= $this->openNode('div', ['class' => array_merge(['col-' . $this->options['vertical-size'], ($this->options['card'] ? 'card-header border-right' : '')], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}", "border-{$this->options['header-border-variant']}"])]); $html .= $this->genNav(); - $html .= ''; - $html .= $this->genNode('div', ['class' => array_merge(['col-' . (12 - $this->options['vertical-size']), ($this->options['card'] ? 'card-body2' : '')], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); + $html .= $this->closeNode('div'); + $html .= $this->openNode('div', ['class' => array_merge(['col-' . (12 - $this->options['vertical-size']), ($this->options['card'] ? 'card-body2' : '')], ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"])]); $html .= $this->genContent(); - $html .= ''; - $html .= ''; + $html .= $this->closeNode('div'); + $html .= $this->closeNode('div'); return $html; } private function genNav() { - $html = $this->genNode('ul', [ + $html = $this->openNode('ul', [ 'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']), 'role' => 'tablist', ]); foreach ($this->data['navs'] as $navItem) { $html .= $this->genNavItem($navItem); } - $html .= ''; + $html .= $this->closeNode('ul'); return $html; } private function genNavItem($navItem) { - $html = $this->genNode('li', [ + $html = $this->openNode('li', [ 'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']), 'role' => 'presentation', ]); - $html .= $this->genNode('a', [ + $html .= $this->openNode('a', [ 'class' => array_merge( ['nav-link'], [!empty($navItem['active']) ? 'active' : ''], @@ -296,34 +306,32 @@ class BootstrapTabs extends BootstrapGeneric } else { $html .= h($navItem['text']); } - $html .= ''; + $html .= $this->closeNode('a'); + $html .= $this->closeNode('li'); return $html; } private function genContent() { - $html = $this->genNode('div', [ + $html = $this->openNode('div', [ 'class' => array_merge(['tab-content'], $this->options['content-class']), ]); foreach ($this->data['content'] as $i => $content) { $navItem = $this->data['navs'][$i]; $html .= $this->genContentItem($navItem, $content); } - $html .= ''; + $html .= $this->closeNode('div'); return $html; } private function genContentItem($navItem, $content) { - $html = $this->genNode('div', [ + return $this->genNode('div', [ 'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']), 'role' => 'tabpanel', 'id' => $navItem['id'], 'aria-labelledby' => $navItem['id'] . '-tab' - ]); - $html .= $content; - $html .= ''; - return $html; + ], $content); } } @@ -358,7 +366,7 @@ class BoostrapAlert extends BootstrapGeneric { private function genAlert() { - $html = BootstrapGeneric::genNode('div', [ + $html = $this->openNode('div', [ 'class' => [ 'alert', "alert-{$this->options['variant']}", @@ -370,7 +378,7 @@ class BoostrapAlert extends BootstrapGeneric { $html .= $this->genContent(); $html .= $this->genCloseButton(); - $html .= ''; + $html .= $this->closeNode('div'); return $html; } @@ -378,30 +386,23 @@ class BoostrapAlert extends BootstrapGeneric { { $html = ''; if ($this->options['dismissible']) { - $html .= BootstrapGeneric::genNode('button', [ + $html .= $this->openNode('button', [ 'type' => 'button', 'class' => 'close', 'data-dismiss' => 'alert', 'arial-label' => 'close' ]); - $html .= BootstrapGeneric::genNode('span', [ + $html .= $this->genNode('span', [ 'arial-hidden' => 'true' - ]); - $html .= '×'; - $html .= ''; + ], '×'); + $html .= $this->closeNode('button'); } return $html; } private function genContent() { - $html = ''; - if (!is_null($this->options['html'])) { - $html .= $this->options['html']; - } else { - $html .= h($this->options['text']); - } - return $html; + return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text']; } } @@ -443,7 +444,7 @@ class BoostrapTable extends BootstrapGeneric { private function genTable() { - $html = BootstrapGeneric::genNode('table', [ + $html = $this->openNode('table', [ 'class' => [ 'table', "table-{$this->options['variant']}", @@ -461,43 +462,40 @@ class BoostrapTable extends BootstrapGeneric { $html .= $this->genHeader(); $html .= $this->genBody(); - $html .= ''; + $html .= $this->closeNode('table'); return $html; } private function genHeader() { - $head = BootstrapGeneric::genNode('thead', [ + $head = $this->openNode('thead', [ 'class' => [ !empty($this->options['headerClass']) ? $this->options['headerClass'] : '' ], ]); - $head .= BootstrapGeneric::genNode('tr'); + $head .= $this->openNode('tr'); foreach ($this->fields as $i => $field) { if (is_array($field)) { $label = !empty($field['label']) ? $field['label'] : Inflector::humanize($field['key']); } else { $label = Inflector::humanize($field); } - $head .= BootstrapGeneric::genNode('th', [ - ]); - $head .= h($label); - $head .= ''; + $head .= $this->genNode('th', [], h($label)); } - $head .= ''; - $head .= ''; + $head .= $this->closeNode('tr'); + $head .= $this->closeNode('thead'); return $head; } private function genBody() { - $body = BootstrapGeneric::genNode('tbody', [ + $body = $this->openNode('tbody', [ 'class' => [ !empty($this->options['bodyClass']) ? $this->options['bodyClass'] : '' ], ]); foreach ($this->items as $i => $row) { - $body .= BootstrapGeneric::genNode('tr',[ + $body .= $this->openNode('tr',[ 'class' => [ !empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : '' ] @@ -510,27 +508,21 @@ class BoostrapTable extends BootstrapGeneric { $key = $field; } $cellValue = $row[$key]; - $body .= BootstrapGeneric::genNode('td', [ - ]); - $body .= h($cellValue); - $body .= ''; + $body .= $this->genNode('td', [], h($cellValue)); } } else { foreach ($row as $cellValue) { - $body .= BootstrapGeneric::genNode('td', [ - ]); - $body .= h($cellValue); - $body .= ''; + $body .= $this->genNode('td', [], h($cellValue)); } } - $body .= ''; + $body .= $this->closeNode('tr');; } - $body .= 'closeNode('tbody');; return $body; } private function genCaption() { - return "{$this->caption}"; + return $this->genNode('caption', [], h($this->caption)); } -} \ No newline at end of file +} From 2be8add3207378d6512cd63fcb69d88e6e751f2a Mon Sep 17 00:00:00 2001 From: mokaddem Date: Tue, 23 Feb 2021 11:25:20 +0100 Subject: [PATCH 5/7] new: [helpers:bootstrap] Added support of button --- src/View/Helper/BootstrapHelper.php | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index e0bb28c..5ce4400 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -64,6 +64,12 @@ class BootstrapHelper extends Helper $bsTable = new BoostrapTable($options, $data); return $bsTable->table(); } + + public function button($options) + { + $bsButton = new BoostrapButton($options); + return $bsButton->button(); + } } class BootstrapGeneric @@ -526,3 +532,82 @@ class BoostrapTable extends BootstrapGeneric { return $this->genNode('caption', [], h($this->caption)); } } + +class BoostrapButton extends BootstrapGeneric { + private $defaultOptions = [ + 'id' => '', + 'text' => '', + 'html' => null, + 'variant' => 'primary', + 'outline' => false, + 'size' => '', + 'block' => false, + 'icon' => null, + 'class' => [], + 'type' => 'button', + 'nodeType' => 'button', + 'params' => [] + ]; + + private $bsClasses = []; + + function __construct($options) { + $this->allowedOptionValues = [ + 'variant' => BootstrapGeneric::$variants, + 'size' => ['', 'sm', 'lg'], + 'type' => ['button', 'submit', 'reset'] + ]; + $options['class'] = !is_array($options['class']) ? [$options['class']] : $options['class']; + $this->processOptions($options); + } + + private function processOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->checkOptionValidity(); + + $this->bsClasses[] = 'btn'; + if ($this->options['outline']) { + $this->bsClasses[] = "btn-outline-{$this->options['variant']}"; + } else { + $this->bsClasses[] = "btn-{$this->options['variant']}"; + } + if (!empty($this->options['size'])) { + $this->bsClasses[] = "btn-$this->options['size']"; + } + if ($this->options['block']) { + $this->bsClasses[] = 'btn-block'; + } + } + + public function button() + { + return $this->genButton(); + } + + private function genButton() + { + $html = $this->openNode($this->options['nodeType'], array_merge($this->options['params'], [ + 'class' => array_merge($this->options['class'], $this->bsClasses), + 'role' => "alert", + 'type' => $this->options['type'] + ])); + + $html .= $this->genIcon(); + $html .= $this->genContent(); + $html .= $this->closeNode($this->options['nodeType']); + return $html; + } + + private function genIcon() + { + return $this->genNode('span', [ + 'class' => ['mr-1', "fa fa-{$this->options['icon']}"], + ]); + } + + private function genContent() + { + return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text']; + } +} \ No newline at end of file From 9a25d98c9a3bfcb2db6a71262a2efdab5bcf7900 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Tue, 23 Feb 2021 11:42:26 +0100 Subject: [PATCH 6/7] chg: [helpers:bootstrap] Improvements for table --- src/View/Helper/BootstrapHelper.php | 64 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php index 5ce4400..d41fb89 100644 --- a/src/View/Helper/BootstrapHelper.php +++ b/src/View/Helper/BootstrapHelper.php @@ -501,32 +501,52 @@ class BoostrapTable extends BootstrapGeneric { ], ]); foreach ($this->items as $i => $row) { - $body .= $this->openNode('tr',[ - 'class' => [ - !empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : '' - ] - ]); - if (array_keys($row) !== range(0, count($row) - 1)) { // associative array - foreach ($this->fields as $i => $field) { - if (is_array($field)) { - $key = $field['key']; - } else { - $key = $field; - } - $cellValue = $row[$key]; - $body .= $this->genNode('td', [], h($cellValue)); - } - } else { - foreach ($row as $cellValue) { - $body .= $this->genNode('td', [], h($cellValue)); - } - } - $body .= $this->closeNode('tr');; + $body .= $this->genRow($row); } - $body .= $this->closeNode('tbody');; + $body .= $this->closeNode('tbody'); return $body; } + private function genRow($row) + { + $html = $this->openNode('tr',[ + 'class' => [ + !empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : '' + ] + ]); + if (array_keys($row) !== range(0, count($row) - 1)) { // associative array + foreach ($this->fields as $i => $field) { + if (is_array($field)) { + $key = $field['key']; + } else { + $key = $field; + } + $cellValue = $row[$key]; + $html .= $this->genCell($cellValue, $field, $row); + } + } else { // indexed array + foreach ($row as $cellValue) { + $html .= $this->genCell($cellValue, $field, $row); + } + } + $html .= $this->closeNode('tr'); + return $html; + } + + private function genCell($value, $field=[], $row=[]) + { + if (isset($field['formatter'])) { + $cellContent = $field['formatter']($value, $row); + } else { + $cellContent = h($value); + } + return $this->genNode('td', [ + 'class' => [ + !empty($row['_cellVariant']) ? "bg-{$row['_cellVariant']}" : '' + ] + ], $cellContent); + } + private function genCaption() { return $this->genNode('caption', [], h($this->caption)); From a8951ed69eca99e2a1249782ea26a27e0b43b812 Mon Sep 17 00:00:00 2001 From: mokaddem Date: Wed, 24 Feb 2021 11:05:23 +0100 Subject: [PATCH 7/7] new: [instance] Added first version of database migration plugin --- src/Controller/Component/ACLComponent.php | 4 ++ src/Controller/InstanceController.php | 81 ++++++++++++++++++++++- src/Model/Table/InstanceTable.php | 40 +++++++++++ src/Model/Table/PhinxlogTable.php | 48 ++++++++++++++ templates/Instance/migration_index.php | 61 +++++++++++++++++ webroot/js/api-helper.js | 6 +- 6 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/Model/Table/PhinxlogTable.php create mode 100644 templates/Instance/migration_index.php diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 9917fe6..e7ff59f 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -704,6 +704,10 @@ class ACLComponent extends Component 'home' => [ 'url' => '/instance/home', 'label' => __('Home') + ], + 'migration' => [ + 'url' => '/instance/migrationIndex', + 'label' => __('Database migration') ] ] ], diff --git a/src/Controller/InstanceController.php b/src/Controller/InstanceController.php index e88fa07..df141a9 100644 --- a/src/Controller/InstanceController.php +++ b/src/Controller/InstanceController.php @@ -6,12 +6,18 @@ use App\Controller\AppController; use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; +use Cake\Event\EventInterface; class InstanceController extends AppController { + public function beforeFilter(EventInterface $event) + { + parent::beforeFilter($event); + $this->set('metaGroup', !empty($this->isAdmin) ? 'Cerebrate' : 'Administration'); + } + public function home() { - $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); $this->set('md', file_get_contents(ROOT . '/README.md')); } @@ -22,4 +28,77 @@ class InstanceController extends AppController $data['user'] = $this->ACL->getUser(); return $this->RestResponse->viewData($data, 'json'); } + + public function migrationIndex() + { + $migrationStatus = $this->Instance->getMigrationStatus(); + + $this->loadModel('Phinxlog'); + $status = $this->Phinxlog->mergeMigrationLogIntoStatus($migrationStatus['status']); + + $this->set('status', $status); + $this->set('updateAvailables', $migrationStatus['updateAvailables']); + } + + public function migrate($version=null) { + if ($this->request->is('post')) { + if (is_null($version)) { + $migrateResult = $this->Instance->migrate(); + } else { + $migrateResult = $this->Instance->migrate(['target' => $version]); + } + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { + if ($migrateResult['success']) { + return $this->RestResponse->saveSuccessResponse('instance', 'migrate', false, false, __('Migration sucessful')); + } else { + return $this->RestResponse->saveFailResponse('instance', 'migrate', false, $migrateResult['error']); + } + } else { + if ($migrateResult['success']) { + $this->Flash->success(__('Migration sucessful')); + $this->redirect(['action' => 'migrationIndex']); + } else { + $this->Flash->error(__('Migration fail')); + $this->redirect(['action' => 'migrationIndex']); + } + } + } + $migrationStatus = $this->Instance->getMigrationStatus(); + $this->set('title', __n('Run database update?', 'Run all database updates?', count($migrationStatus['updateAvailables']))); + $this->set('question', __('The process might take some time.')); + $this->set('actionName', __n('Run update', 'Run all updates', count($migrationStatus['updateAvailables']))); + $this->set('path', ['controller' => 'instance', 'action' => 'migrate']); + $this->render('/genericTemplates/confirm'); + } + + public function rollback($version=null) { + if ($this->request->is('post')) { + if (is_null($version)) { + $migrateResult = $this->Instance->rollback(); + } else { + $migrateResult = $this->Instance->rollback(['target' => $version]); + } + if ($this->ParamHandler->isRest() || $this->ParamHandler->isAjax()) { + if ($migrateResult['success']) { + return $this->RestResponse->saveSuccessResponse('instance', 'rollback', false, false, __('Rollback sucessful')); + } else { + return $this->RestResponse->saveFailResponse('instance', 'rollback', false, $migrateResult['error']); + } + } else { + if ($migrateResult['success']) { + $this->Flash->success(__('Rollback sucessful')); + $this->redirect(['action' => 'migrationIndex']); + } else { + $this->Flash->error(__('Rollback fail')); + $this->redirect(['action' => 'migrationIndex']); + } + } + } + $migrationStatus = $this->Instance->getMigrationStatus(); + $this->set('title', __('Run database rollback?')); + $this->set('question', __('The process might take some time.')); + $this->set('actionName', __('Run rollback')); + $this->set('path', ['controller' => 'instance', 'action' => 'rollback']); + $this->render('/genericTemplates/confirm'); + } } diff --git a/src/Model/Table/InstanceTable.php b/src/Model/Table/InstanceTable.php index 91e38fe..5613e95 100644 --- a/src/Model/Table/InstanceTable.php +++ b/src/Model/Table/InstanceTable.php @@ -5,6 +5,7 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; use Cake\Validation\Validator; +use Migrations\Migrations; class InstanceTable extends AppTable { @@ -17,4 +18,43 @@ class InstanceTable extends AppTable { return $validator; } + + public function getMigrationStatus() + { + $migrations = new Migrations(); + $status = $migrations->status(); + $status = array_reverse($status); + + $updateAvailables = array_filter($status, function ($update) { + return $update['status'] != 'up'; + }); + return [ + 'status' => $status, + 'updateAvailables' => $updateAvailables, + ]; + } + + public function migrate($version=null) { + $migrations = new Migrations(); + if (is_null($version)) { + $migrationResult = $migrations->migrate(); + } else { + $migrationResult = $migrations->migrate(['target' => $version]); + } + return [ + 'success' => true + ]; + } + + public function rollback($version=null) { + $migrations = new Migrations(); + if (is_null($version)) { + $migrationResult = $migrations->rollback(); + } else { + $migrationResult = $migrations->rollback(['target' => $version]); + } + return [ + 'success' => true + ]; + } } diff --git a/src/Model/Table/PhinxlogTable.php b/src/Model/Table/PhinxlogTable.php new file mode 100644 index 0000000..36d9222 --- /dev/null +++ b/src/Model/Table/PhinxlogTable.php @@ -0,0 +1,48 @@ +find('list', [ + 'keyField' => 'version', + 'valueField' => function ($entry) { + return $entry; + } + ])->toArray(); + foreach ($status as &$entry) { + if (!empty($logs[$entry['id']])) { + $logEntry = $logs[$entry['id']]; + $startTime = $logEntry['start_time']; + $startTime->setToStringFormat('yyyy-MM-dd HH:mm:ss'); + $endTime = $logEntry['end_time']; + $endTime->setToStringFormat('yyyy-MM-dd HH:mm:ss'); + $timeTaken = $logEntry['end_time']->diff($logEntry['start_time']); + $timeTakenFormated = sprintf('%s min %s sec', + floor(abs($logEntry['end_time']->getTimestamp() - $logEntry['start_time']->getTimestamp()) / 60), + abs($logEntry['end_time']->getTimestamp() - $logEntry['start_time']->getTimestamp()) % 60 + ); + } else { + $startTime = 'N/A'; + $endTime = 'N/A'; + $timeTaken = 'N/A'; + $timeTakenFormated = 'N/A'; + } + $entry['start_time'] = $startTime; + $entry['end_time'] = $endTime; + $entry['time_taken'] = $timeTaken; + $entry['time_taken_formated'] = $timeTakenFormated; + } + return $status; + } +} diff --git a/templates/Instance/migration_index.php b/templates/Instance/migration_index.php new file mode 100644 index 0000000..b554dd1 --- /dev/null +++ b/templates/Instance/migration_index.php @@ -0,0 +1,61 @@ +%s%s
%s
', + __n('A new update is available!', 'New updates are available!', count($updateAvailables)), + __('Updating to the latest version is highly recommanded.'), + $this->Bootstrap->button([ + 'variant' => 'success', + 'icon' => 'arrow-alt-circle-up', + 'class' => 'mt-1', + 'text' => __n('Run update', 'Run all updates', count($updateAvailables)), + 'params' => [ + 'onclick' => 'runAllUpdate()' + ] + ]) + ); + echo $this->Bootstrap->alert([ + 'variant' => 'warning', + 'html' => $alertHtml, + 'dismissible' => false, + ]); +} + +foreach ($status as $i => &$update) { + if ($update['status'] == 'up') { + $update['_rowVariant'] = 'success'; + } else if ($update['status'] == 'down') { + $update['_rowVariant'] = 'danger'; + } +} + +echo $this->Bootstrap->table([], [ + 'fields' => [ + ['key' => 'id', 'label' => __('ID')], + ['key' => 'name', 'label' => __('Name')], + ['key' => 'end_time', 'label' => __('End Time')], + ['key' => 'time_taken_formated', 'label' => __('Time Taken')], + ['key' => 'status', 'label' => __('Status')] + ], + 'items' => $status, +]); +?> + + \ No newline at end of file diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js index 8987afb..282b1b4 100644 --- a/webroot/js/api-helper.js +++ b/webroot/js/api-helper.js @@ -142,7 +142,7 @@ class AJAXApi { static async quickFetchAndPostForm(url, dataToMerge={}, options={}) { const constAlteredOptions = Object.assign({}, {}, options) const tmpApi = new AJAXApi(constAlteredOptions) - return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks) + return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks, constAlteredOptions.skipFeedback) } /** @@ -335,14 +335,14 @@ class AJAXApi { * @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped * @return {Promise} Promise object resolving to the result of the POST operation */ - async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false) { + async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false, skipFeedback=false) { if (!skipRequestHooks) { this.beforeRequest() } let toReturn try { const form = await this.fetchForm(url, true, true); - toReturn = await this.postForm(form, dataToMerge, true, true) + toReturn = await this.postForm(form, dataToMerge, true, skipFeedback) } catch (error) { toReturn = Promise.reject(error); } finally {