From 7bab65b569fccc17616ebb4f0c164dd8be23067b Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:50:53 +0100 Subject: [PATCH] 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\ ### Refs #TWNTY-5491 --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa Co-authored-by: Weiko --- .../calendar/components/Calendar.tsx | 22 +- .../modules/app/components/SettingsRoutes.tsx | 7 - .../hooks/useUpdateOneObjectMetadataItem.ts | 3 +- .../components/SettingsPageContainer.tsx | 4 +- .../components/tabs/ObjectFields.tsx | 44 ++++ .../components/tabs/ObjectIndexes.tsx | 20 ++ .../components/tabs/ObjectSettings.tsx} | 174 +++++++------- .../SettingsDataModelObjectAboutForm.tsx | 219 ++++++++++-------- .../SyncObjectLabelAndNameToggle.tsx | 14 +- .../src/modules/types/SettingsPath.ts | 1 - .../modules/ui/input/components/TextArea.tsx | 3 + .../ui/input/components/TextInputV2.tsx | 2 +- .../components/SubMenuTopBarContainer.tsx | 2 +- .../modules/ui/layout/tab/components/Tab.tsx | 5 +- .../ui/layout/tab/components/TabList.tsx | 2 +- .../data-model/SettingsObjectDetailPage.tsx | 179 +++++++++++++- .../SettingsObjectDetailPageContent.tsx | 107 --------- .../SettingsObjectDetail.stories.tsx | 16 +- .../SettingsObjectEdit.stories.tsx | 39 ---- .../states/updatedObjectSlugState.ts | 6 + .../object-metadata.service.ts | 2 + .../object-metadata-migration.service.ts | 9 +- .../display/icon/components/TablerIcons.ts | 2 + .../display/typography/components/H2Title.tsx | 2 +- .../display/typography/components/H3Title.tsx | 3 +- 25 files changed, 496 insertions(+), 391 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectIndexes.tsx rename packages/twenty-front/src/{pages/settings/data-model/SettingsObjectEdit.tsx => modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx} (59%) delete mode 100644 packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx delete mode 100644 packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectEdit.stories.tsx create mode 100644 packages/twenty-front/src/pages/settings/data-model/states/updatedObjectSlugState.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 29beb1225..0be4c731a 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -38,6 +38,10 @@ const StyledYear = styled.span` color: ${({ theme }) => theme.font.color.light}; `; +const StyledTitleContainer = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + export const Calendar = ({ targetableObject, }: { @@ -131,14 +135,16 @@ export const Calendar = ({ return (
- - {monthLabel} - {isLastMonthOfYear && {year}} - - } - /> + + + {monthLabel} + {isLastMonthOfYear && {year}} + + } + /> +
); diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 4b48c3c1b..b758acdc1 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -143,12 +143,6 @@ const SettingsDevelopers = lazy(() => })), ); -const SettingsObjectEdit = lazy(() => - import('~/pages/settings/data-model/SettingsObjectEdit').then((module) => ({ - default: module.SettingsObjectEdit, - })), -); - const SettingsIntegrations = lazy(() => import('~/pages/settings/integrations/SettingsIntegrations').then( (module) => ({ @@ -292,7 +286,6 @@ export const SettingsRoutes = ({ path={SettingsPath.ObjectDetail} element={} /> - } /> } /> } /> {isCRMMigrationEnabled && ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index aa02b1e73..a70ff41c2 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -16,7 +16,7 @@ import { useApolloMetadataClient } from './useApolloMetadataClient'; export const useUpdateOneObjectMetadataItem = () => { const apolloClientMetadata = useApolloMetadataClient(); - const [mutate] = useMutation< + const [mutate, { loading }] = useMutation< UpdateOneObjectMetadataItemMutation, UpdateOneObjectMetadataItemMutationVariables >(UPDATE_ONE_OBJECT_METADATA_ITEM, { @@ -42,5 +42,6 @@ export const useUpdateOneObjectMetadataItem = () => { return { updateOneObjectMetadataItem, + loading, }; }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx index 879e95b82..1c4f7de6b 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx @@ -5,7 +5,9 @@ import styled from '@emotion/styled'; import { ReactNode } from 'react'; import { isDefined } from '~/utils/isDefined'; -const StyledSettingsPageContainer = styled.div<{ width?: number }>` +const StyledSettingsPageContainer = styled.div<{ + width?: number; +}>` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(8)}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx new file mode 100644 index 000000000..4721ad21e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectFields.tsx @@ -0,0 +1,44 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; + +import styled from '@emotion/styled'; +import { Button, H2Title, IconPlus, Section, UndecoratedLink } from 'twenty-ui'; + +const StyledDiv = styled.div` + display: flex; + justify-content: flex-end; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +type ObjectFieldsProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => { + const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote; + + return ( +
+ + + {shouldDisplayAddFieldButton && ( + + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectIndexes.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectIndexes.tsx new file mode 100644 index 000000000..50c9a4067 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectIndexes.tsx @@ -0,0 +1,20 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +import { H2Title, Section } from 'twenty-ui'; +import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable'; + +type ObjectIndexesProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const ObjectIndexes = ({ objectMetadataItem }: ObjectIndexesProps) => { + return ( +
+ + +
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx similarity index 59% rename from packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx rename to packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 4747d88f1..105443082 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -1,20 +1,16 @@ /* eslint-disable react/jsx-props-no-spreading */ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { useNavigate } from 'react-router-dom'; import { Button, H2Title, IconArchive, Section } from 'twenty-ui'; -import { z } from 'zod'; +import { z, ZodError } 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, @@ -24,13 +20,15 @@ import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-mo 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 styled from '@emotion/styled'; +import isEmpty from 'lodash.isempty'; +import pick from 'lodash.pick'; import { useSetRecoilState } from 'recoil'; +import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState'; import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils'; const objectEditFormSchema = z @@ -42,21 +40,30 @@ type SettingsDataModelObjectEditFormValues = z.infer< typeof objectEditFormSchema >; -export const SettingsObjectEdit = () => { +type ObjectSettingsProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +const StyledContentContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledFormSection = styled(Section)` + padding-left: 0 !important; +`; + +export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { const navigate = useNavigate(); const { enqueueSnackBar } = useSnackBar(); + const setUpdatedObjectSlugState = useSetRecoilState(updatedObjectSlugState); - 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({ @@ -68,15 +75,6 @@ export const SettingsObjectEdit = () => { 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, ) => { @@ -88,19 +86,19 @@ export const SettingsObjectEdit = () => { IS_LABEL_SYNCED_WITH_NAME_LABEL, ) ? (formValues.isLabelSyncedWithName as boolean) - : activeObjectMetadataItem.isLabelSyncedWithName; + : objectMetadataItem.isLabelSyncedWithName; if (shouldComputeNamesFromLabels) { values = { ...values, - ...(values.labelSingular + ...(values.labelSingular && dirtyFieldKeys.includes('labelSingular') ? { nameSingular: computeMetadataNameFromLabelOrThrow( formValues.labelSingular, ), } : {}), - ...(values.labelPlural + ...(values.labelPlural && dirtyFieldKeys.includes('labelPlural') ? { namePlural: computeMetadataNameFromLabelOrThrow( formValues.labelPlural, @@ -113,8 +111,14 @@ export const SettingsObjectEdit = () => { return settingsUpdateObjectInputSchema.parse( pick(values, [ ...dirtyFieldKeys, - ...(values.namePlural ? ['namePlural'] : []), - ...(values.nameSingular ? ['nameSingular'] : []), + ...(shouldComputeNamesFromLabels && + dirtyFieldKeys.includes('labelPlural') + ? ['namePlural'] + : []), + ...(shouldComputeNamesFromLabels && + dirtyFieldKeys.includes('labelSingular') + ? ['nameSingular'] + : []), ]), ); }; @@ -122,41 +126,53 @@ export const SettingsObjectEdit = () => { const handleSave = async ( formValues: SettingsDataModelObjectEditFormValues, ) => { + if (isEmpty(formConfig.formState.dirtyFields) === true) { + return; + } try { const updatePayload = getUpdatePayload(formValues); + const objectNamePluralForRedirection = + updatePayload.namePlural ?? objectMetadataItem.namePlural; + const objectSlug = getObjectSlug({ + ...updatePayload, + namePlural: objectNamePluralForRedirection, + }); + + setUpdatedObjectSlugState(objectSlug); + await updateOneObjectMetadataItem({ - idToUpdate: activeObjectMetadataItem.id, + idToUpdate: objectMetadataItem.id, updatePayload, }); - const objectNamePluralForRedirection = - updatePayload.namePlural ?? activeObjectMetadataItem.namePlural; + formConfig.reset(undefined, { keepValues: true }); - if (lastVisitedObjectMetadataItemId === activeObjectMetadataItem.id) { + if (lastVisitedObjectMetadataItemId === objectMetadataItem.id) { const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId( - activeObjectMetadataItem.id, + objectMetadataItem.id, ); setNavigationMemorizedUrl( `/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`, ); } - navigate( - `${settingsObjectsPagePath}/${getObjectSlug({ - ...updatePayload, - namePlural: objectNamePluralForRedirection, - })}`, - ); + navigate(`${settingsObjectsPagePath}/${objectSlug}`); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, - }); + if (error instanceof ZodError) { + enqueueSnackBar(error.issues[0].message, { + variant: SnackBarVariant.Error, + }); + } else { + enqueueSnackBar((error as Error).message, { + variant: SnackBarVariant.Error, + }); + } } }; const handleDisable = async () => { await updateOneObjectMetadataItem({ - idToUpdate: activeObjectMetadataItem.id, + idToUpdate: objectMetadataItem.id, updatePayload: { isActive: false }, }); navigate(settingsObjectsPagePath); @@ -165,57 +181,33 @@ export const SettingsObjectEdit = () => { return ( - - navigate(`${settingsObjectsPagePath}/${objectSlug}`) - } - onSave={formConfig.handleSubmit(handleSave)} - /> - ) - } - > - + + + + { + formConfig.handleSubmit(handleSave)(); + }} + /> + +
- -
-
-
+
+
-
-
+ +
); diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index 9e59ff4a4..1e7752d9a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -49,6 +49,7 @@ type SettingsDataModelObjectAboutFormProps = { disabled?: boolean; disableNameEdit?: boolean; objectMetadataItem?: ObjectMetadataItem; + onBlur?: () => void; }; const StyledInputsContainer = styled.div` @@ -68,12 +69,16 @@ const StyledAdvancedSettingsSectionInputWrapper = styled.div` flex-direction: column; gap: ${({ theme }) => theme.spacing(4)}; width: 100%; + flex: 1; +`; + +const StyledAdvancedSettingsOuterContainer = styled.div` + padding-top: ${({ theme }) => theme.spacing(4)}; `; const StyledAdvancedSettingsContainer = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(4)}; position: relative; width: 100%; `; @@ -81,7 +86,7 @@ const StyledAdvancedSettingsContainer = styled.div` const StyledIconToolContainer = styled.div` border-right: 1px solid ${MAIN_COLORS.yellow}; display: flex; - left: ${({ theme }) => theme.spacing(-5)}; + left: ${({ theme }) => theme.spacing(-6)}; position: absolute; height: 100%; `; @@ -105,6 +110,7 @@ export const SettingsDataModelObjectAboutForm = ({ disabled, disableNameEdit, objectMetadataItem, + onBlur, }: SettingsDataModelObjectAboutFormProps) => { const { control, watch, setValue } = useFormContext(); @@ -117,6 +123,9 @@ export const SettingsDataModelObjectAboutForm = ({ const isLabelSyncedWithName = watch(IS_LABEL_SYNCED_WITH_NAME_LABEL); const labelSingular = watch('labelSingular'); const labelPlural = watch('labelPlural'); + watch('nameSingular'); + watch('namePlural'); + watch('description'); const apiNameTooltipText = isLabelSyncedWithName ? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name' : 'Input must be in camel case and cannot start with a number'; @@ -138,14 +147,14 @@ export const SettingsDataModelObjectAboutForm = ({ setValue( 'nameSingular', computeMetadataNameFromLabelOrThrow(labelSingular), - { shouldDirty: false }, + { shouldDirty: true }, ); }; const fillNamePluralFromLabelPlural = (labelPlural: string) => { isDefined(labelPlural) && setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), { - shouldDirty: false, + shouldDirty: true, }); }; @@ -184,6 +193,7 @@ export const SettingsDataModelObjectAboutForm = ({ fillNameSingularFromLabelSingular(value); } }} + onBlur={onBlur} disabled={disabled || disableNameEdit} fullWidth maxLength={OBJECT_NAME_MAXIMUM_LENGTH} @@ -236,105 +246,110 @@ export const SettingsDataModelObjectAboutForm = ({ exit="exit" variants={motionAnimationVariants} > - - - - - - {[ - { - label: 'API Name (Singular)', - fieldName: 'nameSingular' as const, - placeholder: 'listing', - defaultValue: objectMetadataItem?.nameSingular, - disabled: - disabled || disableNameEdit || isLabelSyncedWithName, - tooltip: apiNameTooltipText, - }, - { - label: 'API Name (Plural)', - fieldName: 'namePlural' as const, - placeholder: 'listings', - defaultValue: objectMetadataItem?.namePlural, - disabled: - disabled || disableNameEdit || isLabelSyncedWithName, - tooltip: apiNameTooltipText, - }, - ].map( - ({ - defaultValue, - fieldName, - label, - placeholder, - disabled, - tooltip, - }) => ( - - ( - <> - - tooltip && ( - <> - - - - - ) - } - /> - - )} - /> - - ), - )} - ( - { - onChange(value); - if (value === true) { - fillNamePluralFromLabelPlural(labelPlural); - fillNameSingularFromLabelSingular(labelSingular); - } - }} - /> + + + + + + + {[ + { + label: 'API Name (Singular)', + fieldName: 'nameSingular' as const, + placeholder: 'listing', + defaultValue: objectMetadataItem?.nameSingular, + disabled: + disabled || disableNameEdit || isLabelSyncedWithName, + tooltip: apiNameTooltipText, + }, + { + label: 'API Name (Plural)', + fieldName: 'namePlural' as const, + placeholder: 'listings', + defaultValue: objectMetadataItem?.namePlural, + disabled: + disabled || disableNameEdit || isLabelSyncedWithName, + tooltip: apiNameTooltipText, + }, + ].map( + ({ + defaultValue, + fieldName, + label, + placeholder, + disabled, + tooltip, + }) => ( + + ( + <> + + tooltip && ( + <> + + + + ) + } + /> + + )} + /> + + ), )} - /> - - + ( + { + onChange(value); + if (value === true) { + fillNamePluralFromLabelPlural(labelPlural); + fillNameSingularFromLabelSingular(labelSingular); + } + onBlur?.(); + }} + /> + )} + /> + + + )} diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle.tsx index d220b33f0..c3252c82c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle.tsx @@ -5,10 +5,11 @@ import { IconRefresh, MAIN_COLORS, Toggle } from 'twenty-ui'; const StyledToggleContainer = styled.div` align-items: center; border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.sm}; + border-radius: ${({ theme }) => theme.border.radius.md}; display: flex; justify-content: space-between; padding: ${({ theme }) => theme.spacing(4)}; + background: ${({ theme }) => theme.background.secondary}; `; const StyledIconRefreshContainer = styled.div` @@ -40,17 +41,19 @@ const StyledDescription = styled.h3` font-size: ${({ theme }) => theme.font.size.md}; font-weight: ${({ theme }) => theme.font.weight.regular}; margin: 0; - margin-top: ${({ theme }) => theme.spacing(2)}; + margin-top: ${({ theme }) => theme.spacing(1)}; `; type SyncObjectLabelAndNameToggleProps = { value: boolean; onChange: (value: boolean) => void; + disabled?: boolean; }; export const SyncObjectLabelAndNameToggle = ({ value, onChange, + disabled, }: SyncObjectLabelAndNameToggleProps) => { const theme = useTheme(); return ( @@ -66,7 +69,12 @@ export const SyncObjectLabelAndNameToggle = ({ - + ); }; diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index 2d7b9ebdc..dc8f3c1cb 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -11,7 +11,6 @@ export enum SettingsPath { Objects = 'objects', ObjectOverview = 'objects/overview', ObjectDetail = 'objects/:objectSlug', - ObjectEdit = 'objects/:objectSlug/edit', ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select', ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure', ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug', diff --git a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx index b6bb9d545..c8feb6c42 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx @@ -17,6 +17,7 @@ export type TextAreaProps = { placeholder?: string; value?: string; className?: string; + onBlur?: () => void; }; const StyledContainer = styled.div` @@ -70,6 +71,7 @@ export const TextArea = ({ value = '', className, onChange, + onBlur, }: TextAreaProps) => { const computedMinRows = Math.min(minRows, MAX_ROWS); @@ -86,6 +88,7 @@ export const TextArea = ({ const handleBlur: FocusEventHandler = () => { goBackToPreviousHotkeyScope(); + onBlur?.(); }; return ( diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx index db306bd1d..557e67ced 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx @@ -99,7 +99,7 @@ const StyledTrailingIconContainer = styled.div< align-items: center; display: flex; justify-content: center; - padding-right: ${({ theme }) => theme.spacing(1)}; + padding-right: ${({ theme }) => theme.spacing(2)}; position: absolute; top: 0; bottom: 0; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx index 8772bced5..91a1ad09e 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx @@ -10,7 +10,7 @@ import { PageHeader } from './PageHeader'; type SubMenuTopBarContainerProps = { children: JSX.Element | JSX.Element[]; - title?: string; + title?: string | JSX.Element; actionButton?: ReactNode; className?: string; links: BreadcrumbProps['links']; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index e723f0ae8..93a96eb46 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -1,5 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { ReactElement } from 'react'; import { IconComponent, Pill } from 'twenty-ui'; type TabProps = { @@ -10,7 +11,7 @@ type TabProps = { className?: string; onClick?: () => void; disabled?: boolean; - pill?: string; + pill?: string | ReactElement; }; const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>` @@ -73,7 +74,7 @@ export const Tab = ({ {Icon && } {title} - {pill && } + {pill && typeof pill === 'string' ? : pill} ); diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index eac1de426..d0936c28f 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -15,7 +15,7 @@ export type SingleTabProps = { id: string; hide?: boolean; disabled?: boolean; - pill?: string; + pill?: string | React.ReactElement; }; type TabListProps = { diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx index 899c36909..52d22747a 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx @@ -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: , + }, + ]; + + const renderActiveTabContent = () => { + switch (activeTabId) { + case FIELDS_TAB_ID: + return ; + case SETTINGS_TAB_ID: + return ; + case INDEXES_TAB_ID: + return ; + default: + return <>; + } + }; + + const objectTypeLabel = getObjectTypeLabel(objectMetadataItem); return ( - + + + + + } + links={[ + { + children: 'Workspace', + href: getSettingsPagePath(SettingsPath.Workspace), + }, + { children: 'Objects', href: '/settings/objects' }, + { + children: objectMetadataItem.labelPlural, + }, + ]} + actionButton={ + activeTabId === FIELDS_TAB_ID && ( + +