From c1dff7149dec7c7406300ed77dd0fc0c3f5bf322 Mon Sep 17 00:00:00 2001 From: Curtis Conard Date: Thu, 16 Oct 2025 17:28:21 -0400 Subject: [PATCH 1/5] refactor timeline js and lazy load user info cards --- js/modules/ITIL/Timeline/TimelineView.js | 331 +++++++++++++++++ .../itilobject/actors/field.html.twig | 346 +----------------- .../itilobject/actors/main.html.twig | 3 + .../components/itilobject/layout.html.twig | 24 +- 4 files changed, 361 insertions(+), 343 deletions(-) create mode 100644 js/modules/ITIL/Timeline/TimelineView.js diff --git a/js/modules/ITIL/Timeline/TimelineView.js b/js/modules/ITIL/Timeline/TimelineView.js new file mode 100644 index 00000000000..8f7289a9e64 --- /dev/null +++ b/js/modules/ITIL/Timeline/TimelineView.js @@ -0,0 +1,331 @@ +/** + * --------------------------------------------------------------------- + * + * GLPI - Gestionnaire Libre de Parc Informatique + * + * http://glpi-project.org + * + * @copyright 2015-2025 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 . + * + * --------------------------------------------------------------------- + */ + +export class TimelineView { + constructor(element, safe_item_fields) { + this.element = element; + this.safe_item_fields = safe_item_fields; + this.info_card_cache = new Map(); + this.initActorFields(); + } + + initActorFields() { + const entities_id = this.element.attr('data-entities-id'); + const itiltemplate_type = this.element.attr('data-itiltemplate-class'); + const itiltemplate_id = this.element.attr('data-itiltemplate-id'); + const itemtype = this.element.attr('data-itemtype'); + const item_id = this.element.attr('data-items-id'); + const is_new_item = item_id <= 0; + + $('select[data-actor-type]').each((index, element) => { + const $element = $(element); + const actor_type = $element.attr('data-actor-type'); + const idor_token = $element.attr('data-idor'); + const returned_itemtypes = ($element.attr('data-returned-itemtypes') || '').split(','); + const can_update = $element.attr('data-canupdate') !== undefined; + const allow_auto_submit = $element.attr('data-allow-auto-submit') !== undefined; + + const genericTemplate = (option = {}, is_selection = false) => { + const element = $(option.element); + const itemtype = element.data('itemtype') ?? option.itemtype; + const items_id = element.data('items-id') ?? option.items_id; + let text = window._.escape(element.data('text') ?? option.text ?? ''); + const title = window._.escape(element.data('title') ?? option.title ?? ''); + const use_notif = element.data('use-notification') ?? option.use_notification ?? 1; + const alt_email = element.data('alternative-email') ?? option.alternative_email ?? ''; + + let icon = ""; + let fk = ""; + + switch (itemtype) { + case 'User': + if (items_id == 0) { + text = alt_email; + icon = ``; + } else { + icon = ``; + } + if (actor_type === "assign") { + fk = "users_id_assign"; + } else if (actor_type === "requester") { + fk = "users_id_requester"; + } else if (actor_type === "observer") { + fk = "users_id_observer"; + } + break; + case "Group": + icon = ``; + if (actor_type === "assign") { + fk = "groups_id_assign"; + } else if (actor_type === "requester") { + fk = "groups_id_requester"; + } else if (actor_type === "observer") { + fk = "groups_id_observer"; + } + break; + case "Supplier": + icon = ``; + fk = "suppliers_id_assign"; + break; + } + + let actions = ''; + if (can_update && ['User', 'Supplier', 'Email'].includes(itemtype) && is_selection) { + actions = ` + + `; + } + // manage specific display for tree data (like groups) + let indent = ""; + if (!is_selection && "level" in option && option.level > 1) { + for (let index = 1; index < option.level; index++) { + indent = `   ${indent}`; + } + indent = `${indent}»`; + } + // prepare html for option element + text = (is_selection && itemtype === "Group") ? title : text; + const option_text = `${text}`; + const option_element = $(` + ${indent}${icon}${option_text}${actions}`); + + // manage ticket information (number of assigned ticket for an actor) + if (is_selection && itemtype !== "Email") { + let label = ''; + if (actor_type === "assign") { + label = __('Number of tickets already assigned'); + } else if (actor_type === "requester") { + label = __('Number of tickets as requester'); + } + const existing_element = $(` + + + + `); + option_element.append(existing_element); + + $.get(`${CFG_GLPI.root_doc}/ajax/actorinformation.php`, { + [fk]: items_id, + only_number: true, + }).done((number) => { + const badge = number.length > 0 ? `${number}` : ''; + existing_element.html(badge); + }); + } + + return option_element; + }; + + $element.select2({ + tags: true, + width: ($element.attr('data-can-assign-me') !== undefined) ? 'calc(100% - 30px)' : '100%', + tokenSeparators: [',', ' '], + containerCssClass: 'actor-field', + templateSelection: (option) => genericTemplate(option, true), + templateResult: (option) => genericTemplate(option, false), + disabled: !can_update, + createTag: (params) => { + const term = $.trim(params.term); + + if (term === '') { + return null; + } + + // Don't offset to create a tag if it's not an email + if (!new RegExp(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,63}$/).test(term)) { + // Return null to disable tag creation + return null; + } + + return { + id: term, + text: term, + itemtype: "User", + items_id: 0, + use_notification: 1, + alternative_email: term, + }; + }, + ajax: { + url: `${CFG_GLPI.root_doc}/ajax/actors.php`, + datatype: 'json', + type: 'POST', + delay: 250, + data: (params) => { + return { + action: 'getActors', + actortype: actor_type, + users_right: actor_type === 'assign' ? 'own_ticket' : 'all', + entity_restrict: (window.actors.requester.length === 0 && is_new_item) ? -1 : entities_id, + searchText: params.term, + _idor_token: idor_token, + itiltemplate_class: itiltemplate_type, + itiltemplates_id: itiltemplate_id, + itemtype: itemtype, + items_id: is_new_item ? -1 : item_id, + item: this.safe_item_fields, + returned_itemtypes: returned_itemtypes, + page: params.page || 1 + }; + } + } + }); + + const updateActors = () => { + const data = $element.select2('data'); + const new_actors = []; + data.forEach((selection) => { + const element = $(selection.element); + + let itemtype = selection.itemtype ?? element.data('itemtype'); + const items_id = selection.items_id ?? element.data('items-id'); + let use_notif = selection.use_notification ?? element.data('use-notification') ?? false; + const def_email = selection.default_email ?? element.data('default-email') ?? ''; + let alt_email = selection.alternative_email ?? element.data('alternative-email') ?? ''; + + if (itemtype === "Email") { + itemtype = "User"; + use_notif = true; + alt_email = selection.id; + } + + new_actors.push({ + itemtype: itemtype, + items_id: items_id, + use_notification: use_notif, + default_email: def_email, + alternative_email: alt_email, + }); + }); + + window.actors[actor_type] = new_actors; + + window.saveActorsToDom(); + }; + + const auto_submit = () => { + if (allow_auto_submit && is_new_item && actor_type === 'requester') { + const form = $element.closest('form'); + if (form.length === 1) { + form.submit(); + } + } + }; + + $element.on('select2:select select2:unselect', () => { + updateActors(); + auto_submit(); + }); + + // intercept event for edit notification button + document.addEventListener('click', event => { + const target = $(event.target); + if (target.closest(`#${$element.id} + .select2 .edit-notify-user`)) { + return window.openNotifyModal(event); + } + // if a click on assign info is detected prevent opening of select2 + if (target.closest(`#${$element.id} + .select2 .assign_infos`)) { + event.stopPropagation(); + } + }, {capture: true}); + document.addEventListener('keydown', event => { + if (event.target.closest(`#${$element.id} + .select2 .edit-notify-user`) + && event.key == "Enter") { + return window.openNotifyModal(event); + } + }, {capture: true}); + }); + + this.element.on('mouseenter', '.actor_entry', (e) => { + // Delay fetching user info card until actually needed + const target = $(e.target).closest('.actor_entry'); + this.addActorInfoPopover(target, target.attr('data-itemtype'), target.attr('data-items-id'), true); + }); + } + + getActorInfoCard(itemtype, items_id) { + if (this.info_card_cache.has(`${itemtype}_${items_id}`)) { + return Promise.resolve(this.info_card_cache.get(`${itemtype}_${items_id}`)); + } else { + return $.ajax({ + url: `${CFG_GLPI.root_doc}/ajax/comments.php`, + type: 'POST', + data: { + 'itemtype': itemtype, + 'value': items_id, + } + }).then((data) => { + this.info_card_cache.set(`${itemtype}_${items_id}`, data); + return data; + }); + } + } + + /** + * + * @param {jQuery} element + * @param {string} itemtype + * @param {number} items_id + * @param {boolean} show_immediately + */ + addActorInfoPopover(element, itemtype, items_id, show_immediately = false) { + if (window.bootstrap.Popover.getInstance(element)) { + // already initialized + return; + } + this.getActorInfoCard(itemtype, items_id).then((data) => { + element.popover({ + container: element.parent(), + html: true, + sanitize: false, + trigger: 'hover', + delay: { hide: 300 }, + content: data + }).on('show.bs.popover', () => { + // hide other popovers + $('.popover').popover('hide'); + }); + if (show_immediately) { + element.popover('show'); + } + }); + } +} diff --git a/templates/components/itilobject/actors/field.html.twig b/templates/components/itilobject/actors/field.html.twig index b8e776d9017..6c0c293f3f1 100644 --- a/templates/components/itilobject/actors/field.html.twig +++ b/templates/components/itilobject/actors/field.html.twig @@ -53,7 +53,11 @@ {% if not is_actor_hidden %}