From bb0f58e6cbeca08aff5bdf78aee04f2ad57689e3 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Sat, 18 Jan 2025 11:02:25 -0500 Subject: [PATCH 1/9] custom field option profile restrictions --- .../ProfileRestrictOption.php | 98 +++++++++++++++++++ .../Asset/CustomFieldType/AbstractType.php | 4 +- tests/cypress/e2e/Asset/custom_fields.cy.js | 2 +- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php diff --git a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php new file mode 100644 index 00000000000..e764e43ad40 --- /dev/null +++ b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php @@ -0,0 +1,98 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Asset\CustomFieldOption; + +use Glpi\Application\View\TemplateRenderer; + +class ProfileRestrictOption extends AbstractOption +{ + public function getFormInput(): string + { + $twig_params = [ + 'item' => $this->custom_field, + 'key' => $this->getKey(), + 'label' => $this->getName(), + 'value' => parent::getValue(), + 'inverted' => $this->getInverted(), + ]; + // language=Twig + return TemplateRenderer::getInstance()->renderFromStringTemplate(<<custom_field->fields['field_options'][$this->getKey() . '_invert'] ?? false); + } + + public function getValue(): bool + { + $inverted = $this->getInverted(); + $value = parent::getValue() ?? []; + + if (!is_array($value)) { + $value = [$value]; + } + + // Handle special 'All' value + if (in_array(-1, $value, true)) { + return !$inverted; + } + + $active_profile = $_SESSION['glpiactiveprofile']['id'] ?? null; + if ($active_profile === null) { + return false; + } + return $inverted ? !in_array($active_profile, $value, false) : in_array($active_profile, $value, false); + } +} diff --git a/src/Glpi/Asset/CustomFieldType/AbstractType.php b/src/Glpi/Asset/CustomFieldType/AbstractType.php index 53c3750fb30..7db4b5f9fc2 100644 --- a/src/Glpi/Asset/CustomFieldType/AbstractType.php +++ b/src/Glpi/Asset/CustomFieldType/AbstractType.php @@ -36,6 +36,7 @@ use Glpi\Asset\CustomFieldDefinition; use Glpi\Asset\CustomFieldOption\BooleanOption; +use Glpi\Asset\CustomFieldOption\ProfileRestrictOption; use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QueryFunction; @@ -70,9 +71,10 @@ public function getOptions(): array { return [ new BooleanOption($this->custom_field, 'full_width', __('Full width'), false), - new BooleanOption($this->custom_field, 'readonly', __('Readonly'), false), + new ProfileRestrictOption($this->custom_field, 'readonly', __('Readonly'), false), new BooleanOption($this->custom_field, 'required', __('Mandatory'), false), new BooleanOption($this->custom_field, 'disabled', __('Disabled'), false), // Not exposed in the UI. Only used in field order preview + new ProfileRestrictOption($this->custom_field, 'hidden', __('Hidden'), false), ]; } diff --git a/tests/cypress/e2e/Asset/custom_fields.cy.js b/tests/cypress/e2e/Asset/custom_fields.cy.js index 032b5a71b7f..c8daeb257b8 100644 --- a/tests/cypress/e2e/Asset/custom_fields.cy.js +++ b/tests/cypress/e2e/Asset/custom_fields.cy.js @@ -144,7 +144,7 @@ describe("Custom Assets - Custom Fields", () => { cy.findByLabelText('Multiple values').check(); } if (options.has('readonly')) { - cy.findByLabelText('Readonly').check(); + cy.findByLabelText('Readonly').selectDropdownValue('Super-Admin'); } if (options.has('mandatory')) { cy.findByLabelText('Mandatory').check(); From 0ba3ef76d358c1db3bc1e1e645d8b0d7e90b3ab9 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Mon, 10 Feb 2025 21:53:16 -0500 Subject: [PATCH 2/9] handle hidden custom fields --- src/Glpi/Asset/Asset.php | 3 +++ src/Glpi/Asset/CustomFieldType/AbstractType.php | 5 +---- src/Glpi/Asset/CustomFieldType/TypeInterface.php | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Glpi/Asset/Asset.php b/src/Glpi/Asset/Asset.php index d952caf5764..f52930a53a9 100644 --- a/src/Glpi/Asset/Asset.php +++ b/src/Glpi/Asset/Asset.php @@ -337,6 +337,9 @@ public function showForm($ID, array $options = []) $fields_display = static::getDefinition()->getDecodedFieldsField(); $core_field_options = []; + // Remove custom fields that are hidden for the current profile + $custom_fields = array_filter($custom_fields, static fn ($f) => !$f->getFieldType()->getOptionValues()['hidden']); + foreach ($fields_display as $field) { $core_field_options[$field['key']] = $field['field_options'] ?? []; } diff --git a/src/Glpi/Asset/CustomFieldType/AbstractType.php b/src/Glpi/Asset/CustomFieldType/AbstractType.php index 7db4b5f9fc2..2f08fe8f313 100644 --- a/src/Glpi/Asset/CustomFieldType/AbstractType.php +++ b/src/Glpi/Asset/CustomFieldType/AbstractType.php @@ -78,10 +78,7 @@ public function getOptions(): array ]; } - /** - * @return array - */ - protected function getOptionValues(bool $default_field = false): array + public function getOptionValues(bool $default_field = false): array { $values = []; foreach ($this->getOptions() as $option) { diff --git a/src/Glpi/Asset/CustomFieldType/TypeInterface.php b/src/Glpi/Asset/CustomFieldType/TypeInterface.php index 2c85fe79079..93659e0073b 100644 --- a/src/Glpi/Asset/CustomFieldType/TypeInterface.php +++ b/src/Glpi/Asset/CustomFieldType/TypeInterface.php @@ -88,6 +88,11 @@ public function formatValueFromDB(mixed $value): mixed; */ public function getOptions(): array; + /** + * @return array + */ + public function getOptionValues(bool $default_field = false): array; + /** * Defines configured default value. */ From c776f1726f656e913cbe9ff92f1a0d404d273447 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 11 Feb 2025 07:49:00 -0500 Subject: [PATCH 3/9] fix e2e --- ajax/asset/assetdefinition.php | 2 +- tests/cypress/e2e/Asset/custom_fields.cy.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ajax/asset/assetdefinition.php b/ajax/asset/assetdefinition.php index 53b74ed186a..de036423f32 100644 --- a/ajax/asset/assetdefinition.php +++ b/ajax/asset/assetdefinition.php @@ -38,9 +38,9 @@ use Glpi\Exception\Http\NotFoundHttpException; Session::checkRight(AssetDefinition::$rightname, READ); -Session::writeClose(); if ($_REQUEST['action'] === 'get_all_fields') { + Session::writeClose(); header("Content-Type: application/json; charset=UTF-8"); $definition = new AssetDefinition(); if (!$definition->getFromDB($_GET['assetdefinitions_id'])) { diff --git a/tests/cypress/e2e/Asset/custom_fields.cy.js b/tests/cypress/e2e/Asset/custom_fields.cy.js index c8daeb257b8..6487163ac07 100644 --- a/tests/cypress/e2e/Asset/custom_fields.cy.js +++ b/tests/cypress/e2e/Asset/custom_fields.cy.js @@ -144,7 +144,7 @@ describe("Custom Assets - Custom Fields", () => { cy.findByLabelText('Multiple values').check(); } if (options.has('readonly')) { - cy.findByLabelText('Readonly').selectDropdownValue('Super-Admin'); + cy.getDropdownByLabelText('Readonly').selectDropdownValue('Super-Admin'); } if (options.has('mandatory')) { cy.findByLabelText('Mandatory').check(); @@ -211,13 +211,12 @@ describe("Custom Assets - Custom Fields", () => { cy.get('.sortable-field[data-key="name"] .edit-field').click({force: true}); cy.get('#core_field_options_editor').within(() => { cy.findByLabelText('Full width').should('be.visible').check(); - cy.findByLabelText('Readonly').should('be.visible').check(); + cy.getDropdownByLabelText('Readonly').selectDropdownValue('Super-Admin'); cy.findByLabelText('Mandatory').should('be.visible').check(); - cy.intercept('POST', '/ajax/asset/assetdefinition.php').as('saveFieldOptions'); cy.findByRole('button', {name: 'Save'}).click(); }); cy.get('input[name="field_options[name][full_width]"]').should('have.value', '1'); - cy.get('input[name="field_options[name][readonly]"]').should('have.value', '1'); + cy.get('input[name="field_options[name][readonly][]"]').should('have.value', '4'); cy.get('input[name="field_options[name][required]"]').should('have.value', '1'); }); }); From 63bb0d306cf979648f2c3f9060344cf1652cf41c Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 11 Feb 2025 14:11:07 -0500 Subject: [PATCH 4/9] handle for core fields --- .../CustomObject/FieldPreview/FieldDisplay.vue | 10 ++++++++-- src/Glpi/Asset/Asset.php | 15 ++++++++++++--- src/Glpi/Asset/AssetDefinition.php | 2 +- .../CustomFieldOption/ProfileRestrictOption.php | 6 +++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue index 8e4505aba74..d210bb5e5ea 100644 --- a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue +++ b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue @@ -242,8 +242,14 @@ sortable_field.field_options = {}; form_data.entries().forEach(([name, value]) => { if (name.startsWith('field_options[')) { - const option_name = name.replace('field_options[', '').slice(0, -1); - sortable_field.field_options[option_name] = value; + const is_array = name.endsWith('[]'); + const option_name = name.replace('field_options[', '').replace(/\[\]/, ''); + if (is_array) { + sortable_field.field_options[option_name] = sortable_field.field_options[option_name] ?? []; + sortable_field.field_options[option_name].push(value); + } else { + sortable_field.field_options[option_name] = value; + } } }); } else if (btn_submit.attr('name') === 'purge') { diff --git a/src/Glpi/Asset/Asset.php b/src/Glpi/Asset/Asset.php index f52930a53a9..2c4833c9139 100644 --- a/src/Glpi/Asset/Asset.php +++ b/src/Glpi/Asset/Asset.php @@ -337,20 +337,29 @@ public function showForm($ID, array $options = []) $fields_display = static::getDefinition()->getDecodedFieldsField(); $core_field_options = []; - // Remove custom fields that are hidden for the current profile + // Remove fields that are hidden for the current profile $custom_fields = array_filter($custom_fields, static fn ($f) => !$f->getFieldType()->getOptionValues()['hidden']); + $core_fields = static::getDefinition()->getAllFields(); foreach ($fields_display as $field) { - $core_field_options[$field['key']] = $field['field_options'] ?? []; + $f = new CustomFieldDefinition(); + $core_field = $core_fields[$field['key']]; + $f->fields['system_name'] = $field['key']; + $f->fields['type'] = $core_field['type']; + $f->fields['field_options'] = $field['field_options'] ?? []; + $core_field_options[$field['key']] = $f->getFieldType()->getOptionValues(); } + $field_order = $this->getFormFields(); + $field_order = array_filter($field_order, static fn ($f) => $core_field_options[$f]['hidden'] !== true); + TemplateRenderer::getInstance()->display( 'pages/assets/asset.html.twig', [ 'item' => $this, 'params' => $options, 'custom_fields' => $custom_fields, - 'field_order' => $this->getFormFields(), + 'field_order' => $field_order, 'additional_field_options' => $core_field_options, ] ); diff --git a/src/Glpi/Asset/AssetDefinition.php b/src/Glpi/Asset/AssetDefinition.php index 05d4dff4677..0507f7a8b41 100644 --- a/src/Glpi/Asset/AssetDefinition.php +++ b/src/Glpi/Asset/AssetDefinition.php @@ -237,7 +237,7 @@ public function showFieldOptionsForCoreField(string $field_key, array $field_opt $custom_field->fields['itemtype'] = \Computer::class; // Doesn't matter what it is as long as it's not empty $custom_field->fields['field_options'] = $field_options; - $options_allowlist = ['required', 'readonly', 'full_width']; + $options_allowlist = ['required', 'readonly', 'full_width', 'hidden']; $twig_params = [ 'options' => array_filter($custom_field->getFieldType()->getOptions(), static fn ($option) => in_array($option->getKey(), $options_allowlist, true)), diff --git a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php index e764e43ad40..7e04dd79dd8 100644 --- a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php +++ b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php @@ -40,11 +40,15 @@ class ProfileRestrictOption extends AbstractOption { public function getFormInput(): string { + $value = parent::getValue(); + if (!is_array($value)) { + $value = [$value]; + } $twig_params = [ 'item' => $this->custom_field, 'key' => $this->getKey(), 'label' => $this->getName(), - 'value' => parent::getValue(), + 'value' => $value, 'inverted' => $this->getInverted(), ]; // language=Twig From c77f37a8e547ae0ee811bd0a67840a38ba789a7e Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 11 Feb 2025 16:52:16 -0500 Subject: [PATCH 5/9] fix array handling --- js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue index d210bb5e5ea..465dc373bf3 100644 --- a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue +++ b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue @@ -246,6 +246,9 @@ const option_name = name.replace('field_options[', '').replace(/\[\]/, ''); if (is_array) { sortable_field.field_options[option_name] = sortable_field.field_options[option_name] ?? []; + if (!Array.isArray(sortable_field.field_options[option_name])) { + sortable_field.field_options[option_name] = [sortable_field.field_options[option_name]]; + } sortable_field.field_options[option_name].push(value); } else { sortable_field.field_options[option_name] = value; From 001158d29105840cda9d671fc30435b75d139ddf Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 18 Feb 2025 13:30:10 -0500 Subject: [PATCH 6/9] remove slider --- .../ProfileRestrictOption.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php index 7e04dd79dd8..455ae10b4c4 100644 --- a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php +++ b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php @@ -54,16 +54,8 @@ public function getFormInput(): string // language=Twig return TemplateRenderer::getInstance()->renderFromStringTemplate(<<custom_field->fields['field_options'][$this->getKey() . '_invert'] ?? false); - } - public function getValue(): bool { - $inverted = $this->getInverted(); $value = parent::getValue() ?? []; if (!is_array($value)) { @@ -90,13 +76,13 @@ public function getValue(): bool // Handle special 'All' value if (in_array(-1, $value, true)) { - return !$inverted; + return true; } $active_profile = $_SESSION['glpiactiveprofile']['id'] ?? null; if ($active_profile === null) { return false; } - return $inverted ? !in_array($active_profile, $value, false) : in_array($active_profile, $value, false); + return in_array($active_profile, $value, false); } } From 2f2b1be58b065059cc46cd866b4e730c614cba87 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 18 Feb 2025 13:30:31 -0500 Subject: [PATCH 7/9] fix empty value on selection --- src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php index 455ae10b4c4..6755f0d79b1 100644 --- a/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php +++ b/src/Glpi/Asset/CustomFieldOption/ProfileRestrictOption.php @@ -48,8 +48,7 @@ public function getFormInput(): string 'item' => $this->custom_field, 'key' => $this->getKey(), 'label' => $this->getName(), - 'value' => $value, - 'inverted' => $this->getInverted(), + 'value' => array_filter($value), ]; // language=Twig return TemplateRenderer::getInstance()->renderFromStringTemplate(<< Date: Tue, 18 Feb 2025 13:33:38 -0500 Subject: [PATCH 8/9] match modal sizes --- js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue index 465dc373bf3..9acac8bf3c3 100644 --- a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue +++ b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue @@ -158,7 +158,7 @@ const url = `${CFG_GLPI.root_doc}/ajax/asset/assetdefinition.php?${url_params}`; window.glpi_ajax_dialog({ id: 'core_field_options_editor', - modalclass: 'modal-lg', + modalclass: 'modal-xl', appendTo: `#${$(sortable_fields_container.value).attr('id')}`, title: field_el.text(), url: url, From bea65e0f8392da96f36410eb1e24c2e4904945f8 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Tue, 18 Feb 2025 13:36:14 -0500 Subject: [PATCH 9/9] align fields and remove unused disabled option --- src/Glpi/Asset/CustomFieldType/AbstractType.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Glpi/Asset/CustomFieldType/AbstractType.php b/src/Glpi/Asset/CustomFieldType/AbstractType.php index 2f08fe8f313..5dc8f99c3a2 100644 --- a/src/Glpi/Asset/CustomFieldType/AbstractType.php +++ b/src/Glpi/Asset/CustomFieldType/AbstractType.php @@ -71,9 +71,8 @@ public function getOptions(): array { return [ new BooleanOption($this->custom_field, 'full_width', __('Full width'), false), - new ProfileRestrictOption($this->custom_field, 'readonly', __('Readonly'), false), new BooleanOption($this->custom_field, 'required', __('Mandatory'), false), - new BooleanOption($this->custom_field, 'disabled', __('Disabled'), false), // Not exposed in the UI. Only used in field order preview + new ProfileRestrictOption($this->custom_field, 'readonly', __('Readonly'), false), new ProfileRestrictOption($this->custom_field, 'hidden', __('Hidden'), false), ]; }