Skip to content
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

Add basic support for form tags #17089

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions ajax/form/form_tags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2024 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @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 <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

use Glpi\Form\Form;

include('../../inc/includes.php');

// The user must be able to respond to forms.
Session::checkRight(Form::$rightname, UPDATE);

// Get tags
$tags = [
[
'label' => 'Exemple tag 1',
],
[
'label' => 'Exemple tag 2',
],
[
'label' => 'Exemple tag 3',
],
];
header('Content-Type: application/json');
echo json_encode($tags);
7 changes: 7 additions & 0 deletions css/includes/components/_richtext.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
body.mce-content-body {
margin: 15px 10px;

[data-form-tag="true"],
[data-user-mention="true"] {
cursor: default !important; // Overrides "not-allowed" cursor on noneditable content
}
Expand Down Expand Up @@ -100,3 +101,9 @@ body.mce-content-body {
border-radius: 3px;
padding: 5px;
}

[data-form-tag="true"] {
background: rgba(0, 0, 0, 20%);
border-radius: 3px;
padding: 5px;
}
106 changes: 106 additions & 0 deletions js/RichText/FormTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2024 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @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 <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

/* global tinymce */

var GLPI = GLPI || {};
GLPI.RichText = GLPI.RichText || {};

/**
* Form tags rich text autocompleter.
*
* @since 11.0.0
*/
GLPI.RichText.FormTags = class
{
/**
* Target tinymce editor.
* @type {TinyMCE.Editor}
*/
#editor;

/**
* @param {Editor} editor
*/
constructor(editor) {
this.#editor = editor;
}

/**
* Register as autocompleter to editor.
*
* @returns {void}
*/
register() {
// Register autocompleter
this.#editor.ui.registry.addAutocompleter(
'form_tags',
{
trigger: '#',
minChars: 0,
fetch: () => this.#fetchItems(),
onAction: (autocompleteApi, range, value) => {
this.#insertTag(autocompleteApi, range, value);
}
}
);
}

async #fetchItems() {
const url = CFG_GLPI.root_doc + '/ajax/form/form_tags.php';
const data = await $.get(url);

return data.map((tag) => ({
type: 'autocompleteitem',
value: JSON.stringify(tag),
text: tag.label,
}));
}

#insertTag(autocompleteApi, range, value) {
this.#editor.selection.setRng(range);
this.#editor.insertContent(this.#generateTagHtml(value));

autocompleteApi.hide();
}

#generateTagHtml(value) {
const tag = JSON.parse(value);
return `
<span
contenteditable="false"
data-form-tag="true"
>${tag.label}</span>&nbsp;
`;
}
};
1 change: 1 addition & 0 deletions src/Form/Destination/CommonITILField/ContentField.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public function renderConfigForm(
options|merge({
'enable_richtext': true,
'enable_images': false,
'enable_form_tags': true,
cedric-anne marked this conversation as resolved.
Show resolved Hide resolved
})
) }}
TWIG;
Expand Down
1 change: 1 addition & 0 deletions src/Html.php
Original file line number Diff line number Diff line change
Expand Up @@ -5876,6 +5876,7 @@ public static function requireJs($name)
break;
case 'tinymce':
$_SESSION['glpi_js_toload'][$name][] = 'public/lib/tinymce.js';
$_SESSION['glpi_js_toload'][$name][] = 'js/RichText/FormTags.js';
$_SESSION['glpi_js_toload'][$name][] = 'js/RichText/UserMention.js';
$_SESSION['glpi_js_toload'][$name][] = 'js/RichText/ContentTemplatesParameters.js';
break;
Expand Down
1 change: 1 addition & 0 deletions src/RichText/RichText.php
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ private static function getHtmlSanitizer(): HtmlSanitizer
// required for user mentions
'data-user-mention',
'data-user-id',
'data-form-tag',
];
foreach ($rich_text_completion_attributes as $attribute) {
$config = $config->allowAttribute($attribute, 'span');
Expand Down
12 changes: 12 additions & 0 deletions templates/components/form/basic_inputs_macros.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
'toolbar_location': 'top',
'init': true,
'placeholder': "",
'enable_form_tags': false,
}|merge(options) %}

{% if options.fields_template.isMandatoryField(name) %}
Expand Down Expand Up @@ -340,6 +341,17 @@
]) %}
{% endif %}

{% if options.enable_form_tags %}
<script>
$(function() {
const form_tags = new GLPI.RichText.FormTags(
tinymce.get('{{ options.id }}'),
);
form_tags.register();
});
</script>
{% endif %}

{% if options.enable_mentions and config('use_notifications') %}
<script>
$(function() {
Expand Down
91 changes: 91 additions & 0 deletions tests/cypress/e2e/form_tags.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2024 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @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 <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

describe('Form tags', () => {
beforeEach(() => {
cy.createWithAPI('Glpi\\Form\\Form', {
'name': 'Test form for the form tags suite',
}).as('form_id').then((form_id) => {
cy.createWithAPI('Glpi\\Form\\Destination\\FormDestination', {
'forms_forms_id': form_id,
'itemtype': 'Glpi\\Form\\Destination\\FormDestinationTicket',
'name': 'Test ticket 1',
});
});

cy.login();
cy.changeProfile('Super-Admin', true);

cy.get('@form_id').then((form_id) => {
const tab = 'Glpi\\Form\\Destination\\FormDestination$1';
cy.visit(`/front/form/form.form.php?id=${form_id}&forcetab=${tab}`);
cy.findByRole('button', {name: "Test ticket 1"}).click();
});
});

it('tags autocompletion is loaded and values are preserved on reload', () => {
// Auto completion is not yet opened
cy.findByRole("menuitem", {name: "Exemple tag 1"}).should('not.exist');
cy.findByRole("menuitem", {name: "Exemple tag 2"}).should('not.exist');
cy.findByRole("menuitem", {name: "Exemple tag 3"}).should('not.exist');

// Use autocomplete
cy.findByLabelText("Content").awaitTinyMCE().as("rich_text_editor");
cy.get("@rich_text_editor").type("#");
cy.findByRole("menuitem", {name: "Exemple tag 1"}).should('exist');
cy.findByRole("menuitem", {name: "Exemple tag 2"}).should('exist');
cy.findByRole("menuitem", {name: "Exemple tag 3"}).should('exist').click();

// Auto completion UI is terminated after clicking on the item.
cy.findByRole("menuitem", {name: "Exemple tag 1"}).should('not.exist');
cy.findByRole("menuitem", {name: "Exemple tag 2"}).should('not.exist');
cy.findByRole("menuitem", {name: "Exemple tag 3"}).should('not.exist');

// Item has been inserted into rich text
cy.get("@rich_text_editor")
.findByText("Exemple tag 3")
.should('have.attr', 'contenteditable', 'false')
.should('have.attr', 'data-form-tag', 'true')
;

// Save form
cy.findByRole("button", {name: "Update item"}).click();

// Rich text content provided by autocompleted values should be displayed properly
cy.findByLabelText("Content").awaitTinyMCE()
.findByText("Exemple tag 3")
.should('have.attr', 'contenteditable', 'false')
.should('have.attr', 'data-form-tag', 'true')
;
});
});
11 changes: 11 additions & 0 deletions tests/units/Glpi/RichText/RichText.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,17 @@ public function () {
</p>
HTML,
];
yield 'Do not remove content editable on span' => [
'content' => '<span contenteditable="true">Editable content</span>',
'encode_output_entities' => false,
'expected_result' => '<span contenteditable="true">Editable content</span>',
];

yield 'Do not remove data-form-tag property' => [
'content' => '<span data-form-tag="true">Tag label</span>',
'encode_output_entities' => false,
'expected_result' => '<span data-form-tag="true">Tag label</span>',
];
}

/**
Expand Down