From 0d4949484c83ffa0687709e25be63650571af156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Thu, 9 Nov 2023 17:13:34 +0100 Subject: [PATCH] feat: add Money field type in settings (#2405) Closes #2346 --- .../metadata/hooks/useCreateOneObject.ts | 24 +++++++- .../components/SettingsObjectFieldPreview.tsx | 4 +- .../SettingsObjectFieldTypeSelectSection.tsx | 2 +- .../SettingsObjectFieldPreview.stories.tsx | 24 +++++--- .../data-model/constants/dataTypes.ts | 11 +++- .../SettingsObjectFieldItemTableRow.tsx | 15 ++--- .../data-model/types/ObjectFieldDataType.ts | 1 + front/src/modules/ui/display/icon/index.ts | 1 + .../field/meta-types/hooks/useTextField.ts | 6 +- .../isEntityFieldEmptyFamilySelector.ts | 60 +++++++++++-------- .../types/guards/isFieldMoneyAmountV2Value.ts | 4 +- .../money.object-definition.ts | 1 + 12 files changed, 103 insertions(+), 50 deletions(-) diff --git a/front/src/modules/metadata/hooks/useCreateOneObject.ts b/front/src/modules/metadata/hooks/useCreateOneObject.ts index 9eb0aba3f..5d2f9e161 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 dcd607395..ae8ab2b84 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 aae265770..b8c5e0619 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 08aa52e29..1e25259a8 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 27a4059f0..b94748e71 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 a40052a4a..177bd60bf 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 903be78d7..88d12b938 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 dc8675517..8c8471d86 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 eb49da6ce..bf672dd9a 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 66cad2cfa..78476f925 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 b27a7a136..86975033b 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',