diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index ad355fdf5..5163d3fb7 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -9,6 +9,7 @@ import { AvatarChip, AvatarChipVariant, ChipSize, + ChipVariant, LinkAvatarChip, } from 'twenty-ui/components'; import { isModifiedEvent } from 'twenty-ui/utilities'; @@ -56,6 +57,7 @@ export const RecordChip = ({ avatarType={recordChipData.avatarType} avatarUrl={recordChipData.avatarUrl ?? ''} className={className} + variant={ChipVariant.Static} /> ); } diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx index a602bfa09..0c5768697 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx @@ -9,6 +9,7 @@ export const ChipFieldDisplay = () => { objectNameSingular, labelIdentifierLink, isLabelIdentifierCompact, + isReadOnly, } = useChipFieldDisplay(); if (!isDefined(recordValue)) { @@ -22,6 +23,7 @@ export const ChipFieldDisplay = () => { size={ChipSize.Small} to={labelIdentifierLink} isLabelHidden={isLabelIdentifierCompact} + forceDisableClick={isReadOnly} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx index 0dfefca8f..b76b5c7d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx @@ -3,14 +3,19 @@ import { NoteTarget } from '@/activities/types/NoteTarget'; import { TaskTarget } from '@/activities/types/TaskTarget'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordChip } from '@/object-record/components/RecordChip'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay'; + import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; +import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { pascalCase } from '~/utils/string/pascalCase'; export const RelationFromManyFieldDisplay = () => { const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay(); const { isFocused } = useFieldFocus(); + const { isReadOnly } = useContext(FieldContext); const { fieldName, objectMetadataNameSingular } = fieldDefinition.metadata; @@ -45,19 +50,36 @@ export const RelationFromManyFieldDisplay = () => { : CoreObjectNameSingular.Task; const relationFieldName = fieldName === 'noteTargets' ? 'note' : 'task'; + const formattedRecords = fieldValue.map((record) => { + if (!isDefined(record[relationFieldName])) { + return { + ...record, + [relationFieldName]: { + id: 'fallback-id', + title: pascalCase(relationFieldName), + }, + }; + } + return record; + }); return ( - {fieldValue - .map((record) => - isDefined(record) && isDefined(record[relationFieldName]) ? ( + {formattedRecords + .map((record) => { + if (!isDefined(record)) { + return undefined; + } + + return ( - ) : undefined, - ) + ); + }) .filter(isDefined)} ); @@ -69,6 +91,7 @@ export const RelationFromManyFieldDisplay = () => { key={record.targetObject.id} objectNameSingular={record.targetObjectMetadataItem.nameSingular} record={record.targetObject} + forceDisableClick={isReadOnly} /> ))} @@ -81,6 +104,7 @@ export const RelationFromManyFieldDisplay = () => { key={record.id} objectNameSingular={objectNameSingular} record={record} + forceDisableClick={isReadOnly} /> ))} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx index d1edf4c61..79d08686b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx @@ -1,12 +1,16 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordChip } from '@/object-record/components/RecordChip'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay'; +import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; export const RelationToOneFieldDisplay = () => { const { fieldValue, fieldDefinition, generateRecordChipData } = useRelationToOneFieldDisplay(); + const { isReadOnly } = useContext(FieldContext); + if ( !isDefined(fieldValue) || !isDefined(fieldDefinition?.metadata.relationObjectMetadataNameSingular) @@ -24,7 +28,7 @@ export const RelationToOneFieldDisplay = () => { key={recordChipData.recordId} objectNameSingular={recordChipData.objectNameSingular} record={fieldValue} - forceDisableClick={isWorkspaceMemberFieldMetadataRelation} + forceDisableClick={isWorkspaceMemberFieldMetadataRelation || isReadOnly} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts index 4ddfdd667..6b1edd0a9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts @@ -18,6 +18,7 @@ export const useChipFieldDisplay = () => { isLabelIdentifier, labelIdentifierLink, isLabelIdentifierCompact, + isReadOnly, } = useContext(FieldContext); const { chipGeneratorPerObjectPerField } = useContext( @@ -48,5 +49,6 @@ export const useChipFieldDisplay = () => { isLabelIdentifier, labelIdentifierLink, isLabelIdentifierCompact, + isReadOnly, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts index 7b9f94ac6..27d026853 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts @@ -8,10 +8,10 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth'; import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from 'twenty-shared/utils'; import { FieldContext } from '../../contexts/FieldContext'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldRelation } from '../../types/guards/isFieldRelation'; -import { isDefined } from 'twenty-shared/utils'; export const useRelationFromManyFieldDisplay = () => { const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx index 92fe2e887..79bdb454f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx @@ -17,6 +17,7 @@ import { import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { FieldMetadataType, + RelationDefinition, RelationDefinitionType, } from '~/generated-metadata/graphql'; type SettingsDataModelFieldRelationSettingsFormCardProps = { @@ -80,12 +81,22 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ const relationType = watchFormValue('relation.type', initialRelationType); const relationTypeConfig = RELATION_TYPES[relationType]; + const oppositeRelationType = + relationType === RelationDefinitionType.MANY_TO_ONE + ? RelationDefinitionType.ONE_TO_MANY + : RelationDefinitionType.MANY_TO_ONE; + return ( & { id?: string; name?: string; @@ -31,7 +38,7 @@ export type SettingsDataModelFieldPreviewProps = { }; const StyledFieldPreview = styled.div<{ shrink?: boolean }>` - align-items: flex-start; + align-items: center; background-color: ${({ theme }) => theme.background.primary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; @@ -95,7 +102,7 @@ export const SettingsDataModelFieldPreview = ({ fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`; const recordId = previewRecord?.id ?? - `${objectMetadataItem.nameSingular}-${fieldName}-preview`; + `${objectMetadataItem.nameSingular}-${fieldName}-${fieldMetadataItem.relationDefinition?.direction}-preview`; return ( <> @@ -104,7 +111,7 @@ export const SettingsDataModelFieldPreview = ({ instanceId: 'record-field-component-instance-id', }} > - {previewRecord ? ( + {isDefined(previewRecord) ? ( {fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? ( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx index 8a377259b..3997720a0 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx @@ -92,14 +92,16 @@ describe('useFieldPreviewValue', () => { ); // Then - expect(result.current).toEqual({ - __typename: 'Person', - id: '', - name: { - firstName: 'John', - lastName: 'Doe', + expect(result.current).toEqual([ + { + __typename: 'Person', + id: '', + name: { + firstName: 'John', + lastName: 'Doe', + }, }, - }); + ]); }); it("returns the field's preview value for a Select field", () => { diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts index 6c645dd0b..fbacbc235 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/useFieldPreviewValue.ts @@ -8,12 +8,15 @@ import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils import { getMultiSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getMultiSelectFieldPreviewValue'; import { getPhonesFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue'; import { getSelectFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getSelectFieldPreviewValue'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { + FieldMetadataType, + RelationDefinitionType, +} from '~/generated-metadata/graphql'; type UseFieldPreviewParams = { fieldMetadataItem: Pick< FieldMetadataItem, - 'type' | 'options' | 'defaultValue' + 'type' | 'options' | 'defaultValue' | 'relationDefinition' >; relationObjectMetadataItem?: ObjectMetadataItem; skip?: boolean; @@ -43,7 +46,10 @@ export const useFieldPreviewValue = ({ case FieldMetadataType.CURRENCY: return getCurrencyFieldPreviewValue({ fieldMetadataItem }); case FieldMetadataType.RELATION: - return relationFieldPreviewValue; + return fieldMetadataItem.relationDefinition?.direction === + RelationDefinitionType.MANY_TO_ONE + ? relationFieldPreviewValue + : [relationFieldPreviewValue]; case FieldMetadataType.SELECT: return getSelectFieldPreviewValue({ fieldMetadataItem }); case FieldMetadataType.MULTI_SELECT: diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts index b84ba73e7..17c8daa2e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/usePreviewRecord.ts @@ -1,12 +1,13 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; +import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { pascalCase } from '~/utils/string/pascalCase'; -import { isDefined } from 'twenty-shared/utils'; type UsePreviewRecordParams = { objectMetadataItem: Pick< @@ -27,8 +28,15 @@ export const usePreviewRecord = ({ getLabelIdentifierFieldMetadataItem(objectMetadataItem); const skip = skipFromProps || !labelIdentifierFieldMetadataItem; + let recordGqlFields: Record | undefined = undefined; + if (objectMetadataItem.nameSingular === CoreObjectNameSingular.NoteTarget) + recordGqlFields = { id: true, note: true }; + if (objectMetadataItem.nameSingular === CoreObjectNameSingular.TaskTarget) + recordGqlFields = { id: true, task: true }; + const { records } = useFindManyRecords({ objectNameSingular: objectMetadataItem.nameSingular, + recordGqlFields, limit: 1, skip, }); diff --git a/packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx b/packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx index 8b2e96047..0b9fec5a0 100644 --- a/packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx +++ b/packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx @@ -14,10 +14,11 @@ export const AvatarChip = ({ maxWidth, placeholderColorSeed, size, + variant = ChipVariant.Transparent, }: AvatarChipProps) => ( & { +export type LinkAvatarChipProps = Omit< + AvatarChipsCommonProps, + 'clickable' | 'variant' +> & { to: string; onClick?: LinkChipProps['onClick']; variant?: AvatarChipVariant; diff --git a/packages/twenty-ui/src/components/avatar-chip/types/AvatarChipsCommonProps.type.ts b/packages/twenty-ui/src/components/avatar-chip/types/AvatarChipsCommonProps.type.ts index ae415e5af..9139a491a 100644 --- a/packages/twenty-ui/src/components/avatar-chip/types/AvatarChipsCommonProps.type.ts +++ b/packages/twenty-ui/src/components/avatar-chip/types/AvatarChipsCommonProps.type.ts @@ -1,8 +1,9 @@ import { AvatarChipsLeftComponentProps } from '@ui/components/avatar-chip/AvatarChipLeftComponent'; -import { ChipSize } from '@ui/components/chip/Chip'; +import { ChipSize, ChipVariant } from '@ui/components/chip/Chip'; export type AvatarChipsCommonProps = { size?: ChipSize; className?: string; maxWidth?: number; + variant?: ChipVariant; } & AvatarChipsLeftComponentProps; diff --git a/packages/twenty-ui/src/components/chip/Chip.tsx b/packages/twenty-ui/src/components/chip/Chip.tsx index 98103b08a..204c9e492 100644 --- a/packages/twenty-ui/src/components/chip/Chip.tsx +++ b/packages/twenty-ui/src/components/chip/Chip.tsx @@ -19,6 +19,7 @@ export enum ChipVariant { Regular = 'regular', Transparent = 'transparent', Rounded = 'rounded', + Static = 'static', } export type ChipProps = { @@ -89,7 +90,9 @@ const StyledContainer = withTheme(styled.div< ? theme.background.transparent.light : variant === ChipVariant.Highlighted ? theme.background.transparent.medium - : 'inherit'}; + : variant === ChipVariant.Static + ? theme.background.transparent.light + : 'inherit'}; } &:active { @@ -98,11 +101,13 @@ const StyledContainer = withTheme(styled.div< ? theme.background.transparent.medium : variant === ChipVariant.Highlighted ? theme.background.transparent.strong - : 'inherit'}; + : variant === ChipVariant.Static + ? theme.background.transparent.light + : 'inherit'}; } background-color: ${({ theme, variant }) => - variant === ChipVariant.Highlighted + variant === ChipVariant.Highlighted || variant === ChipVariant.Static ? theme.background.transparent.light : variant === ChipVariant.Rounded ? theme.background.transparent.lighter