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 %}