diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts index a5ce4e2a1..a7c551df1 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery.ts @@ -28,6 +28,7 @@ export const useMapFieldMetadataToGraphQLQuery = () => { 'EMAIL', 'NUMBER', 'BOOLEAN', + 'SELECT', ] as FieldType[] ).includes(fieldType); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts index 7343ef16c..365f3b3dc 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition.ts @@ -40,6 +40,7 @@ export const formatFieldMetadataItemAsColumnDefinition = ({ relationObjectMetadataNamePlural: relationObjectMetadataItem?.namePlural ?? '', objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '', + options: field.options, }, iconName: field.icon ?? 'Icon123', isVisible: true, diff --git a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx index 6f12e35e1..4d7adf44e 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx @@ -15,7 +15,6 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/ import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { RecordRelationFieldCardSection } from '@/object-record/record-relation-card/components/RecordRelationFieldCardSection'; -import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable'; import { IconBuildingSkyscraper } from '@/ui/display/icon'; import { PageBody } from '@/ui/layout/page/PageBody'; @@ -51,12 +50,13 @@ export const RecordShowPage = () => { throw new Error(`Object name is not defined`); } - const { objectMetadataItem, labelIdentifierFieldMetadata } = - useObjectMetadataItem({ - objectNameSingular, - }); - - const { identifiersMapper } = useRelationPicker(); + const { + objectMetadataItem, + labelIdentifierFieldMetadata, + mapToObjectRecordIdentifier, + } = useObjectMetadataItem({ + objectNameSingular, + }); const { favorites, createFavorite, deleteFavorite } = useFavorites(); @@ -107,11 +107,6 @@ export const RecordShowPage = () => { ? record?.name.firstName + ' ' + record?.name.lastName : record?.name; - const recordIdentifiers = identifiersMapper?.( - record, - objectMetadataItem?.nameSingular ?? '', - ); - const onUploadPicture = async (file: File) => { if (objectNameSingular !== 'person') { return; @@ -201,8 +196,12 @@ export const RecordShowPage = () => { <> { } - avatarType={recordIdentifiers?.avatarType ?? 'rounded'} + avatarType={ + mapToObjectRecordIdentifier(record).avatarType ?? + 'rounded' + } onUploadPicture={ objectNameSingular === 'person' ? onUploadPicture diff --git a/packages/twenty-front/src/modules/object-record/field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/components/FieldInput.tsx index 8f7328710..c9505d357 100644 --- a/packages/twenty-front/src/modules/object-record/field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/field/components/FieldInput.tsx @@ -1,7 +1,9 @@ import { useContext } from 'react'; import { FullNameFieldInput } from '@/object-record/field/meta-types/input/components/FullNameFieldInput'; +import { SelectFieldInput } from '@/object-record/field/meta-types/input/components/SelectFieldInput'; import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName'; +import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { FieldContext } from '../contexts/FieldContext'; @@ -120,6 +122,8 @@ export const FieldInput = ({ ) : isFieldRating(fieldDefinition) ? ( + ) : isFieldSelect(fieldDefinition) ? ( + ) : ( <> )} diff --git a/packages/twenty-front/src/modules/object-record/field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/field/hooks/usePersistField.ts index e5d62d69f..b887504e8 100644 --- a/packages/twenty-front/src/modules/object-record/field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/field/hooks/usePersistField.ts @@ -3,6 +3,8 @@ import { useRecoilCallback } from 'recoil'; import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName'; import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue'; +import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect'; +import { isFieldSelectValue } from '@/object-record/field/types/guards/isFieldSelectValue'; import { FieldContext } from '../contexts/FieldContext'; import { entityFieldsFamilySelector } from '../states/selectors/entityFieldsFamilySelector'; @@ -77,6 +79,9 @@ export const usePersistField = () => { const fieldIsPhone = isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist); + const fieldIsSelect = + isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist); + if (fieldIsRelation) { const fieldName = fieldDefinition.metadata.fieldName; @@ -104,7 +109,8 @@ export const usePersistField = () => { fieldIsPhone || fieldIsLink || fieldIsCurrency || - fieldIsFullName + fieldIsFullName || + fieldIsSelect ) { const fieldName = fieldDefinition.metadata.fieldName; set( diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/display/components/SelectFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/display/components/SelectFieldDisplay.tsx index a355daf7b..5c2b52b3c 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/display/components/SelectFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/display/components/SelectFieldDisplay.tsx @@ -3,7 +3,15 @@ import { Tag } from '@/ui/display/tag/components/Tag'; import { useSelectField } from '../../hooks/useSelectField'; export const SelectFieldDisplay = () => { - const { fieldValue } = useSelectField(); + const { fieldValue, fieldDefinition } = useSelectField(); - return ; + const selectedOption = fieldDefinition.metadata.options.find( + (option) => option.value === fieldValue, + ); + + return selectedOption ? ( + + ) : ( + <> + ); }; diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/hooks/useSelectField.ts b/packages/twenty-front/src/modules/object-record/field/meta-types/hooks/useSelectField.ts index ef0862ceb..55de15c58 100644 --- a/packages/twenty-front/src/modules/object-record/field/meta-types/hooks/useSelectField.ts +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/hooks/useSelectField.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; -import { ThemeColor } from '@/ui/theme/constants/colors'; +import { usePersistField } from '@/object-record/field/hooks/usePersistField'; import { FieldMetadataType } from '~/generated/graphql'; import { FieldContext } from '../../contexts/FieldContext'; @@ -25,23 +25,18 @@ export const useSelectField = () => { fieldName: fieldName, }), ); - const fieldSelectValue = isFieldSelectValue(fieldValue) - ? fieldValue - : { color: 'green' as ThemeColor, label: '' }; + + const fieldSelectValue = isFieldSelectValue(fieldValue) ? fieldValue : null; const fieldInitialValue = useFieldInitialValue(); - const initialValue = { - color: 'green' as ThemeColor, - label: fieldInitialValue?.isEmpty - ? '' - : fieldInitialValue?.value ?? fieldSelectValue?.label ?? '', - }; + const persistField = usePersistField(); return { fieldDefinition, + persistField, fieldValue: fieldSelectValue, - initialValue, + initialValue: fieldInitialValue, setFieldValue, hotkeyScope, }; diff --git a/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/SelectFieldInput.tsx new file mode 100644 index 000000000..6d60b9cd0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/field/meta-types/input/components/SelectFieldInput.tsx @@ -0,0 +1,38 @@ +import styled from '@emotion/styled'; +import { MenuItem } from 'tsup.ui.index'; + +import { useSelectField } from '@/object-record/field/meta-types/hooks/useSelectField'; +import { FieldInputEvent } from '@/object-record/field/types/FieldInputEvent'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; + +const StyledRelationPickerContainer = styled.div` + left: -1px; + position: absolute; + top: -1px; +`; + +export type SelectFieldInputProps = { + onSubmit?: FieldInputEvent; +}; + +export const SelectFieldInput = ({ onSubmit }: SelectFieldInputProps) => { + const { persistField, fieldDefinition } = useSelectField(); + + return ( + + + + {fieldDefinition.metadata.options.map((option) => { + return ( + onSubmit?.(() => persistField(option.value))} + /> + ); + })} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/field/types/FieldMetadata.ts index 0ff54de2f..e02c9df83 100644 --- a/packages/twenty-front/src/modules/object-record/field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/field/types/FieldMetadata.ts @@ -87,6 +87,7 @@ export type FieldRelationMetadata = { export type FieldSelectMetadata = { objectMetadataNameSingular?: string; fieldName: string; + options: { label: string; color: ThemeColor; value: string }[]; }; export type FieldMetadata = @@ -119,6 +120,6 @@ export type FieldCurrencyValue = { }; export type FieldFullNameValue = { firstName: string; lastName: string }; export type FieldRatingValue = '1' | '2' | '3' | '4' | '5'; -export type FieldSelectValue = { color: ThemeColor; label: string }; +export type FieldSelectValue = string | null; export type FieldRelationValue = EntityForSelect | null; diff --git a/packages/twenty-front/src/modules/object-record/field/types/guards/isFieldSelectValue.ts b/packages/twenty-front/src/modules/object-record/field/types/guards/isFieldSelectValue.ts index 6ff57f649..b3b256228 100644 --- a/packages/twenty-front/src/modules/object-record/field/types/guards/isFieldSelectValue.ts +++ b/packages/twenty-front/src/modules/object-record/field/types/guards/isFieldSelectValue.ts @@ -1,13 +1,7 @@ -import { z } from 'zod'; +import { isString } from '@sniptt/guards'; -import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema'; - -const selectValueSchema = z.object({ - color: themeColorSchema, - label: z.string(), -}); +import { FieldSelectValue } from '@/object-record/field/types/FieldMetadata'; export const isFieldSelectValue = ( fieldValue: unknown, -): fieldValue is z.infer => - selectValueSchema.safeParse(fieldValue).success; +): fieldValue is FieldSelectValue => isString(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts index 8a83c7573..0ef057221 100644 --- a/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/field/utils/isFieldValueEmpty.ts @@ -14,6 +14,7 @@ import { isFieldRating } from '@/object-record/field/types/guards/isFieldRating' import { isFieldRelation } from '@/object-record/field/types/guards/isFieldRelation'; import { isFieldRelationValue } from '@/object-record/field/types/guards/isFieldRelationValue'; import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect'; +import { isFieldSelectValue } from '@/object-record/field/types/guards/isFieldSelectValue'; import { isFieldText } from '@/object-record/field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid'; import { assertNotNull } from '~/utils/assert'; @@ -34,8 +35,7 @@ export const isFieldValueEmpty = ({ isFieldNumber(fieldDefinition) || isFieldRating(fieldDefinition) || isFieldEmail(fieldDefinition) || - isFieldBoolean(fieldDefinition) || - isFieldSelect(fieldDefinition) + isFieldBoolean(fieldDefinition) //|| isFieldPhone(fieldDefinition) ) { return isValueEmpty(fieldValue); @@ -45,6 +45,10 @@ export const isFieldValueEmpty = ({ return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue); } + if (isFieldSelect(fieldDefinition)) { + return isFieldSelectValue(fieldValue) && !assertNotNull(fieldValue); + } + if (isFieldCurrency(fieldDefinition)) { return ( !isFieldCurrencyValue(fieldValue) || diff --git a/packages/twenty-server/src/core/client-config/client-config.entity.ts b/packages/twenty-server/src/core/client-config/client-config.entity.ts index dc56ea37a..c4fd3a836 100644 --- a/packages/twenty-server/src/core/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/core/client-config/client-config.entity.ts @@ -41,7 +41,7 @@ class Support { @ObjectType() class Sentry { - @Field(() => String) + @Field(() => String, { nullable: true }) dsn: string | undefined; }