Remove and retire AzuraForms

This commit is contained in:
Buster "Silver Eagle" Neece 2021-11-05 01:36:44 -05:00
parent 87ea336a12
commit 7a393bda11
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
16 changed files with 1 additions and 725 deletions

View File

@ -23,7 +23,6 @@
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"azuracast/azuraforms": "dev-main",
"azuracast/flysystem-v2-extensions": "dev-main",
"azuracast/metadata-manager": "dev-main",
"azuracast/nowplaying": "dev-main",

76
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5ff9d6ce6c6161765b7aaf4dce99fc31",
"content-hash": "16e701ab7337c8e0d43e7d0f8303bfde",
"packages": [
{
"name": "aws/aws-crt-php",
@ -147,79 +147,6 @@
},
"time": "2021-11-02T19:37:19+00:00"
},
{
"name": "azuracast/azuraforms",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/AzuraCast/azuraforms.git",
"reference": "d1ca581e29881e183df43f6979d3eab6f816988d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/AzuraCast/azuraforms/zipball/d1ca581e29881e183df43f6979d3eab6f816988d",
"reference": "d1ca581e29881e183df43f6979d3eab6f816988d",
"shasum": ""
},
"require": {
"ext-json": "*",
"league/mime-type-detection": "^1.7",
"php": ">=7.4",
"psr/http-message": ">1.0"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^0.5.0",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^0.12",
"roave/security-advisories": "dev-master"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"AzuraForms\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Buster 'Silver Eagle' Neece",
"email": "buster@busterneece.com",
"homepage": "https://dashdev.net/",
"role": "Lead Developer of AzuraForms Fork"
},
{
"name": "Luke Rotherfield",
"email": "luke@lrotherfield.com",
"homepage": "http://lrotherfield.com",
"role": "Developer of original Nibble Forms"
}
],
"description": "A modern, namespaced, configuration-driven forms engine for PHP.",
"homepage": "https://github.com/AzuraCast/azuraforms",
"support": {
"issues": "https://github.com/AzuraCast/azuraforms/issues",
"source": "https://github.com/AzuraCast/azuraforms/tree/main"
},
"funding": [
{
"url": "https://github.com/AzuraCast",
"type": "github"
},
{
"url": "https://opencollective.com/azuracast",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/AzuraCast",
"type": "patreon"
}
],
"time": "2021-09-05T01:04:18+00:00"
},
{
"name": "azuracast/flysystem-v2-extensions",
"version": "dev-main",
@ -14262,7 +14189,6 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"azuracast/azuraforms": 20,
"azuracast/flysystem-v2-extensions": 20,
"azuracast/metadata-manager": 20,
"azuracast/nowplaying": 20,

View File

@ -11,9 +11,6 @@ use Psr\Http\Message\ServerRequestInterface as Request;
*/
return [
/*
* Core libraries
*/
'jquery' => [
'order' => 0,
'files' => [
@ -25,9 +22,6 @@ return [
],
],
/*
* Main per-layout dependencies
*/
'minimal' => [
'order' => 2,
'require' => ['jquery'],
@ -139,61 +133,6 @@ return [
],
],
/*
* Asset collections
*/
'forms_common' => [
'require' => ['zxcvbn', 'select2', 'dirrty'],
'files' => [
'js' => [
[
'src' => 'dist/form.js',
'defer' => true,
],
[
'src' => 'dist/lib/autosize/autosize.min.js',
'defer' => true,
],
],
],
],
/*
* Individual libraries
*/
'zxcvbn' => [
'order' => 10,
'files' => [
'js' => [
[
'src' => 'dist/lib/zxcvbn/zxcvbn.js',
'defer' => true,
],
[
'src' => 'dist/zxcvbn.js',
'defer' => true,
],
],
],
],
'select2' => [
'order' => 10,
'files' => [
'js' => [
[
'src' => 'dist/lib/select2/select2.full.min.js',
'defer' => true,
],
],
'css' => [
[
'href' => 'dist/lib/select2/select2.min.css',
],
],
],
],
'luxon' => [
'order' => 8,
'files' => [
@ -242,19 +181,6 @@ return [
],
],
'dirrty' => [
'order' => 10,
'require' => ['jquery'],
'files' => [
'js' => [
[
'src' => 'dist/lib/dirrty/jquery.dirrty.js',
'defer' => true,
],
],
],
],
'Vue_PublicWebDJ' => [
'order' => 10,
'files' => [

View File

@ -14,15 +14,12 @@ const mode = require('gulp-mode')();
const run = require('gulp-run-command').default;
var jsFiles = {
// Core Libraries
'jquery': {
base: 'node_modules/jquery/dist',
files: [
'node_modules/jquery/dist/jquery.min.js'
]
},
// Main per-layout dependencies
'bootstrap': {
base: null,
files: [
@ -41,12 +38,6 @@ var jsFiles = {
'node_modules/sweetalert2/dist/sweetalert2.min.js'
]
},
'autosize': {
base: 'node_modules/autosize/dist',
files: [
'node_modules/autosize/dist/autosize.min.js'
]
},
'material-icons': {
files: [
'font/*'
@ -59,26 +50,6 @@ var jsFiles = {
'node_modules/roboto-fontface/fonts/roboto/*'
]
},
'dirrty': {
base: null,
files: [
'node_modules/dirrty/dist/jquery.dirrty.js'
]
},
// Individual libraries
'zxcvbn': {
base: 'node_modules/zxcvbn/dist',
files: [
'node_modules/zxcvbn/dist/zxcvbn.js'
]
},
'select2': {
files: [
'node_modules/select2/dist/css/select2.min.css',
'node_modules/select2/dist/js/select2.full.min.js'
]
},
'luxon': {
files: [
'node_modules/luxon/build/global/luxon.min.js'

View File

@ -1,71 +0,0 @@
$(function () {
$('form.form').not('.vue-form').each(function () {
styleForm(this);
});
});
function styleForm (form) {
var $form = $(form);
// Prevent leaving the page if the form is "dirty" (has unsaved changes).
if ($.fn.dirrty) {
$form.dirrty();
}
$form.find('input:not(input[type=button],input[type=submit],input[type=reset],input[type=radio],input[type=checkbox]),textarea,select').addClass('form-control');
if ($.fn.select2) {
$form.find('select').select2({
width: '100%',
theme: 'bootstrap4',
language: App.lang.locale_short
});
}
autosize($form.find('textarea'));
$form.find('input[type=radio]').each(function () {
$(this).addClass('custom-control-input');
$(this).closest('.form-field');
$(this).next('label').addClass('custom-control-label').addBack().wrapAll('<div class="custom-control custom-radio" />');
});
$form.find('input[type=checkbox]').each(function () {
$(this).addClass('custom-control-input');
$(this).closest('.form-field');
$(this).next('label')
.addClass('custom-control-label')
.addBack()
.wrapAll('<div class="custom-control custom-checkbox" />');
});
$form.find('.help-block').addClass('form-text');
$form.find('.help-block.form-error').parent().addClass('has-error');
$form.find('.help-block.form-success').parent().addClass('has-success');
$form.find('.help-block.form-warning').parent().addClass('has-warning');
// noinspection JSAnnotator
$form.find('label.advanced,fieldset.advanced legend')
.prepend('<span class="text-info">' + App.lang.advanced + '</span> ');
$form.find('input[type=button],input[type=submit],input[type=reset]').addClass('btn m-t-10');
// Scroll to errors.
var error_fields = $form.find('.has-error:visible');
if (error_fields.length > 0) {
$([document.documentElement, document.body]).animate({
scrollTop: error_fields.first().offset().top - $('#header').outerHeight() - 15
}, 1000);
}
}
$('form button.file-upload').on('click', function (e) {
let inputElement = $(this).siblings('input[type=file]')[0];
$(inputElement).trigger('click');
});
$('form input[type=file]').change(function (e) {
let fileNameElement = $(this).siblings('.file-name')[0];
$(fileNameElement).text($(this).val().split('\\').pop());
});

View File

@ -1,59 +0,0 @@
$(document).ready(function () {
$('input[type=password].strength').on('keyup', function (e) {
let currentPassword = $(this).val(),
result = zxcvbn(currentPassword),
score = result.score;
let group = $(this).closest('.form-group');
if (!group.length) {
group = $(this).closest('div');
}
let explanation = group.find('.form-text.password-explanation');
if (!explanation.length) {
explanation = $('<small class="form-text password-explanation" />');
$(this).after(explanation);
explanation = group.find('.form-text.password-explanation');
}
let explanationText = '';
let explanationMeter = $('<meter class="password-strength" min="0" max="4" low="2" high="3" optimum="4" />')
.val(score);
if (currentPassword === '') {
explanationText = App.lang.pw_blank;
} else if (result.feedback.warning) {
explanationText = result.feedback.warning;
} else if (result.feedback.suggestions.length) {
explanationText = result.feedback.suggestions[0];
} else {
explanationText = App.lang.pw_good;
}
explanation.html('')
.append(explanationMeter)
.append('&nbsp;' + explanationText);
group.removeClass('has-error has-success has-warning');
switch (score) {
case 0:
case 1:
group.addClass('has-error');
break;
case 2:
case 3:
group.addClass('has-warning');
break;
case 4:
group.addClass('has-success');
break;
}
});
});

View File

@ -16,7 +16,6 @@
"@fullcalendar/luxon": "^5.9.0",
"@fullcalendar/timegrid": "^5.9.0",
"@fullcalendar/vue": "^5.9.0",
"autosize": "^5.0",
"axios": "^0.24",
"bootstrap": "^4.6.0",
"bootstrap-notify": "^3.1.3",
@ -28,7 +27,6 @@
"codemirror": "^5.62.3",
"css-loader": "^6.5.0",
"del": "^6",
"dirrty": "^1.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4",
@ -56,8 +54,6 @@
"roboto-fontface": "^0.10.0",
"sass": "^1.39.2",
"sass-loader": "^12.2",
"select2": "^4",
"sortablejs": "^1.14.0",
"store": "^1.3.20",
"sweetalert2": "^11",
"vue": "^2.6.14",
@ -2471,11 +2467,6 @@
"node": ">= 4.5.0"
}
},
"node_modules/autosize": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/autosize/-/autosize-5.0.1.tgz",
"integrity": "sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA=="
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
@ -3486,11 +3477,6 @@
"node": ">=8"
}
},
"node_modules/dirrty": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dirrty/-/dirrty-1.0.0.tgz",
"integrity": "sha512-z2wCoTCjnBBJL+8Tb00DqAhrrUzU0o4aNvZ7bTT5k4hM13ir64I6lSs4L/Ea0R2mL+C8vFfRcCco/9Ev17F7PA=="
},
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -7617,11 +7603,6 @@
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"node_modules/select2": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz",
"integrity": "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw=="
},
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@ -7873,11 +7854,6 @@
"node": ">=4"
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -11067,11 +11043,6 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"autosize": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/autosize/-/autosize-5.0.1.tgz",
"integrity": "sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA=="
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
@ -11875,11 +11846,6 @@
"path-type": "^4.0.0"
}
},
"dirrty": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dirrty/-/dirrty-1.0.0.tgz",
"integrity": "sha512-z2wCoTCjnBBJL+8Tb00DqAhrrUzU0o4aNvZ7bTT5k4hM13ir64I6lSs4L/Ea0R2mL+C8vFfRcCco/9Ev17F7PA=="
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -15024,11 +14990,6 @@
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"select2": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz",
"integrity": "sha512-1JeB87s6oN/TDxQQYCvS5EFoQyvV6eYMZZ0AeA4tdFDYWN3BAGZ8npr17UBFddU0lgAt3H0yjX3X6/ekOj1yjw=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@ -15233,11 +15194,6 @@
"is-plain-obj": "^1.0.0"
}
},
"sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

View File

@ -16,7 +16,6 @@
"@fullcalendar/luxon": "^5.9.0",
"@fullcalendar/timegrid": "^5.9.0",
"@fullcalendar/vue": "^5.9.0",
"autosize": "^5.0",
"axios": "^0.24",
"bootstrap": "^4.6.0",
"bootstrap-notify": "^3.1.3",
@ -28,7 +27,6 @@
"codemirror": "^5.62.3",
"css-loader": "^6.5.0",
"del": "^6",
"dirrty": "^1.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4",
@ -56,8 +54,6 @@
"roboto-fontface": "^0.10.0",
"sass": "^1.39.2",
"sass-loader": "^12.2",
"select2": "^4",
"sortablejs": "^1.14.0",
"store": "^1.3.20",
"sweetalert2": "^11",
"vue": "^2.6.14",

View File

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Form\Field;
use App\Exception\CsrfValidationException;
use App\Session;
class Csrf extends \AzuraForms\Field\Csrf
{
protected function verifyCsrf(string $token): bool
{
try {
$this->getCsrf()->verify($token, 'form_' . $this->options['csrf_key']);
} catch (CsrfValidationException) {
return false;
}
return true;
}
protected function generateCsrf(): string
{
return $this->getCsrf()->generate('form_' . $this->options['csrf_key']);
}
protected function getCsrf(): Session\Csrf
{
return Session\Csrf::getInstance();
}
}

View File

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Form\Field;
class File extends \AzuraForms\Field\File
{
public function configure(array $config = []): void
{
parent::configure($config);
$this->options['button_text'] = $this->attributes['button_text'] ?? __('Select File');
$this->options['button_icon'] = $this->attributes['button_icon'] ?? null;
}
public function getField($form_name): ?string
{
[, $class] = $this->_attributeString();
$button_text = $this->options['button_text'];
if ($this->options['button_icon'] !== null) {
$button_text .= sprintf(
' <i class="material-icons" aria-hidden="true">%1$s</i>',
$this->options['button_icon']
);
}
// phpcs:disable Generic.Files.LineLength
$output = '<button name="%1$s_button" id="%2$s_%1$s_button" class="file-upload btn btn-primary btn-block text-center %3$s" type="button">';
$output .= '%4$s';
$output .= '</button>';
$output .= '<small class="file-name"></small>';
$output .= '<input type="file" name="%1$s" id="%2$s_%1$s" style="visibility: hidden; position: absolute; left: -9999px;">';
// phpcs:enable
return sprintf(
$output,
$this->getFullName(),
$form_name,
$class,
$button_text
);
}
}

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Form\Field;
use AzuraForms;
use AzuraForms\Field\Time;
class PlaylistTime extends Time
{
public function __construct(AzuraForms\Form $form, $element_name, array $config = [], $group = null)
{
parent::__construct($form, $element_name, $config, $group);
$this->attributes['pattern'] = '[0-9]{2}:[0-9]{2}';
$this->attributes['placeholder'] = '13:45';
// Handle the "time code" format used by the database entity,
// which is just the regular 24-hour time minus the ":".
$this->filters[] = static function ($new_value) {
// Don't use regular empty() check because 0 (which is 00:00, 12:00AM) is considered empty.
if ('' !== $new_value && null !== $new_value && !str_contains($new_value, ':')) {
$time_code = str_pad($new_value, 4, '0', STR_PAD_LEFT);
return substr($time_code, 0, 2) . ':' . substr($time_code, 2);
}
return $new_value;
};
}
/**
*/
public function getValue(): string|int
{
if (empty($this->value)) {
return '';
}
[$hours, $minutes] = explode(':', $this->value);
return ((int)$hours * 100) + (int)$minutes;
}
}

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Form;
class Form extends \AzuraForms\Form
{
public function __construct(array $options = [], ?array $defaults = null)
{
array_unshift($this->field_namespaces, '\\App\\Form\\Field');
$this->field_name_conversions['playlisttime'] = 'PlaylistTime';
parent::__construct($options, $defaults);
}
protected function addCsrfField(): void
{
$this->addField(
self::CSRF_FIELD_NAME,
Field\Csrf::class,
[
'csrf_key' => $this->name,
]
);
}
/**
* @return string[]
*/
protected function getFormAttributes(): array
{
$attrs = parent::getFormAttributes();
if (!empty($this->options['tabs'])) {
$attrs['novalidate'] = 'novalidate';
}
return $attrs;
}
}

View File

@ -1,38 +0,0 @@
<?php $this->layout('main', ['title' => __('API Keys'), 'manual' => true]); ?>
<div class="card">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('API Keys') ?></h2>
</div>
<table class="table table-responsive-md table-striped mb-0">
<colgroup>
<col width="20%">
<col width="25%">
<col width="30%">
<col width="25%">
</colgroup>
<thead>
<tr>
<th>&nbsp;</th>
<th><?=__('API Key') ?></th>
<th><?=__('Comments') ?></th>
<th><?=__('Owner') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($records as $record): ?>
<tr class="align-middle">
<td class="center">
<div class="btn-group btn-group-sm">
<a class="btn btn-sm btn-primary" href="<?=$router->named('admin:api:edit', ['id' => $record['id']]) ?>"><?=__('Edit') ?></a>
<a class="btn btn-sm btn-danger" href="<?=$router->named('admin:api:delete', ['id' => $record['id'], 'csrf' => $csrf]) ?>"><?=__('Revoke') ?></a>
</div>
</td>
<td><code><?=$record['id'] ?></code></td>
<td><?=$this->e($record['comment']) ?></td>
<td><?=$this->e($record['user']['email']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@ -1,10 +0,0 @@
<?php
/**
* @var \App\Assets $assets
* @var \App\Form\Form $form
*/
$assets
->load('forms_common');
echo $form->render();

View File

@ -1,43 +0,0 @@
<?php
/**
* @var \App\Form\Form $form
* @var string $fieldset_id
* @var array $fieldset
*/
$hide_fieldset = (bool)($fieldset['hide_fieldset'] ?? false);
$use_grid = (bool)($fieldset['use_grid'] ?? false);
?>
<?php if (!$hide_fieldset): ?>
<fieldset id="<?=$fieldset_id ?>" class="<?=($fieldset['class'] ?? '') ?>">
<?php if (!empty($fieldset['legend'])): ?>
<div class="fieldset-legend">
<legend class="<?=($fieldset['legend_class'] ?? '') ?>"><?=$fieldset['legend'] ?></legend>
<?php if (!empty($fieldset['description'])): ?>
<p class="<?=($fieldset['description_class'] ?? '') ?>"><?=$fieldset['description'] ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($use_grid): ?>
<div class="row">
<?php endif; ?>
<?php endif; ?>
<?php
foreach($fieldset['elements'] as $element_id => $element_info) {
if ($form->hasField($element_id)) {
$field = $form->getField($element_id);
echo $field->render($form->getName());
}
}
?>
<?php if (!$hide_fieldset): ?>
<?php if ($use_grid): ?>
</div>
<?php endif; ?>
</fieldset>
<?php endif; ?>

View File

@ -1,115 +0,0 @@
<?php
/**
* @var string|null $title
* @var string|null $header
* @var string|null $stepper
* @var string|null $prefix
* @var \App\Assets $assets
* @var \App\Form\Form $form
*/
$this->layout('main', [
'title' => $title ?? null,
'header' => $header ?? null,
'manual' => true,
]);
$assets
->load('forms_common');
$options = $form->getOptions();
$tabs = [];
$groups = $options['groups'] ?? [];
if (isset($options['tabs'])) {
$i = 0;
foreach ($options['tabs'] as $tab_key => $tab_name) {
$i++;
$tabs[$tab_key] = [
'label' => $tab_name,
'is_active' => 1 === $i,
'groups' => [],
];
}
foreach ($groups as $group_key => $group) {
if (isset($group['tab'])) {
$tab = $group['tab'];
if (isset($tabs[$tab])) {
$tabs[$tab]['groups'][$group_key] = $group;
unset($groups[$group_key]);
}
}
}
$tabs = array_filter($tabs, function ($tab) {
return !empty($tab['groups']);
});
}
?>
<?=($stepper ?? '')?>
<section class="card mb-3" role="region">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=$title?></h2>
</div>
<?php if ($form->hasAnyErrors()): ?>
<div class="card-body alert-danger form-errors d-flex" role="alert">
<div class="flex-shrink-0 mt-3 mr-3">
<i class="material-icons lg" aria-hidden="true">warning</i>
</div>
<div class="flex-fill">
<p><?=__('Errors were encountered when trying to save changes:')?></p>
<dl class="row mb-0">
<?php foreach ($form->getAllErrors() as $error): ?>
<dt class="col-sm-3 text-truncate"><?=($error->hasLabel() ? $error->getLabel() : __('General'))?></dt>
<dd class="col-sm-9"><?=$error->getMessage()?></dd>
<?php endforeach; ?>
</dl>
</div>
</div>
<?php endif; ?>
<?=($prefix ?? '')?>
<?php if (!empty($tabs)): ?>
<div class="card-header">
<ul class="nav nav-justified nav-tabs card-header-tabs" role="tablist">
<?php foreach ($tabs as $tab_key => $tab): ?>
<li class="nav-item">
<a aria-controls="<?=$tab_key?>" aria-selected="true" class="nav-link <?=($tab['is_active'] ? 'active' : '')?>" data-toggle="tab" href="#tab-<?=$tab_key?>" id="tab-<?=$tab_key?>-link" role="tab"><?=$tab['label']?></a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="card-body">
<?=$form->openForm()?>
<?php if (!empty($tabs)): ?>
<div class="tab-content">
<?php foreach ($tabs as $tab_key => $tab): ?>
<div aria-labelledby="tab-<?=$tab_key?>-link" class="tab-pane fade <?=($tab['is_active'] ? 'show active' : '')?>" id="tab-<?=$tab_key?>" role="tabpanel">
<?php foreach ($tab['groups'] as $fieldset_id => $fieldset): ?>
<?=$this->fetch('system/form_group',
['fieldset_id' => $fieldset_id, 'fieldset' => $fieldset, 'form' => $form])?>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php foreach ($groups as $fieldset_id => $fieldset): ?>
<?=$this->fetch('system/form_group',
['fieldset_id' => $fieldset_id, 'fieldset' => $fieldset, 'form' => $form])?>
<?php endforeach; ?>
<?=$form->renderHidden()?>
<?=$form->closeForm()?>
</div>
</section>