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