mirror of
https://github.com/docmost/docmost.git
synced 2026-05-15 05:04:06 +08:00
feat(EE): resolve comments (#1420)
* feat: resolve comment (EE) * Add resolve to comment mark in editor (EE) * comment ui permissions * sticky comment state tabs (EE) * cleanup * feat: add space_id to comments and allow space admins to delete any comment - Add space_id column to comments table with data migration from pages - Add last_edited_by_id, resolved_by_id, and updated_at columns to comments - Update comment deletion permissions to allow space admins to delete any comment - Backfill space_id on old comments * fix foreign keys
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { type Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// Add last_edited_by_id column to comments table
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.addColumn('last_edited_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
// Add resolved_by_id column to comments table
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.addColumn('resolved_by_id', 'uuid', (col) =>
|
||||
col.references('users.id').onDelete('set null'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
// Add updated_at timestamp column to comments table
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
// Add space_id column to comments table
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.addColumn('space_id', 'uuid', (col) =>
|
||||
col.references('spaces.id').onDelete('cascade'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
// Backfill space_id from the related pages
|
||||
await db
|
||||
.updateTable('comments as c')
|
||||
.set((eb) => ({
|
||||
space_id: eb.ref('p.space_id'),
|
||||
}))
|
||||
.from('pages as p')
|
||||
.whereRef('c.page_id', '=', 'p.id')
|
||||
.execute();
|
||||
|
||||
// Make space_id NOT NULL after populating data
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.alterColumn('space_id', (col) => col.setNotNull())
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('comments')
|
||||
.dropColumn('last_edited_by_id')
|
||||
.execute();
|
||||
await db.schema.alterTable('comments').dropColumn('resolved_by_id').execute();
|
||||
await db.schema.alterTable('comments').dropColumn('updated_at').execute();
|
||||
await db.schema.alterTable('comments').dropColumn('space_id').execute();
|
||||
}
|
||||
@@ -20,12 +20,13 @@ export class CommentRepo {
|
||||
// todo, add workspaceId
|
||||
async findById(
|
||||
commentId: string,
|
||||
opts?: { includeCreator: boolean },
|
||||
opts?: { includeCreator: boolean; includeResolvedBy: boolean },
|
||||
): Promise<Comment> {
|
||||
return await this.db
|
||||
.selectFrom('comments')
|
||||
.selectAll('comments')
|
||||
.$if(opts?.includeCreator, (qb) => qb.select(this.withCreator))
|
||||
.$if(opts?.includeResolvedBy, (qb) => qb.select(this.withResolvedBy))
|
||||
.where('id', '=', commentId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
@@ -35,6 +36,7 @@ export class CommentRepo {
|
||||
.selectFrom('comments')
|
||||
.selectAll('comments')
|
||||
.select((eb) => this.withCreator(eb))
|
||||
.select((eb) => this.withResolvedBy(eb))
|
||||
.where('pageId', '=', pageId)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
@@ -80,7 +82,37 @@ export class CommentRepo {
|
||||
).as('creator');
|
||||
}
|
||||
|
||||
withResolvedBy(eb: ExpressionBuilder<DB, 'comments'>) {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(['users.id', 'users.name', 'users.avatarUrl'])
|
||||
.whereRef('users.id', '=', 'comments.resolvedById'),
|
||||
).as('resolvedBy');
|
||||
}
|
||||
|
||||
async deleteComment(commentId: string): Promise<void> {
|
||||
await this.db.deleteFrom('comments').where('id', '=', commentId).execute();
|
||||
}
|
||||
|
||||
async hasChildren(commentId: string): Promise<boolean> {
|
||||
const result = await this.db
|
||||
.selectFrom('comments')
|
||||
.select((eb) => eb.fn.count('id').as('count'))
|
||||
.where('parentCommentId', '=', commentId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Number(result?.count) > 0;
|
||||
}
|
||||
|
||||
async hasChildrenFromOtherUsers(commentId: string, userId: string): Promise<boolean> {
|
||||
const result = await this.db
|
||||
.selectFrom('comments')
|
||||
.select((eb) => eb.fn.count('id').as('count'))
|
||||
.where('parentCommentId', '=', commentId)
|
||||
.where('creatorId', '!=', userId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Number(result?.count) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -119,11 +119,15 @@ export interface Comments {
|
||||
deletedAt: Timestamp | null;
|
||||
editedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
lastEditedById: string | null;
|
||||
pageId: string;
|
||||
parentCommentId: string | null;
|
||||
resolvedAt: Timestamp | null;
|
||||
resolvedById: string | null;
|
||||
selection: string | null;
|
||||
spaceId: string;
|
||||
type: string | null;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user