Import - Increase record import limit (#12627)
<img width="700" alt="Screenshot 2025-06-16 at 15 05 09" src="https://github.com/user-attachments/assets/a09c3fae-c0ae-4a63-8bda-9d29c97a6a66" /> closes https://github.com/twentyhq/twenty/issues/11980
This commit is contained in:
@ -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 (
|
||||
<StyledFooter>
|
||||
{!isUndefinedOrNull(onBack) && (
|
||||
<MainButton
|
||||
Icon={isLoading ? CircularProgressBar : undefined}
|
||||
title="Back"
|
||||
title={backTitle}
|
||||
onClick={!isLoading ? onBack : undefined}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
<MainButton
|
||||
Icon={isLoading ? CircularProgressBar : undefined}
|
||||
title={title}
|
||||
onClick={!isLoading ? onClick : undefined}
|
||||
variant="primary"
|
||||
disabled={isNextDisabled}
|
||||
/>
|
||||
{!isUndefinedOrNull(onContinue) && (
|
||||
<MainButton
|
||||
Icon={isLoading ? CircularProgressBar : undefined}
|
||||
title={continueTitle}
|
||||
onClick={!isLoading ? onContinue : undefined}
|
||||
variant="primary"
|
||||
disabled={isContinueDisabled}
|
||||
/>
|
||||
)}
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const SpreadsheetImportCreateRecordsBatchSize = 500;
|
||||
@ -1 +1 @@
|
||||
export const SpreadsheetMaxRecordImportCapacity = 2000;
|
||||
export const SpreadsheetMaxRecordImportCapacity = 10000;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -15,6 +15,7 @@ export const useOpenSpreadsheetImportDialog = <T extends string>() => {
|
||||
openModal(SPREADSHEET_IMPORT_MODAL_ID);
|
||||
setSpreadSheetImport({
|
||||
isOpen: true,
|
||||
isStepBarVisible: true,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
@ -43,8 +43,10 @@ export const SpreadsheetImportProvider = (
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const handleClose = () => {
|
||||
spreadsheetImportDialog.options?.onAbortSubmit?.();
|
||||
setSpreadsheetImportDialog({
|
||||
isOpen: false,
|
||||
isStepBarVisible: true,
|
||||
options: null,
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const spreadsheetImportCreatedRecordsProgressState = createState({
|
||||
key: 'spreadsheetImportCreatedRecordsProgressState',
|
||||
defaultValue: 0,
|
||||
});
|
||||
@ -1,8 +1,9 @@
|
||||
import { SpreadsheetImportDialogOptions } from '../types';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { SpreadsheetImportDialogOptions } from '../types';
|
||||
|
||||
export type SpreadsheetImportDialogState<T extends string> = {
|
||||
isOpen: boolean;
|
||||
isStepBarVisible: boolean;
|
||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||
};
|
||||
|
||||
@ -12,6 +13,7 @@ export const spreadsheetImportDialogState = createState<
|
||||
key: 'spreadsheetImportDialogState',
|
||||
defaultValue: {
|
||||
isOpen: false,
|
||||
isStepBarVisible: true,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<StyledContent>
|
||||
<StyledHeader>{t`Importing Data ...`}</StyledHeader>
|
||||
<StyledDescription>{t`${formattedCreatedRecordsProgress} out of ${formattedRecordsToImportCount} records imported.`}</StyledDescription>
|
||||
<Loader />
|
||||
</StyledContent>
|
||||
<StepNavigationButton onBack={onClose} backTitle={t`Cancel`} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -279,14 +279,14 @@ export const MatchColumnsStep = <T extends string>({
|
||||
</ScrollWrapper>
|
||||
</StyledContent>
|
||||
<StepNavigationButton
|
||||
onClick={handleOnContinue}
|
||||
onContinue={handleOnContinue}
|
||||
isLoading={isLoading}
|
||||
title={t`Next Step`}
|
||||
continueTitle={t`Next Step`}
|
||||
onBack={() => {
|
||||
onBack?.();
|
||||
setColumns([]);
|
||||
}}
|
||||
isNextDisabled={!hasMatchedColumns}
|
||||
isContinueDisabled={!hasMatchedColumns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -114,9 +114,9 @@ export const SelectHeaderStep = ({
|
||||
</StyledTableContainer>
|
||||
</Modal.Content>
|
||||
<StepNavigationButton
|
||||
onClick={handleOnContinue}
|
||||
onContinue={handleOnContinue}
|
||||
onBack={onBack}
|
||||
title={t`Continue`}
|
||||
continueTitle={t`Continue`}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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 = ({
|
||||
</StyledRadioContainer>
|
||||
</StyledContent>
|
||||
<StepNavigationButton
|
||||
onClick={() => handleOnContinue(value)}
|
||||
onContinue={() => handleOnContinue(value)}
|
||||
onBack={onBack}
|
||||
isLoading={isLoading}
|
||||
title={t`Next Step`}
|
||||
continueTitle={t`Next Step`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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 (
|
||||
<ImportDataStep
|
||||
recordsToImportCount={currentStepState.recordsToImportCount}
|
||||
/>
|
||||
);
|
||||
case SpreadsheetImportStepType.loading:
|
||||
default:
|
||||
return (
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<StyledHeader>
|
||||
<StepBar activeStep={activeStep}>
|
||||
{steps.map((key) => (
|
||||
<StepBar.Step
|
||||
activeStep={activeStep}
|
||||
label={stepTitles[key]}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</StepBar>
|
||||
{spreadsheetImportDialog.isStepBarVisible && (
|
||||
<StepBar activeStep={activeStep}>
|
||||
{steps.map((key) => (
|
||||
<StepBar.Step
|
||||
activeStep={activeStep}
|
||||
label={stepTitles[key]}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</StepBar>
|
||||
)}
|
||||
</StyledHeader>
|
||||
<SpreadsheetImportStepper nextStep={nextStep} prevStep={prevStep} />
|
||||
</>
|
||||
|
||||
@ -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 (
|
||||
<StyledContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
@ -179,7 +184,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
</StyledText>
|
||||
<MainButton onClick={open} title={t`Select file`} />
|
||||
<StyledFooterText>
|
||||
{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.`}{' '}
|
||||
<StyledTextAction onClick={downloadSample}>
|
||||
{t`Download sample file.`}
|
||||
</StyledTextAction>
|
||||
|
||||
@ -237,7 +237,8 @@ export const ValidationStep = <T extends string>({
|
||||
);
|
||||
|
||||
setCurrentStepState({
|
||||
type: SpreadsheetImportStepType.loading,
|
||||
type: SpreadsheetImportStepType.importData,
|
||||
recordsToImportCount: calculatedData.validStructuredRows.length,
|
||||
});
|
||||
|
||||
await onSubmit(calculatedData, file);
|
||||
@ -321,9 +322,9 @@ export const ValidationStep = <T extends string>({
|
||||
</StyledToolbar>
|
||||
</StyledContent>
|
||||
<StepNavigationButton
|
||||
onClick={onContinue}
|
||||
onContinue={onContinue}
|
||||
onBack={onBack}
|
||||
title={t`Confirm`}
|
||||
continueTitle={t`Confirm`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -27,4 +27,8 @@ export type SpreadsheetImportStep =
|
||||
}
|
||||
| {
|
||||
type: SpreadsheetImportStepType.loading;
|
||||
}
|
||||
| {
|
||||
type: SpreadsheetImportStepType.importData;
|
||||
recordsToImportCount: number;
|
||||
};
|
||||
|
||||
@ -4,5 +4,6 @@ export enum SpreadsheetImportStepType {
|
||||
selectHeader = 'selectHeader',
|
||||
matchColumns = 'matchColumns',
|
||||
validateData = 'validateData',
|
||||
importData = 'importData',
|
||||
loading = 'loading',
|
||||
}
|
||||
|
||||
@ -35,6 +35,8 @@ export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
||||
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
|
||||
file: File,
|
||||
) => Promise<void>;
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user