@baack-software/baack-ts-tasks 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +158 -2
  3. package/dist/activity.d.ts +20 -0
  4. package/dist/activity.d.ts.map +1 -0
  5. package/dist/activity.js +75 -0
  6. package/dist/activity.js.map +1 -0
  7. package/dist/blockers.d.ts +28 -0
  8. package/dist/blockers.d.ts.map +1 -0
  9. package/dist/blockers.js +134 -0
  10. package/dist/blockers.js.map +1 -0
  11. package/dist/conflicts.d.ts +50 -0
  12. package/dist/conflicts.d.ts.map +1 -0
  13. package/dist/conflicts.js +73 -0
  14. package/dist/conflicts.js.map +1 -0
  15. package/dist/draft.d.ts +45 -0
  16. package/dist/draft.d.ts.map +1 -0
  17. package/dist/draft.js +155 -0
  18. package/dist/draft.js.map +1 -0
  19. package/dist/errors.d.ts +30 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +37 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +11 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/react/TaskActivity.d.ts +15 -0
  28. package/dist/react/TaskActivity.d.ts.map +1 -0
  29. package/dist/react/TaskActivity.js +60 -0
  30. package/dist/react/TaskActivity.js.map +1 -0
  31. package/dist/react/TaskBoard.d.ts +38 -0
  32. package/dist/react/TaskBoard.d.ts.map +1 -0
  33. package/dist/react/TaskBoard.js +85 -0
  34. package/dist/react/TaskBoard.js.map +1 -0
  35. package/dist/react/TaskConflicts.d.ts +26 -0
  36. package/dist/react/TaskConflicts.d.ts.map +1 -0
  37. package/dist/react/TaskConflicts.js +87 -0
  38. package/dist/react/TaskConflicts.js.map +1 -0
  39. package/dist/react/TaskCreateForm.d.ts +13 -0
  40. package/dist/react/TaskCreateForm.d.ts.map +1 -0
  41. package/dist/react/TaskCreateForm.js +68 -0
  42. package/dist/react/TaskCreateForm.js.map +1 -0
  43. package/dist/react/TaskEditor.d.ts +33 -0
  44. package/dist/react/TaskEditor.d.ts.map +1 -0
  45. package/dist/react/TaskEditor.js +172 -0
  46. package/dist/react/TaskEditor.js.map +1 -0
  47. package/dist/react/TaskFilters.d.ts +15 -0
  48. package/dist/react/TaskFilters.d.ts.map +1 -0
  49. package/dist/react/TaskFilters.js +48 -0
  50. package/dist/react/TaskFilters.js.map +1 -0
  51. package/dist/react/TaskView.d.ts +30 -0
  52. package/dist/react/TaskView.d.ts.map +1 -0
  53. package/dist/react/TaskView.js +108 -0
  54. package/dist/react/TaskView.js.map +1 -0
  55. package/dist/react/TasksAbout.d.ts +24 -0
  56. package/dist/react/TasksAbout.d.ts.map +1 -0
  57. package/dist/react/TasksAbout.js +82 -0
  58. package/dist/react/TasksAbout.js.map +1 -0
  59. package/dist/react/copy.d.ts +154 -0
  60. package/dist/react/copy.d.ts.map +1 -0
  61. package/dist/react/copy.js +173 -0
  62. package/dist/react/copy.js.map +1 -0
  63. package/dist/react/fields.d.ts +36 -0
  64. package/dist/react/fields.d.ts.map +1 -0
  65. package/dist/react/fields.js +21 -0
  66. package/dist/react/fields.js.map +1 -0
  67. package/dist/react/hooks.d.ts +9 -0
  68. package/dist/react/hooks.d.ts.map +1 -0
  69. package/dist/react/hooks.js +31 -0
  70. package/dist/react/hooks.js.map +1 -0
  71. package/dist/react/index.d.ts +13 -0
  72. package/dist/react/index.d.ts.map +1 -0
  73. package/dist/react/index.js +13 -0
  74. package/dist/react/index.js.map +1 -0
  75. package/dist/react/parts.d.ts +55 -0
  76. package/dist/react/parts.d.ts.map +1 -0
  77. package/dist/react/parts.js +92 -0
  78. package/dist/react/parts.js.map +1 -0
  79. package/dist/react/ui/alert.d.ts +11 -0
  80. package/dist/react/ui/alert.d.ts.map +1 -0
  81. package/dist/react/ui/alert.js +29 -0
  82. package/dist/react/ui/alert.js.map +1 -0
  83. package/dist/react/ui/avatar.d.ts +12 -0
  84. package/dist/react/ui/avatar.d.ts.map +1 -0
  85. package/dist/react/ui/avatar.js +24 -0
  86. package/dist/react/ui/avatar.js.map +1 -0
  87. package/dist/react/ui/badge.d.ts +10 -0
  88. package/dist/react/ui/badge.d.ts.map +1 -0
  89. package/dist/react/ui/badge.js +26 -0
  90. package/dist/react/ui/badge.js.map +1 -0
  91. package/dist/react/ui/button.d.ts +11 -0
  92. package/dist/react/ui/button.d.ts.map +1 -0
  93. package/dist/react/ui/button.js +37 -0
  94. package/dist/react/ui/button.js.map +1 -0
  95. package/dist/react/ui/card.d.ts +12 -0
  96. package/dist/react/ui/card.d.ts.map +1 -0
  97. package/dist/react/ui/card.js +26 -0
  98. package/dist/react/ui/card.js.map +1 -0
  99. package/dist/react/ui/input.d.ts +4 -0
  100. package/dist/react/ui/input.d.ts.map +1 -0
  101. package/dist/react/ui/input.js +8 -0
  102. package/dist/react/ui/input.js.map +1 -0
  103. package/dist/react/ui/label.d.ts +5 -0
  104. package/dist/react/ui/label.d.ts.map +1 -0
  105. package/dist/react/ui/label.js +10 -0
  106. package/dist/react/ui/label.js.map +1 -0
  107. package/dist/react/ui/radio-group.d.ts +6 -0
  108. package/dist/react/ui/radio-group.d.ts.map +1 -0
  109. package/dist/react/ui/radio-group.js +12 -0
  110. package/dist/react/ui/radio-group.js.map +1 -0
  111. package/dist/react/ui/select.d.ts +16 -0
  112. package/dist/react/ui/select.d.ts.map +1 -0
  113. package/dist/react/ui/select.js +37 -0
  114. package/dist/react/ui/select.js.map +1 -0
  115. package/dist/react/ui/separator.d.ts +5 -0
  116. package/dist/react/ui/separator.d.ts.map +1 -0
  117. package/dist/react/ui/separator.js +10 -0
  118. package/dist/react/ui/separator.js.map +1 -0
  119. package/dist/react/ui/sheet.d.ts +15 -0
  120. package/dist/react/ui/sheet.d.ts.map +1 -0
  121. package/dist/react/ui/sheet.js +38 -0
  122. package/dist/react/ui/sheet.js.map +1 -0
  123. package/dist/react/ui/skeleton.d.ts +3 -0
  124. package/dist/react/ui/skeleton.d.ts.map +1 -0
  125. package/dist/react/ui/skeleton.js +7 -0
  126. package/dist/react/ui/skeleton.js.map +1 -0
  127. package/dist/react/ui/table.d.ts +11 -0
  128. package/dist/react/ui/table.d.ts.map +1 -0
  129. package/dist/react/ui/table.js +29 -0
  130. package/dist/react/ui/table.js.map +1 -0
  131. package/dist/react/ui/tabs.d.ts +12 -0
  132. package/dist/react/ui/tabs.d.ts.map +1 -0
  133. package/dist/react/ui/tabs.js +31 -0
  134. package/dist/react/ui/tabs.js.map +1 -0
  135. package/dist/react/ui/textarea.d.ts +4 -0
  136. package/dist/react/ui/textarea.d.ts.map +1 -0
  137. package/dist/react/ui/textarea.js +8 -0
  138. package/dist/react/ui/textarea.js.map +1 -0
  139. package/dist/react/ui/toggle-group.d.ts +11 -0
  140. package/dist/react/ui/toggle-group.d.ts.map +1 -0
  141. package/dist/react/ui/toggle-group.js +25 -0
  142. package/dist/react/ui/toggle-group.js.map +1 -0
  143. package/dist/react/ui/toggle.d.ts +10 -0
  144. package/dist/react/ui/toggle.d.ts.map +1 -0
  145. package/dist/react/ui/toggle.js +27 -0
  146. package/dist/react/ui/toggle.js.map +1 -0
  147. package/dist/refs.d.ts +16 -0
  148. package/dist/refs.d.ts.map +1 -0
  149. package/dist/refs.js +43 -0
  150. package/dist/refs.js.map +1 -0
  151. package/dist/server/index.d.ts +4 -0
  152. package/dist/server/index.d.ts.map +1 -0
  153. package/dist/server/index.js +4 -0
  154. package/dist/server/index.js.map +1 -0
  155. package/dist/server/recipes.d.ts +39 -0
  156. package/dist/server/recipes.d.ts.map +1 -0
  157. package/dist/server/recipes.js +81 -0
  158. package/dist/server/recipes.js.map +1 -0
  159. package/dist/server/webhooks.d.ts +135 -0
  160. package/dist/server/webhooks.d.ts.map +1 -0
  161. package/dist/server/webhooks.js +243 -0
  162. package/dist/server/webhooks.js.map +1 -0
  163. package/dist/store.d.ts +124 -0
  164. package/dist/store.d.ts.map +1 -0
  165. package/dist/store.js +443 -0
  166. package/dist/store.js.map +1 -0
  167. package/dist/task-client.d.ts +137 -0
  168. package/dist/task-client.d.ts.map +1 -0
  169. package/dist/task-client.js +271 -0
  170. package/dist/task-client.js.map +1 -0
  171. package/dist/tasks.d.ts +84 -0
  172. package/dist/tasks.d.ts.map +1 -0
  173. package/dist/tasks.js +208 -0
  174. package/dist/tasks.js.map +1 -0
  175. package/dist/time.d.ts +15 -0
  176. package/dist/time.d.ts.map +1 -0
  177. package/dist/time.js +34 -0
  178. package/dist/time.js.map +1 -0
  179. package/dist/types.d.ts +74 -0
  180. package/dist/types.d.ts.map +1 -0
  181. package/dist/types.js +9 -0
  182. package/dist/types.js.map +1 -0
  183. package/package.json +101 -4
  184. package/src/activity.ts +86 -0
  185. package/src/blockers.ts +134 -0
  186. package/src/conflicts.ts +115 -0
  187. package/src/draft.ts +162 -0
  188. package/src/errors.ts +40 -0
  189. package/src/index.ts +10 -0
  190. package/src/react/TaskActivity.tsx +187 -0
  191. package/src/react/TaskBoard.tsx +264 -0
  192. package/src/react/TaskConflicts.tsx +217 -0
  193. package/src/react/TaskCreateForm.tsx +165 -0
  194. package/src/react/TaskEditor.tsx +485 -0
  195. package/src/react/TaskFilters.tsx +122 -0
  196. package/src/react/TaskView.tsx +291 -0
  197. package/src/react/TasksAbout.tsx +226 -0
  198. package/src/react/copy.tsx +253 -0
  199. package/src/react/fields.tsx +104 -0
  200. package/src/react/hooks.ts +41 -0
  201. package/src/react/index.ts +12 -0
  202. package/src/react/parts.tsx +176 -0
  203. package/src/react/ui/README.md +6 -0
  204. package/src/react/ui/alert.tsx +75 -0
  205. package/src/react/ui/avatar.tsx +109 -0
  206. package/src/react/ui/badge.tsx +48 -0
  207. package/src/react/ui/button.tsx +66 -0
  208. package/src/react/ui/card.tsx +102 -0
  209. package/src/react/ui/input.tsx +18 -0
  210. package/src/react/ui/label.tsx +23 -0
  211. package/src/react/ui/radio-group.tsx +41 -0
  212. package/src/react/ui/select.tsx +189 -0
  213. package/src/react/ui/separator.tsx +27 -0
  214. package/src/react/ui/sheet.tsx +145 -0
  215. package/src/react/ui/skeleton.tsx +13 -0
  216. package/src/react/ui/table.tsx +113 -0
  217. package/src/react/ui/tabs.tsx +89 -0
  218. package/src/react/ui/textarea.tsx +17 -0
  219. package/src/react/ui/toggle-group.tsx +89 -0
  220. package/src/react/ui/toggle.tsx +44 -0
  221. package/src/refs.ts +49 -0
  222. package/src/server/index.ts +3 -0
  223. package/src/server/recipes.ts +107 -0
  224. package/src/server/webhooks.ts +369 -0
  225. package/src/store.ts +538 -0
  226. package/src/task-client.ts +373 -0
  227. package/src/tasks.ts +263 -0
  228. package/src/time.ts +35 -0
  229. package/src/types.ts +101 -0
package/dist/time.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Baack timestamps are ISO 8601 in UTC without a zone, for example `2025-09-13T17:08:00` or `2025-09-13T17:08:00.000`.
3
+ * A plain `new Date(timestamp)` would read them as local time, so use these helpers instead.
4
+ */
5
+ const HAS_ZONE = /(?:Z|[+-]\d{2}:?\d{2})$/i;
6
+ /** Reads a Baack timestamp as a `Date`, or undefined when it is empty or not a valid timestamp. */
7
+ export function parseTimestamp(timestamp) {
8
+ if (!timestamp)
9
+ return undefined;
10
+ const date = new Date(HAS_ZONE.test(timestamp) ? timestamp : timestamp + 'Z');
11
+ return Number.isNaN(date.getTime()) ? undefined : date;
12
+ }
13
+ /** Writes a `Date` as a Baack timestamp: UTC, with milliseconds, without a zone. */
14
+ export function toTimestamp(date) {
15
+ return date.toISOString().replace(/Z$/, '');
16
+ }
17
+ /**
18
+ * The value for an `<input type="datetime-local">` showing a Baack timestamp in the viewer's local time, or `''`.
19
+ */
20
+ export function toLocalInput(timestamp) {
21
+ const date = parseTimestamp(timestamp);
22
+ if (!date)
23
+ return '';
24
+ const pad = (n) => String(n).padStart(2, '0');
25
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
26
+ }
27
+ /** Reads an `<input type="datetime-local">` value (local time) as a Baack timestamp, or undefined when empty. */
28
+ export function fromLocalInput(value) {
29
+ if (!value)
30
+ return undefined;
31
+ const date = new Date(value);
32
+ return Number.isNaN(date.getTime()) ? undefined : toTimestamp(date);
33
+ }
34
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../src/time.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,QAAQ,GAAG,0BAA0B,CAAC;AAE5C,mGAAmG;AACnG,MAAM,UAAU,cAAc,CAAC,SAAoC;IACjE,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC;IAC9E,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,IAAU;IACpC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,SAAoC;IAC/D,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;AACtI,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,cAAc,CAAC,KAAgC;IAC7D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { Pagination, TaskActivity, TaskLog, TaskPriority, TaskStatus } from '@baack-software/baack-ts-client';
2
+ /**
3
+ * A reference to another representation by URN. The API resolves references by `urn`, so this is all a request needs,
4
+ * unlike the client's read types (for example `Identity` requires `owner`).
5
+ */
6
+ export interface Ref {
7
+ urn: string;
8
+ url?: string | null;
9
+ }
10
+ /** A URN, or anything carrying one, such as a task or identity read from the API. */
11
+ export type RefLike = string | Ref;
12
+ /**
13
+ * Task log activities, with both spellings of completed: earlier versions of baack-ts-client's `TaskActivity` have only
14
+ * `STATUS_COMPL_ETED`, later ones only `STATUS_COMPLETED`.
15
+ */
16
+ export type Activity = TaskActivity | 'STATUS_COMPLETED' | 'STATUS_COMPL_ETED';
17
+ /**
18
+ * Task fields to create or change. The API stores what an update sends, so `TaskClient.update` sends these changes on
19
+ * top of the whole task (see `updateBody`). Lists are sent whole: the server compares them with the current lists and
20
+ * logs each addition and removal.
21
+ */
22
+ export interface TaskInput {
23
+ owner?: Ref;
24
+ /** `null` unsets. */
25
+ assignee?: Ref | null;
26
+ /** `null` unsets. */
27
+ project?: Ref | null;
28
+ /** `null` unsets. */
29
+ workflowStage?: Ref | null;
30
+ status?: TaskStatus;
31
+ priority?: TaskPriority;
32
+ title?: string;
33
+ description?: string;
34
+ /** ISO 8601 in UTC without a zone, see `toTimestamp`. `null` unsets. */
35
+ dueTimestamp?: string | null;
36
+ blockedBy?: Ref[];
37
+ blocking?: Ref[];
38
+ associatedTasks?: Ref[];
39
+ associatedEntities?: Ref[];
40
+ associatedIdentities?: Ref[];
41
+ associatedCompanies?: Ref[];
42
+ /** Attributes the change in the activity log without assigning the task. */
43
+ identityForLogs?: Ref;
44
+ }
45
+ /** A new task, with the fields the API requires on create. Build one with `draft`. */
46
+ export interface TaskDraft extends TaskInput {
47
+ owner: Ref;
48
+ status: TaskStatus;
49
+ priority: TaskPriority;
50
+ title: string;
51
+ description: string;
52
+ blockedBy: Ref[];
53
+ blocking: Ref[];
54
+ associatedTasks: Ref[];
55
+ associatedEntities: Ref[];
56
+ associatedIdentities: Ref[];
57
+ associatedCompanies: Ref[];
58
+ }
59
+ /**
60
+ * The body of a task update: the whole task as it should be stored. The server requires `urn`, `owner`, `status`,
61
+ * `priority`, `title` and `description`, and clears any optional field left out. Build one with `updateBody`.
62
+ */
63
+ export interface TaskUpdate extends TaskDraft {
64
+ urn: string;
65
+ }
66
+ /** One page of a task's activity log, newest first. */
67
+ export interface TaskLogPage {
68
+ logs: TaskLog[];
69
+ pagination?: Pagination | null;
70
+ }
71
+ /** The fields of a task that hold lists of references. */
72
+ export type TaskListField = 'blockedBy' | 'blocking' | 'associatedTasks' | 'associatedEntities' | 'associatedIdentities' | 'associatedCompanies';
73
+ export declare const TASK_LIST_FIELDS: readonly TaskListField[];
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,UAAU,EACX,MAAM,iCAAiC,CAAC;AAEzC;;;GAGG;AACH,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,qFAAqF;AACrF,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,GAAG,CAAC;AAEnC;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;AAE/E;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,qBAAqB;IACrB,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IACtB,qBAAqB;IACrB,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB,qBAAqB;IACrB,aAAa,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,GAAG,EAAE,CAAC;IACxB,kBAAkB,CAAC,EAAE,GAAG,EAAE,CAAC;IAC3B,oBAAoB,CAAC,EAAE,GAAG,EAAE,CAAC;IAC7B,mBAAmB,CAAC,EAAE,GAAG,EAAE,CAAC;IAC5B,4EAA4E;IAC5E,eAAe,CAAC,EAAE,GAAG,CAAC;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,KAAK,EAAE,GAAG,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,QAAQ,EAAE,GAAG,EAAE,CAAC;IAChB,eAAe,EAAE,GAAG,EAAE,CAAC;IACvB,kBAAkB,EAAE,GAAG,EAAE,CAAC;IAC1B,oBAAoB,EAAE,GAAG,EAAE,CAAC;IAC5B,mBAAmB,EAAE,GAAG,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,0DAA0D;AAC1D,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,UAAU,GACV,iBAAiB,GACjB,oBAAoB,GACpB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAOpD,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ export const TASK_LIST_FIELDS = [
2
+ 'blockedBy',
3
+ 'blocking',
4
+ 'associatedTasks',
5
+ 'associatedEntities',
6
+ 'associatedIdentities',
7
+ 'associatedCompanies',
8
+ ];
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6FA,MAAM,CAAC,MAAM,gBAAgB,GAA6B;IACxD,WAAW;IACX,UAAU;IACV,iBAAiB;IACjB,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;CACtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,103 @@
1
1
  {
2
2
  "name": "@baack-software/baack-ts-tasks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "TypeScript tools for Baack tasks: typed task API, helpers, a task store and shadcn/ui task components",
5
+ "keywords": [
6
+ "Baack",
7
+ "tasks",
8
+ "projects",
9
+ "workflow"
10
+ ],
11
+ "homepage": "https://github.com/Baack-Software/baack-ts-tasks#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/Baack-Software/baack-ts-tasks.git"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "author": "baack-dakkad",
18
+ "type": "module",
19
+ "main": "dist/index.js",
20
+ "module": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./store": {
28
+ "types": "./dist/store.d.ts",
29
+ "import": "./dist/store.js"
30
+ },
31
+ "./react": {
32
+ "types": "./dist/react/index.d.ts",
33
+ "import": "./dist/react/index.js"
34
+ },
35
+ "./server": {
36
+ "types": "./dist/server/index.d.ts",
37
+ "import": "./dist/server/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src"
43
+ ],
44
+ "sideEffects": false,
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "demo": "vite --config demo/vite.config.ts",
53
+ "demo:build": "vite build --config demo/vite.config.ts --outDir ../demo-dist --emptyOutDir",
54
+ "check:api": "node --env-file-if-exists=.env scripts/check-api.ts",
55
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
56
+ },
57
+ "peerDependencies": {
58
+ "@baack-software/baack-ts-client": ">=0.0.2",
59
+ "@baack-software/baack-ts-components": ">=0.0.1",
60
+ "react": ">=19",
61
+ "react-dom": ">=19"
62
+ },
63
+ "devDependencies": {
64
+ "@baack-software/baack-ts-client": "file:../baack-ts-client",
65
+ "@baack-software/baack-ts-components": "file:../baack-ts-components",
66
+ "@tailwindcss/vite": "^4.3.3",
67
+ "@testing-library/react": "^16.3.3",
68
+ "@testing-library/user-event": "^14.6.8",
69
+ "@types/node": "^24.0.0",
70
+ "@types/react": "^19.3.0",
71
+ "@types/react-dom": "^19.3.0",
72
+ "@vitejs/plugin-react": "^6.1.2",
73
+ "jsdom": "^30.1.2",
74
+ "react": "^19.3.0",
75
+ "react-dom": "^19.3.0",
76
+ "shadcn": "^4.21.0",
77
+ "tailwindcss": "^4.3.3",
78
+ "tw-animate-css": "^1.4.0",
79
+ "typescript": "^5.0.0",
80
+ "vite": "^8.3.4",
81
+ "vitest": "^5.0.2"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public"
85
+ },
86
+ "dependencies": {
87
+ "class-variance-authority": "^0.7.1",
88
+ "cn": "^0.5.0",
89
+ "lucide-react": "^1.56.0",
90
+ "radix-ui": "^1.8.1"
91
+ },
92
+ "peerDependenciesMeta": {
93
+ "react": {
94
+ "optional": true
95
+ },
96
+ "react-dom": {
97
+ "optional": true
98
+ },
99
+ "@baack-software/baack-ts-components": {
100
+ "optional": true
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,86 @@
1
+ import type { TaskLog } from '@baack-software/baack-ts-client';
2
+ import type { Activity } from './types.ts';
3
+
4
+ /** What happened, as a short sentence without the subject, e.g. "Status set to In progress". */
5
+ export const ACTIVITY_LABELS: Readonly<Record<Activity, string>> = {
6
+ CREATED: 'Created',
7
+ STATUS_UNDEFINED: 'Status set to Undefined',
8
+ STATUS_PENDING: 'Status set to Pending',
9
+ STATUS_IN_PROGRESS: 'Status set to In progress',
10
+ STATUS_BLOCKED: 'Status set to Blocked',
11
+ STATUS_COMPLETED: 'Status set to Completed',
12
+ STATUS_COMPL_ETED: 'Status set to Completed',
13
+ STATUS_CLOSED: 'Status set to Closed',
14
+ PRIORITY_NONE: 'Priority set to None',
15
+ PRIORITY_LOW: 'Priority set to Low',
16
+ PRIORITY_MEDIUM: 'Priority set to Medium',
17
+ PRIORITY_HIGH: 'Priority set to High',
18
+ PRIORITY_CRITICAL: 'Priority set to Critical',
19
+ WORKFLOW_STAGE_SET: 'Moved to a workflow stage',
20
+ WORKFLOW_STAGE_UNSET: 'Removed from its workflow stage',
21
+ COMMENT_ADDED: 'Commented',
22
+ WORK_TASK_ASSOCIATION_ADDED: 'Related task added',
23
+ WORK_TASK_ASSOCIATION_REMOVED: 'Related task removed',
24
+ WORK_TASK_BLOCKER_ADDED: 'Blocker added',
25
+ WORK_TASK_BLOCKER_REMOVED: 'Blocker removed',
26
+ WORK_TASK_BLOCKING_ADDED: 'Now blocking a task',
27
+ WORK_TASK_BLOCKING_REMOVED: 'No longer blocking a task',
28
+ WORK_TASK_ENTITY_ADDED: 'Content added',
29
+ WORK_TASK_ENTITY_REMOVED: 'Content removed',
30
+ WORK_TASK_IDENTITY_ADDED: 'Person added',
31
+ WORK_TASK_IDENTITY_REMOVED: 'Person removed',
32
+ WORK_TASK_COMPANY_ADDED: 'Company added',
33
+ WORK_TASK_COMPANY_REMOVED: 'Company removed',
34
+ WORK_PROJECT_SET: 'Added to a project',
35
+ WORK_PROJECT_UNSET: 'Removed from its project',
36
+ ASSIGNED: 'Assigned',
37
+ UNASSIGNED: 'Unassigned',
38
+ TITLE_UPDATED: 'Title changed',
39
+ DESCRIPTION_UPDATED: 'Description changed',
40
+ DUE_DATE_UPDATED: 'Due date changed',
41
+ DUE_DATE_UNSET: 'Due date removed',
42
+ };
43
+
44
+ /** A readable label for an activity, falling back to the raw value in lower case for activities added later. */
45
+ export function activityLabel(activity: string): string {
46
+ return (ACTIVITY_LABELS as Record<string, string>)[activity] ?? activity.toLowerCase().replaceAll('_', ' ');
47
+ }
48
+
49
+ /** Whether the log entry is a comment rather than a recorded change. */
50
+ export function isComment(log: Pick<TaskLog, 'activity'>): boolean {
51
+ return log.activity === 'COMMENT_ADDED';
52
+ }
53
+
54
+ /** A comment and the replies to it, oldest reply first. */
55
+ export interface CommentThread {
56
+ comment: TaskLog;
57
+ replies: TaskLog[];
58
+ }
59
+
60
+ /**
61
+ * Groups comments into threads by `replyTo`, most recently active thread first. Expects `logs` newest first, as the
62
+ * API returns them.
63
+ * A reply whose parent is not in `logs` (for example on an older page) starts its own thread.
64
+ */
65
+ export function commentThreads(logs: readonly TaskLog[]): CommentThread[] {
66
+ const comments = logs.filter(isComment);
67
+ const byUrn = new Map(comments.map((log) => [log.urn, log]));
68
+ const rootOf = (log: TaskLog): TaskLog => {
69
+ const seen = new Set<string>();
70
+ let current = log;
71
+ while (current.replyTo && byUrn.has(current.replyTo.urn) && !seen.has(current.urn)) {
72
+ seen.add(current.urn);
73
+ current = byUrn.get(current.replyTo.urn)!;
74
+ }
75
+ return current;
76
+ };
77
+
78
+ const threads = new Map<string, CommentThread>();
79
+ for (const log of comments) {
80
+ const root = rootOf(log);
81
+ const thread = threads.get(root.urn) ?? { comment: root, replies: [] };
82
+ if (root !== log) thread.replies.unshift(log);
83
+ threads.set(root.urn, thread);
84
+ }
85
+ return [...threads.values()];
86
+ }
@@ -0,0 +1,134 @@
1
+ import type { Task } from '@baack-software/baack-ts-client';
2
+ import { urnOf } from './refs.ts';
3
+ import { isOpen } from './tasks.ts';
4
+ import type { RefLike } from './types.ts';
5
+
6
+ /** Finds a task by URN, for example from a store or a `Map`. */
7
+ export type TaskLookup = (urn: string) => Task | undefined;
8
+
9
+ /** A lookup over a list of tasks. */
10
+ export function lookupFrom(tasks: Iterable<Task>): TaskLookup {
11
+ const byUrn = new Map<string, Task>();
12
+ for (const task of tasks) byUrn.set(task.urn, task);
13
+ return (urn) => byUrn.get(urn);
14
+ }
15
+
16
+ /**
17
+ * The URNs of the tasks blocking `task` that are still open. A blocker the lookup doesn't know counts as open, since
18
+ * the task may be blocked by work the reader can't see.
19
+ */
20
+ export function openBlockers(task: Task, lookup: TaskLookup): string[] {
21
+ return (task.blockedBy ?? [])
22
+ .map((blocker) => blocker.urn)
23
+ .filter((urn) => {
24
+ const blocker = lookup(urn);
25
+ return blocker === undefined || isOpen(blocker);
26
+ });
27
+ }
28
+
29
+ /** Whether every task blocking `task` is done, so work can start. */
30
+ export function isUnblocked(task: Task, lookup: TaskLookup): boolean {
31
+ return openBlockers(task, lookup).length === 0;
32
+ }
33
+
34
+ /**
35
+ * Blocking edges among `tasks`, from each task to the tasks it blocks. Reads both `blocking` and `blockedBy`, so an
36
+ * edge recorded on only one side still counts.
37
+ */
38
+ export function blockingGraph(tasks: Iterable<Task>): Map<string, Set<string>> {
39
+ const graph = new Map<string, Set<string>>();
40
+ const edge = (from: string, to: string) => {
41
+ const targets = graph.get(from) ?? new Set<string>();
42
+ targets.add(to);
43
+ graph.set(from, targets);
44
+ };
45
+ for (const task of tasks) {
46
+ if (!graph.has(task.urn)) graph.set(task.urn, new Set());
47
+ for (const blocked of task.blocking ?? []) edge(task.urn, blocked.urn);
48
+ for (const blocker of task.blockedBy ?? []) edge(blocker.urn, task.urn);
49
+ }
50
+ return graph;
51
+ }
52
+
53
+ /** A cycle of blocking tasks as a list of URNs (the first task blocks the second, and so on, back to the first), or undefined. */
54
+ export function findBlockingCycle(tasks: Iterable<Task>): string[] | undefined {
55
+ const graph = blockingGraph(tasks);
56
+ const state = new Map<string, 'visiting' | 'done'>();
57
+ const path: string[] = [];
58
+
59
+ const visit = (urn: string): string[] | undefined => {
60
+ state.set(urn, 'visiting');
61
+ path.push(urn);
62
+ for (const next of graph.get(urn) ?? []) {
63
+ const seen = state.get(next);
64
+ if (seen === 'visiting') return path.slice(path.indexOf(next));
65
+ if (seen === undefined) {
66
+ const cycle = visit(next);
67
+ if (cycle) return cycle;
68
+ }
69
+ }
70
+ path.pop();
71
+ state.set(urn, 'done');
72
+ return undefined;
73
+ };
74
+
75
+ for (const urn of graph.keys()) {
76
+ if (state.get(urn) === undefined) {
77
+ const cycle = visit(urn);
78
+ if (cycle) return cycle;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ /** Whether making `blocker` block `task` would create a cycle, given the current `tasks`. */
85
+ export function wouldCreateCycle(tasks: Iterable<Task>, task: RefLike, blocker: RefLike): boolean {
86
+ const from = urnOf(blocker);
87
+ const to = urnOf(task);
88
+ if (from === to) return true;
89
+ // A cycle appears when the blocker is already reachable from the task it would block.
90
+ const graph = blockingGraph(tasks);
91
+ const queue = [to];
92
+ const seen = new Set(queue);
93
+ for (let urn = queue.shift(); urn !== undefined; urn = queue.shift()) {
94
+ for (const next of graph.get(urn) ?? []) {
95
+ if (next === from) return true;
96
+ if (!seen.has(next)) {
97
+ seen.add(next);
98
+ queue.push(next);
99
+ }
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+
105
+ /**
106
+ * Tasks in an order where each comes after the tasks blocking it, keeping the given order otherwise. Tasks in a
107
+ * cycle are placed at the end in their given order.
108
+ */
109
+ export function dependencyOrder(tasks: readonly Task[]): Task[] {
110
+ const known = new Set(tasks.map((task) => task.urn));
111
+ const graph = blockingGraph(tasks);
112
+ const waiting = new Map<string, number>(tasks.map((task) => [task.urn, 0]));
113
+ for (const [from, targets] of graph) {
114
+ if (!known.has(from)) continue;
115
+ for (const to of targets) if (known.has(to)) waiting.set(to, (waiting.get(to) ?? 0) + 1);
116
+ }
117
+
118
+ const ordered: Task[] = [];
119
+ const placed = new Set<string>();
120
+ let progress = true;
121
+ while (progress) {
122
+ progress = false;
123
+ for (const task of tasks) {
124
+ if (placed.has(task.urn) || waiting.get(task.urn) !== 0) continue;
125
+ ordered.push(task);
126
+ placed.add(task.urn);
127
+ progress = true;
128
+ for (const to of graph.get(task.urn) ?? []) {
129
+ if (known.has(to)) waiting.set(to, (waiting.get(to) ?? 0) - 1);
130
+ }
131
+ }
132
+ }
133
+ return [...ordered, ...tasks.filter((task) => !placed.has(task.urn))];
134
+ }
@@ -0,0 +1,115 @@
1
+ import type { Task } from '@baack-software/baack-ts-client';
2
+ import { refs, sameRef, sameRefs, urnOf } from './refs.ts';
3
+ import { parseTimestamp } from './time.ts';
4
+ import { TASK_LIST_FIELDS, type Ref, type TaskInput, type TaskListField } from './types.ts';
5
+
6
+ /**
7
+ * Finding and resolving conflicting edits.
8
+ *
9
+ * An editor shows a task (the base), the person changes some fields, and by the time the change is sent someone else
10
+ * may have changed the task too. Fields only the other person changed are kept, since an update applies only this
11
+ * person's changes on top of the task as it is now. A field both changed is a conflict, which the person resolves by
12
+ * keeping their value or using the other one. Lists never conflict: their changes are applied as additions and
13
+ * removals.
14
+ */
15
+
16
+ /** The task fields that hold one value, which can conflict. */
17
+ export type TaskField =
18
+ | 'title'
19
+ | 'description'
20
+ | 'status'
21
+ | 'priority'
22
+ | 'dueTimestamp'
23
+ | 'assignee'
24
+ | 'project'
25
+ | 'workflowStage';
26
+
27
+ export const TASK_FIELDS: readonly TaskField[] = [
28
+ 'title',
29
+ 'description',
30
+ 'status',
31
+ 'priority',
32
+ 'dueTimestamp',
33
+ 'assignee',
34
+ 'project',
35
+ 'workflowStage',
36
+ ];
37
+
38
+ /** A field's value: text, a timestamp, or a reference. Absent and empty values are the same. */
39
+ export type TaskFieldValue = string | Ref | null | undefined;
40
+
41
+ /** A field this person and someone else both changed since the base. */
42
+ export interface TaskConflict {
43
+ field: TaskField;
44
+ /** The value when this person started editing. */
45
+ base: TaskFieldValue;
46
+ /** The value someone else saved since. */
47
+ theirs: TaskFieldValue;
48
+ /** The value this person is saving. */
49
+ mine: TaskFieldValue;
50
+ }
51
+
52
+ /** Which value to keep for each conflicting field. */
53
+ export type ConflictChoices = Partial<Record<TaskField, 'mine' | 'theirs'>>;
54
+
55
+ /** Whether two values of a field are the same: references by URN, timestamps by instant, absent equal to empty. */
56
+ export function sameValue(field: TaskField, a: TaskFieldValue, b: TaskFieldValue): boolean {
57
+ if (field === 'assignee' || field === 'project' || field === 'workflowStage') {
58
+ return sameRef(a as Ref | null | undefined, b as Ref | null | undefined);
59
+ }
60
+ if (field === 'dueTimestamp') {
61
+ return parseTimestamp(a as string | null | undefined)?.getTime() === parseTimestamp(b as string | null | undefined)?.getTime();
62
+ }
63
+ return ((a as string | null | undefined) ?? '') === ((b as string | null | undefined) ?? '');
64
+ }
65
+
66
+ /** The fields that differ between two versions of a task, single values first, then lists. */
67
+ export function changedFields(before: Task, after: Task): (TaskField | TaskListField)[] {
68
+ return [
69
+ ...TASK_FIELDS.filter((field) => !sameValue(field, before[field], after[field])),
70
+ ...TASK_LIST_FIELDS.filter((field) => !sameRefs(before[field], after[field])),
71
+ ];
72
+ }
73
+
74
+ /** The single-value fields `changes` sets. */
75
+ export function fieldsChanged(changes: TaskInput): TaskField[] {
76
+ return TASK_FIELDS.filter((field) => changes[field] !== undefined);
77
+ }
78
+
79
+ /**
80
+ * The fields `changes` sets that someone else also changed between `base` and `theirs`, to a different value.
81
+ * Empty when the change can be saved as it is.
82
+ */
83
+ export function findConflicts(base: Task, theirs: Task, changes: TaskInput): TaskConflict[] {
84
+ return fieldsChanged(changes)
85
+ .filter((field) => !sameValue(field, base[field], theirs[field]) && !sameValue(field, theirs[field], changes[field]))
86
+ .map((field) => ({ field, base: base[field], theirs: theirs[field], mine: changes[field] }));
87
+ }
88
+
89
+ /**
90
+ * `changes` with the choices applied: a field resolved to theirs is dropped, so the other person's value stays. Fields
91
+ * without a choice keep this person's value.
92
+ */
93
+ export function resolveConflicts(changes: TaskInput, choices: ConflictChoices): TaskInput {
94
+ const result: TaskInput = { ...changes };
95
+ for (const field of TASK_FIELDS) if (choices[field] === 'theirs') delete result[field];
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * List changes made against `base`, replayed on `theirs` as additions and removals, so items someone else added or
101
+ * removed since are kept. Other fields are unchanged.
102
+ */
103
+ export function rebaseLists(base: Task, theirs: Task, changes: TaskInput): TaskInput {
104
+ const result: TaskInput = { ...changes };
105
+ for (const field of TASK_LIST_FIELDS) {
106
+ const mine = changes[field];
107
+ if (mine === undefined) continue;
108
+ const before = new Set((base[field] ?? []).map(urnOf));
109
+ const after = new Set(mine.map(urnOf));
110
+ const removed = new Set([...before].filter((urn) => !after.has(urn)));
111
+ const added = mine.filter((item) => !before.has(urnOf(item)));
112
+ result[field] = refs([...(theirs[field] ?? []).filter((item) => !removed.has(urnOf(item))), ...added]);
113
+ }
114
+ return result;
115
+ }