[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] CalendarController: Prevent one-day backward shift for date-only tracker...
"Bruno Kambere \(@kambereBr\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <69eb835c7e49e_3818ede0351d0@gitlab-sidekiq-low-urgency-cpu-bound-v2-998bf4d77-ttjbf.mail> |
Bruno Kambere pushed to branch master at Tiki Wiki CMS Groupware / Tiki
Commits:
1beea816 by Bruno Kambere at 2026-04-24T17:42:49+03:00
[FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones
---
* [FIX] JsCalendar: Improve date normalization logic for form submissions
* [REF] Simplify date-only calendar field handling across picker and field handlers
* [ENH] CalendarController: Support TrackerList calendar with start-only/end-only date fields
* [REF] JsCalendar: Refactor normalizeDateOnlyTimestamp method
* [FIX] JsCalendar: Normalize date-only timestamps and improve date handling in date picker
* [ENH] CalendarController: Enhance date-only event handling for tracker fields
* [FIX] CalendarController: Prevent one-day backward shift for date-only tracker fields in non-UTC timezones
See merge request tikiwiki/tiki!9742
- - - - -
3 changed files:
- lib/core/Services/Tracker/CalendarController.php
- lib/core/Tracker/Field/DateTime.php
- lib/core/Tracker/Field/JsCalendar.php
Changes:
=====================================
lib/core/Services/Tracker/CalendarController.php
=====================================
@@ -24,17 +24,29 @@ class Services_Tracker_CalendarController
$unifiedsearchlib = TikiLib::lib('unifiedsearch');
$index = $unifiedsearchlib->getIndex();
- $start = 'tracker_field_' . $input->beginField->word();
- $end = 'tracker_field_' . $input->endField->word();
- $title = 'tracker_field_' . $input->title->word();
- $description = 'tracker_field_' . $input->description->word();
+ $beginFieldName = $input->beginField->word();
+ $endFieldName = $input->endField->word();
+ $start = 'tracker_field_' . $beginFieldName;
+ $end = 'tracker_field_' . $endFieldName;
+ $hasStartField = ! empty($beginFieldName) && $beginFieldName !== 'null';
+ $hasEndField = ! empty($endFieldName) && $endFieldName !== 'null';
+ $titleFieldName = $input->title->word();
+ $title = ($titleFieldName && $titleFieldName !== 'null') ? 'tracker_field_' . $titleFieldName : null;
+ $descriptionFieldName = $input->description->word();
+ $description = ($descriptionFieldName && $descriptionFieldName !== 'null') ? 'tracker_field_' . $descriptionFieldName : null;
- if ($resource = $input->resourceField->word()) {
- $resource = 'tracker_field_' . $resource;
+ $resource = null;
+ if ($resourceFieldName = $input->resourceField->word()) {
+ if ($resourceFieldName !== 'null') {
+ $resource = 'tracker_field_' . $resourceFieldName;
+ }
}
- if ($coloring = $input->coloringField->word()) {
- $coloring = 'tracker_field_' . $coloring;
+ $coloring = null;
+ if ($coloringFieldName = $input->coloringField->word()) {
+ if ($coloringFieldName !== 'null') {
+ $coloring = 'tracker_field_' . $coloringFieldName;
+ }
}
$query = $unifiedsearchlib->buildQuery([]);
@@ -57,8 +69,12 @@ class Services_Tracker_CalendarController
}
}
- $query->filterRange(0, $to, $start);
- $query->filterRange($from, $to + 1000 * 365 * 86400, $end);
+ if ($hasStartField) {
+ $query->filterRange(0, $to, $start);
+ }
+ if ($hasEndField) {
+ $query->filterRange($from, $to + 1000 * 365 * 86400, $end);
+ }
$maxRecords = $input->maxRecords->int() ?: null;
$query->setRange(0, $maxRecords);
@@ -74,7 +90,25 @@ class Services_Tracker_CalendarController
$response = [];
$fields = [];
+ $beginDateFieldHandler = null;
+ $endDateFieldHandler = null;
if ($definition = Tracker_Definition::get($input->trackerId->int())) {
+ $factory = $definition->getFieldFactory();
+
+ if ($hasStartField) {
+ $beginFieldInfo = $definition->getField($beginFieldName);
+ if ($beginFieldInfo) {
+ $beginDateFieldHandler = $factory->getHandler($beginFieldInfo);
+ }
+ }
+
+ if ($hasEndField) {
+ $endFieldInfo = $definition->getField($endFieldName);
+ if ($endFieldInfo) {
+ $endDateFieldHandler = $factory->getHandler($endFieldInfo);
+ }
+ }
+
foreach ($definition->getPopupFields() as $fieldId) {
if ($field = $definition->getField($fieldId)) {
$fields[] = $field;
@@ -137,22 +171,53 @@ class Services_Tracker_CalendarController
$colormap = base64_decode($input->colormap->word());
- $dtStart = $this->getTimestamp($row[$start]);
- $dtEnd = $this->getTimestamp($row[$end]);
+ $startValue = ($hasStartField && isset($row[$start])) ? $row[$start] : null;
+ $endValue = ($hasEndField && isset($row[$end])) ? $row[$end] : null;
+
+ if ($startValue === null && $endValue === null) {
+ continue;
+ }
+
+ // If only one of start or end is provided, use that value for both to ensure the event appears on the calendar,
+ $dtStart = $this->getTimestamp($startValue ?? $endValue);
+ $dtEnd = $this->getTimestamp($endValue ?? $startValue);
+
+ // If end is before start, treat as a single instant event by using the start value for both
+ if ($dtEnd < $dtStart) {
+ $dtEnd = $dtStart;
+ }
+
+ $beginIsDateOnly = $beginDateFieldHandler instanceof Tracker_Field_DateTime
+ && $beginDateFieldHandler->isDateOnlyCalendarValue();
+ $endIsDateOnly = $endDateFieldHandler instanceof Tracker_Field_DateTime
+ && $endDateFieldHandler->isDateOnlyCalendarValue();
+
+ // Determine if event is date-only
+ // If only one field is specified, check that field; if both are specified, both must be date-only
+ if ($beginDateFieldHandler !== null && $endDateFieldHandler !== null) {
+ $isDateOnlyEvent = $beginIsDateOnly && $endIsDateOnly;
+ } elseif ($beginDateFieldHandler !== null) {
+ $isDateOnlyEvent = $beginIsDateOnly;
+ } elseif ($endDateFieldHandler !== null) {
+ $isDateOnlyEvent = $endIsDateOnly;
+ } else {
+ $isDateOnlyEvent = false;
+ }
$response[] = [
'id' => $row['object_id'],
'trackerId' => $row['tracker_id'] ?? null,
- 'title' => $row[$title] ?: $row['title'],
- 'extendedProps' => ['description' => $row[$description] ?: $row['description']],
+ 'title' => ($title && isset($row[$title])) ? $row[$title] : ($row['title'] ?? ''),
+ 'extendedProps' => ['description' => ($description && isset($row[$description])) ? $row[$description] : ($row['description'] ?? '')],
'url' => smarty_modifier_sefurl($row['object_id'], $row['object_type']),
- 'allDay' => false,
- 'start' => $useTimestamp ? $dtStart : TikiLib::date_format("c", $dtStart, $user, 5, false),
- 'end' => $useTimestamp ? $dtEnd : TikiLib::date_format("c", $dtEnd, $user, 5, false),
+ // For all-day events, return date-only strings so FullCalendar does not apply timezone conversions that can shift the visible day.
+ 'allDay' => $isDateOnlyEvent,
+ 'start' => $isDateOnlyEvent ? gmdate('Y-m-d', $dtStart) : ($useTimestamp ? $dtStart : TikiLib::date_format("c", $dtStart, $user, 5, false)),
+ 'end' => $isDateOnlyEvent ? gmdate('Y-m-d', $dtEnd) : ($useTimestamp ? $dtEnd : TikiLib::date_format("c", $dtEnd, $user, 5, false)),
'editable' => $item->canModify(),
- 'color' => $row[$coloring] ? ($row[$coloring] ?: $row['coloring']) : ($this->getColor($row[$coloring] ?? '', $colormap)),
+ 'color' => ($coloring && isset($row[$coloring])) ? ($row[$coloring] ?: $row['coloring'] ?? '') : ($this->getColor($row[$coloring] ?? '', $colormap)),
'textColor' => '#000',
- 'resourceId' => strtolower($row[$resource] ?? ''),
+ 'resourceId' => $resource && isset($row[$resource]) ? strtolower($row[$resource]) : '',
'resourceEditable' => true,
];
}
=====================================
lib/core/Tracker/Field/DateTime.php
=====================================
@@ -441,4 +441,9 @@ class Tracker_Field_DateTime extends \Tracker\Field\AbstractItemField implements
throw new Services_Exception(tr('Invalid UNIX timestamp "%0"', $value), 400);
}
}
+
+ public function isDateOnlyCalendarValue(): bool
+ {
+ return $this->getOption('datetime') === 'd';
+ }
}
=====================================
lib/core/Tracker/Field/JsCalendar.php
=====================================
@@ -4,6 +4,9 @@
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+use Tiki\Lib\TikiDate;
+
class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
{
/**
@@ -96,7 +99,8 @@ class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
$requestData[$ins_id] = $requestData[$ins_id]['date'];
}
- $value = (isset($requestData[$ins_id]))
+ $valueFromRequest = isset($requestData[$ins_id]);
+ $value = $valueFromRequest
? $requestData[$ins_id]
: $this->getValue();
@@ -104,6 +108,14 @@ class Tracker_Field_JsCalendar extends Tracker_Field_DateTime
try {
// prevent corrupted date values getting saved (e.g. from inline edit sometimes)
$this->validateTimestamp($value);
+
+ // Guard skips values already at UTC midnight. Only normalize when the value comes from a form submission, never from stored data.
+ if ($valueFromRequest && $this->getOption('datetime') === 'd' && is_numeric($value) && (int) $value % 86400 !== 0) {
+ // Normalize to midnight UTC, mirroring DateTime handler (see DateTime::getFieldData).
+ $timezone = $requestData[$ins_id . '_timezone'] ?? TikiLib::lib('tiki')->get_display_timezone();
+ $server_offset = TikiDate::tzServerOffset($timezone, (int) $value);
+ $value = (int) $value + $server_offset;
+ }
} catch (Services_Exception $e) {
$value = '';
Feedback::error(tr('Date Picker Field: %0', $e->getMessage()));
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1beea81635b4b9ef6e393c2f19aab9468057ff76
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1beea81635b4b9ef6e393c2f19aab9468057ff76
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs