convex-feedback 0.1.4 → 0.2.0-beta.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.
Files changed (79) hide show
  1. package/README.md +87 -27
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +80 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/config.d.ts +1 -1
  6. package/dist/client/index.d.ts +9 -5
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +261 -8
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/component/_generated/api.d.ts +8 -0
  11. package/dist/component/_generated/api.d.ts.map +1 -1
  12. package/dist/component/_generated/api.js.map +1 -1
  13. package/dist/component/_generated/component.d.ts +295 -5
  14. package/dist/component/_generated/component.d.ts.map +1 -1
  15. package/dist/component/admin.d.ts +130 -0
  16. package/dist/component/admin.d.ts.map +1 -0
  17. package/dist/component/admin.js +125 -0
  18. package/dist/component/admin.js.map +1 -0
  19. package/dist/component/comments.d.ts +7 -5
  20. package/dist/component/comments.d.ts.map +1 -1
  21. package/dist/component/comments.js +4 -3
  22. package/dist/component/comments.js.map +1 -1
  23. package/dist/component/crons.d.ts +3 -0
  24. package/dist/component/crons.d.ts.map +1 -0
  25. package/dist/component/crons.js +8 -0
  26. package/dist/component/crons.js.map +1 -0
  27. package/dist/component/entries.d.ts +29 -16
  28. package/dist/component/entries.d.ts.map +1 -1
  29. package/dist/component/entries.js +136 -12
  30. package/dist/component/entries.js.map +1 -1
  31. package/dist/component/helpers.d.ts +3 -1
  32. package/dist/component/helpers.d.ts.map +1 -1
  33. package/dist/component/helpers.js +28 -0
  34. package/dist/component/helpers.js.map +1 -1
  35. package/dist/component/migrations.d.ts +11 -0
  36. package/dist/component/migrations.d.ts.map +1 -0
  37. package/dist/component/migrations.js +35 -0
  38. package/dist/component/migrations.js.map +1 -0
  39. package/dist/component/model.d.ts +144 -24
  40. package/dist/component/model.d.ts.map +1 -1
  41. package/dist/component/model.js +34 -1
  42. package/dist/component/model.js.map +1 -1
  43. package/dist/component/roadmap.d.ts +166 -0
  44. package/dist/component/roadmap.d.ts.map +1 -0
  45. package/dist/component/roadmap.js +534 -0
  46. package/dist/component/roadmap.js.map +1 -0
  47. package/dist/component/schema.d.ts +67 -5
  48. package/dist/component/schema.d.ts.map +1 -1
  49. package/dist/component/schema.js +57 -2
  50. package/dist/component/schema.js.map +1 -1
  51. package/dist/component/tags.d.ts +58 -0
  52. package/dist/component/tags.d.ts.map +1 -0
  53. package/dist/component/tags.js +216 -0
  54. package/dist/component/tags.js.map +1 -0
  55. package/dist/react/index.d.ts +173 -12
  56. package/dist/react/index.d.ts.map +1 -1
  57. package/dist/react/index.js +78 -1
  58. package/dist/react/index.js.map +1 -1
  59. package/dist/test.d.ts +67 -5
  60. package/dist/test.d.ts.map +1 -1
  61. package/package.json +6 -2
  62. package/src/client/README.md +1 -1
  63. package/src/client/api.ts +183 -0
  64. package/src/client/config.ts +1 -1
  65. package/src/client/index.ts +340 -13
  66. package/src/component/README.md +3 -1
  67. package/src/component/_generated/api.ts +8 -0
  68. package/src/component/_generated/component.ts +373 -9
  69. package/src/component/admin.ts +180 -0
  70. package/src/component/comments.ts +5 -2
  71. package/src/component/crons.ts +16 -0
  72. package/src/component/entries.ts +175 -11
  73. package/src/component/helpers.ts +44 -1
  74. package/src/component/migrations.ts +44 -0
  75. package/src/component/model.ts +79 -4
  76. package/src/component/roadmap.ts +627 -0
  77. package/src/component/schema.ts +61 -1
  78. package/src/react/README.md +2 -2
  79. package/src/react/index.ts +152 -4
@@ -3,8 +3,11 @@ import { v } from "convex/values";
3
3
 
4
4
  import {
5
5
  entryKindValidator,
6
+ entryPriorityValidator,
7
+ entryStatusFilterValidator,
6
8
  entryStatusValidator,
7
9
  feedbackMetadataValidator,
10
+ roadmapStatusValidator,
8
11
  } from "./model.js";
9
12
 
10
13
  const schema = defineSchema({
@@ -12,6 +15,7 @@ const schema = defineSchema({
12
15
  actorId: v.string(),
13
16
  kind: entryKindValidator,
14
17
  status: entryStatusValidator,
18
+ statusFilter: v.optional(entryStatusFilterValidator),
15
19
  title: v.string(),
16
20
  body: v.string(),
17
21
  normalizedTitle: v.string(),
@@ -20,21 +24,77 @@ const schema = defineSchema({
20
24
  commentCount: v.number(),
21
25
  updatedAt: v.optional(v.number()),
22
26
  metadata: v.optional(feedbackMetadataValidator),
27
+ priority: v.optional(entryPriorityValidator),
28
+ roadmapId: v.optional(v.id("roadmap")),
23
29
  })
24
30
  .index("by_kind", ["kind"])
25
31
  .index("by_status", ["status"])
26
32
  .index("by_kind_status", ["kind", "status"])
33
+ .index("by_status_filter", ["statusFilter"])
34
+ .index("by_kind_status_filter", ["kind", "statusFilter"])
27
35
  .index("by_upvotes", ["upvoteCount"])
28
36
  .index("by_kind_upvotes", ["kind", "upvoteCount"])
29
37
  .index("by_status_upvotes", ["status", "upvoteCount"])
30
38
  .index("by_kind_status_upvotes", ["kind", "status", "upvoteCount"])
39
+ .index("by_status_filter_upvotes", ["statusFilter", "upvoteCount"])
40
+ .index("by_kind_status_filter_upvotes", [
41
+ "kind",
42
+ "statusFilter",
43
+ "upvoteCount",
44
+ ])
31
45
  .index("by_normalized_title", ["normalizedTitle"])
32
46
  .index("by_kind_normalized_title", ["kind", "normalizedTitle"])
47
+ .index("by_priority", ["priority"])
48
+ .index("by_roadmap_id", ["roadmapId"])
33
49
  .searchIndex("search", {
34
50
  searchField: "searchText",
35
- filterFields: ["kind", "status"],
51
+ filterFields: ["kind", "status", "statusFilter", "priority"],
36
52
  }),
37
53
 
54
+ roadmap: defineTable({
55
+ title: v.string(),
56
+ description: v.optional(v.string()),
57
+ status: roadmapStatusValidator,
58
+ position: v.number(),
59
+ feedbackCount: v.number(),
60
+ createdAt: v.number(),
61
+ updatedAt: v.number(),
62
+ deletingAt: v.optional(v.number()),
63
+ rebalanceId: v.optional(v.string()),
64
+ rebalanceRank: v.optional(v.number()),
65
+ rebalancePosition: v.optional(v.number()),
66
+ })
67
+ .index("by_status_and_position", ["status", "position"])
68
+ .index("by_status_deleting_at_position", [
69
+ "status",
70
+ "deletingAt",
71
+ "position",
72
+ ])
73
+ .index("by_deleting_at_and_position", ["deletingAt", "position"])
74
+ .index("by_status_rebalance_rank", [
75
+ "status",
76
+ "rebalanceId",
77
+ "rebalanceRank",
78
+ ])
79
+ .index("by_status_deleting_at_rebalance_id_position", [
80
+ "status",
81
+ "deletingAt",
82
+ "rebalanceId",
83
+ "rebalancePosition",
84
+ ])
85
+ .index("by_position", ["position"])
86
+ .searchIndex("search_title", {
87
+ searchField: "title",
88
+ filterFields: ["status", "deletingAt"],
89
+ }),
90
+
91
+ roadmapRebalances: defineTable({
92
+ status: roadmapStatusValidator,
93
+ nextGeneration: v.number(),
94
+ visibleGeneration: v.optional(v.string()),
95
+ activeGeneration: v.optional(v.string()),
96
+ }).index("by_status", ["status"]),
97
+
38
98
  comments: defineTable({
39
99
  entryId: v.id("entries"),
40
100
  parentCommentId: v.optional(v.id("comments")),
@@ -1,7 +1,7 @@
1
1
  # React hooks layer
2
2
 
3
- `createFeedbackHooks(api, options?)` binds the host application's public Convex API to headless React hooks.
3
+ `createFeedbackHooks(api?, options?)` binds the host application's public Convex API to headless React hooks. With no API argument it uses `anyApi.feedback`; pass a generated API namespace when the host module has another name.
4
4
 
5
- The hooks expose entries, search/similarity, lazy direct-child comment pagination, mutations, and the configured client page sizes. Reply queries are not mounted by the hook layer automatically; the UI decides when a comment is expanded.
5
+ The hooks expose public entries, search/similarity, lazy direct-child comment pagination, mutations, admin inbox queries, and roadmap operations. Reply queries are not mounted by the hook layer automatically; the UI decides when a comment is expanded.
6
6
 
7
7
  Page-size defaults are client concerns. The server-side component still enforces its configured hard maximum. Keep the two aligned when overriding defaults.
@@ -2,13 +2,17 @@
2
2
 
3
3
  import { usePaginatedQuery } from "convex-helpers/react";
4
4
  import { useMutation, useQuery } from "convex/react";
5
+ import { anyApi } from "convex/server";
5
6
 
6
7
  import type { FeedbackPublicApi } from "../client/api.js";
7
8
  import type {
8
9
  CommentSort,
9
10
  EntryKind,
11
+ EntryPriority,
10
12
  EntrySort,
11
13
  EntryStatus,
14
+ EntryStatusFilter,
15
+ RoadmapStatus,
12
16
  SimilarEntriesResult,
13
17
  } from "../component/model.js";
14
18
 
@@ -22,7 +26,8 @@ import type {
22
26
  */
23
27
  export interface FeedbackHooksOptions {
24
28
  /**
25
- * Number of entries initially requested by `useEntries`.
29
+ * Number of entries initially requested by entry-list hooks, including the
30
+ * public board, admin inbox, admin search, and roadmap attachments.
26
31
  *
27
32
  * @default 20
28
33
  */
@@ -42,6 +47,13 @@ export interface FeedbackHooksOptions {
42
47
  * @default 10
43
48
  */
44
49
  replyPageSize?: number;
50
+
51
+ /**
52
+ * Number of roadmap items initially requested by `useRoadmap`.
53
+ *
54
+ * @default 30
55
+ */
56
+ roadmapPageSize?: number;
45
57
  }
46
58
 
47
59
  /**
@@ -61,6 +73,9 @@ export interface UseEntriesArgs {
61
73
  /** Optional workflow-status filter. */
62
74
  status?: EntryStatus;
63
75
 
76
+ /** Optional indexed open or closed board bucket. */
77
+ statusFilter?: EntryStatusFilter;
78
+
64
79
  /**
65
80
  * Server-side entry ordering.
66
81
  *
@@ -120,6 +135,9 @@ export interface SearchEntriesArgs {
120
135
  /** Optional workflow-status filter. */
121
136
  status?: EntryStatus;
122
137
 
138
+ /** Optional indexed open or closed board bucket. */
139
+ statusFilter?: EntryStatusFilter;
140
+
123
141
  /**
124
142
  * Maximum number of results requested.
125
143
  *
@@ -129,6 +147,16 @@ export interface SearchEntriesArgs {
129
147
  limit?: number;
130
148
  }
131
149
 
150
+ export interface UseAdminEntriesArgs {
151
+ kinds?: readonly EntryKind[];
152
+ status?: EntryStatus;
153
+ priority?: EntryPriority;
154
+ }
155
+
156
+ export interface UseAdminSearchEntriesArgs extends UseAdminEntriesArgs {
157
+ searchQuery: string;
158
+ }
159
+
132
160
  /**
133
161
  * Arguments accepted by `useSimilarEntries`.
134
162
  */
@@ -193,6 +221,7 @@ function createFeedbackHooksImplementation<RateLimitResult>(
193
221
  const entryPageSize = positivePageSize(options.entryPageSize, 20);
194
222
  const commentPageSize = positivePageSize(options.commentPageSize, 20);
195
223
  const replyPageSize = positivePageSize(options.replyPageSize, 10);
224
+ const roadmapPageSize = positivePageSize(options.roadmapPageSize, 30);
196
225
 
197
226
  return {
198
227
  /**
@@ -203,6 +232,7 @@ function createFeedbackHooksImplementation<RateLimitResult>(
203
232
  entries: entryPageSize,
204
233
  comments: commentPageSize,
205
234
  replies: replyPageSize,
235
+ roadmap: roadmapPageSize,
206
236
  } as const,
207
237
 
208
238
  /**
@@ -212,6 +242,9 @@ function createFeedbackHooksImplementation<RateLimitResult>(
212
242
  const queryArgs = {
213
243
  ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
214
244
  ...(args.status === undefined ? {} : { status: args.status }),
245
+ ...(args.statusFilter === undefined
246
+ ? {}
247
+ : { statusFilter: args.statusFilter }),
215
248
  ...(args.sort === undefined ? {} : { sort: args.sort }),
216
249
  };
217
250
 
@@ -244,6 +277,9 @@ function createFeedbackHooksImplementation<RateLimitResult>(
244
277
  searchQuery,
245
278
  ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
246
279
  ...(args.status === undefined ? {} : { status: args.status }),
280
+ ...(args.statusFilter === undefined
281
+ ? {}
282
+ : { statusFilter: args.statusFilter }),
247
283
  ...(args.limit === undefined ? {} : { limit: args.limit }),
248
284
  };
249
285
 
@@ -255,6 +291,80 @@ function createFeedbackHooksImplementation<RateLimitResult>(
255
291
  return searchQuery.length === 0 ? [] : result;
256
292
  },
257
293
 
294
+ /** Returns whether the authenticated host actor is an admin. */
295
+ useIsAdmin() {
296
+ return useQuery(api.isAdmin, {});
297
+ },
298
+
299
+ /** Returns whether the current request has an authenticated actor. */
300
+ useIsAuthenticated() {
301
+ return useQuery(api.isAuthenticated, {});
302
+ },
303
+
304
+ /** Returns a cursor-paginated admin inbox with private triage relationships. */
305
+ useAdminEntries(args: UseAdminEntriesArgs = {}) {
306
+ return usePaginatedQuery(
307
+ api.adminListEntries,
308
+ {
309
+ ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
310
+ ...(args.status === undefined ? {} : { status: args.status }),
311
+ ...(args.priority === undefined ? {} : { priority: args.priority }),
312
+ },
313
+ { initialNumItems: entryPageSize },
314
+ );
315
+ },
316
+
317
+ /** Returns one admin-enriched feedback entry. */
318
+ useAdminEntry(entryId: string | null | undefined) {
319
+ return useQuery(
320
+ api.adminGetEntry,
321
+ entryId === null || entryId === undefined ? "skip" : { entryId },
322
+ );
323
+ },
324
+
325
+ /** Searches the admin inbox with triage filters. */
326
+ useAdminSearchEntries(args: UseAdminSearchEntriesArgs) {
327
+ const searchQuery = args.searchQuery.trim();
328
+ return usePaginatedQuery(
329
+ api.adminSearchEntries,
330
+ searchQuery.length === 0
331
+ ? "skip"
332
+ : {
333
+ searchQuery,
334
+ ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
335
+ ...(args.status === undefined ? {} : { status: args.status }),
336
+ ...(args.priority === undefined
337
+ ? {}
338
+ : { priority: args.priority }),
339
+ },
340
+ { initialNumItems: entryPageSize },
341
+ );
342
+ },
343
+
344
+ useRoadmap(status?: RoadmapStatus) {
345
+ return usePaginatedQuery(
346
+ api.listRoadmap,
347
+ status === undefined ? {} : { status },
348
+ { initialNumItems: roadmapPageSize },
349
+ );
350
+ },
351
+
352
+ useSearchRoadmap(searchQuery: string, limit = 10) {
353
+ const normalized = searchQuery.trim();
354
+ return useQuery(
355
+ api.searchRoadmap,
356
+ normalized.length === 0 ? "skip" : { searchQuery: normalized, limit },
357
+ );
358
+ },
359
+
360
+ useRoadmapFeedback(roadmapId: string | null | undefined) {
361
+ return usePaginatedQuery(
362
+ api.listRoadmapFeedback,
363
+ roadmapId === null || roadmapId === undefined ? "skip" : { roadmapId },
364
+ { initialNumItems: entryPageSize },
365
+ );
366
+ },
367
+
258
368
  /**
259
369
  * Reactively finds exact and similar entries for a proposed draft.
260
370
  *
@@ -307,6 +417,38 @@ function createFeedbackHooksImplementation<RateLimitResult>(
307
417
  return useMutation(api.setEntryStatus);
308
418
  },
309
419
 
420
+ useSetEntryPriority() {
421
+ return useMutation(api.setEntryPriority);
422
+ },
423
+
424
+ useCreateRoadmap() {
425
+ return useMutation(api.createRoadmap);
426
+ },
427
+
428
+ useCreateRoadmapForEntry() {
429
+ return useMutation(api.createRoadmapForEntry);
430
+ },
431
+
432
+ useUpdateRoadmap() {
433
+ return useMutation(api.updateRoadmap);
434
+ },
435
+
436
+ useDeleteRoadmap() {
437
+ return useMutation(api.deleteRoadmap);
438
+ },
439
+
440
+ useMoveRoadmapItem() {
441
+ return useMutation(api.moveRoadmapItem);
442
+ },
443
+
444
+ useAttachFeedbackToRoadmap() {
445
+ return useMutation(api.attachFeedbackToRoadmap);
446
+ },
447
+
448
+ useDetachFeedbackFromRoadmap() {
449
+ return useMutation(api.detachFeedbackFromRoadmap);
450
+ },
451
+
310
452
  /** Returns the idempotent entry-upvote state mutation. */
311
453
  useSetEntryUpvote() {
312
454
  return useMutation(api.setEntryUpvote);
@@ -340,18 +482,24 @@ type CreatedFeedbackHooks<RateLimitResult> = ReturnType<
340
482
 
341
483
  /** Creates hooks for a feedback API whose rate limiters reject by throwing. */
342
484
  export function createFeedbackHooks(
343
- api: FeedbackPublicApi<string | undefined, never>,
485
+ api?: FeedbackPublicApi<string | undefined, never>,
344
486
  options?: FeedbackHooksOptions,
345
487
  ): CreatedFeedbackHooks<never>;
346
488
 
347
489
  /** Creates hooks carrying a validated non-throwing rate-limit result. */
348
490
  export function createFeedbackHooks<RateLimitResult>(
349
- api: FeedbackPublicApi<string | undefined, RateLimitResult>,
491
+ api?: FeedbackPublicApi<string | undefined, RateLimitResult>,
350
492
  options?: FeedbackHooksOptions,
351
493
  ): CreatedFeedbackHooks<RateLimitResult>;
352
494
 
353
495
  export function createFeedbackHooks<RateLimitResult>(
354
- api: FeedbackPublicApi<string | undefined, RateLimitResult>,
496
+ api: FeedbackPublicApi<
497
+ string | undefined,
498
+ RateLimitResult
499
+ > = anyApi.feedback as unknown as FeedbackPublicApi<
500
+ string | undefined,
501
+ RateLimitResult
502
+ >,
355
503
  options: FeedbackHooksOptions = {},
356
504
  ): CreatedFeedbackHooks<RateLimitResult> {
357
505
  return createFeedbackHooksImplementation(api, options);