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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 332 additions and 189 deletions

View File

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

View File

@ -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 }) => (
</RecoilRoot>
);
describe('useCreateOneObjectRecordMetadataItem', () => {
describe('useCreateOneObjectMetadataItem', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => useCreateOneObjectRecordMetadataItem(),
{
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 });

View File

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

View File

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

View File

@ -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<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 {
activateObjectMetadataItem,
activeObjectMetadataItems,
createObjectMetadataItem,
inactiveObjectMetadataItems,
disableObjectMetadataItem,
editObjectMetadataItem,
eraseObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById,
findObjectMetadataItemByNamePlural,
inactiveObjectMetadataItems,
objectMetadataItems,
};
};

View File

@ -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: {

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 { 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 = () => {
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: 'Objects',
href: getSettingsPagePath(SettingsPath.Objects),
},
{ children: 'New' },
]}
/>
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigate('/settings/objects');
}}
onCancel={() => navigate(getSettingsPagePath(SettingsPath.Objects))}
onSave={handleSave}
/>
</SettingsHeaderContainer>
<SettingsObjectFormSection
icon={customFormValues.icon}
singularName={customFormValues.labelSingular}
pluralName={customFormValues.labelPlural}
description={customFormValues.description}
icon={formValues.icon}
singularName={formValues.labelSingular}
pluralName={formValues.labelPlural}
description={formValues.description}
onChange={(formValues) => {
setCustomFormValues((previousValues) => ({
setFormValues((previousValues) => ({
...previousValues,
...formValues,
}));

View File

@ -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,
) =>
updateOneObjectMetadataItem({
idToUpdate: activeObjectMetadataItem.id,
updatePayload: {
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
},
});
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">

View File

@ -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 (

View File

@ -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 (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
@ -51,7 +53,9 @@ export const SettingsObjects = () => {
title="Add object"
accent="blue"
size="small"
onClick={() => navigate('/settings/objects/new')}
onClick={() =>
navigate(getSettingsPagePath(SettingsPath.NewObject))
}
/>
</SettingsHeaderContainer>
<div>
@ -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,
)
}
/>

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