Implement object fields and settings new layout (#7979)
### Description - This PR has as the base branch the TWNTY-5491 branch, but we also had to include updates from the main branch, and currently, there are conflicts in the TWNTY-5491, that cause errors on typescript in this PR, so, we can update once the conflicts are resolved on the base branch, but the functionality can be reviewed anyway - We Implemented a new layout of object details settings and new, the data is auto-saved in `Settings `tab of object detail - There is no indication to the user that data are saved automatically in the design, currently we are disabling the form ### Demo\ <https://www.loom.com/share/4198c0aa54b5450780a570ceee574838?sid=b4ef0a42-2d41-435f-9f5f-1b16816939f7> ### Refs #TWNTY-5491 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
committed by
GitHub
parent
3be30651b7
commit
7bab65b569
@ -2,9 +2,71 @@ import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { ObjectFields } from '@/settings/data-model/object-details/components/tabs/ObjectFields';
|
||||
import { ObjectIndexes } from '@/settings/data-model/object-details/components/tabs/ObjectIndexes';
|
||||
import { ObjectSettings } from '@/settings/data-model/object-details/components/tabs/ObjectSettings';
|
||||
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
|
||||
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { SettingsObjectDetailPageContent } from '~/pages/settings/data-model/SettingsObjectDetailPageContent';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import {
|
||||
Button,
|
||||
H3Title,
|
||||
IconCodeCircle,
|
||||
IconListDetails,
|
||||
IconPlus,
|
||||
IconSettings,
|
||||
IconTool,
|
||||
isDefined,
|
||||
MAIN_COLORS,
|
||||
UndecoratedLink,
|
||||
} from 'twenty-ui';
|
||||
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(10)};
|
||||
.tab-list {
|
||||
padding-left: 0px;
|
||||
}
|
||||
.tab-list > div {
|
||||
padding: ${({ theme }) => theme.spacing(3) + ' 0'};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
`;
|
||||
|
||||
const StyledObjectTypeTag = styled(SettingsDataModelObjectTypeTag)`
|
||||
box-sizing: border-box;
|
||||
height: ${({ theme }) => theme.spacing(5)};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'object-details-tab-list';
|
||||
const FIELDS_TAB_ID = 'fields';
|
||||
const SETTINGS_TAB_ID = 'settings';
|
||||
const INDEXES_TAB_ID = 'indexes';
|
||||
|
||||
export const SettingsObjectDetailPage = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -13,18 +75,115 @@ export const SettingsObjectDetailPage = () => {
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
const [updatedObjectSlug, setUpdatedObjectSlug] = useRecoilState(
|
||||
updatedObjectSlugState,
|
||||
);
|
||||
const objectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug) ??
|
||||
findActiveObjectMetadataItemBySlug(updatedObjectSlug);
|
||||
|
||||
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
||||
'IS_UNIQUE_INDEXES_ENABLED',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
if (objectSlug === updatedObjectSlug) setUpdatedObjectSlug('');
|
||||
if (!isDefined(objectMetadataItem)) navigate(AppPath.NotFound);
|
||||
}, [
|
||||
objectMetadataItem,
|
||||
navigate,
|
||||
objectSlug,
|
||||
updatedObjectSlug,
|
||||
setUpdatedObjectSlug,
|
||||
]);
|
||||
|
||||
if (!isDefined(activeObjectMetadataItem)) return <></>;
|
||||
if (!isDefined(objectMetadataItem)) return <></>;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: FIELDS_TAB_ID,
|
||||
title: 'Fields',
|
||||
Icon: IconListDetails,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_TAB_ID,
|
||||
title: 'Settings',
|
||||
Icon: IconSettings,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
id: INDEXES_TAB_ID,
|
||||
title: 'Indexes',
|
||||
Icon: IconCodeCircle,
|
||||
hide: !isAdvancedModeEnabled || !isUniqueIndexesEnabled,
|
||||
pill: <IconTool size={12} color={MAIN_COLORS.yellow} />,
|
||||
},
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case FIELDS_TAB_ID:
|
||||
return <ObjectFields objectMetadataItem={objectMetadataItem} />;
|
||||
case SETTINGS_TAB_ID:
|
||||
return <ObjectSettings objectMetadataItem={objectMetadataItem} />;
|
||||
case INDEXES_TAB_ID:
|
||||
return <ObjectIndexes objectMetadataItem={objectMetadataItem} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
||||
|
||||
return (
|
||||
<SettingsObjectDetailPageContent
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<H3Title title={objectMetadataItem.labelPlural} />
|
||||
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
||||
</StyledTitleContainer>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'Objects', href: '/settings/objects' },
|
||||
{
|
||||
children: objectMetadataItem.labelPlural,
|
||||
},
|
||||
]}
|
||||
actionButton={
|
||||
activeTabId === FIELDS_TAB_ID && (
|
||||
<UndecoratedLink to={'./new-field/select'}>
|
||||
<Button
|
||||
title="New Field"
|
||||
variant="primary"
|
||||
size="small"
|
||||
accent="blue"
|
||||
Icon={IconPlus}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabListId={TAB_LIST_COMPONENT_ID}
|
||||
tabs={tabs}
|
||||
className="tab-list"
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
<StyledContentContainer>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Button, H2Title, IconPlus, Section, UndecoratedLink } from 'twenty-ui';
|
||||
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export type SettingsObjectDetailPageContentProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsObjectDetailPageContent = ({
|
||||
objectMetadataItem,
|
||||
}: SettingsObjectDetailPageContentProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const handleDisableObject = async () => {
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: objectMetadataItem.id,
|
||||
updatePayload: { isActive: false },
|
||||
});
|
||||
navigate(getSettingsPagePath(SettingsPath.Objects));
|
||||
};
|
||||
|
||||
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
||||
|
||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||
|
||||
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
||||
'IS_UNIQUE_INDEXES_ENABLED',
|
||||
);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={objectMetadataItem.labelPlural}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: 'Objects', href: '/settings/objects' },
|
||||
{ children: objectMetadataItem.labelPlural },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="About" description="Manage your object" />
|
||||
<SettingsObjectSummaryCard
|
||||
iconKey={objectMetadataItem.icon ?? undefined}
|
||||
name={objectMetadataItem.labelPlural || ''}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onDeactivate={handleDisableObject}
|
||||
onEdit={() => navigate('./edit')}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Fields"
|
||||
description={`Customise the fields available in the ${objectMetadataItem.labelSingular} views and their display order in the ${objectMetadataItem.labelSingular} detail view and menus.`}
|
||||
/>
|
||||
<SettingsObjectFieldTable
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
mode="view"
|
||||
/>
|
||||
{shouldDisplayAddFieldButton && (
|
||||
<StyledDiv>
|
||||
<UndecoratedLink to={'./new-field/select'}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add Field"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledDiv>
|
||||
)}
|
||||
</Section>
|
||||
{isAdvancedModeEnabled && isUniqueIndexesEnabled && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Indexes"
|
||||
description={`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
||||
/>
|
||||
<SettingsObjectIndexTable objectMetadataItem={objectMetadataItem} />
|
||||
</Section>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@ -1,233 +0,0 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import pick from 'lodash.pick';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import {
|
||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||
SettingsDataModelObjectAboutForm,
|
||||
settingsDataModelObjectAboutFormSchema,
|
||||
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||
import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
|
||||
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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||
|
||||
const objectEditFormSchema = z
|
||||
.object({})
|
||||
.merge(settingsDataModelObjectAboutFormSchema)
|
||||
.merge(settingsDataModelObjectIdentifiersFormSchema);
|
||||
|
||||
type SettingsDataModelObjectEditFormValues = z.infer<
|
||||
typeof objectEditFormSchema
|
||||
>;
|
||||
|
||||
export const SettingsObjectEdit = () => {
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
const { lastVisitedObjectMetadataItemId } =
|
||||
useLastVisitedObjectMetadataItem();
|
||||
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
|
||||
const settingsObjectsPagePath = getSettingsPagePath(SettingsPath.Objects);
|
||||
|
||||
const formConfig = useForm<SettingsDataModelObjectEditFormValues>({
|
||||
mode: 'onTouched',
|
||||
resolver: zodResolver(objectEditFormSchema),
|
||||
});
|
||||
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
||||
}, [activeObjectMetadataItem, navigate]);
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
||||
const canSave = isDirty && isValid && !isSubmitting;
|
||||
|
||||
const getUpdatePayload = (
|
||||
formValues: SettingsDataModelObjectEditFormValues,
|
||||
) => {
|
||||
let values = formValues;
|
||||
const dirtyFieldKeys = Object.keys(
|
||||
formConfig.formState.dirtyFields,
|
||||
) as (keyof SettingsDataModelObjectEditFormValues)[];
|
||||
const shouldComputeNamesFromLabels: boolean = dirtyFieldKeys.includes(
|
||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||
)
|
||||
? (formValues.isLabelSyncedWithName as boolean)
|
||||
: activeObjectMetadataItem.isLabelSyncedWithName;
|
||||
|
||||
if (shouldComputeNamesFromLabels) {
|
||||
values = {
|
||||
...values,
|
||||
...(values.labelSingular
|
||||
? {
|
||||
nameSingular: computeMetadataNameFromLabelOrThrow(
|
||||
formValues.labelSingular,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(values.labelPlural
|
||||
? {
|
||||
namePlural: computeMetadataNameFromLabelOrThrow(
|
||||
formValues.labelPlural,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
return settingsUpdateObjectInputSchema.parse(
|
||||
pick(values, [
|
||||
...dirtyFieldKeys,
|
||||
...(values.namePlural ? ['namePlural'] : []),
|
||||
...(values.nameSingular ? ['nameSingular'] : []),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = async (
|
||||
formValues: SettingsDataModelObjectEditFormValues,
|
||||
) => {
|
||||
try {
|
||||
const updatePayload = getUpdatePayload(formValues);
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: activeObjectMetadataItem.id,
|
||||
updatePayload,
|
||||
});
|
||||
|
||||
const objectNamePluralForRedirection =
|
||||
updatePayload.namePlural ?? activeObjectMetadataItem.namePlural;
|
||||
|
||||
if (lastVisitedObjectMetadataItemId === activeObjectMetadataItem.id) {
|
||||
const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId(
|
||||
activeObjectMetadataItem.id,
|
||||
);
|
||||
setNavigationMemorizedUrl(
|
||||
`/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`,
|
||||
);
|
||||
}
|
||||
|
||||
navigate(
|
||||
`${settingsObjectsPagePath}/${getObjectSlug({
|
||||
...updatePayload,
|
||||
namePlural: objectNamePluralForRedirection,
|
||||
})}`,
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = async () => {
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: activeObjectMetadataItem.id,
|
||||
updatePayload: { isActive: false },
|
||||
});
|
||||
navigate(settingsObjectsPagePath);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<FormProvider {...formConfig}>
|
||||
<SubMenuTopBarContainer
|
||||
title="Edit"
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Objects',
|
||||
href: settingsObjectsPagePath,
|
||||
},
|
||||
{
|
||||
children: activeObjectMetadataItem.labelPlural,
|
||||
href: `${settingsObjectsPagePath}/${objectSlug}`,
|
||||
},
|
||||
{ children: 'Edit Object' },
|
||||
]}
|
||||
actionButton={
|
||||
activeObjectMetadataItem.isCustom && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
isCancelDisabled={isSubmitting}
|
||||
onCancel={() =>
|
||||
navigate(`${settingsObjectsPagePath}/${objectSlug}`)
|
||||
}
|
||||
onSave={formConfig.handleSubmit(handleSave)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="About"
|
||||
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
||||
/>
|
||||
<SettingsDataModelObjectAboutForm
|
||||
disabled={!activeObjectMetadataItem.isCustom}
|
||||
disableNameEdit={!activeObjectMetadataItem.isCustom}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Settings"
|
||||
description="Choose the fields that will identify your records"
|
||||
/>
|
||||
<SettingsDataModelObjectSettingsFormCard
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Deactivate object" />
|
||||
<Button
|
||||
Icon={IconArchive}
|
||||
title="Deactivate"
|
||||
size="small"
|
||||
onClick={handleDisable}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</FormProvider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/test';
|
||||
|
||||
@ -39,20 +40,15 @@ export const CustomObject: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectDropdownMenu: Story = {
|
||||
export const ObjectTabs: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const objectSummaryVerticalDotsIconButton = await canvas.findByRole(
|
||||
'button',
|
||||
{
|
||||
name: 'Object Options',
|
||||
},
|
||||
);
|
||||
|
||||
await userEvent.click(objectSummaryVerticalDotsIconButton);
|
||||
const fieldsTab = await canvas.findByTestId('tab-fields');
|
||||
const settingsTab = await canvas.findByTestId('tab-settings');
|
||||
|
||||
await canvas.findByText('Edit');
|
||||
await canvas.findByText('Deactivate');
|
||||
await expect(fieldsTab).toBeVisible();
|
||||
await expect(settingsTab).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
import { SettingsObjectEdit } from '../SettingsObjectEdit';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/DataModel/SettingsObjectEdit',
|
||||
component: SettingsObjectEdit,
|
||||
decorators: [PageDecorator],
|
||||
args: {
|
||||
routePath: '/settings/objects/:objectSlug/edit',
|
||||
routeParams: { ':objectSlug': 'companies' },
|
||||
},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SettingsObjectEdit>;
|
||||
|
||||
export const StandardObject: Story = {
|
||||
play: async () => {
|
||||
await sleep(100);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomObject: Story = {
|
||||
args: {
|
||||
routeParams: { ':objectSlug': 'my-custom-objects' },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const updatedObjectSlugState = createState<string>({
|
||||
key: 'updatedObjectSlugState',
|
||||
defaultValue: '',
|
||||
});
|
||||
Reference in New Issue
Block a user