From 9226ef0ed909fa4bb6824b4adecdda316a9c93ac Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 24 Oct 2023 17:05:47 +0900 Subject: [PATCH 1/2] refactor: use httpModule instead of axios (#65) * refactor: use httpModule instead of axios --- apps/api/package.json | 1 + apps/api/src/domains/auth/auth.module.ts | 5 + apps/api/src/domains/auth/auth.service.ts | 101 ++++++++++-------- .../providers/auth.service.providers.ts | 8 ++ 4 files changed, 70 insertions(+), 45 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index c54070452..7745d4986 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,6 +27,7 @@ "dependencies": { "@fastify/static": "^6.11.2", "@nestjs-modules/mailer": "^1.9.1", + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^10.2.7", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.2.7", diff --git a/apps/api/src/domains/auth/auth.module.ts b/apps/api/src/domains/auth/auth.module.ts index bbbfe7315..4c278fdaa 100644 --- a/apps/api/src/domains/auth/auth.module.ts +++ b/apps/api/src/domains/auth/auth.module.ts @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; @@ -41,6 +42,10 @@ import { LocalStrategy } from './strategies/local.strategy'; TenantModule, RoleModule, MemberModule, + HttpModule.register({ + timeout: 5000, + maxRedirects: 5, + }), JwtModule.registerAsync({ global: true, imports: [ConfigModule], diff --git a/apps/api/src/domains/auth/auth.service.ts b/apps/api/src/domains/auth/auth.service.ts index bfac6f6da..c76dab1b7 100644 --- a/apps/api/src/domains/auth/auth.service.ts +++ b/apps/api/src/domains/auth/auth.service.ts @@ -14,6 +14,7 @@ * under the License. */ import crypto from 'crypto'; +import { HttpService } from '@nestjs/axios'; import { BadRequestException, Injectable, @@ -21,9 +22,10 @@ import { Logger, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import * as bcrypt from 'bcrypt'; import dayjs from 'dayjs'; +import { catchError, lastValueFrom, map } from 'rxjs'; import { Transactional } from 'typeorm-transactional'; import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service'; @@ -70,6 +72,7 @@ export class AuthService { private readonly tenantService: TenantService, private readonly roleService: RoleService, private readonly memberService: MemberService, + private readonly httpService: HttpService, ) {} async sendEmailCode({ email }: SendEmailCodeDto) { @@ -222,7 +225,7 @@ export class AuthService { return `${oauthConfig.authCodeRequestURL}?${params}`; } - private async getAccessToken(code: string) { + private async getAccessToken(code: string): Promise { const { oauthConfig, useOAuth } = await this.tenantService.findOne(); if (!useOAuth) { @@ -233,35 +236,39 @@ export class AuthService { } const { accessTokenRequestURL, clientId, clientSecret } = oauthConfig; - try { - const { data } = await axios.post( - accessTokenRequestURL, - { - grant_type: 'authorization_code', - code, - redirect_uri: this.REDIRECT_URI, - }, - { - headers: { - Authorization: `Basic ${Buffer.from( - clientId + ':' + clientSecret, - ).toString('base64')}`, - 'Content-Type': 'application/x-www-form-urlencoded', + return await lastValueFrom( + this.httpService + .post( + accessTokenRequestURL, + { + grant_type: 'authorization_code', + code, + redirect_uri: this.REDIRECT_URI, }, - }, - ); - return data.access_token; - } catch (error) { - if (error instanceof AxiosError) { - throw new InternalServerErrorException({ - axiosError: { - ...error.response.data, - status: error.response.status, + { + headers: { + Authorization: `Basic ${Buffer.from( + clientId + ':' + clientSecret, + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, }, - }); - } - throw error; - } + ) + .pipe(map((res) => res.data?.access_token)) + .pipe( + catchError((error) => { + if (error instanceof AxiosError) { + throw new InternalServerErrorException({ + axiosError: { + ...error.response.data, + status: error.response.status, + }, + }); + } + throw error; + }), + ), + ); } private async getEmailByAccessToken(accessToken: string): Promise { @@ -270,22 +277,26 @@ export class AuthService { if (!oauthConfig) { throw new BadRequestException('OAuth Config is required.'); } - try { - const { data } = await axios.get(oauthConfig.userProfileRequestURL, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - return data[oauthConfig.emailKey]; - } catch (error) { - if (error instanceof AxiosError) { - throw new InternalServerErrorException({ - axiosError: { - ...error.response.data, - status: error.response.status, - }, - }); - } - throw error; - } + return await lastValueFrom( + this.httpService + .get(oauthConfig.userProfileRequestURL, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + .pipe(map((res) => res.data?.[oauthConfig.emailKey])) + .pipe( + catchError((error) => { + if (error instanceof AxiosError) { + throw new InternalServerErrorException({ + axiosError: { + ...error.response.data, + status: error.response.status, + }, + }); + } + throw error; + }), + ), + ); } async signInByOAuth(code: string) { diff --git a/apps/api/src/test-utils/providers/auth.service.providers.ts b/apps/api/src/test-utils/providers/auth.service.providers.ts index db843ec44..49106245e 100644 --- a/apps/api/src/test-utils/providers/auth.service.providers.ts +++ b/apps/api/src/test-utils/providers/auth.service.providers.ts @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { HttpService } from '@nestjs/axios'; import { JwtService } from '@nestjs/jwt'; import { ClsService } from 'nestjs-cls'; @@ -49,4 +50,11 @@ export const AuthServiceProviders = [ ...RoleServiceProviders, ...MemberServiceProviders, ClsService, + { + provide: HttpService, + useValue: { + get: jest.fn(), + post: jest.fn(), + }, + }, ]; From 0aad41e3c05d1b9cae9963647f02afb1da81d980 Mon Sep 17 00:00:00 2001 From: Chiyoung Jeong Date: Tue, 24 Oct 2023 17:11:37 +0900 Subject: [PATCH 2/2] feat: feedback detail (#66) * feat: add feedback detail component * fix: jwt expired time, project box, feedback table deletion reload * feat: feedback detail component * fix: auth service spec --- apps/api/.env.example | 5 +- apps/api/README.md | 44 +++--- .../repositories/opensearch.repository.ts | 1 + apps/api/src/configs/jwt.config.ts | 4 + .../api/src/domains/auth/auth.service.spec.ts | 2 + apps/api/src/domains/auth/auth.service.ts | 15 +- apps/api/src/test-utils/util-functions.ts | 3 +- .../ChannelSelectBox/ChannelSelectBox.tsx | 2 +- .../DownloadButton/DownloadButton.tsx | 11 +- .../FeedbackDetail/FeedbackDetail.tsx | 146 ++++++++++++++++++ .../FeedbackTable/FeedbackDetail/index.ts | 16 ++ .../FeedbackTableRow/FeedbackTableRow.tsx | 46 ++++-- .../tables/IssueTable/IssueTable.tsx | 10 +- .../tables/IssueTable/TableRow/TableRow.tsx | 1 + 14 files changed, 247 insertions(+), 59 deletions(-) create mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetail.tsx create mode 100644 apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/index.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 3e52bc44e..ec01b90d1 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -22,4 +22,7 @@ APP_ADDRESS= # default: 0.0.0.0 AUTO_MIGRATION= # default: false MASTER_API_KEY= # default: none -BASE_URL= # default: http://localhost:3000 \ No newline at end of file +BASE_URL= # default: http://localhost:3000 + +ACCESS_TOKEN_EXPIRED_TIME= # default: 10m +REFESH_TOKEN_EXPIRED_TIME= # default: 1h \ No newline at end of file diff --git a/apps/api/README.md b/apps/api/README.md index badd4c0f9..0f27aad85 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -70,27 +70,29 @@ npm run migration:run ## Environment Variables -| Environment | Description | Default Value | -| -------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| JWT_SECRET | JWT secret | # required | -| MYSQL_PRIMARY_URL | mysql url | mysql://userfeedback:userfeedback@localhost:13306/userfeedback | -| MYSQL_SECONDARY_URLS | mysql sub urls (must be json array format) | ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] | -| SMTP_USE | flag for using smtp server (for email verification on creating user) | false | -| SMTP_HOST | smtp server host | localhost | -| SMTP_PORT | smtp server port | 25 | -| SMTP_USERNAME | smtp auth username | | -| SMTP_PASSWORD | smtp auth password | | -| SMTP_SENDER | mail sender email | noreplay@linecorp.com | -| SMTP_BASE_URL | default UserFeedback URL for mail to be redirected | http://localhost:3000 | -| APP_PORT | the post that the server is running on | 4000 | -| APP_ADDRESS | the address that the server is running on | 0.0.0.0 | -| OS_USE | flag for using opensearch (for better performance on searching feedbacks) | false | -| OS_NODE | opensearch node url | http://localhost:9200 | -| OS_USERNAME | opensearch username if exists | | -| OS_PASSWORD | opensearch password if exists | | -| AUTO_MIGRATION | set 'true' if you want to make the database migration automatically | | -| MASTER_API_KEY | set a key if you want to make a master key for creating feedback | | -| NODE_OPTIONS | set some options if you want to add for node execution (e.g. max_old_space_size) | | +| Environment | Description | Default Value | +| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| JWT_SECRET | JWT secret | # required | +| MYSQL_PRIMARY_URL | mysql url | mysql://userfeedback:userfeedback@localhost:13306/userfeedback | +| MYSQL_SECONDARY_URLS | mysql sub urls (must be json array format) | ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] | +| SMTP_USE | flag for using smtp server (for email verification on creating user) | false | +| SMTP_HOST | smtp server host | localhost | +| SMTP_PORT | smtp server port | 25 | +| SMTP_USERNAME | smtp auth username | | +| SMTP_PASSWORD | smtp auth password | | +| SMTP_SENDER | mail sender email | noreplay@linecorp.com | +| SMTP_BASE_URL | default UserFeedback URL for mail to be redirected | http://localhost:3000 | +| APP_PORT | the post that the server is running on | 4000 | +| APP_ADDRESS | the address that the server is running on | 0.0.0.0 | +| OS_USE | flag for using opensearch (for better performance on searching feedbacks) | false | +| OS_NODE | opensearch node url | http://localhost:9200 | +| OS_USERNAME | opensearch username if exists | | +| OS_PASSWORD | opensearch password if exists | | +| AUTO_MIGRATION | set 'true' if you want to make the database migration automatically | | +| MASTER_API_KEY | set a key if you want to make a master key for creating feedback | | +| NODE_OPTIONS | set some options if you want to add for node execution (e.g. max_old_space_size) | | +| ACCESS_TOKEN_EXPIRED_TIME | set expired time of access token | 10m | +| REFESH_TOKEN_EXPIRED_TIME | set expired time of refresh token | 1h | ## Swagger diff --git a/apps/api/src/common/repositories/opensearch.repository.ts b/apps/api/src/common/repositories/opensearch.repository.ts index c33b6f23c..fd89eed06 100644 --- a/apps/api/src/common/repositories/opensearch.repository.ts +++ b/apps/api/src/common/repositories/opensearch.repository.ts @@ -186,6 +186,7 @@ export class OpensearchRepository { await this.opensearchClient.deleteByQuery({ index, body: { query: { terms: { _id: ids } } }, + refresh: true, }); } diff --git a/apps/api/src/configs/jwt.config.ts b/apps/api/src/configs/jwt.config.ts index bf7ac0db9..a3c15bab7 100644 --- a/apps/api/src/configs/jwt.config.ts +++ b/apps/api/src/configs/jwt.config.ts @@ -18,8 +18,12 @@ import * as yup from 'yup'; export const jwtConfigSchema = yup.object({ JWT_SECRET: yup.string().required(), + ACCESS_TOKEN_EXPIRED_TIME: yup.string().default('10m'), + REFESH_TOKEN_EXPIRED_TIME: yup.string().default('1h'), }); export const jwtConfig = registerAs('jwt', () => ({ secret: process.env.JWT_SECRET, + accessTokenExpiredTime: process.env.ACCESS_TOKEN_EXPIRED_TIME, + refreshTokenExpiredTime: process.env.REFESH_TOKEN_EXPIRED_TIME, })); diff --git a/apps/api/src/domains/auth/auth.service.spec.ts b/apps/api/src/domains/auth/auth.service.spec.ts index f7fbb4d84..e60cced01 100644 --- a/apps/api/src/domains/auth/auth.service.spec.ts +++ b/apps/api/src/domains/auth/auth.service.spec.ts @@ -22,6 +22,7 @@ import type { Repository } from 'typeorm'; import { CodeEntity } from '@/shared/code/code.entity'; import { NotVerifiedEmailException } from '@/shared/mailing/exceptions'; +import { TestConfig } from '@/test-utils/util-functions'; import { AuthServiceProviders, MockEmailVerificationMailingService, @@ -52,6 +53,7 @@ describe('auth service ', () => { let apiKeyRepo: Repository; beforeEach(async () => { const module = await Test.createTestingModule({ + imports: [TestConfig], providers: AuthServiceProviders, }).compile(); authService = module.get(AuthService); diff --git a/apps/api/src/domains/auth/auth.service.ts b/apps/api/src/domains/auth/auth.service.ts index c76dab1b7..4ecd21f8a 100644 --- a/apps/api/src/domains/auth/auth.service.ts +++ b/apps/api/src/domains/auth/auth.service.ts @@ -19,8 +19,8 @@ import { BadRequestException, Injectable, InternalServerErrorException, - Logger, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { AxiosError } from 'axios'; import * as bcrypt from 'bcrypt'; @@ -30,6 +30,7 @@ import { Transactional } from 'typeorm-transactional'; import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service'; import { NotVerifiedEmailException } from '@/shared/mailing/exceptions'; +import type { ConfigServiceType } from '@/types/config-service.type'; import { CodeTypeEnum } from '../../shared/code/code-type.enum'; import { CodeService } from '../../shared/code/code.service'; import { ApiKeyService } from '../project/api-key/api-key.service'; @@ -59,7 +60,6 @@ import { PasswordNotMatchException, UserBlockedException } from './exceptions'; @Injectable() export class AuthService { - private logger = new Logger(AuthService.name); private REDIRECT_URI = `${process.env.BASE_URL}/auth/oauth-callback`; constructor( @@ -72,6 +72,7 @@ export class AuthService { private readonly tenantService: TenantService, private readonly roleService: RoleService, private readonly memberService: MemberService, + private readonly configService: ConfigService, private readonly httpService: HttpService, ) {} @@ -172,18 +173,18 @@ export class AuthService { const { email, id, department, name, type } = user; const { state } = await this.userService.findById(id); - if (state === UserStateEnum.Blocked) { - throw new UserBlockedException(); - } + if (state === UserStateEnum.Blocked) throw new UserBlockedException(); + const { accessTokenExpiredTime, refreshTokenExpiredTime } = + this.configService.get('jwt', { infer: true }); return { accessToken: this.jwtService.sign( { sub: id, email, department, name, type }, - { expiresIn: '10m' }, + { expiresIn: accessTokenExpiredTime }, ), refreshToken: this.jwtService.sign( { sub: id, email }, - { expiresIn: '1h' }, + { expiresIn: refreshTokenExpiredTime }, ), }; } diff --git a/apps/api/src/test-utils/util-functions.ts b/apps/api/src/test-utils/util-functions.ts index a772f74e1..b20abacd2 100644 --- a/apps/api/src/test-utils/util-functions.ts +++ b/apps/api/src/test-utils/util-functions.ts @@ -19,6 +19,7 @@ import { ConfigModule } from '@nestjs/config'; import type { DataSource, Repository } from 'typeorm'; import { initializeTransactionalContext } from 'typeorm-transactional'; +import { jwtConfig } from '@/configs/jwt.config'; import { smtpConfig, smtpConfigSchema } from '@/configs/smtp.config'; import type { AuthService } from '@/domains/auth/auth.service'; import { UserDto } from '@/domains/user/dtos'; @@ -33,7 +34,7 @@ export const getMockProvider = ( ): Provider => ({ provide: injectToken, useFactory: () => factory }); export const TestConfig = ConfigModule.forRoot({ - load: [smtpConfig], + load: [smtpConfig, jwtConfig], envFilePath: '.env.test', validate: (config) => ({ ...smtpConfigSchema.validateSync(config), diff --git a/apps/web/src/containers/tables/FeedbackTable/ChannelSelectBox/ChannelSelectBox.tsx b/apps/web/src/containers/tables/FeedbackTable/ChannelSelectBox/ChannelSelectBox.tsx index 28e973e59..d0f49e0f5 100644 --- a/apps/web/src/containers/tables/FeedbackTable/ChannelSelectBox/ChannelSelectBox.tsx +++ b/apps/web/src/containers/tables/FeedbackTable/ChannelSelectBox/ChannelSelectBox.tsx @@ -34,7 +34,7 @@ const ChannelSelectBox: React.FC = ({ onChangeChannel }) => { key={channel.id} onClick={() => onChangeChannel(channel.id)} className={[ - 'flex h-10 min-w-[136px] cursor-pointer items-center gap-2 rounded border px-3 py-2.5', + 'flex h-10 min-w-[136px] cursor-pointer items-center justify-between gap-2 rounded border px-3 py-2.5', channel.id === channelId ? 'border-fill-primary' : 'opacity-50', ].join(' ')} > diff --git a/apps/web/src/containers/tables/FeedbackTable/DownloadButton/DownloadButton.tsx b/apps/web/src/containers/tables/FeedbackTable/DownloadButton/DownloadButton.tsx index a5a6533ab..a2edc3fcb 100644 --- a/apps/web/src/containers/tables/FeedbackTable/DownloadButton/DownloadButton.tsx +++ b/apps/web/src/containers/tables/FeedbackTable/DownloadButton/DownloadButton.tsx @@ -112,10 +112,13 @@ const DownloadButton: React.FC = ({ disabled={!perms.includes('feedback_download_read')} onClick={() => setOpen((prev) => !prev)} > - - {isHead - ? t('main.feedback.button.select-download', { count }) - : t('main.feedback.button.all-download')} +
+ + {isHead + ? t('main.feedback.button.select-download', { count }) + : t('main.feedback.button.all-download')} +
+

diff --git a/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetail.tsx b/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetail.tsx new file mode 100644 index 000000000..0a9df6c8a --- /dev/null +++ b/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/FeedbackDetail.tsx @@ -0,0 +1,146 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { + autoUpdate, + FloatingFocusManager, + FloatingOverlay, + FloatingPortal, + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from '@floating-ui/react'; +import dayjs from 'dayjs'; + +import { Badge, Icon } from '@ufb/ui'; + +import { DATE_TIME_FORMAT } from '@/constants/dayjs-format'; +import { getStatusColor } from '@/constants/issues'; +import { useFeedbackSearch, useOAIQuery } from '@/hooks'; +import type { FieldType } from '@/types/field.type'; +import type { IssueType } from '@/types/issue.type'; + +interface IProps { + id: number; + projectId: number; + channelId: number; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const FeedbackDetail: React.FC = (props) => { + const { channelId, id, projectId, onOpenChange, open } = props; + const { data } = useFeedbackSearch(projectId, channelId, { + query: { ids: [id] }, + }); + const feedbackData = data?.items?.[0] ?? {}; + + const { data: channelData } = useOAIQuery({ + path: '/api/projects/{projectId}/channels/{channelId}', + variables: { channelId, projectId }, + }); + + const { refs, context } = useFloating({ + open, + onOpenChange, + whileElementsMounted: autoUpdate, + }); + const click = useClick(context); + const dismiss = useDismiss(context); + const role = useRole(context); + + const { getFloatingProps } = useInteractions([click, dismiss, role]); + + return ( + + + +

e.stopPropagation()} + > +
+
+

피드백 상세

+ +
+ + + + + + {channelData?.fields.sort(fieldSortType).map((field) => ( + + + + + ))} + +
+ {field.name} + + {field.key === 'issues' ? ( +
+ {( + feedbackData[field.key] ?? ([] as IssueType[]) + ).map((v) => ( + + {v.name} + + ))} +
+ ) : field.format === 'multiSelect' ? ( + (feedbackData[field.key] ?? ([] as string[])).join( + ', ', + ) + ) : field.format === 'date' ? ( + dayjs(feedbackData[field.key]).format( + DATE_TIME_FORMAT, + ) + ) : ( + feedbackData[field.key] + )} +
+
+
+ + + + ); +}; +const fieldSortType = (a: FieldType, b: FieldType) => { + const aNum = a.type === 'DEFAULT' ? 1 : a.type === 'API' ? 2 : 3; + const bNum = b.type === 'DEFAULT' ? 1 : b.type === 'API' ? 2 : 3; + return aNum - bNum; +}; + +export default FeedbackDetail; diff --git a/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/index.ts b/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/index.ts new file mode 100644 index 000000000..1dddb9cc5 --- /dev/null +++ b/apps/web/src/containers/tables/FeedbackTable/FeedbackDetail/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export { default } from './FeedbackDetail'; diff --git a/apps/web/src/containers/tables/FeedbackTable/FeedbackTableRow/FeedbackTableRow.tsx b/apps/web/src/containers/tables/FeedbackTable/FeedbackTableRow/FeedbackTableRow.tsx index 2b84e137b..65aa4ce83 100644 --- a/apps/web/src/containers/tables/FeedbackTable/FeedbackTableRow/FeedbackTableRow.tsx +++ b/apps/web/src/containers/tables/FeedbackTable/FeedbackTableRow/FeedbackTableRow.tsx @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { Row } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; import dayjs from 'dayjs'; @@ -26,11 +26,12 @@ import { DATE_FORMAT } from '@/constants/dayjs-format'; import { useOAIMutation, usePermissions } from '@/hooks'; import useTableStore from '@/zustand/table.store'; import { TableRow } from '../../IssueTable/TableRow'; +import FeedbackDetail from '../FeedbackDetail'; interface IProps { row: Row; channelId: number; - refetch: () => void; + refetch: () => Promise; projectId: number; } @@ -41,8 +42,11 @@ const FeedbackTableRow: React.FC = ({ refetch, }) => { const { t } = useTranslation(); + const [openId, setOpenId] = useState(); + const { disableEditState, enableEditState, editableState, editInput } = useTableStore(); + const perms = usePermissions(); useEffect(() => { disableEditState(); @@ -54,8 +58,8 @@ const FeedbackTableRow: React.FC = ({ pathParams: { projectId, channelId, feedbackId: row.original.id }, queryOptions: { async onSuccess() { + await refetch(); toast.positive({ title: t('toast.save') }); - refetch(); }, onError(error) { toast.negative({ title: error?.message ?? 'Error' }); @@ -79,10 +83,15 @@ const FeedbackTableRow: React.FC = ({ mutate(editInput as any); disableEditState(); }; + const onOpenChange = (open: boolean) => + setOpenId(open ? row.original.id : undefined); + + const open = openId === row.original.id; return ( onOpenChange(true) : undefined} hoverElement={ <> = ({ /> {editableState !== row.original.id ? ( <> - diff --git a/apps/web/src/containers/tables/IssueTable/TableRow/TableRow.tsx b/apps/web/src/containers/tables/IssueTable/TableRow/TableRow.tsx index 0f7475d02..72d510b24 100644 --- a/apps/web/src/containers/tables/IssueTable/TableRow/TableRow.tsx +++ b/apps/web/src/containers/tables/IssueTable/TableRow/TableRow.tsx @@ -50,6 +50,7 @@ const TableRow: React.FC = (props) => { className={[ 'hover:bg-fill-quaternary', isSelected ? 'bg-fill-quaternary' : '', + otherProps.onClick ? 'cursor-pointer' : '', ].join(' ')} {...otherProps} >