convex-feedback 0.1.0 → 0.1.2

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 (36) hide show
  1. package/README.md +351 -92
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +267 -68
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/config.d.ts +168 -29
  6. package/dist/client/config.d.ts.map +1 -1
  7. package/dist/client/config.js.map +1 -1
  8. package/dist/client/index.d.ts +167 -172
  9. package/dist/client/index.d.ts.map +1 -1
  10. package/dist/client/index.js +75 -14
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/component/_generated/component.d.ts +2 -2
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/comments.d.ts +12 -12
  15. package/dist/component/comments.d.ts.map +1 -1
  16. package/dist/component/comments.js +43 -60
  17. package/dist/component/comments.js.map +1 -1
  18. package/dist/component/entries.d.ts +30 -30
  19. package/dist/component/entries.d.ts.map +1 -1
  20. package/dist/component/entries.js +216 -97
  21. package/dist/component/entries.js.map +1 -1
  22. package/dist/component/model.d.ts +151 -0
  23. package/dist/component/model.d.ts.map +1 -1
  24. package/dist/react/index.d.ts +178 -56
  25. package/dist/react/index.d.ts.map +1 -1
  26. package/dist/react/index.js +70 -4
  27. package/dist/react/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/client/api.ts +326 -36
  30. package/src/client/config.ts +191 -29
  31. package/src/client/index.ts +437 -16
  32. package/src/component/_generated/component.ts +2 -2
  33. package/src/component/comments.ts +43 -61
  34. package/src/component/entries.ts +300 -123
  35. package/src/component/model.ts +158 -0
  36. package/src/react/index.ts +217 -9
@@ -66,11 +66,169 @@ export const similarEntriesValidator = v.object({
66
66
  similar: v.array(publicEntryValidator),
67
67
  });
68
68
 
69
+ /**
70
+ * Category of feedback represented by an entry.
71
+ *
72
+ * - `"feedback"` — general feedback that is not specifically a bug or feature request.
73
+ * - `"feature_request"` — a request for new or changed functionality.
74
+ * - `"bug_report"` — a report describing incorrect or broken behavior.
75
+ */
69
76
  export type EntryKind = Infer<typeof entryKindValidator>;
77
+
78
+ /**
79
+ * Workflow state of a feedback entry.
80
+ *
81
+ * Status values are fixed by the component so consumers can rely on a stable,
82
+ * fully typed lifecycle. UI labels and presentation can be customized.
83
+ */
70
84
  export type EntryStatus = Infer<typeof entryStatusValidator>;
85
+
86
+ /**
87
+ * Server-side ordering strategy for feedback entries.
88
+ *
89
+ * - `"top"` — entries with the most upvotes first, with creation time used as
90
+ * the deterministic tie-breaker.
91
+ * - `"newest"` — newest entries first.
92
+ */
71
93
  export type EntrySort = Infer<typeof entrySortValidator>;
94
+
95
+ /**
96
+ * Server-side ordering strategy for comments and replies.
97
+ *
98
+ * - `"top"` — comments with the most likes first, with creation time used as
99
+ * the deterministic tie-breaker.
100
+ * - `"newest"` — newest comments first.
101
+ * - `"oldest"` — oldest comments first.
102
+ */
72
103
  export type CommentSort = Infer<typeof commentSortValidator>;
104
+
105
+ /**
106
+ * Authenticated or anonymous actor resolved by the host application.
107
+ *
108
+ * The component does not access the host application's authentication system
109
+ * directly. The host resolves its current user/session into this shape.
110
+ *
111
+ * @property id
112
+ * Stable identifier for the actor. This can be a Clerk ID, Convex Auth ID,
113
+ * application user ID, anonymous installation ID, or another stable
114
+ * host-controlled identifier.
115
+ *
116
+ * @property isModerator
117
+ * Whether the actor can perform moderator-only actions such as changing entry
118
+ * status or modifying content they do not own.
119
+ */
73
120
  export type FeedbackActor = Infer<typeof actorValidator>;
121
+
122
+ /**
123
+ * Public representation of a feedback, feature-request, or bug-report entry.
124
+ *
125
+ * @property id
126
+ * Public component document identifier.
127
+ *
128
+ * @property creationTime
129
+ * Convex document creation timestamp in milliseconds since the Unix epoch.
130
+ *
131
+ * @property actorId
132
+ * Stable identifier of the actor who created the entry.
133
+ *
134
+ * @property kind
135
+ * Entry category.
136
+ *
137
+ * @property status
138
+ * Current workflow status.
139
+ *
140
+ * @property title
141
+ * User-provided entry title.
142
+ *
143
+ * @property body
144
+ * User-provided entry description.
145
+ *
146
+ * @property upvoteCount
147
+ * Denormalized number of actors currently upvoting this entry.
148
+ *
149
+ * @property commentCount
150
+ * Denormalized total number of comments belonging to the entry, including
151
+ * nested replies.
152
+ *
153
+ * @property updatedAt
154
+ * Millisecond timestamp of the most recent content update. Absent when the
155
+ * entry has never been edited.
156
+ *
157
+ * @property viewerHasUpvoted
158
+ * Whether the actor associated with the current query has upvoted the entry.
159
+ * `false` when no viewer actor is available.
160
+ */
74
161
  export type FeedbackEntry = Infer<typeof publicEntryValidator>;
162
+
163
+ /**
164
+ * Public representation of a comment or reply.
165
+ *
166
+ * Comments are returned one level at a time. Child comments are not included
167
+ * automatically; query them separately using the comment's `id` as
168
+ * `parentCommentId`.
169
+ *
170
+ * @property id
171
+ * Public component document identifier.
172
+ *
173
+ * @property creationTime
174
+ * Convex document creation timestamp in milliseconds since the Unix epoch.
175
+ *
176
+ * @property entryId
177
+ * Entry this comment belongs to.
178
+ *
179
+ * @property parentCommentId
180
+ * Direct parent comment. Absent for top-level comments.
181
+ *
182
+ * @property actorId
183
+ * Stable identifier of the actor who created the comment.
184
+ *
185
+ * @property depth
186
+ * Zero-based nesting depth. Top-level comments have depth `0`.
187
+ *
188
+ * @property body
189
+ * User-provided comment text. `null` when the comment was soft-deleted so
190
+ * nested replies can retain their place in the conversation.
191
+ *
192
+ * @property likeCount
193
+ * Denormalized number of actors currently liking this comment.
194
+ *
195
+ * @property replyCount
196
+ * Number of direct child replies. Descendants below those direct children are
197
+ * not included in this count.
198
+ *
199
+ * @property updatedAt
200
+ * Millisecond timestamp of the latest edit, when the comment has been edited.
201
+ *
202
+ * @property deletedAt
203
+ * Millisecond timestamp at which the comment was soft-deleted.
204
+ *
205
+ * @property viewerHasLiked
206
+ * Whether the actor associated with the current query likes this comment.
207
+ * `false` when no viewer actor is available.
208
+ */
75
209
  export type FeedbackComment = Infer<typeof publicCommentValidator>;
210
+
211
+ /**
212
+ * Duplicate-detection result for a proposed entry.
213
+ *
214
+ * Exact normalized-title matches always receive priority. The requested
215
+ * `limit` is a combined limit across both arrays: exact matches consume the
216
+ * available slots first and only the remaining slots can be populated by
217
+ * relevance-ranked full-text matches.
218
+ *
219
+ * For `limit: 3`:
220
+ *
221
+ * - 3 exact matches → `3 exact + 0 similar`
222
+ * - 2 exact matches → `2 exact + at most 1 similar`
223
+ * - 0 exact matches → `0 exact + at most 3 similar`
224
+ *
225
+ * An entry returned in `exact` is never repeated in `similar`.
226
+ *
227
+ * @property exact
228
+ * Entries whose normalized title exactly matches the proposed title.
229
+ *
230
+ * @property similar
231
+ * Full-text matches ordered by search relevance after exact matches have been
232
+ * removed.
233
+ */
76
234
  export type SimilarEntriesResult = Infer<typeof similarEntriesValidator>;
@@ -9,48 +9,185 @@ import type {
9
9
  EntryKind,
10
10
  EntrySort,
11
11
  EntryStatus,
12
+ SimilarEntriesResult,
12
13
  } from "../component/model.js";
13
14
 
15
+ /**
16
+ * Client-side pagination defaults used by hooks created with
17
+ * `createFeedbackHooks`.
18
+ *
19
+ * These values control initial page sizes and subsequent `loadMore` sizes in
20
+ * the UI. They do not override the server-side maximum page sizes configured
21
+ * on the component.
22
+ */
14
23
  export interface FeedbackHooksOptions {
24
+ /**
25
+ * Number of entries initially requested by `useEntries`.
26
+ *
27
+ * @default 20
28
+ */
15
29
  entryPageSize?: number;
30
+
31
+ /**
32
+ * Number of top-level comments initially requested by `useComments`.
33
+ *
34
+ * @default 20
35
+ */
16
36
  commentPageSize?: number;
37
+
38
+ /**
39
+ * Number of direct child replies initially requested when
40
+ * `parentCommentId` is supplied to `useComments`.
41
+ *
42
+ * @default 10
43
+ */
17
44
  replyPageSize?: number;
18
45
  }
19
46
 
47
+ /**
48
+ * Arguments accepted by `useEntries`.
49
+ */
20
50
  export interface UseEntriesArgs {
21
- kind?: EntryKind;
51
+ /**
52
+ * Entry kinds to include.
53
+ *
54
+ * Filtering occurs in Convex before pagination. Omit to include every kind.
55
+ *
56
+ * The hook accepts a readonly array so configuration constants such as
57
+ * `readonly EntryKind[]` can be passed directly.
58
+ */
59
+ kinds?: readonly EntryKind[];
60
+
61
+ /** Optional workflow-status filter. */
22
62
  status?: EntryStatus;
63
+
64
+ /**
65
+ * Server-side entry ordering.
66
+ *
67
+ * When omitted, the host's configured default is used.
68
+ */
23
69
  sort?: EntrySort;
24
70
  }
25
71
 
72
+ /**
73
+ * Arguments accepted by `useComments`.
74
+ *
75
+ * Comment trees are intentionally loaded lazily. Every invocation retrieves
76
+ * exactly one level of the tree.
77
+ */
26
78
  export interface UseCommentsArgs {
79
+ /** Entry whose comments are being requested. */
27
80
  entryId: string;
81
+
82
+ /**
83
+ * Direct parent whose replies should be requested.
84
+ *
85
+ * - Omitted → returns top-level comments.
86
+ * - Supplied → returns only direct children of this comment.
87
+ *
88
+ * Grandchildren and deeper descendants are never automatically loaded.
89
+ * Call `useComments` again with the child comment's ID when that branch is
90
+ * expanded.
91
+ */
28
92
  parentCommentId?: string;
93
+
94
+ /**
95
+ * Server-side ordering for this level of comments.
96
+ *
97
+ * When omitted, the host's configured default is used.
98
+ */
29
99
  sort?: CommentSort;
30
100
  }
31
101
 
102
+ /**
103
+ * Arguments accepted by `useSearchEntries`.
104
+ */
32
105
  export interface SearchEntriesArgs {
106
+ /**
107
+ * Full-text query.
108
+ *
109
+ * Whitespace-only values skip the Convex query and return an empty array.
110
+ */
33
111
  searchQuery: string;
34
- kind?: EntryKind;
112
+
113
+ /**
114
+ * Entry kinds to include.
115
+ *
116
+ * Filtering occurs on the server.
117
+ */
118
+ kinds?: readonly EntryKind[];
119
+
120
+ /** Optional workflow-status filter. */
35
121
  status?: EntryStatus;
122
+
123
+ /**
124
+ * Maximum number of results requested.
125
+ *
126
+ * The server applies its configured default when omitted and clamps the
127
+ * value to its configured maximum.
128
+ */
36
129
  limit?: number;
37
130
  }
38
131
 
132
+ /**
133
+ * Arguments accepted by `useSimilarEntries`.
134
+ */
39
135
  export interface SimilarEntriesArgs {
136
+ /**
137
+ * Proposed entry title.
138
+ *
139
+ * Used both for normalized exact-title matching and full-text search.
140
+ */
40
141
  title: string;
142
+
143
+ /**
144
+ * Proposed entry body.
145
+ *
146
+ * Used together with the title for full-text similarity search.
147
+ */
41
148
  body: string;
149
+
150
+ /**
151
+ * Restricts duplicate detection to one entry kind.
152
+ *
153
+ * Omit to search every kind.
154
+ */
42
155
  kind?: EntryKind;
156
+
157
+ /**
158
+ * Maximum combined number of suggestions requested.
159
+ *
160
+ * Exact normalized-title matches consume the limit first. Only remaining
161
+ * slots may be filled by relevance-ranked full-text matches.
162
+ *
163
+ * For example, `limit: 3` with two exact matches can return at most one
164
+ * additional similar match.
165
+ */
43
166
  limit?: number;
44
167
  }
45
168
 
169
+ const emptySimilarEntriesResult: SimilarEntriesResult = {
170
+ exact: [],
171
+ similar: [],
172
+ };
173
+
46
174
  function positivePageSize(value: number | undefined, fallback: number): number {
47
175
  return value !== undefined && Number.isInteger(value) && value > 0
48
176
  ? value
49
177
  : fallback;
50
178
  }
51
179
 
52
- export function createFeedbackHooks(
53
- api: FeedbackPublicApi,
180
+ /**
181
+ * Creates React hooks bound to a host application's exposed feedback API.
182
+ *
183
+ * Create this once in the consuming application and reuse the returned hook
184
+ * collection.
185
+ *
186
+ * @typeParam RateLimitResult The validated non-throwing rate-limit rejection
187
+ * type returned by mutation hooks. It is inferred from the supplied API.
188
+ */
189
+ export function createFeedbackHooks<RateLimitResult = never>(
190
+ api: FeedbackPublicApi<string | undefined, RateLimitResult>,
54
191
  options: FeedbackHooksOptions = {},
55
192
  ) {
56
193
  const entryPageSize = positivePageSize(options.entryPageSize, 20);
@@ -58,18 +195,36 @@ export function createFeedbackHooks(
58
195
  const replyPageSize = positivePageSize(options.replyPageSize, 10);
59
196
 
60
197
  return {
198
+ /**
199
+ * Resolved client-side page sizes used by the generated hooks and useful
200
+ * when calling their `loadMore` functions.
201
+ */
61
202
  pageSizes: {
62
203
  entries: entryPageSize,
63
204
  comments: commentPageSize,
64
205
  replies: replyPageSize,
65
206
  } as const,
66
207
 
208
+ /**
209
+ * Returns a cursor-paginated, server-filtered entry list.
210
+ */
67
211
  useEntries(args: UseEntriesArgs = {}) {
68
- return usePaginatedQuery(api.listEntries, args, {
212
+ const queryArgs = {
213
+ ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
214
+ ...(args.status === undefined ? {} : { status: args.status }),
215
+ ...(args.sort === undefined ? {} : { sort: args.sort }),
216
+ };
217
+
218
+ return usePaginatedQuery(api.listEntries, queryArgs, {
69
219
  initialNumItems: entryPageSize,
70
220
  });
71
221
  },
72
222
 
223
+ /**
224
+ * Reactively retrieves one entry.
225
+ *
226
+ * Passing `null` or `undefined` skips the query.
227
+ */
73
228
  useEntry(entryId: string | null | undefined) {
74
229
  return useQuery(
75
230
  api.getEntry,
@@ -77,25 +232,59 @@ export function createFeedbackHooks(
77
232
  );
78
233
  },
79
234
 
235
+ /**
236
+ * Performs reactive full-text entry search.
237
+ *
238
+ * An empty/whitespace-only query returns `[]` without running Convex.
239
+ */
80
240
  useSearchEntries(args: SearchEntriesArgs) {
81
241
  const searchQuery = args.searchQuery.trim();
242
+
243
+ const queryArgs = {
244
+ searchQuery,
245
+ ...(args.kinds === undefined ? {} : { kinds: [...args.kinds] }),
246
+ ...(args.status === undefined ? {} : { status: args.status }),
247
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
248
+ };
249
+
82
250
  const result = useQuery(
83
251
  api.searchEntries,
84
- searchQuery.length === 0 ? "skip" : { ...args, searchQuery },
252
+ searchQuery.length === 0 ? "skip" : queryArgs,
85
253
  );
254
+
86
255
  return searchQuery.length === 0 ? [] : result;
87
256
  },
88
257
 
258
+ /**
259
+ * Reactively finds exact and similar entries for a proposed draft.
260
+ *
261
+ * The query is skipped only when both title and body are blank.
262
+ *
263
+ * `limit` is forwarded to the server unchanged. The server applies the
264
+ * combined exact-first limit semantics documented by `SimilarEntriesArgs`.
265
+ */
89
266
  useSimilarEntries(args: SimilarEntriesArgs) {
90
267
  const shouldSkip =
91
268
  args.title.trim().length === 0 && args.body.trim().length === 0;
269
+
270
+ const queryArgs = {
271
+ title: args.title,
272
+ body: args.body,
273
+ ...(args.kind === undefined ? {} : { kind: args.kind }),
274
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
275
+ };
276
+
92
277
  const result = useQuery(
93
278
  api.findSimilarEntries,
94
- shouldSkip ? "skip" : args,
279
+ shouldSkip ? "skip" : queryArgs,
95
280
  );
96
- return shouldSkip ? { exact: [], similar: [] } : result;
281
+
282
+ return shouldSkip ? emptySimilarEntriesResult : result;
97
283
  },
98
284
 
285
+ /**
286
+ * Returns one independently paginated level of comments.
287
+ */
99
288
  useComments(args: UseCommentsArgs) {
100
289
  return usePaginatedQuery(api.listComments, args, {
101
290
  initialNumItems:
@@ -103,38 +292,57 @@ export function createFeedbackHooks(
103
292
  });
104
293
  },
105
294
 
295
+ /** Returns the bound create-entry mutation. */
106
296
  useCreateEntry() {
107
297
  return useMutation(api.createEntry);
108
298
  },
109
299
 
300
+ /** Returns the bound update-entry mutation. */
110
301
  useUpdateEntry() {
111
302
  return useMutation(api.updateEntry);
112
303
  },
113
304
 
305
+ /** Returns the bound status mutation. */
114
306
  useSetEntryStatus() {
115
307
  return useMutation(api.setEntryStatus);
116
308
  },
117
309
 
310
+ /** Returns the idempotent entry-upvote state mutation. */
118
311
  useSetEntryUpvote() {
119
312
  return useMutation(api.setEntryUpvote);
120
313
  },
121
314
 
315
+ /** Returns the create-comment/reply mutation. */
122
316
  useCreateComment() {
123
317
  return useMutation(api.createComment);
124
318
  },
125
319
 
320
+ /** Returns the update-comment mutation. */
126
321
  useUpdateComment() {
127
322
  return useMutation(api.updateComment);
128
323
  },
129
324
 
325
+ /** Returns the soft-delete-comment mutation. */
130
326
  useDeleteComment() {
131
327
  return useMutation(api.deleteComment);
132
328
  },
133
329
 
330
+ /** Returns the idempotent comment-like state mutation. */
134
331
  useSetCommentLike() {
135
332
  return useMutation(api.setCommentLike);
136
333
  },
137
334
  };
138
335
  }
139
336
 
140
- export type FeedbackHooks = ReturnType<typeof createFeedbackHooks>;
337
+ /**
338
+ * Hook collection returned by `createFeedbackHooks`.
339
+ *
340
+ * This type is intended to be passed to `FeedbackScreen` and other
341
+ * `convex-feedback-ui` integrations.
342
+ *
343
+ * @typeParam RateLimitResult The validated non-throwing rate-limit rejection
344
+ * type returned by mutation hooks.
345
+ */
346
+ export type FeedbackHooks<RateLimitResult = never> = ReturnType<
347
+ typeof createFeedbackHooks<RateLimitResult>
348
+ >;