@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.
- package/dist/index.d.mts +24 -9
- package/dist/index.d.ts +24 -9
- package/dist/index.js +197 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +196 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/src/graphql/__tests__/operations.test.ts +53 -0
- package/src/graphql/operations.ts +9 -2
- package/src/hooks/__tests__/useMultiUpload.test.ts +238 -0
- package/src/hooks/useBoard.ts +4 -2
- package/src/hooks/useGeneration.ts +44 -0
- package/src/hooks/useMultiUpload.ts +309 -0
- package/src/index.ts +7 -5
- package/src/hooks/useUpload.ts +0 -175
|
@@ -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($
|
|
249
|
-
updateBoard(
|
|
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
|
+
});
|
package/src/hooks/useBoard.ts
CHANGED
|
@@ -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
|
};
|