@critical-path/svelte 0.3.4 → 0.5.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # @critical-path/svelte
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1811922: Add attachment URL validation against large data URIs, sanitize undefined properties in FirebaseStore, provide direct attachment upload routes & SDK methods, and introduce TaskActivityState for combined comment & attachment threads.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [1811922]
12
+ - @critical-path/core@0.8.0
13
+ - @critical-path/client@0.4.0
14
+
15
+ ## 0.4.0
16
+
17
+ ### Minor Changes
18
+
19
+ - a48a04a: Add threaded conversations, attachment metadata management, and storage adapters (S3 & Firebase Storage):
20
+
21
+ - **`@critical-path/core`**:
22
+ - Added `Attachment` entity, `CreateAttachmentInput`, `UploadFileInput`, and `FileStorageAdapter` contracts.
23
+ - Added `InMemoryFileStore`, duck-typed `S3StorageAdapter` (compatible with AWS SDK v3, v2, MinIO, and Cloudflare R2), and `FirebaseStorageAdapter` (compatible with Google Cloud Storage and Firebase Admin SDK).
24
+ - Extended `Comment` with `authorType` (`user`, `agent`, `system`) and `parentId` for threaded discussions.
25
+ - Implemented full comment and attachment repository methods across `InMemoryStore`, `SQLiteStore`, and `FirebaseStore`.
26
+ - Added engine operations for upload, presigning, and deleting attachments with domain events (`comment.*`, `attachment.*`) and webhooks.
27
+
28
+ - **`@critical-path/server`**:
29
+ - Added RESTful routes for comments (`GET/POST /tasks/:taskId/comments`, `GET/POST /comments`, `GET/PATCH/DELETE /comments/:id`).
30
+ - Added RESTful routes for attachments (`GET/POST /tasks/:taskId/attachments`, `GET/POST /attachments`, `GET/DELETE /attachments/:id`, `POST /attachments/presign`).
31
+
32
+ - **`@critical-path/client`**:
33
+ - Added SDK methods `getComments`, `getComment`, `addComment`, `updateComment`, `deleteComment`.
34
+ - Added SDK methods `getAttachments`, `getAttachment`, `createAttachment`, `deleteAttachment`, and `getPresignedAttachmentUploadUrl`.
35
+
36
+ - **`@critical-path/react`**:
37
+ - Added `useComments(taskId)` with reactive thread tree derivation (`threads` containing nested `replies`) and comment mutations.
38
+ - Added `useAttachments(filter)` with attachment creation and deletion.
39
+
40
+ - **`@critical-path/svelte`**:
41
+ - Added Svelte 5 Rune-based `CommentState` / `createCommentState(client, taskId)` with derived threaded hierarchy.
42
+ - Added Svelte 5 Rune-based `AttachmentState` / `createAttachmentState(client, filter)`.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [a48a04a]
47
+ - @critical-path/core@0.7.0
48
+ - @critical-path/client@0.3.0
49
+
3
50
  ## 0.3.4
4
51
 
5
52
  ### Patch Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Svelte 5 Runes Reactive Integrations for Critical Path.**
4
4
 
5
- `@critical-path/svelte` provides Svelte 5 Runes reactive state classes and factories (`ProjectState`, `TaskState`) for building project management UIs in Svelte 5 and SvelteKit applications.
5
+ `@critical-path/svelte` provides Svelte 5 Runes reactive state classes and factories (`ProjectState`, `TaskState`, `WorkflowState`, `CommentState`, `AttachmentState`, `TaskActivityState`) for building project management UIs in Svelte 5 and SvelteKit applications.
6
6
 
7
7
  ---
8
8
 
@@ -16,7 +16,9 @@ pnpm add @critical-path/svelte svelte@^5.0.0
16
16
 
17
17
  ---
18
18
 
19
- ## 💡 Usage Example (Svelte 5 Runes)
19
+ ## 💡 Usage Examples (Svelte 5 Runes)
20
+
21
+ ### 1. Projects & Tasks
20
22
 
21
23
  ```svelte
22
24
  <script lang="ts">
@@ -50,8 +52,56 @@ pnpm add @critical-path/svelte svelte@^5.0.0
50
52
  {/if}
51
53
  ```
52
54
 
55
+ ### 2. Unified Task Activity & Threaded Discussions (`TaskActivityState`)
56
+
57
+ Combines threaded comments with inline attachments (`attachment.commentId === comment.id`) and standalone attachments in a single reactive store:
58
+
59
+ ```svelte
60
+ <script lang="ts">
61
+ import { onMount } from 'svelte';
62
+ import { createCriticalPathClient, createTaskActivityState } from '@critical-path/svelte';
63
+
64
+ const client = createCriticalPathClient({ baseUrl: '/api/critical-path' });
65
+ const activityState = createTaskActivityState(client, 'task_1');
66
+
67
+ onMount(() => {
68
+ activityState.fetch();
69
+ });
70
+
71
+ async function handleSend(text: string, fileUrl?: string) {
72
+ await activityState.addComment(
73
+ { content: text, authorId: 'user_1', authorType: 'user' },
74
+ fileUrl ? [{ filename: 'upload.png', url: fileUrl, uploaderId: 'user_1', mimeType: 'image/png', sizeBytes: 1024 }] : []
75
+ );
76
+ }
77
+ </script>
78
+
79
+ {#each activityState.threads as thread}
80
+ <div class="comment">
81
+ <p><strong>{thread.authorId}</strong>: {thread.content}</p>
82
+
83
+ <!-- Inline comment attachments -->
84
+ {#if thread.attachments.length > 0}
85
+ <div class="attachments">
86
+ {#each thread.attachments as att}
87
+ <a href={att.url} target="_blank">{att.filename}</a>
88
+ {/each}
89
+ </div>
90
+ {/if}
91
+
92
+ <!-- Threaded replies -->
93
+ {#each thread.replies as reply}
94
+ <div class="reply">
95
+ <p>↪ {reply.authorId}: {reply.content}</p>
96
+ </div>
97
+ {/each}
98
+ </div>
99
+ {/each}
100
+ ```
101
+
53
102
  ---
54
103
 
55
104
  ## 📄 License
56
105
 
57
106
  MIT © [Critical Path](https://github.com/Pixerate/Critical-Path)
107
+
@@ -0,0 +1,27 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Comment, Attachment } from '@critical-path/core';
3
+ export interface ThreadedCommentWithAttachments extends Comment {
4
+ attachments: Attachment[];
5
+ replies: ThreadedCommentWithAttachments[];
6
+ }
7
+ export declare class TaskActivityState {
8
+ private client;
9
+ taskId?: string | undefined;
10
+ comments: Comment[];
11
+ attachments: Attachment[];
12
+ loading: boolean;
13
+ error: Error | null;
14
+ threads: ThreadedCommentWithAttachments[];
15
+ standaloneAttachments: Attachment[];
16
+ constructor(client: CriticalPathClient, taskId?: string | undefined);
17
+ fetch(taskId?: string): Promise<void>;
18
+ addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>, attachmentInputs?: Array<Omit<Attachment, 'id' | 'taskId' | 'commentId' | 'createdAt' | 'updatedAt'>>): Promise<{
19
+ comment: Comment;
20
+ attachments: Attachment[];
21
+ }>;
22
+ addAttachment(input: Omit<Attachment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
23
+ deleteComment(id: string): Promise<void>;
24
+ deleteAttachment(id: string): Promise<void>;
25
+ }
26
+ export declare function createTaskActivityState(client: CriticalPathClient, taskId?: string): TaskActivityState;
27
+ //# sourceMappingURL=activity-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-state.svelte.d.ts","sourceRoot":"","sources":["../src/activity-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,WAAW,8BAA+B,SAAQ,OAAO;IAC7D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE,8BAA8B,EAAE,CAAC;CAC3C;AAED,qBAAa,iBAAiB;IA6ChB,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,MAAM;IA5CtE,QAAQ,YAAyB;IACjC,WAAW,eAA4B;IACvC,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,mCAiCJ;IAEH,qBAAqB,eAElB;gBAEiB,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,MAAM,YAAA;IAEhE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM;IAwBrB,UAAU,CACd,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC,EACjE,gBAAgB,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;;;;IAiCjG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;IAelF,aAAa,CAAC,EAAE,EAAE,MAAM;IAWxB,gBAAgB,CAAC,EAAE,EAAE,MAAM;CAUlC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAEtG"}
@@ -0,0 +1,20 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Attachment } from '@critical-path/core';
3
+ export interface AttachmentFilter {
4
+ taskId?: string;
5
+ projectId?: string;
6
+ commentId?: string;
7
+ }
8
+ export declare class AttachmentState {
9
+ private client;
10
+ filter?: AttachmentFilter | undefined;
11
+ data: Attachment[];
12
+ loading: boolean;
13
+ error: Error | null;
14
+ constructor(client: CriticalPathClient, filter?: AttachmentFilter | undefined);
15
+ fetch(filter?: AttachmentFilter): Promise<void>;
16
+ createAttachment(input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
17
+ deleteAttachment(id: string): Promise<void>;
18
+ }
19
+ export declare function createAttachmentState(client: CriticalPathClient, initialFilter?: AttachmentFilter): AttachmentState;
20
+ //# sourceMappingURL=attachment-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-state.svelte.d.ts","sourceRoot":"","sources":["../src/attachment-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,eAAe;IAKd,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,gBAAgB;IAJhF,IAAI,eAA4B;IAChC,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAEf,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,gBAAgB,YAAA;IAE1E,KAAK,CAAC,MAAM,CAAC,EAAE,gBAAgB;IAe/B,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IAY1E,gBAAgB,CAAC,EAAE,EAAE,MAAM;CAUlC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,EAAE,aAAa,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAEnH"}
@@ -0,0 +1,20 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Comment } from '@critical-path/core';
3
+ export interface ThreadedComment extends Comment {
4
+ replies: ThreadedComment[];
5
+ }
6
+ export declare class CommentState {
7
+ private client;
8
+ taskId?: string | undefined;
9
+ data: Comment[];
10
+ loading: boolean;
11
+ error: Error | null;
12
+ threads: ThreadedComment[];
13
+ constructor(client: CriticalPathClient, taskId?: string | undefined);
14
+ fetch(taskId?: string): Promise<void>;
15
+ addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
16
+ updateComment(id: string, updates: Partial<Comment>): Promise<Comment>;
17
+ deleteComment(id: string): Promise<void>;
18
+ }
19
+ export declare function createCommentState(client: CriticalPathClient, taskId?: string): CommentState;
20
+ //# sourceMappingURL=comment-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-state.svelte.d.ts","sourceRoot":"","sources":["../src/comment-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,eAAgB,SAAQ,OAAO;IAC9C,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,qBAAa,YAAY;IAyBX,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,MAAM;IAxBtE,IAAI,YAAyB;IAC7B,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,oBAkBJ;gBAEiB,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,MAAM,YAAA;IAEhE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM;IAkBrB,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;IAe5E,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IAYnD,aAAa,CAAC,EAAE,EAAE,MAAM;CAU/B;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAE5F"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,7 @@ export declare function createCriticalPathClient(options: ClientOptions): Critic
3
3
  export * from './project-state.svelte.js';
4
4
  export * from './task-state.svelte.js';
5
5
  export * from './workflow-state.svelte.js';
6
+ export * from './comment-state.svelte.js';
7
+ export * from './attachment-state.svelte.js';
8
+ export * from './activity-state.svelte.js';
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,kBAAkB,CAEnF;AAED,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,kBAAkB,CAEnF;AAED,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -227,9 +227,297 @@ function s(e) {
227
227
  return new o(e);
228
228
  }
229
229
  //#endregion
230
+ //#region src/comment-state.svelte.ts
231
+ var c = class {
232
+ client;
233
+ taskId;
234
+ #e = t.state(t.proxy([]));
235
+ get data() {
236
+ return t.get(this.#e);
237
+ }
238
+ set data(e) {
239
+ t.set(this.#e, e, !0);
240
+ }
241
+ #t = t.state(!1);
242
+ get loading() {
243
+ return t.get(this.#t);
244
+ }
245
+ set loading(e) {
246
+ t.set(this.#t, e, !0);
247
+ }
248
+ #n = t.state(null);
249
+ get error() {
250
+ return t.get(this.#n);
251
+ }
252
+ set error(e) {
253
+ t.set(this.#n, e, !0);
254
+ }
255
+ #r = t.derived(() => {
256
+ let e = /* @__PURE__ */ new Map(), t = [];
257
+ for (let t of this.data) e.set(t.id, {
258
+ ...t,
259
+ replies: []
260
+ });
261
+ for (let n of this.data) {
262
+ let r = e.get(n.id);
263
+ n.parentId && e.has(n.parentId) ? e.get(n.parentId).replies.push(r) : t.push(r);
264
+ }
265
+ return t;
266
+ });
267
+ get threads() {
268
+ return t.get(this.#r);
269
+ }
270
+ set threads(e) {
271
+ t.set(this.#r, e);
272
+ }
273
+ constructor(e, t) {
274
+ this.client = e, this.taskId = t;
275
+ }
276
+ async fetch(e) {
277
+ let t = e || this.taskId;
278
+ if (!t) {
279
+ this.data = [];
280
+ return;
281
+ }
282
+ this.taskId = t, this.loading = !0, this.error = null;
283
+ try {
284
+ this.data = await this.client.getComments(t);
285
+ } catch (e) {
286
+ this.error = e instanceof Error ? e : Error(String(e));
287
+ } finally {
288
+ this.loading = !1;
289
+ }
290
+ }
291
+ async addComment(e) {
292
+ if (!this.taskId) throw Error("CommentState requires a taskId to add comments.");
293
+ try {
294
+ let t = await this.client.addComment({
295
+ ...e,
296
+ taskId: this.taskId
297
+ });
298
+ return this.data = [...this.data, t], t;
299
+ } catch (e) {
300
+ let t = e instanceof Error ? e : Error(String(e));
301
+ throw this.error = t, t;
302
+ }
303
+ }
304
+ async updateComment(e, t) {
305
+ try {
306
+ let n = await this.client.updateComment(e, t);
307
+ return this.data = this.data.map((t) => t.id === e ? n : t), n;
308
+ } catch (e) {
309
+ let t = e instanceof Error ? e : Error(String(e));
310
+ throw this.error = t, t;
311
+ }
312
+ }
313
+ async deleteComment(e) {
314
+ try {
315
+ await this.client.deleteComment(e), this.data = this.data.filter((t) => t.id !== e);
316
+ } catch (e) {
317
+ let t = e instanceof Error ? e : Error(String(e));
318
+ throw this.error = t, t;
319
+ }
320
+ }
321
+ };
322
+ function l(e, t) {
323
+ return new c(e, t);
324
+ }
325
+ //#endregion
326
+ //#region src/attachment-state.svelte.ts
327
+ var u = class {
328
+ client;
329
+ filter;
330
+ #e = t.state(t.proxy([]));
331
+ get data() {
332
+ return t.get(this.#e);
333
+ }
334
+ set data(e) {
335
+ t.set(this.#e, e, !0);
336
+ }
337
+ #t = t.state(!1);
338
+ get loading() {
339
+ return t.get(this.#t);
340
+ }
341
+ set loading(e) {
342
+ t.set(this.#t, e, !0);
343
+ }
344
+ #n = t.state(null);
345
+ get error() {
346
+ return t.get(this.#n);
347
+ }
348
+ set error(e) {
349
+ t.set(this.#n, e, !0);
350
+ }
351
+ constructor(e, t) {
352
+ this.client = e, this.filter = t;
353
+ }
354
+ async fetch(e) {
355
+ e && (this.filter = e), this.loading = !0, this.error = null;
356
+ try {
357
+ this.data = await this.client.getAttachments(this.filter);
358
+ } catch (e) {
359
+ this.error = e instanceof Error ? e : Error(String(e));
360
+ } finally {
361
+ this.loading = !1;
362
+ }
363
+ }
364
+ async createAttachment(e) {
365
+ try {
366
+ let t = await this.client.createAttachment(e);
367
+ return this.data = [t, ...this.data], t;
368
+ } catch (e) {
369
+ let t = e instanceof Error ? e : Error(String(e));
370
+ throw this.error = t, t;
371
+ }
372
+ }
373
+ async deleteAttachment(e) {
374
+ try {
375
+ await this.client.deleteAttachment(e), this.data = this.data.filter((t) => t.id !== e);
376
+ } catch (e) {
377
+ let t = e instanceof Error ? e : Error(String(e));
378
+ throw this.error = t, t;
379
+ }
380
+ }
381
+ };
382
+ function d(e, t) {
383
+ return new u(e, t);
384
+ }
385
+ //#endregion
386
+ //#region src/activity-state.svelte.ts
387
+ var f = class {
388
+ client;
389
+ taskId;
390
+ #e = t.state(t.proxy([]));
391
+ get comments() {
392
+ return t.get(this.#e);
393
+ }
394
+ set comments(e) {
395
+ t.set(this.#e, e, !0);
396
+ }
397
+ #t = t.state(t.proxy([]));
398
+ get attachments() {
399
+ return t.get(this.#t);
400
+ }
401
+ set attachments(e) {
402
+ t.set(this.#t, e, !0);
403
+ }
404
+ #n = t.state(!1);
405
+ get loading() {
406
+ return t.get(this.#n);
407
+ }
408
+ set loading(e) {
409
+ t.set(this.#n, e, !0);
410
+ }
411
+ #r = t.state(null);
412
+ get error() {
413
+ return t.get(this.#r);
414
+ }
415
+ set error(e) {
416
+ t.set(this.#r, e, !0);
417
+ }
418
+ #i = t.derived(() => {
419
+ let e = /* @__PURE__ */ new Map(), t = [], n = /* @__PURE__ */ new Map();
420
+ for (let e of this.attachments) e.commentId && (n.has(e.commentId) || n.set(e.commentId, []), n.get(e.commentId).push(e));
421
+ for (let t of this.comments) e.set(t.id, {
422
+ ...t,
423
+ attachments: n.get(t.id) || [],
424
+ replies: []
425
+ });
426
+ for (let n of this.comments) {
427
+ let r = e.get(n.id);
428
+ n.parentId && e.has(n.parentId) ? e.get(n.parentId).replies.push(r) : t.push(r);
429
+ }
430
+ return t;
431
+ });
432
+ get threads() {
433
+ return t.get(this.#i);
434
+ }
435
+ set threads(e) {
436
+ t.set(this.#i, e);
437
+ }
438
+ #a = t.derived(() => this.attachments.filter((e) => !e.commentId));
439
+ get standaloneAttachments() {
440
+ return t.get(this.#a);
441
+ }
442
+ set standaloneAttachments(e) {
443
+ t.set(this.#a, e);
444
+ }
445
+ constructor(e, t) {
446
+ this.client = e, this.taskId = t;
447
+ }
448
+ async fetch(e) {
449
+ let t = e || this.taskId;
450
+ if (!t) {
451
+ this.comments = [], this.attachments = [];
452
+ return;
453
+ }
454
+ this.taskId = t, this.loading = !0, this.error = null;
455
+ try {
456
+ let [e, n] = await Promise.all([this.client.getComments(t), this.client.getAttachments({ taskId: t })]);
457
+ this.comments = e, this.attachments = n;
458
+ } catch (e) {
459
+ this.error = e instanceof Error ? e : Error(String(e));
460
+ } finally {
461
+ this.loading = !1;
462
+ }
463
+ }
464
+ async addComment(e, t) {
465
+ if (!this.taskId) throw Error("TaskActivityState requires a taskId to add comments.");
466
+ try {
467
+ let n = await this.client.addComment({
468
+ ...e,
469
+ taskId: this.taskId
470
+ }), r = [];
471
+ return t && t.length > 0 && (r = await Promise.all(t.map((e) => this.client.createAttachment({
472
+ ...e,
473
+ taskId: this.taskId,
474
+ commentId: n.id
475
+ })))), this.comments = [...this.comments, n], r.length > 0 && (this.attachments = [...this.attachments, ...r]), {
476
+ comment: n,
477
+ attachments: r
478
+ };
479
+ } catch (e) {
480
+ let t = e instanceof Error ? e : Error(String(e));
481
+ throw this.error = t, t;
482
+ }
483
+ }
484
+ async addAttachment(e) {
485
+ if (!this.taskId) throw Error("TaskActivityState requires a taskId to add attachments.");
486
+ try {
487
+ let t = await this.client.createAttachment({
488
+ ...e,
489
+ taskId: this.taskId
490
+ });
491
+ return this.attachments = [t, ...this.attachments], t;
492
+ } catch (e) {
493
+ let t = e instanceof Error ? e : Error(String(e));
494
+ throw this.error = t, t;
495
+ }
496
+ }
497
+ async deleteComment(e) {
498
+ try {
499
+ await this.client.deleteComment(e), this.comments = this.comments.filter((t) => t.id !== e);
500
+ } catch (e) {
501
+ let t = e instanceof Error ? e : Error(String(e));
502
+ throw this.error = t, t;
503
+ }
504
+ }
505
+ async deleteAttachment(e) {
506
+ try {
507
+ await this.client.deleteAttachment(e), this.attachments = this.attachments.filter((t) => t.id !== e);
508
+ } catch (e) {
509
+ let t = e instanceof Error ? e : Error(String(e));
510
+ throw this.error = t, t;
511
+ }
512
+ }
513
+ };
514
+ function p(e, t) {
515
+ return new f(e, t);
516
+ }
517
+ //#endregion
230
518
  //#region src/index.ts
231
- function c(t) {
519
+ function m(t) {
232
520
  return new e(t);
233
521
  }
234
522
  //#endregion
235
- export { n as ProjectState, i as TaskState, o as WorkflowState, c as createCriticalPathClient, r as createProjectState, a as createTaskState, s as createWorkflowState };
523
+ export { u as AttachmentState, c as CommentState, n as ProjectState, f as TaskActivityState, i as TaskState, o as WorkflowState, d as createAttachmentState, l as createCommentState, m as createCriticalPathClient, r as createProjectState, p as createTaskActivityState, a as createTaskState, s as createWorkflowState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Svelte 5 Runes state and reactive integrations for Critical Path headless project management framework",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
@@ -18,8 +18,8 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@critical-path/client": "0.2.4",
22
- "@critical-path/core": "0.6.0"
21
+ "@critical-path/client": "0.4.0",
22
+ "@critical-path/core": "0.8.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "svelte": "^5.0.0"
@@ -0,0 +1,156 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Comment, Attachment } from '@critical-path/core';
4
+
5
+ export interface ThreadedCommentWithAttachments extends Comment {
6
+ attachments: Attachment[];
7
+ replies: ThreadedCommentWithAttachments[];
8
+ }
9
+
10
+ export class TaskActivityState {
11
+ comments = $state<Comment[]>([]);
12
+ attachments = $state<Attachment[]>([]);
13
+ loading = $state<boolean>(false);
14
+ error = $state<Error | null>(null);
15
+
16
+ threads = $derived.by(() => {
17
+ const map = new Map<string, ThreadedCommentWithAttachments>();
18
+ const roots: ThreadedCommentWithAttachments[] = [];
19
+
20
+ // Group attachments by commentId
21
+ const attachmentsByComment = new Map<string, Attachment[]>();
22
+ for (const a of this.attachments) {
23
+ if (a.commentId) {
24
+ if (!attachmentsByComment.has(a.commentId)) {
25
+ attachmentsByComment.set(a.commentId, []);
26
+ }
27
+ attachmentsByComment.get(a.commentId)!.push(a);
28
+ }
29
+ }
30
+
31
+ for (const c of this.comments) {
32
+ map.set(c.id, {
33
+ ...c,
34
+ attachments: attachmentsByComment.get(c.id) || [],
35
+ replies: []
36
+ });
37
+ }
38
+
39
+ for (const c of this.comments) {
40
+ const threaded = map.get(c.id)!;
41
+ if (c.parentId && map.has(c.parentId)) {
42
+ map.get(c.parentId)!.replies.push(threaded);
43
+ } else {
44
+ roots.push(threaded);
45
+ }
46
+ }
47
+
48
+ return roots;
49
+ });
50
+
51
+ standaloneAttachments = $derived.by(() => {
52
+ return this.attachments.filter((a) => !a.commentId);
53
+ });
54
+
55
+ constructor(private client: CriticalPathClient, public taskId?: string) {}
56
+
57
+ async fetch(taskId?: string) {
58
+ const targetTaskId = taskId || this.taskId;
59
+ if (!targetTaskId) {
60
+ this.comments = [];
61
+ this.attachments = [];
62
+ return;
63
+ }
64
+ this.taskId = targetTaskId;
65
+ this.loading = true;
66
+ this.error = null;
67
+ try {
68
+ const [comments, attachments] = await Promise.all([
69
+ this.client.getComments(targetTaskId),
70
+ this.client.getAttachments({ taskId: targetTaskId })
71
+ ]);
72
+ this.comments = comments;
73
+ this.attachments = attachments;
74
+ } catch (err) {
75
+ this.error = err instanceof Error ? err : new Error(String(err));
76
+ } finally {
77
+ this.loading = false;
78
+ }
79
+ }
80
+
81
+ async addComment(
82
+ input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>,
83
+ attachmentInputs?: Array<Omit<Attachment, 'id' | 'taskId' | 'commentId' | 'createdAt' | 'updatedAt'>>
84
+ ) {
85
+ if (!this.taskId) {
86
+ throw new Error('TaskActivityState requires a taskId to add comments.');
87
+ }
88
+ try {
89
+ const comment = await this.client.addComment({ ...input, taskId: this.taskId });
90
+ let createdAttachments: Attachment[] = [];
91
+
92
+ if (attachmentInputs && attachmentInputs.length > 0) {
93
+ createdAttachments = await Promise.all(
94
+ attachmentInputs.map((att) =>
95
+ this.client.createAttachment({
96
+ ...att,
97
+ taskId: this.taskId,
98
+ commentId: comment.id
99
+ })
100
+ )
101
+ );
102
+ }
103
+
104
+ this.comments = [...this.comments, comment];
105
+ if (createdAttachments.length > 0) {
106
+ this.attachments = [...this.attachments, ...createdAttachments];
107
+ }
108
+ return { comment, attachments: createdAttachments };
109
+ } catch (err) {
110
+ const errorObj = err instanceof Error ? err : new Error(String(err));
111
+ this.error = errorObj;
112
+ throw errorObj;
113
+ }
114
+ }
115
+
116
+ async addAttachment(input: Omit<Attachment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>) {
117
+ if (!this.taskId) {
118
+ throw new Error('TaskActivityState requires a taskId to add attachments.');
119
+ }
120
+ try {
121
+ const created = await this.client.createAttachment({ ...input, taskId: this.taskId });
122
+ this.attachments = [created, ...this.attachments];
123
+ return created;
124
+ } catch (err) {
125
+ const errorObj = err instanceof Error ? err : new Error(String(err));
126
+ this.error = errorObj;
127
+ throw errorObj;
128
+ }
129
+ }
130
+
131
+ async deleteComment(id: string) {
132
+ try {
133
+ await this.client.deleteComment(id);
134
+ this.comments = this.comments.filter((c) => c.id !== id);
135
+ } catch (err) {
136
+ const errorObj = err instanceof Error ? err : new Error(String(err));
137
+ this.error = errorObj;
138
+ throw errorObj;
139
+ }
140
+ }
141
+
142
+ async deleteAttachment(id: string) {
143
+ try {
144
+ await this.client.deleteAttachment(id);
145
+ this.attachments = this.attachments.filter((a) => a.id !== id);
146
+ } catch (err) {
147
+ const errorObj = err instanceof Error ? err : new Error(String(err));
148
+ this.error = errorObj;
149
+ throw errorObj;
150
+ }
151
+ }
152
+ }
153
+
154
+ export function createTaskActivityState(client: CriticalPathClient, taskId?: string): TaskActivityState {
155
+ return new TaskActivityState(client, taskId);
156
+ }
@@ -0,0 +1,59 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Attachment } from '@critical-path/core';
4
+
5
+ export interface AttachmentFilter {
6
+ taskId?: string;
7
+ projectId?: string;
8
+ commentId?: string;
9
+ }
10
+
11
+ export class AttachmentState {
12
+ data = $state<Attachment[]>([]);
13
+ loading = $state<boolean>(false);
14
+ error = $state<Error | null>(null);
15
+
16
+ constructor(private client: CriticalPathClient, public filter?: AttachmentFilter) {}
17
+
18
+ async fetch(filter?: AttachmentFilter) {
19
+ if (filter) {
20
+ this.filter = filter;
21
+ }
22
+ this.loading = true;
23
+ this.error = null;
24
+ try {
25
+ this.data = await this.client.getAttachments(this.filter);
26
+ } catch (err) {
27
+ this.error = err instanceof Error ? err : new Error(String(err));
28
+ } finally {
29
+ this.loading = false;
30
+ }
31
+ }
32
+
33
+ async createAttachment(input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>) {
34
+ try {
35
+ const created = await this.client.createAttachment(input);
36
+ this.data = [created, ...this.data];
37
+ return created;
38
+ } catch (err) {
39
+ const errorObj = err instanceof Error ? err : new Error(String(err));
40
+ this.error = errorObj;
41
+ throw errorObj;
42
+ }
43
+ }
44
+
45
+ async deleteAttachment(id: string) {
46
+ try {
47
+ await this.client.deleteAttachment(id);
48
+ this.data = this.data.filter((a) => a.id !== id);
49
+ } catch (err) {
50
+ const errorObj = err instanceof Error ? err : new Error(String(err));
51
+ this.error = errorObj;
52
+ throw errorObj;
53
+ }
54
+ }
55
+ }
56
+
57
+ export function createAttachmentState(client: CriticalPathClient, initialFilter?: AttachmentFilter): AttachmentState {
58
+ return new AttachmentState(client, initialFilter);
59
+ }
@@ -0,0 +1,95 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Comment } from '@critical-path/core';
4
+
5
+ export interface ThreadedComment extends Comment {
6
+ replies: ThreadedComment[];
7
+ }
8
+
9
+ export class CommentState {
10
+ data = $state<Comment[]>([]);
11
+ loading = $state<boolean>(false);
12
+ error = $state<Error | null>(null);
13
+
14
+ threads = $derived.by(() => {
15
+ const map = new Map<string, ThreadedComment>();
16
+ const roots: ThreadedComment[] = [];
17
+
18
+ for (const c of this.data) {
19
+ map.set(c.id, { ...c, replies: [] });
20
+ }
21
+
22
+ for (const c of this.data) {
23
+ const threaded = map.get(c.id)!;
24
+ if (c.parentId && map.has(c.parentId)) {
25
+ map.get(c.parentId)!.replies.push(threaded);
26
+ } else {
27
+ roots.push(threaded);
28
+ }
29
+ }
30
+
31
+ return roots;
32
+ });
33
+
34
+ constructor(private client: CriticalPathClient, public taskId?: string) {}
35
+
36
+ async fetch(taskId?: string) {
37
+ const targetTaskId = taskId || this.taskId;
38
+ if (!targetTaskId) {
39
+ this.data = [];
40
+ return;
41
+ }
42
+ this.taskId = targetTaskId;
43
+ this.loading = true;
44
+ this.error = null;
45
+ try {
46
+ this.data = await this.client.getComments(targetTaskId);
47
+ } catch (err) {
48
+ this.error = err instanceof Error ? err : new Error(String(err));
49
+ } finally {
50
+ this.loading = false;
51
+ }
52
+ }
53
+
54
+ async addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>) {
55
+ if (!this.taskId) {
56
+ throw new Error('CommentState requires a taskId to add comments.');
57
+ }
58
+ try {
59
+ const created = await this.client.addComment({ ...input, taskId: this.taskId });
60
+ this.data = [...this.data, created];
61
+ return created;
62
+ } catch (err) {
63
+ const errorObj = err instanceof Error ? err : new Error(String(err));
64
+ this.error = errorObj;
65
+ throw errorObj;
66
+ }
67
+ }
68
+
69
+ async updateComment(id: string, updates: Partial<Comment>) {
70
+ try {
71
+ const updated = await this.client.updateComment(id, updates);
72
+ this.data = this.data.map((c) => (c.id === id ? updated : c));
73
+ return updated;
74
+ } catch (err) {
75
+ const errorObj = err instanceof Error ? err : new Error(String(err));
76
+ this.error = errorObj;
77
+ throw errorObj;
78
+ }
79
+ }
80
+
81
+ async deleteComment(id: string) {
82
+ try {
83
+ await this.client.deleteComment(id);
84
+ this.data = this.data.filter((c) => c.id !== id);
85
+ } catch (err) {
86
+ const errorObj = err instanceof Error ? err : new Error(String(err));
87
+ this.error = errorObj;
88
+ throw errorObj;
89
+ }
90
+ }
91
+ }
92
+
93
+ export function createCommentState(client: CriticalPathClient, taskId?: string): CommentState {
94
+ return new CommentState(client, taskId);
95
+ }
package/src/index.test.ts CHANGED
@@ -6,10 +6,16 @@ import {
6
6
  TaskState,
7
7
  createTaskState,
8
8
  WorkflowState,
9
- createWorkflowState
9
+ createWorkflowState,
10
+ CommentState,
11
+ createCommentState,
12
+ AttachmentState,
13
+ createAttachmentState,
14
+ TaskActivityState,
15
+ createTaskActivityState
10
16
  } from './index.js';
11
17
  import type { CriticalPathClient } from '@critical-path/client';
12
- import type { Project, Task, Workflow } from '@critical-path/core';
18
+ import type { Project, Task, Workflow, Comment, Attachment } from '@critical-path/core';
13
19
 
14
20
  describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
15
21
  it('exports client factory and Svelte 5 Runes state factories', () => {
@@ -17,9 +23,15 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
17
23
  expect(createProjectState).toBeDefined();
18
24
  expect(createTaskState).toBeDefined();
19
25
  expect(createWorkflowState).toBeDefined();
26
+ expect(createCommentState).toBeDefined();
27
+ expect(createAttachmentState).toBeDefined();
28
+ expect(createTaskActivityState).toBeDefined();
20
29
  expect(ProjectState).toBeDefined();
21
30
  expect(TaskState).toBeDefined();
22
31
  expect(WorkflowState).toBeDefined();
32
+ expect(CommentState).toBeDefined();
33
+ expect(AttachmentState).toBeDefined();
34
+ expect(TaskActivityState).toBeDefined();
23
35
  });
24
36
 
25
37
  describe('WorkflowState', () => {
@@ -185,4 +197,114 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
185
197
  expect(taskState.data).toEqual([mockTask]);
186
198
  });
187
199
  });
200
+
201
+ describe('CommentState', () => {
202
+ it('fetches, creates, updates, and deletes comments', async () => {
203
+ const mockComment = {
204
+ id: 'cmt_1',
205
+ taskId: 'task_1',
206
+ content: 'Root comment',
207
+ authorId: 'u1',
208
+ authorType: 'user' as const,
209
+ createdAt: '2026-01-01',
210
+ updatedAt: '2026-01-01'
211
+ };
212
+ const updatedComment = { ...mockComment, content: 'Updated comment' };
213
+
214
+ const mockClient = {
215
+ getComments: vi.fn().mockResolvedValue([mockComment]),
216
+ addComment: vi.fn().mockResolvedValue(mockComment),
217
+ updateComment: vi.fn().mockResolvedValue(updatedComment),
218
+ deleteComment: vi.fn().mockResolvedValue(true)
219
+ } as unknown as CriticalPathClient;
220
+
221
+ const commentState = new CommentState(mockClient, 'task_1');
222
+ await commentState.fetch();
223
+
224
+ expect(commentState.data).toEqual([mockComment]);
225
+
226
+ await commentState.updateComment('cmt_1', { content: 'Updated comment' });
227
+ expect(commentState.data[0].content).toBe('Updated comment');
228
+
229
+ await commentState.deleteComment('cmt_1');
230
+ expect(commentState.data).toEqual([]);
231
+ });
232
+ });
233
+
234
+ describe('AttachmentState', () => {
235
+ it('fetches, creates, and deletes attachments', async () => {
236
+ const mockAttachment = {
237
+ id: 'att_1',
238
+ filename: 'spec.pdf',
239
+ mimeType: 'application/pdf',
240
+ sizeBytes: 1024,
241
+ url: 'https://storage.example.com/spec.pdf',
242
+ uploaderId: 'u1',
243
+ uploaderType: 'user' as const,
244
+ createdAt: '2026-01-01',
245
+ updatedAt: '2026-01-01'
246
+ };
247
+
248
+ const mockClient = {
249
+ getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
250
+ createAttachment: vi.fn().mockResolvedValue(mockAttachment),
251
+ deleteAttachment: vi.fn().mockResolvedValue(true)
252
+ } as unknown as CriticalPathClient;
253
+
254
+ const attachmentState = new AttachmentState(mockClient, { taskId: 'task_1' });
255
+ await attachmentState.fetch();
256
+
257
+ expect(attachmentState.data).toEqual([mockAttachment]);
258
+
259
+ await attachmentState.deleteAttachment('att_1');
260
+ expect(attachmentState.data).toEqual([]);
261
+ });
262
+ });
263
+
264
+ describe('TaskActivityState', () => {
265
+ it('fetches comments and attachments and unifies them into threads with attachments', async () => {
266
+ const mockComment: Comment = {
267
+ id: 'cmt_1',
268
+ taskId: 'task_1',
269
+ content: 'Comment with attachment',
270
+ authorId: 'u1',
271
+ authorType: 'user',
272
+ createdAt: '2026-01-01',
273
+ updatedAt: '2026-01-01'
274
+ };
275
+
276
+ const mockAttachment: Attachment = {
277
+ id: 'att_1',
278
+ taskId: 'task_1',
279
+ commentId: 'cmt_1',
280
+ filename: 'screenshot.png',
281
+ mimeType: 'image/png',
282
+ sizeBytes: 2048,
283
+ url: 'https://storage.example.com/screenshot.png',
284
+ uploaderId: 'u1',
285
+ uploaderType: 'user',
286
+ createdAt: '2026-01-01',
287
+ updatedAt: '2026-01-01'
288
+ };
289
+
290
+ const mockClient = {
291
+ getComments: vi.fn().mockResolvedValue([mockComment]),
292
+ getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
293
+ addComment: vi.fn().mockResolvedValue(mockComment),
294
+ createAttachment: vi.fn().mockResolvedValue(mockAttachment),
295
+ deleteComment: vi.fn().mockResolvedValue(true),
296
+ deleteAttachment: vi.fn().mockResolvedValue(true)
297
+ } as unknown as CriticalPathClient;
298
+
299
+ const activityState = createTaskActivityState(mockClient, 'task_1');
300
+ await activityState.fetch();
301
+
302
+ expect(activityState.comments).toEqual([mockComment]);
303
+ expect(activityState.attachments).toEqual([mockAttachment]);
304
+ expect(activityState.threads).toHaveLength(1);
305
+ expect(activityState.threads[0].attachments).toEqual([mockAttachment]);
306
+ expect(activityState.standaloneAttachments).toHaveLength(0);
307
+ });
308
+ });
188
309
  });
310
+
package/src/index.ts CHANGED
@@ -7,3 +7,7 @@ export function createCriticalPathClient(options: ClientOptions): CriticalPathCl
7
7
  export * from './project-state.svelte.js';
8
8
  export * from './task-state.svelte.js';
9
9
  export * from './workflow-state.svelte.js';
10
+ export * from './comment-state.svelte.js';
11
+ export * from './attachment-state.svelte.js';
12
+ export * from './activity-state.svelte.js';
13
+