diff --git a/assets/js/app.js b/assets/js/app.js index 9abcd7a6..6bd7dc3c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -55,7 +55,10 @@ window.bootbox = require('bootbox') // Includes required for tag input require('./tagsinput.js'); -require('../css/tagsinput.css') +require('../css/tagsinput.css'); + +//Tristate checkbox support +require('./jquery.tristate.js'); require('../ts_src/ajax_ui'); import {ajaxUI} from "../ts_src/ajax_ui"; diff --git a/assets/js/jquery.tristate.js b/assets/js/jquery.tristate.js new file mode 100644 index 00000000..c1a85d29 --- /dev/null +++ b/assets/js/jquery.tristate.js @@ -0,0 +1,213 @@ +/*jslint devel: true, bitwise: true, regexp: true, browser: true, confusion: true, unparam: true, eqeq: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */ +/*globals jQuery */ + +/*! + * Tristate v1.2.1 + * + * Copyright (c) 2013-2017 Martijn W. van der Lee + * Licensed under the MIT. + */ +/* Based on work by: + * Chris Coyier (http://css-tricks.com/indeterminate-checkboxes/) + * + * Tristate checkbox with support features + * pseudo selectors + * val() overwrite + */ + +;(function($, undefined) { + 'use strict'; + + var pluginName = 'tristate', + defaults = { + 'change': undefined, + 'checked': undefined, + 'indeterminate': undefined, + 'init': undefined, + 'reverse': false, + 'state': undefined, + 'unchecked': undefined, + 'value': undefined // one-way only! + }, + valFunction = $.fn.val; + + function Plugin(element, options) { + if($(element).is(':checkbox')) { + this.element = $(element); + this.settings = $.extend( {}, defaults, options ); + this._create(); + } + } + + $.extend(Plugin.prototype, { + _create: function() { + var that = this, + state; + + // Fix for #1 + if (window.navigator.userAgent.indexOf('Trident') >= 0) { + this.element.click(function(e) { + that._change.call(that, e); + that.element.closest('form').change(); + }); + } else { + this.element.change(function(e) { + that._change.call(that, e); + }); + } + + this.settings.checked = this.element.attr('checkedvalue') || this.settings.checked; + this.settings.unchecked = this.element.attr('uncheckedvalue') || this.settings.unchecked; + this.settings.indeterminate = this.element.attr('indeterminatevalue') || this.settings.indeterminate; + + // Initially, set state based on option state or attributes + if (typeof this.settings.state === 'undefined') { + this.settings.state = typeof this.element.attr('indeterminate') !== 'undefined'? null : this.element.is(':checked'); + } + + // If value specified, overwrite with value + if (typeof this.settings.value !== 'undefined') { + state = this._parseValue(this.settings.value); + if (typeof state !== 'undefined') { + this.settings.state = state; + } + } + + this._refresh(this.settings.init); + + return this; + }, + + _change: function(e) { + if (e.isTrigger || !e.hasOwnProperty('which')) { + e.preventDefault(); + } + + switch (this.settings.state) { + case true: this.settings.state = (this.settings.reverse ? false : null); break; + case false: this.settings.state = (this.settings.reverse ? null : true); break; + default: this.settings.state = (this.settings.reverse ? true : false); break; + } + + this._refresh(this.settings.change); + }, + + _refresh: function(callback) { + var value = this.value(); + + this.element.data("vanderlee." + pluginName, value); + + this.element[this.settings.state === null ? 'attr' : 'removeAttr']('indeterminate', 'indeterminate'); + this.element.prop('indeterminate', this.settings.state === null); + this.element.get(0).indeterminate = this.settings.state === null; + + this.element[this.settings.state === true ? 'attr' : 'removeAttr']('checked', true); + this.element.prop('checked', this.settings.state === true); + + if ($.isFunction(callback)) { + callback.call(this.element, this.settings.state, this.value()); + } + }, + + state: function(value) { + if (typeof value === 'undefined') { + return this.settings.state; + } else if (value === true || value === false || value === null) { + this.settings.state = value; + + this._refresh(this.settings.change); + } + return this; + }, + + _parseValue: function(value) { + if (value === this.settings.checked) { + return true; + } else if (value === this.settings.unchecked) { + return false; + } else if (value === this.settings.indeterminate) { + return null; + } + }, + + value: function(value) { + if (typeof value === 'undefined') { + var value; + switch (this.settings.state) { + case true: + value = this.settings.checked; + break; + + case false: + value = this.settings.unchecked; + break; + + case null: + value = this.settings.indeterminate; + break; + } + return typeof value === 'undefined'? this.element.attr('value') : value; + } else { + var state = this._parseValue(value); + if (typeof state !== 'undefined') { + this.settings.state = state; + this._refresh(this.settings.change); + } + } + } + }); + + $.fn[pluginName] = function (options, value) { + var result = this; + + this.each(function() { + if (!$.data(this, "plugin.vanderlee." + pluginName)) { + $.data(this, "plugin.vanderlee." + pluginName, new Plugin(this, options)); + } else if (typeof options === 'string') { + if (typeof value === 'undefined') { + result = $(this).data("plugin.vanderlee." + pluginName)[options](); + return false; + } else { + $(this).data("plugin.vanderlee." + pluginName)[options](value); + } + } + }); + + return result; + }; + + // Overwrite fn.val + $.fn.val = function(value) { + var data = this.data("vanderlee." + pluginName); + if (typeof data === 'undefined') { + if (typeof value === 'undefined') { + return valFunction.call(this); + } else { + return valFunction.call(this, value); + } + } else { + if (typeof value === 'undefined') { + return data; + } else { + this.data("vanderlee." + pluginName, value); + return this; + } + } + }; + + // :indeterminate pseudo selector + $.expr.filters.indeterminate = function(element) { + var $element = $(element); + return typeof $element.data("vanderlee." + pluginName) !== 'undefined' && $element.prop('indeterminate'); + }; + + // :determinate pseudo selector + $.expr.filters.determinate = function(element) { + return !($.expr.filters.indeterminate(element)); + }; + + // :tristate selector + $.expr.filters.tristate = function(element) { + return typeof $(element).data("vanderlee." + pluginName) !== 'undefined'; + }; +})(jQuery); diff --git a/assets/ts_src/ajax_ui.ts b/assets/ts_src/ajax_ui.ts index 30acc07e..cb76eb49 100644 --- a/assets/ts_src/ajax_ui.ts +++ b/assets/ts_src/ajax_ui.ts @@ -260,7 +260,7 @@ class AjaxUI { { return { success: this.onAjaxComplete, - beforeSerialize: function() : boolean { + beforeSerialize: function($form, options) : boolean { //Update the content of textarea fields using CKEDITOR before submitting. //@ts-ignore @@ -272,6 +272,9 @@ class AjaxUI { } } + //Check every checkbox field, so that it will be submitted (only valid fields are submitted) + $form.find("input[type=checkbox].tristate").prop('checked', true); + return true; }, beforeSubmit: function (arr, $form, options) : boolean { diff --git a/assets/ts_src/event_listeners.ts b/assets/ts_src/event_listeners.ts index 47e9f785..a5ea85e8 100644 --- a/assets/ts_src/event_listeners.ts +++ b/assets/ts_src/event_listeners.ts @@ -212,6 +212,15 @@ $(document).on("ajaxUI:start ajaxUI:reload", function() { }); }); +$(document).on("ajaxUI:start ajaxUI:reload", function() { + //@ts-ignore + $(".tristate").tristate( { + checked: "true", + unchecked: "false", + indeterminate: "indeterminate", + }); +}); + //Re initialize fileinputs on reload $(document).on("ajaxUI:reload", function () { //@ts-ignore diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index c4a7d885..a6e9458c 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -2,7 +2,7 @@ twig: default_path: '%kernel.project_dir%/templates' debug: '%kernel.debug%' strict_variables: '%kernel.debug%' - form_themes: ['bootstrap_4_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig' ] + form_themes: ['bootstrap_4_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ] globals: partdb_title: '%partdb_title%' diff --git a/config/permissions.yaml b/config/permissions.yaml index 9ded2599..b54f9935 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -1,23 +1,37 @@ # In this file the possible permissions are defined. # This should be compatible with the legacy Part-DB +groups: + parts: + label: "perm.group.parts" + structures: + label: "perm.group.structures" + system: + label: "perm.group.system" + + perms: # Here comes a list with all Permission names (they have a perm_[name] coloumn in DB) # Part related permissions parts: # e.g. this maps to perms_parts in User/Group database - # label: "perm.parts" + group: "parts" + label: "perm.parts" operations: # Here are all possible operations are listed => the op name is mapped to bit value read: + label: "perm.read" bit: 0 edit: - # label: "perm.part.edit" + label: "perm.edit" bit: 2 create: + label: "perm.create" bit: 4 move: + label: "perm.part.move" bit: 6 delete: + label: "perm.delete" bit: 8 search: bit: 10 @@ -41,10 +55,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 30 parts_name: &PART_ATTRIBUTE # We define a template here, that we can use for all part attributes. + group: "parts" operations: read: + label: "perm.read" bit: 0 edit: + label: "perm.edit" bit: 2 parts_description: @@ -81,6 +98,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co *PART_ATTRIBUTE storelocations: &PART_CONTAINING + group: "structures" operations: read: bit: 0 @@ -131,6 +149,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 10 groups: + group: "system" operations: read: bit: 0 @@ -146,6 +165,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 10 users: + group: "system" operations: read: bit: 0 @@ -167,6 +187,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 16 database: + group: "system" operations: see_status: bit: 0 @@ -178,6 +199,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 2 config: + group: "system" operations: read_config: bit: 0 @@ -187,6 +209,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 6 system: + group: "system" operations: use_debug: bit: 0 @@ -196,6 +219,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co bit: 4 devices_parts: + group: "parts" operations: read: bit: 0 diff --git a/src/Configuration/PermissionsConfiguration.php b/src/Configuration/PermissionsConfiguration.php index 52e26358..0c34b2ef 100644 --- a/src/Configuration/PermissionsConfiguration.php +++ b/src/Configuration/PermissionsConfiguration.php @@ -44,11 +44,18 @@ class PermissionsConfiguration implements ConfigurationInterface $treeBuilder = new TreeBuilder('permissions'); $rootNode = $treeBuilder->root('permissions'); + $rootNode->children() + ->arrayNode('groups') + ->arrayPrototype() + ->children() + ->scalarNode('label')->end(); + $rootNode->children() ->arrayNode('perms') ->arrayPrototype() ->children() ->scalarNode('label')->end() + ->scalarNode('group')->end() ->arrayNode('operations') ->arrayPrototype() ->children() diff --git a/src/Entity/UserSystem/Group.php b/src/Entity/UserSystem/Group.php index c8525004..e522ed1a 100644 --- a/src/Entity/UserSystem/Group.php +++ b/src/Entity/UserSystem/Group.php @@ -64,6 +64,12 @@ class Group extends StructuralDBElement implements HasPermissionsInterface */ protected $permissions; + public function __construct() + { + parent::__construct(); + $this->permissions = new PermissionsEmbed(); + } + /** * Returns the ID as an string, defined by the element class. * This should have a form like P000014, for a part with ID 14. diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 6a693e96..5fe135b4 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -186,6 +186,11 @@ class User extends NamedDBElement implements UserInterface, HasPermissionsInterf */ protected $instock_comment_a; + public function __construct() + { + $this->permissions = new PermissionsEmbed(); + } + /** * Checks if the current user, is the user which represents the not logged in (anonymous) users. * diff --git a/src/Form/Permissions/PermissionGroupType.php b/src/Form/Permissions/PermissionGroupType.php new file mode 100644 index 00000000..1cb9cd4a --- /dev/null +++ b/src/Form/Permissions/PermissionGroupType.php @@ -0,0 +1,95 @@ +resolver = $resolver; + $this->perm_structure = $resolver->getPermissionStructure(); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $permissions = $this->perm_structure['perms']; + + foreach ($permissions as $key => $permission) { + //Check if the permission belongs to our group + if (isset($permission['group'])) { + if ($permission['group'] !== $options['group_name']) { + continue; + } + } else { + //Skip perrmissions without groups unless we have this as blanko group + if ($options['group_name'] !== "*") { + continue; + } + } + + $builder->add($key, PermissionType::class, [ + 'perm_name' => $key, + 'label' => $permission['label'] ?? $key, + 'mapped' => false, + 'data' => $builder->getData(), + 'disabled' => $options['disabled'] + ]); + } + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefault('group_name', function (Options $options) { + return trim($options['name']); + }); + + $resolver->setDefault('label', function (Options $options) { + if (!empty($this->perm_structure['groups'][$options['group_name']]['label'])) { + return $this->perm_structure['groups'][$options['group_name']]['label']; + } + + return $options['name']; + }); + } +} \ No newline at end of file diff --git a/src/Form/Permissions/PermissionType.php b/src/Form/Permissions/PermissionType.php new file mode 100644 index 00000000..de9f8835 --- /dev/null +++ b/src/Form/Permissions/PermissionType.php @@ -0,0 +1,157 @@ +resolver = $resolver; + $this->perm_structure = $resolver->getPermissionStructure(); + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefault('perm_name', function (Options $options) { + return $options['name']; + }); + + $resolver->setDefault('label', function (Options $options) { + if (!empty($this->perm_structure['perms'][$options['perm_name']]['label'])) { + return $this->perm_structure['perms'][$options['perm_name']]['label']; + } + + return $options['name']; + }); + + $resolver->setDefaults([ + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $operations = $this->perm_structure['perms'][$options['perm_name']]['operations']; + + foreach ($operations as $key => $operation) { + $builder->add($key, TriStateCheckboxType::class, [ + 'required' => false, + 'mapped' => false, + 'label' => $operation['label'] ?? null, + 'disabled' => $options['disabled'] + ]); + } + + $builder->setDataMapper($this); + } + + /** + * Maps the view data of a compound form to its children. + * + * The method is responsible for calling {@link FormInterface::setData()} + * on the children of compound forms, defining their underlying model data. + * + * @param mixed $viewData View data of the compound form being initialized + * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances + * + * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported + */ + public function mapDataToForms($viewData, $forms) + { + foreach ($forms as $form) { + $value = $this->resolver->dontInherit( + $viewData, + $form->getParent()->getConfig()->getOption('perm_name'), + $form->getName() + ); + $form->setData($value); + } + } + + /** + * Maps the model data of a list of children forms into the view data of their parent. + * + * This is the internal cascade call of FormInterface::submit for compound forms, since they + * cannot be bound to any input nor the request as scalar, but their children may: + * + * $compoundForm->submit($arrayOfChildrenViewData) + * // inside: + * $childForm->submit($childViewData); + * // for each entry, do the same and/or reverse transform + * $this->dataMapper->mapFormsToData($compoundForm, $compoundInitialViewData) + * // then reverse transform + * + * When a simple form is submitted the following is happening: + * + * $simpleForm->submit($submittedViewData) + * // inside: + * $this->viewData = $submittedViewData + * // then reverse transform + * + * The model data can be an array or an object, so this second argument is always passed + * by reference. + * + * @param FormInterface[]|\Traversable $forms A list of {@link FormInterface} instances + * @param mixed $viewData The compound form's view data that get mapped + * its children model data + * + * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported + */ + public function mapFormsToData($forms, &$viewData) + { + foreach ($forms as $form) { + $value = $form->getData(); + $this->resolver->setPermission( + $viewData, + $form->getParent()->getConfig()->getOption('perm_name'), + $form->getName(), + $value + ); + } + } +} \ No newline at end of file diff --git a/src/Form/Permissions/PermissionsType.php b/src/Form/Permissions/PermissionsType.php new file mode 100644 index 00000000..e9ff577d --- /dev/null +++ b/src/Form/Permissions/PermissionsType.php @@ -0,0 +1,72 @@ +resolver = $resolver; + $this->perm_structure = $resolver->getPermissionStructure(); + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $groups = $this->perm_structure['groups']; + + foreach ($groups as $key => $group) { + $builder->add($key,PermissionGroupType::class, [ + 'group_name' => $key, + 'mapped' => false, + 'data' => $builder->getData(), + 'disabled' => $options['disabled'] + ]); + } + + $builder->add('blanko', PermissionGroupType::class, [ + 'group_name' => '*', + 'label' => 'perm.group.other', + 'mapped' => false, + 'data' => $builder->getData(), + 'disabled' => $options['disabled'] + ]); + } +} \ No newline at end of file diff --git a/src/Form/Type/TriStateCheckboxType.php b/src/Form/Type/TriStateCheckboxType.php new file mode 100644 index 00000000..73434892 --- /dev/null +++ b/src/Form/Type/TriStateCheckboxType.php @@ -0,0 +1,174 @@ +addViewTransformer($this); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'label_attr' => ['class' => 'checkbox-custom checkbox-inline'], + 'attr' => ['class' => 'tristate'], + 'compound' => false + ]); + } + + public function getBlockPrefix() + { + return 'tristate'; + } + + /** + * {@inheritdoc} + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars = array_replace($view->vars, [ + 'value' => $form->getViewData(), + 'checked' => true === $form->getData(), + 'indeterminate' => null === $form->getData() + ]); + } + + /** + * Transforms a value from the original representation to a transformed representation. + * + * This method is called when the form field is initialized with its default data, on + * two occasions for two types of transformers: + * + * 1. Model transformers which normalize the model data. + * This is mainly useful when the same form type (the same configuration) + * has to handle different kind of underlying data, e.g The DateType can + * deal with strings or \DateTime objects as input. + * + * 2. View transformers which adapt the normalized data to the view format. + * a/ When the form is simple, the value returned by convention is used + * directly in the view and thus can only be a string or an array. In + * this case the data class should be null. + * + * b/ When the form is compound the returned value should be an array or + * an object to be mapped to the children. Each property of the compound + * data will be used as model data by each child and will be transformed + * too. In this case data class should be the class of the object, or null + * when it is an array. + * + * All transformers are called in a configured order from model data to view value. + * At the end of this chain the view data will be validated against the data class + * setting. + * + * This method must be able to deal with empty values. Usually this will + * be NULL, but depending on your implementation other empty values are + * possible as well (such as empty strings). The reasoning behind this is + * that data transformers must be chainable. If the transform() method + * of the first data transformer outputs NULL, the second must be able to + * process that value. + * + * @param mixed $value The value in the original representation + * + * @return mixed The value in the transformed representation + * + * @throws TransformationFailedException when the transformation fails + */ + public function transform($value) + { + if ($value === true) { + return "true"; + } + + if ($value === false) { + return "false"; + } + + if ($value === null) { + return "indeterminate"; + } + + throw new \InvalidArgumentException('Invalid value encountered!: ' . $value); + } + + /** + * Transforms a value from the transformed representation to its original + * representation. + * + * This method is called when {@link Form::submit()} is called to transform the requests tainted data + * into an acceptable format. + * + * The same transformers are called in the reverse order so the responsibility is to + * return one of the types that would be expected as input of transform(). + * + * This method must be able to deal with empty values. Usually this will + * be an empty string, but depending on your implementation other empty + * values are possible as well (such as NULL). The reasoning behind + * this is that value transformers must be chainable. If the + * reverseTransform() method of the first value transformer outputs an + * empty string, the second value transformer must be able to process that + * value. + * + * By convention, reverseTransform() should return NULL if an empty string + * is passed. + * + * @param mixed $value The value in the transformed representation + * + * @return mixed The value in the original representation + * + * @throws TransformationFailedException when the transformation fails + */ + public function reverseTransform($value) + { + switch ($value) { + case "true": + return true; + case "false": + case '': + return false; + case "indeterminate": + return null; + default: + throw new \InvalidArgumentException('Invalid value encountered!: ' . $value); + } + } +} \ No newline at end of file diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index f6704e55..b0a91fea 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -35,6 +35,8 @@ namespace App\Form; use App\Entity\UserSystem\Group; use App\Entity\Base\NamedDBElement; use App\Entity\Base\StructuralDBElement; +use App\Form\Permissions\PermissionsType; +use App\Form\Permissions\PermissionType; use App\Form\Type\StructuralEntityType; use FOS\CKEditorBundle\Form\Type\CKEditorType; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -111,6 +113,11 @@ class UserAdminForm extends AbstractType 'disabled' => !$this->security->isGranted('edit_infos', $entity), ]) + ->add('permissions', PermissionsType::class, [ + 'mapped' => false, + 'data' => $builder->getData(), + //'user' => $builder->getData(), + ]) ; /*->add('comment', CKEditorType::class, ['required' => false, 'label' => 'comment.label', 'attr' => ['rows' => 4], 'help' => 'bbcode.hint', diff --git a/src/Services/PermissionResolver.php b/src/Services/PermissionResolver.php index 8f92cbe0..62c5df86 100644 --- a/src/Services/PermissionResolver.php +++ b/src/Services/PermissionResolver.php @@ -60,12 +60,17 @@ class PermissionResolver - $this->permission_structure = $this->getPermissionStructure(); + $this->permission_structure = $this->generatePermissionStructure(); //dump($this->permission_structure); } - protected function getPermissionStructure() + public function getPermissionStructure() : array + { + return $this->permission_structure; + } + + protected function generatePermissionStructure() { $cache = new ConfigCache($this->cache_file, $this->is_debug); @@ -166,6 +171,24 @@ class PermissionResolver return null; //The inherited value is never resolved. Should be treat as false, in Voters. } + /** + * Sets the new value for the operation + * @param HasPermissionsInterface $user The user or group for which the value should be changed. + * @param string $permission The name of the permission that should be changed. + * @param string $operation The name of the operation that should be changed. + * @param bool|null $new_val The new value for the permission. true = ALLOW, false = DISALLOW, null = INHERIT + */ + public function setPermission(HasPermissionsInterface $user, string $permission, string $operation, ?bool $new_val) : void + { + //Get the permissions from the user + $perm_list = $user->getPermissions(); + + //Determine bit number using our configuration + $bit = $this->permission_structure['perms'][$permission]['operations'][$operation]['bit']; + + $perm_list->setPermissionValue($permission, $bit, $new_val); + } + /** * Lists the names of all operations that is supported for the given permission. * diff --git a/templates/AdminPages/UserAdmin.html.twig b/templates/AdminPages/UserAdmin.html.twig index 2ab2fd88..5ed2e3e0 100644 --- a/templates/AdminPages/UserAdmin.html.twig +++ b/templates/AdminPages/UserAdmin.html.twig @@ -6,10 +6,21 @@ {% block comment %}{% endblock %} +{% block additional_pills %} + +{% endblock %} + + {% block additional_controls %} {{ form_row(form.group) }} {{ form_row(form.first_name) }} {{ form_row(form.last_name) }} {{ form_row(form.email) }} {{ form_row(form.department) }} +{% endblock %} + +{% block additional_panes %} +
+ {{ form_row(form.permissions) }} +
{% endblock %} \ No newline at end of file diff --git a/templates/Form/extendedBootstrap4_layout.html.twig b/templates/Form/extendedBootstrap4_layout.html.twig index a2279744..06e3cdd5 100644 --- a/templates/Form/extendedBootstrap4_layout.html.twig +++ b/templates/Form/extendedBootstrap4_layout.html.twig @@ -44,4 +44,68 @@ {{ form_errors(form.value) }} -{% endblock %} \ No newline at end of file +{% endblock %} + +{####################################################################################### +# +# Definitions for Tristate Checkbox Type (mostly based on bootstrap checkbox type) +# +#######################################################################################} + +{% block tristate_label -%} + {#- Do not display the label if widget is not defined in order to prevent double label rendering -#} + {%- if widget is defined -%} + {% set is_parent_custom = parent_label_class is defined and ('checkbox-custom' in parent_label_class or 'radio-custom' in parent_label_class) %} + {% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class) %} + {%- if is_parent_custom or is_custom -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' custom-control-label')|trim}) -%} + {%- else %} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' form-check-label')|trim}) -%} + {%- endif %} + {%- if not compound -%} + {% set label_attr = label_attr|merge({'for': id}) %} + {%- endif -%} + {%- if required -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%} + {%- endif -%} + {%- if parent_label_class is defined -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': ''})|trim}) -%} + {%- endif -%} + {%- if label is not same as(false) and label is empty -%} + {%- if label_format is not empty -%} + {%- set label = label_format|replace({ + '%name%': name, + '%id%': id, + }) -%} + {%- else -%} + {%- set label = name|humanize -%} + {%- endif -%} + {%- endif -%} + + {{ widget|raw }} + + {{- label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain)) -}} + {{- form_errors(form) -}} + + {%- endif -%} +{%- endblock tristate_label %} + +{%- block tr_parent -%} + +{%- endblock tr_parent -%} + +{% block tristate_widget -%} + {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} + {%- if 'checkbox-custom' in parent_label_class -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%} +
+ {{- form_label(form, null, { widget: block('tr_parent') }) -}} +
+ {%- else -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%} +
+ {{- form_label(form, null, { widget: block('tr_parent') }) -}} +
+ {%- endif -%} +{%- endblock tristate_widget %} + diff --git a/templates/Form/permissionLayout.html.twig b/templates/Form/permissionLayout.html.twig new file mode 100644 index 00000000..c95b2a19 --- /dev/null +++ b/templates/Form/permissionLayout.html.twig @@ -0,0 +1,52 @@ +{% block permission_row %} + + + {{ form.vars.label | trans }} + {{ form_errors(form) }} + + + {% for op in form %} + {{ form_widget(op) }} + {{ form_errors(op) }} + {% endfor %} + + +{% endblock %} + +{% block permission_group_row %} + {{ form_errors(form) }} + + + + + + + + + + {% for perm in form %} + {{ form_row(perm) }} + {% endfor %} + +
{% trans %}permission.edit.permission{% endtrans %}{% trans %}permission.edit.value{% endtrans %}
+{% endblock %} + +{% block permissions_row %} + + +
+ {% for group in form %} +
+ {{ form_row(group) }} +
+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file