Calendar fixes

This commit is contained in:
2026-03-11 13:50:45 +01:00
parent cc5101fffd
commit 5434ce5f53
4 changed files with 124 additions and 13 deletions

View File

@@ -387,9 +387,18 @@ Embed a list of open maintenance tasks anywhere on a wiki page:
{{maintenance_tasks>}} {{maintenance_tasks>}}
``` ```
To limit how far back overdue tasks are shown, add the `past` setting:
```
{{maintenance_tasks>&past=14}}
```
This renders all non-completed maintenance tasks due today or earlier, sorted This renders all non-completed maintenance tasks due today or earlier, sorted
with overdue tasks first (then by date, time, and title). with overdue tasks first (then by date, time, and title).
The `past` value is an integer number of days. Overdue tasks older than that
window are hidden. The default is `30`.
Each task shows its date, optional time, summary, and a "Complete" button. Each task shows its date, optional time, summary, and a "Complete" button.
### 0.10) CalDAV sync ### 0.10) CalDAV sync

View File

@@ -150,6 +150,22 @@
// ============================================================ // ============================================================
var MaintenanceTasks = (function () { var MaintenanceTasks = (function () {
function getSecurityToken(container) {
var sectok = container ? container.getAttribute('data-luxtools-sectok') : '';
if (sectok) return String(sectok);
if (window.JSINFO && window.JSINFO.sectok) {
return String(window.JSINFO.sectok);
}
var input = document.querySelector('input[name="sectok"], input[name="securitytoken"]');
if (input && input.value) {
return String(input.value);
}
return '';
}
function handleAction(button) { function handleAction(button) {
var action = button.getAttribute('data-action'); var action = button.getAttribute('data-action');
if (!action) return; if (!action) return;
@@ -171,15 +187,12 @@
// Find AJAX URL and security token from parent container or global // Find AJAX URL and security token from parent container or global
var container = item.closest('[data-luxtools-ajax-url]'); var container = item.closest('[data-luxtools-ajax-url]');
var ajaxUrl = container ? container.getAttribute('data-luxtools-ajax-url') : ''; var ajaxUrl = container ? container.getAttribute('data-luxtools-ajax-url') : '';
var sectok = container ? container.getAttribute('data-luxtools-sectok') : ''; var sectok = getSecurityToken(container);
if (!ajaxUrl) { if (!ajaxUrl) {
// Fallback: use DokuWiki's standard AJAX endpoint // Fallback: use DokuWiki's standard AJAX endpoint
ajaxUrl = (window.DOKU_BASE || '/') + 'lib/exe/ajax.php'; ajaxUrl = (window.DOKU_BASE || '/') + 'lib/exe/ajax.php';
} }
if (!sectok && window.JSINFO && window.JSINFO.sectok) {
sectok = window.JSINFO.sectok;
}
button.disabled = true; button.disabled = true;
button.textContent = '...'; button.textContent = '...';

View File

@@ -116,29 +116,33 @@ class CalendarService
* *
* @param CalendarSlot $maintenanceSlot * @param CalendarSlot $maintenanceSlot
* @param string $todayIso YYYY-MM-DD * @param string $todayIso YYYY-MM-DD
* @param int $pastDays Maximum number of overdue days to include
* @return CalendarEvent[] Sorted: overdue first, then today, then by title * @return CalendarEvent[] Sorted: overdue first, then today, then by title
*/ */
public static function openMaintenanceTasks(CalendarSlot $maintenanceSlot, string $todayIso): array public static function openMaintenanceTasks(CalendarSlot $maintenanceSlot, string $todayIso, int $pastDays = 30): array
{ {
if (!$maintenanceSlot->isEnabled()) return []; if (!$maintenanceSlot->isEnabled()) return [];
if (!ChronoID::isIsoDate($todayIso)) return []; if (!ChronoID::isIsoDate($todayIso)) return [];
$pastDays = max(0, $pastDays);
$file = $maintenanceSlot->getFile(); $file = $maintenanceSlot->getFile();
if ($file === '' || !is_file($file) || !is_readable($file)) return []; if ($file === '' || !is_file($file) || !is_readable($file)) return [];
$cacheKey = $maintenanceSlot->getKey() . '|tasks|' . $todayIso; $cacheKey = $maintenanceSlot->getKey() . '|tasks|' . $todayIso . '|' . $pastDays;
if (isset(self::$taskCache[$cacheKey])) { if (isset(self::$taskCache[$cacheKey])) {
return self::$taskCache[$cacheKey]; return self::$taskCache[$cacheKey];
} }
$tasks = self::parseAllTasksFromFile($file, $maintenanceSlot->getKey(), $todayIso); $tasks = self::parseAllTasksFromFile($file, $maintenanceSlot->getKey(), $todayIso);
$oldestOpenDateIso = self::oldestOpenTaskDate($todayIso, $pastDays);
// Filter: only non-completed, due today or earlier // Filter: only non-completed, due today or earlier, and not older than the configured overdue window.
$open = []; $open = [];
foreach ($tasks as $task) { foreach ($tasks as $task) {
if ($task->isCompleted()) continue; if ($task->isCompleted()) continue;
// dateIso is the date the task falls on
if ($task->dateIso > $todayIso) continue; if ($task->dateIso > $todayIso) continue;
if ($task->dateIso < $oldestOpenDateIso) continue;
$open[] = $task; $open[] = $task;
} }
@@ -162,6 +166,26 @@ class CalendarService
return $open; return $open;
} }
/**
* @param string $todayIso
* @param int $pastDays
* @return string
*/
protected static function oldestOpenTaskDate(string $todayIso, int $pastDays): string
{
try {
$today = new DateTimeImmutable($todayIso . ' 00:00:00', new DateTimeZone('UTC'));
} catch (Throwable $e) {
return $todayIso;
}
if ($pastDays === 0) {
return $today->format('Y-m-d');
}
return $today->sub(new DateInterval('P' . $pastDays . 'D'))->format('Y-m-d');
}
/** /**
* Get slot-level day indicator data for a whole month. * Get slot-level day indicator data for a whole month.
* *

View File

@@ -3,7 +3,6 @@
use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\CalendarService; use dokuwiki\plugin\luxtools\CalendarService;
use dokuwiki\plugin\luxtools\CalendarSlot; use dokuwiki\plugin\luxtools\CalendarSlot;
use dokuwiki\plugin\luxtools\ChronoID;
require_once(__DIR__ . '/../autoload.php'); require_once(__DIR__ . '/../autoload.php');
@@ -14,9 +13,12 @@ require_once(__DIR__ . '/../autoload.php');
* *
* Syntax: * Syntax:
* {{maintenance_tasks>}} * {{maintenance_tasks>}}
* {{maintenance_tasks>&past=14}}
*/ */
class syntax_plugin_luxtools_maintenance extends SyntaxPlugin class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
{ {
private const DEFAULT_PAST_DAYS = 30;
/** @inheritdoc */ /** @inheritdoc */
public function getType() public function getType()
{ {
@@ -39,7 +41,7 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
public function connectTo($mode) public function connectTo($mode)
{ {
$this->Lexer->addSpecialPattern( $this->Lexer->addSpecialPattern(
'\{\{maintenance_tasks>\}\}', '\{\{maintenance_tasks>.*?\}\}',
$mode, $mode,
'plugin_luxtools_maintenance' 'plugin_luxtools_maintenance'
); );
@@ -48,7 +50,13 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
/** @inheritdoc */ /** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler) public function handle($match, $state, $pos, Doku_Handler $handler)
{ {
return ['ok' => true]; $match = substr($match, strlen('{{maintenance_tasks>'), -2);
$params = $this->parseFlags($match);
return [
'ok' => true,
'past' => $this->normalizePastDays($params['past'] ?? null),
];
} }
/** @inheritdoc */ /** @inheritdoc */
@@ -72,12 +80,17 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
} }
$todayIso = date('Y-m-d'); $todayIso = date('Y-m-d');
$tasks = CalendarService::openMaintenanceTasks($maintenanceSlot, $todayIso); $pastDays = $this->normalizePastDays($data['past'] ?? null);
$tasks = CalendarService::openMaintenanceTasks($maintenanceSlot, $todayIso, $pastDays);
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
$secToken = function_exists('getSecurityToken') ? (string)getSecurityToken() : '';
$title = (string)$this->getLang('chronological_maintenance_title'); $title = (string)$this->getLang('chronological_maintenance_title');
if ($title === '') $title = 'Tasks'; if ($title === '') $title = 'Tasks';
$renderer->doc .= '<div class="luxtools-plugin luxtools-maintenance-tasks">'; $renderer->doc .= '<div class="luxtools-plugin luxtools-maintenance-tasks"'
. ' data-luxtools-ajax-url="' . hsc($ajaxUrl) . '"'
. ' data-luxtools-sectok="' . hsc($secToken) . '">';
$renderer->doc .= '<h3>' . hsc($title) . '</h3>'; $renderer->doc .= '<h3>' . hsc($title) . '</h3>';
if ($tasks === []) { if ($tasks === []) {
@@ -126,4 +139,56 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin
return true; return true;
} }
/**
* @param string $rawFlags
* @return array<string,string>
*/
protected function parseFlags(string $rawFlags): array
{
$rawFlags = trim($rawFlags);
if ($rawFlags === '') {
return [];
}
if ($rawFlags[0] === '&') {
$rawFlags = substr($rawFlags, 1);
}
$params = [];
foreach (explode('&', $rawFlags) as $flag) {
if (trim($flag) === '') continue;
[$name, $value] = array_pad(explode('=', $flag, 2), 2, '');
$name = strtolower(trim($name));
$value = trim($value);
if ($name === '') continue;
$params[$name] = $value;
}
return $params;
}
/**
* @param mixed $value
* @return int
*/
protected function normalizePastDays($value): int
{
if ($value === null || $value === '') {
return self::DEFAULT_PAST_DAYS;
}
if (is_int($value)) {
return max(0, $value);
}
$value = trim((string)$value);
if ($value === '' || !preg_match('/^-?\d+$/', $value)) {
return self::DEFAULT_PAST_DAYS;
}
return max(0, (int)$value);
}
} }