diff --git a/front/src/modules/metadata/hooks/useCreateOneObject.ts b/front/src/modules/metadata/hooks/useCreateOneObject.ts index 9eb0aba3fb..5d2f9e1617 100644 --- a/front/src/modules/metadata/hooks/useCreateOneObject.ts +++ b/front/src/modules/metadata/hooks/useCreateOneObject.ts @@ -1,10 +1,25 @@ import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; +import { Currency, FieldMetadataType } from '~/generated-metadata/graphql'; + import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; import { useFindOneObjectMetadataItem } from './useFindOneObjectMetadataItem'; +const defaultFieldValues: Record = { + [FieldMetadataType.Money]: { amount: null, currency: Currency.Usd }, + [FieldMetadataType.Boolean]: false, + [FieldMetadataType.Date]: null, + [FieldMetadataType.Email]: '', + [FieldMetadataType.Enum]: null, + [FieldMetadataType.Number]: null, + [FieldMetadataType.Phone]: '', + [FieldMetadataType.Text]: '', + [FieldMetadataType.Url]: { link: '', text: '' }, + [FieldMetadataType.Uuid]: '', +}; + export const useCreateOneObject = ({ objectNamePlural, }: Pick) => { @@ -21,10 +36,17 @@ export const useCreateOneObject = ({ const [mutate] = useMutation(createOneMutation); const createOneObject = foundObjectMetadataItem - ? (input: Record) => { + ? (input: Record = {}) => { return mutate({ variables: { input: { + ...foundObjectMetadataItem.fields.reduce( + (result, field) => ({ + ...result, + [field.name]: defaultFieldValues[field.type], + }), + {}, + ), ...input, }, }, diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx index dcd6073954..ae8ab2b845 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx @@ -4,12 +4,14 @@ import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; import { useFindManyObjects } from '@/metadata/hooks/useFindManyObjects'; +import { parseFieldType } from '@/metadata/utils/parseFieldType'; import { Tag } from '@/ui/display/tag/components/Tag'; import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay'; import { FieldContext } from '@/ui/object/field/contexts/FieldContext'; import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput'; import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector'; +import { FieldMetadataType } from '~/generated/graphql'; import { assertNotNull } from '~/utils/assert'; import { dataTypes } from '../constants/dataTypes'; @@ -137,7 +139,7 @@ export const SettingsObjectFieldPreview = ({ value={{ entityId: objects[0]?.id ?? objectNamePlural, fieldDefinition: { - type: fieldType, + type: parseFieldType(fieldType as FieldMetadataType), Icon: FieldIcon, fieldId: '', label: fieldLabel, diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index aae2657702..b8c5e0619f 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -64,7 +64,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ }), )} /> - {['BOOLEAN', 'NUMBER', 'TEXT'].includes(fieldType) && ( + {['BOOLEAN', 'MONEY', 'NUMBER', 'TEXT'].includes(fieldType) && ( ; export const Text: Story = {}; -export const Number: Story = { - args: { - fieldIconKey: 'IconUsers', - fieldLabel: 'Employees', - fieldType: 'NUMBER', - }, -}; - export const Boolean: Story = { args: { fieldIconKey: 'IconHeadphones', @@ -40,6 +32,22 @@ export const Boolean: Story = { }, }; +export const Currency: Story = { + args: { + fieldIconKey: 'IconCurrencyDollar', + fieldLabel: 'Amount', + fieldType: 'MONEY', + }, +}; + +export const Number: Story = { + args: { + fieldIconKey: 'IconUsers', + fieldLabel: 'Employees', + fieldType: 'NUMBER', + }, +}; + export const CustomObject: Story = { args: { isObjectCustom: true, diff --git a/front/src/modules/settings/data-model/constants/dataTypes.ts b/front/src/modules/settings/data-model/constants/dataTypes.ts index 08aa52e295..1e25259a8f 100644 --- a/front/src/modules/settings/data-model/constants/dataTypes.ts +++ b/front/src/modules/settings/data-model/constants/dataTypes.ts @@ -1,11 +1,13 @@ import { IconCheck, + IconCoins, IconLink, IconNumbers, IconPlug, IconTextSize, } from '@/ui/display/icon'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { Currency } from '~/generated-metadata/graphql'; import { MetadataFieldDataType } from '../types/ObjectFieldDataType'; @@ -13,7 +15,14 @@ export const dataTypes: Record< MetadataFieldDataType, { label: string; Icon: IconComponent; defaultValue?: unknown } > = { + BOOLEAN: { label: 'True/False', Icon: IconCheck, defaultValue: true }, + MONEY: { + label: 'Currency', + Icon: IconCoins, + defaultValue: { amount: 2000, currency: Currency.Usd }, + }, NUMBER: { label: 'Number', Icon: IconNumbers, defaultValue: 2000 }, + RELATION: { label: 'Relation', Icon: IconPlug }, TEXT: { label: 'Text', Icon: IconTextSize, @@ -21,6 +30,4 @@ export const dataTypes: Record< 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', }, URL: { label: 'Link', Icon: IconLink }, - BOOLEAN: { label: 'True/False', Icon: IconCheck, defaultValue: true }, - RELATION: { label: 'Relation', Icon: IconPlug }, }; diff --git a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 27a4059f09..b94748e710 100644 --- a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -7,6 +7,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { Field } from '~/generated-metadata/graphql'; +import { dataTypes } from '../../constants/dataTypes'; import { MetadataFieldDataType } from '../../types/ObjectFieldDataType'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; @@ -30,6 +31,9 @@ const StyledIconTableCell = styled(TableCell)` padding-right: ${({ theme }) => theme.spacing(1)}; `; +// TODO: remove "relation" type for now, add it back when the backend is ready. +const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes; + export const SettingsObjectFieldItemTableRow = ({ ActionIcon, fieldItem, @@ -38,15 +42,12 @@ export const SettingsObjectFieldItemTableRow = ({ const { Icon } = useLazyLoadIcon(fieldItem.icon ?? ''); // TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example) - const fieldDataTypeIsSupported = [ - 'TEXT', - 'NUMBER', - 'BOOLEAN', - 'URL', - ].includes(fieldItem.type); + const fieldDataTypeIsSupported = Object.keys( + dataTypesWithoutRelation, + ).includes(fieldItem.type); if (!fieldDataTypeIsSupported) { - return <>; + return null; } return ( diff --git a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts index a40052a4a9..177bd60bfd 100644 --- a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts +++ b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts @@ -1,5 +1,6 @@ export type MetadataFieldDataType = | 'BOOLEAN' + | 'MONEY' | 'NUMBER' | 'RELATION' | 'TEXT' diff --git a/front/src/modules/ui/display/icon/index.ts b/front/src/modules/ui/display/icon/index.ts index 903be78d72..88d12b9385 100644 --- a/front/src/modules/ui/display/icon/index.ts +++ b/front/src/modules/ui/display/icon/index.ts @@ -29,6 +29,7 @@ export { IconChevronsRight, IconChevronUp, IconCircleDot, + IconCoins, IconColorSwatch, IconMessageCircle as IconComment, IconCopy, diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts index dc8675517e..8c8471d869 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts @@ -6,6 +6,7 @@ import { useFieldInitialValue } from '../../hooks/useFieldInitialValue'; import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldText } from '../../types/guards/isFieldText'; +import { isFieldTextValue } from '../../types/guards/isFieldTextValue'; export const useTextField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); @@ -20,16 +21,17 @@ export const useTextField = () => { fieldName: fieldName, }), ); + const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : ''; const fieldInitialValue = useFieldInitialValue(); const initialValue = fieldInitialValue?.isEmpty ? '' - : fieldInitialValue?.value ?? fieldValue; + : fieldInitialValue?.value ?? fieldTextValue; return { fieldDefinition, - fieldValue, + fieldValue: fieldTextValue, initialValue, setFieldValue, hotkeyScope, diff --git a/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts b/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts index eb49da6ce3..bf672dd9a7 100644 --- a/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts +++ b/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts @@ -1,5 +1,7 @@ import { selectorFamily } from 'recoil'; +import { assertNotNull } from '~/utils/assert'; + import { FieldDefinition } from '../../types/FieldDefinition'; import { FieldMetadata } from '../../types/FieldMetadata'; import { isFieldBoolean } from '../../types/guards/isFieldBoolean'; @@ -8,6 +10,8 @@ import { isFieldDate } from '../../types/guards/isFieldDate'; import { isFieldDoubleTextChip } from '../../types/guards/isFieldDoubleTextChip'; import { isFieldEmail } from '../../types/guards/isFieldEmail'; import { isFieldMoney } from '../../types/guards/isFieldMoney'; +import { isFieldMoneyAmountV2 } from '../../types/guards/isFieldMoneyAmountV2'; +import { isFieldMoneyAmountV2Value } from '../../types/guards/isFieldMoneyAmountV2Value'; import { isFieldNumber } from '../../types/guards/isFieldNumber'; import { isFieldPhone } from '../../types/guards/isFieldPhone'; import { isFieldProbability } from '../../types/guards/isFieldProbability'; @@ -17,6 +21,8 @@ import { isFieldText } from '../../types/guards/isFieldText'; import { isFieldURL } from '../../types/guards/isFieldURL'; import { entityFieldsFamilyState } from '../entityFieldsFamilyState'; +const isValueEmpty = (value: unknown) => !assertNotNull(value) || value === ''; + export const isEntityFieldEmptyFamilySelector = selectorFamily({ key: 'isEntityFieldEmptyFamilySelector', get: ({ @@ -44,32 +50,30 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({ const fieldName = fieldDefinition.metadata.fieldName; const fieldValue = get(entityFieldsFamilyState(entityId))?.[ fieldName - ] as string | null; + ] as string | number | boolean | null; - return ( - fieldValue === null || fieldValue === undefined || fieldValue === '' - ); - } else if (isFieldRelation(fieldDefinition)) { + return isValueEmpty(fieldValue); + } + + if (isFieldRelation(fieldDefinition)) { const fieldName = fieldDefinition.metadata.fieldName; const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; - if (isFieldRelationValue(fieldValue)) { - return fieldValue === null || fieldValue === undefined; - } - } else if (isFieldChip(fieldDefinition)) { + return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue); + } + + if (isFieldChip(fieldDefinition)) { const contentFieldName = fieldDefinition.metadata.contentFieldName; const contentFieldValue = get(entityFieldsFamilyState(entityId))?.[ contentFieldName ] as string | null; - return ( - contentFieldValue === null || - contentFieldValue === undefined || - contentFieldValue === '' - ); - } else if (isFieldDoubleTextChip(fieldDefinition)) { + return isValueEmpty(contentFieldValue); + } + + if (isFieldDoubleTextChip(fieldDefinition)) { const firstValueFieldName = fieldDefinition.metadata.firstValueFieldName; @@ -85,20 +89,24 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({ )?.[secondValueFieldName] as string | null; return ( - (contentFieldFirstValue === null || - contentFieldFirstValue === undefined || - contentFieldFirstValue === '') && - (contentFieldSecondValue === null || - contentFieldSecondValue === undefined || - contentFieldSecondValue === '') - ); - } else { - throw new Error( - `Entity field type not supported in isEntityFieldEmptyFamilySelector : ${fieldDefinition.type}}`, + isValueEmpty(contentFieldFirstValue) && + isValueEmpty(contentFieldSecondValue) ); } - return false; + if (isFieldMoneyAmountV2(fieldDefinition)) { + const fieldName = fieldDefinition.metadata.fieldName; + const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName]; + + return ( + !isFieldMoneyAmountV2Value(fieldValue) || + isValueEmpty(fieldValue?.amount) + ); + } + + throw new Error( + `Entity field type not supported in isEntityFieldEmptyFamilySelector : ${fieldDefinition.type}}`, + ); }; }, }); diff --git a/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2Value.ts b/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2Value.ts index 66cad2cfa2..78476f925e 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2Value.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2Value.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { FieldMoneyValue } from '../FieldMetadata'; +import { FieldMoneyAmountV2Value } from '../FieldMetadata'; const moneyAmountV2Schema = z.object({ currency: z.string(), @@ -9,5 +9,5 @@ const moneyAmountV2Schema = z.object({ export const isFieldMoneyAmountV2Value = ( fieldValue: unknown, -): fieldValue is FieldMoneyValue => +): fieldValue is FieldMoneyAmountV2Value => moneyAmountV2Schema.safeParse(fieldValue).success; diff --git a/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts b/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts index b27a7a1363..86975033b9 100644 --- a/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts +++ b/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts @@ -16,6 +16,7 @@ export const moneyObjectDefinition = { name: 'amount', label: 'Amount', targetColumnMap: { value: 'amount' }, + isNullable: true, }, { id: 'currency',