503a5029da
# Introduction Please first review this PR initial base https://github.com/twentyhq/twenty/pull/15358 In a nutshell refactored the frontend fetchers to display v2 errors format smoothly Please note that the v2 now finished the whole validation and does fail fast anymore ( summary is hardcoded for the moment ) ```json [ { "extensions": { "code": "BAD_USER_INPUT", "errors": { "cronTrigger": [], "databaseEventTrigger": [], "fieldMetadata": [ { "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Default value should be as quoted string", "value": "", }, { "code": "INVALID_FIELD_INPUT", "message": "Default value "" must be one of the option values", "value": "", }, ], "flatEntityMinimalInformation": { "id": Any<String>, "name": "testField", "objectMetadataId": Any<String>, }, "status": "fail", "type": "create_field", }, ], "index": [], "objectMetadata": [], "routeTrigger": [], "serverlessFunction": [], "view": [], "viewField": [], "viewFilter": [], "viewGroup": [], }, "message": "Validation failed for 0 object(s) and 0 field(s)", "summary": { "invalidCronTrigger": 0, "invalidDatabaseEventTrigger": 0, "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, "invalidViewField": 0, "invalidViewFilter": 0, "invalidViewGroup": 0, "totalErrors": 0, }, "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", }, "message": "Multiple validation errors occurred while creating fields", "name": "GraphQLError", }, ] ``` ## What's done - `usePersistView` tool ( CRUD ) - renamed `usePersistViewX` tools accordingly ( no more records or core ) - Now catching a lot of before unhandled exceptions - refactored each services to handle their own exception handlers and return either the response or the error within a discriminated union record ## Result ### Primary entity error When performing an metadata operation on a given metadata, if validation errors occurs we will display each of them in a toast Here while creating an object metadata. <img width="700" height="327" alt="image" src="https://github.com/user-attachments/assets/0c33d13c-c66c-4749-af36-b253abd3449b" /> ### Related entity error Still while creating an object <img width="700" height="327" alt="image" src="https://github.com/user-attachments/assets/52607788-c4e9-470c-ac8c-23437345ee5c" /> ### Translated <img width="700" height="327" alt="image" src="https://github.com/user-attachments/assets/a7198c20-ae82-47a6-910c-761de9594672" /> ## Conclusion This PR is an extract of https://github.com/twentyhq/twenty/pull/15331 close https://github.com/twentyhq/core-team-issues/issues/1776 ## Notes - Not refactor around triggers services as they're not consumed directly by any frontend services
173 lines
5.0 KiB
TypeScript
173 lines
5.0 KiB
TypeScript
import { useRecoilCallback } from 'recoil';
|
|
|
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
|
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
|
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup';
|
|
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
|
|
import { type ViewGroup } from '@/views/types/ViewGroup';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
|
|
export const useSaveCurrentViewGroups = () => {
|
|
const { createViewGroups, updateViewGroups } = usePersistViewGroupRecords();
|
|
|
|
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
|
|
|
|
const currentViewIdCallbackState = useRecoilComponentCallbackState(
|
|
contextStoreCurrentViewIdComponentState,
|
|
);
|
|
|
|
const saveViewGroup = useRecoilCallback(
|
|
({ snapshot }) =>
|
|
async (viewGroupToSave: ViewGroup) => {
|
|
const currentViewId = snapshot
|
|
.getLoadable(currentViewIdCallbackState)
|
|
.getValue();
|
|
|
|
if (!currentViewId) {
|
|
return;
|
|
}
|
|
|
|
const view = getViewFromPrefetchState(currentViewId);
|
|
|
|
if (isUndefinedOrNull(view)) {
|
|
return;
|
|
}
|
|
|
|
const currentViewGroups = view.viewGroups;
|
|
|
|
const existingField = currentViewGroups.find(
|
|
(currentViewGroup) =>
|
|
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
|
|
);
|
|
|
|
if (isUndefinedOrNull(existingField)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
isDeeplyEqual(
|
|
{
|
|
position: existingField.position,
|
|
isVisible: existingField.isVisible,
|
|
},
|
|
{
|
|
position: viewGroupToSave.position,
|
|
isVisible: viewGroupToSave.isVisible,
|
|
},
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
await updateViewGroups([
|
|
{
|
|
input: {
|
|
id: existingField.id,
|
|
update: {
|
|
isVisible: viewGroupToSave.isVisible,
|
|
position: viewGroupToSave.position,
|
|
fieldMetadataId: viewGroupToSave.fieldMetadataId,
|
|
fieldValue: viewGroupToSave.fieldValue,
|
|
},
|
|
},
|
|
},
|
|
]);
|
|
},
|
|
[currentViewIdCallbackState, getViewFromPrefetchState, updateViewGroups],
|
|
);
|
|
|
|
const saveViewGroups = useRecoilCallback(
|
|
({ snapshot }) =>
|
|
async (viewGroupsToSave: ViewGroup[]) => {
|
|
const currentViewId = snapshot
|
|
.getLoadable(currentViewIdCallbackState)
|
|
.getValue();
|
|
|
|
if (!currentViewId) {
|
|
return;
|
|
}
|
|
|
|
const view = getViewFromPrefetchState(currentViewId);
|
|
|
|
if (isUndefinedOrNull(view)) {
|
|
return;
|
|
}
|
|
|
|
const currentViewGroups = view.viewGroups;
|
|
|
|
const viewGroupsToUpdate = viewGroupsToSave
|
|
.map((viewGroupToSave) => {
|
|
const existingField = currentViewGroups.find(
|
|
(currentViewGroup) =>
|
|
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
|
|
);
|
|
|
|
if (isUndefinedOrNull(existingField)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (
|
|
isDeeplyEqual(
|
|
{
|
|
position: existingField.position,
|
|
isVisible: existingField.isVisible,
|
|
},
|
|
{
|
|
position: viewGroupToSave.position,
|
|
isVisible: viewGroupToSave.isVisible,
|
|
},
|
|
)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
input: {
|
|
id: existingField.id,
|
|
update: {
|
|
isVisible: viewGroupToSave.isVisible,
|
|
position: viewGroupToSave.position,
|
|
fieldMetadataId: viewGroupToSave.fieldMetadataId,
|
|
fieldValue: viewGroupToSave.fieldValue,
|
|
},
|
|
},
|
|
};
|
|
})
|
|
.filter(isDefined);
|
|
|
|
const viewGroupsToCreate = viewGroupsToSave.filter(
|
|
(viewFieldToSave) =>
|
|
!currentViewGroups.some(
|
|
(currentViewGroup) =>
|
|
currentViewGroup.fieldValue === viewFieldToSave.fieldValue,
|
|
),
|
|
);
|
|
|
|
await Promise.all([
|
|
createViewGroups(
|
|
viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
|
|
input: {
|
|
...viewGroup,
|
|
viewId: view.id,
|
|
},
|
|
})),
|
|
),
|
|
updateViewGroups(viewGroupsToUpdate),
|
|
]);
|
|
},
|
|
[
|
|
createViewGroups,
|
|
currentViewIdCallbackState,
|
|
getViewFromPrefetchState,
|
|
updateViewGroups,
|
|
],
|
|
);
|
|
|
|
return {
|
|
saveViewGroup,
|
|
saveViewGroups,
|
|
};
|
|
};
|