From e0987fe4fb20d9bd770463710eb4257b281c7605 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Mon, 22 Apr 2024 16:56:41 +0200 Subject: [PATCH] reorder fields for an asset definition --- ajax/asset/assetdefinition.php | 109 ++ .../update_10.0.x_to_11.0.0/assets.php | 3 +- install/mysql/glpi-empty.sql | 1 + .../vue/CustomObject/FieldPreview/Field.vue | 58 + .../FieldPreview/FieldDisplay.vue | 286 +++++ phpunit/functional/CommonDBTMTest.php | 6 + .../Glpi/Asset/CustomFieldDefinitionTest.php | 17 + src/CommonDBTM.php | 18 + src/Glpi/Asset/Asset.php | 21 +- src/Glpi/Asset/AssetDefinition.php | 322 +++-- src/Glpi/Asset/CustomFieldDefinition.php | 36 +- .../Asset/CustomFieldType/AbstractType.php | 6 + src/Glpi/Asset/CustomFieldType/TextType.php | 2 +- .../Asset/CustomFieldType/TypeInterface.php | 2 + src/autoload/CFG_GLPI.php | 1 + .../components/form/fields_macros.html.twig | 43 +- .../components/form/viewsubitem.html.twig | 104 +- templates/generic_show_form.html.twig | 1116 ++++++++--------- .../assetdefinition/fields_display.html.twig | 96 ++ templates/pages/assets/asset.html.twig | 5 - tests/DbTestCase.php | 3 +- tests/cypress/e2e/Asset/custom_fields.cy.js | 409 +++--- .../functional/Glpi/Asset/AssetDefinition.php | 8 + 23 files changed, 1683 insertions(+), 989 deletions(-) create mode 100644 ajax/asset/assetdefinition.php create mode 100644 js/src/vue/CustomObject/FieldPreview/Field.vue create mode 100644 js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue create mode 100644 templates/pages/admin/assetdefinition/fields_display.html.twig diff --git a/ajax/asset/assetdefinition.php b/ajax/asset/assetdefinition.php new file mode 100644 index 00000000000..526c5dc1ea2 --- /dev/null +++ b/ajax/asset/assetdefinition.php @@ -0,0 +1,109 @@ +. + * + * --------------------------------------------------------------------- + */ + +use Glpi\Asset\AssetDefinition; +use Glpi\Asset\CustomFieldDefinition; +use Glpi\Exception\Http\BadRequestHttpException; +use Glpi\Exception\Http\NotFoundHttpException; + +/** @var \Glpi\Controller\LegacyFileLoadController $this */ +$this->setAjax(); + +Session::checkRight(AssetDefinition::$rightname, READ); +Session::writeClose(); + +if ($_REQUEST['action'] === 'get_all_fields') { + header("Content-Type: application/json; charset=UTF-8"); + $definition = new AssetDefinition(); + if (!$definition->getFromDB($_GET['assetdefinitions_id'])) { + throw new NotFoundHttpException(); + } + $all_fields = $definition->getAllFields(); + $field_results = []; + foreach ($all_fields as $k => $v) { + if (!empty($_POST['searchText']) && stripos($v['text'], $_POST['searchText']) === false) { + continue; + } + $v['id'] = $k; + $field_results[] = $v; + } + echo json_encode([ + 'results' => $field_results, + 'count' => count($all_fields) + ], JSON_THROW_ON_ERROR); + return; +} else if ($_REQUEST['action'] === 'get_field_placeholder' && isset($_POST['fields']) && is_array($_POST['fields'])) { + header("Content-Type: application/json; charset=UTF-8"); + $custom_field = new CustomFieldDefinition(); + $results = []; + foreach ($_POST['fields'] as $field) { + if ($field['customfields_id'] > 0) { + if (!$custom_field->getFromDB($field['customfields_id'])) { + throw new NotFoundHttpException(); + } + } else { + $custom_field->fields['name'] = ''; + $custom_field->fields['label'] = $field['label']; + $custom_field->fields['type'] = $field['type']; + $custom_field->fields['itemtype'] = 'Computer'; // Doesn't matter what it is as long as it's not empty + $custom_field->fields['default_value'] = ''; + + $asset_definition = new AssetDefinition(); + if (!$asset_definition->getFromDB($field['assetdefinitions_id'])) { + throw new NotFoundHttpException(); + } + $fields_display = $asset_definition->getDecodedFieldsField(); + foreach ($fields_display as $field_display) { + if ($field_display['key'] === $field['key']) { + $custom_field->fields['field_options'] = $field_display['field_options'] ?? []; + break; + } + } + } + $custom_field->fields['field_options'] = array_merge($custom_field->fields['field_options'] ?? [], $field['field_options'] ?? []); + $custom_field->fields['field_options']['disabled'] = true; + $results[$field['key']] = $custom_field->getFieldType()->getFormInput('', null); + } + echo json_encode($results, JSON_THROW_ON_ERROR); + return; +} else if ($_REQUEST['action'] === 'get_core_field_editor') { + header("Content-Type: text/html; charset=UTF-8"); + $asset_definition = new AssetDefinition(); + if (!$asset_definition->getFromDB($_GET['assetdefinitions_id'])) { + throw new NotFoundHttpException(); + } + $asset_definition->showFieldOptionsForCoreField($_GET['key'], $_GET['field_options'] ?? []); + return; +} +throw new BadRequestHttpException(); diff --git a/install/migrations/update_10.0.x_to_11.0.0/assets.php b/install/migrations/update_10.0.x_to_11.0.0/assets.php index 86bab5f78c8..043fc859c8c 100644 --- a/install/migrations/update_10.0.x_to_11.0.0/assets.php +++ b/install/migrations/update_10.0.x_to_11.0.0/assets.php @@ -54,6 +54,7 @@ `capacities` JSON NOT NULL, `profiles` JSON NOT NULL, `translations` JSON NOT NULL, + `fields_display` JSON NOT NULL, `date_creation` timestamp NULL DEFAULT NULL, `date_mod` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -65,7 +66,7 @@ SQL; $DB->doQuery($query); } else { - foreach (['profiles', 'translations'] as $field) { + foreach (['profiles', 'translations', 'fields_display'] as $field) { $migration->addField('glpi_assets_assetdefinitions', $field, 'JSON NOT NULL', ['update' => "'[]'"]); } } diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index d50031d72e3..5fa72a364ec 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -9900,6 +9900,7 @@ CREATE TABLE `glpi_assets_assetdefinitions` ( `capacities` JSON NOT NULL, `profiles` JSON NOT NULL, `translations` JSON NOT NULL, + `fields_display` JSON NOT NULL, `date_creation` timestamp NULL DEFAULT NULL, `date_mod` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), diff --git a/js/src/vue/CustomObject/FieldPreview/Field.vue b/js/src/vue/CustomObject/FieldPreview/Field.vue new file mode 100644 index 00000000000..309815e1f18 --- /dev/null +++ b/js/src/vue/CustomObject/FieldPreview/Field.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue new file mode 100644 index 00000000000..f8b54d6f078 --- /dev/null +++ b/js/src/vue/CustomObject/FieldPreview/FieldDisplay.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/phpunit/functional/CommonDBTMTest.php b/phpunit/functional/CommonDBTMTest.php index c347149c583..258ed82b0d0 100644 --- a/phpunit/functional/CommonDBTMTest.php +++ b/phpunit/functional/CommonDBTMTest.php @@ -2125,4 +2125,10 @@ public function testDisplayFullPageForItem(array $credentials, string $itemtype, $item = new $itemtype(); $item->displayFullPageForItem($items_id); } + + public function testGetFormFields() + { + $computer = new \Computer(); + $this->assertTrue(!array_diff(['name', 'serial', '_template_is_active'], $computer->getFormFields())); + } } diff --git a/phpunit/functional/Glpi/Asset/CustomFieldDefinitionTest.php b/phpunit/functional/Glpi/Asset/CustomFieldDefinitionTest.php index 7c8c07d49c7..f178c298644 100644 --- a/phpunit/functional/Glpi/Asset/CustomFieldDefinitionTest.php +++ b/phpunit/functional/Glpi/Asset/CustomFieldDefinitionTest.php @@ -78,6 +78,15 @@ public function testCleanDBOnPurge() 'default_value' => 'default text', ]); + $this->assertTrue($asset_definition->update([ + 'id' => $asset_definition->getID(), + 'fields_display' => [ + 0 => 'name', + 1 => 'custom_test_string', + 2 => 'serial' + ], + ])); + $asset = $this->createItem($asset_classname, [ 'entities_id' => $this->getTestRootEntity(true), 'name' => 'Test asset', @@ -89,6 +98,14 @@ public function testCleanDBOnPurge() $asset->getFromDB($asset->getID()); $this->assertEquals('{"' . $custom_field_definition_2->getID() . '": "value2"}', $asset->fields['custom_fields']); + + $this->assertTrue($asset_definition->getFromDB($asset_definition->getID())); + $fields_display = $asset_definition->getDecodedFieldsField(); + $this->assertCount(2, $fields_display); + $this->assertEquals('name', $fields_display[0]['key']); + $this->assertEquals(0, $fields_display[0]['order']); + $this->assertEquals('serial', $fields_display[1]['key']); + $this->assertEquals(1, $fields_display[1]['order']); } public function testGetAllowedDropdownItemtypes() diff --git a/src/CommonDBTM.php b/src/CommonDBTM.php index 83943f6a04a..00b02685607 100644 --- a/src/CommonDBTM.php +++ b/src/CommonDBTM.php @@ -495,6 +495,23 @@ public function post_getFromDB() { } + public function getFormFields(): array + { + // NOTE: Post code field named differently for Suppliers. Placed after town to maintain field order from 9.5 + $fields = [ + 'name', 'firstname', 'template_name', '_template_is_active', 'states_id', static::getForeignKeyField(), 'is_helpdesk_visible', + '_dc_breadcrumbs', 'locations_id', 'item_type', 'itemtype', 'date_domaincreation', $this->getTypeForeignKeyField(), + 'usertitles_id', 'registration_number', 'phone', 'phone2', 'phonenumber', 'mobile', 'fax', 'website', 'email', + 'address', 'postalcode', 'town', 'postcode', 'state', 'country', 'date_expiration', 'ref', 'users_id_tech', + 'manufacturers_id', 'groups_id_tech', $this->getModelForeignKeyField(), 'contact_num', 'serial', 'contact', 'otherserial', + 'sysdescr', 'snmpcredentials_id', 'users_id', 'is_global', 'size', 'networks_id', 'groups_id', 'uuid', 'version', + 'comment', 'ram', 'alarm_threshold', 'brand', 'begin_date', 'autoupdatesystems_id', 'pictures', 'is_active', 'last_boot' + ]; + return array_filter($fields, function ($f) { + return $f !== null && (str_starts_with($f, '_') || $this->isField($f)); + }); + } + /** * Print the item generic form * Use a twig template to detect automatically fields and display them in a two column layout @@ -522,6 +539,7 @@ public function showForm($ID, array $options = []) 'params' => $options, 'no_header' => !$new_item && !$in_modal, 'cluster' => $cluster, + 'field_order' => $this->getFormFields() ]); return true; } diff --git a/src/Glpi/Asset/Asset.php b/src/Glpi/Asset/Asset.php index cc44de0f4e3..ab3686872dc 100644 --- a/src/Glpi/Asset/Asset.php +++ b/src/Glpi/Asset/Asset.php @@ -320,15 +320,34 @@ public function getUnallowedFieldsForUnicity() return $not_allowed; } + public function getFormFields(): array + { + $all_fields = array_keys(static::getDefinition()->getAllFields()); + $fields_display = static::getDefinition()->getDecodedFieldsField(); + $shown_fields = array_column($fields_display, 'key'); + return array_filter($shown_fields, static fn ($f) => in_array($f, $all_fields, true)); + } + public function showForm($ID, array $options = []) { $this->initForm($ID, $options); + $custom_fields = static::getDefinition()->getCustomFieldDefinitions(); + $custom_fields = array_combine(array_map(static fn ($f) => 'custom_' . $f->fields['name'], $custom_fields), $custom_fields); + $fields_display = static::getDefinition()->getDecodedFieldsField(); + $core_field_options = []; + + foreach ($fields_display as $field) { + $core_field_options[$field['key']] = $field['field_options'] ?? []; + } + TemplateRenderer::getInstance()->display( 'pages/assets/asset.html.twig', [ 'item' => $this, 'params' => $options, - 'custom_field_definitions' => static::getDefinition()->getCustomFieldDefinitions(), + 'custom_fields' => $custom_fields, + 'field_order' => $this->getFormFields(), + 'additional_field_options' => $core_field_options, ] ); return true; diff --git a/src/Glpi/Asset/AssetDefinition.php b/src/Glpi/Asset/AssetDefinition.php index 2325702a079..19475e325f3 100644 --- a/src/Glpi/Asset/AssetDefinition.php +++ b/src/Glpi/Asset/AssetDefinition.php @@ -36,18 +36,20 @@ namespace Glpi\Asset; use CommonGLPI; -use Dropdown; -use Gettext\Languages\Category as Language_Category; -use Gettext\Languages\CldrData as Language_CldrData; -use Gettext\Languages\Language; use Glpi\Application\View\TemplateRenderer; use Glpi\Asset\Capacity\CapacityInterface; -use Glpi\Asset\CustomFieldType\TypeInterface; +use Glpi\Asset\CustomFieldType\DropdownType; +use Glpi\Asset\CustomFieldType\StringType; +use Glpi\Asset\CustomFieldType\TextType; use Glpi\CustomObject\AbstractDefinition; use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QueryFunction; use Glpi\Search\SearchOption; +use Group; +use Location; +use Manufacturer; use Session; +use User; /** * @extends AbstractDefinition<\Glpi\Asset\Asset> @@ -100,7 +102,7 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) $capacities_count = count($item->getDecodedCapacitiesField()); $profiles_count = count(array_filter($item->getDecodedProfilesField())); $translations_count = count($item->getDecodedTranslationsField()); - $fields_count = count($item->getCustomFieldDefinitions()); + $fields_count = count($item->getDecodedFieldsField()); } return [ 1 => self::createTabEntry( @@ -110,10 +112,10 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) 'ti ti-adjustments' ), 2 => self::createTabEntry( - CustomFieldDefinition::getTypeName(Session::getPluralNumber()), + __('Fields'), $fields_count, - CustomFieldDefinition::class, - CustomFieldDefinition::getIcon() + self::class, + 'ti ti-forms' ), 3 => self::createTabEntry( _n('Profile', 'Profiles', Session::getPluralNumber()), @@ -141,7 +143,7 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $ $item->showCapacitiesForm(); break; case 2: - $item->showCustomFieldsForm(); + $item->showFieldsForm(); break; case 3: $item->showProfilesForm(); @@ -177,104 +179,87 @@ private function showCapacitiesForm(): void ); } - /** - * Show the custom fields tab including the list of custom fields and a form to add/edit them. + /* + * Display fields form. + * * @return void */ - private function showCustomFieldsForm(): void + private function showFieldsForm(): void { - /** @var \DBmysql $DB */ - global $DB; - - if (!$this->canViewItem()) { - return; - } - - $canedit = $this->canUpdateItem(); - $rand = mt_rand(); - if ($canedit) { - TemplateRenderer::getInstance()->display('components/form/viewsubitem.html.twig', [ - 'cancreate' => CustomFieldDefinition::canCreate(), - 'id' => $this->fields['id'], - 'rand' => $rand, - 'type' => CustomFieldDefinition::class, - 'parenttype' => CustomFieldDefinition::$itemtype, - 'items_id' => CustomFieldDefinition::$items_id, - 'add_new_label' => __('Add a new field'), - 'datatable_id' => 'datatable_customfields' . $rand, - 'subitem_container_id' => 'customfield_form_container' - ]); - } + $fields_display = $this->getDecodedFieldsField(); + $used = array_column($fields_display, 'key'); + $used = array_combine($used, $used); - $iterator = $DB->request([ - 'SELECT' => ['id', 'name', 'label', 'type', 'field_options', 'itemtype'], - 'FROM' => CustomFieldDefinition::getTable(), - 'WHERE' => [ - self::getForeignKeyField() => $this->fields['id'], - ], - ]); - - $entries = []; - $adm = AssetDefinitionManager::getInstance(); - $field_types = $adm->getCustomFieldTypes(); - $allowed_dropdown_itemtypes = $adm->getAllowedDropdownItemtypes(true); - foreach ($iterator as $data) { - $entry = [ - 'id' => $data['id'], - 'itemtype' => CustomFieldDefinition::class, - 'name' => $data['name'], - 'label' => $data['label'], - 'type' => in_array($data['type'], $field_types, true) ? $data['type']::getName() : NOT_AVAILABLE, - 'dropdown_itemtype' => $data['itemtype'] !== '' ? ($allowed_dropdown_itemtypes[$data['itemtype']] ?? NOT_AVAILABLE) : NOT_AVAILABLE, - 'row_class' => 'cursor-pointer' - ]; + TemplateRenderer::getInstance()->display( + 'pages/admin/assetdefinition/fields_display.html.twig', + [ + 'item' => $this, + 'all_fields' => $this->getAllFields(), + 'fields_display' => $fields_display, + 'custom_field_form_params' => [ + 'cancreate' => CustomFieldDefinition::canCreate(), + 'id' => $this->fields['id'], + 'type' => CustomFieldDefinition::class, + 'parenttype' => CustomFieldDefinition::$itemtype, + 'items_id' => CustomFieldDefinition::$items_id, + 'add_new_label' => __('Create new field'), + 'subitem_container_id' => 'customfield_form_container', + 'as_modal' => true, + 'ajax_form_submit' => true, + ] + ] + ); + } - $field_options = json_decode($data['field_options'] ?? '[]', true) ?? []; - $flags = ''; - if ($field_options['readonly'] ?? false) { - $flags .= '' . __s('Read-only') . ''; - } - if ($field_options['required'] ?? false) { - $flags .= '' . __s('Mandatory') . ''; - } - if ($field_options['multiple'] ?? false) { - $flags .= '' . __s('Multiple values') . ''; - } - $entry['flags'] = $flags; - $entries[] = $entry; + /** + * Show field options for a core field. + * @param string $field_key The field key + * @param array $field_option_values Field option value overrides + * @return void + */ + public function showFieldOptionsForCoreField(string $field_key, array $field_option_values = []): void + { + $all_fields = $this->getAllFields(); + $field_display = $this->getDecodedFieldsField(); + $field_match = array_filter($field_display, static fn ($field) => $field['key'] === $field_key); + $field_options = []; + if (!empty($field_match)) { + $field_options = reset($field_match)['field_options'] ?? []; } + // Merge field options with overrides + $field_options = array_merge($field_options, $field_option_values); + + // Fake custom field to represent the core field + $custom_field = new CustomFieldDefinition(); + $custom_field->fields['name'] = $field_key; + $custom_field->fields['label'] = $all_fields[$field_key]['text']; + $custom_field->fields['type'] = $all_fields[$field_key]['type']; + $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']; + + $twig_params = [ + 'options' => array_filter($custom_field->getFieldType()->getOptions(), static fn ($option) => in_array($option->getKey(), $options_allowlist, true)), + 'key' => $field_key, + ]; - TemplateRenderer::getInstance()->display('components/datatable.html.twig', [ - 'datatable_id' => 'datatable_customfields' . $rand, - 'is_tab' => true, - 'nopager' => true, - 'nosort' => true, - 'nofilter' => true, - 'columns' => [ - 'name' => __('Name'), - 'label' => __('Label'), - 'type' => _n('Type', 'Types', 1), - 'flags' => __('Flags'), - 'dropdown_itemtype' => __('Item type'), - ], - 'formatters' => [ - 'flags' => 'raw_html' - ], - 'entries' => $entries, - 'total_number' => count($entries), - 'filtered_number' => count($entries), - 'showmassiveactions' => $canedit, - 'massiveactionparams' => [ - 'num_displayed' => count($entries), - 'container' => 'mass' . str_replace('\\', '_', self::class) . $rand, - 'specific_actions' => ['purge' => _x('button', 'Delete permanently')] - ], - ]); + // language=Twig + echo TemplateRenderer::getInstance()->renderFromStringTemplate(<< + +
+ {% for option in options %} + {{ option.getFormInput()|raw }} + {% endfor %} +
+ +TWIG, $twig_params); } public function prepareInputForAdd($input) { - foreach (['capacities', 'profiles', 'translations'] as $json_field) { + foreach (['capacities', 'profiles', 'translations', 'fields_display'] as $json_field) { if (!array_key_exists($json_field, $input)) { // ensure default value of JSON fields will be a valid array $input[$json_field] = []; @@ -303,6 +288,19 @@ protected function prepareInput(array $input): array|bool } } + if (array_key_exists('fields_display', $input)) { + $formatted_fields_display = []; + foreach ($input['fields_display'] as $field_order => $field_key) { + $field_options = $input['field_options'][$field_key] ?? []; + $formatted_fields_display[] = [ + 'order' => $field_order, + 'key' => $field_key, + 'field_options' => $field_options, + ]; + } + $input['fields_display'] = json_encode($formatted_fields_display); + } + return $has_errors ? false : parent::prepareInput($input); } @@ -572,6 +570,136 @@ private function getDecodedCapacitiesField(): array return $capacities; } + + public function getAllFields(): array + { + $type_class = $this->getAssetTypeClassName(); + $model_class = $this->getAssetModelClassName(); + + $fields = [ + 'name' => [ + 'text' => __('Name'), + 'type' => StringType::class + ], + 'states_id' => [ + 'text' => __('Status'), + 'type' => DropdownType::class + ], + 'locations_id' => [ + 'text' => Location::getTypeName(1), + 'type' => DropdownType::class + ], + $type_class::getForeignKeyField() => [ + 'text' => $type_class::getTypeName(1), + 'type' => DropdownType::class + ], + 'users_id_tech' => [ + 'text' => __('Technician in charge'), + 'type' => DropdownType::class + ], + 'manufacturers_id' => [ + 'text' => Manufacturer::getTypeName(1), + 'type' => DropdownType::class + ], + 'groups_id_tech' => [ + 'text' => __('Group in charge'), + 'type' => DropdownType::class + ], + $model_class::getForeignKeyField() => [ + 'text' => $model_class::getTypeName(1), + 'type' => DropdownType::class + ], + 'contact_num' => [ + 'text' => __('Alternate username number'), + 'type' => StringType::class + ], + 'serial' => [ + 'text' => __('Serial'), + 'type' => StringType::class + ], + 'contact' => [ + 'text' => __('Alternate username'), + 'type' => StringType::class + ], + 'otherserial' => [ + 'text' => __('Inventory number'), + 'type' => StringType::class + ], + 'users_id' => [ + 'text' => User::getTypeName(1), + 'type' => DropdownType::class + ], + 'groups_id' => [ + 'text' => Group::getTypeName(1), + 'type' => DropdownType::class + ], + 'uuid' => [ + 'text' => __('UUID'), + 'type' => StringType::class + ], + 'comment' => [ + 'text' => _n('Comment', 'Comments', Session::getPluralNumber()), + 'type' => TextType::class + ], + 'autoupdatesystems_id' => [ + 'text' => \AutoUpdateSystem::getTypeName(1), + 'type' => DropdownType::class + ], + ]; + + foreach ($this->getCustomFieldDefinitions() as $custom_field_def) { + $fields['custom_' . $custom_field_def->fields['name']] = [ + 'customfields_id' => $custom_field_def->getID(), + 'text' => $custom_field_def->computeFriendlyName(), + 'type' => $custom_field_def->fields['type'], + ]; + } + + return $fields; + } + + private function getDefaultFieldsDisplay(): array + { + $all_fields = $this->getAllFields(); + + $default = []; + $order = 0; + foreach ($all_fields as $key => $label) { + $default[] = [ + 'key' => $key, + 'order' => $order, + ]; + $order++; + } + + return $default; + } + + + /** + * Return the decoded value of the `fields_display` field. + * + * @return array + */ + public function getDecodedFieldsField(): array + { + $fields_display = json_decode($this->fields['fields_display'] ?? '[]', associative: true) ?? []; + if (!is_array($fields_display) || count($fields_display) === 0) { + return $this->getDefaultFieldsDisplay(); + } + return $fields_display; + } + + public function getFieldOrder(): array + { + $fields_display = $this->getDecodedFieldsField(); + usort( + $fields_display, + static fn ($a, $b) => $a['order'] <=> $b['order'] + ); + return array_column($fields_display, 'key'); + } + /** * Validate that the given capacities array contains valid values. * diff --git a/src/Glpi/Asset/CustomFieldDefinition.php b/src/Glpi/Asset/CustomFieldDefinition.php index 9f90952e483..d42c0efa821 100644 --- a/src/Glpi/Asset/CustomFieldDefinition.php +++ b/src/Glpi/Asset/CustomFieldDefinition.php @@ -79,6 +79,35 @@ public function cleanDBonPurge() /** @var \DBmysql $DB */ global $DB; + $it = $DB->request([ + 'SELECT' => ['fields_display'], + 'FROM' => AssetDefinition::getTable(), + 'WHERE' => [ + 'id' => $this->fields[self::$items_id], + ], + ]); + $fields_display = json_decode($it->current()['fields_display'] ?? '[]', true) ?? []; + $order = 0; + foreach ($fields_display as $k => $field) { + if ($field['key'] === 'custom_' . $this->fields['name']) { + $order = $field['order']; + unset($fields_display[$k]); + break; + } + } + if ($order > 0) { + foreach ($fields_display as $k => $field) { + if ($field['order'] > $order) { + $fields_display[$k]['order']--; + } + } + } + $DB->update(AssetDefinition::getTable(), [ + 'fields_display' => json_encode(array_values($fields_display)), + ], [ + 'id' => $this->fields[self::$items_id], + ]); + $DB->update('glpi_assets_assets', [ 'custom_fields' => QueryFunction::jsonRemove([ 'custom_fields', @@ -107,6 +136,9 @@ public function showForm($ID, array $options = []) 'assetdefinitions_id' => $options[self::$items_id], 'allowed_dropdown_itemtypes' => $adm->getAllowedDropdownItemtypes(), 'field_types' => $field_types, + 'params' => [ + 'formfooter' => false + ] ]); return true; } @@ -127,6 +159,8 @@ private function validateSystemName(array &$input): bool // Spaces are replaced with underscores and the name is made lowercase. Only lowercase letters and underscores are kept. $input['name'] = preg_replace('/[^a-z_]/', '', strtolower(str_replace(' ', '_', $input['name']))); + // The name cannot start with an underscore + $input['name'] = ltrim($input['name'], '_'); if ($input['name'] === '') { Session::addMessageAfterRedirect(__s('The system name must not be empty'), false, ERROR); return false; @@ -250,7 +284,7 @@ public function getFieldType(): TypeInterface */ public function getDecodedTranslationsField(): array { - $translations = json_decode($this->fields['translations'] ?? [], associative: true) ?? []; + $translations = json_decode($this->fields['translations'] ?? '[]', associative: true) ?? []; if (!$this->validateTranslationsArray($translations)) { trigger_error( sprintf('Invalid `translations` value (`%s`).', $this->fields['translations']), diff --git a/src/Glpi/Asset/CustomFieldType/AbstractType.php b/src/Glpi/Asset/CustomFieldType/AbstractType.php index 280ac1c4989..6bee88feee9 100644 --- a/src/Glpi/Asset/CustomFieldType/AbstractType.php +++ b/src/Glpi/Asset/CustomFieldType/AbstractType.php @@ -46,6 +46,11 @@ public function __construct( ) { } + public function getLabel(): string + { + return $this->custom_field->getFriendlyName(); + } + public function normalizeValue(mixed $value): mixed { return $value; @@ -67,6 +72,7 @@ public function getOptions(): array new BooleanOption($this->custom_field, 'full_width', __('Full width'), false), new BooleanOption($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 ]; } diff --git a/src/Glpi/Asset/CustomFieldType/TextType.php b/src/Glpi/Asset/CustomFieldType/TextType.php index a7bf03b7c09..38d9975dd7b 100644 --- a/src/Glpi/Asset/CustomFieldType/TextType.php +++ b/src/Glpi/Asset/CustomFieldType/TextType.php @@ -54,7 +54,7 @@ public function getFormInput(string $name, mixed $value, ?string $label = null, // language=Twig return TemplateRenderer::getInstance()->renderFromStringTemplate(<< [ + 'assetdefinition' => ['sortable'], 'commondropdown' => [ 'ITILFollowupTemplate' => ['tinymce'], 'ProjectTaskTemplate' => ['tinymce'], diff --git a/templates/components/form/fields_macros.html.twig b/templates/components/form/fields_macros.html.twig index 47cc8b2e841..ac2a931c62d 100644 --- a/templates/components/form/fields_macros.html.twig +++ b/templates/components/form/fields_macros.html.twig @@ -702,6 +702,44 @@ {% endif %} {% endmacro %} +{% macro dropdownAjaxField(url, name, value, label = '', options = {}) %} + {% if options.multiple %} + {# Needed for empty value as the input wont be sent in this case... we need something to know the input was displayed AND empty #} + {% set defined_input_name = "_#{name}_defined" %} + + + {# Multiple values will be set, input need to be an array #} + {% set name = "#{name}[]" %} + {% endif %} + {% set options = { + 'rand': random(), + 'width': '100%', + }|merge(options) %} + {% if options.fields_template.isMandatoryField(name) %} + {% set options = {'specific_tags': {'required': true}}|merge(options) %} + {% endif %} + + {% if options.fields_template.isReadonlyField(name) %} + {% set options = options|merge({'readonly': true}) %} + {% endif %} + + {% if options.disabled %} + {% set options = options|merge({'specific_tags': {'disabled': 'disabled'}}) %} + {% endif %} + + {% set options = options|merge({ + 'id': 'dropdown_' ~ name|replace({'[': '_', ']': '_'}) ~ options.rand + }) %} + {% set field %} + {% set ajax_opts = options|filter((v, k) => k in ['templateResult', 'templateSelection', 'rand']) %} + {{ call('Html::jsAjaxDropdown', [name, options.id, url, ajax_opts])|raw }} + {% endset %} + + {% if field|trim is not empty %} + {{ _self.field(name, field, label, options) }} + {% endif %} +{% endmacro %} + {% macro htmlField(name, value, label = '', options = {}) %} {% if value|length == 0 %} {% set value = ' ' %} @@ -820,6 +858,7 @@ 'add_field_attribs': {}, 'center': false, 'label_align': 'end', + 'inline_add_field_html': false, }|merge(options) %} {% if options.icon_label %} @@ -862,9 +901,7 @@
{% import 'components/form/basic_inputs_macros.html.twig' as _inputs %} {{ _inputs.label(label, id, options, 'col-form-label ' ~ options.label_class ~ ' ' ~ options.add_label_class) }} - {% if options.center %} - {% set flex_class = "d-flex align-items-center" %} - {% endif %} + {% set flex_class = options.center ? 'd-flex align-items-center' : (options.inline_add_field_html ? 'd-flex' : '') %}
{{ field|raw }} {{ add_field_html|raw }} diff --git a/templates/components/form/viewsubitem.html.twig b/templates/components/form/viewsubitem.html.twig index 753741680fb..d0327fa8547 100644 --- a/templates/components/form/viewsubitem.html.twig +++ b/templates/components/form/viewsubitem.html.twig @@ -30,22 +30,51 @@ # --------------------------------------------------------------------- #} +{% set rand = rand|default(random()) %} {% set subitem_container_id = subitem_container_id|default('viewsubitem' ~ rand) %} +{% set as_modal = as_modal|default(false) %} +{% set add_new_label = add_new_label|default(__('Add')) %} + +{% if as_modal %} + +{% endif %} + {% if cancreate %} -
- -
+ {% if not add_new_inline %} +
+ {% endif %} {% endif %} diff --git a/templates/generic_show_form.html.twig b/templates/generic_show_form.html.twig index 5ca0672d2b5..e1ea3ff3306 100644 --- a/templates/generic_show_form.html.twig +++ b/templates/generic_show_form.html.twig @@ -53,635 +53,505 @@ 'rand': rand, } %} + {# Support custom fields for generic assets #} + {% set custom_fields = custom_fields|default({}) %} + {% set field_order = field_order|default(item.getFormFields()) %} +
{% block form_fields %} - {% if item.isField('name') %} - {{ fields.autoNameField( - 'name', - item, - (item_type == 'Contact' ? __('Surname') : __('Name')), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('firstname') %} - {{ fields.autoNameField( - 'firstname', - item, - __('First name'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('template_name') and withtemplate == 1 and no_header %} - {{ fields.autoNameField( - 'template_name', - item, - __('Template name'), - 0, - field_options - ) }} - {% endif %} - - {% if item.isField('is_active') %} - {% if withtemplate >= 1 %} - {{ fields.hiddenField('is_active', item.fields['is_active'], __('Is active'), { - 'add_field_html': __('No') - }) }} - {% else %} - {% endif %} - {% endif %} - - {% if item.isField('states_id') and item is usingtrait('Glpi\\Features\\State') %} - {{ fields.dropdownField( - 'State', - 'states_id', - item.fields['states_id'], - __('Status'), - field_options|merge({ - 'entity': item.fields['entities_id'], - 'condition': item.getStateVisibilityCriteria() - }) - ) }} - {% endif %} - - {% set fk = item.getForeignKeyField() %} - {% if item.isField(fk) and item_type != 'Software' %} - {{ fields.dropdownField( - item_type, - fk, - item.fields[fk], - __('As child of'), - field_options|merge({ - 'entity': item.fields['entities_id'], - }) - ) }} - {% endif %} - - {% if item_type != 'SoftwareLicense' and item.isField('is_helpdesk_visible') %} - {# TODO Drop unused 'is_helpdesk_visible' field in SoftwareLicense schema? #} - {{ fields.checkboxField( - 'is_helpdesk_visible', - item.fields['is_helpdesk_visible'], - __('Associable to a ticket'), - field_options - ) }} - {% endif %} - - - {% set conditions_met = 0 %} - {% set dc_breadcrumbs %} - {% if item is usingtrait('Glpi\\Features\\DCBreadcrumb') %} - {{ call(item.getType() ~ '::renderDcBreadcrumb', [ - item.getID - ])|raw }} - {% endif %} - {% endset %} - {% if dc_breadcrumbs|trim|length > 0 %} - {{ fields.htmlField( - '', - dc_breadcrumbs, - __('Data center position'), - ) }} - {% set conditions_met = conditions_met + 1 %} - {% endif %} - - {% if cluster is not null %} - {{ fields.htmlField( - '', - cluster.getLink(), - _n('Cluster', 'Clusters', 1), - ) }} - {% set conditions_met = conditions_met + 1 %} - {% endif %} - {% if conditions_met == 1 %} - {{ fields.nullField() }} - {% endif %} - - {% if item.isField('locations_id') %} - {{ fields.dropdownField( - 'Location', - 'locations_id', - item.fields['locations_id'], - 'Location'|itemtype_name, - field_options|merge({ - 'entity': item.fields['entities_id'], - }) - ) }} - {% endif %} - - {% if item_type == 'Unmanaged' and item.isField('item_type') %} - {{ fields.dropdownArrayField( - 'item_type', - item.fields['itemtype'], - _n('Type', 'Types', 1), - [ - 'Computer', 'NetworkEquipment', 'Printer', 'Peripheral', 'Phone' - ], - field_options - ) }} - {% endif %} - - {% if item_type == 'DefaultFilter' and item.isField('itemtype') %} - {% set field_options = field_options|merge({'types': config('globalsearch_types')}) %} - {{ fields.dropdownItemTypes( - 'itemtype', - item.fields['itemtype'], - __('Itemtype'), - field_options - ) }} - {% endif %} - - {% if item.isField('date_domaincreation') %} - {{ fields.datetimeField('date_domaincreation', item.fields['date_domaincreation'], __('Registration date')) }} - {% endif %} - - {% set type_itemtype = item.getTypeClass() %} - {% set type_fk = item.getTypeForeignKeyField() %} - {% if item.isField(type_fk) %} - {{ fields.dropdownField( - type_itemtype, - type_fk, - item.fields[type_fk], - type_itemtype|itemtype_name, - field_options - ) }} - {% endif %} - - {% if item.isField('usertitles_id') %} - {{ fields.dropdownField( - 'UserTitle', - 'usertitles_id', - item.fields['usertitles_id'], - 'UserTitle'|itemtype_name, - field_options|merge({ - 'entity': item.fields['entities_id'], - }) - ) }} - {% endif %} - - {% if item.isField('registration_number') %} - {{ fields.autoNameField( - 'registration_number', - item, - (item_type starts with 'User' ? _x('user', 'Administrative number') : _x('infocom', 'Administrative number')), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('phone') %} - {{ fields.autoNameField( - 'phone', - item, - 'Phone'|itemtype_name, - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('phone2') %} - {{ fields.autoNameField( - 'phone2', - item, - __('Phone 2'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('phonenumber') %} - {{ fields.autoNameField( - 'phonenumber', - item, - 'Phone'|itemtype_name, - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('mobile') %} - {{ fields.autoNameField( - 'mobile', - item, - __('Mobile phone'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('fax') %} - {{ fields.autoNameField( - 'fax', - item, - __('Fax'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('website') %} - {{ fields.autoNameField( - 'website', - item, - __('Website'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('email') %} - {{ fields.autoNameField( - 'email', - item, - _n('Email', 'Emails', 1), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('address') %} - {{ fields.textareaField('address', item.fields['address'], __('Address')) }} - {% endif %} - - {% if item.isField('postalcode') %} - {{ fields.autoNameField( - 'postalcode', - item, - __('Postal code'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('town') %} - {{ fields.autoNameField( - 'town', - item, - __('City'), - withtemplate, - field_options - ) }} - {% endif %} - - {# Post code field named differently for Suppliers. Placed after town to maintain field order from 9.5)#} - {% if item.isField('postcode') %} - {{ fields.autoNameField( - 'postcode', - item, - __('Postal code'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item_type == 'Supplier' or item_type == 'Contact' %} - {{ fields.autoNameField( - 'state', - item, - _x('location', 'State'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('country') %} - {{ fields.autoNameField( - 'country', - item, - __('Country'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('date_expiration') %} - {% if item_type == 'Certificate' %} - {{ fields.dateField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { - 'helper': __('Empty for infinite'), - 'checkIsExpired': false, - 'expiration_class' : params.expiration_class - }|merge(field_options)) }} - {% elseif item_type == 'ItemAntivirus' %} - {{ fields.dateField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { - 'helper': __('Empty for infinite'), - 'checkIsExpired': true, - }|merge(field_options)) }} - {% else %} - {{ fields.datetimeField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { - 'helper': __('Empty for infinite'), - 'checkIsExpired': true - }|merge(field_options)) }} - {% endif %} - {% endif %} - - {% if item.isField('ref') %} - {{ fields.textField( - 'ref', - item.fields['ref'], - __('Reference'), - field_options - ) }} - {% endif %} - - {% if item.isField('users_id_tech') %} - {{ fields.dropdownField( - 'User', - 'users_id_tech', - item.fields['users_id_tech'], - __('Technician in charge'), - field_options|merge({ - 'entity': item.fields['entities_id'], - 'right': 'own_ticket', - }) - ) }} - {% endif %} - - {% if item.isField('manufacturers_id') %} - {{ fields.dropdownField( - 'Manufacturer', - 'manufacturers_id', - item.fields['manufacturers_id'], - (item_type starts with 'Software' ? __('Publisher') : 'Manufacturer'|itemtype_name), - field_options - ) }} - {% endif %} - - {% if item.isField('groups_id_tech') or item is usingtrait('Glpi\\Features\\AssignableItem') %} - {{ fields.dropdownField( - 'Group', - 'groups_id_tech', - item.fields['groups_id_tech'], - __('Group in charge'), - field_options|merge({ - 'entity': item.fields['entities_id'], - 'condition': {'is_assign': 1}, - 'multiple': item is usingtrait('Glpi\\Features\\AssignableItem') ? true : false - }) - ) }} - {% endif %} - - {% set model_itemtype = item.getModelClass() %} - {% set model_fk = item.getModelForeignKeyField() %} - {% if item.isField(model_fk) %} - {{ fields.dropdownField( - model_itemtype, - model_fk, - item.fields[model_fk], - _n('Model', 'Models', 1), - field_options - ) }} - {% endif %} - - {% if item_type != 'SoftwareLicense' and item.isField('contact_num') %} - {# TODO Drop unused 'contact_num' field in Software License schema? #} - {{ fields.textField( - 'contact_num', - item.fields['contact_num'], - __('Alternate username number'), - field_options - ) }} - {% endif %} - - {% if item.isField('serial') %} - {{ fields.textField( - 'serial', - item.fields['serial'], - __('Serial number'), - field_options - ) }} - {% endif %} - - {% if item_type != 'SoftwareLicense' and item.isField('contact') %} - {# TODO Drop unused 'contact' field in Software License schema? #} - {{ fields.textField( - 'contact', - item.fields['contact'], - __('Alternate username'), - field_options - ) }} - {% endif %} - - {% if item.isField('otherserial') %} - {{ fields.autoNameField( - 'otherserial', - item, - __('Inventory number'), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('sysdescr') %} - {{ fields.textareaField('sysdescr', item.fields['sysdescr'], __('Sysdescr')) }} - {% endif %} - - {% if item.isField('snmpcredentials_id') %} - {{ fields.dropdownField( - 'SNMPCredential', - 'snmpcredentials_id', - item.fields['snmpcredentials_id'], - 'SNMPCredential'|itemtype_name, - field_options - ) }} - {% endif %} - - {% if item.isField('users_id') %} - {{ fields.dropdownField( - 'User', - 'users_id', - item.fields['users_id'], - 'User'|itemtype_name, - field_options|merge({ - 'entity': item.fields['entities_id'], - 'right': 'all', - }) - ) }} - {% endif %} - - {% if item.isField('is_global') %} - {% set management_restrict = 0 %} - {% if item_type == 'Monitor' %} - {% set management_restrict = config('monitors_management_restrict') %} - {% elseif item_type == 'Peripheral' %} - {% set management_restrict = config('peripherals_management_restrict') %} - {% elseif item_type == 'Phone' %} - {% set management_restrict = config('phones_management_restrict') %} - {% elseif item_type == 'Printer' %} - {% set management_restrict = config('printers_management_restrict') %} - {% else %} - {% set management_restrict = 0 %} - {% endif %} - {% set dd_globalswitch %} - {% do call('Dropdown::showGlobalSwitch', [item.fields['id'], { - 'withtemplate': withtemplate, - 'value': item.fields['is_global'], - 'management_restrict': management_restrict, - 'target': target, - 'width': '100%', - }]) %} - {% endset %} - {{ fields.htmlField( - 'is_global', - dd_globalswitch, - __('Management type'), - ) }} - {% endif %} - - {% if item.isField('size') %} - {{ fields.numberField( - 'size', - item.fields['size'], - __('Size'), - field_options|merge({ - 'min': 0, - 'step': 0.01 - }), - ) }} - {% endif %} - - {% if item.isField('networks_id') %} - {{ fields.dropdownField( - 'Network', - 'networks_id', - item.fields['networks_id'], - _n('Network', 'Networks', 1), - field_options - ) }} - {% endif %} - - {% if item.isField('groups_id') or item is usingtrait('Glpi\\Features\\AssignableItem') %} - {{ fields.dropdownField( - 'Group', - 'groups_id', - item.fields['groups_id'], - 'Group'|itemtype_name, - field_options|merge({ - 'entity': item.fields['entities_id'], - 'condition': {'is_itemgroup': 1}, - 'multiple': item is usingtrait('Glpi\\Features\\AssignableItem') ? true : false - }) - ) }} - {% endif %} - - {% if item.isField('uuid') %} - {{ fields.textField( - 'uuid', - item.fields['uuid'], - __('UUID'), - field_options - ) }} - {% endif %} - - {% if item.isField('version') %} - {{ fields.autoNameField( - 'version', - item, - _n('Version', 'Versions', 1), - withtemplate, - field_options - ) }} - {% endif %} - - {% if item.isField('comment') %} - {{ fields.textareaField( - 'comment', - item.fields['comment'], - _n('Comment', 'Comments', get_plural_number()), - field_options - ) }} - {% endif %} - - {% if item.isField('ram') %} - {{ fields.numberField( - 'ram', - item.fields['ram'], - __('%1$s (%2$s)')|format(_n('Memory', 'Memories', 1), __('Mio')), - field_options - ) }} - {% endif %} - - {% if item.isField('alarm_threshold') %} - {% set dd_alarm_treshold %} - {% do call('Dropdown::showNumber', ['alarm_threshold', { - 'value': item.fields['alarm_threshold'], - 'rand': rand, - 'width': '100%', - 'min': 0, - 'max': 100, - 'step': 1, - 'toadd': {'-1': __('Never')} - }|merge(params)]) %} - {% endset %} - {% set last_alert_html %} - - {% do call('Alert::displayLastAlert', [item_type, item.fields['id']]) %} - - {% endset %} - {{ fields.htmlField( - 'alarm_threshold', - dd_alarm_treshold, - __('Alert threshold'), - field_options|merge({ - add_field_html: last_alert_html - }) - ) }} - {% endif %} - - {% if item.isField('brand') %} - {{ fields.textField( - 'brand', - item.fields['brand'], - __('Brand'), - field_options - ) }} - {% endif %} - - {% if item.isField('begin_date') %} - {{ fields.dateField( - 'begin_date', - item.fields['begin_date'], - __('Start date'), - field_options - ) }} - {% endif %} - - {% if item.isField('autoupdatesystems_id') %} - {{ fields.dropdownField( - 'AutoUpdateSystem', - 'autoupdatesystems_id', - item.fields['autoupdatesystems_id'], - 'AutoUpdateSystem'|itemtype_name, - field_options - ) }} - {% endif %} - - {% if item.isField('pictures') %} - {{ fields.fileField('pictures', null, _n('Picture', 'Pictures', get_plural_number()), { - 'onlyimages': true, - 'multiple': true, - }) }} - {% endif %} - - {% if item.isField('is_active') %} - {{ fields.dropdownYesNo( - 'is_active', - item.fields['is_active'], - __('Active'), - field_options - ) }} - {% endif %} - - {# display last_boot data only if item is dynamic and have field #} - {% if item.isField('last_boot') and item.isField('is_dynamic') and item.fields['is_dynamic'] %} - {{ fields.htmlField('last_boot', item.fields['last_boot']|formatted_datetime(true), __('Last boot date')) }} - {% endif %} + {% for field in field_order %} + {% set specific_field_options = field_options|merge(additional_field_options[field]|default({})) %} + + {% if field is same as 'name' %} + {{ fields.autoNameField('name', item, (item_type == 'Contact' ? __('Surname') : __('Name')), withtemplate, specific_field_options) }} + {% elseif field is same as 'firstname' %} + {{ fields.autoNameField('firstname', item, __('First name'), withtemplate, specific_field_options) }} + {% elseif field is same as 'template_name' and withtemplate == 1 and no_header %} + {{ fields.autoNameField('template_name', item, __('Template name'), 0, specific_field_options) }} + {% elseif item.isField('is_active') and field is same as '_template_is_active' and withtemplate >= 1 %} + {{ fields.hiddenField('is_active', item.fields['is_active'], __('Is active'), { + 'add_field_html': __('No') + }) }} + {% elseif field is same as 'states_id' and item is usingtrait('Glpi\\Features\\State') %} + {{ fields.dropdownField( + 'State', + 'states_id', + item.fields['states_id'], + __('Status'), + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + 'condition': item.getStateVisibilityCriteria() + }) + ) }} + {% elseif field is same as item.getForeignKeyField() and item_type != 'Software' %} + {{ fields.dropdownField( + item_type, + field, + item.fields[field], + __('As child of'), + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + }) + ) }} + {% elseif field is same as 'is_helpdesk_visible' and item_type != 'SoftwareLicense' %} + {# TODO Drop unused 'is_helpdesk_visible' field in SoftwareLicense schema? #} + {{ fields.checkboxField( + 'is_helpdesk_visible', + item.fields['is_helpdesk_visible'], + __('Associable to a ticket'), + specific_field_options + ) }} + {% elseif field is same as '_dc_breadcrumbs' %} + {% set conditions_met = 0 %} + {% set dc_breadcrumbs %} + {% if item is usingtrait('Glpi\\Features\\DCBreadcrumb') %} + {{ call(item.getType() ~ '::renderDcBreadcrumb', [ + item.getID + ])|raw }} + {% endif %} + {% endset %} + {% if dc_breadcrumbs|trim|length > 0 %} + {{ fields.htmlField( + '', + dc_breadcrumbs, + __('Data center position'), + ) }} + {% set conditions_met = conditions_met + 1 %} + {% endif %} + + {% if cluster is not null %} + {{ fields.htmlField( + '', + cluster.getLink(), + _n('Cluster', 'Clusters', 1), + ) }} + {% set conditions_met = conditions_met + 1 %} + {% endif %} + {% if conditions_met == 1 %} + {{ fields.nullField() }} + {% endif %} + {% elseif field is same as 'locations_id' %} + {{ fields.dropdownField( + 'Location', + 'locations_id', + item.fields['locations_id'], + 'Location'|itemtype_name, + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + }) + ) }} + {% elseif field is same as 'item_type' and item_type == 'Unmanaged' %} + {{ fields.dropdownArrayField( + 'item_type', + item.fields['itemtype'], + _n('Type', 'Types', 1), + [ + 'Computer', 'NetworkEquipment', 'Printer', 'Peripheral', 'Phone' + ], + specific_field_options + ) }} + {% elseif field is same as 'itemtype' and item_type == 'DefaultFilter' %} + {{ fields.dropdownItemTypes( + 'itemtype', + item.fields['itemtype'], + __('Itemtype'), + specific_field_options|merge({'types': config('globalsearch_types')}) + ) }} + {% elseif field is same as 'date_domaincreation' %} + {{ fields.datetimeField('date_domaincreation', item.fields['date_domaincreation'], __('Registration date')) }} + {% elseif field is same as item.getTypeForeignKeyField() %} + {% set type_itemtype = item.getTypeClass() %} + {{ fields.dropdownField( + type_itemtype, + field, + item.fields[field], + type_itemtype|itemtype_name, + specific_field_options + ) }} + {% elseif field is same as 'usertitles_id' %} + {{ fields.dropdownField( + 'UserTitle', + 'usertitles_id', + item.fields['usertitles_id'], + 'UserTitle'|itemtype_name, + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + }) + ) }} + {% elseif field is same as 'registration_number' %} + {{ fields.autoNameField( + 'registration_number', + item, + (item_type starts with 'User' ? _x('user', 'Administrative number') : _x('infocom', 'Administrative number')), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'phone' %} + {{ fields.autoNameField( + 'phone', + item, + 'Phone'|itemtype_name, + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'phone2' %} + {{ fields.autoNameField( + 'phone2', + item, + __('Phone 2'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'phonenumber' %} + {{ fields.autoNameField( + 'phonenumber', + item, + 'Phone'|itemtype_name, + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'mobile' %} + {{ fields.autoNameField( + 'mobile', + item, + __('Mobile phone'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'fax' %} + {{ fields.autoNameField( + 'fax', + item, + __('Fax'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'website' %} + {{ fields.autoNameField( + 'website', + item, + __('Website'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'email' %} + {{ fields.autoNameField( + 'email', + item, + _n('Email', 'Emails', 1), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'address' %} + {{ fields.textareaField('address', item.fields['address'], __('Address')) }} + {% elseif field is same as 'postalcode' %} + {{ fields.autoNameField( + 'postalcode', + item, + __('Postal code'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'town' %} + {{ fields.autoNameField( + 'town', + item, + __('City'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'postcode' %} + {{ fields.autoNameField( + 'postcode', + item, + __('Postal code'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'state' and (item_type == 'Supplier' or item_type == 'Contact') %} + {{ fields.autoNameField( + 'state', + item, + _x('location', 'State'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'country' %} + {{ fields.autoNameField( + 'country', + item, + __('Country'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'date_expiration' %} + {% if item_type == 'Certificate' %} + {{ fields.dateField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { + 'helper': __('Empty for infinite'), + 'checkIsExpired': false, + 'expiration_class' : params.expiration_class + }|merge(specific_field_options)) }} + {% elseif item_type == 'ItemAntivirus' %} + {{ fields.dateField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { + 'helper': __('Empty for infinite'), + 'checkIsExpired': true, + }|merge(specific_field_options)) }} + {% else %} + {{ fields.datetimeField('date_expiration', item.fields['date_expiration'], __('Expiration date'), { + 'helper': __('Empty for infinite'), + 'checkIsExpired': true + }|merge(specific_field_options)) }} + {% endif %} + {% elseif field is same as 'ref' %} + {{ fields.textField( + 'ref', + item.fields['ref'], + __('Reference'), + specific_field_options + ) }} + {% elseif field is same as 'users_id_tech' %} + {{ fields.dropdownField( + 'User', + 'users_id_tech', + item.fields['users_id_tech'], + __('Technician in charge'), + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + 'right': 'own_ticket', + }) + ) }} + {% elseif field is same as 'manufacturers_id' %} + {{ fields.dropdownField( + 'Manufacturer', + 'manufacturers_id', + item.fields['manufacturers_id'], + (item_type starts with 'Software' ? __('Publisher') : 'Manufacturer'|itemtype_name), + specific_field_options + ) }} + {% elseif field is same as 'groups_id_tech' %} + {{ fields.dropdownField( + 'Group', + 'groups_id_tech', + item.fields['groups_id_tech'], + __('Group in charge'), + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + 'condition': {'is_assign': 1}, + 'multiple': item is usingtrait('Glpi\\Features\\AssignableItem') ? true : false + }) + ) }} + {% elseif field is same as item.getModelForeignKeyField() %} + {% set model_itemtype = item.getModelClass() %} + {{ fields.dropdownField( + model_itemtype, + field, + item.fields[field], + _n('Model', 'Models', 1), + specific_field_options + ) }} + {% elseif field is same as 'contact_num' and item_type != 'SoftwareLicense' %} + {# TODO Drop unused 'contact_num' field in Software License schema? #} + {{ fields.textField( + 'contact_num', + item.fields['contact_num'], + __('Alternate username number'), + specific_field_options + ) }} + {% elseif field is same as 'serial' %} + {{ fields.textField( + 'serial', + item.fields['serial'], + __('Serial number'), + specific_field_options + ) }} + {% elseif field is same as 'contact' and item_type != 'SoftwareLicense' %} + {# TODO Drop unused 'contact' field in Software License schema? #} + {{ fields.textField( + 'contact', + item.fields['contact'], + __('Alternate username'), + specific_field_options + ) }} + {% elseif field is same as 'otherserial' %} + {{ fields.autoNameField( + 'otherserial', + item, + __('Inventory number'), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'sysdescr' %} + {{ fields.textareaField('sysdescr', item.fields['sysdescr'], __('Sysdescr')) }} + {% elseif field is same as 'snmpcredentials_id' %} + {{ fields.dropdownField( + 'SNMPCredential', + 'snmpcredentials_id', + item.fields['snmpcredentials_id'], + 'SNMPCredential'|itemtype_name, + specific_field_options + ) }} + {% elseif field is same as 'users_id' %} + {{ fields.dropdownField( + 'User', + 'users_id', + item.fields['users_id'], + 'User'|itemtype_name, + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + 'right': 'all', + }) + ) }} + {% elseif field is same as 'is_global' %} + {% set management_restrict = 0 %} + {% if item_type == 'Monitor' %} + {% set management_restrict = config('monitors_management_restrict') %} + {% elseif item_type == 'Peripheral' %} + {% set management_restrict = config('peripherals_management_restrict') %} + {% elseif item_type == 'Phone' %} + {% set management_restrict = config('phones_management_restrict') %} + {% elseif item_type == 'Printer' %} + {% set management_restrict = config('printers_management_restrict') %} + {% else %} + {% set management_restrict = 0 %} + {% endif %} + {% set dd_globalswitch %} + {% do call('Dropdown::showGlobalSwitch', [item.fields['id'], { + 'withtemplate': withtemplate, + 'value': item.fields['is_global'], + 'management_restrict': management_restrict, + 'target': target, + 'width': '100%', + }]) %} + {% endset %} + {{ fields.htmlField( + 'is_global', + dd_globalswitch, + __('Management type'), + ) }} + {% elseif field is same as 'size' %} + {{ fields.numberField( + 'size', + item.fields['size'], + __('Size'), + specific_field_options|merge({ + 'min': 0, + 'step': 0.01 + }), + ) }} + {% elseif field is same as 'networks_id' %} + {{ fields.dropdownField( + 'Network', + 'networks_id', + item.fields['networks_id'], + _n('Network', 'Networks', 1), + specific_field_options + ) }} + {% elseif field is same as 'groups_id' %} + {{ fields.dropdownField( + 'Group', + 'groups_id', + item.fields['groups_id'], + 'Group'|itemtype_name, + specific_field_options|merge({ + 'entity': item.fields['entities_id'], + 'condition': {'is_itemgroup': 1}, + 'multiple': item is usingtrait('Glpi\\Features\\AssignableItem') ? true : false + }) + ) }} + {% elseif field is same as 'uuid' %} + {{ fields.textField( + 'uuid', + item.fields['uuid'], + __('UUID'), + specific_field_options + ) }} + {% elseif field is same as 'version' %} + {{ fields.autoNameField( + 'version', + item, + _n('Version', 'Versions', 1), + withtemplate, + specific_field_options + ) }} + {% elseif field is same as 'comment' %} + {{ fields.textareaField( + 'comment', + item.fields['comment'], + _n('Comment', 'Comments', get_plural_number()), + specific_field_options + ) }} + {% elseif field is same as 'ram' %} + {{ fields.numberField( + 'ram', + item.fields['ram'], + __('%1$s (%2$s)')|format(_n('Memory', 'Memories', 1), __('Mio')), + specific_field_options + ) }} + {% elseif field is same as 'alarm_threshold' %} + {% set dd_alarm_treshold %} + {% do call('Dropdown::showNumber', ['alarm_threshold', { + 'value': item.fields['alarm_threshold'], + 'rand': rand, + 'width': '100%', + 'min': 0, + 'max': 100, + 'step': 1, + 'toadd': {'-1': __('Never')} + }|merge(params)]) %} + {% endset %} + {% set last_alert_html %} + + {% do call('Alert::displayLastAlert', [item_type, item.fields['id']]) %} + + {% endset %} + {{ fields.htmlField( + 'alarm_threshold', + dd_alarm_treshold, + __('Alert threshold'), + specific_field_options|merge({ + add_field_html: last_alert_html + }) + ) }} + {% elseif field is same as 'brand' %} + {{ fields.textField( + 'brand', + item.fields['brand'], + __('Brand'), + specific_field_options + ) }} + {% elseif field is same as 'begin_date' %} + {{ fields.dateField( + 'begin_date', + item.fields['begin_date'], + __('Start date'), + specific_field_options + ) }} + {% elseif field is same as 'autoupdatesystems_id' %} + {{ fields.dropdownField( + 'AutoUpdateSystem', + 'autoupdatesystems_id', + item.fields['autoupdatesystems_id'], + 'AutoUpdateSystem'|itemtype_name, + specific_field_options + ) }} + {% elseif field is same as 'pictures' %} + {{ fields.fileField('pictures', null, _n('Picture', 'Pictures', get_plural_number()), { + 'onlyimages': true, + 'multiple': true, + }) }} + {% elseif field is same as 'is_active' %} + {{ fields.dropdownYesNo('is_active', item.fields['is_active'], __('Active'), specific_field_options) }} + {% elseif field is same as 'last_boot' and item.isField('is_dynamic') and item.fields['is_dynamic'] %} + {# display last_boot data only if item is dynamic and have field #} + {{ fields.htmlField('last_boot', item.fields['last_boot']|formatted_datetime(true), __('Last boot date')) }} + {% elseif field in custom_fields|keys %} + {{ custom_fields[field].getFieldType().getFormInput(field, item.fields[field])|raw }} + {% endif %} + {% endfor %} {% if item_type in config("asset_types") %} {% set barcode %} diff --git a/templates/pages/admin/assetdefinition/fields_display.html.twig b/templates/pages/admin/assetdefinition/fields_display.html.twig new file mode 100644 index 00000000000..3a87f1b03a8 --- /dev/null +++ b/templates/pages/admin/assetdefinition/fields_display.html.twig @@ -0,0 +1,96 @@ +{# + # --------------------------------------------------------------------- + # + # GLPI - Gestionnaire Libre de Parc Informatique + # + # http://glpi-project.org + # + # @copyright 2015-2024 Teclib' and contributors. + # @licence https://www.gnu.org/licenses/gpl-3.0.html + # + # --------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of GLPI. + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + # + # --------------------------------------------------------------------- + #} + +{% extends "generic_show_form.html.twig" %} +{% import 'components/form/fields_macros.html.twig' as fields %} + +{% set all_fields = all_fields ?? {} %} + +{% set params = {} %} +{# do not display delete button #} +{% set params = params|merge({'candel': false}) %} +{# do not display footer with dates #} +{% set params = params|merge({'formfooter': false}) %} +{% set rand = rand|default(random()) %} + +{% block form_fields %} +
+
+ {% set btns %} + + {{ include('components/form/viewsubitem.html.twig', custom_field_form_params|merge({ + add_new_btn_class: 'btn-outline-secondary ms-2', + add_new_inline: true, + rand: rand, + }), with_context = false) }} + {% endset %} + + {% set get_fields_url = path('ajax/asset/assetdefinition.php?action=get_all_fields&assetdefinitions_id=' ~ item.getID()) %} + {{ fields.dropdownAjaxField(get_fields_url, 'new_field', '', _n('Field', 'Fields', 1), { + rand: rand, + templateSelection: 'formatAllFields', + templateResult: 'formatAllFields', + add_field_html: btns, + inline_add_field_html: true, + full_width: true, + flex_class: 'd-flex' + }) }} +
+
+
+ + + +{% endblock %} diff --git a/templates/pages/assets/asset.html.twig b/templates/pages/assets/asset.html.twig index 8eff694eb63..cd6b536ed83 100644 --- a/templates/pages/assets/asset.html.twig +++ b/templates/pages/assets/asset.html.twig @@ -43,9 +43,4 @@ _get['Glpi\\Asset\\AssetDefinition'|itemtype_foreign_key]|default(0), ) }} {% endif %} - - {% for custom_field_definition in custom_field_definitions %} - {% set field_name = 'custom_' ~ custom_field_definition.fields['name'] %} - {{ custom_field_definition.getFieldType().getFormInput(field_name, item.fields[field_name])|raw }} - {% endfor %} {% endblock %} diff --git a/tests/DbTestCase.php b/tests/DbTestCase.php index 041191586a7..aeb169ddf01 100644 --- a/tests/DbTestCase.php +++ b/tests/DbTestCase.php @@ -347,8 +347,9 @@ protected function initAssetDefinition( 'is_active' => true, 'capacities' => $capacities, 'profiles' => $profiles, + 'fields_display' => [], ], - skip_fields: ['capacities', 'profiles'] // JSON encoded fields cannot be automatically checked + skip_fields: ['capacities', 'profiles', 'fields_display'] // JSON encoded fields cannot be automatically checked ); $this->array($this->callPrivateMethod($definition, 'getDecodedCapacitiesField'))->isEqualTo($capacities); $this->array($this->callPrivateMethod($definition, 'getDecodedProfilesField'))->isEqualTo($profiles); diff --git a/tests/cypress/e2e/Asset/custom_fields.cy.js b/tests/cypress/e2e/Asset/custom_fields.cy.js index 37604f8d279..4c4428d96e2 100644 --- a/tests/cypress/e2e/Asset/custom_fields.cy.js +++ b/tests/cypress/e2e/Asset/custom_fields.cy.js @@ -36,11 +36,54 @@ describe("Custom Assets - Custom Fields", () => { cy.login(); }); - it('Custom field creation and display', () => { - // This can be split into multiple tests when the new API supports custom assets and custom fields - cy.visit('/front/asset/assetdefinition.form.php'); + function parseRequest(interception) { + const formDataObject = {}; + + if (interception.request.headers['content-type'].startsWith('multipart/form-data')) { + // Parse the multipart form data to an object + const boundary = interception.request.headers['content-type'].split('boundary=')[1]; + const parts = interception.request.body.split(`--${boundary}`).map((part) => part.trim()).filter((part) => part !== ''); + for (const part of parts) { + if (part.trim() === '--') { + continue; + } + const [, name, value] = part.match(/name="([^"]*)"\s*([\s\S]*)\s*/); + if (name.endsWith('[]')) { + const arrayName = name.slice(0, -2); + if (!formDataObject[arrayName]) { + formDataObject[arrayName] = []; + } + formDataObject[arrayName].push(value.trim()); + } else { + formDataObject[name] = value.trim(); + } + } + } else if (interception.request.headers['content-type'].startsWith('application/x-www-form-urlencoded')) { + // Parse the urlencoded form data to an object + const urlSearchParams = new URLSearchParams(interception.request.body); + for (const [name, value] of urlSearchParams) { + if (name.endsWith('[]')) { + const arrayName = name.slice(0, -2); + if (!formDataObject[arrayName]) { + formDataObject[arrayName] = []; + } + formDataObject[arrayName].push(value); + } else { + formDataObject[name] = value; + } + } + } + return formDataObject; + } + + const getAssetName = () => { const asset_name_chars = 'abcdefghijklmnopqrstuvwxyz'; - const asset_name = `customasset${Array.from({ length: 10 }, () => asset_name_chars.charAt(Math.floor(Math.random() * asset_name_chars.length))).join('')}`; + return `customasset${Array.from({ length: 10 }, () => asset_name_chars.charAt(Math.floor(Math.random() * asset_name_chars.length))).join('')}`; + }; + + it('Reordering fields', () => { + cy.visit('/front/asset/assetdefinition.form.php'); + const asset_name = getAssetName(); cy.findByLabelText(/System name/i).type(asset_name); cy.findByLabelText("Active").select('1', { force: true }); cy.findByRole('button', {name: "Add"}).click(); @@ -48,264 +91,164 @@ describe("Custom Assets - Custom Fields", () => { cy.get('div.toast-container .toast-body a').click(); cy.url().should('include', '/front/asset/assetdefinition.form.php?id='); - cy.findByRole('tab', {name: 'Custom fields'}).click(); - - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test String'); - cy.findByLabelText('Type').select('String', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'input:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test Text'); - cy.findByLabelText('Type').select('Text', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'input:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test Number'); - cy.findByLabelText('Type').select('Number', { force: true }); - - cy.findByLabelText('Minimum').type('{selectall}{del}10'); - cy.findByLabelText('Maximum').type('{selectall}{del}20'); - cy.findByLabelText('Step').type('{selectall}{del}2'); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'input:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value').type('{selectall}{del}12'); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test Date'); - cy.findByLabelText('Type').select('Date', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.get('input[name="default_value"]:not([readonly]):not([disabled]):not([required])').should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test Datetime'); - cy.findByLabelText('Type').select('Date and time', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.get('input[name="default_value"]:not([readonly]):not([disabled]):not([required])').should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test Dropdown'); - cy.findByLabelText('Type').select('Dropdown', { force: true }); - cy.findByLabelText('Item type').select('Monitor', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'select:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - // Test the default value input respects the "Multiple values" option - cy.findByLabelText('Multiple values').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value').should('have.attr', 'multiple'); - cy.findByLabelText('Multiple values').uncheck(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value').should('not.have.attr', 'multiple'); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test MultiDropdown'); - cy.findByLabelText('Type').select('Dropdown', { force: true }); - cy.findByLabelText('Item type').select('Monitor', { force: true }); - cy.findByLabelText('Multiple values').check(); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'select:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test URL'); - cy.findByLabelText('Type').select('URL', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'input:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test YesNo'); - cy.findByLabelText('Type').select('Yes/No', { force: true }); - - // Verify readonly and mandatory options don't affect the default value field - cy.findByLabelText('Readonly').check(); - cy.findByLabelText('Mandatory').check(); - cy.waitForNetworkIdle('/ajax/asset/customfield.php', 100); - cy.findByLabelText('Default value', {selector: 'select:not([readonly]):not([disabled]):not([required])'}).should('exist'); - cy.findByLabelText('Readonly').uncheck(); - cy.findByLabelText('Mandatory').uncheck(); - - cy.findByRole('button', {name: 'Add'}).click(); - }); - - //Check the Item type column in the custom fields list is correct for dropdowns - cy.findByRole('cell', {name: 'Test Dropdown'}).should('exist').siblings().contains('Monitor').should('exist'); - cy.findByRole('cell', {name: 'Test MultiDropdown'}).should('exist').siblings().contains('Monitor').should('exist'); - cy.findByRole('tab', {name: 'Profiles'}).click(); cy.get('input[type="checkbox"][id^="cb_checkall_table"]').check({ force: true }); cy.findByRole('button', {name: 'Save'}).click(); - cy.visit(`/front/asset/asset.form.php?class=${asset_name}&id=-1&withtemplate=2`); - // Validate the custom fields look OK - cy.findByLabelText('Test String') - .should('have.attr', 'type', 'text') - .should('have.attr', 'maxlength', '255'); - cy.findByLabelText('Test Text').should('be.visible'); - cy.findByLabelText('Test Number') - .should('have.attr', 'type', 'number') - .should('have.attr', 'min', '10') - .should('have.attr', 'max', '20') - .should('have.attr', 'step', '2'); - - // FlatPickr input is not reachable by its label. - cy.get('label').contains('Test Date').next().within(() => { - cy.get('.flatpickr-input').should('exist'); - }); - - // FlatPickr input is not reachable by its label. - cy.get('label').contains('Test Datetime').next().within(() => { - cy.get('.flatpickr-input').should('exist'); + cy.findByRole('tab', {name: /^Fields/}).click(); + cy.get('#sortable-fields[aria-dropeffect]').within(() => { + cy.get('.sortable-field[data-key="name"][draggable="true"]') + .as('name_field') + .should('be.visible') + .invoke('index').should('eq', 0); + cy.get('.sortable-field[data-key="states_id"][draggable="true"]') + .as('states_id_field') + .should('be.visible') + .invoke('index').should('eq', 1); + cy.get("@name_field").then(($name_field) => { + cy.get("@states_id_field").then(($states_id_field) => { + $name_field.insertAfter($states_id_field); + }); + }); }); + cy.findByRole('button', {name: 'Save'}).click(); - cy.getDropdownByLabelText('Test Dropdown') - .should('be.visible') - .should('have.class', 'select2-selection--single'); - cy.getDropdownByLabelText('Test MultiDropdown') - .should('be.visible') - .should('have.class', 'select2-selection--multiple'); - - cy.findByLabelText('Test URL').should('have.attr', 'type', 'url'); + cy.get('.sortable-field[data-key="name"][draggable="true"]').invoke('index').should('eq', 1); + cy.get('.sortable-field[data-key="states_id"][draggable="true"]').invoke('index').should('eq', 0); - cy.getDropdownByLabelText('Test YesNo').selectDropdownValue('No'); - cy.getDropdownByLabelText('Test YesNo').selectDropdownValue('Yes'); + cy.visit(`/front/asset/asset.form.php?class=${asset_name}&id=-1&withtemplate=2`); + cy.findByLabelText('Name').closest('.form-field').should('be.visible').invoke('index').should('eq', 1); + cy.findByLabelText('Status').closest('.form-field').should('be.visible').invoke('index').should('eq', 0); }); - it('Custom field update', () => { + it('Create custom fields', () => { + function createField(label, type, options = new Map()) { + cy.findByRole('button', {name: 'Create new field'}).click(); + cy.findByRole('button', {name: 'Create new field'}).siblings('.modal[data-cy-ready=true]').should('be.visible').within(() => { + cy.findByLabelText('Label').type(label); + cy.findByLabelText('Type').select(type, {force: true}); + + if (options.has('item_type')) { + cy.findByLabelText('Item type').select(options.get('item_type'), {force: true}); + } + if (options.has('min')) { + cy.findByLabelText('Minimum').type(`{selectall}{del}${options.get('min')}`); + } + if (options.has('max')) { + cy.findByLabelText('Maximum').type(`{selectall}{del}${options.get('max')}`); + } + if (options.has('step')) { + cy.findByLabelText('Step').type(`{selectall}{del}${options.get('step')}`); + } + if (options.has('multiple_values')) { + cy.findByLabelText('Multiple values').check(); + } + if (options.has('readonly')) { + cy.findByLabelText('Readonly').check(); + } + if (options.has('mandatory')) { + cy.findByLabelText('Mandatory').check(); + } + + cy.findByRole('button', {name: 'Add'}).click(); + cy.waitForNetworkIdle('/front/asset/customfielddefinition.form.php', 100); + }); + cy.findByRole('button', {name: 'Create new field'}).siblings('.modal').should('not.be.visible'); + cy.get(`.sortable-field[data-key="custom_${label.toLowerCase().replace(' ', '_')}"]`).should('be.visible'); + } + cy.visit('/front/asset/assetdefinition.form.php'); - const asset_name_chars = 'abcdefghijklmnopqrstuvwxyz'; - const asset_name = `customasset${Array.from({ length: 10 }, () => asset_name_chars.charAt(Math.floor(Math.random() * asset_name_chars.length))).join('')}`; + const asset_name = getAssetName(); cy.findByLabelText(/System name/i).type(asset_name); cy.findByLabelText("Active").select('1', { force: true }); cy.findByRole('button', {name: "Add"}).click(); - cy.get('div.toast-container .toast-body a').click(); cy.url().should('include', '/front/asset/assetdefinition.form.php?id='); - cy.findByRole('tab', {name: 'Custom fields'}).click(); - - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test String'); - cy.findByLabelText('Type').select('String', { force: true }); - cy.findByRole('button', {name: 'Add'}).click(); + cy.findByRole('tab', {name: /^Fields/}).click(); + + cy.intercept({ + pathname: '/ajax/asset/assetdefinition.php', + query: { + action: 'get_all_fields' + }, + times: 1 + }, (req) => { + req.reply({ + results: [ + {text: "Fake field", type: 'Glpi\\Asset\\CustomFieldType\\StringType', id: "fake_field"}, + {text: "Fake custom field", type: 'Glpi\\Asset\\CustomFieldType\\StringType', id: "custom_fake_field"} + ] + }); }); - cy.findByRole('cell', {name: 'Test String'}).click(); - cy.findByLabelText('Label').should('have.value', 'Test String'); - cy.findByLabelText('Type').should('not.exist'); - cy.get('span.form-control').contains('String').should('exist'); - cy.findByLabelText('System name').should('have.value', 'test_string'); - cy.findByLabelText('Label').type(' Updated'); - // System name preview should not update because the system name cannot/will not actually change - cy.findByLabelText('System name').should('have.value', 'test_string'); - cy.findByRole('button', {name: 'Save'}).click(); - - cy.findByRole('cell', {name: 'Test String Updated'}).click(); - cy.findByLabelText('Label').should('have.value', 'Test String Updated'); - cy.findByLabelText('Type').should('not.exist'); - cy.get('span.form-control').contains('String').should('exist'); - cy.findByLabelText('System name').should('have.value', 'test_string'); + cy.findByRole('button', {name: 'Create new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { + cy.findByLabelText('Field').siblings('.select2').find('.select2-selection__arrow').trigger('mousedown', {which: 1}); + + cy.root().closest('body').find('.select2-results__option').contains('Fake field').should('be.visible'); + cy.root().closest('body').find('.select2-results__option').contains('Fake custom field').should('be.visible'); + + createField('Test String', 'String'); + createField('Test Text', 'Text'); + createField('Test Number', 'Number', new Map([['min', '10'], ['max', '20'], ['step', '2']])); + createField('Test Date', 'Date'); + createField('Test Datetime', 'Date and time'); + createField('Test Dropdown', 'Dropdown', new Map([['item_type', 'Monitor']])); + createField('Test MultiDropdown', 'Dropdown', new Map([['item_type', 'Monitor'], ['multiple_values', true]])); + createField('Test URL', 'URL'); + createField('Test YesNo', 'Yes/No'); + + // Intercept form submission to check the form display values sent + cy.intercept('POST', '/front/asset/assetdefinition.form.php').as('saveFieldsDisplay'); + cy.findByRole('button', {name: 'Save'}).click({force: true}); + cy.wait('@saveFieldsDisplay').then((interception) => { + const formDataObject = parseRequest(interception); + expect(formDataObject['_update_fields_display']).to.be.equal('1'); + expect(formDataObject['fields_display']).to.include.members([ + 'custom_test_string', 'custom_test_text', 'custom_test_number', 'custom_test_date', 'custom_test_datetime', + 'custom_test_dropdown', 'custom_test_multidropdown', 'custom_test_url', 'custom_test_yesno' + ]); + }); + }); }); - it('Custom field delete', () => { + it('Edit core fields', () => { cy.visit('/front/asset/assetdefinition.form.php'); - const asset_name_chars = 'abcdefghijklmnopqrstuvwxyz'; - const asset_name = `customasset${Array.from({ length: 10 }, () => asset_name_chars.charAt(Math.floor(Math.random() * asset_name_chars.length))).join('')}`; + const asset_name = getAssetName(); cy.findByLabelText(/System name/i).type(asset_name); cy.findByLabelText("Active").select('1', { force: true }); cy.findByRole('button', {name: "Add"}).click(); - cy.get('div.toast-container .toast-body a').click(); cy.url().should('include', '/front/asset/assetdefinition.form.php?id='); - cy.findByRole('tab', {name: 'Custom fields'}).click(); + cy.findByRole('tab', {name: /^Fields/}).click(); - cy.findByRole('button', {name: 'Add a new field'}).parents('.tab-pane').should('have.class', 'active').first().within(() => { - cy.findByRole('button', {name: 'Add a new field'}).click(); - cy.findByLabelText('Label').type('Test String'); - cy.findByLabelText('Type').select('String', { force: true }); - cy.findByRole('button', {name: 'Add'}).click(); + cy.intercept({ + pathname: '/ajax/asset/assetdefinition.php', + query: { action: 'get_all_fields' }, + times: 1 + }, (req) => { + req.reply({ results: [] }); }); - cy.findByRole('cell', {name: 'Test String'}).click(); - cy.findByRole('button', {name: 'Delete permanently'}).click(); - cy.findByRole('cell', {name: 'Test String'}).should('not.exist'); + cy.get('.sortable-field[data-key="name"] .edit-field').click(); + cy.get('#core_field_options_editor').within(() => { + cy.findByLabelText('Full width').should('be.visible').check(); + cy.findByLabelText('Readonly').should('be.visible').check(); + cy.findByLabelText('Mandatory').should('be.visible').check(); + cy.intercept('POST', '/ajax/asset/assetdefinition.php').as('saveFieldOptions'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.wait('@saveFieldOptions').then((interception) => { + const formDataObject = parseRequest(interception); + expect(formDataObject['action']).to.be.equal('get_field_placeholder'); + cy.location('search').then((search) => { + expect(`${formDataObject['assetdefinitions_id']}`).to.be.equal((new URLSearchParams(search)).get('id')); + }); + expect(formDataObject['fields[0][key]']).to.be.equal('name'); + expect(formDataObject['fields[0][field_options][full_width]']).to.be.equal('1'); + expect(formDataObject['fields[0][field_options][readonly]']).to.be.equal('1'); + expect(formDataObject['fields[0][field_options][required]']).to.be.equal('1'); + }); + }); }); }); diff --git a/tests/functional/Glpi/Asset/AssetDefinition.php b/tests/functional/Glpi/Asset/AssetDefinition.php index bfb07580079..fc9e08c9093 100644 --- a/tests/functional/Glpi/Asset/AssetDefinition.php +++ b/tests/functional/Glpi/Asset/AssetDefinition.php @@ -254,6 +254,7 @@ protected function addInputProvider(): iterable 'capacities' => '[]', 'profiles' => '[]', 'translations' => '[]', + 'fields_display' => '[]', ], 'messages' => [], ]; @@ -295,6 +296,7 @@ protected function addInputProvider(): iterable 'capacities' => '[]', 'profiles' => '[]', 'translations' => '[]', + 'fields_display' => '[]', ], 'messages' => [], ]; @@ -308,6 +310,7 @@ protected function addInputProvider(): iterable 'capacities' => '[]', 'profiles' => '[]', 'translations' => '[]', + 'fields_display' => '[]', ], 'messages' => [], ]; @@ -335,6 +338,7 @@ protected function addInputProvider(): iterable 'capacities' => '[]', 'profiles' => '[]', 'translations' => '[]', + 'fields_display' => '[]', ], 'messages' => [], ]; @@ -361,6 +365,7 @@ protected function addInputProvider(): iterable 'capacities' => '[]', 'profiles' => '[]', 'translations' => '[]', + 'fields_display' => '[]', ], 'messages' => [], ]; @@ -385,6 +390,9 @@ protected function addInputProvider(): iterable // default value for `translations` $data['output']['translations'] = '[]'; } + if (is_array($data['output']) && !array_key_exists('fields_display', $data['output'])) { + $data['output']['fields_display'] = '[]'; + } yield $data; } }