diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts index 1e2a63af9c..622eaeda49 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts @@ -1,8 +1,8 @@ import { errors } from '@/settings/data-model/fields/forms/utils/errorMessages'; import { z } from 'zod'; -import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabelOrThrow'; import { METADATA_LABEL_VALID_PATTERN } from '~/pages/settings/data-model/constants/MetadataLabelValidPattern'; +import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel'; export const metadataLabelSchema = (existingLabels?: string[]) => { return z .string() @@ -11,12 +11,9 @@ export const metadataLabelSchema = (existingLabels?: string[]) => { .regex(METADATA_LABEL_VALID_PATTERN, errors.LabelNotFormattable) .refine( (label) => { - try { - computeMetadataNameFromLabelOrThrow(label); - return true; - } catch { - return false; - } + const computedName = computeMetadataNameFromLabel(label); + + return computedName !== ''; }, { message: errors.LabelNotFormattable, @@ -24,16 +21,12 @@ export const metadataLabelSchema = (existingLabels?: string[]) => { ) .refine( (label) => { - try { - if (!existingLabels || !label?.length) { - return true; - } - return !existingLabels.includes( - computeMetadataNameFromLabelOrThrow(label), - ); - } catch { - return false; + if (!existingLabels || !label?.length) { + return true; } + const computedName = computeMetadataNameFromLabel(label); + + return computedName !== '' && !existingLabels.includes(computedName); }, { message: errors.LabelNotUnique, diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx deleted file mode 100644 index e1a945b6bd..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; -import { isDefined } from 'twenty-shared/utils'; -import { - IconArchive, - IconDotsVertical, - IconEye, - IconPencil, - IconTextSize, -} from 'twenty-ui/display'; -import { LightIconButton } from 'twenty-ui/input'; -import { MenuItem } from 'twenty-ui/navigation'; - -type SettingsObjectFieldActiveActionDropdownProps = { - isCustomField?: boolean; - onDeactivate?: () => void; - onEdit: () => void; - onSetAsLabelIdentifier?: () => void; - fieldMetadataItemId: string; - readonly?: boolean; -}; - -export const SettingsObjectFieldActiveActionDropdown = ({ - isCustomField, - readonly = false, - onDeactivate, - onEdit, - onSetAsLabelIdentifier, - fieldMetadataItemId, -}: SettingsObjectFieldActiveActionDropdownProps) => { - const dropdownId = `${fieldMetadataItemId}-settings-field-active-action-dropdown`; - - const { closeDropdown } = useCloseDropdown(); - - const handleEdit = () => { - onEdit(); - closeDropdown(dropdownId); - }; - - const handleDeactivate = () => { - onDeactivate?.(); - closeDropdown(dropdownId); - }; - - const handleSetAsLabelIdentifier = () => { - onSetAsLabelIdentifier?.(); - closeDropdown(dropdownId); - }; - - return ( - - } - dropdownComponents={ - - - - {isDefined(onSetAsLabelIdentifier) && !readonly && ( - - )} - {isDefined(onDeactivate) && !readonly && ( - - )} - - - } - /> - ); -}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 785dcaa27c..fa6a39b3b5 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -2,36 +2,34 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useDeleteOneFieldMetadataItem } from '@/object-metadata/hooks/useDeleteOneFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; -import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown'; import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown'; import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState'; import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; import { useMemo } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { FieldMetadataType, SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { - getSettingsPath, - isDefined, - isLabelIdentifierFieldMetadataTypes, -} from 'twenty-shared/utils'; -import { IconMinus, IconPlus, useIcons } from 'twenty-ui/display'; + IconChevronRight, + IconMinus, + IconPlus, + useIcons, +} from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; import { UndecoratedLink } from 'twenty-ui/navigation'; import { RelationType } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; +import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; import { RELATION_TYPES } from '../../constants/RelationTypes'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; -import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; type SettingsObjectFieldItemTableRowProps = { settingsObjectDetailTableItem: SettingsObjectDetailTableItem; @@ -48,22 +46,50 @@ const StyledNameTableCell = styled(TableCell)` gap: ${({ theme }) => theme.spacing(2)}; `; +const StyledNameContainer = styled.div` + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: ${({ theme }) => theme.spacing(1)}; +`; + const StyledNameLabel = styled.div` white-space: nowrap; text-overflow: ellipsis; overflow: hidden; `; +const StyledInactiveLabel = styled.span` + color: ${({ theme }) => theme.font.color.extraLight}; + font-size: ${({ theme }) => theme.font.size.sm}; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 0 999 auto; + min-width: 48px; + + &::before { + content: '·'; + margin-right: ${({ theme }) => theme.spacing(1)}; + } +`; + const StyledIconTableCell = styled(TableCell)` justify-content: center; padding-right: ${({ theme }) => theme.spacing(1)}; `; +const StyledIconChevronRight = styled(IconChevronRight)` + color: ${({ theme }) => theme.font.color.tertiary}; +`; + export const SettingsObjectFieldItemTableRow = ({ settingsObjectDetailTableItem, mode, status, }: SettingsObjectFieldItemTableRowProps) => { + const { t } = useLingui(); const { fieldMetadataItem, identifierType, objectMetadataItem } = settingsObjectDetailTableItem; @@ -80,10 +106,6 @@ export const SettingsObjectFieldItemTableRow = ({ const navigate = useNavigateSettings(); - const [navigationMemorizedUrl, setNavigationMemorizedUrl] = useRecoilState( - navigationMemorizedUrlState, - ); - const theme = useTheme(); const { getIcon } = useIcons(); const Icon = getIcon(fieldMetadataItem.icon); @@ -109,73 +131,15 @@ export const SettingsObjectFieldItemTableRow = ({ const canToggleField = !isLabelIdentifier; - const canBeSetAsLabelIdentifier = - objectMetadataItem.isCustom && - !isLabelIdentifier && - isLabelIdentifierFieldMetadataTypes(fieldMetadataItem.type); - const linkToNavigate = getSettingsPath(SettingsPath.ObjectFieldEdit, { objectNamePlural: objectMetadataItem.namePlural, fieldName: fieldMetadataItem.name, }); - const { activateMetadataField, deactivateMetadataField } = - useFieldMetadataItem(); + const { activateMetadataField } = useFieldMetadataItem(); const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem(); - const handleDisableField = async ( - activeFieldMetadatItem: FieldMetadataItem, - ) => { - if (readonly) { - return; - } - - const deactivationResult = await deactivateMetadataField( - activeFieldMetadatItem.id, - objectMetadataItem.id, - ); - - if (deactivationResult.status === 'failed') { - return; - } - - // TODO: Add optimistic rendering for core views - const deletedViewIds: string[] = []; - - const [baseUrl, queryParams] = navigationMemorizedUrl.includes('?') - ? navigationMemorizedUrl.split('?') - : [navigationMemorizedUrl, '']; - - const params = new URLSearchParams(queryParams); - const currentViewId = params.get('view'); - - if (isDefined(currentViewId) && deletedViewIds.includes(currentViewId)) { - params.delete('view'); - const updatedUrl = params.toString() - ? `${baseUrl}?${params.toString()}` - : baseUrl; - setNavigationMemorizedUrl(updatedUrl); - } - }; - - const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); - - const handleSetLabelIdentifierField = ( - activeFieldMetadatItem: FieldMetadataItem, - ) => { - if (readonly) { - return; - } - - updateOneObjectMetadataItem({ - idToUpdate: objectMetadataItem.id, - updatePayload: { - labelIdentifierFieldMetadataId: activeFieldMetadatItem.id, - }, - }); - }; - const [, setActiveSettingsObjectFields] = useRecoilState( settingsObjectFieldsFamilyState({ objectMetadataItemId: objectMetadataItem.id, @@ -249,9 +213,14 @@ export const SettingsObjectFieldItemTableRow = ({ stroke={theme.icon.stroke.sm} /> )} - - {fieldMetadataItem.label} - + + + {fieldMetadataItem.label} + + {!fieldMetadataItem.isActive && ( + {t`Deactivated`} + )} + @@ -281,27 +250,17 @@ export const SettingsObjectFieldItemTableRow = ({ {status === 'active' ? ( mode === 'view' ? ( - - navigate(SettingsPath.ObjectFieldEdit, { - objectNamePlural: objectMetadataItem.namePlural, - fieldName: fieldMetadataItem.name, - }) - } - onSetAsLabelIdentifier={ - canBeSetAsLabelIdentifier - ? () => handleSetLabelIdentifierField(fieldMetadataItem) - : undefined - } - onDeactivate={ - isLabelIdentifier - ? undefined - : () => handleDisableField(fieldMetadataItem) - } - /> + + + ) : ( canToggleField && ( theme.spacing(2)}; `; +const StyledNameContainer = styled.div` + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: ${({ theme }) => theme.spacing(1)}; +`; + const StyledNameLabel = styled.div` white-space: nowrap; text-overflow: ellipsis; overflow: hidden; `; +const StyledInactiveLabel = styled.span` + color: ${({ theme }) => theme.font.color.extraLight}; + font-size: ${({ theme }) => theme.font.size.sm}; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 0 999 auto; + min-width: 48px; + + &::before { + content: '·'; + margin-right: ${({ theme }) => theme.spacing(1)}; + } +`; + const StyledActionTableCell = styled(TableCell)` justify-content: center; padding-right: ${({ theme }) => theme.spacing(1)}; @@ -41,6 +65,7 @@ export const SettingsObjectMetadataItemTableRow = ({ link, totalObjectCount, }: SettingsObjectMetadataItemTableRowProps) => { + const { t } = useLingui(); const theme = useTheme(); const { getIcon } = useIcons(); @@ -56,9 +81,14 @@ export const SettingsObjectMetadataItemTableRow = ({ stroke={theme.icon.stroke.sm} /> )} - - {objectMetadataItem.labelPlural} - + + + {objectMetadataItem.labelPlural} + + {!objectMetadataItem.isActive && ( + {t`Deactivated`} + )} + diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 44b2338aaa..7a1c42a93f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -1,21 +1,27 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; import { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm'; import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; -import { H2Title, IconArchive } from 'twenty-ui/display'; +import { H2Title, IconArchive, IconTrash } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; type ObjectSettingsProps = { objectMetadataItem: ObjectMetadataItem; + isDeleting: boolean; + setIsDeleting: (isDeleting: boolean) => void; }; const StyledContentContainer = styled.div` @@ -28,7 +34,18 @@ const StyledFormSection = styled(Section)` padding-left: 0 !important; `; -export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { +const StyledDangerButtonsContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const DELETE_OBJECT_MODAL_ID = 'delete-object-confirmation-modal'; + +export const ObjectSettings = ({ + objectMetadataItem, + isDeleting, + setIsDeleting, +}: ObjectSettingsProps) => { const { t } = useLingui(); const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ @@ -38,6 +55,9 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { }); const navigate = useNavigateSettings(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); + const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem(); + const { enqueueSuccessSnackBar } = useSnackBar(); + const { openModal, closeModal } = useModal(); const handleDisable = async () => { const result = await updateOneObjectMetadataItem({ @@ -50,6 +70,29 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { } }; + const handleDelete = () => { + openModal(DELETE_OBJECT_MODAL_ID); + }; + + const confirmDelete = async () => { + setIsDeleting(true); + const result = await deleteOneObjectMetadataItem(objectMetadataItem.id); + + if (result.status === 'successful') { + enqueueSuccessSnackBar({ + message: t`Object deleted`, + }); + closeModal(DELETE_OBJECT_MODAL_ID); + navigate(SettingsPath.Objects); + return; + } + + setIsDeleting(false); + closeModal(DELETE_OBJECT_MODAL_ID); + }; + + const objectLabel = objectMetadataItem.labelPlural; + return ( @@ -79,15 +122,38 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { title={t`Danger zone`} description={t`Deactivate object`} /> -