convex-feedback 0.2.0-beta.0 → 0.2.0-beta.10
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 +13 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +2 -0
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +10 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/admin.d.ts +3 -87
- package/dist/component/admin.d.ts.map +1 -1
- package/dist/component/admin.js +47 -6
- package/dist/component/admin.js.map +1 -1
- package/dist/component/comments.d.ts +1 -14
- package/dist/component/comments.d.ts.map +1 -1
- package/dist/component/entries.d.ts +6 -85
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +7 -0
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/helpers.js +1 -0
- package/dist/component/helpers.js.map +1 -1
- package/dist/component/model.d.ts +170 -114
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +2 -0
- package/dist/component/model.js.map +1 -1
- package/dist/component/roadmap.d.ts +3 -39
- package/dist/component/roadmap.d.ts.map +1 -1
- package/dist/component/roadmap.js +11 -6
- package/dist/component/roadmap.js.map +1 -1
- package/dist/react/index.d.ts +12 -228
- package/dist/react/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/client/api.ts +3 -0
- package/src/client/index.ts +2 -0
- package/src/component/_generated/component.ts +10 -0
- package/src/component/admin.ts +70 -6
- package/src/component/entries.ts +7 -0
- package/src/component/helpers.ts +2 -0
- package/src/component/model.ts +202 -110
- package/src/component/roadmap.ts +11 -6
- package/dist/component/tags.d.ts +0 -58
- package/dist/component/tags.d.ts.map +0 -1
- package/dist/component/tags.js +0 -216
- package/dist/component/tags.js.map +0 -1
|
@@ -62,6 +62,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
62
62
|
updatedAt?: number;
|
|
63
63
|
upvoteCount: number;
|
|
64
64
|
viewerHasUpvoted: boolean;
|
|
65
|
+
viewerIsAuthor?: boolean;
|
|
65
66
|
} | null,
|
|
66
67
|
Name
|
|
67
68
|
>;
|
|
@@ -125,6 +126,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
125
126
|
updatedAt?: number;
|
|
126
127
|
upvoteCount: number;
|
|
127
128
|
viewerHasUpvoted: boolean;
|
|
129
|
+
viewerIsAuthor?: boolean;
|
|
128
130
|
}>;
|
|
129
131
|
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
130
132
|
splitCursor?: string | null;
|
|
@@ -192,6 +194,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
192
194
|
updatedAt?: number;
|
|
193
195
|
upvoteCount: number;
|
|
194
196
|
viewerHasUpvoted: boolean;
|
|
197
|
+
viewerIsAuthor?: boolean;
|
|
195
198
|
}>;
|
|
196
199
|
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
197
200
|
splitCursor?: string | null;
|
|
@@ -338,6 +341,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
338
341
|
updatedAt?: number;
|
|
339
342
|
upvoteCount: number;
|
|
340
343
|
viewerHasUpvoted: boolean;
|
|
344
|
+
viewerIsAuthor?: boolean;
|
|
341
345
|
} | null,
|
|
342
346
|
Name
|
|
343
347
|
>;
|
|
@@ -390,6 +394,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
390
394
|
updatedAt?: number;
|
|
391
395
|
upvoteCount: number;
|
|
392
396
|
viewerHasUpvoted: boolean;
|
|
397
|
+
viewerIsAuthor?: boolean;
|
|
393
398
|
}>;
|
|
394
399
|
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
395
400
|
splitCursor?: string | null;
|
|
@@ -435,6 +440,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
435
440
|
updatedAt?: number;
|
|
436
441
|
upvoteCount: number;
|
|
437
442
|
viewerHasUpvoted: boolean;
|
|
443
|
+
viewerIsAuthor?: boolean;
|
|
438
444
|
}>,
|
|
439
445
|
Name
|
|
440
446
|
>;
|
|
@@ -506,6 +512,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
506
512
|
updatedAt?: number;
|
|
507
513
|
upvoteCount: number;
|
|
508
514
|
viewerHasUpvoted: boolean;
|
|
515
|
+
viewerIsAuthor?: boolean;
|
|
509
516
|
}>;
|
|
510
517
|
similar: Array<{
|
|
511
518
|
actorId: string;
|
|
@@ -529,6 +536,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
529
536
|
updatedAt?: number;
|
|
530
537
|
upvoteCount: number;
|
|
531
538
|
viewerHasUpvoted: boolean;
|
|
539
|
+
viewerIsAuthor?: boolean;
|
|
532
540
|
}>;
|
|
533
541
|
},
|
|
534
542
|
Name
|
|
@@ -541,6 +549,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
541
549
|
body: string;
|
|
542
550
|
editableByAuthor: boolean;
|
|
543
551
|
entryId: string;
|
|
552
|
+
kind?: "feedback" | "feature_request" | "bug_report";
|
|
544
553
|
maxBodyLength: number;
|
|
545
554
|
maxTitleLength: number;
|
|
546
555
|
title: string;
|
|
@@ -669,6 +678,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
669
678
|
updatedAt?: number;
|
|
670
679
|
upvoteCount: number;
|
|
671
680
|
viewerHasUpvoted: boolean;
|
|
681
|
+
viewerIsAuthor?: boolean;
|
|
672
682
|
}>;
|
|
673
683
|
pageStatus?: "SplitRecommended" | "SplitRequired" | null;
|
|
674
684
|
splitCursor?: string | null;
|
package/src/component/admin.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { stream } from "convex-helpers/server/stream";
|
|
1
2
|
import {
|
|
2
3
|
paginationOptsValidator,
|
|
3
4
|
paginationResultValidator,
|
|
5
|
+
type PaginationOptions,
|
|
6
|
+
type PaginationResult,
|
|
4
7
|
} from "convex/server";
|
|
5
8
|
import { ConvexError, v } from "convex/values";
|
|
6
|
-
import { stream } from "convex-helpers/server/stream";
|
|
7
9
|
|
|
8
10
|
import type { Doc } from "./_generated/dataModel.js";
|
|
9
11
|
import { query } from "./_generated/server.js";
|
|
@@ -19,6 +21,68 @@ import {
|
|
|
19
21
|
} from "./model.js";
|
|
20
22
|
import schema from "./schema.js";
|
|
21
23
|
|
|
24
|
+
const ADMIN_SEARCH_CURSOR_PREFIX = "convex-feedback:admin-search:";
|
|
25
|
+
|
|
26
|
+
type TakeQuery<T> = {
|
|
27
|
+
take(n: number): Promise<T[]>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function encodeAdminSearchCursor(offset: number): string {
|
|
31
|
+
return `${ADMIN_SEARCH_CURSOR_PREFIX}${offset}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeAdminSearchCursor(cursor: string | null | undefined): number {
|
|
35
|
+
if (cursor === null || cursor === undefined) return 0;
|
|
36
|
+
if (!cursor.startsWith(ADMIN_SEARCH_CURSOR_PREFIX)) {
|
|
37
|
+
throw new ConvexError("InvalidCursor: invalid admin search cursor.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const offset = Number(cursor.slice(ADMIN_SEARCH_CURSOR_PREFIX.length));
|
|
41
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
42
|
+
throw new ConvexError("InvalidCursor: invalid admin search cursor.");
|
|
43
|
+
}
|
|
44
|
+
return offset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function addAdminSearchCursorOffsets(left: number, right: number): number {
|
|
48
|
+
const result = left + right;
|
|
49
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
50
|
+
throw new ConvexError("InvalidCursor: admin search cursor is too large.");
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function paginateAdminSearchIndex(
|
|
56
|
+
query: TakeQuery<Doc<"entries">>,
|
|
57
|
+
paginationOpts: PaginationOptions,
|
|
58
|
+
): Promise<PaginationResult<Doc<"entries">>> {
|
|
59
|
+
const start = decodeAdminSearchCursor(paginationOpts.cursor);
|
|
60
|
+
const end = decodeAdminSearchCursor(paginationOpts.endCursor);
|
|
61
|
+
const requestedEnd =
|
|
62
|
+
paginationOpts.endCursor === undefined || paginationOpts.endCursor === null
|
|
63
|
+
? addAdminSearchCursorOffsets(start, paginationOpts.numItems)
|
|
64
|
+
: end;
|
|
65
|
+
|
|
66
|
+
if (requestedEnd < start) {
|
|
67
|
+
throw new ConvexError("InvalidCursor: admin search cursors are reversed.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Search-index queries cannot expose a component-safe continuation cursor.
|
|
71
|
+
// Read through the requested range and use the result offset as our opaque
|
|
72
|
+
// cursor instead.
|
|
73
|
+
const results = await query.take(
|
|
74
|
+
addAdminSearchCursorOffsets(requestedEnd, 1),
|
|
75
|
+
);
|
|
76
|
+
const page = results.slice(start, requestedEnd);
|
|
77
|
+
const nextOffset = addAdminSearchCursorOffsets(start, page.length);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
page,
|
|
81
|
+
isDone: results.length <= requestedEnd,
|
|
82
|
+
continueCursor: encodeAdminSearchCursor(nextOffset),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
22
86
|
function matchesFilters(
|
|
23
87
|
entry: Doc<"entries">,
|
|
24
88
|
kinds: EntryKind[] | undefined,
|
|
@@ -127,9 +191,8 @@ export const searchEntries = query({
|
|
|
127
191
|
const canUseSearchIndex = kinds === undefined || kinds.length === 1;
|
|
128
192
|
|
|
129
193
|
const result = canUseSearchIndex
|
|
130
|
-
? await
|
|
131
|
-
.query("entries")
|
|
132
|
-
.withSearchIndex("search", (q) => {
|
|
194
|
+
? await paginateAdminSearchIndex(
|
|
195
|
+
ctx.db.query("entries").withSearchIndex("search", (q) => {
|
|
133
196
|
const searched = q.search("searchText", searchQuery);
|
|
134
197
|
const withKind =
|
|
135
198
|
kinds?.length === 1 ? searched.eq("kind", kinds[0]!) : searched;
|
|
@@ -140,8 +203,9 @@ export const searchEntries = query({
|
|
|
140
203
|
return args.priority === undefined
|
|
141
204
|
? withStatus
|
|
142
205
|
: withStatus.eq("priority", args.priority);
|
|
143
|
-
})
|
|
144
|
-
|
|
206
|
+
}),
|
|
207
|
+
args.paginationOpts,
|
|
208
|
+
)
|
|
145
209
|
: await (
|
|
146
210
|
args.priority !== undefined
|
|
147
211
|
? stream(ctx.db, schema)
|
package/src/component/entries.ts
CHANGED
|
@@ -609,6 +609,7 @@ export const update = mutation({
|
|
|
609
609
|
args: {
|
|
610
610
|
actor: actorValidator,
|
|
611
611
|
entryId: v.id("entries"),
|
|
612
|
+
kind: v.optional(entryKindValidator),
|
|
612
613
|
title: v.string(),
|
|
613
614
|
body: v.string(),
|
|
614
615
|
editableByAuthor: v.boolean(),
|
|
@@ -625,6 +626,11 @@ export const update = mutation({
|
|
|
625
626
|
actorIsAdmin(args.actor) ||
|
|
626
627
|
(args.editableByAuthor && entry.actorId === args.actor.id);
|
|
627
628
|
if (!canEdit) throw new ConvexError("Not authorized to edit this entry.");
|
|
629
|
+
if (args.kind !== undefined && args.kind !== entry.kind) {
|
|
630
|
+
if (!actorIsAdmin(args.actor)) {
|
|
631
|
+
throw new ConvexError("Admin access is required to change entry kind.");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
628
634
|
|
|
629
635
|
const title = normalizeRequiredText(
|
|
630
636
|
args.title,
|
|
@@ -634,6 +640,7 @@ export const update = mutation({
|
|
|
634
640
|
const body = normalizeRequiredText(args.body, "Body", args.maxBodyLength);
|
|
635
641
|
|
|
636
642
|
await ctx.db.patch("entries", args.entryId, {
|
|
643
|
+
kind: args.kind ?? entry.kind,
|
|
637
644
|
title,
|
|
638
645
|
body,
|
|
639
646
|
normalizedTitle: normalizeTitle(title),
|
package/src/component/helpers.ts
CHANGED
|
@@ -136,6 +136,8 @@ export async function serializeEntry(
|
|
|
136
136
|
commentCount: entry.commentCount,
|
|
137
137
|
...(entry.updatedAt === undefined ? {} : { updatedAt: entry.updatedAt }),
|
|
138
138
|
viewerHasUpvoted: reaction !== null,
|
|
139
|
+
viewerIsAuthor:
|
|
140
|
+
viewerActorId !== undefined && entry.actorId === viewerActorId,
|
|
139
141
|
...(includeMetadata && entry.metadata !== undefined
|
|
140
142
|
? { metadata: entry.metadata }
|
|
141
143
|
: {}),
|
package/src/component/model.ts
CHANGED
|
@@ -104,6 +104,8 @@ export const publicEntryValidator = v.object({
|
|
|
104
104
|
commentCount: v.number(),
|
|
105
105
|
updatedAt: v.optional(v.number()),
|
|
106
106
|
viewerHasUpvoted: v.boolean(),
|
|
107
|
+
/** Whether the current viewer is the actor who created this entry. */
|
|
108
|
+
viewerIsAuthor: v.optional(v.boolean()),
|
|
107
109
|
metadata: v.optional(feedbackMetadataValidator),
|
|
108
110
|
});
|
|
109
111
|
|
|
@@ -133,6 +135,14 @@ export const adminEntryValidator = publicEntryValidator.extend({
|
|
|
133
135
|
metadata: v.optional(feedbackMetadataValidator),
|
|
134
136
|
});
|
|
135
137
|
|
|
138
|
+
type InferredFeedbackActor = Infer<typeof actorValidator>;
|
|
139
|
+
type InferredRoadmapItem = Infer<typeof roadmapItemValidator>;
|
|
140
|
+
type InferredFeedbackMetadata = Infer<typeof feedbackMetadataValidator>;
|
|
141
|
+
type InferredFeedbackEntry = Infer<typeof publicEntryValidator>;
|
|
142
|
+
type InferredAdminFeedbackEntry = Infer<typeof adminEntryValidator>;
|
|
143
|
+
type InferredFeedbackComment = Infer<typeof publicCommentValidator>;
|
|
144
|
+
type InferredSimilarEntriesResult = Infer<typeof similarEntriesValidator>;
|
|
145
|
+
|
|
136
146
|
/**
|
|
137
147
|
* Category of feedback represented by an entry.
|
|
138
148
|
*
|
|
@@ -175,7 +185,34 @@ export type EntryPriority = Infer<typeof entryPriorityValidator>;
|
|
|
175
185
|
export type RoadmapStatus = Infer<typeof roadmapStatusValidator>;
|
|
176
186
|
|
|
177
187
|
/** Admin-managed roadmap item and its current attached-feedback count. */
|
|
178
|
-
export
|
|
188
|
+
export interface RoadmapItem {
|
|
189
|
+
/** Public component document identifier. */
|
|
190
|
+
id: InferredRoadmapItem["id"];
|
|
191
|
+
|
|
192
|
+
/** Convex document creation timestamp in milliseconds since the Unix epoch. */
|
|
193
|
+
creationTime: InferredRoadmapItem["creationTime"];
|
|
194
|
+
|
|
195
|
+
/** Roadmap item title. */
|
|
196
|
+
title: InferredRoadmapItem["title"];
|
|
197
|
+
|
|
198
|
+
/** Optional roadmap item description. */
|
|
199
|
+
description?: InferredRoadmapItem["description"];
|
|
200
|
+
|
|
201
|
+
/** Current roadmap workflow stage. */
|
|
202
|
+
status: InferredRoadmapItem["status"];
|
|
203
|
+
|
|
204
|
+
/** Relative ordering position among roadmap items. */
|
|
205
|
+
position: InferredRoadmapItem["position"];
|
|
206
|
+
|
|
207
|
+
/** Millisecond timestamp at which the roadmap item was created. */
|
|
208
|
+
createdAt: InferredRoadmapItem["createdAt"];
|
|
209
|
+
|
|
210
|
+
/** Millisecond timestamp of the latest roadmap item update. */
|
|
211
|
+
updatedAt: InferredRoadmapItem["updatedAt"];
|
|
212
|
+
|
|
213
|
+
/** Number of feedback entries currently attached to the roadmap item. */
|
|
214
|
+
feedbackCount: InferredRoadmapItem["feedbackCount"];
|
|
215
|
+
}
|
|
179
216
|
|
|
180
217
|
/**
|
|
181
218
|
* Server-side ordering strategy for comments and replies.
|
|
@@ -192,21 +229,25 @@ export type CommentSort = Infer<typeof commentSortValidator>;
|
|
|
192
229
|
*
|
|
193
230
|
* The component does not access the host application's authentication system
|
|
194
231
|
* directly. The host resolves its current user/session into this shape.
|
|
195
|
-
*
|
|
196
|
-
* @property id
|
|
197
|
-
* Stable identifier for the actor. This can be a Clerk ID, Convex Auth ID,
|
|
198
|
-
* application user ID, anonymous installation ID, or another stable
|
|
199
|
-
* host-controlled identifier.
|
|
200
|
-
*
|
|
201
|
-
* @property isAdmin
|
|
202
|
-
* Whether the actor can perform admin-only actions such as changing entry
|
|
203
|
-
* status or modifying content they do not own. When omitted, the deprecated
|
|
204
|
-
* `isModerator` field is used.
|
|
205
|
-
*
|
|
206
|
-
* @property isModerator
|
|
207
|
-
* @deprecated Use `isAdmin`.
|
|
208
232
|
*/
|
|
209
|
-
export
|
|
233
|
+
export interface FeedbackActor {
|
|
234
|
+
/**
|
|
235
|
+
* Stable identifier for the actor. This can be a Clerk ID, Convex Auth ID,
|
|
236
|
+
* application user ID, anonymous installation ID, or another stable
|
|
237
|
+
* host-controlled identifier.
|
|
238
|
+
*/
|
|
239
|
+
id: InferredFeedbackActor["id"];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether the actor can perform admin-only actions such as changing entry
|
|
243
|
+
* status or modifying content they do not own. When omitted, the deprecated
|
|
244
|
+
* `isModerator` field is used.
|
|
245
|
+
*/
|
|
246
|
+
isAdmin?: InferredFeedbackActor["isAdmin"];
|
|
247
|
+
|
|
248
|
+
/** @deprecated Use `isAdmin`. */
|
|
249
|
+
isModerator?: InferredFeedbackActor["isModerator"];
|
|
250
|
+
}
|
|
210
251
|
|
|
211
252
|
/** Scalar value accepted in entry diagnostic metadata. */
|
|
212
253
|
export type FeedbackMetadataValue = Infer<
|
|
@@ -214,56 +255,111 @@ export type FeedbackMetadataValue = Infer<
|
|
|
214
255
|
>;
|
|
215
256
|
|
|
216
257
|
/** Flat metadata values grouped by their source. */
|
|
217
|
-
export
|
|
258
|
+
export interface FeedbackMetadata {
|
|
259
|
+
/** Standard diagnostic metadata collected by the component. */
|
|
260
|
+
standard?: InferredFeedbackMetadata["standard"];
|
|
218
261
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
/** Additional host-provided diagnostic metadata. */
|
|
263
|
+
additional?: InferredFeedbackMetadata["additional"];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Public representation of a feedback, feature-request, or bug-report entry. */
|
|
267
|
+
export interface FeedbackEntry {
|
|
268
|
+
/** Public component document identifier. */
|
|
269
|
+
id: InferredFeedbackEntry["id"];
|
|
270
|
+
|
|
271
|
+
/** Convex document creation timestamp in milliseconds since the Unix epoch. */
|
|
272
|
+
creationTime: InferredFeedbackEntry["creationTime"];
|
|
273
|
+
|
|
274
|
+
/** Stable identifier of the actor who created the entry. */
|
|
275
|
+
actorId: InferredFeedbackEntry["actorId"];
|
|
276
|
+
|
|
277
|
+
/** Entry category. */
|
|
278
|
+
kind: InferredFeedbackEntry["kind"];
|
|
279
|
+
|
|
280
|
+
/** Current workflow status. */
|
|
281
|
+
status: InferredFeedbackEntry["status"];
|
|
282
|
+
|
|
283
|
+
/** User-provided entry title. */
|
|
284
|
+
title: InferredFeedbackEntry["title"];
|
|
285
|
+
|
|
286
|
+
/** User-provided entry description. */
|
|
287
|
+
body: InferredFeedbackEntry["body"];
|
|
288
|
+
|
|
289
|
+
/** Denormalized number of actors currently upvoting this entry. */
|
|
290
|
+
upvoteCount: InferredFeedbackEntry["upvoteCount"];
|
|
291
|
+
|
|
292
|
+
/** Denormalized total number of comments belonging to the entry, including nested replies. */
|
|
293
|
+
commentCount: InferredFeedbackEntry["commentCount"];
|
|
294
|
+
|
|
295
|
+
/** Millisecond timestamp of the most recent content update. Absent when the entry has never been edited. */
|
|
296
|
+
updatedAt?: InferredFeedbackEntry["updatedAt"];
|
|
297
|
+
|
|
298
|
+
/** Whether the actor associated with the current query has upvoted the entry. `false` when no viewer actor is available. */
|
|
299
|
+
viewerHasUpvoted: InferredFeedbackEntry["viewerHasUpvoted"];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Whether the actor associated with the current query created the entry.
|
|
303
|
+
* `false` when no viewer actor is available. This is optional for compatibility
|
|
304
|
+
* with hosts that have not yet redeployed the updated wrapper.
|
|
305
|
+
*/
|
|
306
|
+
viewerIsAuthor?: InferredFeedbackEntry["viewerIsAuthor"];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Creation-time diagnostic metadata. Present only when `getEntry` is queried
|
|
310
|
+
* by an admin. Ordinary entry lists, searches, and non-admin reads omit this
|
|
311
|
+
* property.
|
|
312
|
+
*/
|
|
313
|
+
metadata?: InferredFeedbackEntry["metadata"];
|
|
314
|
+
}
|
|
264
315
|
|
|
265
316
|
/** Feedback entry enriched with private triage metadata for admin clients. */
|
|
266
|
-
export
|
|
317
|
+
export interface AdminFeedbackEntry {
|
|
318
|
+
/** Public component document identifier. */
|
|
319
|
+
id: InferredAdminFeedbackEntry["id"];
|
|
320
|
+
|
|
321
|
+
/** Convex document creation timestamp in milliseconds since the Unix epoch. */
|
|
322
|
+
creationTime: InferredAdminFeedbackEntry["creationTime"];
|
|
323
|
+
|
|
324
|
+
/** Stable identifier of the actor who created the entry. */
|
|
325
|
+
actorId: InferredAdminFeedbackEntry["actorId"];
|
|
326
|
+
|
|
327
|
+
/** Entry category. */
|
|
328
|
+
kind: InferredAdminFeedbackEntry["kind"];
|
|
329
|
+
|
|
330
|
+
/** Current workflow status. */
|
|
331
|
+
status: InferredAdminFeedbackEntry["status"];
|
|
332
|
+
|
|
333
|
+
/** User-provided entry title. */
|
|
334
|
+
title: InferredAdminFeedbackEntry["title"];
|
|
335
|
+
|
|
336
|
+
/** User-provided entry description. */
|
|
337
|
+
body: InferredAdminFeedbackEntry["body"];
|
|
338
|
+
|
|
339
|
+
/** Denormalized number of actors currently upvoting this entry. */
|
|
340
|
+
upvoteCount: InferredAdminFeedbackEntry["upvoteCount"];
|
|
341
|
+
|
|
342
|
+
/** Denormalized total number of comments belonging to the entry, including nested replies. */
|
|
343
|
+
commentCount: InferredAdminFeedbackEntry["commentCount"];
|
|
344
|
+
|
|
345
|
+
/** Millisecond timestamp of the most recent content update. Absent when the entry has never been edited. */
|
|
346
|
+
updatedAt?: InferredAdminFeedbackEntry["updatedAt"];
|
|
347
|
+
|
|
348
|
+
/** Whether the actor associated with the current query has upvoted the entry. `false` when no viewer actor is available. */
|
|
349
|
+
viewerHasUpvoted: InferredAdminFeedbackEntry["viewerHasUpvoted"];
|
|
350
|
+
|
|
351
|
+
/** Whether the actor associated with the current query created the entry. */
|
|
352
|
+
viewerIsAuthor?: InferredAdminFeedbackEntry["viewerIsAuthor"];
|
|
353
|
+
|
|
354
|
+
/** Creation-time diagnostic metadata returned to admin clients. */
|
|
355
|
+
metadata?: InferredAdminFeedbackEntry["metadata"];
|
|
356
|
+
|
|
357
|
+
/** Internal triage priority. */
|
|
358
|
+
priority?: InferredAdminFeedbackEntry["priority"];
|
|
359
|
+
|
|
360
|
+
/** Roadmap item attached to the entry, when one exists. */
|
|
361
|
+
roadmap?: InferredAdminFeedbackEntry["roadmap"];
|
|
362
|
+
}
|
|
267
363
|
|
|
268
364
|
/**
|
|
269
365
|
* Public representation of a comment or reply.
|
|
@@ -271,47 +367,44 @@ export type AdminFeedbackEntry = Infer<typeof adminEntryValidator>;
|
|
|
271
367
|
* Comments are returned one level at a time. Child comments are not included
|
|
272
368
|
* automatically; query them separately using the comment's `id` as
|
|
273
369
|
* `parentCommentId`.
|
|
274
|
-
*
|
|
275
|
-
* @property id
|
|
276
|
-
* Public component document identifier.
|
|
277
|
-
*
|
|
278
|
-
* @property creationTime
|
|
279
|
-
* Convex document creation timestamp in milliseconds since the Unix epoch.
|
|
280
|
-
*
|
|
281
|
-
* @property entryId
|
|
282
|
-
* Entry this comment belongs to.
|
|
283
|
-
*
|
|
284
|
-
* @property parentCommentId
|
|
285
|
-
* Direct parent comment. Absent for top-level comments.
|
|
286
|
-
*
|
|
287
|
-
* @property actorId
|
|
288
|
-
* Stable identifier of the actor who created the comment.
|
|
289
|
-
*
|
|
290
|
-
* @property depth
|
|
291
|
-
* Zero-based nesting depth. Top-level comments have depth `0`.
|
|
292
|
-
*
|
|
293
|
-
* @property body
|
|
294
|
-
* User-provided comment text. `null` when the comment was soft-deleted so
|
|
295
|
-
* nested replies can retain their place in the conversation.
|
|
296
|
-
*
|
|
297
|
-
* @property likeCount
|
|
298
|
-
* Denormalized number of actors currently liking this comment.
|
|
299
|
-
*
|
|
300
|
-
* @property replyCount
|
|
301
|
-
* Number of direct child replies. Descendants below those direct children are
|
|
302
|
-
* not included in this count.
|
|
303
|
-
*
|
|
304
|
-
* @property updatedAt
|
|
305
|
-
* Millisecond timestamp of the latest edit, when the comment has been edited.
|
|
306
|
-
*
|
|
307
|
-
* @property deletedAt
|
|
308
|
-
* Millisecond timestamp at which the comment was soft-deleted.
|
|
309
|
-
*
|
|
310
|
-
* @property viewerHasLiked
|
|
311
|
-
* Whether the actor associated with the current query likes this comment.
|
|
312
|
-
* `false` when no viewer actor is available.
|
|
313
370
|
*/
|
|
314
|
-
export
|
|
371
|
+
export interface FeedbackComment {
|
|
372
|
+
/** Public component document identifier. */
|
|
373
|
+
id: InferredFeedbackComment["id"];
|
|
374
|
+
|
|
375
|
+
/** Convex document creation timestamp in milliseconds since the Unix epoch. */
|
|
376
|
+
creationTime: InferredFeedbackComment["creationTime"];
|
|
377
|
+
|
|
378
|
+
/** Entry this comment belongs to. */
|
|
379
|
+
entryId: InferredFeedbackComment["entryId"];
|
|
380
|
+
|
|
381
|
+
/** Direct parent comment. Absent for top-level comments. */
|
|
382
|
+
parentCommentId?: InferredFeedbackComment["parentCommentId"];
|
|
383
|
+
|
|
384
|
+
/** Stable identifier of the actor who created the comment. */
|
|
385
|
+
actorId: InferredFeedbackComment["actorId"];
|
|
386
|
+
|
|
387
|
+
/** Zero-based nesting depth. Top-level comments have depth `0`. */
|
|
388
|
+
depth: InferredFeedbackComment["depth"];
|
|
389
|
+
|
|
390
|
+
/** User-provided comment text. `null` when the comment was soft-deleted so nested replies can retain their place in the conversation. */
|
|
391
|
+
body: InferredFeedbackComment["body"];
|
|
392
|
+
|
|
393
|
+
/** Denormalized number of actors currently liking this comment. */
|
|
394
|
+
likeCount: InferredFeedbackComment["likeCount"];
|
|
395
|
+
|
|
396
|
+
/** Number of direct child replies. Descendants below those direct children are not included in this count. */
|
|
397
|
+
replyCount: InferredFeedbackComment["replyCount"];
|
|
398
|
+
|
|
399
|
+
/** Millisecond timestamp of the latest edit, when the comment has been edited. */
|
|
400
|
+
updatedAt?: InferredFeedbackComment["updatedAt"];
|
|
401
|
+
|
|
402
|
+
/** Millisecond timestamp at which the comment was soft-deleted. */
|
|
403
|
+
deletedAt?: InferredFeedbackComment["deletedAt"];
|
|
404
|
+
|
|
405
|
+
/** Whether the actor associated with the current query likes this comment. `false` when no viewer actor is available. */
|
|
406
|
+
viewerHasLiked: InferredFeedbackComment["viewerHasLiked"];
|
|
407
|
+
}
|
|
315
408
|
|
|
316
409
|
/**
|
|
317
410
|
* Duplicate-detection result for a proposed entry.
|
|
@@ -328,12 +421,11 @@ export type FeedbackComment = Infer<typeof publicCommentValidator>;
|
|
|
328
421
|
* - 0 exact matches → `0 exact + at most 3 similar`
|
|
329
422
|
*
|
|
330
423
|
* An entry returned in `exact` is never repeated in `similar`.
|
|
331
|
-
*
|
|
332
|
-
* @property exact
|
|
333
|
-
* Entries whose normalized title exactly matches the proposed title.
|
|
334
|
-
*
|
|
335
|
-
* @property similar
|
|
336
|
-
* Full-text matches ordered by search relevance after exact matches have been
|
|
337
|
-
* removed.
|
|
338
424
|
*/
|
|
339
|
-
export
|
|
425
|
+
export interface SimilarEntriesResult {
|
|
426
|
+
/** Entries whose normalized title exactly matches the proposed title. */
|
|
427
|
+
exact: InferredSimilarEntriesResult["exact"];
|
|
428
|
+
|
|
429
|
+
/** Full-text matches ordered by search relevance after exact matches have been removed. */
|
|
430
|
+
similar: InferredSimilarEntriesResult["similar"];
|
|
431
|
+
}
|
package/src/component/roadmap.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
1
2
|
import {
|
|
2
3
|
paginationOptsValidator,
|
|
3
4
|
paginationResultValidator,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
roadmapStatusValidator,
|
|
22
23
|
} from "./model.js";
|
|
23
24
|
import type { RoadmapStatus } from "./model.js";
|
|
25
|
+
import schema from "./schema.js";
|
|
24
26
|
|
|
25
27
|
const POSITION_STEP = 1_000_000;
|
|
26
28
|
const REBALANCE_GAP_THRESHOLD = 10;
|
|
@@ -128,13 +130,14 @@ export const list = query({
|
|
|
128
130
|
},
|
|
129
131
|
returns: paginationResultValidator(roadmapItemValidator),
|
|
130
132
|
handler: async (ctx, args) => {
|
|
133
|
+
const db = paginator(ctx.db, schema);
|
|
131
134
|
const status = args.status;
|
|
132
135
|
const state =
|
|
133
136
|
status === undefined ? null : await getRebalanceState(ctx, status);
|
|
134
137
|
const visibleGeneration = state?.visibleGeneration;
|
|
135
138
|
const roadmapQuery =
|
|
136
139
|
status !== undefined && visibleGeneration !== undefined
|
|
137
|
-
?
|
|
140
|
+
? db
|
|
138
141
|
.query("roadmap")
|
|
139
142
|
.withIndex("by_status_deleting_at_rebalance_id_position", (q) =>
|
|
140
143
|
q
|
|
@@ -143,12 +146,12 @@ export const list = query({
|
|
|
143
146
|
.eq("rebalanceId", visibleGeneration),
|
|
144
147
|
)
|
|
145
148
|
: status === undefined
|
|
146
|
-
?
|
|
149
|
+
? db
|
|
147
150
|
.query("roadmap")
|
|
148
151
|
.withIndex("by_deleting_at_and_position", (q) =>
|
|
149
152
|
q.eq("deletingAt", undefined),
|
|
150
153
|
)
|
|
151
|
-
:
|
|
154
|
+
: db
|
|
152
155
|
.query("roadmap")
|
|
153
156
|
.withIndex("by_status_deleting_at_position", (q) =>
|
|
154
157
|
q.eq("status", status).eq("deletingAt", undefined),
|
|
@@ -348,11 +351,12 @@ export const rebalanceBatch = internalMutation({
|
|
|
348
351
|
},
|
|
349
352
|
returns: v.null(),
|
|
350
353
|
handler: async (ctx, args) => {
|
|
354
|
+
const db = paginator(ctx.db, schema);
|
|
351
355
|
const state = await getRebalanceState(ctx, args.status);
|
|
352
356
|
if (state?.activeGeneration !== args.rebalanceId) return null;
|
|
353
357
|
|
|
354
358
|
if (args.phase === "mark") {
|
|
355
|
-
const result = await
|
|
359
|
+
const result = await db
|
|
356
360
|
.query("roadmap")
|
|
357
361
|
.withIndex("by_status_deleting_at_position", (q) =>
|
|
358
362
|
q.eq("status", args.status).eq("deletingAt", undefined),
|
|
@@ -391,7 +395,7 @@ export const rebalanceBatch = internalMutation({
|
|
|
391
395
|
return null;
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
const result = await
|
|
398
|
+
const result = await db
|
|
395
399
|
.query("roadmap")
|
|
396
400
|
.withIndex("by_status_rebalance_rank", (q) =>
|
|
397
401
|
q.eq("status", args.status).eq("rebalanceId", args.rebalanceId),
|
|
@@ -610,7 +614,8 @@ export const listFeedback = query({
|
|
|
610
614
|
},
|
|
611
615
|
returns: paginationResultValidator(publicEntryValidator),
|
|
612
616
|
handler: async (ctx, args) => {
|
|
613
|
-
const
|
|
617
|
+
const db = paginator(ctx.db, schema);
|
|
618
|
+
const result = await db
|
|
614
619
|
.query("entries")
|
|
615
620
|
.withIndex("by_roadmap_id", (q) => q.eq("roadmapId", args.roadmapId))
|
|
616
621
|
.order("desc")
|