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:
@ -69,7 +69,9 @@ export const useCreateActivityInDB = ({
|
|||||||
activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? [];
|
activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? [];
|
||||||
|
|
||||||
if (isNonEmptyArray(activityTargetsToCreate)) {
|
if (isNonEmptyArray(activityTargetsToCreate)) {
|
||||||
await createManyActivityTargets(activityTargetsToCreate);
|
await createManyActivityTargets({
|
||||||
|
recordsToCreate: activityTargetsToCreate,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const activityTargetsConnection = getRecordConnectionFromRecords({
|
const activityTargetsConnection = getRecordConnectionFromRecords({
|
||||||
|
|||||||
@ -76,7 +76,9 @@ describe('useCreateManyRecords', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.createManyRecords(input);
|
const res = await result.current.createManyRecords({
|
||||||
|
recordsToCreate: input,
|
||||||
|
});
|
||||||
expect(res).toEqual(response);
|
expect(res).toEqual(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,7 +98,10 @@ describe('useCreateManyRecords', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
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);
|
expect(res).toEqual(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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<CreatedObjectRecord>[];
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -32,11 +32,12 @@ type PartialObjectRecordWithOptionalId = Partial<ObjectRecord> & {
|
|||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type useCreateManyRecordsProps = {
|
export type useCreateManyRecordsProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
skipPostOptimisticEffect?: boolean;
|
skipPostOptimisticEffect?: boolean;
|
||||||
shouldMatchRootQueryFilter?: boolean;
|
shouldMatchRootQueryFilter?: boolean;
|
||||||
|
shouldRefetchAggregateQueries?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateManyRecords = <
|
export const useCreateManyRecords = <
|
||||||
@ -46,6 +47,7 @@ export const useCreateManyRecords = <
|
|||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
skipPostOptimisticEffect = false,
|
skipPostOptimisticEffect = false,
|
||||||
shouldMatchRootQueryFilter,
|
shouldMatchRootQueryFilter,
|
||||||
|
shouldRefetchAggregateQueries = true,
|
||||||
}: useCreateManyRecordsProps) => {
|
}: useCreateManyRecordsProps) => {
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
@ -76,10 +78,17 @@ export const useCreateManyRecords = <
|
|||||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createManyRecords = async (
|
type createManyRecordsProps = {
|
||||||
recordsToCreate: Partial<CreatedObjectRecord>[],
|
recordsToCreate: Partial<CreatedObjectRecord>[];
|
||||||
upsert?: boolean,
|
upsert?: boolean;
|
||||||
) => {
|
abortController?: AbortController;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createManyRecords = async ({
|
||||||
|
recordsToCreate,
|
||||||
|
upsert,
|
||||||
|
abortController,
|
||||||
|
}: createManyRecordsProps) => {
|
||||||
const sanitizedCreateManyRecordsInput: PartialObjectRecordWithOptionalId[] =
|
const sanitizedCreateManyRecordsInput: PartialObjectRecordWithOptionalId[] =
|
||||||
[];
|
[];
|
||||||
const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = [];
|
const recordOptimisticRecordsInput: PartialObjectRecordWithId[] = [];
|
||||||
@ -169,6 +178,11 @@ export const useCreateManyRecords = <
|
|||||||
data: sanitizedCreateManyRecordsInput,
|
data: sanitizedCreateManyRecordsInput,
|
||||||
upsert: upsert,
|
upsert: upsert,
|
||||||
},
|
},
|
||||||
|
context: {
|
||||||
|
fetchOptions: {
|
||||||
|
signal: abortController?.signal,
|
||||||
|
},
|
||||||
|
},
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const records = data?.[mutationResponseField];
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
@ -205,7 +219,8 @@ export const useCreateManyRecords = <
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetchAggregateQueries();
|
if (shouldRefetchAggregateQueries) await refetchAggregateQueries();
|
||||||
|
|
||||||
return createdObjects.data?.[mutationResponseField] ?? [];
|
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
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 { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport';
|
||||||
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow';
|
||||||
import { spreadsheetImportFilterAvailableFieldMetadataItems } from '@/object-record/spreadsheet-import/utils/spreadsheetImportFilterAvailableFieldMetadataItems.ts';
|
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 { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
||||||
|
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
|
||||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||||
@ -19,8 +22,17 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createManyRecords } = useCreateManyRecords({
|
const setCreatedRecordsProgress = useSetRecoilState(
|
||||||
|
spreadsheetImportCreatedRecordsProgressState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
const { batchCreateManyRecords } = useBatchCreateManyRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
mutationBatchSize: SpreadsheetImportCreateRecordsBatchSize,
|
||||||
|
setBatchedRecordsCount: setCreatedRecordsProgress,
|
||||||
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport();
|
const { buildAvailableFieldsForImport } = useBuildAvailableFieldsForImport();
|
||||||
@ -61,8 +73,10 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const upsert = true;
|
await batchCreateManyRecords({
|
||||||
await createManyRecords(createInputs, upsert);
|
recordsToCreate: createInputs,
|
||||||
|
upsert: true,
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
@ -71,6 +85,9 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
|||||||
},
|
},
|
||||||
fields: availableFieldsForMatching,
|
fields: availableFieldsForMatching,
|
||||||
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
availableFieldMetadataItems: availableFieldMetadataItemsToImport,
|
||||||
|
onAbortSubmit: () => {
|
||||||
|
abortController.abort();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { CircularProgressBar } from 'twenty-ui/feedback';
|
import { CircularProgressBar } from 'twenty-ui/feedback';
|
||||||
import { MainButton } from 'twenty-ui/input';
|
import { MainButton } from 'twenty-ui/input';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
@ -15,37 +16,41 @@ const StyledFooter = styled(Modal.Footer)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type StepNavigationButtonProps = {
|
type StepNavigationButtonProps = {
|
||||||
onClick: () => void;
|
onContinue?: () => void;
|
||||||
title: string;
|
continueTitle?: string;
|
||||||
|
isContinueDisabled?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
isNextDisabled?: boolean;
|
backTitle?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StepNavigationButton = ({
|
export const StepNavigationButton = ({
|
||||||
onClick,
|
onContinue,
|
||||||
title,
|
continueTitle = t`Continue`,
|
||||||
isLoading,
|
isLoading,
|
||||||
onBack,
|
onBack,
|
||||||
isNextDisabled = false,
|
backTitle = t`Back`,
|
||||||
|
isContinueDisabled = false,
|
||||||
}: StepNavigationButtonProps) => {
|
}: StepNavigationButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledFooter>
|
<StyledFooter>
|
||||||
{!isUndefinedOrNull(onBack) && (
|
{!isUndefinedOrNull(onBack) && (
|
||||||
<MainButton
|
<MainButton
|
||||||
Icon={isLoading ? CircularProgressBar : undefined}
|
Icon={isLoading ? CircularProgressBar : undefined}
|
||||||
title="Back"
|
title={backTitle}
|
||||||
onClick={!isLoading ? onBack : undefined}
|
onClick={!isLoading ? onBack : undefined}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MainButton
|
{!isUndefinedOrNull(onContinue) && (
|
||||||
Icon={isLoading ? CircularProgressBar : undefined}
|
<MainButton
|
||||||
title={title}
|
Icon={isLoading ? CircularProgressBar : undefined}
|
||||||
onClick={!isLoading ? onClick : undefined}
|
title={continueTitle}
|
||||||
variant="primary"
|
onClick={!isLoading ? onContinue : undefined}
|
||||||
disabled={isNextDisabled}
|
variant="primary"
|
||||||
/>
|
disabled={isContinueDisabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledFooter>
|
</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({
|
expect(result.current.spreadsheetImportState).toStrictEqual({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
isStepBarVisible: true,
|
||||||
options: null,
|
options: null,
|
||||||
});
|
});
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -69,6 +70,7 @@ describe('useSpreadsheetImport', () => {
|
|||||||
});
|
});
|
||||||
expect(result.current.spreadsheetImportState).toStrictEqual({
|
expect(result.current.spreadsheetImportState).toStrictEqual({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isStepBarVisible: true,
|
||||||
options: mockedSpreadsheetOptions,
|
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);
|
openModal(SPREADSHEET_IMPORT_MODAL_ID);
|
||||||
setSpreadSheetImport({
|
setSpreadSheetImport({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
isStepBarVisible: true,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,8 +43,10 @@ export const SpreadsheetImportProvider = (
|
|||||||
const { closeModal } = useModal();
|
const { closeModal } = useModal();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
spreadsheetImportDialog.options?.onAbortSubmit?.();
|
||||||
setSpreadsheetImportDialog({
|
setSpreadsheetImportDialog({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
isStepBarVisible: true,
|
||||||
options: null,
|
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 { createState } from 'twenty-ui/utilities';
|
||||||
|
import { SpreadsheetImportDialogOptions } from '../types';
|
||||||
|
|
||||||
export type SpreadsheetImportDialogState<T extends string> = {
|
export type SpreadsheetImportDialogState<T extends string> = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
isStepBarVisible: boolean;
|
||||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ export const spreadsheetImportDialogState = createState<
|
|||||||
key: 'spreadsheetImportDialogState',
|
key: 'spreadsheetImportDialogState',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
isStepBarVisible: true,
|
||||||
options: null,
|
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>
|
</ScrollWrapper>
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
<StepNavigationButton
|
<StepNavigationButton
|
||||||
onClick={handleOnContinue}
|
onContinue={handleOnContinue}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
title={t`Next Step`}
|
continueTitle={t`Next Step`}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
onBack?.();
|
onBack?.();
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
}}
|
}}
|
||||||
isNextDisabled={!hasMatchedColumns}
|
isContinueDisabled={!hasMatchedColumns}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -114,9 +114,9 @@ export const SelectHeaderStep = ({
|
|||||||
</StyledTableContainer>
|
</StyledTableContainer>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<StepNavigationButton
|
<StepNavigationButton
|
||||||
onClick={handleOnContinue}
|
onContinue={handleOnContinue}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
title={t`Continue`}
|
continueTitle={t`Continue`}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
|
|||||||
|
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { WorkBook } from 'xlsx-ugnis';
|
|
||||||
import { Radio, RadioGroup } from 'twenty-ui/input';
|
import { Radio, RadioGroup } from 'twenty-ui/input';
|
||||||
|
import { WorkBook } from 'xlsx-ugnis';
|
||||||
|
|
||||||
const StyledContent = styled(Modal.Content)`
|
const StyledContent = styled(Modal.Content)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -116,10 +116,10 @@ export const SelectSheetStep = ({
|
|||||||
</StyledRadioContainer>
|
</StyledRadioContainer>
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
<StepNavigationButton
|
<StepNavigationButton
|
||||||
onClick={() => handleOnContinue(value)}
|
onContinue={() => handleOnContinue(value)}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
isLoading={isLoading}
|
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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
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 { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||||
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||||
|
import { CircularProgressBar } from 'twenty-ui/feedback';
|
||||||
import { MatchColumnsStep } from './MatchColumnsStep/MatchColumnsStep';
|
import { MatchColumnsStep } from './MatchColumnsStep/MatchColumnsStep';
|
||||||
import { SelectHeaderStep } from './SelectHeaderStep/SelectHeaderStep';
|
import { SelectHeaderStep } from './SelectHeaderStep/SelectHeaderStep';
|
||||||
import { SelectSheetStep } from './SelectSheetStep/SelectSheetStep';
|
import { SelectSheetStep } from './SelectSheetStep/SelectSheetStep';
|
||||||
import { UploadStep } from './UploadStep/UploadStep';
|
import { UploadStep } from './UploadStep/UploadStep';
|
||||||
import { ValidationStep } from './ValidationStep/ValidationStep';
|
import { ValidationStep } from './ValidationStep/ValidationStep';
|
||||||
import { CircularProgressBar } from 'twenty-ui/feedback';
|
|
||||||
|
|
||||||
const StyledProgressBarContainer = styled(Modal.Content)`
|
const StyledProgressBarContainer = styled(Modal.Content)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -128,6 +129,12 @@ export const SpreadsheetImportStepper = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case SpreadsheetImportStepType.importData:
|
||||||
|
return (
|
||||||
|
<ImportDataStep
|
||||||
|
recordsToImportCount={currentStepState.recordsToImportCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case SpreadsheetImportStepType.loading:
|
case SpreadsheetImportStepType.loading:
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
|
|||||||
import { StepBar } from '@/ui/navigation/step-bar/components/StepBar';
|
import { StepBar } from '@/ui/navigation/step-bar/components/StepBar';
|
||||||
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
|
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 { Modal } from '@/ui/layout/modal/components/Modal';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||||
import { SpreadsheetImportStepper } from './SpreadsheetImportStepper';
|
import { SpreadsheetImportStepper } from './SpreadsheetImportStepper';
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ const StyledHeader = styled(Modal.Header)`
|
|||||||
export const SpreadsheetImportStepperContainer = () => {
|
export const SpreadsheetImportStepperContainer = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const spreadsheetImportDialog = useRecoilValue(spreadsheetImportDialogState);
|
||||||
|
|
||||||
const stepTitles = {
|
const stepTitles = {
|
||||||
uploadStep: t`Upload File`,
|
uploadStep: t`Upload File`,
|
||||||
matchColumnsStep: t`Match Columns`,
|
matchColumnsStep: t`Match Columns`,
|
||||||
@ -45,15 +49,17 @@ export const SpreadsheetImportStepperContainer = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<StepBar activeStep={activeStep}>
|
{spreadsheetImportDialog.isStepBarVisible && (
|
||||||
{steps.map((key) => (
|
<StepBar activeStep={activeStep}>
|
||||||
<StepBar.Step
|
{steps.map((key) => (
|
||||||
activeStep={activeStep}
|
<StepBar.Step
|
||||||
label={stepTitles[key]}
|
activeStep={activeStep}
|
||||||
key={key}
|
label={stepTitles[key]}
|
||||||
/>
|
key={key}
|
||||||
))}
|
/>
|
||||||
</StepBar>
|
))}
|
||||||
|
</StepBar>
|
||||||
|
)}
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
<SpreadsheetImportStepper nextStep={nextStep} prevStep={prevStep} />
|
<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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { MainButton } from 'twenty-ui/input';
|
import { MainButton } from 'twenty-ui/input';
|
||||||
|
import { formatNumber } from '~/utils/format/number';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -154,6 +155,10 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const formatSpreadsheetMaxRecordImportCapacity = formatNumber(
|
||||||
|
SpreadsheetMaxRecordImportCapacity,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
@ -179,7 +184,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
|||||||
</StyledText>
|
</StyledText>
|
||||||
<MainButton onClick={open} title={t`Select file`} />
|
<MainButton onClick={open} title={t`Select file`} />
|
||||||
<StyledFooterText>
|
<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}>
|
<StyledTextAction onClick={downloadSample}>
|
||||||
{t`Download sample file.`}
|
{t`Download sample file.`}
|
||||||
</StyledTextAction>
|
</StyledTextAction>
|
||||||
|
|||||||
@ -237,7 +237,8 @@ export const ValidationStep = <T extends string>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
setCurrentStepState({
|
setCurrentStepState({
|
||||||
type: SpreadsheetImportStepType.loading,
|
type: SpreadsheetImportStepType.importData,
|
||||||
|
recordsToImportCount: calculatedData.validStructuredRows.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
await onSubmit(calculatedData, file);
|
await onSubmit(calculatedData, file);
|
||||||
@ -321,9 +322,9 @@ export const ValidationStep = <T extends string>({
|
|||||||
</StyledToolbar>
|
</StyledToolbar>
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
<StepNavigationButton
|
<StepNavigationButton
|
||||||
onClick={onContinue}
|
onContinue={onContinue}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
title={t`Confirm`}
|
continueTitle={t`Confirm`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,4 +27,8 @@ export type SpreadsheetImportStep =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: SpreadsheetImportStepType.loading;
|
type: SpreadsheetImportStepType.loading;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SpreadsheetImportStepType.importData;
|
||||||
|
recordsToImportCount: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,5 +4,6 @@ export enum SpreadsheetImportStepType {
|
|||||||
selectHeader = 'selectHeader',
|
selectHeader = 'selectHeader',
|
||||||
matchColumns = 'matchColumns',
|
matchColumns = 'matchColumns',
|
||||||
validateData = 'validateData',
|
validateData = 'validateData',
|
||||||
|
importData = 'importData',
|
||||||
loading = 'loading',
|
loading = 'loading',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,8 @@ export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
|||||||
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
|
validationResult: SpreadsheetImportImportValidationResult<FieldNames>,
|
||||||
file: File,
|
file: File,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
// Function called when user aborts the importing flow
|
||||||
|
onAbortSubmit?: () => void;
|
||||||
// Allows submitting with errors. Default: true
|
// Allows submitting with errors. Default: true
|
||||||
allowInvalidSubmit?: boolean;
|
allowInvalidSubmit?: boolean;
|
||||||
// Theme configuration passed to underlying Chakra-UI
|
// Theme configuration passed to underlying Chakra-UI
|
||||||
|
|||||||
@ -31,12 +31,12 @@ export const usePersistViewGroupRecords = () => {
|
|||||||
({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => {
|
({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => {
|
||||||
if (viewGroupsToCreate.length === 0) return;
|
if (viewGroupsToCreate.length === 0) return;
|
||||||
|
|
||||||
return createManyRecords(
|
return createManyRecords({
|
||||||
viewGroupsToCreate.map((viewGroup) => ({
|
recordsToCreate: viewGroupsToCreate.map((viewGroup) => ({
|
||||||
...viewGroup,
|
...viewGroup,
|
||||||
viewId,
|
viewId,
|
||||||
})),
|
})),
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
[createManyRecords],
|
[createManyRecords],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user