diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectRecordMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts similarity index 97% rename from packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectRecordMetadataItem.ts rename to packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts index 37adb3417a..263745e388 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectRecordMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts @@ -24,6 +24,7 @@ export const query = gql` export const variables = { input: { object: { + icon: 'IconPlus', labelPlural: 'View Filters', labelSingular: 'View Filter', nameSingular: 'viewFilter', diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectRecordMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx similarity index 76% rename from packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectRecordMetadataItem.test.tsx rename to packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx index 588e6d9a04..067eeff19c 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectRecordMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx @@ -3,14 +3,14 @@ import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; -import { useCreateOneObjectRecordMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; +import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider'; import { query, responseData, variables, -} from '../__mocks__/useCreateOneObjectRecordMetadataItem'; +} from '../__mocks__/useCreateOneObjectMetadataItem'; const mocks = [ { @@ -36,21 +36,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( ); -describe('useCreateOneObjectRecordMetadataItem', () => { +describe('useCreateOneObjectMetadataItem', () => { it('should work as expected', async () => { - const { result } = renderHook( - () => useCreateOneObjectRecordMetadataItem(), - { - wrapper: Wrapper, - }, - ); + const { result } = renderHook(() => useCreateOneObjectMetadataItem(), { + wrapper: Wrapper, + }); await act(async () => { const res = await result.current.createOneObjectMetadataItem({ + icon: 'IconPlus', labelPlural: 'View Filters', labelSingular: 'View Filter', - nameSingular: 'viewFilter', namePlural: 'viewFilters', + nameSingular: 'viewFilter', }); expect(res.data).toEqual({ createOneObject: responseData }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx index dd382ebcb6..c93057e2e8 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx @@ -103,28 +103,4 @@ describe('useObjectMetadataItemForSettings', () => { expect(res?.namePlural).toBe('opportunities'); }); }); - - it('should editObjectMetadataItem', async () => { - const { result } = renderHook( - () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState); - setMetadataItems(mockObjectMetadataItems); - - return useObjectMetadataItemForSettings(); - }, - { - wrapper: Wrapper, - }, - ); - - await act(async () => { - const res = await result.current.editObjectMetadataItem({ - id: 'idToUpdate', - description: 'newDescription', - labelPlural: 'labelPlural', - labelSingular: 'labelSingular', - }); - expect(res.data).toEqual({ updateOneObject: responseData }); - }); - }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index e379448810..e187ba8321 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -2,6 +2,7 @@ import { ApolloClient, useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { + CreateObjectInput, CreateOneObjectMetadataItemMutation, CreateOneObjectMetadataItemMutationVariables, } from '~/generated-metadata/graphql'; @@ -11,7 +12,7 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries'; import { useApolloMetadataClient } from './useApolloMetadataClient'; -export const useCreateOneObjectRecordMetadataItem = () => { +export const useCreateOneObjectMetadataItem = () => { const apolloMetadataClient = useApolloMetadataClient(); const [mutate] = useMutation< @@ -21,16 +22,10 @@ export const useCreateOneObjectRecordMetadataItem = () => { client: apolloMetadataClient ?? ({} as ApolloClient), }); - const createOneObjectMetadataItem = async ( - input: CreateOneObjectMetadataItemMutationVariables['input']['object'], - ) => { + const createOneObjectMetadataItem = async (input: CreateObjectInput) => { return await mutate({ variables: { - input: { - object: { - ...input, - }, - }, + input: { object: input }, }, awaitRefetchQueries: true, refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''], diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts index d97dfc8406..eb8bfe651f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts @@ -2,14 +2,8 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; -import { formatObjectMetadataItemInput } from '../utils/formatObjectMetadataItemInput'; import { getObjectSlug } from '../utils/getObjectSlug'; -import { useCreateOneObjectRecordMetadataItem } from './useCreateOneObjectMetadataItem'; -import { useDeleteOneObjectMetadataItem } from './useDeleteOneObjectMetadataItem'; -import { useUpdateOneObjectMetadataItem } from './useUpdateOneObjectMetadataItem'; - export const useObjectMetadataItemForSettings = () => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -36,65 +30,12 @@ export const useObjectMetadataItemForSettings = () => { (objectMetadataItem) => objectMetadataItem.namePlural === namePlural, ); - const { createOneObjectMetadataItem } = - useCreateOneObjectRecordMetadataItem(); - const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); - const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem(); - - const createObjectMetadataItem = ( - input: Pick< - ObjectMetadataItem, - 'labelPlural' | 'labelSingular' | 'icon' | 'description' - >, - ) => createOneObjectMetadataItem(formatObjectMetadataItemInput(input)); - - const editObjectMetadataItem = ( - input: Pick< - ObjectMetadataItem, - | 'description' - | 'icon' - | 'id' - | 'labelIdentifierFieldMetadataId' - | 'labelPlural' - | 'labelSingular' - >, - ) => - updateOneObjectMetadataItem({ - idToUpdate: input.id, - updatePayload: formatObjectMetadataItemInput(input), - }); - - const activateObjectMetadataItem = ( - objectMetadataItem: Pick, - ) => - updateOneObjectMetadataItem({ - idToUpdate: objectMetadataItem.id, - updatePayload: { isActive: true }, - }); - - const disableObjectMetadataItem = ( - objectMetadataItem: Pick, - ) => - updateOneObjectMetadataItem({ - idToUpdate: objectMetadataItem.id, - updatePayload: { isActive: false }, - }); - - const eraseObjectMetadataItem = ( - objectMetadataItem: Pick, - ) => deleteOneObjectMetadataItem(objectMetadataItem.id); - return { - activateObjectMetadataItem, activeObjectMetadataItems, - createObjectMetadataItem, - inactiveObjectMetadataItems, - disableObjectMetadataItem, - editObjectMetadataItem, - eraseObjectMetadataItem, findActiveObjectMetadataItemBySlug, findObjectMetadataItemById, findObjectMetadataItemByNamePlural, + inactiveObjectMetadataItems, objectMetadataItems, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index fe0ef2854c..aaa7b0ae77 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { + UpdateObjectInput, UpdateOneObjectMetadataItemMutation, UpdateOneObjectMetadataItemMutationVariables, } from '~/generated-metadata/graphql'; @@ -27,16 +28,7 @@ export const useUpdateOneObjectMetadataItem = () => { updatePayload, }: { idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate']; - updatePayload: Pick< - UpdateOneObjectMetadataItemMutationVariables['updatePayload'], - | 'description' - | 'icon' - | 'isActive' - | 'labelPlural' - | 'labelSingular' - | 'namePlural' - | 'nameSingular' - >; + updatePayload: UpdateObjectInput; }) => { return await mutate({ variables: { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatObjectMetadataItemInput.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatObjectMetadataItemInput.ts deleted file mode 100644 index 0afceb2949..0000000000 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatObjectMetadataItemInput.ts +++ /dev/null @@ -1,23 +0,0 @@ -import toCamelCase from 'lodash.camelcase'; - -import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; - -export const formatObjectMetadataItemInput = ( - input: Pick< - ObjectMetadataItem, - | 'description' - | 'icon' - | 'labelIdentifierFieldMetadataId' - | 'labelPlural' - | 'labelSingular' - >, -) => ({ - description: input.description?.trim() ?? null, - icon: input.icon, - labelIdentifierFieldMetadataId: - input.labelIdentifierFieldMetadataId?.trim() ?? null, - labelPlural: input.labelPlural.trim(), - labelSingular: input.labelSingular.trim(), - namePlural: toCamelCase(input.labelPlural.trim()), - nameSingular: toCamelCase(input.labelSingular.trim()), -}); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts new file mode 100644 index 0000000000..48db789805 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts @@ -0,0 +1,48 @@ +import { SafeParseSuccess } from 'zod'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mockedCompanyObjectMetadataItem } from '@/object-record/record-field/__mocks__/fieldDefinitions'; + +import { objectMetadataItemSchema } from '../objectMetadataItemSchema'; + +describe('objectMetadataItemSchema', () => { + it('validates a valid object metadata item', () => { + // Given + const validObjectMetadataItem = mockedCompanyObjectMetadataItem; + + // When + const result = objectMetadataItemSchema.safeParse(validObjectMetadataItem); + + // Then + expect(result.success).toBe(true); + expect((result as SafeParseSuccess).data).toEqual( + validObjectMetadataItem, + ); + }); + + it('fails for an invalid object metadata item', () => { + // Given + const invalidObjectMetadataItem = { + createdAt: 'invalid date', + dataSourceId: 'invalid uuid', + fields: 'not an array', + icon: 'invalid icon', + isActive: 'not a boolean', + isCustom: 'not a boolean', + isSystem: 'not a boolean', + labelPlural: 123, + labelSingular: 123, + namePlural: 'notCamelCase', + nameSingular: 'notCamelCase', + updatedAt: 'invalid date', + }; + + // When + const result = objectMetadataItemSchema.safeParse( + invalidObjectMetadataItem, + ); + + // Then + expect(result.success).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts new file mode 100644 index 0000000000..0943a2e832 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; + +// TODO: implement fieldMetadataItemSchema +export const fieldMetadataItemSchema: z.ZodType = z.any(); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts new file mode 100644 index 0000000000..b925e1d2c1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema'; +import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema'; + +export const objectMetadataItemSchema = z.object({ + __typename: z.literal('object').optional(), + createdAt: z.string().datetime(), + dataSourceId: z.string().uuid(), + description: z.string().trim().nullable().optional(), + fields: z.array(fieldMetadataItemSchema), + icon: z.string().startsWith('Icon').trim(), + id: z.string().uuid(), + imageIdentifierFieldMetadataId: z.string().uuid().nullable(), + isActive: z.boolean(), + isCustom: z.boolean(), + isSystem: z.boolean(), + labelIdentifierFieldMetadataId: z.string().uuid().nullable(), + labelPlural: z.string().trim().min(1), + labelSingular: z.string().trim().min(1), + namePlural: camelCaseStringSchema, + nameSingular: camelCaseStringSchema, + updatedAt: z.string().datetime(), +}) satisfies z.ZodType; diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsCreateObjectInputSchema.test.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsCreateObjectInputSchema.test.ts new file mode 100644 index 0000000000..ff28a7a822 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsCreateObjectInputSchema.test.ts @@ -0,0 +1,47 @@ +import { SafeParseSuccess } from 'zod'; + +import { CreateObjectInput } from '~/generated-metadata/graphql'; + +import { settingsCreateObjectInputSchema } from '..//settingsCreateObjectInputSchema'; + +describe('settingsCreateObjectInputSchema', () => { + it('validates a valid input and adds name properties', () => { + // Given + const validInput = { + description: 'A valid description', + icon: 'IconPlus', + labelPlural: ' Labels ', + labelSingular: 'Label ', + }; + + // When + const result = settingsCreateObjectInputSchema.safeParse(validInput); + + // Then + expect(result.success).toBe(true); + expect((result as SafeParseSuccess).data).toEqual({ + description: validInput.description, + icon: validInput.icon, + labelPlural: 'Labels', + labelSingular: 'Label', + namePlural: 'labels', + nameSingular: 'label', + }); + }); + + it('fails for an invalid input', () => { + // Given + const invalidInput = { + description: 123, + icon: true, + labelPlural: [], + labelSingular: {}, + }; + + // When + const result = settingsCreateObjectInputSchema.safeParse(invalidInput); + + // Then + expect(result.success).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsUpdateObjectInputSchema.test.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsUpdateObjectInputSchema.test.ts new file mode 100644 index 0000000000..0147790fed --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsUpdateObjectInputSchema.test.ts @@ -0,0 +1,50 @@ +import { SafeParseSuccess } from 'zod'; + +import { UpdateObjectInput } from '~/generated-metadata/graphql'; + +import { settingsUpdateObjectInputSchema } from '../settingsUpdateObjectInputSchema'; + +describe('settingsUpdateObjectInputSchema', () => { + it('validates a valid input and adds name properties', () => { + // Given + const validInput = { + description: 'A valid description', + icon: 'IconName', + labelPlural: 'Labels Plural ', + labelSingular: ' Label Singular', + labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + }; + + // When + const result = settingsUpdateObjectInputSchema.safeParse(validInput); + + // Then + expect(result.success).toBe(true); + expect((result as SafeParseSuccess).data).toEqual({ + description: validInput.description, + icon: validInput.icon, + labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId, + labelPlural: 'Labels Plural', + labelSingular: 'Label Singular', + namePlural: 'labelsPlural', + nameSingular: 'labelSingular', + }); + }); + + it('fails for an invalid input', () => { + // Given + const invalidInput = { + description: 123, + icon: true, + labelPlural: [], + labelSingular: {}, + labelIdentifierFieldMetadataId: 'invalid uuid', + }; + + // When + const result = settingsUpdateObjectInputSchema.safeParse(invalidInput); + + // Then + expect(result.success).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsCreateObjectInputSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsCreateObjectInputSchema.ts new file mode 100644 index 0000000000..0188eda77e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsCreateObjectInputSchema.ts @@ -0,0 +1,17 @@ +import camelCase from 'lodash.camelcase'; + +import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; +import { CreateObjectInput } from '~/generated-metadata/graphql'; + +export const settingsCreateObjectInputSchema = objectMetadataItemSchema + .pick({ + description: true, + icon: true, + labelPlural: true, + labelSingular: true, + }) + .transform((value) => ({ + ...value, + nameSingular: camelCase(value.labelSingular), + namePlural: camelCase(value.labelPlural), + })); diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema.ts new file mode 100644 index 0000000000..36e8ee008c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema.ts @@ -0,0 +1,23 @@ +import camelCase from 'lodash.camelcase'; + +import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; +import { UpdateObjectInput } from '~/generated-metadata/graphql'; + +export const settingsUpdateObjectInputSchema = objectMetadataItemSchema + .pick({ + description: true, + icon: true, + imageIdentifierFieldMetadataId: true, + isActive: true, + labelIdentifierFieldMetadataId: true, + labelPlural: true, + labelSingular: true, + }) + .partial() + .transform((value) => ({ + ...value, + nameSingular: value.labelSingular + ? camelCase(value.labelSingular) + : undefined, + namePlural: value.labelPlural ? camelCase(value.labelPlural) : undefined, + })); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx index 97b83f2724..83a11115c2 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -1,12 +1,15 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection'; +import { settingsCreateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsCreateObjectInputSchema'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; import { IconSettings } from '@/ui/display/icon'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; @@ -16,27 +19,22 @@ export const SettingsNewObject = () => { const navigate = useNavigate(); const { enqueueSnackBar } = useSnackBar(); - const { createObjectMetadataItem: createObject } = - useObjectMetadataItemForSettings(); + const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem(); - const [customFormValues, setCustomFormValues] = useState<{ + const [formValues, setFormValues] = useState<{ description?: string; icon: string; labelPlural: string; labelSingular: string; }>({ icon: 'IconListNumbers', labelPlural: '', labelSingular: '' }); - const canSave = - !!customFormValues.labelPlural && !!customFormValues.labelSingular; + const canSave = !!formValues.labelPlural && !!formValues.labelSingular; const handleSave = async () => { try { - const createdObject = await createObject({ - labelPlural: customFormValues.labelPlural, - labelSingular: customFormValues.labelSingular, - description: customFormValues.description, - icon: customFormValues.icon, - }); + const createdObject = await createOneObjectMetadataItem( + settingsCreateObjectInputSchema.parse(formValues), + ); navigate( createdObject.data?.createOneObject.isActive @@ -58,25 +56,26 @@ export const SettingsNewObject = () => { { - navigate('/settings/objects'); - }} + onCancel={() => navigate(getSettingsPagePath(SettingsPath.Objects))} onSave={handleSave} /> { - setCustomFormValues((previousValues) => ({ + setFormValues((previousValues) => ({ ...previousValues, ...formValues, })); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx index 7a76d1c9c4..e1090d1f2e 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; @@ -16,7 +17,9 @@ import { StyledObjectFieldTableRow, } from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow'; import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; import { IconPlus, IconSettings } from '@/ui/display/icon'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Button } from '@/ui/input/button/components/Button'; @@ -38,11 +41,9 @@ export const SettingsObjectDetail = () => { const navigate = useNavigate(); const { objectSlug = '' } = useParams(); - const { - disableObjectMetadataItem, - editObjectMetadataItem, - findActiveObjectMetadataItemBySlug, - } = useObjectMetadataItemForSettings(); + const { findActiveObjectMetadataItemBySlug } = + useObjectMetadataItemForSettings(); + const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); @@ -64,8 +65,11 @@ export const SettingsObjectDetail = () => { ); const handleDisableObject = async () => { - await disableObjectMetadataItem(activeObjectMetadataItem); - navigate('/settings/objects'); + await updateOneObjectMetadataItem({ + idToUpdate: activeObjectMetadataItem.id, + updatePayload: { isActive: false }, + }); + navigate(getSettingsPagePath(SettingsPath.Objects)); }; const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => { @@ -74,12 +78,13 @@ export const SettingsObjectDetail = () => { const handleSetLabelIdentifierField = ( activeFieldMetadatItem: FieldMetadataItem, - ) => { - editObjectMetadataItem({ - ...activeObjectMetadataItem, - labelIdentifierFieldMetadataId: activeFieldMetadatItem.id, + ) => + updateOneObjectMetadataItem({ + idToUpdate: activeObjectMetadataItem.id, + updatePayload: { + labelIdentifierFieldMetadataId: activeFieldMetadatItem.id, + }, }); - }; return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx index 3963c57745..3d7232a637 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx @@ -2,13 +2,17 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection'; import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; +import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; import { IconArchive, IconSettings } from '@/ui/display/icon'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -22,11 +26,9 @@ export const SettingsObjectEdit = () => { const { enqueueSnackBar } = useSnackBar(); const { objectSlug = '' } = useParams(); - const { - disableObjectMetadataItem, - editObjectMetadataItem, - findActiveObjectMetadataItemBySlug, - } = useObjectMetadataItemForSettings(); + const { findActiveObjectMetadataItemBySlug } = + useObjectMetadataItemForSettings(); + const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); @@ -76,7 +78,10 @@ export const SettingsObjectEdit = () => { }; try { - await editObjectMetadataItem(editedObjectMetadataItem); + await updateOneObjectMetadataItem({ + idToUpdate: activeObjectMetadataItem.id, + updatePayload: settingsUpdateObjectInputSchema.parse(formValues), + }); navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`); } catch (error) { @@ -87,8 +92,11 @@ export const SettingsObjectEdit = () => { }; const handleDisable = async () => { - await disableObjectMetadataItem(activeObjectMetadataItem); - navigate('/settings/objects'); + await updateOneObjectMetadataItem({ + idToUpdate: activeObjectMetadataItem.id, + updatePayload: { isActive: false }, + }); + navigate(getSettingsPagePath(SettingsPath.Objects)); }; return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx index 4e5af2b6ac..7a986fa986 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -2,7 +2,9 @@ import { useNavigate } from 'react-router-dom'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; @@ -12,6 +14,8 @@ import { } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow'; import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage'; import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; import { IconChevronRight, IconPlus, IconSettings } from '@/ui/display/icon'; import { H1Title } from '@/ui/display/typography/components/H1Title'; import { H2Title } from '@/ui/display/typography/components/H2Title'; @@ -34,12 +38,10 @@ export const SettingsObjects = () => { const theme = useTheme(); const navigate = useNavigate(); - const { - activateObjectMetadataItem, - activeObjectMetadataItems, - inactiveObjectMetadataItems, - eraseObjectMetadataItem, - } = useObjectMetadataItemForSettings(); + const { activeObjectMetadataItems, inactiveObjectMetadataItems } = + useObjectMetadataItemForSettings(); + const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem(); + const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); return ( @@ -51,7 +53,9 @@ export const SettingsObjects = () => { title="Add object" accent="blue" size="small" - onClick={() => navigate('/settings/objects/new')} + onClick={() => + navigate(getSettingsPagePath(SettingsPath.NewObject)) + } />
@@ -101,13 +105,14 @@ export const SettingsObjects = () => { isCustomObject={inactiveObjectMetadataItem.isCustom} scopeKey={inactiveObjectMetadataItem.namePlural} onActivate={() => - activateObjectMetadataItem( - inactiveObjectMetadataItem, - ) + updateOneObjectMetadataItem({ + idToUpdate: inactiveObjectMetadataItem.id, + updatePayload: { isActive: true }, + }) } onErase={() => - eraseObjectMetadataItem( - inactiveObjectMetadataItem, + deleteOneObjectMetadataItem( + inactiveObjectMetadataItem.id, ) } /> diff --git a/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts b/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts new file mode 100644 index 0000000000..4eb0677c39 --- /dev/null +++ b/packages/twenty-front/src/utils/validation-schemas/__tests__/camelCaseStringSchema.test.ts @@ -0,0 +1,22 @@ +import { SafeParseError } from 'zod'; + +import { camelCaseStringSchema } from '../camelCaseStringSchema'; + +describe('camelCaseStringSchema', () => { + it('validates a camel case string', () => { + const result = camelCaseStringSchema.safeParse('camelCaseString'); + expect(result.success).toBe(true); + }); + + it('fails for non-camel case strings', () => { + const result = camelCaseStringSchema.safeParse('NotCamelCase'); + expect(result.success).toBe(false); + expect((result as SafeParseError).error.errors).toEqual([ + { + code: 'custom', + message: 'String should be camel case', + path: [], + }, + ]); + }); +}); diff --git a/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts b/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts new file mode 100644 index 0000000000..0e96c167ca --- /dev/null +++ b/packages/twenty-front/src/utils/validation-schemas/camelCaseStringSchema.ts @@ -0,0 +1,8 @@ +import camelCase from 'lodash.camelcase'; +import { z } from 'zod'; + +export const camelCaseStringSchema = z + .string() + .refine((value) => camelCase(value) === value, { + message: 'String should be camel case', + });