-
Notifications
You must be signed in to change notification settings - Fork 17
Issue 88 Subtasks Could Not Be Completed
Ticking a subtask's checkbox did not tick it. On some installations it could not be completed at all, and on every installation the click opened the subtask instead of ticking it.
Reported in issue #88 by dschipfel.
Fixed in fe32e1d6, released as update #1199.
There were four separate faults stacked here, and each one hid the one beneath it.
Open a task, add a subtask, click its checkbox. The panel changes to show the subtask itself, and the box is still empty. Go back to the parent and the subtask is still outstanding. The progress on the parent card never moves.
No error appears anywhere, including in the console β which the report noted, and which turned out to be a fault in its own right rather than a missing detail.
The subtask row and its checkbox were wired like this:
<div class="subtask-item" onclick="openDetailPanel(${s.id})">
<input type="checkbox" β¦ onchange="event.stopPropagation(); toggleSubtask(${s.id})">The guard was on the wrong event.
Ticking a checkbox fires a click on the input. That click bubbles up to the row, whose job is to open things β so the panel navigated into the subtask. Calling stopPropagation() inside onchange cannot help: change is a different event, fired at a different moment, and the row was never listening for it.
So a click did two things at once β opened the subtask, and fired the toggle β and the navigation repainted the panel over the top of whatever the toggle did.
This one happens on every installation, whatever language it is in. It is the fault you actually see.
// after
<input type="checkbox" β¦
onclick="event.stopPropagation()"
onchange="toggleSubtask(${s.id})">Ticking a box now ticks the box and nothing else. Clicking the row still opens the subtask, exactly as before.
Underneath, the endpoint asked the database for a status by name:
// api/tasks/toggle_subtask.php β before
$newStatusName = $task['status_is_closed'] ? 'To Do' : 'Done';
$newStatusStmt = $conn->prepare("SELECT id, is_closed FROM task_statuses WHERE name = ? LIMIT 1");To Do and Done are display names. They are listed under Tasks β Settings β Statuses precisely so they can be renamed, and a German site would reasonably call them Zu erledigen and Erledigt.
Rename them and the lookup matches nothing:
{"success":false,"error":"Status 'Done' not configured"}Every time, in both directions. On such an installation a subtask could never be completed.
This is the same fault as issue #79, where seven ticket-creation paths asked for the status named Open and got nothing on an installation that had renamed it.
The fix asks the question the code actually means β which status counts as complete? β using the flag rather than the word:
$task['status_is_closed']
? "SELECT id, name, is_closed FROM task_statuses
WHERE is_closed = 0 AND is_active = 1
ORDER BY is_default DESC, display_order ASC LIMIT 1"
: "SELECT id, name, is_closed FROM task_statuses
WHERE is_closed = 1 AND is_active = 1
ORDER BY display_order ASC LIMIT 1"Reopening prefers whichever status is marked as the default β that is what a default is for. Completing takes the first closed status in display order, which puts Done ahead of Cancelled: completing a subtask must never cancel it.
The codebase already had the right pattern next door.
lookupDefault()inincludes/services/tasks.phptries the name and then falls back tois_default. The toggle endpoint had no fallback at all.
Even a genuinely completed subtask showed an empty box, because the rendering asked the same bad question:
// before
<input type="checkbox" ${s.status === 'Done' ? 'checked' : ''}>
<span class="subtask-title ${s.status === 'Done' ? 'completed' : ''}">Both the tick and the strikethrough compared the status name to the English word. The API had been returning status_is_closed alongside it the whole time; the render simply was not using it.
// after
<input type="checkbox" ${s.status_is_closed ? 'checked' : ''}>This is why the console was clean, and why fault two could sit there unnoticed:
// before
async function toggleSubtask(id) {
try {
await fetch(API_BASE + 'toggle_subtask.php', { β¦ }); // response discarded
if (selectedTaskId) openDetailPanel(selectedTaskId);
} catch (e) { console.error(e); }
}The response was never parsed, success was never checked, and error was never shown. The server refused in plain terms and nobody was listening. The only visible result was the panel repainting with an empty box.
A silent failure is worse than the failure it hides: even with faults one to three fixed, this one would have concealed whatever went wrong next. The response is now read and a refusal raises a toast.
The completion time was written in the wrong clock. completed_datetime was set with date('Y-m-d H:i:s') β the server's local time β into a column sitting directly beside updated_datetime = UTC_TIMESTAMP(). And api/tasks/reorder.php already wrote that same column in UTC. So the stored value depended on which route happened to close the subtask. Now UTC either way.
Three error messages were dressed as successes. showToast(β¦, 'success') on failure branches in Tasks β a green tick announcing that something had gone wrong.
| File | What changed |
|---|---|
assets/js/tasks.js |
Propagation guard moved to onclick; tick and strikethrough read status_is_closed; the toggle response is read and failures surfaced; three toasts restyled. |
api/tasks/toggle_subtask.php |
Status chosen by the is_closed flag, not by name; completed_datetime in UTC. |
lang/*/tasks.php |
Two new strings in all 24 languages. |
api/tasks/list.php |
Parent progress was always counted with SUM(CASE WHEN ts.is_closed = 1 β¦) β flag-driven, so the count was right all along. Nothing ever reached it. |
api/tasks/reorder.php |
Also looks a status up by name, but the name comes from the client, which sends back a real status it was given. Not the same fault. |
Two strings in the Tasks panel were still hardcoded English β a tag chip's Remove tooltip, and the label on a linked change (Change #12). Both now go through t(), translated into all 23 non-English languages.
Worth recording what was not wrong: the German pack for Tasks was already complete. If a German installation shows English in this area, it is the status names β To Do, In Progress, Done β which are rows in the database seeded in English, not translated strings. Renaming them under Tasks β Settings β Statuses is the intended route, and after this fix doing so no longer breaks anything.
The bug was reproduced first, on a dev installation with the statuses renamed to German exactly as a German site would: {"success":false,"error":"Status 'Done' not configured"} in both directions. After the fix, the same calls return {"success":true,"new_status":"Erledigt"} and "Zu erledigen".
Then the real page was driven in a browser, clicking the actual checkbox:
| ticking stays on the parent task | PASS |
| the subtask becomes complete | PASS |
| the strikethrough follows | PASS |
| unticking works | PASS |
| clicking the row still opens the subtask | PASS β negative control |
The obvious way to check "did it navigate?" is to read selectedTaskId from the harness. That silently false-passes: it is a top-level let, which is not a window property, so it reads as undefined before and after β and undefined === undefined passes however badly the page behaved.
The test asserts on the DOM instead. The subtask section only renders for a parent task, so if the panel navigated into the subtask, #subtaskList disappears entirely. That cannot pass by accident.
Worth recording. The explanation of fault one was first written as an HTML comment inside the JavaScript template literal, using backticks around `click` and `change`. Backticks end a template literal. tasks.js stopped parsing, and nothing on the page was defined.
The page still rendered β the HTML is server-side β so it looked completely normal. Only loading it in a browser and asking whether the functions existed found it:
openDetailPanel: undefined
toggleSubtask: undefined
loadTasks: undefined
Rendered markup is not proof that the JavaScript parsed.
Ticking a subtask ticks it. Clicking the row still opens it. The parent's progress moves.
And if you have renamed your task statuses β or would like to, into your own language β subtasks now work regardless of what you call them.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)