Skip to content

Commit 681bef3

Browse files
authored
feat(forms): add Fields destination (#1064)
* feat(forms): add Fields destination * refactor: move inline twig template to dedicated twig file * fix: PHP lint * chore: update CHANGELOG
1 parent eb9719c commit 681bef3

11 files changed

+670
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Added
1111

1212
- Implement `Field` question type for new GLPI forms
13+
- Bind the answers to the `Field` question type to the corresponding additional fields
1314

1415
## [1.22.2] - 2025-10-24
1516

inc/destinationfield.class.php

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
/**
4+
* -------------------------------------------------------------------------
5+
* Fields plugin for GLPI
6+
* -------------------------------------------------------------------------
7+
*
8+
* LICENSE
9+
*
10+
* This file is part of Fields.
11+
*
12+
* Fields is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License as published by
14+
* the Free Software Foundation; either version 2 of the License, or
15+
* (at your option) any later version.
16+
*
17+
* Fields is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU General Public License
23+
* along with Fields. If not, see <http://www.gnu.org/licenses/>.
24+
* -------------------------------------------------------------------------
25+
* @copyright Copyright (C) 2013-2023 by Fields plugin team.
26+
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
27+
* @link https://github.com/pluginsGLPI/fields
28+
* -------------------------------------------------------------------------
29+
*/
30+
31+
use Glpi\Application\View\TemplateRenderer;
32+
use Glpi\DBAL\JsonFieldInterface;
33+
use Glpi\Form\AnswersSet;
34+
use Glpi\Form\Destination\AbstractCommonITILFormDestination;
35+
use Glpi\Form\Destination\AbstractConfigField;
36+
use Glpi\Form\Destination\CommonITILField\Category;
37+
use Glpi\Form\Destination\CommonITILField\SimpleValueConfig;
38+
use Glpi\Form\Destination\FormDestination;
39+
use Glpi\Form\Form;
40+
use Glpi\Form\Question;
41+
use Glpi\Form\QuestionType\QuestionTypeItemDropdown;
42+
43+
class PluginFieldsDestinationField extends AbstractConfigField
44+
{
45+
public function __construct(private AbstractCommonITILFormDestination $itil_destination) {}
46+
47+
#[Override]
48+
public function getLabel(): string
49+
{
50+
return __('Additional fields', 'fields');
51+
}
52+
53+
#[Override]
54+
public function getConfigClass(): string
55+
{
56+
return SimpleValueConfig::class;
57+
}
58+
59+
#[Override]
60+
public function renderConfigForm(
61+
Form $form,
62+
FormDestination $destination,
63+
JsonFieldInterface $config,
64+
string $input_name,
65+
array $display_options
66+
): string {
67+
if (!$config instanceof SimpleValueConfig) {
68+
throw new InvalidArgumentException("Unexpected config class");
69+
}
70+
71+
$twig = TemplateRenderer::getInstance();
72+
return $twig->render('@fields/destinationfield.html.twig', [
73+
'value' => $config->getValue(),
74+
'input_name' => $input_name . "[" . SimpleValueConfig::VALUE . "]",
75+
'options' => $display_options,
76+
]);
77+
}
78+
79+
#[Override]
80+
public function applyConfiguratedValueToInputUsingAnswers(
81+
JsonFieldInterface $config,
82+
array $input,
83+
AnswersSet $answers_set
84+
): array {
85+
if (!$config instanceof SimpleValueConfig) {
86+
throw new InvalidArgumentException("Unexpected config class");
87+
}
88+
89+
if ((bool) $config->getValue()) {
90+
$answers = $answers_set->getAnswersByTypes([
91+
PluginFieldsQuestionType::class,
92+
QuestionTypeItemDropdown::class,
93+
]);
94+
95+
foreach ($answers as $answer) {
96+
$question = Question::getById($answer->getQuestionId());
97+
$block_id = PluginFieldsContainer::findContainer($this->itil_destination->getTarget()::class, 'dom');
98+
if (!$block_id) {
99+
continue;
100+
}
101+
102+
if ($question->getQuestionType() instanceof QuestionTypeItemDropdown) {
103+
$itemtype = (new QuestionTypeItemDropdown())->getDefaultValueItemtype($question);
104+
$field_name = $itemtype::getForeignKeyField();
105+
if (!str_starts_with($field_name, 'plugin_fields_')) {
106+
continue;
107+
}
108+
109+
/** @var object{field_name: string} $item */
110+
$item = getItemForItemtype($itemtype);
111+
$field = new PluginFieldsField();
112+
if (!$field->getFromDBByCrit(['name' => $item->field_name])) {
113+
continue;
114+
}
115+
116+
$value = $answer->getRawAnswer()['items_id'];
117+
} else {
118+
$field_id = (new PluginFieldsQuestionType())->getDefaultValueFieldId($question);
119+
$field = PluginFieldsField::getById($field_id);
120+
}
121+
122+
// Check that the field belongs to the correct block
123+
if ($block_id != $field->fields[PluginFieldsContainer::getForeignKeyField()]) {
124+
continue;
125+
}
126+
127+
$input['c_id'] = $block_id;
128+
if ($field->fields['type'] == 'dropdown') {
129+
$field_name = 'plugin_fields_' . $field->fields['name'] . 'dropdowns_id';
130+
} else {
131+
$field_name = $field->fields['name'];
132+
}
133+
134+
if ($field->fields['type'] == 'glpi_item') {
135+
$input[sprintf('itemtype_%s', $field_name)] = $answer->getRawAnswer()['itemtype'];
136+
$input[sprintf('items_id_%s', $field_name)] = $answer->getRawAnswer()['items_id'];
137+
} else {
138+
$input[$field_name] = $value ?? $answer->getRawAnswer();
139+
}
140+
}
141+
}
142+
return $input;
143+
}
144+
145+
#[Override]
146+
public function getDefaultConfig(Form $form): SimpleValueConfig
147+
{
148+
return new SimpleValueConfig("1");
149+
}
150+
151+
#[Override]
152+
public function getWeight(): int
153+
{
154+
return 1000;
155+
}
156+
157+
#[Override]
158+
public function getCategory(): Category
159+
{
160+
return Category::PROPERTIES;
161+
}
162+
}

inc/questiontype.class.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ public function formatRawAnswer(mixed $answer, Question $question): string
205205
case 'datetime':
206206
return (new DateTime($answer))->format('Y-m-d H:i');
207207
case 'glpi_item':
208+
$itemtype = $answer['itemtype'];
209+
if (!is_a($itemtype, CommonDBTM::class, true)) {
210+
return '';
211+
}
212+
208213
$item = $answer['itemtype']::getById($answer['items_id']);
209214
if (!$item) {
210215
return '';
@@ -219,10 +224,17 @@ public function formatRawAnswer(mixed $answer, Question $question): string
219224
return '';
220225
}
221226

222-
if (is_string($answer)) {
227+
if (!is_array($answer)) {
223228
$answer = [$answer];
224229
}
225-
return implode(', ', array_map(fn($items_id) => $itemtype::getById($items_id)->fields['name'], $answer));
230+
$names = [];
231+
foreach ($answer as $items_id) {
232+
$item = $itemtype::getById($items_id);
233+
if ($item) {
234+
$names[] = $item->fields['name'];
235+
}
236+
}
237+
return implode(', ', $names);
226238
}
227239

228240
return (string) $answer;

public/css/fields.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,9 @@ div.fields_clear {
8989
}
9090
}
9191
}
92+
93+
.glpi-fields-plugin-question-type-glpi-destination-toggle {
94+
.field-container > label {
95+
margin-top: 0 !important;
96+
}
97+
}

setup.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@
6565
if (!file_exists(PLUGINFIELDS_FRONT_PATH)) {
6666
mkdir(PLUGINFIELDS_FRONT_PATH);
6767
}
68-
68+
use Glpi\Form\Destination\FormDestinationChange;
69+
use Glpi\Form\Destination\FormDestinationManager;
70+
use Glpi\Form\Destination\FormDestinationProblem;
71+
use Glpi\Form\Destination\FormDestinationTicket;
6972
use Glpi\Form\Migration\TypesConversionMapper;
7073
use Glpi\Form\QuestionType\QuestionTypesManager;
7174
use Symfony\Component\Yaml\Yaml;
@@ -399,11 +402,29 @@ function plugin_fields_register_plugin_types(): void
399402
{
400403
$types = QuestionTypesManager::getInstance();
401404
$type_mapper = TypesConversionMapper::getInstance();
405+
$destination_manager = FormDestinationManager::getInstance();
402406

403407
// Register question category, type and converter if valid fields are defined
404408
if (PluginFieldsQuestionType::hasAvailableFields()) {
409+
// Register question category
405410
$types->registerPluginCategory(new PluginFieldsQuestionTypeCategory());
411+
412+
// Register question type
406413
$types->registerPluginQuestionType(new PluginFieldsQuestionType());
414+
415+
// Register common ITIL field for tickets, changes and problems
416+
foreach ([
417+
new FormDestinationTicket(),
418+
new FormDestinationChange(),
419+
new FormDestinationProblem(),
420+
] as $itil_destination) {
421+
$destination_manager->registerPluginCommonITILConfigField(
422+
$itil_destination::class,
423+
new PluginFieldsDestinationField($itil_destination),
424+
);
425+
}
426+
427+
// Register converter for migration
407428
$type_mapper->registerPluginQuestionTypeConverter('fields', new PluginFieldsQuestionType());
408429
}
409430
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{#
2+
# -------------------------------------------------------------------------
3+
# Fields plugin for GLPI
4+
# -------------------------------------------------------------------------
5+
#
6+
# LICENSE
7+
#
8+
# This file is part of Fields.
9+
#
10+
# Fields is free software; you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# Fields is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with Fields. If not, see <http://www.gnu.org/licenses/>.
22+
# -------------------------------------------------------------------------
23+
# @copyright Copyright (C) 2013-2023 by Fields plugin team.
24+
# @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
25+
# @link https://github.com/pluginsGLPI/fields
26+
# -------------------------------------------------------------------------
27+
#}
28+
29+
{% import 'components/form/fields_macros.html.twig' as fields %}
30+
31+
{{ fields.sliderField(
32+
input_name,
33+
value,
34+
__('Bind additional fields to the destination', 'fields'),
35+
options|merge({
36+
'field_class': 'glpi-fields-plugin-question-type-glpi-destination-toggle',
37+
'label_class': 'col fw-normal pt-0',
38+
'input_class': 'col-auto',
39+
'label_align': 'start',
40+
'mb' : '',
41+
})
42+
) }}

tests/FieldTestCase.php

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,75 @@
3131
namespace GlpiPlugin\Field\Tests;
3232

3333
use DBmysql;
34-
use DbTestCase;
3534
use PluginFieldsContainer;
35+
use PluginFieldsField;
3636

37-
abstract class FieldTestCase extends DbTestCase
37+
trait FieldTestTrait
3838
{
39+
/** @var PluginFieldsContainer[] */
3940
private static array $createdContainers = [];
41+
/** @var PluginFieldsField[] */
42+
private static array $createdFields = [];
4043

41-
public function tearDown(): void
44+
public function tearDownFieldTest(): void
4245
{
46+
// Re-login to ensure we are logged in
47+
$this->login();
48+
4349
// Clean created containers
4450
array_map(
4551
fn(PluginFieldsContainer $container) => $container->delete($container->fields, true),
4652
self::$createdContainers,
4753
);
4854
self::$createdContainers = [];
4955

56+
// Clean created fields
57+
array_map(
58+
fn(PluginFieldsField $field) => $field->delete($field->fields, true),
59+
self::$createdFields,
60+
);
61+
self::$createdFields = [];
62+
5063
/** @var DBmysql $DB */
5164
global $DB;
5265
$DB->clearSchemaCache();
53-
54-
parent::tearDown();
5566
}
5667

5768
public function createFieldContainer(array $inputs): PluginFieldsContainer
5869
{
70+
// Re-login to ensure we are logged in
71+
$this->login();
72+
5973
$container = $this->createItem(PluginFieldsContainer::class, $inputs, ['itemtypes']);
6074
self::$createdContainers[] = $container;
6175

76+
// Re-initialize fields plugin to register new container logic
77+
plugin_init_fields();
78+
79+
// Clear DB schema cache to avoid issues with new container
80+
/** @var DBmysql $DB */
81+
global $DB;
82+
$DB->clearSchemaCache();
83+
6284
return $container;
6385
}
86+
87+
public function createField(array $inputs): PluginFieldsField
88+
{
89+
// Re-login to ensure we are logged in
90+
$this->login();
91+
92+
$field = $this->createItem(PluginFieldsField::class, $inputs, ['allowed_values', 'question_types']);
93+
self::$createdFields[] = $field;
94+
95+
// Re-initialize fields plugin to register new field logic
96+
plugin_init_fields();
97+
98+
// Clear DB schema cache to avoid issues with new field
99+
/** @var DBmysql $DB */
100+
global $DB;
101+
$DB->clearSchemaCache();
102+
103+
return $field;
104+
}
64105
}

0 commit comments

Comments
 (0)