From 9c00b5eea1c31bf449fa14adea427ac04b5a4cdd Mon Sep 17 00:00:00 2001 From: chavda-bhavik Date: Tue, 26 Dec 2023 09:58:11 +0530 Subject: [PATCH] feat: Added Grouping functionality for Review Data --- apps/api/src/app/review/review.controller.ts | 14 +++-- .../do-review/re-review-data.usecase.ts | 2 +- .../get-upload-data.usecase.ts | 12 +++-- .../widget/Phases/Phase3/Phase3.tsx | 53 +++++++----------- .../widget/Phases/Phase3/Styles.tsx | 34 ------------ apps/widget/src/config/texts.config.ts | 3 -- .../src/design-system/Modal/Modal.style.ts | 1 - .../SegmentedControl.styles.tsx | 15 ++++++ .../SegmentedControl/SegmentedControl.tsx | 36 +++++++++++++ .../design-system/SegmentedControl/index.ts | 1 + apps/widget/src/hooks/Phase3/usePhase3.tsx | 54 +++++++++++++------ libs/dal/src/dal.service.ts | 10 +++- libs/shared/src/types/index.ts | 1 + libs/shared/src/types/review/index.ts | 1 + libs/shared/src/types/review/review.types.ts | 5 ++ packages/client/src/api/api.service.ts | 18 ++++--- 16 files changed, 155 insertions(+), 105 deletions(-) delete mode 100644 apps/widget/src/components/widget/Phases/Phase3/Styles.tsx create mode 100644 apps/widget/src/design-system/SegmentedControl/SegmentedControl.styles.tsx create mode 100644 apps/widget/src/design-system/SegmentedControl/SegmentedControl.tsx create mode 100644 apps/widget/src/design-system/SegmentedControl/index.ts create mode 100644 libs/shared/src/types/review/index.ts create mode 100644 libs/shared/src/types/review/review.types.ts diff --git a/apps/api/src/app/review/review.controller.ts b/apps/api/src/app/review/review.controller.ts index d616462d3..2bb52c315 100644 --- a/apps/api/src/app/review/review.controller.ts +++ b/apps/api/src/app/review/review.controller.ts @@ -5,7 +5,7 @@ import { APIMessages } from '@shared/constants'; import { RecordEntity, UploadEntity } from '@impler/dal'; import { JwtAuthGuard } from '@shared/framework/auth.gaurd'; import { validateUploadStatus } from '@shared/helpers/upload.helpers'; -import { Defaults, ACCESS_KEY_NAME, UploadStatusEnum } from '@impler/shared'; +import { Defaults, ACCESS_KEY_NAME, UploadStatusEnum, ReviewDataTypesEnum } from '@impler/shared'; import { DoReview, @@ -53,6 +53,13 @@ export class ReviewController { type: Number, description: 'Size of data to return', }) + @ApiQuery({ + name: 'type', + required: false, + type: String, + enum: ReviewDataTypesEnum, + description: 'Type of data filter to apply', + }) @ApiOkResponse({ description: 'Paginated reviewed data', type: PaginationResponseDto, @@ -60,14 +67,15 @@ export class ReviewController { async getReview( @Param('uploadId') _uploadId: string, @Query('page') page = Defaults.ONE, - @Query('limit') limit = Defaults.PAGE_LIMIT + @Query('limit') limit = Defaults.PAGE_LIMIT, + @Query('type') type = ReviewDataTypesEnum.ALL ) { const uploadData = await this.getUpload.execute({ uploadId: _uploadId, }); if (!uploadData) throw new BadRequestException(APIMessages.UPLOAD_NOT_FOUND); - return await this.getFileInvalidData.execute(_uploadId, page, limit, uploadData.totalRecords); + return await this.getFileInvalidData.execute(_uploadId, page, limit, uploadData.totalRecords, type); } @Post(':uploadId') diff --git a/apps/api/src/app/review/usecases/do-review/re-review-data.usecase.ts b/apps/api/src/app/review/usecases/do-review/re-review-data.usecase.ts index 798c05c2f..cd12459ea 100644 --- a/apps/api/src/app/review/usecases/do-review/re-review-data.usecase.ts +++ b/apps/api/src/app/review/usecases/do-review/re-review-data.usecase.ts @@ -64,7 +64,7 @@ export class DoReReview extends BaseReview { const uniqueFields = (JSON.parse(uploadInfo.customSchema) as ITemplateSchemaItem[]) .filter((column) => column.isUnique) .map((column) => column.key); - const uniqueFieldData = await this.dalService.getFieldData(_uploadId, uniqueFields); + const uniqueFieldData = uniqueFields.length ? await this.dalService.getFieldData(_uploadId, uniqueFields) : []; uniqueFieldData.forEach((item) => { uniqueFields.forEach((field) => { diff --git a/apps/api/src/app/review/usecases/get-upload-data/get-upload-data.usecase.ts b/apps/api/src/app/review/usecases/get-upload-data/get-upload-data.usecase.ts index 1ddd04c45..b16880c86 100644 --- a/apps/api/src/app/review/usecases/get-upload-data/get-upload-data.usecase.ts +++ b/apps/api/src/app/review/usecases/get-upload-data/get-upload-data.usecase.ts @@ -1,13 +1,19 @@ import { Injectable } from '@nestjs/common'; import { DalService } from '@impler/dal'; -import { PaginationResult } from '@impler/shared'; +import { PaginationResult, ReviewDataTypesEnum } from '@impler/shared'; @Injectable() export class GetUploadData { constructor(private dalService: DalService) {} - async execute(_uploadId: string, page: number, limit: number, totalRecords: number): Promise { - const data = await this.dalService.getRecords(_uploadId, page, limit); + async execute( + _uploadId: string, + page: number, + limit: number, + totalRecords: number, + type: ReviewDataTypesEnum + ): Promise { + const data = await this.dalService.getRecords(_uploadId, page, limit, type); return { data, diff --git a/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx b/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx index eebd424ca..0a8f3d991 100644 --- a/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx +++ b/apps/widget/src/components/widget/Phases/Phase3/Phase3.tsx @@ -1,20 +1,19 @@ -import { Group, Text } from '@mantine/core'; +import { Stack } from '@mantine/core'; import { HotTable } from '@handsontable/react'; import { useRef, useState, useEffect } from 'react'; import { PhasesEum } from '@types'; -import { colors, TEXTS, variables } from '@config'; -import { IUpload, numberFormatter, replaceVariablesInString } from '@impler/shared'; -import { CheckIcon, Warning } from '@icons'; +import { IUpload, numberFormatter } from '@impler/shared'; +import { logAmplitudeEvent } from '@amplitude'; import { usePhase3 } from '@hooks/Phase3/usePhase3'; -import useStyles from './Styles'; -import { Pagination } from '@ui/Pagination'; import { ConfirmModal } from '../ConfirmModal'; import { Table } from 'components/Common/Table'; import { Footer } from 'components/Common/Footer'; + +import { Pagination } from '@ui/Pagination'; import { LoadingOverlay } from '@ui/LoadingOverlay'; -import { logAmplitudeEvent } from '@amplitude'; +import { SegmentedControl } from '@ui/SegmentedControl'; interface IPhase3Props { onNextClick: (uploadData: IUpload) => void; @@ -22,15 +21,16 @@ interface IPhase3Props { } export function Phase3(props: IPhase3Props) { - const { classes } = useStyles(); const tableRef = useRef(null); const { onNextClick, onPrevClick } = props; const { page, + type, headings, columnDefs, totalPages, reviewData, + onTypeChange, reReviewData, updateRecord, onPageChange, @@ -53,7 +53,7 @@ export function Phase3(props: IPhase3Props) { useEffect(() => { // setting wrapper height setTableWrapperDimentions({ - height: tableWrapperRef.current.getBoundingClientRect().height - 40, + height: tableWrapperRef.current.getBoundingClientRect().height - 50, width: tableWrapperRef.current.getBoundingClientRect().width, }); }, []); @@ -66,32 +66,15 @@ export function Phase3(props: IPhase3Props) { return ( <> - {typeof invalidRecords === 'undefined' || typeof totalRecords === 'undefined' ? null : ( - - {invalidRecords === variables.baseIndex ? ( - <> - - - {replaceVariablesInString(TEXTS.PHASE3.VALID_DATA_INFO, { - total: numberFormatter(totalRecords), - })} - - - ) : ( - <> - - - {replaceVariablesInString(TEXTS.PHASE3.INVALID_DATA_INFO, { - total: numberFormatter(totalRecords), - invalid: numberFormatter(invalidRecords), - })} - - - )} - - )} -
+ + - +
({ - flexDirection: 'column', - width: '100%', - alignItems: 'unset', -}); - -export const getTextContainerStyles = (): React.CSSProperties => ({ - justifyContent: 'space-between', -}); - -export const getWarningIconStyles = (): React.CSSProperties => ({ - backgroundColor: colors.lightDanger, - borderRadius: '100%', - padding: 2, -}); -export const getSuccessIconStyles = (): CSSObject => ({ - backgroundColor: colors.lightSuccess, - borderRadius: '100%', - padding: 2, - height: 20, - color: colors.success, -}); - -export default createStyles((): Record => { - return { - container: getContainerStyles(), - textContainer: getTextContainerStyles(), - warningIcon: getWarningIconStyles(), - successIcon: getSuccessIconStyles(), - }; -}); diff --git a/apps/widget/src/config/texts.config.ts b/apps/widget/src/config/texts.config.ts index 2b0625cf6..bd84f850d 100644 --- a/apps/widget/src/config/texts.config.ts +++ b/apps/widget/src/config/texts.config.ts @@ -34,9 +34,6 @@ export const TEXTS = { NAME_IN_SHEET_TITLE: 'Column in your sheet', }, PHASE3: { - VALID_DATA_INFO: 'All {total} row(s) are found valid!', - INVALID_DATA_INFO: - 'Out of {total} row(s), {invalid} row(s) have invalid data. Please update the data and try again.', EXPORT_DATA: 'Export Data', RE_REVIEW_DATA: 'Re-Review Data', COMPLETE: 'Complete', diff --git a/apps/widget/src/design-system/Modal/Modal.style.ts b/apps/widget/src/design-system/Modal/Modal.style.ts index d25e6fe36..7376c464a 100644 --- a/apps/widget/src/design-system/Modal/Modal.style.ts +++ b/apps/widget/src/design-system/Modal/Modal.style.ts @@ -3,7 +3,6 @@ import { createStyles, MantineTheme } from '@mantine/core'; import React from 'react'; export const getHeaderStyles = (theme: MantineTheme): React.CSSProperties => ({ - marginBottom: theme.spacing.xs, marginRight: 0, }); diff --git a/apps/widget/src/design-system/SegmentedControl/SegmentedControl.styles.tsx b/apps/widget/src/design-system/SegmentedControl/SegmentedControl.styles.tsx new file mode 100644 index 000000000..932b5de66 --- /dev/null +++ b/apps/widget/src/design-system/SegmentedControl/SegmentedControl.styles.tsx @@ -0,0 +1,15 @@ +import { colors } from '../../config/colors.config'; +import { createStyles } from '@mantine/core'; + +export default createStyles(() => { + return { + label: { + marginBottom: 0, + }, + control: { + '&:last-of-type .mantine-SegmentedControl-label': { + color: colors.red, + }, + }, + }; +}); diff --git a/apps/widget/src/design-system/SegmentedControl/SegmentedControl.tsx b/apps/widget/src/design-system/SegmentedControl/SegmentedControl.tsx new file mode 100644 index 000000000..f0c3736dd --- /dev/null +++ b/apps/widget/src/design-system/SegmentedControl/SegmentedControl.tsx @@ -0,0 +1,36 @@ +import { ReviewDataTypesEnum } from '@impler/shared'; +import { SegmentedControl as MantineSegmentedControl } from '@mantine/core'; + +import useStyles from './SegmentedControl.styles'; + +interface SegmentedControlProps { + value: string; + allDataLength?: string; + validDataLength?: string; + invalidDataLength?: string; + onChange: (value: ReviewDataTypesEnum) => void; +} + +export function SegmentedControl({ + onChange, + value, + allDataLength = '', + validDataLength = '', + invalidDataLength = '', +}: SegmentedControlProps) { + const { classes } = useStyles(); + + return ( + + ); +} diff --git a/apps/widget/src/design-system/SegmentedControl/index.ts b/apps/widget/src/design-system/SegmentedControl/index.ts new file mode 100644 index 000000000..efea3d34f --- /dev/null +++ b/apps/widget/src/design-system/SegmentedControl/index.ts @@ -0,0 +1 @@ +export * from './SegmentedControl'; diff --git a/apps/widget/src/hooks/Phase3/usePhase3.tsx b/apps/widget/src/hooks/Phase3/usePhase3.tsx index a7a512dbb..763f246a9 100644 --- a/apps/widget/src/hooks/Phase3/usePhase3.tsx +++ b/apps/widget/src/hooks/Phase3/usePhase3.tsx @@ -7,7 +7,15 @@ import { HotItemSchema } from '@types'; import { logAmplitudeEvent } from '@amplitude'; import { useAPIState } from '@store/api.context'; import { useAppState } from '@store/app.context'; -import { ColumnTypesEnum, ISchemaColumn, IErrorObject, IReviewData, IUpload, IRecord } from '@impler/shared'; +import { + ColumnTypesEnum, + ISchemaColumn, + IErrorObject, + IReviewData, + IUpload, + IRecord, + ReviewDataTypesEnum, +} from '@impler/shared'; interface IUsePhase3Props { onNext: (uploadData: IUpload) => void; @@ -23,6 +31,7 @@ export function usePhase3({ onNext }: IUsePhase3Props) { const [reviewData, setReviewData] = useState([]); const [columnDefs, setColumnDefs] = useState([]); const [totalPages, setTotalPages] = useState(defaultPage); + const [type, setType] = useState(ReviewDataTypesEnum.ALL); const [showAllDataValidModal, setShowAllDataValidModal] = useState(undefined); useQuery( @@ -101,21 +110,26 @@ export function usePhase3({ onNext }: IUsePhase3Props) { const { mutate: refetchReviewData, isLoading: isReviewDataLoading } = useMutation< IReviewData, IErrorObject, - number, + [number, string], [string] - >(['review'], (reviewPageNumber) => api.getReviewData(uploadInfo._id, reviewPageNumber), { - onSuccess(data) { - setReviewData(data.data); - logAmplitudeEvent('VALIDATE', { - invalidRecords: data.totalRecords, - }); - setPage(Number(data.page)); - setTotalPages(data.totalPages); - }, - onError(error: IErrorObject) { - notifier.showError({ message: error.message, title: error.error }); - }, - }); + >( + ['review'], + ([reviewPageNumber, reviewDataType]) => + api.getReviewData({ uploadId: uploadInfo._id, page: reviewPageNumber, type: reviewDataType }), + { + onSuccess(data) { + setReviewData(data.data); + logAmplitudeEvent('VALIDATE', { + invalidRecords: data.totalRecords, + }); + setPage(Number(data.page)); + setTotalPages(data.totalPages); + }, + onError(error: IErrorObject) { + notifier.showError({ message: error.message, title: error.error }); + }, + } + ); const { refetch: reReviewData, isFetching: isDoReviewLoading } = useQuery< unknown, IErrorObject, @@ -126,7 +140,7 @@ export function usePhase3({ onNext }: IUsePhase3Props) { staleTime: 0, onSuccess() { fetchUploadInfo(); - refetchReviewData(defaultPage); + refetchReviewData([defaultPage, type]); }, onError(error: IErrorObject) { notifier.showError({ message: error.message, title: error.error }); @@ -160,18 +174,24 @@ export function usePhase3({ onNext }: IUsePhase3Props) { api.updateRecord(uploadInfo._id, record) ); + const onTypeChange = (newType: ReviewDataTypesEnum) => { + setType(newType); + refetchReviewData([page, newType]); + }; const onPageChange = (newPageNumber: number) => { - refetchReviewData(newPageNumber); + refetchReviewData([newPageNumber, type]); }; return { page, + type, headings, totalPages, columnDefs, reReviewData, updateRecord, onPageChange, + onTypeChange, setReviewData, isDoReviewLoading, isReviewDataLoading, diff --git a/libs/dal/src/dal.service.ts b/libs/dal/src/dal.service.ts index 353c1aa47..0f5bd3cb4 100644 --- a/libs/dal/src/dal.service.ts +++ b/libs/dal/src/dal.service.ts @@ -45,13 +45,19 @@ export class DalService { await model.collection.drop(); } - async getRecords(_uploadId: string, page: number, limit: number): Promise { + async getRecords( + _uploadId: string, + page: number, + limit: number, + type: 'all' | 'valid' | 'invalid' = 'all' + ): Promise { const model = this.getRecordCollection(_uploadId); if (!model) return []; + const conditions = type === 'all' ? {} : type === 'invalid' ? { isValid: false } : { isValid: { $exists: false } }; return model - .find({}, 'index isValid errors record updated') + .find(conditions, 'index isValid errors record updated') .skip((page - 1) * limit) .limit(limit) .exec(); diff --git a/libs/shared/src/types/index.ts b/libs/shared/src/types/index.ts index 33633c455..3f44525cf 100644 --- a/libs/shared/src/types/index.ts +++ b/libs/shared/src/types/index.ts @@ -6,3 +6,4 @@ export * from './widget/widget.types'; export * from './auth'; export * from './project/project.types'; export * from './environment/environment.types'; +export * from './review'; diff --git a/libs/shared/src/types/review/index.ts b/libs/shared/src/types/review/index.ts new file mode 100644 index 000000000..a200a9f61 --- /dev/null +++ b/libs/shared/src/types/review/index.ts @@ -0,0 +1 @@ +export * from './review.types'; diff --git a/libs/shared/src/types/review/review.types.ts b/libs/shared/src/types/review/review.types.ts new file mode 100644 index 000000000..7a7ef4f49 --- /dev/null +++ b/libs/shared/src/types/review/review.types.ts @@ -0,0 +1,5 @@ +export enum ReviewDataTypesEnum { + 'ALL' = 'all', + 'VALID' = 'valid', + 'INVALID' = 'invalid', +} diff --git a/packages/client/src/api/api.service.ts b/packages/client/src/api/api.service.ts index ad8e0cb40..633755b7a 100644 --- a/packages/client/src/api/api.service.ts +++ b/packages/client/src/api/api.service.ts @@ -103,12 +103,18 @@ export class ApiService { return this.httpClient.post(`/review/${uploadId}`); } - async getReviewData( - uploadId: string, - page?: number, - limit?: number - ): Promise { - const queryString = this.constructQueryString({ limit, page }); + async getReviewData({ + uploadId, + page, + limit, + type, + }: { + uploadId: string; + page?: number; + limit?: number; + type?: string; + }): Promise { + const queryString = this.constructQueryString({ limit, page, type }); return this.httpClient.get( `/review/${uploadId}${queryString}`