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
5 changes: 5 additions & 0 deletions advancedforms.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
<author>Teclib'</author>
</authors>
<versions>
<version>
<num>1.4.1</num>
<compatibility>&gt;=11.0.0</compatibility>
<download_url>https://github.com/pluginsGLPI/advancedforms/releases/download/1.4.1/glpi-advancedforms-1.4.1.tar.bz2</download_url>
</version>
<version>
<num>1.4.0</num>
<compatibility>~12.0.0</compatibility>
Expand Down
74 changes: 54 additions & 20 deletions public/js/modules/ReservationQuestionWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -241,41 +241,75 @@ export class ReservationQuestionWidget {
}

/** Builds the calendar once; item changes afterwards just refetch its events (see #onItemSelected). */
#ensureCalendar() {
#ensureCalendar(isV6) {
if (this.#calendar) {
return;
}

// 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 ?? '';
Expand Down
8 changes: 6 additions & 2 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand All @@ -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();
}

Expand Down
3 changes: 2 additions & 1 deletion src/Controller/TreeDropdownChildrenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/Model/QuestionType/TreeCascadeDropdownQuestion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 0 additions & 6 deletions src/Model/TicketReservationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions templates/config_form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
</div>

<input type="hidden" name="config_context" value="advancedforms"/>
{% if constant('GLPI_VERSION')|slice(0, 2) == '11' %}<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token() }}"/>{% endif %}

<div class="mx-n2 d-flex">
<button
Expand Down
5 changes: 4 additions & 1 deletion templates/reservation_question.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
data-reservation-question-widget
data-input-name="{{ input_name }}"
data-allowed-itemtypes="{{ allowed_itemtypes|json_encode }}"
{% if show_calendar %}data-now="{{ now }}"{% endif %}
{% if show_calendar %}data-now="{{ now }}" data-fullcalendar-v6="{{ constant('GLPI_VERSION')|slice(0, 2) == '12' ? '1' : '0' }}"{% if constant('GLPI_VERSION')|slice(0, 2) == '11' %} data-calendar-css="{{ css_path('lib/fullcalendar.css') }}"{% endif %}{% endif %}
>
<select class="form-select" data-reservation-question-item-select>
<option value=""></option>
Expand Down Expand Up @@ -76,6 +76,9 @@

<input type="hidden" name="{{ input_name }}[reservationitems_id]" data-reservation-question-field="reservationitems_id">
</div>
{% if show_calendar and constant('GLPI_VERSION')|slice(0, 2) == '11' %}
{% do call(['Html', 'requireJs'], ['fullcalendar']) %}
{% endif %}
<script type="module">
import { ReservationQuestionWidget } from '/plugins/advancedforms/js/modules/ReservationQuestionWidget.js?v={{ constant('PLUGIN_ADVANCEDFORMS_VERSION') }}';
$(function() {
Expand Down