diff --git a/advancedforms.xml b/advancedforms.xml index ae01cb6..e73bb75 100644 --- a/advancedforms.xml +++ b/advancedforms.xml @@ -25,6 +25,11 @@ Teclib' + + 1.4.1 + >=11.0.0 + https://github.com/pluginsGLPI/advancedforms/releases/download/1.4.1/glpi-advancedforms-1.4.1.tar.bz2 + 1.4.0 ~12.0.0 diff --git a/public/js/modules/ReservationQuestionWidget.js b/public/js/modules/ReservationQuestionWidget.js index d95859d..09c66ee 100644 --- a/public/js/modules/ReservationQuestionWidget.js +++ b/public/js/modules/ReservationQuestionWidget.js @@ -34,14 +34,13 @@ let fullcalendar_loader = null; /** - * GLPI core bundles FullCalendar inside its Vue planning and no longer exposes it globally: - * load the plugin's own copy (see public/lib/fullcalendar), once per page whatever the number of questions. - * FullCalendar v6 injects its own styles, no stylesheet to load. + * On GLPI 12+ core no longer exposes FullCalendar globally, so we load + * the plugin's own v6 copy. On GLPI 11 core has already loaded v5 via + * Html::requireJs, so we just resolve immediately. */ function loadFullCalendar() { fullcalendar_loader ??= new Promise((resolve, reject) => { const script = document.createElement('script'); - // Relative to this module, with the same cache-busting query string. script.src = new URL(`../../lib/fullcalendar/fullcalendar.js${new URL(import.meta.url).search}`, import.meta.url).href; script.onload = resolve; script.onerror = reject; @@ -146,12 +145,13 @@ export class ReservationQuestionWidget { return; } - loadFullCalendar().then(() => { - this.#ensureCalendar(); + const isV6 = this.#root.dataset.fullcalendarV6 === '1'; + (isV6 ? loadFullCalendar().then(() => true) : Promise.resolve(false)).then((useV6) => { + this.#ensureCalendar(useV6); this.#calendar.unselect(); this.#calendar.today(); this.#calendar.refetchEvents(); - }); + }).catch(() => this.#showStatus(__('Could not load availability calendar', 'advancedforms'), 'text-danger')); } #getCalendarContainer() { @@ -241,7 +241,7 @@ export class ReservationQuestionWidget { } /** Builds the calendar once; item changes afterwards just refetch its events (see #onItemSelected). */ - #ensureCalendar() { + #ensureCalendar(isV6) { if (this.#calendar) { return; } @@ -249,33 +249,67 @@ export class ReservationQuestionWidget { // Like core planning: UTC mode + server "now", so the grid shows server/session wall-clock // times (those of the reservations) whatever the browser timezone. const now = new Date(`${this.#root.dataset.now.replace(' ', 'T')}Z`); - this.#calendar = new FullCalendar.Calendar(this.#getCalendarContainer(), { - initialView: 'timeGridWeek', - headerToolbar: { left: 'prev,next today', center: 'title', right: 'timeGridWeek,timeGridDay' }, - // Like core BaseFullCalendar.vue: FullCalendar falls back from e.g. "fr-FR" to "fr", then to English. - locale: document.documentElement.lang, + + if (!isV6) { + this.#loadCalendarCss(); + } + + const commonOpts = { timeZone: 'UTC', now, nowIndicator: true, - // Planning hours are absent from CFG_GLPI for anonymous sessions. - slotMinTime: CFG_GLPI.planning_begin ?? '00:00:00', - slotMaxTime: CFG_GLPI.planning_end ?? '24:00:00', height: 450, selectable: true, selectMirror: true, - // Don't offer slots the server would reject: past ones or overlapping an existing reservation. selectOverlap: false, validRange: { start: now }, selectAllow: (info) => info.start >= now, - // Keep the highlight visible when focus leaves the calendar (e.g. another question); - // it is cleared explicitly on item change/select instead (see #onItemSelected/#onItemCleared). unselectAuto: false, select: (info) => this.#onSlotSelected(info), events: (info, successCallback, failureCallback) => this.#fetchEvents(info, successCallback, failureCallback), - }); + }; + + const opts = isV6 ? { + ...commonOpts, + initialView: 'timeGridWeek', + headerToolbar: { left: 'prev,next today', center: 'title', right: 'timeGridWeek,timeGridDay' }, + locale: document.documentElement.lang, + slotMinTime: CFG_GLPI.planning_begin ?? '00:00:00', + slotMaxTime: CFG_GLPI.planning_end ?? '24:00:00', + } : { + ...commonOpts, + plugins: ['timeGrid', 'interaction'], + defaultView: 'timeGridWeek', + header: { left: 'prev,next today', center: 'title', right: 'timeGridWeek,timeGridDay' }, + locale: (typeof FullCalendarLocales !== 'undefined' && Object.keys(FullCalendarLocales).length === 1) + ? Object.keys(FullCalendarLocales)[0] : undefined, + minTime: CFG_GLPI.planning_begin ?? '00:00:00', + maxTime: CFG_GLPI.planning_end ?? '24:00:00', + }; + + this.#calendar = new FullCalendar.Calendar(this.#getCalendarContainer(), opts); this.#calendar.render(); } + #loadCalendarCss() { + const href = this.#root.dataset.calendarCss; + if (!href) { + return; + } + + let link = document.querySelector(`link[rel="stylesheet"][href="${CSS.escape(href)}"]`); + if (!link) { + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + document.head.appendChild(link); + } + + if (!link.sheet) { + link.addEventListener('load', () => this.#calendar?.updateSize(), { once: true }); + } + } + /** FullCalendar event source: reuses the existing Reservations endpoint, scoped to the visible range. */ #fetchEvents(info, successCallback, failureCallback) { const reservationitems_id = this.#root.querySelector('[data-reservation-question-field="reservationitems_id"]')?.value ?? ''; diff --git a/setup.php b/setup.php index a33e426..8964eb2 100644 --- a/setup.php +++ b/setup.php @@ -36,11 +36,11 @@ use GlpiPlugin\Advancedforms\Service\InitManager; /** @phpstan-ignore theCodingMachineSafe.function (safe to assume this isn't already defined) */ -define('PLUGIN_ADVANCEDFORMS_VERSION', '1.4.0'); +define('PLUGIN_ADVANCEDFORMS_VERSION', '1.4.1'); // Minimal GLPI version, inclusive /** @phpstan-ignore theCodingMachineSafe.function (safe to assume this isn't already defined) */ -define("PLUGIN_ADVANCEDFORMS_MIN_GLPI_VERSION", "12.0.0"); +define("PLUGIN_ADVANCEDFORMS_MIN_GLPI_VERSION", "11.0.0"); // Maximum GLPI version, exclusive /** @phpstan-ignore theCodingMachineSafe.function (safe to assume this isn't already defined) */ @@ -64,6 +64,10 @@ function plugin_init_advancedforms(): void ImportMapGenerator::getInstance()->registerModulesPath('advancedforms', '/public/js/modules'); + \GlpiPlugin\Advancedforms\Model\TicketReservationRequest::$itemtype = 'Ticket'; + \GlpiPlugin\Advancedforms\Model\TicketReservationRequest::$items_id = 'tickets_id'; + \GlpiPlugin\Advancedforms\Model\TicketReservationRequest::$rightname = 'ticket'; + InitManager::getInstance()->init(); } diff --git a/src/Controller/TreeDropdownChildrenController.php b/src/Controller/TreeDropdownChildrenController.php index 4745ff1..7d3f845 100644 --- a/src/Controller/TreeDropdownChildrenController.php +++ b/src/Controller/TreeDropdownChildrenController.php @@ -76,7 +76,8 @@ public function __invoke(Request $request): Response $question_type = $question->getQuestionType(); $itemtype = $question_type->getDefaultValueItemtype($question) ?? ''; - $field_name = $question->getEndUserInputName() . '[items_ids]'; + $items_id_key = method_exists($question_type, 'getDefaultValuesItemIds') ? 'items_ids' : 'items_id'; + $field_name = $question->getEndUserInputName() . "[$items_id_key]"; $aria_label = $question_type->items_id_aria_label ?? __('Select a dropdown item'); $dropdown_restriction_params = $question_type->getDropdownRestrictionParams($question); diff --git a/src/Model/ConditionHandler/TreeCascadeItemAsTextConditionHandler.php b/src/Model/ConditionHandler/TreeCascadeItemAsTextConditionHandler.php index 71c9ccf..38aa7e2 100644 --- a/src/Model/ConditionHandler/TreeCascadeItemAsTextConditionHandler.php +++ b/src/Model/ConditionHandler/TreeCascadeItemAsTextConditionHandler.php @@ -84,13 +84,18 @@ public function applyValueOperator( mixed $a, ValueOperator $operator, mixed $b, - ?JsonFieldInterface $config, + ?JsonFieldInterface $config = null, ): bool { - if (!is_array($a) || !isset($a['items_ids'])) { + if (!is_array($a)) { return false; } - $items_id = is_array($a['items_ids']) ? reset($a['items_ids']) : $a['items_ids']; + $raw = $a['items_ids'] ?? $a['items_id'] ?? null; + if ($raw === null) { + return false; + } + + $items_id = is_array($raw) ? reset($raw) : $raw; $item = $this->itemtype::getById($items_id); if (!$item) { return false; diff --git a/src/Model/QuestionType/TreeCascadeDropdownQuestion.php b/src/Model/QuestionType/TreeCascadeDropdownQuestion.php index 731e38c..3803293 100644 --- a/src/Model/QuestionType/TreeCascadeDropdownQuestion.php +++ b/src/Model/QuestionType/TreeCascadeDropdownQuestion.php @@ -117,14 +117,17 @@ public function renderEndUserTemplate(Question $question): string return parent::renderEndUserTemplate($question); } - $default_items_id = $this->getDefaultValuesItemIds($question)[0] ?? 0; + $default_items_id = method_exists($this, 'getDefaultValuesItemIds') + ? ($this->getDefaultValuesItemIds($question)[0] ?? 0) + : $this->getDefaultValueItemId($question); $aria_label = $this->items_id_aria_label; $tree_table = $itemtype::getTable(); $foreign_key = $itemtype::getForeignKeyField(); $rand_tree = random_int(1000000, 9999999); - $final_items_id_name = $question->getEndUserInputName() . '[items_ids]'; + $items_id_key = method_exists($this, 'getDefaultValuesItemIds') ? 'items_ids' : 'items_id'; + $final_items_id_name = $question->getEndUserInputName() . "[$items_id_key]"; $level2_container = 'level2_container_' . $rand_tree; $dropdown_restriction_params = $this->getDropdownRestrictionParams($question); diff --git a/src/Model/TicketReservationRequest.php b/src/Model/TicketReservationRequest.php index 7de4c9a..45c397f 100644 --- a/src/Model/TicketReservationRequest.php +++ b/src/Model/TicketReservationRequest.php @@ -47,12 +47,6 @@ /** A ticket-driven request to reserve an equipment item for a timeframe. */ final class TicketReservationRequest extends CommonDBChild { - public static string $itemtype = 'Ticket'; - - public static string $items_id = 'tickets_id'; - - public static string $rightname = 'ticket'; - public const STATUS_WAITING = 1; public const STATUS_ACCEPTED = 2; diff --git a/templates/config_form.html.twig b/templates/config_form.html.twig index 4c4c265..55d0ce4 100644 --- a/templates/config_form.html.twig +++ b/templates/config_form.html.twig @@ -53,6 +53,7 @@ + {% if constant('GLPI_VERSION')|slice(0, 2) == '11' %}{% endif %}
+{% if show_calendar and constant('GLPI_VERSION')|slice(0, 2) == '11' %} + {% do call(['Html', 'requireJs'], ['fullcalendar']) %} +{% endif %}