@weirdfingers/boards 0.6.1 → 0.7.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.
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { UPDATE_BOARD, type UpdateBoardInput } from "../operations";
3
+
4
+ describe("UPDATE_BOARD mutation", () => {
5
+ it("should have correct signature with id inside input", () => {
6
+ const mutationString = UPDATE_BOARD.loc?.source.body || "";
7
+
8
+ // Should have single $input parameter
9
+ expect(mutationString).toContain(
10
+ "mutation UpdateBoard($input: UpdateBoardInput!)"
11
+ );
12
+
13
+ // Should call updateBoard with input only
14
+ expect(mutationString).toContain("updateBoard(input: $input)");
15
+
16
+ // Should NOT have separate $id parameter (the bug from issue #189)
17
+ expect(mutationString).not.toContain("$id: UUID!");
18
+ expect(mutationString).not.toContain("updateBoard(id: $id");
19
+ });
20
+ });
21
+
22
+ describe("UpdateBoardInput interface", () => {
23
+ it("should include id as a required field", () => {
24
+ // TypeScript compile-time check - if this compiles, the interface is correct
25
+ const validInput: UpdateBoardInput = {
26
+ id: "board-123",
27
+ title: "Test",
28
+ };
29
+
30
+ expect(validInput.id).toBe("board-123");
31
+ });
32
+
33
+ it("should allow optional fields", () => {
34
+ // Should compile with just id
35
+ const minimalInput: UpdateBoardInput = {
36
+ id: "board-123",
37
+ };
38
+
39
+ expect(minimalInput.id).toBe("board-123");
40
+
41
+ // Should compile with all fields
42
+ const fullInput: UpdateBoardInput = {
43
+ id: "board-123",
44
+ title: "Test Title",
45
+ description: "Test Description",
46
+ isPublic: true,
47
+ settings: { foo: "bar" },
48
+ metadata: { baz: "qux" },
49
+ };
50
+
51
+ expect(fullInput.title).toBe("Test Title");
52
+ });
53
+ });
@@ -245,8 +245,8 @@ export const CREATE_BOARD = gql`
245
245
 
246
246
  export const UPDATE_BOARD = gql`
247
247
  ${BOARD_FRAGMENT}
248
- mutation UpdateBoard($id: UUID!, $input: UpdateBoardInput!) {
249
- updateBoard(id: $id, input: $input) {
248
+ mutation UpdateBoard($input: UpdateBoardInput!) {
249
+ updateBoard(input: $input) {
250
250
  ...BoardFragment
251
251
  }
252
252
  }
@@ -322,6 +322,12 @@ export const RETRY_GENERATION = gql`
322
322
  }
323
323
  `;
324
324
 
325
+ export const DELETE_GENERATION = gql`
326
+ mutation DeleteGeneration($id: UUID!) {
327
+ deleteGeneration(id: $id)
328
+ }
329
+ `;
330
+
325
331
  export const UPLOAD_ARTIFACT_FROM_URL = gql`
326
332
  ${GENERATION_FRAGMENT}
327
333
  mutation UploadArtifactFromUrl($input: UploadArtifactInput!) {
@@ -341,6 +347,7 @@ export interface CreateBoardInput {
341
347
  }
342
348
 
343
349
  export interface UpdateBoardInput {
350
+ id: string;
344
351
  title?: string;
345
352
  description?: string;
346
353
  isPublic?: boolean;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Tests for useMultiUpload hook.
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from "vitest";
6
+ import { renderHook, waitFor } from "@testing-library/react";
7
+ import { useMultiUpload } from "../useMultiUpload";
8
+ import { ArtifactType } from "../../graphql/operations";
9
+
10
+ // Mock dependencies
11
+ vi.mock("urql", async (importOriginal) => {
12
+ const actual = await importOriginal<typeof import("urql")>();
13
+ return {
14
+ ...actual,
15
+ useMutation: vi.fn(() => [
16
+ {},
17
+ vi.fn().mockResolvedValue({
18
+ data: {
19
+ uploadArtifact: {
20
+ id: "test-artifact-id",
21
+ storageUrl: "https://example.com/artifact.jpg",
22
+ thumbnailUrl: "https://example.com/thumb.jpg",
23
+ artifactType: "IMAGE",
24
+ generatorName: "user-upload-image",
25
+ },
26
+ },
27
+ }),
28
+ ]),
29
+ };
30
+ });
31
+
32
+ vi.mock("../../auth/context", () => ({
33
+ useAuth: vi.fn(() => ({
34
+ getToken: vi.fn().mockResolvedValue("test-token"),
35
+ })),
36
+ }));
37
+
38
+ vi.mock("../../config/ApiConfigContext", () => ({
39
+ useApiConfig: vi.fn(() => ({
40
+ apiUrl: "http://localhost:8088",
41
+ })),
42
+ }));
43
+
44
+ describe("useMultiUpload", () => {
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it("should initialize with empty uploads", () => {
50
+ const { result } = renderHook(() => useMultiUpload());
51
+
52
+ expect(result.current.uploads).toEqual([]);
53
+ expect(result.current.isUploading).toBe(false);
54
+ expect(result.current.overallProgress).toBe(0);
55
+ });
56
+
57
+ it("should handle URL upload via GraphQL", async () => {
58
+ const { result } = renderHook(() => useMultiUpload());
59
+
60
+ const uploadPromise = result.current.uploadMultiple([
61
+ {
62
+ boardId: "test-board-id",
63
+ artifactType: ArtifactType.IMAGE,
64
+ source: "https://example.com/image.jpg",
65
+ },
66
+ ]);
67
+
68
+ // Wait for the upload to start
69
+ await waitFor(() => {
70
+ expect(result.current.uploads.length).toBe(1);
71
+ });
72
+
73
+ expect(result.current.uploads[0].fileName).toBe("image.jpg");
74
+
75
+ // Wait for upload to complete
76
+ const results = await uploadPromise;
77
+
78
+ await waitFor(() => {
79
+ expect(result.current.uploads[0].status).toBe("completed");
80
+ });
81
+
82
+ expect(results).toHaveLength(1);
83
+ expect(results[0].id).toBe("test-artifact-id");
84
+ });
85
+
86
+ it("should handle multiple URL uploads", async () => {
87
+ const { result } = renderHook(() => useMultiUpload());
88
+
89
+ const uploadPromise = result.current.uploadMultiple([
90
+ {
91
+ boardId: "test-board-id",
92
+ artifactType: ArtifactType.IMAGE,
93
+ source: "https://example.com/image1.jpg",
94
+ },
95
+ {
96
+ boardId: "test-board-id",
97
+ artifactType: ArtifactType.IMAGE,
98
+ source: "https://example.com/image2.jpg",
99
+ },
100
+ ]);
101
+
102
+ await waitFor(() => {
103
+ expect(result.current.uploads.length).toBe(2);
104
+ });
105
+
106
+ const results = await uploadPromise;
107
+
108
+ await waitFor(() => {
109
+ expect(result.current.uploads.filter((u) => u.status === "completed")).toHaveLength(2);
110
+ });
111
+
112
+ expect(results).toHaveLength(2);
113
+ });
114
+
115
+ it("should track upload progress", async () => {
116
+ const { result } = renderHook(() => useMultiUpload());
117
+
118
+ result.current.uploadMultiple([
119
+ {
120
+ boardId: "test-board-id",
121
+ artifactType: ArtifactType.IMAGE,
122
+ source: "https://example.com/image.jpg",
123
+ },
124
+ ]);
125
+
126
+ await waitFor(() => {
127
+ expect(result.current.uploads.length).toBeGreaterThan(0);
128
+ });
129
+
130
+ // Initially progress should be 0
131
+ expect(result.current.overallProgress).toBeGreaterThanOrEqual(0);
132
+ });
133
+
134
+ it("should clear uploads", async () => {
135
+ const { result } = renderHook(() => useMultiUpload());
136
+
137
+ await result.current.uploadMultiple([
138
+ {
139
+ boardId: "test-board-id",
140
+ artifactType: ArtifactType.IMAGE,
141
+ source: "https://example.com/image.jpg",
142
+ },
143
+ ]);
144
+
145
+ await waitFor(() => {
146
+ expect(result.current.uploads.length).toBeGreaterThan(0);
147
+ });
148
+
149
+ result.current.clearUploads();
150
+
151
+ await waitFor(() => {
152
+ expect(result.current.uploads).toEqual([]);
153
+ });
154
+ });
155
+
156
+ it("should extract filename from URL", async () => {
157
+ const { result } = renderHook(() => useMultiUpload());
158
+
159
+ result.current.uploadMultiple([
160
+ {
161
+ boardId: "test-board-id",
162
+ artifactType: ArtifactType.IMAGE,
163
+ source: "https://example.com/path/to/my-image.jpg",
164
+ },
165
+ ]);
166
+
167
+ await waitFor(() => {
168
+ expect(result.current.uploads.length).toBe(1);
169
+ });
170
+
171
+ expect(result.current.uploads[0].fileName).toBe("my-image.jpg");
172
+ });
173
+
174
+ it("should handle upload failures gracefully", async () => {
175
+ // Mock a failed mutation
176
+ const { useMutation } = await import("urql");
177
+ vi.mocked(useMutation).mockReturnValueOnce([
178
+ {},
179
+ vi.fn().mockResolvedValue({
180
+ error: new Error("Upload failed"),
181
+ }),
182
+ ]);
183
+
184
+ const { result } = renderHook(() => useMultiUpload());
185
+
186
+ const uploadPromise = result.current.uploadMultiple([
187
+ {
188
+ boardId: "test-board-id",
189
+ artifactType: ArtifactType.IMAGE,
190
+ source: "https://example.com/image.jpg",
191
+ },
192
+ ]);
193
+
194
+ await waitFor(() => {
195
+ expect(result.current.uploads.length).toBe(1);
196
+ });
197
+
198
+ const results = await uploadPromise;
199
+
200
+ // Should return empty array when all uploads fail
201
+ expect(results).toEqual([]);
202
+
203
+ await waitFor(() => {
204
+ expect(result.current.uploads[0].status).toBe("failed");
205
+ });
206
+
207
+ expect(result.current.uploads[0].error).toBeDefined();
208
+ });
209
+
210
+ it("should calculate overall progress correctly", async () => {
211
+ const { result } = renderHook(() => useMultiUpload());
212
+
213
+ result.current.uploadMultiple([
214
+ {
215
+ boardId: "test-board-id",
216
+ artifactType: ArtifactType.IMAGE,
217
+ source: "https://example.com/image1.jpg",
218
+ },
219
+ {
220
+ boardId: "test-board-id",
221
+ artifactType: ArtifactType.IMAGE,
222
+ source: "https://example.com/image2.jpg",
223
+ },
224
+ ]);
225
+
226
+ await waitFor(() => {
227
+ expect(result.current.uploads.length).toBe(2);
228
+ });
229
+
230
+ // Wait for completion
231
+ await waitFor(() => {
232
+ expect(result.current.uploads.filter((u) => u.status === "completed")).toHaveLength(2);
233
+ });
234
+
235
+ // Overall progress should be 100 when all complete
236
+ expect(result.current.overallProgress).toBe(100);
237
+ });
238
+ });
@@ -189,8 +189,10 @@ export function useBoard(
189
189
  }
190
190
 
191
191
  const result = await updateBoardMutation({
192
- id: boardId,
193
- input: updates,
192
+ input: {
193
+ id: boardId,
194
+ ...updates,
195
+ },
194
196
  });
195
197
 
196
198
  if (result.error) {
@@ -9,6 +9,7 @@ import {
9
9
  CREATE_GENERATION,
10
10
  CANCEL_GENERATION,
11
11
  RETRY_GENERATION,
12
+ DELETE_GENERATION,
12
13
  CreateGenerationInput,
13
14
  ArtifactType,
14
15
  } from "../graphql/operations";
@@ -98,6 +99,7 @@ export interface GenerationHook {
98
99
  submit: (request: GenerationRequest) => Promise<string>;
99
100
  cancel: (jobId: string) => Promise<void>;
100
101
  retry: (jobId: string) => Promise<void>;
102
+ deleteGeneration: (jobId: string) => Promise<void>;
101
103
 
102
104
  // History
103
105
  history: GenerationResult[];
@@ -122,6 +124,7 @@ export function useGeneration(): GenerationHook {
122
124
  const [, createGenerationMutation] = useMutation(CREATE_GENERATION);
123
125
  const [, cancelGenerationMutation] = useMutation(CANCEL_GENERATION);
124
126
  const [, retryGenerationMutation] = useMutation(RETRY_GENERATION);
127
+ const [, deleteGenerationMutation] = useMutation(DELETE_GENERATION);
125
128
 
126
129
  // Clean up SSE connections on unmount
127
130
  useEffect(() => {
@@ -411,6 +414,46 @@ export function useGeneration(): GenerationHook {
411
414
  [retryGenerationMutation, connectToSSE]
412
415
  );
413
416
 
417
+ const deleteGeneration = useCallback(
418
+ async (jobId: string): Promise<void> => {
419
+ try {
420
+ // Delete via GraphQL
421
+ const result = await deleteGenerationMutation({ id: jobId });
422
+
423
+ if (result.error) {
424
+ throw new Error(result.error.message);
425
+ }
426
+
427
+ if (!result.data?.deleteGeneration) {
428
+ throw new Error("Failed to delete generation");
429
+ }
430
+
431
+ // Close SSE connection if any
432
+ const controller = abortControllers.current.get(jobId);
433
+ if (controller) {
434
+ controller.abort();
435
+ abortControllers.current.delete(jobId);
436
+ }
437
+
438
+ // Clear state if this was the current generation
439
+ if (progress?.jobId === jobId) {
440
+ setProgress(null);
441
+ setResult(null);
442
+ setIsGenerating(false);
443
+ }
444
+
445
+ // Remove from history
446
+ setHistory((prev) => prev.filter((item) => item.jobId !== jobId));
447
+ } catch (err) {
448
+ setError(
449
+ err instanceof Error ? err : new Error("Failed to delete generation")
450
+ );
451
+ throw err;
452
+ }
453
+ },
454
+ [deleteGenerationMutation, progress]
455
+ );
456
+
414
457
  const clearHistory = useCallback(() => {
415
458
  setHistory([]);
416
459
  }, []);
@@ -423,6 +466,7 @@ export function useGeneration(): GenerationHook {
423
466
  submit,
424
467
  cancel,
425
468
  retry,
469
+ deleteGeneration,
426
470
  history,
427
471
  clearHistory,
428
472
  };