Files
twenty/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts
T
Paul Rastoin a3dc6e5c59 View field create many mutation (#15576)
# Introduction
When creating a view with v2 flag activated in production result in race
condition due to request being slow and //.
That's why we're introducing a batch create on view field here
closing https://github.com/twentyhq/core-team-issues/issues/1836

## In v2
- batch create view field endpoint is available
- frontend will target the new endpoint

## In v1
- batch create view field endpoint is not available
- frontend will stick to old fake batch view field creation loop

## Polish
- use persist view field is quite verbose as contains two data model we
could aim to create an update many view fields in order to standardize
new pattern
2025-11-04 11:36:08 +01:00

349 lines
9.6 KiB
TypeScript

import { useCallback } from 'react';
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
type CreateCoreViewFieldMutationVariables,
type CreateManyCoreViewFieldsMutationVariables,
type DeleteCoreViewFieldMutationVariables,
type DestroyCoreViewFieldMutationVariables,
FeatureFlagKey,
type UpdateCoreViewFieldMutationVariables,
useCreateCoreViewFieldMutation,
useCreateManyCoreViewFieldsMutation,
useDeleteCoreViewFieldMutation,
useDestroyCoreViewFieldMutation,
useUpdateCoreViewFieldMutation,
} from '~/generated/graphql';
export const usePersistViewField = () => {
const { triggerViewFieldOptimisticEffect } =
useTriggerViewFieldOptimisticEffect();
const isWorkspaceMigrationV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
);
const [createCoreViewFieldMutation] = useCreateCoreViewFieldMutation();
const [createManyCoreViewFieldsMutation] =
useCreateManyCoreViewFieldsMutation();
const [updateCoreViewFieldMutation] = useUpdateCoreViewFieldMutation();
const [deleteCoreViewFieldMutation] = useDeleteCoreViewFieldMutation();
const [destroyCoreViewFieldMutation] = useDestroyCoreViewFieldMutation();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const oldCreateViewFields = useCallback(
async (
createCoreViewFieldInputs: CreateCoreViewFieldMutationVariables[],
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof createCoreViewFieldMutation>>[]
>
> => {
if (createCoreViewFieldInputs.length === 0) {
return {
status: 'successful',
response: [],
};
}
try {
const results = await Promise.all(
createCoreViewFieldInputs.map(async (variables) =>
createCoreViewFieldMutation({
variables,
update: (_cache, { data }) => {
const createdViewField = data?.createCoreViewField;
if (!isDefined(createdViewField)) {
return;
}
triggerViewFieldOptimisticEffect({
createdViewFields: [createdViewField],
});
},
}),
),
);
return {
status: 'successful',
response: results,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
triggerViewFieldOptimisticEffect,
createCoreViewFieldMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const createViewFields = useCallback(
async (
createCoreViewFieldInputs: CreateManyCoreViewFieldsMutationVariables,
): Promise<
| MetadataRequestResult<Awaited<
ReturnType<typeof createManyCoreViewFieldsMutation>
> | null>
| MetadataRequestResult<
Awaited<ReturnType<typeof createCoreViewFieldMutation>>[]
>
> => {
if (
!Array.isArray(createCoreViewFieldInputs.inputs) ||
createCoreViewFieldInputs.inputs.length === 0
) {
return {
status: 'successful',
response: null,
};
}
if (!isWorkspaceMigrationV2Enabled) {
const oldFormatInputs: CreateCoreViewFieldMutationVariables[] =
createCoreViewFieldInputs.inputs.map((input) => ({ input }));
return await oldCreateViewFields(oldFormatInputs);
}
try {
const result = await createManyCoreViewFieldsMutation({
variables: createCoreViewFieldInputs,
update: (_cache, { data }) => {
const createdViewFields = data?.createManyCoreViewFields;
if (!isDefined(createdViewFields)) {
return;
}
triggerViewFieldOptimisticEffect({
createdViewFields,
});
},
});
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
isWorkspaceMigrationV2Enabled,
oldCreateViewFields,
triggerViewFieldOptimisticEffect,
createManyCoreViewFieldsMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const updateViewFields = useCallback(
async (
createCoreViewFieldInputs: UpdateCoreViewFieldMutationVariables[],
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof updateCoreViewFieldMutation>>[]
>
> => {
if (createCoreViewFieldInputs.length === 0) {
return {
status: 'successful',
response: [],
};
}
try {
const results = await Promise.all(
createCoreViewFieldInputs.map((variables) =>
updateCoreViewFieldMutation({
variables,
update: (_cache, { data }) => {
const updatedViewField = data?.updateCoreViewField;
if (!isDefined(updatedViewField)) {
return;
}
triggerViewFieldOptimisticEffect({
updatedViewFields: [updatedViewField],
});
},
}),
),
);
return {
status: 'successful',
response: results,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
triggerViewFieldOptimisticEffect,
updateCoreViewFieldMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const deleteViewFields = useCallback(
async (
deleteCoreViewFieldInputs: DeleteCoreViewFieldMutationVariables[],
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof deleteCoreViewFieldMutation>>[]
>
> => {
if (deleteCoreViewFieldInputs.length === 0) {
return {
status: 'successful',
response: [],
};
}
try {
const results = await Promise.all(
deleteCoreViewFieldInputs.map((variables) =>
deleteCoreViewFieldMutation({
variables,
update: (_cache, { data }) => {
const deletedViewField = data?.deleteCoreViewField;
if (!isDefined(deletedViewField)) {
return;
}
triggerViewFieldOptimisticEffect({
deletedViewFields: [deletedViewField],
});
},
}),
),
);
return {
status: 'successful',
response: results,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
triggerViewFieldOptimisticEffect,
deleteCoreViewFieldMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
const destroyViewFields = useCallback(
async (
destroyCoreViewFieldInputs: DestroyCoreViewFieldMutationVariables[],
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof destroyCoreViewFieldMutation>>[]
>
> => {
if (destroyCoreViewFieldInputs.length === 0) {
return {
status: 'successful',
response: [],
};
}
try {
const results = await Promise.all(
destroyCoreViewFieldInputs.map((variables) =>
destroyCoreViewFieldMutation({
variables,
}),
),
);
return {
status: 'successful',
response: results,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'viewField',
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[destroyCoreViewFieldMutation, handleMetadataError, enqueueErrorSnackBar],
);
return {
createViewFields,
updateViewFields,
deleteViewFields,
destroyViewFields,
};
};