Skip to content
Open
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: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed the issue where a ticket could be solved without a solution

## [1.0.0-rc1]

4 changes: 3 additions & 1 deletion public/css/moreoptions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@
display: none;
}

.form-field:has(.mo-seg-inherit:checked) .mo-inherited {
// Same for the status dropdowns, whose "Inherit" option is Config::CONFIG_PARENT (-2).
.form-field:has(.mo-seg-inherit:checked) .mo-inherited,
.form-field:has(.mo-status option[value="-2"]:checked) .mo-inherited {
display: inline-flex;
align-items: center;
}
Expand Down
100 changes: 94 additions & 6 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@
namespace GlpiPlugin\Moreoptions;

use DBmysql;
use Change;
use CommonDBTM;
use CommonGLPI;
use CommonITILObject;
use Entity;
use Glpi\Application\View\TemplateRenderer;
use Migration;
use Plugin;
use Problem;
use Session;
use Ticket;

class Config extends CommonDBTM
{
Expand Down Expand Up @@ -141,7 +145,36 @@ private static function getActorGroupConfigFields(): array
*/
private static function getAllConfigFields(): array
{
return array_merge(self::getItilConfigFields(), self::getActorGroupConfigFields());
return array_merge(
self::getItilConfigFields(),
self::getActorGroupConfigFields(),
array_keys(self::getStatusConfigFields()),
);
}

/**
* Fields of kind `status`, with the ITIL itemtype whose statuses they hold.
*
* @return array<string, class-string<CommonITILObject>>
*/
private static function getStatusConfigFields(): array
{
return [
'escalade_status_after_escalation_ticket' => Ticket::class,
'escalade_status_after_escalation_change' => Change::class,
'escalade_status_after_escalation_problem' => Problem::class,
];
}

/**
* Choices of a `status` field: "No change" (0), then every status of the itemtype.
*
* @param class-string<CommonITILObject> $itemtype
* @return array<int, string>
*/
public static function getSelectableStatus(string $itemtype): array
{
return [0 => __('No change', 'moreoptions')] + $itemtype::getAllStatusArray();
}

/**
Expand Down Expand Up @@ -290,6 +323,14 @@ public static function showForEntity(Entity $item): void
'entities_id' => $item->getID(),
]);

$status_options = [];
foreach (self::getStatusConfigFields() as $field => $itemtype) {
$status_options[$field] = self::getSelectableStatus($itemtype);
if ($item->getID() > 0) {
$status_options[$field] = [self::CONFIG_PARENT => __('Inherit', 'moreoptions')] + $status_options[$field];
}
}

$tabs = self::getScreenTabs();
$sections_by_tab = [];
foreach ($tabs as $tab) {
Expand All @@ -305,6 +346,7 @@ public static function showForEntity(Entity $item): void
'parent_entity_id' => $item->getID() > 0 ? (int) $item->fields['entities_id'] : null,
'parent_badges' => self::getParentValueBadges($item),
'dropdown_options' => self::getSelectableActorGroup(),
'status_options' => $status_options,
'config_parent' => self::CONFIG_PARENT,
'escalade_takes_technician_group' => self::isTechnicianGroupHandledByEscalade(),
'params' => [
Expand All @@ -327,11 +369,11 @@ public static function getIcon(): string
public static function getScreenTabs(): array
{
return [
['id' => 'ticket', 'label' => __('Ticket'), 'icon' => 'ti-ticket'],
['id' => 'change', 'label' => __('Change'), 'icon' => 'ti-git-branch'],
['id' => 'problem', 'label' => __('Problem'), 'icon' => 'ti-alert-circle'],
['id' => 'ticket', 'label' => __('Ticket'), 'icon' => Ticket::getIcon()],
['id' => 'change', 'label' => __('Change'), 'icon' => Change::getIcon()],
['id' => 'problem', 'label' => __('Problem'), 'icon' => Problem::getIcon()],
['id' => 'task', 'label' => _n('Task', 'Tasks', 2), 'icon' => 'ti-checklist'],
['id' => 'escalate', 'label' => __('Escalate', 'moreoptions'), 'icon' => 'ti-arrow-up'],
['id' => 'escalate', 'label' => __('Escalate', 'moreoptions'), 'icon' => Escalation::getIcon(),],
];
}

Expand Down Expand Up @@ -401,9 +443,21 @@ private static function getScreenSections(): array
'escalate' => [
[
'title' => __('Escalate', 'moreoptions'),
'icon' => 'ti-arrow-up',
'icon' => Escalation::getIcon(),
'rows' => [
['key' => 'escalate_is_active', 'kind' => 'yes_no', 'label' => __('Activate escalation', 'moreoptions')],
['key' => 'escalate_remove_technician', 'kind' => 'yes_no', 'label' => __('Remove technician after escalation', 'moreoptions')],
['key' => 'escalade_status_after_escalation_ticket', 'kind' => 'status', 'label' => __('Ticket status after escalation', 'moreoptions')],
['key' => 'escalade_status_after_escalation_change', 'kind' => 'status', 'label' => __('Change status after escalation', 'moreoptions')],
['key' => 'escalade_status_after_escalation_problem', 'kind' => 'status', 'label' => __('Problem status after escalation', 'moreoptions')],
],
],
[
'title' => __('Default options values', 'moreoptions'),
'icon' => 'ti-settings',
'rows' => [
['key' => 'escalade_assign_me_as_obsever_by_default', 'kind' => 'yes_no', 'label' => __('Assign me as observer after escalation', 'moreoptions')],
['key' => 'escalade_is_private_by_default', 'kind' => 'yes_no', 'label' => __('Escalate event is private', 'moreoptions')],
],
],
],
Expand Down Expand Up @@ -477,6 +531,11 @@ private static function getParentValueBadges(Entity $item): array
$badges[$field] = Entity::inheritedValue(htmlescape($text), false, false);
}

foreach (self::getStatusConfigFields() as $field => $itemtype) {
$text = self::getSelectableStatus($itemtype)[(int) ($parent_config->fields[$field] ?? 0)] ?? __('No change', 'moreoptions');
$badges[$field] = Entity::inheritedValue(htmlescape($text), false, false);
}

return $badges;
}

Expand Down Expand Up @@ -577,6 +636,12 @@ public static function install(Migration $migration): void
`assign_technician_from_task_change` tinyint NOT NULL DEFAULT '0',
`assign_technician_from_task_problem` tinyint NOT NULL DEFAULT '0',
`escalate_is_active` tinyint NOT NULL DEFAULT '0',
`escalate_remove_technician` tinyint NOT NULL DEFAULT '0',
`escalade_status_after_escalation_ticket` tinyint NOT NULL DEFAULT '0',
`escalade_status_after_escalation_change` tinyint NOT NULL DEFAULT '0',
`escalade_status_after_escalation_problem` tinyint NOT NULL DEFAULT '0',
`escalade_assign_me_as_obsever_by_default` tinyint NOT NULL DEFAULT '0',
`escalade_is_private_by_default` tinyint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `entities_id` (`entities_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
Expand All @@ -596,15 +661,38 @@ public static function install(Migration $migration): void
'assign_technician_from_task_change',
'assign_technician_from_task_problem',
'escalate_is_active',
'escalate_remove_technician',
'escalade_status_after_escalation_ticket',
'escalade_status_after_escalation_change',
'escalade_status_after_escalation_problem',
'escalade_assign_me_as_obsever_by_default',
'escalade_is_private_by_default',
] as $field
) {
if (!$DB->fieldExists($table, $field)) {
$migration->addField($table, $field, 'bool', ['value' => '0']);
}
}

// Status fields added to an existing table: child entities inherit by default.
$new_status_fields = [];
foreach (array_keys(self::getStatusConfigFields()) as $field) {
if (!$DB->fieldExists($table, $field)) {
$migration->addField($table, $field, 'bool', ['value' => '0']);
$new_status_fields[] = $field;
}
}

$migration->executeMigration();

if ($new_status_fields !== []) {
$DB->update(
$table,
array_fill_keys($new_status_fields, self::CONFIG_PARENT),
['entities_id' => ['>', 0]],
);
}

$entities = new Entity();
foreach ($entities->find() as $entity) {
if (is_array($entity) && isset($entity['id'])) {
Expand Down
87 changes: 86 additions & 1 deletion src/Escalation.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static function getTypeName($nb = 0): string

public static function getIcon(): string
{
return 'ti ti-arrow-up';
return 'ti ti-escalator-up';
}

/**
Expand Down Expand Up @@ -275,6 +275,32 @@ public function post_addItem()
'type' => CommonITILActor::ASSIGN,
'_plugin_moreoptions_escalade' => true,
]);

if ((int) ($this->input['add_me_as_observer'] ?? 0) === 1) {
$this->addAuthorAsObserver($item);
}
}

/**
* Adds the author of the escalation as an observer of the escalated item, unless they already are.
*/
private function addAuthorAsObserver(CommonITILObject $item): void
{
$user_link = getItemForItemtype($item->userlinkclass);
if (!$user_link instanceof CommonITILActor) {
return;
}

$input = [
$item->getForeignKeyField() => (int) $this->fields['items_id'],
'users_id' => (int) $this->fields['users_id'],
'type' => CommonITILActor::OBSERVER,
];
if (countElementsInTable($user_link::getTable(), $input) > 0) {
return;
}

$user_link->add($input);
}

/**
Expand Down Expand Up @@ -342,9 +368,16 @@ public static function showEscalationForm(CommonITILObject $item): void
$groups_used[$key] = (int) $row['groups_id'];
}

$config = Config::getConfig((int) $item->fields['entities_id']);

TemplateRenderer::getInstance()->display('@moreoptions/escalation_form.html.twig', [
'item' => $item,
'groups_used' => $groups_used ?? [],
// Default values of the form options
'config' => [
'assign_to_observer' => (int) ($config->fields['escalade_assign_me_as_obsever_by_default'] ?? 0) === 1,
'is_private' => (int) ($config->fields['escalade_is_private_by_default'] ?? 0) === 1,
],
]);
}

Expand All @@ -364,6 +397,14 @@ public static function escalate(CommonITILActor $group_link): void
}

self::keepOnlyAssignedGroup($group_link);

$item = getItemForItemtype($group_link::$itemtype_1 ?? '');
if (!$item instanceof CommonITILObject || !$item->getFromDB((int) $group_link->fields[$group_link::$items_id_1])) {
return;
}

self::removeTechnician($item);
self::changeStatusAfterEscalation($item);
}

/**
Expand All @@ -383,6 +424,50 @@ private static function keepOnlyAssignedGroup(CommonITILActor $group_link): void
}
}

/**
* Drop the technicians assigned to the escalated item, when the "remove technician" option
* is enabled for its entity.
*/
private static function removeTechnician(CommonITILObject $item): void
{
$config = Config::getConfig((int) $item->fields['entities_id']);
if ((int) ($config->fields['escalate_remove_technician'] ?? 0) !== 1) {
return;
}

$user_link = getItemForItemtype($item->userlinkclass);
if (!$user_link instanceof CommonITILActor) {
return;
}

$technician_links = $user_link->find([
$item->getForeignKeyField() => $item->getID(),
'type' => CommonITILActor::ASSIGN,
]);
foreach ($technician_links as $technician_link) {
(new ($user_link::class)())->delete(['id' => $technician_link['id']]);
}
}

/**
* Set the status configured for its entity on the escalated item, unless it already has it.
*/
private static function changeStatusAfterEscalation(CommonITILObject $item): void
{
// Reload: removing the actors (see self::removeTechnician()) may have changed the status.
if (!$item->getFromDB($item->getID())) {
return;
}

$config = Config::getConfig((int) $item->fields['entities_id']);
$new_status = (int) ($config->fields['escalade_status_after_escalation_' . strtolower($item::class)] ?? 0);
if ($new_status === 0 || (int) $item->fields['status'] === $new_status) {
return;
}

$item->update(['id' => $item->getID(), 'status' => $new_status]);
}

public static function install(Migration $migration): void
{
/** @var DBmysql $DB */
Expand Down
20 changes: 18 additions & 2 deletions templates/config.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
aria-selected="{{ loop.first ? 'true' : 'false' }}"
>
<i class="ti {{ tab.icon }}"></i>
{{ tab.label }}
&nbsp;{{ tab.label }}
</a>
</li>
{% endfor %}
Expand Down Expand Up @@ -120,7 +120,23 @@
{% endif %}
</div>
<div class="col-6 d-flex align-items-center justify-content-end gap-2">
{{ mo.render(row.field, options, current, parent_badges[row.field]|default(null), disabled, canedit, rand) }}
{% if row.kind == 'status' %}
{% if parent_badges[row.field] is defined %}
<div class="mo-inherited">{{ parent_badges[row.field]|raw }}</div>
{% endif %}
<div class="mo-status">
{{ call('Dropdown::showFromArray', [row.field, status_options[row.field], {
'value': current,
'width': '220px',
'disabled': disabled or not canedit,
'templateResult': 'templateItilStatus',
'templateSelection': 'templateItilStatus',
'display': false,
}])|raw }}
</div>
{% else %}
{{ mo.render(row.field, options, current, parent_badges[row.field]|default(null), disabled, canedit, rand) }}
{% endif %}
</div>
</div>
<div class="hr my-1" style="opacity: 0.075;"></div>
Expand Down
2 changes: 1 addition & 1 deletion templates/escalation_button.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
button.type = 'button';
button.className = 'btn btn-sm btn-ghost-secondary moreoptions-escalate-btn ms-2 py-0 px-1';
button.title = {{ __('Escalate', 'moreoptions')|json_encode|raw }};
button.innerHTML = '<i class="ti ti-arrow-up me-1"></i>'
button.innerHTML = '<i class="ti ti-escalator-up me-1"></i>'
+ '<span>' + _.escape({{ __('Escalate', 'moreoptions')|json_encode|raw }}) + '</span>';
button.addEventListener('click', function() {
glpi_ajax_dialog({
Expand Down
Loading
Loading