diff --git a/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts index 57a5fe227c..9586d60a9b 100644 --- a/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts +++ b/packages/twenty-front/src/modules/object-record/read-only/utils/__tests__/isObjectMetadataSettingsReadOnly.test.ts @@ -12,13 +12,12 @@ describe('isObjectMetadataSettingsReadOnly', () => { isUIReadOnly: false, isRemote: false, }, - workspaceCustomApplicationId: 'workspaceApplicationId', }); expect(result).toBe(false); }); - it('should return true if object is managed by application', () => { + it('should return true if object is remote', () => { const result = isObjectMetadataSettingsReadOnly({ objectPermissions: { canUpdateObjectRecords: true, @@ -26,24 +25,36 @@ describe('isObjectMetadataSettingsReadOnly', () => { restrictedFields: {}, }, objectMetadataItem: { - applicationId: 'applicationId', isUIReadOnly: false, - isRemote: false, + isRemote: true, }, - workspaceCustomApplicationId: null, }); expect(result).toBe(true); }); - it('should return false if object is owned by workspace custom application', () => { + it('should return true if object is UI read only', () => { const result = isObjectMetadataSettingsReadOnly({ + objectMetadataItem: { + isUIReadOnly: true, + isRemote: false, + }, + }); + + expect(result).toBe(true); + }); + + it('should return false for standard/third-party objects (they are editable via standardOverrides)', () => { + const result = isObjectMetadataSettingsReadOnly({ + objectPermissions: { + canUpdateObjectRecords: true, + objectMetadataId: '123', + restrictedFields: {}, + }, objectMetadataItem: { isUIReadOnly: false, isRemote: false, - applicationId: 'workspaceApplicationId', }, - workspaceCustomApplicationId: 'workspaceApplicationId', }); expect(result).toBe(false); diff --git a/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts b/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts index d3f339e71d..3700aedd7f 100644 --- a/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/read-only/utils/isObjectMetadataSettingsReadOnly.ts @@ -1,28 +1,17 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; -import { isDefined } from 'twenty-shared/utils'; import { type ObjectPermission } from '~/generated/graphql'; -type IsObjectMetadataReadOnlyParams = { +type IsObjectMetadataSettingsReadOnlyParams = { objectPermissions?: ObjectPermission; - objectMetadataItem?: Pick< - ObjectMetadataItem, - 'isUIReadOnly' | 'isRemote' | 'applicationId' - >; - workspaceCustomApplicationId: string | null; + objectMetadataItem?: Pick; }; +// Returns true only for remote or UI read-only objects +// Standard and third-party app objects are editable (label/icon/description via standardOverrides) export const isObjectMetadataSettingsReadOnly = ({ objectPermissions, objectMetadataItem, - workspaceCustomApplicationId, -}: IsObjectMetadataReadOnlyParams) => { - return ( - isObjectMetadataReadOnly({ objectPermissions, objectMetadataItem }) || - (isDefined(objectMetadataItem?.applicationId) - ? isDefined(workspaceCustomApplicationId) - ? objectMetadataItem.applicationId !== workspaceCustomApplicationId - : true - : false) - ); +}: IsObjectMetadataSettingsReadOnlyParams) => { + return isObjectMetadataReadOnly({ objectPermissions, objectMetadataItem }); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx b/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx index 086249851f..2ed9f6b4b9 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsItemTypeTag.tsx @@ -1,7 +1,9 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { Tag } from 'twenty-ui/components'; +import { Avatar } from 'twenty-ui/display'; type SettingsItemTypeTagProps = { item: { @@ -12,10 +14,18 @@ type SettingsItemTypeTagProps = { className?: string; }; +const StyledContainer = styled.div` + align-items: center; + display: flex; + font-size: ${({ theme }) => theme.font.size.sm}; + gap: ${({ theme }) => theme.spacing(1)}; +`; + export const SettingsItemTypeTag = ({ className, item: { isCustom, isRemote, applicationId }, }: SettingsItemTypeTagProps) => { + const theme = useTheme(); const currentWorkspace = useRecoilValue(currentWorkspaceState); const itemTagInfo = getItemTagInfo({ objectMetadataItem: { isCustom, isRemote, applicationId }, @@ -24,11 +34,16 @@ export const SettingsItemTypeTag = ({ }); return ( - + + + {itemTagInfo.labelText} + ); }; 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 31f8ad600d..9a87ef2eee 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 @@ -1,8 +1,10 @@ -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 { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; +import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; +import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; 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'; @@ -12,7 +14,7 @@ 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 { useRecoilState } from 'recoil'; import { FieldMetadataType, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { @@ -26,9 +28,6 @@ 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 '@/settings/data-model/constants/RelationTypes'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; type SettingsObjectFieldItemTableRowProps = { @@ -90,20 +89,13 @@ export const SettingsObjectFieldItemTableRow = ({ status, }: SettingsObjectFieldItemTableRowProps) => { const { t } = useLingui(); - const { fieldMetadataItem, identifierType, objectMetadataItem } = + const { fieldMetadataItem, objectMetadataItem } = settingsObjectDetailTableItem; - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); - const isRemoteObjectField = objectMetadataItem.isRemote; - - const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type'; - const navigate = useNavigateSettings(); const theme = useTheme(); @@ -167,21 +159,6 @@ export const SettingsObjectFieldItemTableRow = ({ }); }; - const typeLabel = - variant === 'field-type' - ? isRemoteObjectField - ? t`Remote` - : fieldMetadataItem.isCustom - ? t`Custom` - : t`Standard` - : variant === 'identifier' - ? isDefined(identifierType) - ? identifierType === 'label' - ? t`Record text` - : t`Record image` - : '' - : ''; - if (!isFieldTypeSupported) return null; const isRelatedObjectLinkable = @@ -224,7 +201,15 @@ export const SettingsObjectFieldItemTableRow = ({ - {typeLabel} + + + theme.font.color.primary}; + 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}; +`; + +const StyledRelationType = styled.div` + align-items: center; + display: flex; + font-size: ${({ theme }) => theme.font.size.sm}; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledLink = styled(Link)` + color: ${({ theme }) => theme.font.color.primary}; + text-decoration: underline; + text-decoration-color: ${({ theme }) => theme.border.color.strong}; + text-underline-offset: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover { + color: ${({ theme }) => theme.color.blue}; + text-decoration-color: ${({ theme }) => theme.color.blue}; + } +`; + +export const SettingsObjectRelationItemTableRow = ({ + fieldMetadataItem, + objectMetadataItem, +}: SettingsObjectRelationItemTableRowProps) => { + const { t } = useLingui(); + const navigate = useNavigateSettings(); + const theme = useTheme(); + const { getIcon } = useIcons(); + + const Icon = getIcon(fieldMetadataItem.icon); + + const getRelationMetadata = useGetRelationMetadata(); + const { relationObjectMetadataItem, relationType } = + useMemo( + () => getRelationMetadata({ fieldMetadataItem }), + [fieldMetadataItem, getRelationMetadata], + ) ?? {}; + + const readonly = isObjectMetadataSettingsReadOnly({ + objectMetadataItem, + }); + + const { activateMetadataField } = useFieldMetadataItem(); + const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem(); + + const linkToNavigate = getSettingsPath(SettingsPath.ObjectFieldEdit, { + objectNamePlural: objectMetadataItem.namePlural, + fieldName: fieldMetadataItem.name, + }); + + // eslint-disable-next-line @nx/workspace-no-navigate-prefer-link + const navigateToFieldEdit = () => + navigate(SettingsPath.ObjectFieldEdit, { + objectNamePlural: objectMetadataItem.namePlural, + fieldName: fieldMetadataItem.name, + }); + + const isRelatedObjectLinkable = + isDefined(relationObjectMetadataItem?.namePlural) && + !relationObjectMetadataItem.isSystem; + + const morphRelationCount = fieldMetadataItem.morphRelations?.length; + + const relationTypeLabel = (() => { + if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) { + return t`${morphRelationCount} Objects`; + } + if (isDefined(relationType) === true) { + return RELATION_TYPES[relationType].label; + } + return ''; + })(); + + const RelationIcon = relationType + ? RELATION_TYPES[relationType].Icon + : undefined; + + const targetObjectLabel = + isRelatedObjectLinkable && relationObjectMetadataItem + ? relationObjectMetadataItem.labelPlural + : fieldMetadataItem.label; + + return ( + // eslint-disable-next-line @nx/workspace-no-navigate-prefer-link + + + {!!Icon && ( + + )} + + {isRelatedObjectLinkable ? ( + event.stopPropagation()} + title={targetObjectLabel} + > + {targetObjectLabel} + + ) : ( + + {targetObjectLabel} + + )} + {!fieldMetadataItem.isActive && ( + {t`Deactivated`} + )} + + + + + + + + + + {RelationIcon && ( + + )} + {relationTypeLabel} + + + + + {fieldMetadataItem.isActive ? ( + + + + ) : ( + + activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id) + } + onDelete={() => + deleteOneFieldMetadataItem({ + idToDelete: fieldMetadataItem.id, + objectMetadataId: objectMetadataItem.id, + }) + } + /> + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx new file mode 100644 index 0000000000..71eea032e0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx @@ -0,0 +1,167 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; +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 { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; +import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata'; +import styled from '@emotion/styled'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react/macro'; +import { useMemo, useState } from 'react'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { MenuItemToggle } from 'twenty-ui/navigation'; +import { normalizeSearchText } from '~/utils/normalizeSearchText'; +import { + SettingsObjectRelationItemTableRow, + StyledObjectRelationTableRow, +} from './SettingsObjectRelationItemTableRow'; + +const StyledSearchAndFilterContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding-bottom: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledSearchInput = styled(SettingsTextInput)` + flex: 1; +`; + +const SETTINGS_OBJECT_RELATION_TABLE_METADATA: TableMetadata = + { + tableId: 'settingsObjectRelations', + fields: [ + { + fieldLabel: msg`Name`, + fieldName: 'label', + fieldType: 'string', + align: 'left', + }, + { + fieldLabel: msg`App`, + fieldName: 'isCustom', + fieldType: 'string', + align: 'left', + }, + { + fieldLabel: msg`Type`, + fieldName: 'type', + fieldType: 'string', + align: 'left', + }, + ], + initialSort: { + fieldName: 'label', + orderBy: 'AscNullsLast', + }, + }; + +type SettingsObjectRelationsTableProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const SettingsObjectRelationsTable = ({ + objectMetadataItem, +}: SettingsObjectRelationsTableProps) => { + const { t } = useLingui(); + const [searchTerm, setSearchTerm] = useState(''); + const [showInactive, setShowInactive] = useState(true); + + const tableMetadata = SETTINGS_OBJECT_RELATION_TABLE_METADATA; + + const relationFields = useMemo(() => { + return objectMetadataItem.fields.filter( + (field) => + !field.isSystem && + (field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.MORPH_RELATION), + ); + }, [objectMetadataItem.fields]); + + const sortedRelationFields = useSortedArray(relationFields, tableMetadata); + + const filteredRelationFields = useMemo(() => { + const searchNormalized = normalizeSearchText(searchTerm); + + return sortedRelationFields.filter((field) => { + const matchesActiveFilter = showInactive || field.isActive; + const matchesSearch = normalizeSearchText(field.label).includes( + searchNormalized, + ); + return matchesActiveFilter && matchesSearch; + }); + }, [sortedRelationFields, searchTerm, showInactive]); + + if (relationFields.length === 0) { + return null; + } + + return ( + <> + + + + } + dropdownComponents={ + + + setShowInactive(!showInactive)} + toggled={showInactive} + text={t`Inactive`} + toggleSize="small" + /> + + + } + /> + + + + {tableMetadata.fields.map((item) => ( + + ))} + + + {filteredRelationFields.map((fieldMetadataItem) => ( + + ))} +
+ + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx index 04f3803706..1047b4dba1 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -1,4 +1,3 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm'; @@ -8,7 +7,7 @@ import { } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; import { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState'; @@ -21,11 +20,8 @@ type SettingsUpdateDataModelObjectAboutFormProps = { export const SettingsUpdateDataModelObjectAboutForm = ({ objectMetadataItem, }: SettingsUpdateDataModelObjectAboutFormProps) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const navigate = useNavigateSettings(); const setUpdatedObjectNamePlural = useSetRecoilState( 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 index 6e0240a8e6..7e5e051c54 100644 --- 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 @@ -1,19 +1,17 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; -import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; - +import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; +import { SettingsObjectRelationsTable } from '@/settings/data-model/object-details/components/SettingsObjectRelationsTable'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { FieldMetadataType, SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; import { H2Title, IconPlus } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { UndecoratedLink } from 'twenty-ui/navigation'; -import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; -import { useRecoilValue } from 'recoil'; +import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; -const StyledDiv = styled.div` +const StyledButtonContainer = styled.div` display: flex; justify-content: flex-end; padding-top: ${({ theme }) => theme.spacing(2)}; @@ -24,42 +22,78 @@ type ObjectFieldsProps = { }; export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const { t } = useLingui(); const objectLabelSingular = objectMetadataItem.labelSingular; + const hasRelations = objectMetadataItem.fields.some( + (field) => + !field.isSystem && + (field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.MORPH_RELATION), + ); + return ( -
- - - {!readonly && ( - - -
)} - +
+ + + + {!readonly && ( + +
+ ); }; 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 7a1c42a93f..60adcc7538 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,6 +1,5 @@ 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'; @@ -11,7 +10,6 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa 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, IconTrash } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; @@ -47,11 +45,8 @@ export const ObjectSettings = ({ setIsDeleting, }: ObjectSettingsProps) => { const { t } = useLingui(); - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const navigate = useNavigateSettings(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 66d9088b6e..a2653c3931 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -1,4 +1,3 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; @@ -11,7 +10,6 @@ import { t } from '@lingui/core/macro'; import { useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils'; import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; @@ -44,11 +42,8 @@ const StyledContainer = styled.div` export const SettingsDataModelObjectIdentifiersForm = ({ objectMetadataItem, }: SettingsDataModelObjectIdentifiersFormProps) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const formConfig = useForm({ mode: 'onTouched', 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 881c445f21..53e23f437d 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx @@ -14,16 +14,13 @@ import styled from '@emotion/styled'; import { useRecoilState, useRecoilValue } from 'recoil'; import { AppPath, SettingsPath } from 'twenty-shared/types'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isObjectMetadataSettingsReadOnly } from '@/object-record/read-only/utils/isObjectMetadataSettingsReadOnly'; -import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useTheme } from '@emotion/react'; import { useLingui } from '@lingui/react/macro'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { - H3Title, IconCodeCircle, IconListDetails, IconPlus, @@ -43,16 +40,6 @@ const StyledContentContainer = styled.div` padding-left: 0; `; -const StyledObjectTypeTag = styled(SettingsItemTypeTag)` - box-sizing: border-box; - height: ${({ theme }) => theme.spacing(5)}; - margin-left: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledTitleContainer = styled.div` - display: flex; -`; - export const SettingsObjectDetailPage = () => { const navigateApp = useNavigateApp(); const { t } = useLingui(); @@ -70,11 +57,8 @@ export const SettingsObjectDetailPage = () => { findObjectMetadataItemByNamePlural(objectNamePlural) ?? findObjectMetadataItemByNamePlural(updatedObjectNamePlural); - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const activeTabId = useRecoilComponentValue( @@ -157,12 +141,7 @@ export const SettingsObjectDetailPage = () => { return ( <> - - - - } + title={objectMetadataItem.labelPlural} links={[ { children: t`Workspace`, diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index bde77c2ede..bc795c557a 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -5,7 +5,6 @@ import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; import { type z } from 'zod'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; @@ -30,7 +29,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe import { shouldNavigateBackToMemorizedUrlOnSaveState } from '@/ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState } from 'recoil'; import { AppPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { @@ -84,11 +83,8 @@ export const SettingsObjectFieldEdit = () => { const objectMetadataItem = findObjectMetadataItemByNamePlural(objectNamePlural); - const currentWorkspace = useRecoilValue(currentWorkspaceState); const readonly = isObjectMetadataSettingsReadOnly({ objectMetadataItem, - workspaceCustomApplicationId: - currentWorkspace?.workspaceCustomApplication?.id, }); const { diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx index 95406a2e61..77afc69c9d 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx @@ -18,6 +18,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; import { useEffect, useMemo, useState } from 'react'; import { useRecoilState } from 'recoil'; +import { FieldMetadataType } from 'twenty-shared/types'; import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { MenuItemToggle } from 'twenty-ui/navigation'; @@ -25,7 +26,18 @@ import { useMapFieldMetadataItemToSettingsObjectDetailTableItem } from '~/pages/ import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; import { normalizeSearchText } from '~/utils/normalizeSearchText'; -const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata = +const StyledSearchAndFilterContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding-bottom: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledSearchInput = styled(SettingsTextInput)` + flex: 1; +`; + +const SETTINGS_OBJECT_FIELD_TABLE_METADATA: TableMetadata = { tableId: 'settingsObjectDetail', fields: [ @@ -36,7 +48,7 @@ const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata = - { - tableId: 'settingsObjectDetail', - fields: [ - { - fieldLabel: msg`Name`, - fieldName: 'label', - fieldType: 'string', - align: 'left', - }, - { - fieldLabel: msg`Identifier`, - fieldName: 'identifierType', - fieldType: 'string', - align: 'left', - }, - { - fieldLabel: msg`Data type`, - fieldName: 'dataType', - fieldType: 'string', - align: 'left', - }, - ], - initialSort: { - fieldName: 'label', - orderBy: 'AscNullsLast', - }, - }; - -const StyledSearchAndFilterContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - padding-bottom: ${({ theme }) => theme.spacing(2)}; - width: 100%; -`; - -const StyledSearchInput = styled(SettingsTextInput)` - flex: 1; -`; - export type SettingsObjectFieldTableProps = { objectMetadataItem: ObjectMetadataItem; mode: 'view' | 'new-field'; + excludeRelations?: boolean; }; // TODO: find another way than using mode which feels like it could be replaced by another pattern export const SettingsObjectFieldTable = ({ objectMetadataItem, mode, + excludeRelations = false, }: SettingsObjectFieldTableProps) => { const { t } = useLingui(); const [searchTerm, setSearchTerm] = useState(''); const [showInactive, setShowInactive] = useState(true); - const tableMetadata = objectMetadataItem.isCustom - ? GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM - : GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD; + const tableMetadata = SETTINGS_OBJECT_FIELD_TABLE_METADATA; const { mapFieldMetadataItemToSettingsObjectDetailTableItem } = useMapFieldMetadataItemToSettingsObjectDetailTableItem(objectMetadataItem); @@ -130,14 +102,23 @@ export const SettingsObjectFieldTable = ({ (fieldMetadataItem) => !fieldMetadataItem.isSystem, ); + const fieldsToDisplay = excludeRelations + ? nonSystemFields?.filter( + (fieldMetadataItem) => + fieldMetadataItem.type !== FieldMetadataType.RELATION && + fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION, + ) + : nonSystemFields; + return ( - nonSystemFields?.map( + fieldsToDisplay?.map( mapFieldMetadataItemToSettingsObjectDetailTableItem, ) ?? [] ); }, [ settingsObjectFields, mapFieldMetadataItemToSettingsObjectDetailTableItem, + excludeRelations, ]); const sortedAllObjectSettingsDetailItems = useSortedArray( diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx index 4aa32f1068..e26db105ef 100644 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx @@ -35,11 +35,6 @@ export const Default: Story = { const employeeInput = await canvas.findByPlaceholderText('Employees'); await userEvent.type(employeeInput, 'Test'); - const descriptionInput = await canvas.findByPlaceholderText( - 'Write a description', - ); - await userEvent.type(descriptionInput, 'Test description'); - const saveButton = await canvas.findByText('Save'); await new Promise((resolve) => setTimeout(resolve, 5000)); await userEvent.click(saveButton); diff --git a/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts index d54823484f..6e7c419097 100644 --- a/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts +++ b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts @@ -13,7 +13,7 @@ export const GET_SETTINGS_OBJECT_TABLE_METADATA: TableMetadata { icon: DEFAULT_ICONS_BY_FIELD_TYPE[fieldType] ?? DEFAULT_ICON_FOR_NEW_FIELD, label: '', - description: '', name: '', }, }); @@ -237,13 +235,6 @@ export const SettingsObjectNewFieldConfigure = () => { objectNameSingular={activeObjectMetadataItem.nameSingular} /> -
- - -