refactor: validate objectMetadataItem with Zod on creation and update… (#4270)

* refactor: validate objectMetadataItem with Zod on creation and update & remove logic from useObjectMetadataItemForSettings

* refactor: review
This commit is contained in:
Thaïs
2024-03-05 07:32:30 -03:00
committed by GitHub
parent 0a2d8056bd
commit a9f4a66c4f
20 changed files with 332 additions and 189 deletions

View File

@ -24,6 +24,7 @@ export const query = gql`
export const variables = { export const variables = {
input: { input: {
object: { object: {
icon: 'IconPlus',
labelPlural: 'View Filters', labelPlural: 'View Filters',
labelSingular: 'View Filter', labelSingular: 'View Filter',
nameSingular: 'viewFilter', nameSingular: 'viewFilter',

View File

@ -3,14 +3,14 @@ import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { useCreateOneObjectRecordMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider'; import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
import { import {
query, query,
responseData, responseData,
variables, variables,
} from '../__mocks__/useCreateOneObjectRecordMetadataItem'; } from '../__mocks__/useCreateOneObjectMetadataItem';
const mocks = [ const mocks = [
{ {
@ -36,21 +36,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
</RecoilRoot> </RecoilRoot>
); );
describe('useCreateOneObjectRecordMetadataItem', () => { describe('useCreateOneObjectMetadataItem', () => {
it('should work as expected', async () => { it('should work as expected', async () => {
const { result } = renderHook( const { result } = renderHook(() => useCreateOneObjectMetadataItem(), {
() => useCreateOneObjectRecordMetadataItem(), wrapper: Wrapper,
{ });
wrapper: Wrapper,
},
);
await act(async () => { await act(async () => {
const res = await result.current.createOneObjectMetadataItem({ const res = await result.current.createOneObjectMetadataItem({
icon: 'IconPlus',
labelPlural: 'View Filters', labelPlural: 'View Filters',
labelSingular: 'View Filter', labelSingular: 'View Filter',
nameSingular: 'viewFilter',
namePlural: 'viewFilters', namePlural: 'viewFilters',
nameSingular: 'viewFilter',
}); });
expect(res.data).toEqual({ createOneObject: responseData }); expect(res.data).toEqual({ createOneObject: responseData });

View File

@ -103,28 +103,4 @@ describe('useObjectMetadataItemForSettings', () => {
expect(res?.namePlural).toBe('opportunities'); 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 });
});
});
}); });

View File

@ -2,6 +2,7 @@ import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { import {
CreateObjectInput,
CreateOneObjectMetadataItemMutation, CreateOneObjectMetadataItemMutation,
CreateOneObjectMetadataItemMutationVariables, CreateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
@ -11,7 +12,7 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient'; import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useCreateOneObjectRecordMetadataItem = () => { export const useCreateOneObjectMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient(); const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation< const [mutate] = useMutation<
@ -21,16 +22,10 @@ export const useCreateOneObjectRecordMetadataItem = () => {
client: apolloMetadataClient ?? ({} as ApolloClient<any>), client: apolloMetadataClient ?? ({} as ApolloClient<any>),
}); });
const createOneObjectMetadataItem = async ( const createOneObjectMetadataItem = async (input: CreateObjectInput) => {
input: CreateOneObjectMetadataItemMutationVariables['input']['object'],
) => {
return await mutate({ return await mutate({
variables: { variables: {
input: { input: { object: input },
object: {
...input,
},
},
}, },
awaitRefetchQueries: true, awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''], refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],

View File

@ -2,14 +2,8 @@ import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
import { formatObjectMetadataItemInput } from '../utils/formatObjectMetadataItemInput';
import { getObjectSlug } from '../utils/getObjectSlug'; import { getObjectSlug } from '../utils/getObjectSlug';
import { useCreateOneObjectRecordMetadataItem } from './useCreateOneObjectMetadataItem';
import { useDeleteOneObjectMetadataItem } from './useDeleteOneObjectMetadataItem';
import { useUpdateOneObjectMetadataItem } from './useUpdateOneObjectMetadataItem';
export const useObjectMetadataItemForSettings = () => { export const useObjectMetadataItemForSettings = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -36,65 +30,12 @@ export const useObjectMetadataItemForSettings = () => {
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural, (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<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: true },
});
const disableObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: false },
});
const eraseObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) => deleteOneObjectMetadataItem(objectMetadataItem.id);
return { return {
activateObjectMetadataItem,
activeObjectMetadataItems, activeObjectMetadataItems,
createObjectMetadataItem,
inactiveObjectMetadataItems,
disableObjectMetadataItem,
editObjectMetadataItem,
eraseObjectMetadataItem,
findActiveObjectMetadataItemBySlug, findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById, findObjectMetadataItemById,
findObjectMetadataItemByNamePlural, findObjectMetadataItemByNamePlural,
inactiveObjectMetadataItems,
objectMetadataItems, objectMetadataItems,
}; };
}; };

View File

@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { import {
UpdateObjectInput,
UpdateOneObjectMetadataItemMutation, UpdateOneObjectMetadataItemMutation,
UpdateOneObjectMetadataItemMutationVariables, UpdateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
@ -27,16 +28,7 @@ export const useUpdateOneObjectMetadataItem = () => {
updatePayload, updatePayload,
}: { }: {
idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate']; idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate'];
updatePayload: Pick< updatePayload: UpdateObjectInput;
UpdateOneObjectMetadataItemMutationVariables['updatePayload'],
| 'description'
| 'icon'
| 'isActive'
| 'labelPlural'
| 'labelSingular'
| 'namePlural'
| 'nameSingular'
>;
}) => { }) => {
return await mutate({ return await mutate({
variables: { variables: {

View File

@ -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()),
});

View File

@ -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<ObjectMetadataItem>).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);
});
});

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
// TODO: implement fieldMetadataItemSchema
export const fieldMetadataItemSchema: z.ZodType<FieldMetadataItem> = z.any();

View File

@ -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<ObjectMetadataItem>;

View File

@ -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<CreateObjectInput>).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);
});
});

View File

@ -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<UpdateObjectInput>).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);
});
});

View File

@ -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<CreateObjectInput>((value) => ({
...value,
nameSingular: camelCase(value.labelSingular),
namePlural: camelCase(value.labelPlural),
}));

View File

@ -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<UpdateObjectInput>((value) => ({
...value,
nameSingular: value.labelSingular
? camelCase(value.labelSingular)
: undefined,
namePlural: value.labelPlural ? camelCase(value.labelPlural) : undefined,
}));

View File

@ -1,12 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; 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 { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection'; 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 { IconSettings } from '@/ui/display/icon';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
@ -16,27 +19,22 @@ export const SettingsNewObject = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { createObjectMetadataItem: createObject } = const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
useObjectMetadataItemForSettings();
const [customFormValues, setCustomFormValues] = useState<{ const [formValues, setFormValues] = useState<{
description?: string; description?: string;
icon: string; icon: string;
labelPlural: string; labelPlural: string;
labelSingular: string; labelSingular: string;
}>({ icon: 'IconListNumbers', labelPlural: '', labelSingular: '' }); }>({ icon: 'IconListNumbers', labelPlural: '', labelSingular: '' });
const canSave = const canSave = !!formValues.labelPlural && !!formValues.labelSingular;
!!customFormValues.labelPlural && !!customFormValues.labelSingular;
const handleSave = async () => { const handleSave = async () => {
try { try {
const createdObject = await createObject({ const createdObject = await createOneObjectMetadataItem(
labelPlural: customFormValues.labelPlural, settingsCreateObjectInputSchema.parse(formValues),
labelSingular: customFormValues.labelSingular, );
description: customFormValues.description,
icon: customFormValues.icon,
});
navigate( navigate(
createdObject.data?.createOneObject.isActive createdObject.data?.createOneObject.isActive
@ -58,25 +56,26 @@ export const SettingsNewObject = () => {
<SettingsHeaderContainer> <SettingsHeaderContainer>
<Breadcrumb <Breadcrumb
links={[ links={[
{ children: 'Objects', href: '/settings/objects' }, {
children: 'Objects',
href: getSettingsPagePath(SettingsPath.Objects),
},
{ children: 'New' }, { children: 'New' },
]} ]}
/> />
<SaveAndCancelButtons <SaveAndCancelButtons
isSaveDisabled={!canSave} isSaveDisabled={!canSave}
onCancel={() => { onCancel={() => navigate(getSettingsPagePath(SettingsPath.Objects))}
navigate('/settings/objects');
}}
onSave={handleSave} onSave={handleSave}
/> />
</SettingsHeaderContainer> </SettingsHeaderContainer>
<SettingsObjectFormSection <SettingsObjectFormSection
icon={customFormValues.icon} icon={formValues.icon}
singularName={customFormValues.labelSingular} singularName={formValues.labelSingular}
pluralName={customFormValues.labelPlural} pluralName={formValues.labelPlural}
description={customFormValues.description} description={formValues.description}
onChange={(formValues) => { onChange={(formValues) => {
setCustomFormValues((previousValues) => ({ setFormValues((previousValues) => ({
...previousValues, ...previousValues,
...formValues, ...formValues,
})); }));

View File

@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
@ -16,7 +17,9 @@ import {
StyledObjectFieldTableRow, StyledObjectFieldTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow'; } from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType'; import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { IconPlus, IconSettings } from '@/ui/display/icon'; import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
@ -38,11 +41,9 @@ export const SettingsObjectDetail = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectSlug = '' } = useParams();
const { const { findActiveObjectMetadataItemBySlug } =
disableObjectMetadataItem, useObjectMetadataItemForSettings();
editObjectMetadataItem, const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
findActiveObjectMetadataItemBySlug,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemBySlug(objectSlug);
@ -64,8 +65,11 @@ export const SettingsObjectDetail = () => {
); );
const handleDisableObject = async () => { const handleDisableObject = async () => {
await disableObjectMetadataItem(activeObjectMetadataItem); await updateOneObjectMetadataItem({
navigate('/settings/objects'); idToUpdate: activeObjectMetadataItem.id,
updatePayload: { isActive: false },
});
navigate(getSettingsPagePath(SettingsPath.Objects));
}; };
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => { const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
@ -74,12 +78,13 @@ export const SettingsObjectDetail = () => {
const handleSetLabelIdentifierField = ( const handleSetLabelIdentifierField = (
activeFieldMetadatItem: FieldMetadataItem, activeFieldMetadatItem: FieldMetadataItem,
) => { ) =>
editObjectMetadataItem({ updateOneObjectMetadataItem({
...activeObjectMetadataItem, idToUpdate: activeObjectMetadataItem.id,
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id, updatePayload: {
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
},
}); });
};
return ( return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">

View File

@ -2,13 +2,17 @@ import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection'; import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; 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 { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { IconArchive, IconSettings } from '@/ui/display/icon'; import { IconArchive, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -22,11 +26,9 @@ export const SettingsObjectEdit = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { objectSlug = '' } = useParams(); const { objectSlug = '' } = useParams();
const { const { findActiveObjectMetadataItemBySlug } =
disableObjectMetadataItem, useObjectMetadataItemForSettings();
editObjectMetadataItem, const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
findActiveObjectMetadataItemBySlug,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemBySlug(objectSlug);
@ -76,7 +78,10 @@ export const SettingsObjectEdit = () => {
}; };
try { try {
await editObjectMetadataItem(editedObjectMetadataItem); await updateOneObjectMetadataItem({
idToUpdate: activeObjectMetadataItem.id,
updatePayload: settingsUpdateObjectInputSchema.parse(formValues),
});
navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`); navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`);
} catch (error) { } catch (error) {
@ -87,8 +92,11 @@ export const SettingsObjectEdit = () => {
}; };
const handleDisable = async () => { const handleDisable = async () => {
await disableObjectMetadataItem(activeObjectMetadataItem); await updateOneObjectMetadataItem({
navigate('/settings/objects'); idToUpdate: activeObjectMetadataItem.id,
updatePayload: { isActive: false },
});
navigate(getSettingsPagePath(SettingsPath.Objects));
}; };
return ( return (

View File

@ -2,7 +2,9 @@ import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -12,6 +14,8 @@ import {
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow'; } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage'; import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown'; 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 { IconChevronRight, IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title'; import { H1Title } from '@/ui/display/typography/components/H1Title';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -34,12 +38,10 @@ export const SettingsObjects = () => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const { const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
activateObjectMetadataItem, useObjectMetadataItemForSettings();
activeObjectMetadataItems, const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
inactiveObjectMetadataItems, const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
eraseObjectMetadataItem,
} = useObjectMetadataItemForSettings();
return ( return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">
@ -51,7 +53,9 @@ export const SettingsObjects = () => {
title="Add object" title="Add object"
accent="blue" accent="blue"
size="small" size="small"
onClick={() => navigate('/settings/objects/new')} onClick={() =>
navigate(getSettingsPagePath(SettingsPath.NewObject))
}
/> />
</SettingsHeaderContainer> </SettingsHeaderContainer>
<div> <div>
@ -101,13 +105,14 @@ export const SettingsObjects = () => {
isCustomObject={inactiveObjectMetadataItem.isCustom} isCustomObject={inactiveObjectMetadataItem.isCustom}
scopeKey={inactiveObjectMetadataItem.namePlural} scopeKey={inactiveObjectMetadataItem.namePlural}
onActivate={() => onActivate={() =>
activateObjectMetadataItem( updateOneObjectMetadataItem({
inactiveObjectMetadataItem, idToUpdate: inactiveObjectMetadataItem.id,
) updatePayload: { isActive: true },
})
} }
onErase={() => onErase={() =>
eraseObjectMetadataItem( deleteOneObjectMetadataItem(
inactiveObjectMetadataItem, inactiveObjectMetadataItem.id,
) )
} }
/> />

View File

@ -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<string>).error.errors).toEqual([
{
code: 'custom',
message: 'String should be camel case',
path: [],
},
]);
});
});

View File

@ -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',
});