Skip to content

Api: add REST API handler and JSON format for Tracker items#3

Open
amirdhs wants to merge 5 commits into
masterfrom
feature/REST-API
Open

Api: add REST API handler and JSON format for Tracker items#3
amirdhs wants to merge 5 commits into
masterfrom
feature/REST-API

Conversation

@amirdhs
Copy link
Copy Markdown

@amirdhs amirdhs commented May 28, 2026

No description provided.

Copilot AI review requested due to automatic review settings May 28, 2026 15:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a REST/JSON layer for the Tracker app analogous to other EGroupware apps' CalDAV handlers. The handler hooks into the GroupDAV endpoint and the JsTracker class converts tracker items to/from a JSON representation.

Changes:

  • New ApiHandler (CalDAV Handler subclass) implementing PROPFIND/GET/PUT/POST/PATCH/DELETE, sync-collection, filter mapping, ETag and exception handling for /tracker collections.
  • New JsTracker (CalDAV JsBase subclass) providing JsTicket() serialization and parseJsTicket()/parseStatus()/parseAssigned() deserialization including custom-status and custom-field support.
  • Auto-creates a "Default" tracker queue on first REST use if none exist, so search/list does not return empty.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
src/ApiHandler.php New REST handler for /tracker collection: routing, filters, sync-collection, ACL, ETag, exception responses.
src/JsTracker.php New JSON renderer/parser for tracker items (JsTicket/parseJsTicket), including status label mapping and assigned/account handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ApiHandler.php Outdated
Comment thread src/ApiHandler.php
Comment on lines +231 to +237
// sync-collection: deleted items have no properties
if ((string)$ticket['tr_status'] === \tracker_so::STATUS_DELETED)
{
yield ['path' => $path . urldecode($this->get_path($entry))];
if (++$yielded && isset($nresults) && $yielded >= $nresults) break 2;
continue;
}
Comment thread src/JsTracker.php
'cc' => $ticket['cc'] ?: null,
'group' => !empty($ticket['group']) ? self::account($ticket['group']) : null,
'egroupware.org:customfields' => self::customfields($ticket),
'etag' => ApiHandler::etag($ticket),
Comment thread src/JsTracker.php
Comment on lines +67 to +95
$data = array_filter([
self::AT_TYPE => self::TYPE_TICKET,
'id' => (int)$ticket['id'],
'summary' => $ticket['summary'],
'description' => $ticket['description'] ?: null,
'tracker' => (int)$ticket['tracker'] ?: null,
'status' => $status_label,
'priority' => (int)$ticket['priority'],
'completion' => (int)$ticket['completion'],
'startDate' => !empty($ticket['startdate']) ? self::UTCDateTime($ticket['startdate'], true) : null,
'dueDate' => !empty($ticket['duedate']) ? self::UTCDateTime($ticket['duedate'], true) : null,
'closed' => !empty($ticket['closed']) ? self::UTCDateTime($ticket['closed'], true) : null,
'private' => (bool)$ticket['private'],
'category' => self::categories($ticket['cat_id']),
'version' => $ticket['version'] ? self::categories($ticket['version']) : null,
'creator' => self::account($ticket['creator']),
'created' => self::UTCDateTime($ticket['created'], true),
'modified' => !empty($ticket['modified']) ? self::UTCDateTime($ticket['modified'], true) : null,
'modifier' => !empty($ticket['modifier']) ? self::account($ticket['modifier']) : null,
'assigned' => !empty($ticket['assigned']) ? self::assigned($ticket['assigned']) : null,
'cc' => $ticket['cc'] ?: null,
'group' => !empty($ticket['group']) ? self::account($ticket['group']) : null,
'egroupware.org:customfields' => self::customfields($ticket),
'etag' => ApiHandler::etag($ticket),
]);

// @type and private must always be present even when falsy
$data[self::AT_TYPE] = self::TYPE_TICKET;
$data['private'] = (bool)$ticket['private'];
Comment thread src/ApiHandler.php Outdated
Comment thread src/ApiHandler.php
Comment on lines +203 to +211
for (
$chunk = 0;
($tickets = $this->bo->search(
$criteria, false, $order, '', '', false, 'AND',
[$initial_offset + $chunk * self::CHUNK_SIZE, $nresults ?: self::CHUNK_SIZE],
$filter, false
));
++$chunk
)
Comment thread src/JsTracker.php
Comment on lines +148 to +230
foreach ($data as $name => $value)
{
switch ($name)
{
case 'summary':
$ticket['tr_summary'] = $value;
break;

case 'description':
$ticket['tr_description'] = $value;
break;

case 'tracker':
$ticket['tr_tracker'] = self::parseInt($value);
break;

case 'status':
$ticket['tr_status'] = self::parseStatus($value);
break;

case 'priority':
$ticket['tr_priority'] = self::parseInt($value);
break;

case 'completion':
$ticket['tr_completion'] = min(100, max(0, self::parseInt($value)));
break;

case 'startDate':
$ticket['tr_startdate'] = $value ? self::parseDateTime($value) : null;
break;

case 'dueDate':
$ticket['tr_duedate'] = $value ? self::parseDateTime($value) : null;
break;

case 'private':
$ticket['tr_private'] = $value ? 1 : 0;
break;

case 'category':
$ticket['cat_id'] = self::parseCategories($value, false);
break;

case 'version':
$ticket['tr_version'] = $value ? self::parseInt($value) : null;
break;

case 'creator':
$ticket['tr_creator'] = self::parseAccount($value);
break;

case 'assigned':
$ticket['tr_assigned'] = self::parseAssigned($value);
break;

case 'cc':
$ticket['tr_cc'] = $value;
break;

case 'group':
$ticket['tr_group'] = $value ? self::parseAccount($value) : null;
break;

case 'egroupware.org:customfields':
$ticket = array_merge($ticket, self::parseCustomfields($value));
break;

// read-only / auto-set fields — silently ignore
case self::AT_TYPE:
case 'id':
case 'etag':
case 'created':
case 'modified':
case 'modifier':
case 'closed':
break;

default:
error_log(__METHOD__ . "() unknown field $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored');
break;
}
}
Comment thread src/JsTracker.php
Comment on lines +135 to +148
$data = json_decode($json, true, 10, JSON_THROW_ON_ERROR);

// For PATCH: only parse what's in the request body.
// Do NOT re-serialize $old and merge — that converts raw IDs to display names
// and causes lookup failures. so_sql::save() will merge the partial update
// with the existing $this->bo->data that was already loaded by read().
if ($method !== 'PATCH' && empty($data['summary']))
{
throw new Api\CalDAV\JsParseException("Required field 'summary' missing");
}

$ticket = [];

foreach ($data as $name => $value)
@ralfbecker
Copy link
Copy Markdown
Member

ralfbecker commented Jun 1, 2026

Hi Amir,

as I said in our AI meeting:

  1. all of the Copilot requests are valid in my opinion and should be addressed
  2. your pull request completely missed the replies:
    a) GET requests should return an object with reply_id => reply-object
    b) there need to be an endpoint path for the replies: /tracker/{id}/replies/{reply_id} with:
    • POST to /tracker/{id}/replies/ to create a new reply
    • GET/PUT/DELETE/PATCH to /tracker/{id}/replies/{reply_id} to get, replace, delete or change a reply
      c) documentation on how to use the general links facility to add or modify attachments to tickets AND replies

Ralf

amirdhs and others added 4 commits June 2, 2026 21:17
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Align propfind paging stride with the fetch size so nresults values above CHUNK_SIZE do not overlap pages, and only emit deleted tickets as path-only entries during sync-collection reports.

Based on Copilot review.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants