diff --git a/.github/workflows/qe-dispatch.yml b/.github/workflows/qe-dispatch.yml index 9de5e086..96a6fb21 100644 --- a/.github/workflows/qe-dispatch.yml +++ b/.github/workflows/qe-dispatch.yml @@ -13,7 +13,7 @@ on: jobs: quality-engineering: name: QE - uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2 + uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2.1.14 with: cypress: true cyRunnerBranch: ${{ inputs.cyRunnerBranch }} diff --git a/.github/workflows/qe-pull-request-target.yml b/.github/workflows/qe-pull-request-target.yml index 812e8274..6ce62aea 100644 --- a/.github/workflows/qe-pull-request-target.yml +++ b/.github/workflows/qe-pull-request-target.yml @@ -11,7 +11,7 @@ on: jobs: quality-engineering: name: QE - uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2 + uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2.1.14 with: danger: true dangerRequireChangelog: false diff --git a/.github/workflows/qe-pull-request.yml b/.github/workflows/qe-pull-request.yml index b7af9f62..2d88cb6f 100644 --- a/.github/workflows/qe-pull-request.yml +++ b/.github/workflows/qe-pull-request.yml @@ -14,7 +14,7 @@ on: jobs: quality-engineering: name: QE - uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2 + uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2.1.14 with: danger: true dangerRequireChangelog: false diff --git a/.github/workflows/qe-push.yml b/.github/workflows/qe-push.yml index 6498a382..e2274a54 100644 --- a/.github/workflows/qe-push.yml +++ b/.github/workflows/qe-push.yml @@ -9,7 +9,7 @@ on: jobs: quality-engineering: name: QE - uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2 + uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2.1.14 with: nodeLint: true nodeTest: false diff --git a/.github/workflows/qe-schedule.yml b/.github/workflows/qe-schedule.yml index 893c675a..f6b0a4d9 100644 --- a/.github/workflows/qe-schedule.yml +++ b/.github/workflows/qe-schedule.yml @@ -7,7 +7,7 @@ on: jobs: quality-engineering: name: QE - uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2 + uses: vtex-apps/usqa/.github/workflows/quality-engineering.yml@v2.1.14 with: cypress: true cyRunnerTimeOut: 45 diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ef1a67..0c0bb425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. Arabic, Bulgarian, Catalan, Czech, Danish, German, Greek, English, Spanish, Finnish, French, Indonesian, Italian, Japanese, Korean, Dutch, Norwegian, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Swedish, Thai, and Ukrainian translations. +## [1.30.0] - 2024-01-26 + +### Added + +- Refactor Bulk import Uploading Modal to Async Validation + ## [1.29.2] - 2024-01-12 ### Fixed diff --git a/manifest.json b/manifest.json index fd73ea83..7fef7900 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "b2b-organizations", "vendor": "vtex", - "version": "1.29.2", + "version": "1.30.0", "title": "B2B Organizations", "description": "App to create and manage B2B Organizations and Cost Centers", "mustUpdateAt": "2022-08-28", diff --git a/react/components/CreateOrganizationButton/CreateOrganizationButton.tsx b/react/components/CreateOrganizationButton/CreateOrganizationButton.tsx index 8bca5f32..ec054427 100644 --- a/react/components/CreateOrganizationButton/CreateOrganizationButton.tsx +++ b/react/components/CreateOrganizationButton/CreateOrganizationButton.tsx @@ -25,6 +25,7 @@ import type { } from '../../types/BulkImport' import useStartBulkImport from '../../hooks/useStartBulkImport' import ReportDownloadLink from '../ReportDownloadLink/ReportDownloadLink' +import { ValidationScreen } from '../UploadingScreen' const CreateOrganizationButton = () => { const { formatMessage } = useTranslate() @@ -70,6 +71,7 @@ const CreateOrganizationButton = () => { onOpenChange={setUploadModalOpen} uploadFile={uploadBulkImportFile} onUploadFinish={handleUploadFinish} + uploadingScreen={props => } errorScreen={props => ( )} diff --git a/react/components/ImportReportModal/ImportReportModal.tsx b/react/components/ImportReportModal/ImportReportModal.tsx index f39cb9fd..e036437a 100644 --- a/react/components/ImportReportModal/ImportReportModal.tsx +++ b/react/components/ImportReportModal/ImportReportModal.tsx @@ -28,7 +28,7 @@ const ImportReportModal = ({ }: ImportReportModalProps) => { const { translate: t, formatDate } = useTranslate() - const { data, error } = useBulkImportDetailsQuery(importId) + const { data, error } = useBulkImportDetailsQuery({ importId }) const reportDownloadLink = data?.importResult?.reportDownloadLink diff --git a/react/components/UploadingScreen/UploadingScreen.tsx b/react/components/UploadingScreen/UploadingScreen.tsx new file mode 100644 index 00000000..7f205dc1 --- /dev/null +++ b/react/components/UploadingScreen/UploadingScreen.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react' +import { UploadingScreen as BulkImportUploadingScreen } from '@vtex/bulk-import-ui' + +import type { UploadFileData } from '../../types/BulkImport' +import ValidatingScreen from './ValidatingScreen' +import useValidateBulkImport from '../../hooks/useValidateBulkImport' + +export type UploadingScreenProps = { + name: string + size: number + uploadFile: () => Promise + onUploadFinished: (data: UploadFileData) => void +} + +export type UploadingStep = 'UPLOADING' | 'VALIDATING' + +const UploadingScreen = ({ + uploadFile, + onUploadFinished: onUploadFinishedProp, + ...otherProps +}: UploadingScreenProps) => { + const [step, setStep] = useState('UPLOADING') + + const [importId, setImportId] = useState(undefined) + + const { startBulkImportValidation } = useValidateBulkImport({ + onSuccess: () => { + setStep('VALIDATING') + }, + }) + + const onUploadFinished = (data: UploadFileData) => { + if (data.status === 'error') { + onUploadFinishedProp(data) + + return + } + + startBulkImportValidation({ importId: data?.data?.fileData?.importId }) + setImportId(data?.data?.fileData?.importId) + } + + return step === 'UPLOADING' ? ( + + ) : ( + + ) +} + +export default UploadingScreen diff --git a/react/components/UploadingScreen/ValidatingScreen.tsx b/react/components/UploadingScreen/ValidatingScreen.tsx new file mode 100644 index 00000000..979ce77c --- /dev/null +++ b/react/components/UploadingScreen/ValidatingScreen.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { Flex, Spinner, Text, csx } from '@vtex/admin-ui' + +import { useTranslate } from '../../hooks' +import { bytesToSize } from '../utils/bytesToSize' +import type { UploadFileData } from '../../types/BulkImport' +import useBulkImportDetailsQuery from '../../hooks/useBulkImportDetailsQuery' + +export type ValidatingScreenProps = { + importId?: string + name: string + size: number + onUploadFinished: (data: UploadFileData) => void +} + +const ValidatingScreen = ({ + name, + size, + importId, + onUploadFinished, +}: ValidatingScreenProps) => { + const { translate: t } = useTranslate() + + useBulkImportDetailsQuery({ + importId, + refreshInterval: 30 * 1000, + onSuccess: data => { + if (data.importState === 'ReadyToImport') { + onUploadFinished({ + status: 'success', + data: { + fileData: { + ...data, + percentage: data.percentage.toString(), + }, + }, + }) + + return + } + + onUploadFinished({ + status: 'error', + showReport: data?.importState === 'ValidationFailed', + data: { + error: 'FieldValidationError', + errorDownloadLink: data?.validationResult?.reportDownloadLink ?? '', + validationResult: data?.validationResult?.validationResult ?? [], + fileName: data.fileName, + }, + }) + }, + }) + + return ( + + + + {t('uploading')} + +
+ {name} + + {' ยท '} + {bytesToSize(size)} + +
+
+ ) +} + +export default ValidatingScreen diff --git a/react/components/UploadingScreen/index.tsx b/react/components/UploadingScreen/index.tsx new file mode 100644 index 00000000..332b1c3e --- /dev/null +++ b/react/components/UploadingScreen/index.tsx @@ -0,0 +1,2 @@ +export { default as ValidationScreen } from './UploadingScreen' +export type { UploadingScreenProps } from './UploadingScreen' diff --git a/react/components/utils/bytesToSize.ts b/react/components/utils/bytesToSize.ts new file mode 100644 index 00000000..74ecfdc8 --- /dev/null +++ b/react/components/utils/bytesToSize.ts @@ -0,0 +1,10 @@ +export const bytesToSize = (bytes: number) => { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] + + if (bytes === 0) return '0 Bytes' + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + + if (i === 0) return `${bytes} ${sizes[i]}` + + return `${Math.round(bytes / 1024 ** i)}${sizes[i]}` +} diff --git a/react/hooks/useBulkImportDetailsQuery.ts b/react/hooks/useBulkImportDetailsQuery.ts index 67336dc2..8d359cbf 100644 --- a/react/hooks/useBulkImportDetailsQuery.ts +++ b/react/hooks/useBulkImportDetailsQuery.ts @@ -1,13 +1,26 @@ import useSWR from 'swr' import { getBulkImportDetails } from '../services' +import type { BulkImportDetails } from '../services/getBulkImportDetails' -const useBulkImportDetailsQuery = (importId: string) => { +export type UseBulkImportDetailsQueryProps = { + importId?: string + onSuccess?: (data: BulkImportDetails) => void + refreshInterval?: number +} + +const useBulkImportDetailsQuery = ({ + importId, + onSuccess, + refreshInterval = 0, +}: UseBulkImportDetailsQueryProps) => { return useSWR( importId ? `/buyer-orgs/${importId}` : null, () => getBulkImportDetails(importId), { + refreshInterval, revalidateOnFocus: false, + onSuccess, } ) } diff --git a/react/hooks/useBulkImportsQuery.ts b/react/hooks/useBulkImportsQuery.ts index d06aabe5..73383ab4 100644 --- a/react/hooks/useBulkImportsQuery.ts +++ b/react/hooks/useBulkImportsQuery.ts @@ -24,7 +24,7 @@ const useBulkImportQuery = ( account ? '/buyer-orgs' : null, () => getBulkImportList(account), { - refreshInterval: shouldPoll ? 5 * 1000 : 0, // 30 seconds + refreshInterval: shouldPoll ? 30 * 1000 : 0, // 30 seconds onError: errorData => { const status = errorData?.response?.status ?? 0 diff --git a/react/hooks/useValidateBulkImport.ts b/react/hooks/useValidateBulkImport.ts new file mode 100644 index 00000000..5c6f1ef0 --- /dev/null +++ b/react/hooks/useValidateBulkImport.ts @@ -0,0 +1,18 @@ +import useSWRMutation from 'swr/mutation' + +import { validateBulkImport } from '../services' + +const useValidateBulkImport = ({ onSuccess }: { onSuccess?: () => void }) => { + const { trigger } = useSWRMutation( + '/buyer-orgs/start', + (_, { arg }: { arg: { importId: string } }) => + validateBulkImport(arg.importId), + { + onSuccess, + } + ) + + return { startBulkImportValidation: trigger } +} + +export default useValidateBulkImport diff --git a/react/package.json b/react/package.json index e18dd221..6367053a 100644 --- a/react/package.json +++ b/react/package.json @@ -1,6 +1,6 @@ { "name": "vtex.b2b-organizations", - "version": "1.29.2", + "version": "1.30.0", "license": "UNLICENSED", "scripts": { "test": "vtex-test-tools test --passWithNoTests" @@ -36,7 +36,7 @@ }, "dependencies": { "@vtex/admin-ui": "^0.136.1", - "@vtex/bulk-import-ui": "1.1.6", + "@vtex/bulk-import-ui": "1.1.8", "@vtex/css-handles": "^1.0.0", "apollo-client": "^2.6.10", "axios": "1.4.0", diff --git a/react/services/getBulkImportDetails.ts b/react/services/getBulkImportDetails.ts index 070e59d6..65d726f9 100644 --- a/react/services/getBulkImportDetails.ts +++ b/react/services/getBulkImportDetails.ts @@ -1,12 +1,14 @@ import bulkImportClient from '.' import type { ImportDetails, ImportReportData } from '../types/BulkImport' -type BulkImportList = Omit & { +export type BulkImportDetails = Omit & { importReportList: ImportReportData[] percentage: number } -const getBulkImportList = async (importId: string): Promise => { +const getBulkImportDetails = async ( + importId?: string +): Promise => { const importListResponse = await bulkImportClient.get( `/buyer-orgs/${importId}` ) @@ -15,8 +17,6 @@ const getBulkImportList = async (importId: string): Promise => { const { importResult } = data - if (!importResult?.imports) throw Error('Import result not provided') - const importList = importResult?.imports ?? [] const [totalSuccess, totalError] = importList.reduce( @@ -49,4 +49,4 @@ const getBulkImportList = async (importId: string): Promise => { } } -export default getBulkImportList +export default getBulkImportDetails diff --git a/react/services/getBulkImportList.ts b/react/services/getBulkImportList.ts index 37b1da8f..4d8b87fe 100644 --- a/react/services/getBulkImportList.ts +++ b/react/services/getBulkImportList.ts @@ -15,9 +15,10 @@ const getBulkImportList = async (account: string) => { const importListData = importListResponse.data as ImportDetails[] return importListData - .filter( - item => - !['ReadyToImport', 'Failed'].some(status => status === item.importState) + .filter(item => + ['InProgress', 'Completed', 'CompletedWithError'].some( + status => status === item.importState + ) ) .map(item => ({ importId: item.importId, diff --git a/react/services/index.ts b/react/services/index.ts index 8954fe35..3ef3d230 100644 --- a/react/services/index.ts +++ b/react/services/index.ts @@ -3,3 +3,4 @@ export { default as getBulkImportList } from './getBulkImportList' export { default as getBulkImportDetails } from './getBulkImportDetails' export { default as uploadBulkImportFile } from './uploadBulkImportFile' export { default as startBulkImport } from './startBulkImport' +export { default as validateBulkImport } from './validateBulkImport' diff --git a/react/services/validateBulkImport.ts b/react/services/validateBulkImport.ts new file mode 100644 index 00000000..d3d3196b --- /dev/null +++ b/react/services/validateBulkImport.ts @@ -0,0 +1,7 @@ +import bulkImportClient from '.' + +const validateBulkImport = async (importId?: string): Promise => { + return bulkImportClient.post(`/buyer-orgs/validate/${importId}`) +} + +export default validateBulkImport diff --git a/react/types/BulkImport.d.ts b/react/types/BulkImport.d.ts index 8450e22c..2004611c 100644 --- a/react/types/BulkImport.d.ts +++ b/react/types/BulkImport.d.ts @@ -4,6 +4,8 @@ type ImportState = | 'ReadyToImport' | 'InProgress' | 'Completed' + | 'InValidation' + | 'ValidationFailed' | 'CompletedWithError' | 'Failed' @@ -56,6 +58,10 @@ export type ImportDetails = { importedAt: string importedUserEmail: string importedUserName: string + validationResult?: { + reportDownloadLink: string + validationResult: ValidationResult[] + } } export type UploadFileResult = { @@ -70,7 +76,7 @@ export type ValidationResult = { } export type FieldValidationError = { - description: string + description?: string error: 'FieldValidationError' errorDownloadLink: string validationResult: ValidationResult[] diff --git a/react/yarn.lock b/react/yarn.lock index aa31e3bf..82933305 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -2192,10 +2192,10 @@ tiny-warning "^1.0.3" use-debounce "^7.0.0" -"@vtex/bulk-import-ui@1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@vtex/bulk-import-ui/-/bulk-import-ui-1.1.6.tgz#aa4e812dfc9c9936ece5cee4158518f09eda6d73" - integrity sha512-1SrB3y4VOq2dD7NERYxeibnuLWAuu0um0lO/cet8b6F2z8ZivT1bE3bibDscoEI59eIr319796HTYXINjDCocg== +"@vtex/bulk-import-ui@1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@vtex/bulk-import-ui/-/bulk-import-ui-1.1.8.tgz#fcbdda205504b746ae87e1398944e3c14c5d9670" + integrity sha512-wZh6++7yOL8ZY29ga2lEGrpQJVUnDF/9ILIlC/m8grxCKCGu1FsVDL1dDBMtJ8l48sW7SrIquHJTMm5DhTr+QA== dependencies: "@vtex/admin-ui" "^0.136.1"