convex-feedback 0.1.0 → 0.1.1
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/README.md +281 -92
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +262 -67
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/config.d.ts +168 -29
- package/dist/client/config.d.ts.map +1 -1
- package/dist/client/config.js.map +1 -1
- package/dist/client/index.d.ts +26 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +2 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/comments.d.ts +5 -5
- package/dist/component/entries.d.ts +22 -22
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +198 -72
- package/dist/component/entries.js.map +1 -1
- package/dist/component/model.d.ts +151 -0
- package/dist/component/model.d.ts.map +1 -1
- package/dist/react/index.d.ts +170 -54
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +67 -4
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +315 -30
- package/src/client/config.ts +191 -29
- package/src/client/index.ts +32 -4
- package/src/component/_generated/component.ts +2 -2
- package/src/component/entries.ts +282 -101
- package/src/component/model.ts +158 -0
- package/src/react/index.ts +206 -6
package/README.md
CHANGED
|
@@ -1,91 +1,125 @@
|
|
|
1
|
+
[](https://badge.fury.io/js/convex-feedback)    
|
|
2
|
+
|
|
1
3
|
# convex-feedback
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
A headless, fully typed Convex component for product feedback, feature requests, bug reports, entry upvotes, lazy nested comments, comment likes, full-text search, and duplicate suggestions.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
> Looking for a ready-made interface? `[convex-feedback-ui](../convex-feedback-ui/README.md)` provides optional React DOM and React Native screens and compound primitives on top of this package.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Features
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
- Feedback, feature requests, and bug reports.
|
|
12
|
+
- Canny-style entry upvotes.
|
|
13
|
+
- Recursive comments loaded one level at a time.
|
|
14
|
+
- Comment likes.
|
|
15
|
+
- Indexed `top` / `newest` entry ordering.
|
|
16
|
+
- Indexed `top` / `newest` / `oldest` comment ordering.
|
|
17
|
+
- Convex full-text search.
|
|
18
|
+
- Exact-title + full-text duplicate suggestions.
|
|
19
|
+
- Host-controlled authentication and moderator permissions.
|
|
20
|
+
- Configurable limits and behavior
|
|
21
|
+
- Typed React hooks.
|
|
22
|
+
- `convex-test` helper entry point.
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
The component owns only three tables: `entries`, `comments`, and `reactions`.
|
|
12
25
|
|
|
13
|
-
|
|
14
|
-
- `status`: `open | under_review | planned | in_progress | completed | closed`
|
|
15
|
-
- `actorId`, `title`, `body`
|
|
16
|
-
- `normalizedTitle`, `searchText`
|
|
17
|
-
- `upvoteCount`, `commentCount`
|
|
18
|
-
- optional `updatedAt`
|
|
26
|
+
## Requirements
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
- An existing Convex application.
|
|
29
|
+
- `convex` installed in the host project.
|
|
30
|
+
- React is required only when using `convex-feedback/react`.
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
## Installation
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
```bash
|
|
35
|
+
npm install convex-feedback
|
|
36
|
+
```
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
- optional `parentCommentId`
|
|
28
|
-
- `actorId`, `depth`, `body`
|
|
29
|
-
- `likeCount`
|
|
30
|
-
- `replyCount` (direct children only)
|
|
31
|
-
- optional `updatedAt`, `deletedAt`
|
|
38
|
+
## 1. Install the component in Convex
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
Create or update your host application's `convex/convex.config.ts`:
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
```ts
|
|
43
|
+
import { defineApp } from "convex/server";
|
|
44
|
+
import feedback from "convex-feedback/convex.config.js";
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
const app = defineApp();
|
|
47
|
+
app.use(feedback);
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
export default app;
|
|
50
|
+
```
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
You can install multiple independent instances by giving them different component names using Convex's normal component configuration APIs.
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
Run Convex so the host application's component references are generated:
|
|
44
55
|
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
comment A
|
|
48
|
-
click "View replies"
|
|
49
|
-
direct replies query for A
|
|
50
|
-
reply A1
|
|
51
|
-
click "View replies"
|
|
52
|
-
direct replies query for A1
|
|
56
|
+
```bash
|
|
57
|
+
npx convex dev
|
|
53
58
|
```
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
## 2. Expose the component through your host API
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
A Convex component cannot make authorization decisions using your host application's authentication state directly. `convex-feedback` therefore exposes a host wrapper: your app resolves the current actor, and the wrapper passes the stable actor identity into the component.
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
Create a host module such as `convex/feedback.ts`:
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
```ts
|
|
67
|
+
import { exposeFeedbackApi } from "convex-feedback";
|
|
68
|
+
|
|
69
|
+
import { components } from "./_generated/api";
|
|
70
|
+
|
|
71
|
+
export const {
|
|
72
|
+
listEntries,
|
|
73
|
+
getEntry,
|
|
74
|
+
searchEntries,
|
|
75
|
+
findSimilarEntries,
|
|
76
|
+
createEntry,
|
|
77
|
+
updateEntry,
|
|
78
|
+
setEntryStatus,
|
|
79
|
+
setEntryUpvote,
|
|
80
|
+
listComments,
|
|
81
|
+
createComment,
|
|
82
|
+
updateComment,
|
|
83
|
+
deleteComment,
|
|
84
|
+
setCommentLike,
|
|
85
|
+
} = exposeFeedbackApi(components.feedback, {
|
|
86
|
+
actor: async (ctx) => {
|
|
87
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
88
|
+
|
|
89
|
+
if (identity === null) return null;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
id: identity.tokenIdentifier,
|
|
93
|
+
isModerator: false,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Actor IDs
|
|
64
100
|
|
|
65
|
-
|
|
101
|
+
`actor.id` should be stable for the same user.
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
The component does not store a user/profile table. Keep display names, avatars, roles, and profile data in your application.
|
|
68
104
|
|
|
69
|
-
|
|
105
|
+
### Moderators
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
Return `isModerator: true` for actors that may perform moderator-only operations such as status changes.
|
|
72
108
|
|
|
73
109
|
```ts
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
110
|
+
return {
|
|
111
|
+
id: identity.tokenIdentifier,
|
|
112
|
+
isModerator: await isFeedbackModerator(ctx, identity.tokenIdentifier),
|
|
113
|
+
};
|
|
78
114
|
```
|
|
79
115
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Static configuration
|
|
116
|
+
## 3. Configure behavior
|
|
83
117
|
|
|
84
|
-
Configuration is host code
|
|
118
|
+
Configuration is optional static host code; The values shown are the default configuration.
|
|
85
119
|
|
|
86
120
|
```ts
|
|
87
|
-
exposeFeedbackApi(components.feedback, {
|
|
88
|
-
actor:
|
|
121
|
+
export const feedbackApi = exposeFeedbackApi(components.feedback, {
|
|
122
|
+
actor: resolveFeedbackActor,
|
|
89
123
|
config: {
|
|
90
124
|
entries: {
|
|
91
125
|
enabledKinds: ["feedback", "feature_request", "bug_report"],
|
|
@@ -109,75 +143,230 @@ exposeFeedbackApi(components.feedback, {
|
|
|
109
143
|
},
|
|
110
144
|
limits: {
|
|
111
145
|
titleLength: 160,
|
|
112
|
-
bodyLength:
|
|
113
|
-
commentLength:
|
|
146
|
+
bodyLength: 10_000,
|
|
147
|
+
commentLength: 5_000,
|
|
114
148
|
},
|
|
115
149
|
},
|
|
116
150
|
});
|
|
117
151
|
```
|
|
118
152
|
|
|
119
|
-
All
|
|
153
|
+
All configuration fields are documented in the exported TypeScript types and appear in editor IntelliSense.
|
|
120
154
|
|
|
121
|
-
##
|
|
155
|
+
## 4. Create typed React hooks
|
|
122
156
|
|
|
123
|
-
|
|
157
|
+
If your client uses React or React Native, bind the generated host API once:
|
|
124
158
|
|
|
125
159
|
```ts
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
// src/feedback.ts
|
|
161
|
+
import { createFeedbackHooks } from "convex-feedback/react";
|
|
162
|
+
|
|
163
|
+
import { api } from "../convex/_generated/api";
|
|
164
|
+
|
|
165
|
+
export const feedbackHooks = createFeedbackHooks(api.feedback, {
|
|
166
|
+
entryPageSize: 20,
|
|
167
|
+
commentPageSize: 20,
|
|
168
|
+
replyPageSize: 10,
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Then use the hooks directly or pass `feedbackHooks` to `convex-feedback-ui`.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
const entries = feedbackHooks.useEntries({
|
|
176
|
+
kinds: ["feature_request", "bug_report"],
|
|
177
|
+
sort: "top",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const createEntry = feedbackHooks.useCreateEntry();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Entry kinds and statuses
|
|
184
|
+
|
|
185
|
+
### Kinds
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
type EntryKind = "feedback" | "feature_request" | "bug_report";
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Statuses
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
type EntryStatus =
|
|
195
|
+
"open" | "under_review" | "planned" | "in_progress" | "completed" | "closed";
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The values are intentionally fixed for type safety. UI labels and presentation can be localized/customized in `convex-feedback-ui`.
|
|
199
|
+
|
|
200
|
+
## Search
|
|
201
|
+
|
|
202
|
+
Full-text search is backed by Convex search indexes:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const results = feedbackHooks.useSearchEntries({
|
|
206
|
+
searchQuery: "dark mode",
|
|
207
|
+
kinds: ["feature_request"],
|
|
208
|
+
limit: 10,
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Duplicate suggestions
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const result = feedbackHooks.useSimilarEntries({
|
|
216
|
+
title,
|
|
217
|
+
body,
|
|
218
|
+
kind: "feature_request",
|
|
219
|
+
limit: 3,
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The result has two groups:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
{
|
|
227
|
+
exact: FeedbackEntry[];
|
|
228
|
+
similar: FeedbackEntry[];
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`limit` is the **maximum combined number of suggestions**. Exact normalized-title matches consume the limit first; only remaining slots are filled by relevance-ranked full-text matches.
|
|
233
|
+
|
|
234
|
+
For `limit: 3`:
|
|
235
|
+
|
|
236
|
+
- 3 exact matches → 3 exact, 0 similar;
|
|
237
|
+
- 2 exact matches → 2 exact, at most 1 similar;
|
|
238
|
+
- 0 exact matches → at most 3 similar.
|
|
239
|
+
|
|
240
|
+
Exact matches are never duplicated in `similar`.
|
|
241
|
+
|
|
242
|
+
This is lexical/full-text duplicate detection.
|
|
243
|
+
|
|
244
|
+
## Comments and replies
|
|
245
|
+
|
|
246
|
+
Comments are recursive but deliberately lazy.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// Top-level comments
|
|
250
|
+
const comments = feedbackHooks.useComments({
|
|
251
|
+
entryId,
|
|
252
|
+
sort: "top",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Direct replies to one comment
|
|
256
|
+
const replies = feedbackHooks.useComments({
|
|
257
|
+
entryId,
|
|
258
|
+
parentCommentId: comment.id,
|
|
259
|
+
sort: "top",
|
|
260
|
+
});
|
|
131
261
|
```
|
|
132
262
|
|
|
133
|
-
|
|
263
|
+
A comment query returns exactly one direct-child level. Opening a reply branch should mount another query for that child's direct replies.
|
|
264
|
+
|
|
265
|
+
`replyCount` is the number of **direct children**. `entry.commentCount` is the total number of comments/replies belonging to the entry.
|
|
266
|
+
|
|
267
|
+
Soft-deleted comments remain as tombstones so descendants keep their position in the thread.
|
|
268
|
+
|
|
269
|
+
## Upvotes and likes
|
|
270
|
+
|
|
271
|
+
Entry upvotes and comment likes have separate public APIs even though the component stores both efficiently in one reactions table.
|
|
134
272
|
|
|
135
|
-
|
|
273
|
+
Both state mutations are idempotent and accept a desired final state:
|
|
136
274
|
|
|
137
|
-
|
|
275
|
+
```ts
|
|
276
|
+
await setEntryUpvote({
|
|
277
|
+
entryId,
|
|
278
|
+
desiredState: true,
|
|
279
|
+
});
|
|
138
280
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- `updateEntry`
|
|
145
|
-
- `setEntryStatus`
|
|
146
|
-
- `setEntryUpvote`
|
|
147
|
-
- `listComments`
|
|
148
|
-
- `createComment`
|
|
149
|
-
- `updateComment`
|
|
150
|
-
- `deleteComment`
|
|
151
|
-
- `setCommentLike`
|
|
281
|
+
await setCommentLike({
|
|
282
|
+
commentId,
|
|
283
|
+
desiredState: false,
|
|
284
|
+
});
|
|
285
|
+
```
|
|
152
286
|
|
|
153
|
-
|
|
287
|
+
The mutation result uses `active` to report the authoritative final state returned by the server.
|
|
154
288
|
|
|
155
|
-
##
|
|
289
|
+
## Host API
|
|
156
290
|
|
|
157
|
-
|
|
291
|
+
The wrapper exposes:
|
|
158
292
|
|
|
159
|
-
|
|
293
|
+
| Function | Type | Purpose |
|
|
294
|
+
| -------------------- | -------- | -------------------------------------------------- |
|
|
295
|
+
| `listEntries` | query | Paginated entry list with server-side filters/sort |
|
|
296
|
+
| `getEntry` | query | Fetch one entry |
|
|
297
|
+
| `searchEntries` | query | Full-text search |
|
|
298
|
+
| `findSimilarEntries` | query | Exact + similar duplicate suggestions |
|
|
299
|
+
| `createEntry` | mutation | Create feedback |
|
|
300
|
+
| `updateEntry` | mutation | Edit author-owned/moderated feedback |
|
|
301
|
+
| `setEntryStatus` | mutation | Moderator workflow status change |
|
|
302
|
+
| `setEntryUpvote` | mutation | Idempotently set entry upvote state |
|
|
303
|
+
| `listComments` | query | One paginated direct-child comment level |
|
|
304
|
+
| `createComment` | mutation | Create comment or reply |
|
|
305
|
+
| `updateComment` | mutation | Edit a comment |
|
|
306
|
+
| `deleteComment` | mutation | Soft-delete a comment |
|
|
307
|
+
| `setCommentLike` | mutation | Idempotently set comment like state |
|
|
160
308
|
|
|
161
|
-
|
|
309
|
+
Every public argument/result type is exported and documented for editor IntelliSense.
|
|
162
310
|
|
|
163
|
-
|
|
311
|
+
## Package entry points
|
|
164
312
|
|
|
165
313
|
```ts
|
|
314
|
+
// Host wrapper, configuration, public model/API types
|
|
315
|
+
import { exposeFeedbackApi } from "convex-feedback";
|
|
316
|
+
|
|
317
|
+
// React / React Native hooks
|
|
318
|
+
import { createFeedbackHooks } from "convex-feedback/react";
|
|
319
|
+
|
|
320
|
+
// Convex component definition
|
|
321
|
+
import feedback from "convex-feedback/convex.config.js";
|
|
322
|
+
|
|
323
|
+
// ComponentApi type generated by Convex
|
|
324
|
+
import type { ComponentApi } from "convex-feedback/_generated/component";
|
|
325
|
+
|
|
326
|
+
// convex-test helper
|
|
166
327
|
import feedbackTest from "convex-feedback/test";
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Testing host integrations
|
|
331
|
+
|
|
332
|
+
The package exposes a `/test` entry point for `convex-test`.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
167
335
|
import { convexTest } from "convex-test";
|
|
336
|
+
import feedbackTest from "convex-feedback/test";
|
|
337
|
+
|
|
338
|
+
import schema from "./convex/schema";
|
|
339
|
+
|
|
340
|
+
const modules = import.meta.glob("./convex/**/*.ts");
|
|
341
|
+
const t = convexTest(schema, modules);
|
|
168
342
|
|
|
169
|
-
const t = convexTest();
|
|
170
343
|
feedbackTest.register(t, "feedback");
|
|
171
344
|
```
|
|
172
345
|
|
|
173
|
-
|
|
346
|
+
Use host-level tests when you need to verify your authentication wrapper and public host API in the same shape your client will consume.
|
|
347
|
+
|
|
348
|
+
## UI package
|
|
349
|
+
|
|
350
|
+
`convex-feedback` is intentionally headless. For a complete board or customizable primitives, install:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
npm install convex-feedback-ui
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Then read `[convex-feedback-ui](../convex-feedback-ui/README.md)`.
|
|
174
357
|
|
|
175
|
-
##
|
|
358
|
+
## Development in this repository
|
|
176
359
|
|
|
177
|
-
|
|
360
|
+
From the monorepo root:
|
|
178
361
|
|
|
179
362
|
```bash
|
|
180
|
-
npm
|
|
363
|
+
npm install
|
|
364
|
+
npm run codegen
|
|
365
|
+
npm run test:all
|
|
181
366
|
```
|
|
182
367
|
|
|
183
|
-
|
|
368
|
+
When changing the component's schema or functions, regenerate component code before committing generated output.
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
Apache-2.0
|