diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts index 1f0b3958c..4278ef7cf 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts @@ -69,7 +69,9 @@ export const useCreateActivityInDB = ({ activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? []; if (isNonEmptyArray(activityTargetsToCreate)) { - await createManyActivityTargets(activityTargetsToCreate); + await createManyActivityTargets({ + recordsToCreate: activityTargetsToCreate, + }); } const activityTargetsConnection = getRecordConnectionFromRecords({ diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx index 1523b99c1..6b76c9494 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx @@ -76,7 +76,9 @@ describe('useCreateManyRecords', () => { ); await act(async () => { - const res = await result.current.createManyRecords(input); + const res = await result.current.createManyRecords({ + recordsToCreate: input, + }); expect(res).toEqual(response); }); @@ -96,7 +98,10 @@ describe('useCreateManyRecords', () => { ); await act(async () => { - const res = await result.current.createManyRecords(input, true); + const res = await result.current.createManyRecords({ + recordsToCreate: input, + upsert: true, + }); expect(res).toEqual(response); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts new file mode 100644 index 000000000..f20d9c5fb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts @@ -0,0 +1,106 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; +import { + useCreateManyRecords, + useCreateManyRecordsProps, +} from '@/object-record/hooks/useCreateManyRecords'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { formatNumber } from '~/utils/format/number'; + +export const useBatchCreateManyRecords = < + CreatedObjectRecord extends ObjectRecord = ObjectRecord, +>({ + objectNameSingular, + recordGqlFields, + skipPostOptimisticEffect = false, + shouldMatchRootQueryFilter, + mutationBatchSize = DEFAULT_MUTATION_BATCH_SIZE, + setBatchedRecordsCount, + abortController, +}: useCreateManyRecordsProps & { + mutationBatchSize?: number; + setBatchedRecordsCount?: (count: number) => void; + abortController?: AbortController; +}) => { + const { createManyRecords } = useCreateManyRecords({ + objectNameSingular, + recordGqlFields, + skipPostOptimisticEffect, + shouldMatchRootQueryFilter, + shouldRefetchAggregateQueries: false, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + + const { enqueueSnackBar } = useSnackBar(); + + const batchCreateManyRecords = async ({ + recordsToCreate, + upsert, + }: { + recordsToCreate: Partial[]; + upsert?: boolean; + }) => { + const numberOfBatches = Math.ceil( + recordsToCreate.length / mutationBatchSize, + ); + + setBatchedRecordsCount?.(0); + + const allCreatedRecords = []; + let createdRecordsCount = 0; + try { + for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) { + const batchedRecordsToCreate = recordsToCreate.slice( + batchIndex * mutationBatchSize, + (batchIndex + 1) * mutationBatchSize, + ); + + createdRecordsCount = + batchIndex + 1 === numberOfBatches + ? recordsToCreate.length + : (batchIndex + 1) * mutationBatchSize; + + const createdRecords = await createManyRecords({ + recordsToCreate: batchedRecordsToCreate, + upsert, + abortController, + }); + + setBatchedRecordsCount?.(createdRecordsCount); + allCreatedRecords.push(...createdRecords); + } + } catch (error) { + if (error instanceof ApolloError && error.message.includes('aborted')) { + const formattedCreatedRecordsCount = formatNumber(createdRecordsCount); + enqueueSnackBar( + t`Record creation stopped. ${formattedCreatedRecordsCount} records created.`, + { + variant: SnackBarVariant.Warning, + duration: 5000, + }, + ); + } else { + throw error; + } + } + + await refetchAggregateQueries(); + return allCreatedRecords; + }; + + return { + batchCreateManyRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index 4e1b75de6..d3c175ca9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -32,11 +32,12 @@ type PartialObjectRecordWithOptionalId = Partial & { id?: string; }; -type useCreateManyRecordsProps = { +export type useCreateManyRecordsProps = { objectNameSingular: string; recordGqlFields?: RecordGqlOperationGqlRecordFields; skipPostOptimisticEffect?: boolean; shouldMatchRootQueryFilter?: boolean; + shouldRefetchAggregateQueries?: boolean; }; export const useCreateManyRecords = < @@ -46,6 +47,7 @@ export const useCreateManyRecords = < recordGqlFields, skipPostOptimisticEffect = false, shouldMatchRootQueryFilter, + shouldRefetchAggregateQueries = true, }: useCreateManyRecordsProps) => { const apolloClient = useApolloClient(); @@ -76,10 +78,17 @@ export const useCreateManyRecords = < objectMetadataNamePlural: objectMetadataItem.namePlural, }); - const createManyRecords = async ( - recordsToCreate: Partial[], - upsert?: boolean, - ) => { + type createManyRecordsProps = { + recordsToCreate: Partial[]; + upsert?: boolean; + abortController?: AbortController; + }; + + const createManyRecords = async ({ + recordsToCreate, + upsert, + abortController, + }: createManyRecordsProps) => { const sanitizedCreateManyRecordsInput: PartialObjectRecordWithOptionalId[] = []; const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = []; @@ -169,6 +178,11 @@ export const useCreateManyRecords = < data: sanitizedCreateManyRecordsInput, upsert: upsert, }, + context: { + fetchOptions: { + signal: abortController?.signal, + }, + }, update: (cache, { data }) => { const records = data?.[mutationResponseField]; @@ -205,7 +219,8 @@ export const useCreateManyRecords = < throw error; }); - await refetchAggregateQueries(); + if (shouldRefetchAggregateQueries) await refetchAggregateQueries(); + return createdObjects.data?.[mutationResponseField] ?? []; }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index f48cfd232..a62cde8ac 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -1,12 +1,15 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { useBatchCreateManyRecords } from '@/object-record/hooks/useBatchCreateManyRecords'; import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport'; import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow'; import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts'; +import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize'; import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog'; +import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState'; import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useSetRecoilState } from 'recoil'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useOpenObjectRecordsSpreadsheetImportDialog = ( @@ -19,8 +22,17 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( objectNameSingular, }); - const { createManyRecords } = useCreateManyRecords({ + const setCreatedRecordsProgress = useSetRecoilState( + spreadsheetImportCreatedRecordsProgressState, + ); + + const abortController = new AbortController(); + + const { batchCreateManyRecords } = useBatchCreateManyRecords({ objectNameSingular, + mutationBatchSize: SpreadsheetImportCreateRecordsBatchSize, + setBatchedRecordsCount: setCreatedRecordsProgress, + abortController, }); const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport(); @@ -61,8 +73,10 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( }); try { - const upsert = true; - await createManyRecords(createInputs, upsert); + await batchCreateManyRecords({ + recordsToCreate: createInputs, + upsert: true, + }); } catch (error: any) { enqueueSnackBar(error?.message || 'Something went wrong', { variant: SnackBarVariant.Error, @@ -71,6 +85,9 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = ( }, fields: availableFieldsForMatching, availableFieldMetadataItems: availableFieldMetadataItemsToImport, + onAbortSubmit: () => { + abortController.abort(); + }, }); }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx index fe7c12075..c24ffd30a 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { t } from '@lingui/core/macro'; import { CircularProgressBar } from 'twenty-ui/feedback'; import { MainButton } from 'twenty-ui/input'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -15,37 +16,41 @@ const StyledFooter = styled(Modal.Footer)` `; type StepNavigationButtonProps = { - onClick: () => void; - title: string; + onContinue?: () => void; + continueTitle?: string; + isContinueDisabled?: boolean; isLoading?: boolean; onBack?: () => void; - isNextDisabled?: boolean; + backTitle?: string; }; export const StepNavigationButton = ({ - onClick, - title, + onContinue, + continueTitle = t`Continue`, isLoading, onBack, - isNextDisabled = false, + backTitle = t`Back`, + isContinueDisabled = false, }: StepNavigationButtonProps) => { return ( {!isUndefinedOrNull(onBack) && ( )} - + {!isUndefinedOrNull(onContinue) && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize.ts b/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize.ts new file mode 100644 index 000000000..c8e8c2250 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetImportCreateRecordsBatchSize.ts @@ -0,0 +1 @@ +export const SpreadsheetImportCreateRecordsBatchSize = 500; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity.ts b/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity.ts index 6baa07a73..78a3a6057 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/constants/SpreadsheetMaxRecordImportCapacity.ts @@ -1 +1 @@ -export const SpreadsheetMaxRecordImportCapacity = 2000; +export const SpreadsheetMaxRecordImportCapacity = 10000; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx index 7f835812b..e48fe1b38 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -60,6 +60,7 @@ describe('useSpreadsheetImport', () => { ); expect(result.current.spreadsheetImportState).toStrictEqual({ isOpen: false, + isStepBarVisible: true, options: null, }); act(() => { @@ -69,6 +70,7 @@ describe('useSpreadsheetImport', () => { }); expect(result.current.spreadsheetImportState).toStrictEqual({ isOpen: true, + isStepBarVisible: true, options: mockedSpreadsheetOptions, }); }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useHideStepBar.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useHideStepBar.ts new file mode 100644 index 000000000..65084458e --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useHideStepBar.ts @@ -0,0 +1,23 @@ +import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; +import { useRecoilCallback } from 'recoil'; + +export const useHideStepBar = () => { + const hideStepBar = useRecoilCallback( + ({ set, snapshot }) => + () => { + const isStepBarVisible = snapshot + .getLoadable(spreadsheetImportDialogState) + .getValue().isStepBarVisible; + + if (isStepBarVisible) { + set(spreadsheetImportDialogState, (state) => ({ + ...state, + isStepBarVisible: false, + })); + } + }, + [], + ); + + return hideStepBar; +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts index 8ae064da7..f68cc75cd 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog.ts @@ -15,6 +15,7 @@ export const useOpenSpreadsheetImportDialog = () => { openModal(SPREADSHEET_IMPORT_MODAL_ID); setSpreadSheetImport({ isOpen: true, + isStepBarVisible: true, options, }); }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx index 9cd15d700..2b204a557 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx @@ -43,8 +43,10 @@ export const SpreadsheetImportProvider = ( const { closeModal } = useModal(); const handleClose = () => { + spreadsheetImportDialog.options?.onAbortSubmit?.(); setSpreadsheetImportDialog({ isOpen: false, + isStepBarVisible: true, options: null, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState.ts b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState.ts new file mode 100644 index 000000000..d6e1fd2e4 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const spreadsheetImportCreatedRecordsProgressState = createState({ + key: 'spreadsheetImportCreatedRecordsProgressState', + defaultValue: 0, +}); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts index f2efc20c3..cd0d5c904 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/states/spreadsheetImportDialogState.ts @@ -1,8 +1,9 @@ -import { SpreadsheetImportDialogOptions } from '../types'; import { createState } from 'twenty-ui/utilities'; +import { SpreadsheetImportDialogOptions } from '../types'; export type SpreadsheetImportDialogState = { isOpen: boolean; + isStepBarVisible: boolean; options: Omit, 'isOpen' | 'onClose'> | null; }; @@ -12,6 +13,7 @@ export const spreadsheetImportDialogState = createState< key: 'spreadsheetImportDialogState', defaultValue: { isOpen: false, + isStepBarVisible: true, options: null, }, }); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx new file mode 100644 index 000000000..7ba45e779 --- /dev/null +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx @@ -0,0 +1,64 @@ +import { useRecoilValue } from 'recoil'; + +import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; +import { useHideStepBar } from '@/spreadsheet-import/hooks/useHideStepBar'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; +import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState'; +import { Modal } from '@/ui/layout/modal/components/Modal'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { Loader } from 'twenty-ui/feedback'; +import { formatNumber } from '~/utils/format/number'; + +const StyledContent = styled(Modal.Content)` + align-items: center; + display: flex; + justify-content: center; + padding: 0px; +`; + +const StyledHeader = styled.span` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledDescription = styled.span` + color: ${({ theme }) => theme.font.color.tertiary}; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + margin-bottom: ${({ theme }) => theme.spacing(5)}; +`; + +type ImportDataStepProps = { + recordsToImportCount: number; +}; + +export const ImportDataStep = ({ + recordsToImportCount, +}: ImportDataStepProps) => { + const hideStepBar = useHideStepBar(); + hideStepBar(); + + const { onClose } = useSpreadsheetImportInternal(); + const spreadsheetImportCreatedRecordsProgress = useRecoilValue( + spreadsheetImportCreatedRecordsProgressState, + ); + + const formattedCreatedRecordsProgress = formatNumber( + spreadsheetImportCreatedRecordsProgress, + ); + const formattedRecordsToImportCount = formatNumber(recordsToImportCount); + + return ( + <> + + {t`Importing Data ...`} + {t`${formattedCreatedRecordsProgress} out of ${formattedRecordsToImportCount} records imported.`} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index 12bbcdf39..51d86cc32 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -279,14 +279,14 @@ export const MatchColumnsStep = ({ { onBack?.(); setColumns([]); }} - isNextDisabled={!hasMatchedColumns} + isContinueDisabled={!hasMatchedColumns} /> ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx index 238ee79ed..b3dafae81 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx @@ -114,9 +114,9 @@ export const SelectHeaderStep = ({ diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx index 7631366f1..2e2798aca 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx @@ -11,8 +11,8 @@ import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { useLingui } from '@lingui/react/macro'; -import { WorkBook } from 'xlsx-ugnis'; import { Radio, RadioGroup } from 'twenty-ui/input'; +import { WorkBook } from 'xlsx-ugnis'; const StyledContent = styled(Modal.Content)` align-items: center; @@ -116,10 +116,10 @@ export const SelectSheetStep = ({ handleOnContinue(value)} + onContinue={() => handleOnContinue(value)} onBack={onBack} isLoading={isLoading} - title={t`Next Step`} + continueTitle={t`Next Step`} /> ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx index 4ecaabd3f..f30b0590c 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx @@ -7,14 +7,15 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { ImportDataStep } from '@/spreadsheet-import/steps/components/ImportDataStep'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; +import { CircularProgressBar } from 'twenty-ui/feedback'; import { MatchColumnsStep } from './MatchColumnsStep/MatchColumnsStep'; import { SelectHeaderStep } from './SelectHeaderStep/SelectHeaderStep'; import { SelectSheetStep } from './SelectSheetStep/SelectSheetStep'; import { UploadStep } from './UploadStep/UploadStep'; import { ValidationStep } from './ValidationStep/ValidationStep'; -import { CircularProgressBar } from 'twenty-ui/feedback'; const StyledProgressBarContainer = styled(Modal.Content)` align-items: center; @@ -128,6 +129,12 @@ export const SpreadsheetImportStepper = ({ }} /> ); + case SpreadsheetImportStepType.importData: + return ( + + ); case SpreadsheetImportStepType.loading: default: return ( diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx index 2964b892d..76c54284e 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx @@ -6,8 +6,10 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { StepBar } from '@/ui/navigation/step-bar/components/StepBar'; import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; +import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { useLingui } from '@lingui/react/macro'; +import { useRecoilValue } from 'recoil'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; import { SpreadsheetImportStepper } from './SpreadsheetImportStepper'; @@ -26,6 +28,8 @@ const StyledHeader = styled(Modal.Header)` export const SpreadsheetImportStepperContainer = () => { const { t } = useLingui(); + const spreadsheetImportDialog = useRecoilValue(spreadsheetImportDialogState); + const stepTitles = { uploadStep: t`Upload File`, matchColumnsStep: t`Match Columns`, @@ -45,15 +49,17 @@ export const SpreadsheetImportStepperContainer = () => { return ( <> - - {steps.map((key) => ( - - ))} - + {spreadsheetImportDialog.isStepBarVisible && ( + + {steps.map((key) => ( + + ))} + + )} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index f5cc409de..d1ad453d2 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -11,6 +11,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Trans, useLingui } from '@lingui/react/macro'; import { MainButton } from 'twenty-ui/input'; +import { formatNumber } from '~/utils/format/number'; const StyledContainer = styled.div` align-items: center; @@ -154,6 +155,10 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { const { t } = useLingui(); + const formatSpreadsheetMaxRecordImportCapacity = formatNumber( + SpreadsheetMaxRecordImportCapacity, + ); + return ( { - {t`Max import capacity: ${SpreadsheetMaxRecordImportCapacity} records. Otherwise, consider splitting your file or using the API.`}{' '} + {t`Max import capacity: ${formatSpreadsheetMaxRecordImportCapacity} records. Otherwise, consider splitting your file or using the API.`}{' '} {t`Download sample file.`} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index 78427ccd1..f2971c7ea 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -237,7 +237,8 @@ export const ValidationStep = ({ ); setCurrentStepState({ - type: SpreadsheetImportStepType.loading, + type: SpreadsheetImportStepType.importData, + recordsToImportCount: calculatedData.validStructuredRows.length, }); await onSubmit(calculatedData, file); @@ -321,9 +322,9 @@ export const ValidationStep = ({ ); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts index 6e13bb902..22f2292c4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStep.ts @@ -27,4 +27,8 @@ export type SpreadsheetImportStep = } | { type: SpreadsheetImportStepType.loading; + } + | { + type: SpreadsheetImportStepType.importData; + recordsToImportCount: number; }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStepType.ts b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStepType.ts index 9c2bf555d..da4764ede 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStepType.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/types/SpreadsheetImportStepType.ts @@ -4,5 +4,6 @@ export enum SpreadsheetImportStepType { selectHeader = 'selectHeader', matchColumns = 'matchColumns', validateData = 'validateData', + importData = 'importData', loading = 'loading', } diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts index e46a22272..e308df5c6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/types/SpreadsheetImportDialogOptions.ts @@ -35,6 +35,8 @@ export type SpreadsheetImportDialogOptions = { validationResult: SpreadsheetImportImportValidationResult, file: File, ) => Promise; + // Function called when user aborts the importing flow + onAbortSubmit?: () => void; // Allows submitting with errors. Default: true allowInvalidSubmit?: boolean; // Theme configuration passed to underlying Chakra-UI diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts index eb155ff69..98668fab1 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts @@ -31,12 +31,12 @@ export const usePersistViewGroupRecords = () => { ({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => { if (viewGroupsToCreate.length === 0) return; - return createManyRecords( - viewGroupsToCreate.map((viewGroup) => ({ + return createManyRecords({ + recordsToCreate: viewGroupsToCreate.map((viewGroup) => ({ ...viewGroup, viewId, })), - ); + }); }, [createManyRecords], );