Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

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

## [1.22.2] - 2025-10-24

Expand Down
162 changes: 162 additions & 0 deletions inc/destinationfield.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

/**
* -------------------------------------------------------------------------
* Fields plugin for GLPI
* -------------------------------------------------------------------------
*
* LICENSE
*
* This file is part of Fields.
*
* Fields 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 2 of the License, or
* (at your option) any later version.
*
* Fields 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 Fields. If not, see <http://www.gnu.org/licenses/>.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2013-2023 by Fields plugin team.
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
* @link https://github.com/pluginsGLPI/fields
* -------------------------------------------------------------------------
*/

use Glpi\Application\View\TemplateRenderer;
use Glpi\DBAL\JsonFieldInterface;
use Glpi\Form\AnswersSet;
use Glpi\Form\Destination\AbstractCommonITILFormDestination;
use Glpi\Form\Destination\AbstractConfigField;
use Glpi\Form\Destination\CommonITILField\Category;
use Glpi\Form\Destination\CommonITILField\SimpleValueConfig;
use Glpi\Form\Destination\FormDestination;
use Glpi\Form\Form;
use Glpi\Form\Question;
use Glpi\Form\QuestionType\QuestionTypeItemDropdown;

class PluginFieldsDestinationField extends AbstractConfigField
{
public function __construct(private AbstractCommonITILFormDestination $itil_destination) {}

#[Override]
public function getLabel(): string
{
return __('Additional fields', 'fields');
}

#[Override]
public function getConfigClass(): string
{
return SimpleValueConfig::class;
}

#[Override]
public function renderConfigForm(
Form $form,
FormDestination $destination,
JsonFieldInterface $config,
string $input_name,
array $display_options
): string {
if (!$config instanceof SimpleValueConfig) {
throw new InvalidArgumentException("Unexpected config class");
}

$twig = TemplateRenderer::getInstance();
return $twig->render('@fields/destinationfield.html.twig', [
'value' => $config->getValue(),
'input_name' => $input_name . "[" . SimpleValueConfig::VALUE . "]",
'options' => $display_options,
]);
}

#[Override]
public function applyConfiguratedValueToInputUsingAnswers(
JsonFieldInterface $config,
array $input,
AnswersSet $answers_set
): array {
if (!$config instanceof SimpleValueConfig) {
throw new InvalidArgumentException("Unexpected config class");
}

if ((bool) $config->getValue()) {
$answers = $answers_set->getAnswersByTypes([
PluginFieldsQuestionType::class,
QuestionTypeItemDropdown::class,
]);

foreach ($answers as $answer) {
$question = Question::getById($answer->getQuestionId());
$block_id = PluginFieldsContainer::findContainer($this->itil_destination->getTarget()::class, 'dom');
if (!$block_id) {
continue;
}

if ($question->getQuestionType() instanceof QuestionTypeItemDropdown) {
$itemtype = (new QuestionTypeItemDropdown())->getDefaultValueItemtype($question);
$field_name = $itemtype::getForeignKeyField();
if (!str_starts_with($field_name, 'plugin_fields_')) {
continue;
}

/** @var object{field_name: string} $item */
$item = getItemForItemtype($itemtype);
$field = new PluginFieldsField();
if (!$field->getFromDBByCrit(['name' => $item->field_name])) {
continue;
}

$value = $answer->getRawAnswer()['items_id'];
} else {
$field_id = (new PluginFieldsQuestionType())->getDefaultValueFieldId($question);
$field = PluginFieldsField::getById($field_id);
}

// Check that the field belongs to the correct block
if ($block_id != $field->fields[PluginFieldsContainer::getForeignKeyField()]) {
continue;
}

$input['c_id'] = $block_id;
if ($field->fields['type'] == 'dropdown') {
$field_name = 'plugin_fields_' . $field->fields['name'] . 'dropdowns_id';
} else {
$field_name = $field->fields['name'];
}

if ($field->fields['type'] == 'glpi_item') {
$input[sprintf('itemtype_%s', $field_name)] = $answer->getRawAnswer()['itemtype'];
$input[sprintf('items_id_%s', $field_name)] = $answer->getRawAnswer()['items_id'];
} else {
$input[$field_name] = $value ?? $answer->getRawAnswer();
}
}
}
return $input;
}

#[Override]
public function getDefaultConfig(Form $form): SimpleValueConfig
{
return new SimpleValueConfig("1");
}

#[Override]
public function getWeight(): int
{
return 1000;
}

#[Override]
public function getCategory(): Category
{
return Category::PROPERTIES;
}
}
16 changes: 14 additions & 2 deletions inc/questiontype.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ public function formatRawAnswer(mixed $answer, Question $question): string
case 'datetime':
return (new DateTime($answer))->format('Y-m-d H:i');
case 'glpi_item':
$itemtype = $answer['itemtype'];
if (!is_a($itemtype, CommonDBTM::class, true)) {
return '';
}

$item = $answer['itemtype']::getById($answer['items_id']);
if (!$item) {
return '';
Expand All @@ -219,10 +224,17 @@ public function formatRawAnswer(mixed $answer, Question $question): string
return '';
}

if (is_string($answer)) {
if (!is_array($answer)) {
$answer = [$answer];
}
return implode(', ', array_map(fn($items_id) => $itemtype::getById($items_id)->fields['name'], $answer));
$names = [];
foreach ($answer as $items_id) {
$item = $itemtype::getById($items_id);
if ($item) {
$names[] = $item->fields['name'];
}
}
return implode(', ', $names);
}

return (string) $answer;
Expand Down
6 changes: 6 additions & 0 deletions public/css/fields.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@ div.fields_clear {
}
}
}

.glpi-fields-plugin-question-type-glpi-destination-toggle {
.field-container > label {
margin-top: 0 !important;
Comment thread
Rom1-B marked this conversation as resolved.
}
}
23 changes: 22 additions & 1 deletion setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@
if (!file_exists(PLUGINFIELDS_FRONT_PATH)) {
mkdir(PLUGINFIELDS_FRONT_PATH);
}

use Glpi\Form\Destination\FormDestinationChange;
use Glpi\Form\Destination\FormDestinationManager;
use Glpi\Form\Destination\FormDestinationProblem;
use Glpi\Form\Destination\FormDestinationTicket;
use Glpi\Form\Migration\TypesConversionMapper;
use Glpi\Form\QuestionType\QuestionTypesManager;
use Symfony\Component\Yaml\Yaml;
Expand Down Expand Up @@ -399,11 +402,29 @@ function plugin_fields_register_plugin_types(): void
{
$types = QuestionTypesManager::getInstance();
$type_mapper = TypesConversionMapper::getInstance();
$destination_manager = FormDestinationManager::getInstance();

// Register question category, type and converter if valid fields are defined
if (PluginFieldsQuestionType::hasAvailableFields()) {
// Register question category
$types->registerPluginCategory(new PluginFieldsQuestionTypeCategory());

// Register question type
$types->registerPluginQuestionType(new PluginFieldsQuestionType());

// Register common ITIL field for tickets, changes and problems
foreach ([
new FormDestinationTicket(),
new FormDestinationChange(),
new FormDestinationProblem(),
] as $itil_destination) {
$destination_manager->registerPluginCommonITILConfigField(
$itil_destination::class,
new PluginFieldsDestinationField($itil_destination),
);
}

// Register converter for migration
$type_mapper->registerPluginQuestionTypeConverter('fields', new PluginFieldsQuestionType());
}
}
42 changes: 42 additions & 0 deletions templates/destinationfield.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{#
# -------------------------------------------------------------------------
# Fields plugin for GLPI
# -------------------------------------------------------------------------
#
# LICENSE
#
# This file is part of Fields.
#
# Fields 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 2 of the License, or
# (at your option) any later version.
#
# Fields 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 Fields. If not, see <http://www.gnu.org/licenses/>.
# -------------------------------------------------------------------------
# @copyright Copyright (C) 2013-2023 by Fields plugin team.
# @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html
# @link https://github.com/pluginsGLPI/fields
# -------------------------------------------------------------------------
#}

{% import 'components/form/fields_macros.html.twig' as fields %}

{{ fields.sliderField(
input_name,
value,
__('Bind additional fields to the destination', 'fields'),
options|merge({
'field_class': 'glpi-fields-plugin-question-type-glpi-destination-toggle',
'label_class': 'col fw-normal pt-0',
'input_class': 'col-auto',
'label_align': 'start',
'mb' : '',
})
) }}
51 changes: 46 additions & 5 deletions tests/FieldTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,75 @@
namespace GlpiPlugin\Field\Tests;

use DBmysql;
use DbTestCase;
use PluginFieldsContainer;
use PluginFieldsField;

abstract class FieldTestCase extends DbTestCase
trait FieldTestTrait
{
/** @var PluginFieldsContainer[] */
private static array $createdContainers = [];
/** @var PluginFieldsField[] */
private static array $createdFields = [];

public function tearDown(): void
public function tearDownFieldTest(): void
{
// Re-login to ensure we are logged in
$this->login();

// Clean created containers
array_map(
fn(PluginFieldsContainer $container) => $container->delete($container->fields, true),
self::$createdContainers,
);
self::$createdContainers = [];

// Clean created fields
array_map(
fn(PluginFieldsField $field) => $field->delete($field->fields, true),
self::$createdFields,
);
self::$createdFields = [];

/** @var DBmysql $DB */
global $DB;
$DB->clearSchemaCache();

parent::tearDown();
}

public function createFieldContainer(array $inputs): PluginFieldsContainer
{
// Re-login to ensure we are logged in
$this->login();

$container = $this->createItem(PluginFieldsContainer::class, $inputs, ['itemtypes']);
self::$createdContainers[] = $container;

// Re-initialize fields plugin to register new container logic
plugin_init_fields();

// Clear DB schema cache to avoid issues with new container
/** @var DBmysql $DB */
global $DB;
$DB->clearSchemaCache();

return $container;
}

public function createField(array $inputs): PluginFieldsField
{
// Re-login to ensure we are logged in
$this->login();

$field = $this->createItem(PluginFieldsField::class, $inputs, ['allowed_values', 'question_types']);
self::$createdFields[] = $field;

// Re-initialize fields plugin to register new field logic
plugin_init_fields();

// Clear DB schema cache to avoid issues with new field
/** @var DBmysql $DB */
global $DB;
$DB->clearSchemaCache();

return $field;
}
}
Loading