Skip to content

Commit

Permalink
feat: support record comment (#910)
Browse files Browse the repository at this point in the history
* feat: comment backend initialize

* feat: comment openapi initialize

* feat: support comment reaction and notify

* chore: expose `HoverPortal` component

* chore: update `@teable/icons`

* feat: create comment prisma tempalte

* feat: generate comment open-api

* feat: support comment advance feature

* feat: comment notify user list add quote creator and mention user

* feat: grid support display comment count

* chore: add comment e2e test suit

* fix: fix type definition clash

* chore: add comment migration

* fix: separate partial comment type definition for zod2openapi unknown error

* fix: import `@teable/icons` error when `moduleResolution` set to `Bundler`

* fix: sdk i18n error code

* feat: rename comment subscribe table name and generate migration

* feat: add comment reaction thumbsdown

* fix: add presence unsubscribe judgment relative to comment

* chore: css adjustment

fix: zod2openapi error by separate type define

* fix: comment e2e error

* feat: add composite index for comment list table

* feat: add record comment count query api

* fix: reply to someone should be receive the notification

* fix: comment count inaccurate when add view group condition

* fix: abundant field expand card display

* feat: @ mention user filter self

* fix: not found subscribe throw null
  • Loading branch information
caoxing9 authored Sep 20, 2024
1 parent 39e97c3 commit 5cf73f2
Show file tree
Hide file tree
Showing 141 changed files with 7,510 additions and 73 deletions.
2 changes: 2 additions & 0 deletions apps/nestjs-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuthModule } from './features/auth/auth.module';
import { BaseModule } from './features/base/base.module';
import { ChatModule } from './features/chat/chat.module';
import { CollaboratorModule } from './features/collaborator/collaborator.module';
import { CommentOpenApiModule } from './features/comment/comment-open-api.module';
import { DashboardModule } from './features/dashboard/dashboard.module';
import { ExportOpenApiModule } from './features/export/open-api/export-open-api.module';
import { FieldOpenApiModule } from './features/field/open-api/field-open-api.module';
Expand Down Expand Up @@ -59,6 +60,7 @@ export const appModules = {
TrashModule,
PluginModule,
DashboardModule,
CommentOpenApiModule,
],
providers: [InitBootstrapProvider],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default abstract class StorageAdapter {
case UploadType.Form:
case UploadType.Plugin:
return storageConfig().publicBucket;
case UploadType.Comment:
return storageConfig().privateBucket;
default:
throw new BadRequestException('Invalid upload type');
}
Expand All @@ -34,6 +36,8 @@ export default abstract class StorageAdapter {
return 'import';
case UploadType.Plugin:
return 'plugin';
case UploadType.Comment:
return 'comment';
default:
throw new BadRequestException('Invalid upload type');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { CommentOpenApiController } from './comment-open-api.controller';

describe('CommentOpenApiController', () => {
let controller: CommentOpenApiController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CommentOpenApiController],
}).compile();

controller = module.get<CommentOpenApiController>(CommentOpenApiController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Controller, Get, Post, Body, Param, Patch, Delete, Query } from '@nestjs/common';
import type { ICommentVo, IGetCommentListVo, ICommentSubscribeVo } from '@teable/openapi';
import {
getRecordsRoSchema,
createCommentRoSchema,
ICreateCommentRo,
IUpdateCommentRo,
updateCommentRoSchema,
updateCommentReactionRoSchema,
IUpdateCommentReactionRo,
getCommentListQueryRoSchema,
IGetCommentListQueryRo,
IGetRecordsRo,
} from '@teable/openapi';
import { ZodValidationPipe } from '../../zod.validation.pipe';
import { AttachmentsStorageService } from '../attachments/attachments-storage.service';
import { Permissions } from '../auth/decorators/permissions.decorator';
import { TokenAccess } from '../auth/decorators/token.decorator';
import { TqlPipe } from '../record/open-api/tql.pipe';
import { CommentOpenApiService } from './comment-open-api.service';

@Controller('api/comment/:tableId')
@TokenAccess()
export class CommentOpenApiController {
constructor(
private readonly commentOpenApiService: CommentOpenApiService,
private readonly attachmentsStorageService: AttachmentsStorageService
) {}

@Get('/:recordId/count')
@Permissions('view|read')
async getRecordCommentCount(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string
) {
return this.commentOpenApiService.getRecordCommentCount(tableId, recordId);
}

@Get('/count')
@Permissions('view|read')
async getTableCommentCount(
@Param('tableId') tableId: string,
@Query(new ZodValidationPipe(getRecordsRoSchema), TqlPipe) query: IGetRecordsRo
) {
return this.commentOpenApiService.getTableCommentCount(tableId, query);
}

@Get('/:recordId/attachment/:path')
// eslint-disable-next-line sonarjs/no-duplicate-string
@Permissions('record|read')
async getAttachmentPresignedUrl(@Param('path') path: string) {
const [bucket, token] = path.split('/');
return this.attachmentsStorageService.getPreviewUrlByPath(bucket, path, token);
}

// eslint-disable-next-line sonarjs/no-duplicate-string
@Get('/:recordId/subscribe')
@Permissions('record|read')
async getSubscribeDetail(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string
): Promise<ICommentSubscribeVo | null> {
return this.commentOpenApiService.getSubscribeDetail(tableId, recordId);
}

@Post('/:recordId/subscribe')
@Permissions('record|read')
async subscribeComment(@Param('tableId') tableId: string, @Param('recordId') recordId: string) {
return this.commentOpenApiService.subscribeComment(tableId, recordId);
}

@Delete('/:recordId/subscribe')
@Permissions('record|read')
async unsubscribeComment(@Param('tableId') tableId: string, @Param('recordId') recordId: string) {
return this.commentOpenApiService.unsubscribeComment(tableId, recordId);
}

@Get('/:recordId/list')
@Permissions('record|read')
async getCommentList(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Query(new ZodValidationPipe(getCommentListQueryRoSchema))
getCommentListQueryRo: IGetCommentListQueryRo
): Promise<IGetCommentListVo> {
return this.commentOpenApiService.getCommentList(tableId, recordId, getCommentListQueryRo);
}

@Post('/:recordId/create')
// eslint-disable-next-line sonarjs/no-duplicate-string
@Permissions('record|comment')
async createComment(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Body(new ZodValidationPipe(createCommentRoSchema)) createCommentRo: ICreateCommentRo
) {
return this.commentOpenApiService.createComment(tableId, recordId, createCommentRo);
}

// eslint-disable-next-line sonarjs/no-duplicate-string
@Get('/:recordId/:commentId')
@Permissions('record|read')
async getCommentDetail(@Param('commentId') commentId: string): Promise<ICommentVo | null> {
return this.commentOpenApiService.getCommentDetail(commentId);
}

@Patch('/:recordId/:commentId')
@Permissions('record|comment')
async updateComment(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Param('commentId') commentId: string,
@Body(new ZodValidationPipe(updateCommentRoSchema)) updateCommentRo: IUpdateCommentRo
) {
return this.commentOpenApiService.updateComment(tableId, recordId, commentId, updateCommentRo);
}

@Delete('/:recordId/:commentId')
@Permissions('record|read')
async deleteComment(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Param('commentId') commentId: string
) {
return this.commentOpenApiService.deleteComment(tableId, recordId, commentId);
}

@Delete('/:recordId/:commentId/reaction')
@Permissions('record|comment')
async deleteCommentReaction(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Param('commentId') commentId: string,
@Body(new ZodValidationPipe(updateCommentReactionRoSchema)) reactionRo: IUpdateCommentReactionRo
) {
return this.commentOpenApiService.deleteCommentReaction(
tableId,
recordId,
commentId,
reactionRo
);
}

@Patch('/:recordId/:commentId/reaction')
@Permissions('record|comment')
async updateCommentReaction(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Param('commentId') commentId: string,
@Body(new ZodValidationPipe(updateCommentReactionRoSchema)) reactionRo: IUpdateCommentReactionRo
) {
return this.commentOpenApiService.createCommentReaction(
tableId,
recordId,
commentId,
reactionRo
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { ShareDbModule } from '../../share-db/share-db.module';
import { AttachmentsStorageModule } from '../attachments/attachments-storage.module';
import { NotificationModule } from '../notification/notification.module';
import { RecordOpenApiModule } from '../record/open-api/record-open-api.module';
import { RecordModule } from '../record/record.module';
import { CommentOpenApiController } from './comment-open-api.controller';
import { CommentOpenApiService } from './comment-open-api.service';

@Module({
imports: [
NotificationModule,
RecordOpenApiModule,
AttachmentsStorageModule,
RecordModule,
ShareDbModule,
],
controllers: [CommentOpenApiController],
providers: [CommentOpenApiService],
exports: [CommentOpenApiService],
})
export class CommentOpenApiModule {}
Loading

0 comments on commit 5cf73f2

Please sign in to comment.