diff --git a/README.md b/README.md index ab06dd8..b047839 100644 --- a/README.md +++ b/README.md @@ -387,9 +387,18 @@ Embed a list of open maintenance tasks anywhere on a wiki page: {{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 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. ### 0.10) CalDAV sync diff --git a/js/event-popup.js b/js/event-popup.js index 194258c..6dfcfbe 100644 --- a/js/event-popup.js +++ b/js/event-popup.js @@ -150,6 +150,22 @@ // ============================================================ 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) { var action = button.getAttribute('data-action'); if (!action) return; @@ -171,15 +187,12 @@ // Find AJAX URL and security token from parent container or global var container = item.closest('[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) { // Fallback: use DokuWiki's standard AJAX endpoint ajaxUrl = (window.DOKU_BASE || '/') + 'lib/exe/ajax.php'; } - if (!sectok && window.JSINFO && window.JSINFO.sectok) { - sectok = window.JSINFO.sectok; - } button.disabled = true; button.textContent = '...'; diff --git a/src/CalendarService.php b/src/CalendarService.php index 7482105..d650e33 100644 --- a/src/CalendarService.php +++ b/src/CalendarService.php @@ -116,29 +116,33 @@ class CalendarService * * @param CalendarSlot $maintenanceSlot * @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 */ - 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 (!ChronoID::isIsoDate($todayIso)) return []; + $pastDays = max(0, $pastDays); + $file = $maintenanceSlot->getFile(); 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])) { return self::$taskCache[$cacheKey]; } $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 = []; foreach ($tasks as $task) { if ($task->isCompleted()) continue; - // dateIso is the date the task falls on if ($task->dateIso > $todayIso) continue; + if ($task->dateIso < $oldestOpenDateIso) continue; $open[] = $task; } @@ -162,6 +166,26 @@ class CalendarService 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. * diff --git a/syntax/maintenance.php b/syntax/maintenance.php index e7614ee..60cbc64 100644 --- a/syntax/maintenance.php +++ b/syntax/maintenance.php @@ -3,7 +3,6 @@ use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\plugin\luxtools\CalendarService; use dokuwiki\plugin\luxtools\CalendarSlot; -use dokuwiki\plugin\luxtools\ChronoID; require_once(__DIR__ . '/../autoload.php'); @@ -14,9 +13,12 @@ require_once(__DIR__ . '/../autoload.php'); * * Syntax: * {{maintenance_tasks>}} + * {{maintenance_tasks>&past=14}} */ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin { + private const DEFAULT_PAST_DAYS = 30; + /** @inheritdoc */ public function getType() { @@ -39,7 +41,7 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin public function connectTo($mode) { $this->Lexer->addSpecialPattern( - '\{\{maintenance_tasks>\}\}', + '\{\{maintenance_tasks>.*?\}\}', $mode, 'plugin_luxtools_maintenance' ); @@ -48,7 +50,13 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin /** @inheritdoc */ 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 */ @@ -72,12 +80,17 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin } $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'); if ($title === '') $title = 'Tasks'; - $renderer->doc .= '
'; + $renderer->doc .= '
'; $renderer->doc .= '

' . hsc($title) . '

'; if ($tasks === []) { @@ -126,4 +139,56 @@ class syntax_plugin_luxtools_maintenance extends SyntaxPlugin return true; } + + /** + * @param string $rawFlags + * @return array + */ + 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); + } }