tasks.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <pstm-uri>/tasks</pstm-uri>
  5. <pstm-template>main</pstm-template>
  6. <script>
  7. // The task queue. Plan 12: the filters (status / project / goal)
  8. // run CLIENT-SIDE - the Statum entrypoint fetch never carries the
  9. // page's own query string to the server, so the status chips and
  10. // the selects filter the rendered rows directly (window.taskFilters
  11. // + taskRowVisible) and survive re-renders; the create form gains
  12. // the mode select (with descriptions) and the auto-enqueue-after
  13. // dropdown (the global human-task list; goal tasks are the goal
  14. // agent's to sequence).
  15. //
  16. // Goal 41 T4: the status select became faceted chips with counts
  17. // (the goal-detail strip pattern) and the default view is OPEN
  18. // WORK - queued+assigned+working+blocked+suspended - instead of
  19. // "all", so months of done rows no longer bury the queue (ux-notes
  20. // tasks A+B). taskFilters.status holds the chip state:
  21. // null -> every status (the "All" chip)
  22. // 'open' -> the default open-work set
  23. // '<status>' -> one exact status
  24. if (window.statum) {
  25. (function () {
  26. 'use strict';
  27. window.TASK_OPEN_STATUSES = ['queued', 'assigned', 'working',
  28. 'blocked', 'suspended'];
  29. window.taskFilters = window.taskFilters || {};
  30. window.taskStatusCount = function (status) {
  31. var tasks = statum.read('tasks');
  32. var rows = (tasks && tasks.tasks) || [];
  33. var count = 0;
  34. for (var i = 0; i < rows.length; i++) {
  35. if (status === 'open') {
  36. if (window.TASK_OPEN_STATUSES.indexOf(rows[i].status) !== -1) {
  37. count += 1;
  38. }
  39. } else if (status === 'all') {
  40. count += 1;
  41. } else if (rows[i].status === status) {
  42. count += 1;
  43. }
  44. }
  45. return count;
  46. };
  47. window.taskRowVisible = function (row) {
  48. var filters = window.taskFilters || {};
  49. if (filters.status && filters.status !== 'all') {
  50. if (filters.status === 'open') {
  51. if (window.TASK_OPEN_STATUSES.indexOf(row.status) === -1) {
  52. return false;
  53. }
  54. } else if (row.status !== filters.status) {
  55. return false;
  56. }
  57. }
  58. if (filters.project && filters.project > 0
  59. && String(row.project_id) !== String(filters.project)) {
  60. return false;
  61. }
  62. if (filters.goal) {
  63. if (filters.goal === 'standalone' && row.goal_id > 0) {
  64. return false;
  65. }
  66. if (filters.goal !== 'standalone'
  67. && String(row.goal_id) !== String(filters.goal)) {
  68. return false;
  69. }
  70. }
  71. return true;
  72. };
  73. window.setTaskFilter = function (name, value) {
  74. window.taskFilters[name] = value;
  75. var tasks = statum.read('tasks');
  76. if (tasks && tasks.refresh) {
  77. statum.act(tasks.refresh).catch(function () { });
  78. } else {
  79. // No sealed refresh on this build: force a render
  80. // cycle through the pulse listener if present.
  81. document.body.setAttribute('data-filter-bump', String(Date.now()));
  82. }
  83. };
  84. window.taskChipClass = function (status) {
  85. var current = (window.taskFilters || {}).status;
  86. if (current == null || current === '') current = 'open';
  87. return current === status ? 'goal-filter active' : 'goal-filter';
  88. };
  89. window.setTaskStatusFilter = function (status) {
  90. var current = (window.taskFilters || {}).status;
  91. if (current == null || current === '') current = 'open';
  92. if (current === status) return; // one chip is always on
  93. window.setTaskFilter('status', status);
  94. };
  95. // Default to open work until a chip says otherwise.
  96. if (window.taskFilters.status == null) {
  97. window.taskFilters.status = 'open';
  98. }
  99. })();
  100. }
  101. </script>
  102. </head>
  103. <body>
  104. <div class="stack">
  105. <div class="cluster cluster-between">
  106. <h1>Tasks</h1>
  107. <button type="button" class="btn-primary"
  108. onclick="window.openAppDialog('task-create-dialog')">+ New task</button>
  109. </div>
  110. <!-- Goal 41 T4: faceted status chips with counts (the
  111. goal-strip pattern ported to the queue). One chip is
  112. always active; the default is "Open" - queued, assigned,
  113. working, blocked, suspended - so the queue leads and
  114. done/failed stay one click away with their counts
  115. visible. -->
  116. <p class="goal-strip">
  117. <button type="button" stm-attribute.class="window.taskChipClass('open')"
  118. onclick="window.setTaskStatusFilter('open')"
  119. stm-text="'Open ' + window.taskStatusCount('open')">open</button>
  120. <button type="button" stm-attribute.class="window.taskChipClass('draft')"
  121. onclick="window.setTaskStatusFilter('draft')"
  122. stm-text="'draft ' + window.taskStatusCount('draft')">draft</button>
  123. <button type="button" stm-attribute.class="window.taskChipClass('queued')"
  124. onclick="window.setTaskStatusFilter('queued')"
  125. stm-text="'queued ' + window.taskStatusCount('queued')">queued</button>
  126. <button type="button" stm-attribute.class="window.taskChipClass('assigned')"
  127. onclick="window.setTaskStatusFilter('assigned')"
  128. stm-text="'assigned ' + window.taskStatusCount('assigned')">assigned</button>
  129. <button type="button" stm-attribute.class="window.taskChipClass('working')"
  130. onclick="window.setTaskStatusFilter('working')"
  131. stm-text="'working ' + window.taskStatusCount('working')">working</button>
  132. <button type="button" stm-attribute.class="window.taskChipClass('blocked')"
  133. onclick="window.setTaskStatusFilter('blocked')"
  134. stm-text="'blocked ' + window.taskStatusCount('blocked')">blocked</button>
  135. <button type="button" stm-attribute.class="window.taskChipClass('suspended')"
  136. onclick="window.setTaskStatusFilter('suspended')"
  137. stm-text="'suspended ' + window.taskStatusCount('suspended')">suspended</button>
  138. <button type="button" stm-attribute.class="window.taskChipClass('done')"
  139. onclick="window.setTaskStatusFilter('done')"
  140. stm-text="'done ' + window.taskStatusCount('done')">done</button>
  141. <button type="button" stm-attribute.class="window.taskChipClass('failed')"
  142. onclick="window.setTaskStatusFilter('failed')"
  143. stm-text="'failed ' + window.taskStatusCount('failed')">failed</button>
  144. <button type="button" stm-attribute.class="window.taskChipClass('all')"
  145. onclick="window.setTaskStatusFilter('all')"
  146. stm-text="'all ' + window.taskStatusCount('all')">all</button>
  147. </p>
  148. <form class="cluster" onsubmit="return false">
  149. <label for="filter-project">Project</label>
  150. <select id="filter-project" onchange="window.setTaskFilter('project', this.value)">
  151. <option value="0">all</option>
  152. <option stm-for-project-in="tasks.projects" stm-attribute.value="project.id"
  153. stm-text="project.name">project</option>
  154. </select>
  155. <label for="filter-goal">Goal</label>
  156. <select id="filter-goal" onchange="window.setTaskFilter('goal', this.value)">
  157. <option value="all">all</option>
  158. <option value="standalone">standalone only</option>
  159. <option stm-for-goal-in="tasks.goal_options" stm-attribute.value="goal.id"
  160. stm-text="goal.title">goal</option>
  161. </select>
  162. </form>
  163. <div class="table-wrap">
  164. <table class="table-hover">
  165. <thead>
  166. <tr><th>#</th><th>Title</th><th>Status</th><th>Mode</th><th>Goal</th>
  167. <th>Priority</th><th>Project</th><th>Capabilities</th></tr>
  168. </thead>
  169. <tbody>
  170. <tr stm-for-task-in="tasks.tasks" stm-if="window.taskRowVisible(task)">
  171. <td stm-text="task.id">id</td>
  172. <td>
  173. <a stm-attribute.href="'/tasks/' + task.id" stm-text="task.title">title</a>
  174. </td>
  175. <td><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span></td>
  176. <td><span stm-attribute.class="'badge badge-mode-' + task.mode" stm-text="task.mode">mode</span></td>
  177. <td stm-if="task.goal_id > 0">
  178. <a stm-attribute.href="'/goals/' + task.goal_id" stm-text="task.goal_title">goal</a>
  179. <span class="text-muted" stm-if="task.creator == 'goal'">(agent)</span>
  180. </td>
  181. <td stm-if="task.goal_id == 0" class="text-muted">—</td>
  182. <td stm-text="task.priority">5</td>
  183. <td stm-text="task.project_name">project</td>
  184. <td><span stm-if="task.capabilities.length > 0"
  185. stm-text="task.capabilities.join(', ')">caps</span>
  186. <span class="text-muted" stm-if="task.capabilities.length == 0">—</span></td>
  187. </tr>
  188. <tr stm-if="tasks.tasks.length == 0"><td colspan="8" class="text-muted">No tasks.</td></tr>
  189. </tbody>
  190. </table>
  191. </div>
  192. </div>
  193. <!-- Goal 41 T4: the create form moved from a collapsed expander
  194. into a Spry-styled native <dialog> opened by the "+ New
  195. task" button (ux-notes quick win 4). The form itself is
  196. unchanged - same fields, same stm-action - and the shared
  197. openAppDialog helper closes it on the action's notify. -->
  198. <dialog id="task-create-dialog">
  199. <div class="dialog-titlebar">
  200. <p class="dialog-title">New task</p>
  201. <button type="button" class="dialog-close" aria-label="Close"
  202. onclick="window.closeAppDialog('task-create-dialog')">×</button>
  203. </div>
  204. <form stm-action="tasks.create">
  205. <div class="dialog-body form-rows">
  206. <div class="field"><label for="task-project">Project</label>
  207. <!-- Goal 8 task 4: "No project" posts project_id 0 —
  208. a STANDALONE task with its own ephemeral
  209. workspace; the completion report is the
  210. deliverable. -->
  211. <select id="task-project" name="project_id">
  212. <option value="0" selected>No project (standalone)</option>
  213. <option stm-for-project-in="tasks.projects" stm-attribute.value="project.id"
  214. stm-text="project.name">project</option>
  215. </select></div>
  216. <div class="field"><label for="task-title">Title</label>
  217. <input type="text" id="task-title" name="title" required></div>
  218. <div class="field"><label for="task-mode">Mode</label>
  219. <select id="task-mode" name="mode">
  220. <option value="implement" selected>implement — build the change on a task branch</option>
  221. <option value="investigate">investigate — explore and report, no code changes</option>
  222. <option value="review">review — read and critique existing work</option>
  223. <option value="test">test — write and run tests</option>
  224. </select></div>
  225. <div class="field"><label for="task-description">Mandate</label>
  226. <textarea id="task-description" name="description" rows="4"
  227. placeholder="What the agent must accomplish (the mandate)"></textarea></div>
  228. <div class="field"><label for="task-branch">Task branch name
  229. <span class="text-muted">(optional — empty lets the system name the branch; goal worker tasks auto-name task/&lt;goal-id&gt;-&lt;task-id&gt;)</span></label>
  230. <input type="text" id="task-branch" name="task_branch"
  231. placeholder="task/&lt;id&gt;-&lt;slug&gt;"></div>
  232. <div class="field"><label for="task-branch-from">Branch from
  233. <span class="text-muted">(default: the project's default branch)</span></label>
  234. <input type="text" id="task-branch-from" name="branch_from"
  235. placeholder="main"></div>
  236. <div class="field"><label for="task-priority">Priority</label>
  237. <select id="task-priority" name="priority">
  238. <option value="1">1 — highest</option>
  239. <option value="2">2</option>
  240. <option value="3">3</option>
  241. <option value="4">4</option>
  242. <option value="5" selected>5 — default</option>
  243. <option value="6">6</option>
  244. <option value="7">7</option>
  245. <option value="8">8</option>
  246. <option value="9">9 — lowest</option>
  247. </select></div>
  248. <div class="field"><label for="task-enqueue-after">Auto-enqueue after
  249. <span class="text-muted">(optional — enqueues itself when the predecessor is done)</span></label>
  250. <select id="task-enqueue-after" name="enqueue_after_task_id">
  251. <option value="">none</option>
  252. <option stm-for-option-in="tasks.enqueue_after_options"
  253. stm-attribute.value="option.id" stm-text="option.name">task</option>
  254. </select></div>
  255. <div class="field" stm-if="tasks.capability_options.length > 0">
  256. <label for="task-capabilities">Capabilities
  257. <span class="text-muted">(the saddle fleet must carry them until assigned)</span></label>
  258. <select id="task-capabilities" name="required_capabilities" multiple
  259. size="3">
  260. <option stm-for-capability-in="tasks.capability_options"
  261. stm-attribute.value="capability.id"
  262. stm-text="capability.name">cap</option>
  263. </select></div>
  264. </div>
  265. <div class="dialog-footer">
  266. <button type="button" class="btn-ghost"
  267. onclick="window.closeAppDialog('task-create-dialog')">Cancel</button>
  268. <button type="submit" class="btn-primary">Create task</button>
  269. </div>
  270. </form>
  271. </dialog>
  272. </body>
  273. </html>