-
Notifications
You must be signed in to change notification settings - Fork 23
feat(forms): add Tag question type and category implementation #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ccailly
wants to merge
16
commits into
main
Choose a base branch
from
feature/implement-tag-question-type
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
18e58e4
feat(forms): add Tag question type and category implementation
ccailly 117e105
chore: remove empty template file
ccailly 8cbdb0f
chore: add margin bottom for itil objects
ccailly b5ec381
Update inc/questiontypecategory.class.php
ccailly ab48321
chore: add margin bottom for itil objects
ccailly 969cc43
chore: extract js to a module
ccailly 84e8a8d
Update phpstan.neon
ccailly 1ccee71
Update templates/question_dropdown.html.twig
ccailly 727ee5e
chore: update phpstan configuration and refine test assertions
ccailly 55aa37e
chore: update phpstan configuration to include tests directory and re…
ccailly 9bfa51b
Fix HR when tag is displayed bottom
stonebuzz a0a8ae4
fix UI for dropdown
stonebuzz 44a5809
test: add migration tests for tag question type
ccailly 375bba2
chore: update CHANGELOG to include Tag question type for new GLPI forms
ccailly d9da8c0
Fix lint
ccailly 915f80a
refactor: rename registerPluginTypes to plugin_tag_register_plugin_types
ccailly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include ../../PluginsMakefile.mk |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<?php | ||
|
||
/** | ||
* ------------------------------------------------------------------------- | ||
* Tag plugin for GLPI | ||
* ------------------------------------------------------------------------- | ||
* | ||
* LICENSE | ||
* | ||
* This file is part of Tag. | ||
* | ||
* Tag 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. | ||
* | ||
* Tag 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 Tag. If not, see <http://www.gnu.org/licenses/>. | ||
* ------------------------------------------------------------------------- | ||
* @copyright Copyright (C) 2014-2023 by Teclib'. | ||
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html | ||
* @link https://github.com/pluginsGLPI/tag | ||
* ------------------------------------------------------------------------- | ||
*/ | ||
|
||
use Glpi\Application\View\TemplateRenderer; | ||
use Glpi\Form\Form; | ||
use Glpi\Form\Migration\FormQuestionDataConverterInterface; | ||
use Glpi\Form\Question; | ||
use Glpi\Form\QuestionType\AbstractQuestionType; | ||
use Glpi\Form\QuestionType\QuestionTypeCategoryInterface; | ||
|
||
class PluginTagQuestionType extends AbstractQuestionType implements FormQuestionDataConverterInterface | ||
{ | ||
#[Override] | ||
Rom1-B marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public function getCategory(): QuestionTypeCategoryInterface | ||
{ | ||
return new PluginTagQuestionTypeCategory(); | ||
} | ||
|
||
#[Override] | ||
public function isAllowedForUnauthenticatedAccess(): bool | ||
{ | ||
return true; | ||
} | ||
|
||
#[Override] | ||
public function formatDefaultValueForDB(mixed $value): ?string | ||
{ | ||
if (!is_array($value)) { | ||
return null; | ||
} | ||
|
||
return implode(',', $value); | ||
} | ||
|
||
#[Override] | ||
public function formatRawAnswer(mixed $answer, Question $question): string | ||
{ | ||
if (!is_array($answer)) { | ||
throw new LogicException('Answer must be an array'); | ||
} | ||
|
||
if (count(array_filter($answer, 'is_numeric')) !== count($answer)) { | ||
throw new LogicException('Answer must be an array of numeric IDs'); | ||
} | ||
|
||
$tags = PluginTagTag::getByIds($answer); | ||
$tag_names = array_map(fn($tag) => $tag->fields['name'], $tags); | ||
|
||
return implode(',', $tag_names); | ||
} | ||
|
||
#[Override] | ||
public function renderAdministrationTemplate(?Question $question): string | ||
{ | ||
[$available_tags, $available_tags_color] = $this->getAvailableTags(); | ||
|
||
$twig = TemplateRenderer::getInstance(); | ||
return $twig->render('@tag/question_dropdown.html.twig', [ | ||
'input_name' => 'default_value', | ||
'selected_tags' => empty($question?->fields['default_value']) ? [] : explode(',', $question->fields['default_value']), | ||
'available_tags' => $available_tags, | ||
'tags_color' => $available_tags_color, | ||
'dropdown_params' => [ | ||
'no_label' => true, | ||
'init' => $question !== null, | ||
], | ||
]); | ||
} | ||
|
||
#[Override] | ||
public function renderEndUserTemplate(Question $question): string | ||
{ | ||
[$available_tags, $available_tags_color] = $this->getAvailableTags($question->getForm()); | ||
|
||
$twig = TemplateRenderer::getInstance(); | ||
return $twig->render('@tag/question_dropdown.html.twig', [ | ||
'input_name' => $question->getEndUserInputName(), | ||
'selected_tags' => empty($question?->fields['default_value']) ? [] : explode(',', $question->fields['default_value']), | ||
'available_tags' => $available_tags, | ||
'tags_color' => $available_tags_color, | ||
'show_search_tooltip' => false, | ||
'dropdown_params' => [ | ||
'no_label' => true, | ||
'init' => true, | ||
], | ||
]); | ||
} | ||
|
||
#[Override] | ||
public function beforeConversion(array $rawData): void {} | ||
|
||
#[Override] | ||
public function convertDefaultValue(array $rawData): null | ||
{ | ||
return null; | ||
} | ||
|
||
#[Override] | ||
public function convertExtraData(array $rawData): null | ||
{ | ||
return null; | ||
} | ||
|
||
#[Override] | ||
public function getTargetQuestionType(array $rawData): string | ||
{ | ||
return self::class; | ||
} | ||
|
||
private function getAvailableTags(?Form $form = null): array | ||
{ | ||
$active_entities_ids = Session::getActiveEntities(); | ||
if ($active_entities_ids === [] && $form) { | ||
$active_entities_ids = [$form->getEntityID()]; | ||
} | ||
|
||
$tag = new PluginTagTag(); | ||
$available_tags = []; | ||
$available_tags_color = []; | ||
$result = $tag->find([ | ||
'is_active' => 1, | ||
'OR' => [ | ||
['type_menu' => ['LIKE', '%\"Ticket\"%']], | ||
['type_menu' => ['LIKE', '%\"Change\"%']], | ||
['type_menu' => ['LIKE', '%\"Problem\"%']], | ||
['type_menu' => '0'], | ||
['type_menu' => ''], | ||
['type_menu' => 'NULL'], | ||
], | ||
] + getEntitiesRestrictCriteria('', '', $active_entities_ids, true), 'name'); | ||
foreach ($result as $id => $data) { | ||
$available_tags[$id] = $data['name']; | ||
$available_tags_color[$id] = $data['color'] ?: '#DDDDDD'; | ||
} | ||
return [$available_tags, $available_tags_color]; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -28,32 +28,22 @@ | |||||
* ------------------------------------------------------------------------- | ||||||
*/ | ||||||
|
||||||
use Glpi\Exception\Http\AccessDeniedHttpException; | ||||||
use Glpi\Form\QuestionType\QuestionTypeCategoryInterface; | ||||||
|
||||||
Session::checkRight(PluginTagTag::$rightname, UPDATE); | ||||||
|
||||||
if (!Plugin::isPluginActive("tag")) { | ||||||
throw new AccessDeniedHttpException(); | ||||||
} | ||||||
|
||||||
if (isset($_POST['add'])) { | ||||||
$item = new PluginTagTagItem(); | ||||||
class PluginTagQuestionTypeCategory implements QuestionTypeCategoryInterface | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{ | ||||||
public function getLabel(): string | ||||||
{ | ||||||
return __('Tag', 'tag'); | ||||||
} | ||||||
|
||||||
// Check unicity : | ||||||
if (isset($_REQUEST['plugin_tag_tags_id'])) { | ||||||
$found = $item->find([ | ||||||
'plugin_tag_tags_id' => $_REQUEST['plugin_tag_tags_id'], | ||||||
'items_id' => $_REQUEST['items_id'], | ||||||
'itemtype' => $_REQUEST['itemtype'], | ||||||
]); | ||||||
public function getIcon(): string | ||||||
{ | ||||||
return 'ti ti-tag'; | ||||||
} | ||||||
|
||||||
if (count($found) == 0) { | ||||||
$item->add($_REQUEST); | ||||||
} | ||||||
} else { | ||||||
$item->add($_REQUEST); | ||||||
public function getWeight(): int | ||||||
{ | ||||||
return 1000; | ||||||
} | ||||||
} | ||||||
|
||||||
$dropdown = new PluginTagTag(); | ||||||
include(GLPI_ROOT . "/front/dropdown.common.form.php"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/** | ||
* ------------------------------------------------------------------------- | ||
* Tag plugin for GLPI | ||
* ------------------------------------------------------------------------- | ||
* | ||
* LICENSE | ||
* | ||
* This file is part of Tag. | ||
* | ||
* Tag 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. | ||
* | ||
* Tag 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 Tag. If not, see <http://www.gnu.org/licenses/>. | ||
* ------------------------------------------------------------------------- | ||
* @copyright Copyright (C) 2014-2023 by Teclib'. | ||
* @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html | ||
* @link https://github.com/pluginsGLPI/tag | ||
* ------------------------------------------------------------------------- | ||
*/ | ||
|
||
export class GlpiPluginTagTagDropdownColorizer { | ||
constructor(tagsColor, selector, $container) { | ||
this.tagsColor = tagsColor; | ||
this.selector = selector; | ||
this.$container = $container; | ||
|
||
this.init(); | ||
} | ||
|
||
isDark(hexColor) { | ||
if (!hexColor) return false; | ||
hexColor = hexColor.replace('#', ''); | ||
const r = parseInt(hexColor.substr(0, 2), 16); | ||
const g = parseInt(hexColor.substr(2, 2), 16); | ||
const b = parseInt(hexColor.substr(4, 2), 16); | ||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; | ||
return luminance < 0.5; | ||
} | ||
|
||
applyTagColors($select) { | ||
const selectedIds = $select.find('option:selected').map(function() { | ||
return $(this).val(); | ||
}).get(); | ||
|
||
const $container = $select.nextAll('.select2').find('.select2-selection__rendered'); | ||
$container.find('.select2-selection__choice').each((index, element) => { | ||
const id = selectedIds[index]; | ||
const color = this.tagsColor[id]; | ||
if (color) { | ||
$(element).css('background-color', color); | ||
$(element).css('color', this.isDark(color) ? '#eeeeee' : ''); | ||
|
||
// Also style the remove button for better visibility | ||
$(element).find('.select2-selection__choice__remove').css('color', this.isDark(color) ? '#eeeeee' : ''); | ||
} | ||
}); | ||
} | ||
|
||
init() { | ||
const $select = this.$container.find(this.selector); | ||
|
||
$select.each((index, element) => { | ||
this.applyTagColors($(element)); | ||
}); | ||
|
||
$select.on('change select2:select select2:unselect', (event) => { | ||
this.applyTagColors($(event.target)); | ||
}); | ||
|
||
$select.on('select2:open', () => { | ||
setTimeout(() => { | ||
$('.select2-results__option').each((index, element) => { | ||
const matches = element.id.match(/result-[^-]+-(\d+)$/); | ||
if (matches && matches[1]) { | ||
const color = this.tagsColor[matches[1]]; | ||
// Cible uniquement le span SANS la classe select2-rendered__match | ||
$(element).find('span:not(.select2-rendered__match)').css({ | ||
'background-color': color ? color : '', | ||
'padding': color ? '2px' : '', | ||
'color': (color && this.isDark(color)) ? '#fff' : '', | ||
'border-radius': '2px' | ||
}); | ||
} | ||
}); | ||
}, 0); | ||
}); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.