@weirdfingers/boards 0.1.4
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 +508 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.js +1150 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1096 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/auth/context.tsx +104 -0
- package/src/auth/hooks/useAuth.ts +6 -0
- package/src/auth/providers/__tests__/none.test.ts +187 -0
- package/src/auth/providers/base.ts +62 -0
- package/src/auth/providers/none.ts +157 -0
- package/src/auth/types.ts +67 -0
- package/src/config/ApiConfigContext.tsx +47 -0
- package/src/graphql/client.ts +130 -0
- package/src/graphql/operations.ts +293 -0
- package/src/hooks/useBoard.ts +323 -0
- package/src/hooks/useBoards.ts +138 -0
- package/src/hooks/useGeneration.ts +429 -0
- package/src/hooks/useGenerators.ts +44 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +25 -0
- package/src/providers/BoardsProvider.tsx +68 -0
- package/src/test-setup.ts +29 -0
- package/tsconfig.json +37 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing a single board.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useMemo } from "react";
|
|
6
|
+
import { useQuery, useMutation } from "urql";
|
|
7
|
+
import { useAuth } from "../auth/hooks/useAuth";
|
|
8
|
+
import {
|
|
9
|
+
GET_BOARD,
|
|
10
|
+
UPDATE_BOARD,
|
|
11
|
+
DELETE_BOARD,
|
|
12
|
+
ADD_BOARD_MEMBER,
|
|
13
|
+
UPDATE_BOARD_MEMBER_ROLE,
|
|
14
|
+
REMOVE_BOARD_MEMBER,
|
|
15
|
+
UpdateBoardInput,
|
|
16
|
+
BoardRole,
|
|
17
|
+
} from "../graphql/operations";
|
|
18
|
+
|
|
19
|
+
interface User {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
displayName: string;
|
|
23
|
+
avatarUrl?: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BoardMember {
|
|
28
|
+
id: string;
|
|
29
|
+
boardId: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
role: BoardRole;
|
|
32
|
+
invitedBy?: string;
|
|
33
|
+
joinedAt: string;
|
|
34
|
+
user: User;
|
|
35
|
+
inviter?: User;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Generation {
|
|
39
|
+
id: string;
|
|
40
|
+
boardId: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
generatorName: string;
|
|
43
|
+
artifactType: string;
|
|
44
|
+
status: string;
|
|
45
|
+
progress: number;
|
|
46
|
+
storageUrl?: string | null;
|
|
47
|
+
thumbnailUrl?: string | null;
|
|
48
|
+
inputParams: Record<string, unknown>;
|
|
49
|
+
outputMetadata: Record<string, unknown>;
|
|
50
|
+
errorMessage?: string | null;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
completedAt?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Board {
|
|
57
|
+
id: string;
|
|
58
|
+
tenantId: string;
|
|
59
|
+
ownerId: string;
|
|
60
|
+
title: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
isPublic: boolean;
|
|
63
|
+
settings: Record<string, unknown>;
|
|
64
|
+
metadata: Record<string, unknown>;
|
|
65
|
+
createdAt: string;
|
|
66
|
+
updatedAt: string;
|
|
67
|
+
generationCount: number;
|
|
68
|
+
owner: User;
|
|
69
|
+
members: BoardMember[];
|
|
70
|
+
generations: Generation[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type MemberRole = BoardRole;
|
|
74
|
+
|
|
75
|
+
interface BoardPermissions {
|
|
76
|
+
canEdit: boolean;
|
|
77
|
+
canDelete: boolean;
|
|
78
|
+
canAddMembers: boolean;
|
|
79
|
+
canRemoveMembers: boolean;
|
|
80
|
+
canGenerate: boolean;
|
|
81
|
+
canExport: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ShareLinkOptions {
|
|
85
|
+
expiresIn?: number;
|
|
86
|
+
permissions?: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ShareLink {
|
|
90
|
+
id: string;
|
|
91
|
+
url: string;
|
|
92
|
+
expiresAt?: string;
|
|
93
|
+
permissions: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface BoardHook {
|
|
97
|
+
board: Board | null;
|
|
98
|
+
members: BoardMember[];
|
|
99
|
+
permissions: BoardPermissions;
|
|
100
|
+
loading: boolean;
|
|
101
|
+
error: Error | null;
|
|
102
|
+
|
|
103
|
+
// Board operations
|
|
104
|
+
updateBoard: (updates: Partial<UpdateBoardInput>) => Promise<Board>;
|
|
105
|
+
deleteBoard: () => Promise<void>;
|
|
106
|
+
refresh: () => Promise<void>;
|
|
107
|
+
|
|
108
|
+
// Member management
|
|
109
|
+
addMember: (email: string, role: MemberRole) => Promise<BoardMember>;
|
|
110
|
+
removeMember: (memberId: string) => Promise<void>;
|
|
111
|
+
updateMemberRole: (
|
|
112
|
+
memberId: string,
|
|
113
|
+
role: MemberRole
|
|
114
|
+
) => Promise<BoardMember>;
|
|
115
|
+
|
|
116
|
+
// Sharing (placeholder - would need backend implementation)
|
|
117
|
+
generateShareLink: (options: ShareLinkOptions) => Promise<ShareLink>;
|
|
118
|
+
revokeShareLink: (linkId: string) => Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useBoard(boardId: string): BoardHook {
|
|
122
|
+
const { user } = useAuth();
|
|
123
|
+
|
|
124
|
+
// Query for board data
|
|
125
|
+
const [{ data, fetching, error }, reexecuteQuery] = useQuery({
|
|
126
|
+
query: GET_BOARD,
|
|
127
|
+
variables: { id: boardId },
|
|
128
|
+
pause: !boardId,
|
|
129
|
+
requestPolicy: "cache-and-network", // Always fetch fresh data while showing cached data
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Mutations
|
|
133
|
+
const [, updateBoardMutation] = useMutation(UPDATE_BOARD);
|
|
134
|
+
const [, deleteBoardMutation] = useMutation(DELETE_BOARD);
|
|
135
|
+
const [, addMemberMutation] = useMutation(ADD_BOARD_MEMBER);
|
|
136
|
+
const [, updateMemberRoleMutation] = useMutation(UPDATE_BOARD_MEMBER_ROLE);
|
|
137
|
+
const [, removeMemberMutation] = useMutation(REMOVE_BOARD_MEMBER);
|
|
138
|
+
|
|
139
|
+
const board = useMemo(() => data?.board || null, [data?.board]);
|
|
140
|
+
const members = useMemo(() => board?.members || [], [board?.members]);
|
|
141
|
+
|
|
142
|
+
// Calculate permissions based on user role
|
|
143
|
+
const permissions = useMemo((): BoardPermissions => {
|
|
144
|
+
if (!board || !user) {
|
|
145
|
+
return {
|
|
146
|
+
canEdit: false,
|
|
147
|
+
canDelete: false,
|
|
148
|
+
canAddMembers: false,
|
|
149
|
+
canRemoveMembers: false,
|
|
150
|
+
canGenerate: false,
|
|
151
|
+
canExport: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if user is the board owner
|
|
156
|
+
const isOwner = board.ownerId === user.id;
|
|
157
|
+
|
|
158
|
+
// Find user's role in board members
|
|
159
|
+
const userMember = members.find(
|
|
160
|
+
(member: BoardMember) => member.userId === user.id
|
|
161
|
+
);
|
|
162
|
+
const userRole = userMember?.role;
|
|
163
|
+
|
|
164
|
+
const isAdmin = userRole === BoardRole.ADMIN;
|
|
165
|
+
const isEditor = userRole === BoardRole.EDITOR || isAdmin;
|
|
166
|
+
const isViewer = userRole === BoardRole.VIEWER || isEditor;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
canEdit: isOwner || isAdmin || isEditor,
|
|
170
|
+
canDelete: isOwner,
|
|
171
|
+
canAddMembers: isOwner || isAdmin,
|
|
172
|
+
canRemoveMembers: isOwner || isAdmin,
|
|
173
|
+
canGenerate: isOwner || isAdmin || isEditor,
|
|
174
|
+
canExport: isViewer, // Even viewers can export
|
|
175
|
+
};
|
|
176
|
+
}, [board, user, members]);
|
|
177
|
+
|
|
178
|
+
const updateBoard = useCallback(
|
|
179
|
+
async (updates: Partial<UpdateBoardInput>): Promise<Board> => {
|
|
180
|
+
if (!boardId) {
|
|
181
|
+
throw new Error("Board ID is required");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = await updateBoardMutation({
|
|
185
|
+
id: boardId,
|
|
186
|
+
input: updates,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (result.error) {
|
|
190
|
+
throw new Error(result.error.message);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!result.data?.updateBoard) {
|
|
194
|
+
throw new Error("Failed to update board");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result.data.updateBoard;
|
|
198
|
+
},
|
|
199
|
+
[boardId, updateBoardMutation]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const deleteBoard = useCallback(async (): Promise<void> => {
|
|
203
|
+
if (!boardId) {
|
|
204
|
+
throw new Error("Board ID is required");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = await deleteBoardMutation({ id: boardId });
|
|
208
|
+
|
|
209
|
+
if (result.error) {
|
|
210
|
+
throw new Error(result.error.message);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!result.data?.deleteBoard?.success) {
|
|
214
|
+
throw new Error("Failed to delete board");
|
|
215
|
+
}
|
|
216
|
+
}, [boardId, deleteBoardMutation]);
|
|
217
|
+
|
|
218
|
+
const addMember = useCallback(
|
|
219
|
+
async (email: string, role: MemberRole): Promise<BoardMember> => {
|
|
220
|
+
if (!boardId) {
|
|
221
|
+
throw new Error("Board ID is required");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await addMemberMutation({
|
|
225
|
+
boardId,
|
|
226
|
+
email,
|
|
227
|
+
role,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (result.error) {
|
|
231
|
+
throw new Error(result.error.message);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!result.data?.addBoardMember) {
|
|
235
|
+
throw new Error("Failed to add member");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Refresh board data to get updated members list
|
|
239
|
+
reexecuteQuery({ requestPolicy: "network-only" });
|
|
240
|
+
|
|
241
|
+
return result.data.addBoardMember;
|
|
242
|
+
},
|
|
243
|
+
[boardId, addMemberMutation, reexecuteQuery]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const removeMember = useCallback(
|
|
247
|
+
async (memberId: string): Promise<void> => {
|
|
248
|
+
const result = await removeMemberMutation({ id: memberId });
|
|
249
|
+
|
|
250
|
+
if (result.error) {
|
|
251
|
+
throw new Error(result.error.message);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!result.data?.removeBoardMember?.success) {
|
|
255
|
+
throw new Error("Failed to remove member");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Refresh board data to get updated members list
|
|
259
|
+
reexecuteQuery({ requestPolicy: "network-only" });
|
|
260
|
+
},
|
|
261
|
+
[removeMemberMutation, reexecuteQuery]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const updateMemberRole = useCallback(
|
|
265
|
+
async (memberId: string, role: MemberRole): Promise<BoardMember> => {
|
|
266
|
+
const result = await updateMemberRoleMutation({
|
|
267
|
+
id: memberId,
|
|
268
|
+
role,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (result.error) {
|
|
272
|
+
throw new Error(result.error.message);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!result.data?.updateBoardMemberRole) {
|
|
276
|
+
throw new Error("Failed to update member role");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Refresh board data to get updated members list
|
|
280
|
+
reexecuteQuery({ requestPolicy: "network-only" });
|
|
281
|
+
|
|
282
|
+
return result.data.updateBoardMemberRole;
|
|
283
|
+
},
|
|
284
|
+
[updateMemberRoleMutation, reexecuteQuery]
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Placeholder implementations for sharing features
|
|
288
|
+
const generateShareLink = useCallback(
|
|
289
|
+
async (_options: ShareLinkOptions): Promise<ShareLink> => {
|
|
290
|
+
// TODO: Implement share link generation
|
|
291
|
+
throw new Error("Share links not implemented yet");
|
|
292
|
+
},
|
|
293
|
+
[]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const revokeShareLink = useCallback(
|
|
297
|
+
async (_linkId: string): Promise<void> => {
|
|
298
|
+
// TODO: Implement share link revocation
|
|
299
|
+
throw new Error("Share link revocation not implemented yet");
|
|
300
|
+
},
|
|
301
|
+
[]
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
305
|
+
await reexecuteQuery({ requestPolicy: "network-only" });
|
|
306
|
+
}, [reexecuteQuery]);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
board,
|
|
310
|
+
members,
|
|
311
|
+
permissions,
|
|
312
|
+
loading: fetching,
|
|
313
|
+
error: error ? new Error(error.message) : null,
|
|
314
|
+
updateBoard,
|
|
315
|
+
deleteBoard,
|
|
316
|
+
refresh,
|
|
317
|
+
addMember,
|
|
318
|
+
removeMember,
|
|
319
|
+
updateMemberRole,
|
|
320
|
+
generateShareLink,
|
|
321
|
+
revokeShareLink,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for managing multiple boards.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useMemo, useState } from "react";
|
|
6
|
+
import { useQuery, useMutation } from "urql";
|
|
7
|
+
// import { Cache } from '@urql/core';
|
|
8
|
+
import {
|
|
9
|
+
GET_BOARDS,
|
|
10
|
+
CREATE_BOARD,
|
|
11
|
+
DELETE_BOARD,
|
|
12
|
+
CreateBoardInput,
|
|
13
|
+
} from "../graphql/operations";
|
|
14
|
+
|
|
15
|
+
interface Board {
|
|
16
|
+
id: string;
|
|
17
|
+
tenantId: string;
|
|
18
|
+
ownerId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
isPublic: boolean;
|
|
22
|
+
settings: Record<string, unknown>;
|
|
23
|
+
metadata: Record<string, unknown>;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
generationCount: number;
|
|
27
|
+
owner: {
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
avatarUrl?: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface UseBoardsOptions {
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface BoardsHook {
|
|
42
|
+
boards: Board[];
|
|
43
|
+
loading: boolean;
|
|
44
|
+
error: Error | null;
|
|
45
|
+
createBoard: (data: CreateBoardInput) => Promise<Board>;
|
|
46
|
+
deleteBoard: (boardId: string) => Promise<void>;
|
|
47
|
+
searchBoards: (query: string) => Promise<Board[]>;
|
|
48
|
+
refresh: () => Promise<void>;
|
|
49
|
+
setSearchQuery: (query: string) => void;
|
|
50
|
+
searchQuery: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useBoards(options: UseBoardsOptions = {}): BoardsHook {
|
|
54
|
+
const { limit = 50, offset = 0 } = options;
|
|
55
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
56
|
+
|
|
57
|
+
// Query for boards
|
|
58
|
+
const [{ data, fetching, error }, reexecuteQuery] = useQuery({
|
|
59
|
+
query: GET_BOARDS,
|
|
60
|
+
variables: { limit, offset },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Mutations
|
|
64
|
+
const [, createBoardMutation] = useMutation(CREATE_BOARD);
|
|
65
|
+
const [, deleteBoardMutation] = useMutation(DELETE_BOARD);
|
|
66
|
+
|
|
67
|
+
const boards = useMemo(() => data?.myBoards || [], [data?.myBoards]);
|
|
68
|
+
|
|
69
|
+
const createBoard = useCallback(
|
|
70
|
+
async (input: CreateBoardInput): Promise<Board> => {
|
|
71
|
+
const result = await createBoardMutation({ input });
|
|
72
|
+
if (result.error) {
|
|
73
|
+
throw new Error(result.error.message);
|
|
74
|
+
}
|
|
75
|
+
if (!result.data?.createBoard) {
|
|
76
|
+
throw new Error("Failed to create board");
|
|
77
|
+
}
|
|
78
|
+
return result.data.createBoard;
|
|
79
|
+
},
|
|
80
|
+
[createBoardMutation]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const deleteBoard = useCallback(
|
|
84
|
+
async (boardId: string): Promise<void> => {
|
|
85
|
+
const result = await deleteBoardMutation({ id: boardId });
|
|
86
|
+
|
|
87
|
+
if (result.error) {
|
|
88
|
+
throw new Error(result.error.message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!result.data?.deleteBoard?.success) {
|
|
92
|
+
throw new Error("Failed to delete board");
|
|
93
|
+
}
|
|
94
|
+
reexecuteQuery({ requestPolicy: "network-only" });
|
|
95
|
+
},
|
|
96
|
+
[deleteBoardMutation, reexecuteQuery]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const searchBoards = useCallback(
|
|
100
|
+
async (query: string): Promise<Board[]> => {
|
|
101
|
+
// Set search query which will trigger debounced search via useEffect
|
|
102
|
+
setSearchQuery(query);
|
|
103
|
+
|
|
104
|
+
// Return promise that resolves when search completes
|
|
105
|
+
// This is a simplified implementation - in a real app you might want
|
|
106
|
+
// to return the actual search results from the API
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
// Wait for debounce delay plus a bit more for API response
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
resolve(
|
|
111
|
+
boards.filter(
|
|
112
|
+
(board: Board) =>
|
|
113
|
+
board.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
114
|
+
board.description?.toLowerCase().includes(query.toLowerCase())
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}, 350);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
[boards]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
124
|
+
await reexecuteQuery({ requestPolicy: "network-only" });
|
|
125
|
+
}, [reexecuteQuery]);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
boards,
|
|
129
|
+
loading: fetching,
|
|
130
|
+
error: error ? new Error(error.message) : null,
|
|
131
|
+
createBoard,
|
|
132
|
+
deleteBoard,
|
|
133
|
+
searchBoards,
|
|
134
|
+
refresh,
|
|
135
|
+
setSearchQuery,
|
|
136
|
+
searchQuery,
|
|
137
|
+
};
|
|
138
|
+
}
|