feat: add Money field type in settings (#2405)

Closes #2346
This commit is contained in:
Thaïs
2023-11-09 17:13:34 +01:00
committed by GitHub
parent c8eda61704
commit 0d4949484c
12 changed files with 103 additions and 50 deletions

View File

@ -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, unknown> = {
[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<ObjectMetadataItemIdentifier, 'objectNamePlural'>) => {
@ -21,10 +36,17 @@ export const useCreateOneObject = ({
const [mutate] = useMutation(createOneMutation);
const createOneObject = foundObjectMetadataItem
? (input: Record<string, any>) => {
? (input: Record<string, unknown> = {}) => {
return mutate({
variables: {
input: {
...foundObjectMetadataItem.fields.reduce(
(result, field) => ({
...result,
[field.name]: defaultFieldValues[field.type],
}),
{},
),
...input,
},
},

View File

@ -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,

View File

@ -64,7 +64,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
}),
)}
/>
{['BOOLEAN', 'NUMBER', 'TEXT'].includes(fieldType) && (
{['BOOLEAN', 'MONEY', 'NUMBER', 'TEXT'].includes(fieldType) && (
<StyledSettingsObjectFieldTypeCard
preview={
<SettingsObjectFieldPreview

View File

@ -24,14 +24,6 @@ type Story = StoryObj<typeof SettingsObjectFieldPreview>;
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,

View File

@ -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 },
};

View File

@ -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 (

View File

@ -1,5 +1,6 @@
export type MetadataFieldDataType =
| 'BOOLEAN'
| 'MONEY'
| 'NUMBER'
| 'RELATION'
| 'TEXT'

View File

@ -29,6 +29,7 @@ export {
IconChevronsRight,
IconChevronUp,
IconCircleDot,
IconCoins,
IconColorSwatch,
IconMessageCircle as IconComment,
IconCopy,

View File

@ -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,

View File

@ -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}}`,
);
};
},
});

View File

@ -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;

View File

@ -16,6 +16,7 @@ export const moneyObjectDefinition = {
name: 'amount',
label: 'Amount',
targetColumnMap: { value: 'amount' },
isNullable: true,
},
{
id: 'currency',