Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
name: "Generate CI matrix"
uses: "glpi-project/plugin-ci-workflows/.github/workflows/generate-ci-matrix.yml@v1"
with:
glpi-version: "11.0.x"
glpi-version: "12.0.x"
ci:
name: "GLPI ${{ matrix.glpi-version }} - php:${{ matrix.php-version }} - ${{ matrix.db-image }}"
needs: "generate-ci-matrix"
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [unreleased]

- GLPI 12 Compatibility

### Fixed

- Fixed an issue where a group could be added to a ticket even though it did not have the necessary permissions
Expand Down
1 change: 1 addition & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
require_once __DIR__ . '/../../src/Plugin.php';

$baseline_file = __DIR__ . '/../../PluginsRector.php';

if (!file_exists($baseline_file)) {
throw new RuntimeException(
sprintf(
Expand Down
4 changes: 2 additions & 2 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@

// Minimal GLPI version, inclusive
/** @phpstan-ignore theCodingMachineSafe.function (safe to assume this isn't already defined) */
define("PLUGIN_MOREOPTIONS_MIN_GLPI_VERSION", "11.0.0");
define("PLUGIN_MOREOPTIONS_MIN_GLPI_VERSION", "12.0.0");

// Maximum GLPI version, exclusive
/** @phpstan-ignore theCodingMachineSafe.function (safe to assume this isn't already defined) */
define("PLUGIN_MOREOPTIONS_MAX_GLPI_VERSION", "11.0.99");
define("PLUGIN_MOREOPTIONS_MAX_GLPI_VERSION", "12.0.99");

/**
* Init hooks of the plugin.
Expand Down
20 changes: 8 additions & 12 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@

class Config extends CommonDBTM
{
public $dohistory = true;
public bool $dohistory = true;

public static $rightname = 'config';
public static string $rightname = 'config';

public const CONFIG_PARENT = Entity::CONFIG_PARENT;

Expand All @@ -75,12 +75,12 @@ public function defineTabs($options = []): array

public static function canView(): bool
{
return (bool) Session::haveRight(self::$rightname, READ);
return Session::haveRight(self::$rightname, READ);
}

public function canEdit($ID): bool
{
return (bool) Session::haveRight(self::$rightname, UPDATE);
return Session::haveRight(self::$rightname, UPDATE);
}

public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string
Expand All @@ -89,7 +89,7 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string
return '';
}

if ($item->getType() === Entity::class) {
if ($item::class === Entity::class) {
return self::createTabEntry(__('More options', 'moreoptions'), 0);
}

Expand All @@ -103,10 +103,8 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $
return false;
}

if ($item->getType() === Entity::class) {
if ($item instanceof Entity) {
self::showForEntity($item);
}
if ($item::class === Entity::class) {
self::showForEntity($item);

return true;
}
Expand Down Expand Up @@ -515,9 +513,7 @@ public static function addConfig(CommonDBTM $item): void
*/
public static function getConfig(?int $entityId = null, bool $useInheritance = true): self
{
if ($entityId === null) {
$entityId = Session::getActiveEntity();
}
$entityId ??= Session::getActiveEntity();

$moconfig = new self();
$moconfig->getFromDBByCrit([
Expand Down
20 changes: 9 additions & 11 deletions src/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@

class Controller extends CommonDBTM
{
public $dohistory = true;
public bool $dohistory = true;

public static $rightname = 'config';
public static string $rightname = 'config';

private static bool $solution_check_done = false;

Expand Down Expand Up @@ -563,7 +563,7 @@ public static function showSolutionRequirementsWarning(array $params): void
);

TemplateRenderer::getInstance()->display('@moreoptions/timeline_solution_warning.html.twig', [
'marker_id' => 'moreoptions-solution-warning-' . $item->getType() . '-' . $item->getID(),
'marker_id' => 'moreoptions-solution-warning-' . $item::class . '-' . $item->getID(),
'header' => $header,
'missing_fields' => $missing,
]);
Expand Down Expand Up @@ -610,12 +610,11 @@ public static function checkTaskRequirements(CommonDBTM $item): CommonDBTM
/**
* Hooked on {@link \Glpi\Plugin\Hooks::POST_ITEM_FORM}. Renders, into the task creation/edit
* form, a script that marks the fields configured as mandatory in moreoptions (category,
* duration, technician, technician group) with the usual red "required" marker and blocks
* client-side submission of the form until they are filled.
* duration, technician, technician group) with the usual red "required" marker.
*
* The server-side block already performed by {@see self::checkTaskRequirements()} on actual
* submission (PRE_ITEM_ADD) is kept as-is; this only prevents the user from submitting an
* incomplete task in the first place.
* This is purely a visual hint: the actual block on submission is performed server-side by
* {@see self::checkTaskRequirements()} (PRE_ITEM_ADD), whose message is what gets shown to
* the user.
*
* @param array<string, mixed> $params
*/
Expand Down Expand Up @@ -655,9 +654,8 @@ public static function markMandatoryTaskFields(array $params): void

TemplateRenderer::getInstance()->display('@moreoptions/timeline_task_mandatory_fields.html.twig', [
// Unique per-call anchor: lets the injected script find its own <form> reliably.
'marker_id' => 'moreoptions-task-mandatory-' . bin2hex(random_bytes(6)),
'field_labels' => $labels,
'error_message' => __('To create this task, you must fill in the following fields:', 'moreoptions'),
'marker_id' => 'moreoptions-task-mandatory-' . bin2hex(random_bytes(6)),
'field_labels' => $labels,
]);
}

Expand Down
22 changes: 14 additions & 8 deletions templates/timeline_solution_warning.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,20 @@
// A tooltip (not a popover) so it keeps GLPI's dark bubble style: core deliberately
// styles popovers with a light background (see `.popover .popover-body` in
// _base.scss), while tooltips stay dark. Tooltips support HTML content too.
solutionToggle.setAttribute('data-bs-toggle', 'tooltip');
solutionToggle.setAttribute('data-bs-placement', 'right');
solutionToggle.setAttribute('data-bs-trigger', 'hover focus');
solutionToggle.setAttribute('data-bs-html', 'true');
solutionToggle.setAttribute('title', {{ tooltip_content|json_encode|raw }});

if (typeof initTooltips === 'function') {
initTooltips(footer);
//
// The toggle already carries `data-bs-toggle="collapse"` (it's still a working
// "Add a solution" action): it can't also be set to "tooltip", as Bootstrap only
// honors one `data-bs-toggle` value per element and would silently drop the
// collapse behavior. The tooltip is instantiated directly through the JS API
// instead of going through `initTooltips()`'s `[data-bs-toggle="tooltip"]` selector.
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
new bootstrap.Tooltip(solutionToggle, {
delay: {show: 50, hide: 50},
html: true,
placement: 'right',
trigger: 'hover focus',
title: {{ tooltip_content|json_encode|raw }},
});
}
})();
</script>
31 changes: 8 additions & 23 deletions templates/timeline_task_mandatory_fields.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,25 @@
{#
# Rendered through the core `Hooks::POST_ITEM_FORM` hook (see Controller::markMandatoryTaskFields),
# right before the task form's closing tag. Marks the configured fields as required (red asterisk
# on their label, matching GLPI's own convention) and blocks client-side submission until they
# are filled.
# on their label, matching GLPI's own convention).
#
# This is purely a visual hint: submission itself is left to go through normally so the existing
# server-side block ({@see Controller::checkTaskRequirements()}, on PRE_ITEM_ADD) can render its
# usual accessible, styled message. An earlier version of this script also intercepted the
# form's `submit` event to reject it client-side through a `window.alert()`: that stopped the
# AJAX round-trip entirely, so the styled server-side message never got a chance to show, and a
# native `alert()` popup isn't reachable through accessible/role-based locators anyway.
#}
<span id="{{ marker_id }}" style="display: none;" aria-hidden="true"></span>
<script>
(function() {
var fieldLabels = {{ field_labels|json_encode|raw }};
var errorMessage = {{ error_message|json_encode|raw }};
var marker = document.getElementById({{ marker_id|json_encode|raw }});
var form = marker ? marker.closest('form') : null;
if (!form) {
return;
}

function isEmpty(value) {
return value === null || value === undefined || value === '' || value === '0';
}

Object.keys(fieldLabels).forEach(function(name) {
var fieldContainer = form.querySelector('[data-testid="form-field-' + name + '"]');
var label = fieldContainer ? fieldContainer.querySelector('label') : null;
Expand All @@ -61,21 +62,5 @@
label.appendChild(mark);
}
});

form.addEventListener('submit', function(e) {
var missing = [];
Object.keys(fieldLabels).forEach(function(name) {
var field = form.querySelector('[name="' + name + '"]');
var value = field ? field.value : '';
if (isEmpty(value)) {
missing.push(fieldLabels[name]);
}
});
if (missing.length > 0) {
e.preventDefault();
e.stopPropagation();
window.alert(errorMessage + '\n- ' + missing.join('\n- '));
}
});
})();
</script>
12 changes: 4 additions & 8 deletions tests/MoreOptionsTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ protected function updateTestConfig(Config $config, array $updates): bool
protected function getCurrentConfig(): Config
{
$config = Config::getConfig();
if (empty($config->fields) || $config->isNewItem()) {
$config = $this->createTestConfig();
if ($config->fields === [] || $config->isNewItem()) {
return $this->createTestConfig();
}

return $config;
Expand Down Expand Up @@ -171,12 +171,8 @@ protected function clearLogEntriesContaining(string $pattern): void
*/
protected function initEntitySession(): void
{
if (!isset($_SESSION['glpiactiveentities_string'])) {
$_SESSION['glpiactiveentities_string'] = '';
}
$_SESSION['glpiactiveentities_string'] ??= '';

if (!isset($_SESSION['glpiactiveentities'])) {
$_SESSION['glpiactiveentities'] = [];
}
$_SESSION['glpiactiveentities'] ??= [];
}
}
75 changes: 75 additions & 0 deletions tests/e2e/fixtures/moreoptions_fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* -------------------------------------------------------------------------
* MoreOptions plugin for GLPI
* -------------------------------------------------------------------------
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2025 by the MoreOptions plugin team.
* @license MIT https://opensource.org/licenses/mit-license.php
* @link https://github.com/pluginsGLPI/moreoptions
* -------------------------------------------------------------------------
*/

import { randomUUID } from 'crypto';
import { Page } from '@playwright/test';
// eslint-disable-next-line playwright/no-raw-locators
import { Api } from '../../../../../tests/e2e/utils/Api';
// eslint-disable-next-line playwright/no-raw-locators
import { EntitySwitcher } from '../../../../../tests/e2e/utils/EntitySwitcher';
// eslint-disable-next-line playwright/no-raw-locators
import { getWorkerEntityId } from '../../../../../tests/e2e/utils/WorkerEntities';
import { MoreOptionsConfigPage } from '../pages/MoreOptionsConfigPage';

// Re-export the core GLPI fixture so plugin specs share the same authenticated
// `test`/`expect` and the worker/profile/api helpers.
export * from '../../../../../tests/e2e/fixtures/glpi_fixture';

/**
* Create a sub-entity of the worker's entity, switch to it and apply the
* given moreoptions settings to it.
*
* Each test gets its own entity: the settings it turns on block closures or
* task creations for everything in scope, which would otherwise interfere
* with unrelated specs sharing the worker's entity.
*/
export async function createConfiguredEntity(
page: Page,
api: Api,
entity: EntitySwitcher,
config: Record<string, number>,
): Promise<number> {
const entities_id = await api.createItem('Entity', {
'name': `MoreOptions ${Object.keys(config).join(' ')} ${randomUUID()}`,
'entities_id': getWorkerEntityId(),
});
// A freshly created entity is not immediately usable otherwise: its
// recursive rights are only picked up on the next explicit entity switch.
await entity.switchToWithRecursion(entities_id);
// The `api` fixture caches one authenticated session per worker for its
// whole lifetime: without a refresh, it never sees an entity created
// after that session's first request.
api.refreshSession();

await new MoreOptionsConfigPage(page).doSetConfig(entities_id, config);

return entities_id;
}
Loading
Loading