[REFACTOR][FRONT]: Remove objectMetadata and fieldMetadata sluggification (#9441)

# Introduction
For motivations and context please have a look to
https://github.com/twentyhq/twenty/pull/9394 whom this PR results from.
In this pull-request we remove any `metadataField` and `objectMetadata`
sluggification. We directly consume `objectMetadata.namePlural` and
`metadataField.name`, ***it seems like that historically the consumed
`metadataField.name`*** are we sure that we wanna change this behavior ?

## Notes
Unless I'm mistaken by reverting the `kebabcase` url formatting we might
be creating deadlinks that user could have save beforehand => Discussed
with Charles said it's controlled risk.

---------

Co-authored-by: Paul Rastoin <paulrastoin@Pauls-MacBook-Pro.local>
This commit is contained in:
Paul Rastoin
2025-01-08 11:31:53 +01:00
committed by GitHub
parent 00a9646d68
commit aa0d8546a8
31 changed files with 98 additions and 156 deletions

View File

@ -20,7 +20,7 @@ export const useLastVisitedObjectMetadataItem = () => {
useRecoilState(lastVisitedObjectMetadataItemIdState); useRecoilState(lastVisitedObjectMetadataItemIdState);
const { const {
findActiveObjectMetadataItemBySlug, findActiveObjectMetadataItemByNamePlural,
alphaSortedActiveObjectMetadataItems, alphaSortedActiveObjectMetadataItems,
} = useFilteredObjectMetadataItems(); } = useFilteredObjectMetadataItems();
@ -51,7 +51,7 @@ export const useLastVisitedObjectMetadataItem = () => {
const setLastVisitedObjectMetadataItem = (objectNamePlural: string) => { const setLastVisitedObjectMetadataItem = (objectNamePlural: string) => {
const fallbackObjectMetadataItem = const fallbackObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectNamePlural); findActiveObjectMetadataItemByNamePlural(objectNamePlural);
if (isDefined(fallbackObjectMetadataItem)) { if (isDefined(fallbackObjectMetadataItem)) {
setLastVisitedObjectMetadataItemId(fallbackObjectMetadataItem.id); setLastVisitedObjectMetadataItemId(fallbackObjectMetadataItem.id);

View File

@ -35,7 +35,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
); );
describe('useFilteredObjectMetadataItems', () => { describe('useFilteredObjectMetadataItems', () => {
it('should findActiveObjectMetadataItemBySlug', async () => { it('should findActiveObjectMetadataItemByNamePlural', async () => {
const { result } = renderHook( const { result } = renderHook(
() => { () => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState); const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
@ -49,13 +49,14 @@ describe('useFilteredObjectMetadataItems', () => {
); );
act(() => { act(() => {
const res = result.current.findActiveObjectMetadataItemBySlug('people'); const res =
result.current.findActiveObjectMetadataItemByNamePlural('people');
expect(res).toBeDefined(); expect(res).toBeDefined();
expect(res?.namePlural).toBe('people'); expect(res?.namePlural).toBe('people');
}); });
}); });
it('should findObjectMetadataItemBySlug', async () => { it('should findObjectMetadataItemByNamePlural', async () => {
const { result } = renderHook( const { result } = renderHook(
() => { () => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState); const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
@ -69,7 +70,7 @@ describe('useFilteredObjectMetadataItems', () => {
); );
act(() => { act(() => {
const res = result.current.findObjectMetadataItemBySlug('people'); const res = result.current.findObjectMetadataItemByNamePlural('people');
expect(res).toBeDefined(); expect(res).toBeDefined();
expect(res?.namePlural).toBe('people'); expect(res?.namePlural).toBe('people');
}); });

View File

@ -2,8 +2,6 @@ import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectSlug } from '../utils/getObjectSlug';
export const useFilteredObjectMetadataItems = () => { export const useFilteredObjectMetadataItems = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -27,17 +25,6 @@ export const useFilteredObjectMetadataItems = () => {
({ isActive, isSystem }) => !isActive && !isSystem, ({ isActive, isSystem }) => !isActive && !isSystem,
); );
const findObjectMetadataItemBySlug = (slug: string) =>
objectMetadataItems.find(
(objectMetadataItem) => getObjectSlug(objectMetadataItem) === slug,
);
const findActiveObjectMetadataItemBySlug = (slug: string) =>
activeObjectMetadataItems.find(
(activeObjectMetadataItem) =>
getObjectSlug(activeObjectMetadataItem) === slug,
);
const findActiveObjectMetadataItemByNamePlural = (namePlural: string) => const findActiveObjectMetadataItemByNamePlural = (namePlural: string) =>
activeObjectMetadataItems.find( activeObjectMetadataItems.find(
(activeObjectMetadataItem) => (activeObjectMetadataItem) =>
@ -56,13 +43,11 @@ export const useFilteredObjectMetadataItems = () => {
return { return {
activeObjectMetadataItems, activeObjectMetadataItems,
findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById, findObjectMetadataItemById,
findObjectMetadataItemByNamePlural, findObjectMetadataItemByNamePlural,
findActiveObjectMetadataItemByNamePlural, findActiveObjectMetadataItemByNamePlural,
inactiveObjectMetadataItems, inactiveObjectMetadataItems,
objectMetadataItems, objectMetadataItems,
findObjectMetadataItemBySlug,
alphaSortedActiveObjectMetadataItems, alphaSortedActiveObjectMetadataItems,
}; };
}; };

View File

@ -1,8 +0,0 @@
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
describe('getFieldSlug', () => {
it('should work as expected', () => {
const res = getFieldSlug({ label: 'Pipeline Step' });
expect(res).toBe('pipeline-step');
});
});

View File

@ -1,13 +0,0 @@
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
describe('getObjectSlug', () => {
it('should work as expected', () => {
const objectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const res = getObjectSlug(objectMetadataItem);
expect(res).toBe('people');
});
});

View File

@ -1,6 +0,0 @@
import toKebabCase from 'lodash.kebabcase';
import { Field } from '~/generated-metadata/graphql';
export const getFieldSlug = (metadataField: Pick<Field, 'label'>) =>
toKebabCase(metadataField.label);

View File

@ -1,7 +0,0 @@
import toKebabCase from 'lodash.kebabcase';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const getObjectSlug = (
objectMetadataItem: Pick<ObjectMetadataItem, 'namePlural'>,
) => toKebabCase(objectMetadataItem.namePlural);

View File

@ -35,7 +35,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
}); });
const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
objectSlug: objectNamePlural, objectNamePlural,
}); });
const { handleColumnVisibilityChange, hiddenTableColumns } = const { handleColumnVisibilityChange, hiddenTableColumns } =

View File

@ -54,8 +54,8 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => {
const viewGroupSettingsUrl = getSettingsPagePath( const viewGroupSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectFieldEdit, SettingsPath.ObjectFieldEdit,
{ {
objectSlug: objectNamePlural, objectNamePlural,
fieldSlug: recordGroupFieldMetadata?.name ?? '', fieldName: recordGroupFieldMetadata?.name ?? '',
}, },
); );

View File

@ -69,7 +69,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
const newSelectFieldSettingsUrl = getSettingsPagePath( const newSelectFieldSettingsUrl = getSettingsPagePath(
SettingsPath.ObjectNewFieldConfigure, SettingsPath.ObjectNewFieldConfigure,
{ {
objectSlug: objectNamePlural, objectNamePlural,
}, },
{ {
fieldType: FieldMetadataType.Select, fieldType: FieldMetadataType.Select,

View File

@ -1,6 +1,4 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
@ -55,7 +53,7 @@ export const useRecordGroupActions = ({
throw new Error('recordGroupFieldMetadata is not a non-empty string'); throw new Error('recordGroupFieldMetadata is not a non-empty string');
} }
const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(recordGroupFieldMetadata)}`; const settingsPath = `/settings/objects/${objectMetadataItem.namePlural}/${recordGroupFieldMetadata.name}`;
navigate(settingsPath); navigate(settingsPath);
}, [ }, [

View File

@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { IconSettings, MenuItem, UndecoratedLink, useIcons } from 'twenty-ui'; import { IconSettings, MenuItem, UndecoratedLink, useIcons } from 'twenty-ui';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns';
@ -56,7 +55,7 @@ export const RecordTableHeaderPlusButtonContent = () => {
<DropdownMenuItemsContainer scrollable={false}> <DropdownMenuItemsContainer scrollable={false}>
<UndecoratedLink <UndecoratedLink
fullWidth fullWidth
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`} to={`/settings/objects/${objectMetadataItem.namePlural}`}
onClick={() => { onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search); setNavigationMemorizedUrl(location.pathname + location.search);
}} }}

View File

@ -68,7 +68,7 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { objectSlug = '' } = useParams(); const { objectNamePlural = '' } = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const theme = useTheme(); const theme = useTheme();
@ -78,11 +78,11 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
const handleClick = (step: 'select' | 'configure') => { const handleClick = (step: 'select' | 'configure') => {
if (step === 'configure' && isDefined(fieldType)) { if (step === 'configure' && isDefined(fieldType)) {
navigate( navigate(
`/settings/objects/${objectSlug}/new-field/configure?fieldType=${fieldType}`, `/settings/objects/${objectNamePlural}/new-field/configure?fieldType=${fieldType}`,
); );
} else { } else {
navigate( navigate(
`/settings/objects/${objectSlug}/new-field/select${fieldType ? `?fieldType=${fieldType}` : ''}`, `/settings/objects/${objectNamePlural}/new-field/select${fieldType ? `?fieldType=${fieldType}` : ''}`,
); );
} }
closeDropdown(); closeDropdown();

View File

@ -26,7 +26,7 @@ type SettingsObjectNewFieldSelectorProps = {
'defaultValue' | 'options' | 'type' 'defaultValue' | 'options' | 'type'
>; >;
objectSlug: string; objectNamePlural: string;
}; };
const StyledTypeSelectContainer = styled.div` const StyledTypeSelectContainer = styled.div`
@ -58,7 +58,7 @@ const StyledSearchInput = styled(TextInput)`
export const SettingsObjectNewFieldSelector = ({ export const SettingsObjectNewFieldSelector = ({
excludedFieldTypes = [], excludedFieldTypes = [],
fieldMetadataItem, fieldMetadataItem,
objectSlug, objectNamePlural,
}: SettingsObjectNewFieldSelectorProps) => { }: SettingsObjectNewFieldSelectorProps) => {
const theme = useTheme(); const theme = useTheme();
const { control, setValue } = const { control, setValue } =
@ -128,7 +128,7 @@ export const SettingsObjectNewFieldSelector = ({
.map(([key, config]) => ( .map(([key, config]) => (
<StyledCardContainer key={key}> <StyledCardContainer key={key}>
<UndecoratedLink <UndecoratedLink
to={`/settings/objects/${objectSlug}/new-field/configure?fieldType=${key}`} to={`/settings/objects/${objectNamePlural}/new-field/configure?fieldType=${key}`}
fullWidth fullWidth
onClick={() => { onClick={() => {
setValue('type', key as SettingsFieldType); setValue('type', key as SettingsFieldType);

View File

@ -5,7 +5,6 @@ import { Link } from 'react-router-dom';
import { IconChevronDown, IconChevronUp, useIcons } from 'twenty-ui'; import { IconChevronDown, IconChevronUp, useIcons } from 'twenty-ui';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField'; import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag'; import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
@ -100,36 +99,38 @@ const StyledObjectLink = styled(Link)`
`; `;
export const SettingsDataModelOverviewObject = ({ export const SettingsDataModelOverviewObject = ({
data, data: objectMetadataItem,
}: SettingsDataModelOverviewObjectProps) => { }: SettingsDataModelOverviewObjectProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const [otherFieldsExpanded, setOtherFieldsExpanded] = useState(false); const [otherFieldsExpanded, setOtherFieldsExpanded] = useState(false);
const { totalCount } = useFindManyRecords({ const { totalCount } = useFindManyRecords({
objectNameSingular: data.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
}); });
const fields = data.fields.filter((x) => !x.isSystem); const fields = objectMetadataItem.fields.filter((x) => !x.isSystem);
const countNonRelation = fields.filter( const countNonRelation = fields.filter(
(x) => x.type !== FieldMetadataType.Relation, (x) => x.type !== FieldMetadataType.Relation,
).length; ).length;
const Icon = getIcon(data.icon); const Icon = getIcon(objectMetadataItem.icon);
return ( return (
<StyledNode> <StyledNode>
<StyledHeader> <StyledHeader>
<StyledObjectName onMouseEnter={() => {}} onMouseLeave={() => {}}> <StyledObjectName onMouseEnter={() => {}} onMouseLeave={() => {}}>
<StyledObjectLink to={`/settings/objects/${getObjectSlug(data)}`}> <StyledObjectLink
to={`/settings/objects/${objectMetadataItem.namePlural}`}
>
{Icon && <Icon size={theme.icon.size.md} />} {Icon && <Icon size={theme.icon.size.md} />}
{capitalize(data.namePlural)} {capitalize(objectMetadataItem.namePlural)}
</StyledObjectLink> </StyledObjectLink>
<StyledObjectInstanceCount> · {totalCount}</StyledObjectInstanceCount> <StyledObjectInstanceCount> · {totalCount}</StyledObjectInstanceCount>
</StyledObjectName> </StyledObjectName>
<SettingsDataModelObjectTypeTag <SettingsDataModelObjectTypeTag
objectTypeLabel={getObjectTypeLabel(data)} objectTypeLabel={getObjectTypeLabel(objectMetadataItem)}
></SettingsDataModelObjectTypeTag> ></SettingsDataModelObjectTypeTag>
</StyledHeader> </StyledHeader>

View File

@ -4,8 +4,6 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache'; import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
@ -110,7 +108,7 @@ export const SettingsObjectFieldItemTableRow = ({
!isLabelIdentifier && !isLabelIdentifier &&
LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type); LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type);
const linkToNavigate = `./${getFieldSlug(fieldMetadataItem)}`; const linkToNavigate = `./${fieldMetadataItem.name}`;
const { const {
activateMetadataField, activateMetadataField,
@ -246,7 +244,7 @@ export const SettingsObjectFieldItemTableRow = ({
} }
to={ to={
isRelatedObjectLinkable isRelatedObjectLinkable
? `/settings/objects/${getObjectSlug(relationObjectMetadataItem)}` ? `/settings/objects/${relationObjectMetadataItem.namePlural}`
: undefined : undefined
} }
value={fieldType} value={fieldType}

View File

@ -9,7 +9,6 @@ import { z, ZodError } from 'zod';
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { import {
IS_LABEL_SYNCED_WITH_NAME_LABEL, IS_LABEL_SYNCED_WITH_NAME_LABEL,
@ -28,7 +27,7 @@ import styled from '@emotion/styled';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState'; import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils'; import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
const objectEditFormSchema = z const objectEditFormSchema = z
@ -57,7 +56,9 @@ const StyledFormSection = styled(Section)`
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const setUpdatedObjectSlugState = useSetRecoilState(updatedObjectSlugState); const setUpdatedObjectNamePlural = useSetRecoilState(
updatedObjectNamePluralState,
);
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const { lastVisitedObjectMetadataItemId } = const { lastVisitedObjectMetadataItemId } =
@ -131,12 +132,8 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
const updatePayload = getUpdatePayload(formValues); const updatePayload = getUpdatePayload(formValues);
const objectNamePluralForRedirection = const objectNamePluralForRedirection =
updatePayload.namePlural ?? objectMetadataItem.namePlural; updatePayload.namePlural ?? objectMetadataItem.namePlural;
const objectSlug = getObjectSlug({
...updatePayload,
namePlural: objectNamePluralForRedirection,
});
setUpdatedObjectSlugState(objectSlug); setUpdatedObjectNamePlural(objectNamePluralForRedirection);
await updateOneObjectMetadataItem({ await updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id, idToUpdate: objectMetadataItem.id,
@ -154,7 +151,7 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
); );
} }
navigate(`${settingsObjectsPagePath}/${objectSlug}`); navigate(`${settingsObjectsPagePath}/${objectNamePluralForRedirection}`);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
enqueueSnackBar(error.issues[0].message, { enqueueSnackBar(error.issues[0].message, {

View File

@ -10,10 +10,10 @@ export enum SettingsPath {
Billing = 'billing', Billing = 'billing',
Objects = 'objects', Objects = 'objects',
ObjectOverview = 'objects/overview', ObjectOverview = 'objects/overview',
ObjectDetail = 'objects/:objectSlug', ObjectDetail = 'objects/:objectNamePlural',
ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select', ObjectNewFieldSelect = 'objects/:objectNamePlural/new-field/select',
ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure', ObjectNewFieldConfigure = 'objects/:objectNamePlural/new-field/configure',
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug', ObjectFieldEdit = 'objects/:objectNamePlural/:fieldName',
NewObject = 'objects/new', NewObject = 'objects/new',
NewServerlessFunction = 'functions/new', NewServerlessFunction = 'functions/new',
ServerlessFunctionDetail = 'functions/:serverlessFunctionId', ServerlessFunctionDetail = 'functions/:serverlessFunctionId',

View File

@ -3,7 +3,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState'; import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
@ -36,9 +35,9 @@ export const useGetAvailableFieldsForKanban = () => {
if (isDefined(objectMetadataItem?.namePlural)) { if (isDefined(objectMetadataItem?.namePlural)) {
navigate( navigate(
`/settings/objects/${getObjectSlug( `/settings/objects/${
objectMetadataItem, objectMetadataItem.namePlural
)}/new-field/configure?fieldType=${FieldMetadataType.Select}`, }/new-field/configure?fieldType=${FieldMetadataType.Select}`,
); );
} else { } else {
navigate(`/settings/objects`); navigate(`/settings/objects`);

View File

@ -5,7 +5,6 @@ import { H2Title, Section } from 'twenty-ui';
import { z } from 'zod'; import { z } from 'zod';
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { import {
@ -50,9 +49,7 @@ export const SettingsNewObject = () => {
navigate( navigate(
response response
? `${settingsObjectsPagePath}/${getObjectSlug( ? `${settingsObjectsPagePath}/${response.createOneObject.namePlural}`
response.createOneObject,
)}`
: settingsObjectsPagePath, : settingsObjectsPagePath,
); );

View File

@ -32,7 +32,7 @@ import {
} from 'twenty-ui'; } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs'; import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs';
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState'; import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
flex: 1; flex: 1;
@ -53,16 +53,16 @@ const StyledTitleContainer = styled.div`
export const SettingsObjectDetailPage = () => { export const SettingsObjectDetailPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectNamePlural = '' } = useParams();
const { findActiveObjectMetadataItemBySlug } = const { findActiveObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems(); useFilteredObjectMetadataItems();
const [updatedObjectSlug, setUpdatedObjectSlug] = useRecoilState( const [updatedObjectNamePlural, setUpdatedObjectNamePlural] = useRecoilState(
updatedObjectSlugState, updatedObjectNamePluralState,
); );
const objectMetadataItem = const objectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug) ?? findActiveObjectMetadataItemByNamePlural(objectNamePlural) ??
findActiveObjectMetadataItemBySlug(updatedObjectSlug); findActiveObjectMetadataItemByNamePlural(updatedObjectNamePlural);
const { activeTabId } = useTabList( const { activeTabId } = useTabList(
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID, SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
@ -74,14 +74,15 @@ export const SettingsObjectDetailPage = () => {
); );
useEffect(() => { useEffect(() => {
if (objectSlug === updatedObjectSlug) setUpdatedObjectSlug(''); if (objectNamePlural === updatedObjectNamePlural)
setUpdatedObjectNamePlural('');
if (!isDefined(objectMetadataItem)) navigate(AppPath.NotFound); if (!isDefined(objectMetadataItem)) navigate(AppPath.NotFound);
}, [ }, [
objectMetadataItem, objectMetadataItem,
navigate, navigate,
objectSlug, objectNamePlural,
updatedObjectSlug, updatedObjectNamePlural,
setUpdatedObjectSlug, setUpdatedObjectNamePlural,
]); ]);
if (!isDefined(objectMetadataItem)) return <></>; if (!isDefined(objectMetadataItem)) return <></>;

View File

@ -18,7 +18,6 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilte
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdateOneFieldMetadataItem'; import { useUpdateOneFieldMetadataItem } from '@/object-metadata/hooks/useUpdateOneFieldMetadataItem';
import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput'; import { formatFieldMetadataItemInput } from '@/object-metadata/utils/formatFieldMetadataItemInput';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
@ -48,16 +47,18 @@ export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { objectSlug = '', fieldSlug = '' } = useParams(); const { objectNamePlural = '', fieldName = '' } = useParams();
const { findObjectMetadataItemBySlug } = useFilteredObjectMetadataItems(); const { findObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems();
const objectMetadataItem = findObjectMetadataItemBySlug(objectSlug); const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const { deactivateMetadataField, activateMetadataField } = const { deactivateMetadataField, activateMetadataField } =
useFieldMetadataItem(); useFieldMetadataItem();
const fieldMetadataItem = objectMetadataItem?.fields.find( const fieldMetadataItem = objectMetadataItem?.fields.find(
(fieldMetadataItem) => getFieldSlug(fieldMetadataItem) === fieldSlug, (fieldMetadataItem) => fieldMetadataItem.name === fieldName,
); );
const getRelationMetadata = useGetRelationMetadata(); const getRelationMetadata = useGetRelationMetadata();
@ -126,7 +127,7 @@ export const SettingsObjectFieldEdit = () => {
Object.keys(otherDirtyFields), Object.keys(otherDirtyFields),
); );
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectNamePlural}`);
await updateOneFieldMetadataItem({ await updateOneFieldMetadataItem({
objectMetadataId: objectMetadataItem.id, objectMetadataId: objectMetadataItem.id,
@ -143,12 +144,12 @@ export const SettingsObjectFieldEdit = () => {
const handleDeactivate = async () => { const handleDeactivate = async () => {
await deactivateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); await deactivateMetadataField(fieldMetadataItem.id, objectMetadataItem.id);
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectNamePlural}`);
}; };
const handleActivate = async () => { const handleActivate = async () => {
await activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); await activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id);
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectNamePlural}`);
}; };
return ( return (
@ -168,7 +169,7 @@ export const SettingsObjectFieldEdit = () => {
}, },
{ {
children: objectMetadataItem.labelPlural, children: objectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`, href: `/settings/objects/${objectNamePlural}`,
}, },
{ {
children: fieldMetadataItem.label, children: fieldMetadataItem.label,
@ -178,7 +179,7 @@ export const SettingsObjectFieldEdit = () => {
<SaveAndCancelButtons <SaveAndCancelButtons
isSaveDisabled={!canSave} isSaveDisabled={!canSave}
isCancelDisabled={isSubmitting} isCancelDisabled={isSubmitting}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)} onCancel={() => navigate(`/settings/objects/${objectNamePlural}`)}
onSave={formConfig.handleSubmit(handleSave)} onSave={formConfig.handleSubmit(handleSave)}
/> />
} }

View File

@ -41,17 +41,17 @@ const DEFAULT_ICON_FOR_NEW_FIELD = 'IconUsers';
export const SettingsObjectNewFieldConfigure = () => { export const SettingsObjectNewFieldConfigure = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectNamePlural = '' } = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const fieldType = const fieldType =
(searchParams.get('fieldType') as SettingsFieldType) || (searchParams.get('fieldType') as SettingsFieldType) ||
FieldMetadataType.Text; FieldMetadataType.Text;
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { findActiveObjectMetadataItemBySlug } = const { findActiveObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems(); useFilteredObjectMetadataItems();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemByNamePlural(objectNamePlural);
const { createMetadataField } = useFieldMetadataItem(); const { createMetadataField } = useFieldMetadataItem();
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
@ -163,7 +163,7 @@ export const SettingsObjectNewFieldConfigure = () => {
}); });
} }
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectNamePlural}`);
// TODO: fix optimistic update logic // TODO: fix optimistic update logic
// Forcing a refetch for now but it's not ideal // Forcing a refetch for now but it's not ideal
@ -190,7 +190,7 @@ export const SettingsObjectNewFieldConfigure = () => {
{ children: 'Objects', href: '/settings/objects' }, { children: 'Objects', href: '/settings/objects' },
{ {
children: activeObjectMetadataItem.labelPlural, children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`, href: `/settings/objects/${objectNamePlural}`,
}, },
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> }, { children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
@ -201,7 +201,7 @@ export const SettingsObjectNewFieldConfigure = () => {
isCancelDisabled={isSubmitting} isCancelDisabled={isSubmitting}
onCancel={() => onCancel={() =>
navigate( navigate(
`/settings/objects/${objectSlug}/new-field/select?fieldType=${fieldType}`, `/settings/objects/${objectNamePlural}/new-field/select?fieldType=${fieldType}`,
) )
} }
onSave={formConfig.handleSubmit(handleSave)} onSave={formConfig.handleSubmit(handleSave)}

View File

@ -30,11 +30,11 @@ export type SettingsDataModelFieldTypeFormValues = z.infer<
export const SettingsObjectNewFieldSelect = () => { export const SettingsObjectNewFieldSelect = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectNamePlural = '' } = useParams();
const { findActiveObjectMetadataItemBySlug } = const { findActiveObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems(); useFilteredObjectMetadataItems();
const activeObjectMetadataItem = const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug); findActiveObjectMetadataItemByNamePlural(objectNamePlural);
const formMethods = useForm({ const formMethods = useForm({
resolver: zodResolver(settingsDataModelFieldTypeFormSchema), resolver: zodResolver(settingsDataModelFieldTypeFormSchema),
defaultValues: { defaultValues: {
@ -69,14 +69,14 @@ export const SettingsObjectNewFieldSelect = () => {
{ children: 'Objects', href: '/settings/objects' }, { children: 'Objects', href: '/settings/objects' },
{ {
children: activeObjectMetadataItem.labelPlural, children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`, href: `/settings/objects/${objectNamePlural}`,
}, },
{ children: <SettingsDataModelNewFieldBreadcrumbDropDown /> }, { children: <SettingsDataModelNewFieldBreadcrumbDropDown /> },
]} ]}
> >
<SettingsPageContainer> <SettingsPageContainer>
<SettingsObjectNewFieldSelector <SettingsObjectNewFieldSelector
objectSlug={objectSlug} objectNamePlural={objectNamePlural}
excludedFieldTypes={excludedFieldTypes} excludedFieldTypes={excludedFieldTypes}
/> />
</SettingsPageContainer> </SettingsPageContainer>

View File

@ -1,7 +1,6 @@
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount'; import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { import {
@ -197,9 +196,9 @@ export const SettingsObjects = () => {
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
} }
link={`/settings/objects/${getObjectSlug( link={`/settings/objects/${
objectSettingsItem.objectMetadataItem, objectSettingsItem.objectMetadataItem.namePlural
)}`} }`}
/> />
), ),
)} )}

View File

@ -16,8 +16,8 @@ const meta: Meta<PageDecoratorArgs> = {
component: SettingsObjectDetailPage, component: SettingsObjectDetailPage,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { args: {
routePath: '/settings/objects/:objectSlug', routePath: '/settings/objects/:objectNamePlural',
routeParams: { ':objectSlug': 'companies' }, routeParams: { ':objectNamePlural': 'companies' },
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
@ -36,7 +36,7 @@ export const StandardObject: Story = {
export const CustomObject: Story = { export const CustomObject: Story = {
args: { args: {
routeParams: { ':objectSlug': 'my-customs' }, routeParams: { ':objectNamePlural': 'myCustoms' },
}, },
}; };

View File

@ -13,8 +13,8 @@ const meta: Meta<PageDecoratorArgs> = {
component: SettingsObjectFieldEdit, component: SettingsObjectFieldEdit,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { args: {
routePath: '/settings/objects/:objectSlug/:fieldSlug', routePath: '/settings/objects/:objectNamePlural/:fieldName',
routeParams: { ':objectSlug': 'companies', ':fieldSlug': 'name' }, routeParams: { ':objectNamePlural': 'companies', ':fieldName': 'name' },
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
@ -30,8 +30,8 @@ export const StandardField: Story = {};
export const CustomField: Story = { export const CustomField: Story = {
args: { args: {
routeParams: { routeParams: {
':objectSlug': 'companies', ':objectNamePlural': 'companies',
':fieldSlug': 'employees', ':fieldName': 'employees',
}, },
}, },
}; };

View File

@ -14,8 +14,8 @@ const meta: Meta<PageDecoratorArgs> = {
component: SettingsObjectNewFieldConfigure, component: SettingsObjectNewFieldConfigure,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { args: {
routePath: '/settings/objects/:objectSlug/new-field/configure', routePath: '/settings/objects/:objectNamePlural/new-field/configure',
routeParams: { ':objectSlug': 'companies' }, routeParams: { ':objectNamePlural': 'companies' },
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -14,8 +14,8 @@ const meta: Meta<PageDecoratorArgs> = {
component: SettingsObjectNewFieldSelect, component: SettingsObjectNewFieldSelect,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { args: {
routePath: '/settings/objects/:objectSlug/new-field/select', routePath: '/settings/objects/:objectNamePlural/new-field/select',
routeParams: { ':objectSlug': 'companies' }, routeParams: { ':objectNamePlural': 'companies' },
}, },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -0,0 +1,6 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const updatedObjectNamePluralState = createState<string>({
key: 'updatedObjectNamePluralState',
defaultValue: '',
});

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const updatedObjectSlugState = createState<string>({
key: 'updatedObjectSlugState',
defaultValue: '',
});