Refactor types to remove unused types and add FullNameFieldInput (#2590)

This commit is contained in:
Charles Bochet
2023-11-20 13:40:22 +01:00
committed by GitHub
parent eb64baa62e
commit b6665f880d
42 changed files with 101 additions and 1846 deletions

View File

@ -9,29 +9,19 @@ import { isFieldLink } from '@/ui/object/field/types/guards/isFieldLink';
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { FieldContext } from '../contexts/FieldContext';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { EnumFieldDisplay } from '../meta-types/display/components/EnumFieldDisplay';
import { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
import { URLFieldDisplay } from '../meta-types/display/components/URLFieldDisplay';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldEnum } from '../types/guards/isFieldEnum';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
export const FieldDisplay = () => {
const { fieldDefinition } = useContext(FieldContext);
@ -50,10 +40,6 @@ export const FieldDisplay = () => {
<DateFieldDisplay />
) : isFieldNumber(fieldDefinition) ? (
<NumberFieldDisplay />
) : isFieldMoney(fieldDefinition) ? (
<MoneyFieldDisplay />
) : isFieldURL(fieldDefinition) ? (
<URLFieldDisplay />
) : isFieldLink(fieldDefinition) ? (
<LinkFieldDisplay />
) : isFieldCurrency(fieldDefinition) ? (
@ -62,12 +48,6 @@ export const FieldDisplay = () => {
<FullNameFieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldDisplay />
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldDisplay />
) : isFieldEnum(fieldDefinition) ? (
<EnumFieldDisplay />
) : (
<></>
)}

View File

@ -1,37 +1,31 @@
import { useContext } from 'react';
import { FullNameFieldInput } from '@/ui/object/field/meta-types/input/components/FullNameFieldInput';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
import { ChipFieldInput } from '../meta-types/input/components/ChipFieldInput';
import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput';
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
import { DoubleTextChipFieldInput } from '../meta-types/input/components/DoubleTextChipFieldInput';
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
import { MoneyFieldInput } from '../meta-types/input/components/MoneyFieldInput';
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
import { ProbabilityFieldInput } from '../meta-types/input/components/ProbabilityFieldInput';
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
import { URLFieldInput } from '../meta-types/input/components/URLFieldInput';
import { FieldInputEvent } from '../types/FieldInputEvent';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
type FieldInputProps = {
onSubmit?: FieldInputEvent;
@ -76,6 +70,14 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDate(fieldDefinition) ? (
<DateFieldInput
onEnter={onEnter}
@ -92,14 +94,6 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldURL(fieldDefinition) ? (
<URLFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldLink(fieldDefinition) ? (
<LinkFieldInput
onEnter={onEnter}
@ -128,30 +122,6 @@ export const FieldInput = ({
<BooleanFieldInput onSubmit={onSubmit} />
) : isFieldProbability(fieldDefinition) ? (
<ProbabilityFieldInput onSubmit={onSubmit} />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldMoney(fieldDefinition) ? (
<MoneyFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : (
<></>
)}

View File

@ -5,9 +5,9 @@ import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldURL } from '../types/guards/isFieldURL';
export const useGetButtonIcon = (): IconComponent | undefined => {
const { fieldDefinition } = useContext(FieldContext);
@ -15,7 +15,7 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
if (!fieldDefinition) return undefined;
if (
isFieldURL(fieldDefinition) ||
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition) ||
isFieldRelation(fieldDefinition)

View File

@ -11,8 +11,8 @@ export const useIsFieldEmpty = () => {
isEntityFieldEmptyFamilySelector({
fieldDefinition: {
type: fieldDefinition.type,
metadata: { ...fieldDefinition.metadata, mainIdentifierMapper: null },
},
fieldName: fieldDefinition.metadata.fieldName,
entityId,
}),
);

View File

@ -1,26 +1,21 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/ui/object/field/types/guards/isFieldFullNameValue';
import { FieldContext } from '../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../states/selectors/entityFieldsFamilySelector';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldChipValue } from '../types/guards/isFieldChipValue';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '../types/guards/isFieldCurrencyValue';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateValue } from '../types/guards/isFieldDateValue';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldDoubleTextChipValue } from '../types/guards/isFieldDoubleTextChipValue';
import { isFieldDoubleTextValue } from '../types/guards/isFieldDoubleTextValue';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldEmailValue } from '../types/guards/isFieldEmailValue';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldLinkValue } from '../types/guards/isFieldLinkValue';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldMoneyValue } from '../types/guards/isFieldMoneyValue';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldNumberValue } from '../types/guards/isFieldNumberValue';
import { isFieldPhone } from '../types/guards/isFieldPhone';
@ -31,8 +26,6 @@ import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
import { isFieldURL } from '../types/guards/isFieldURL';
import { isFieldURLValue } from '../types/guards/isFieldURLValue';
export const usePersistField = () => {
const {
@ -50,17 +43,6 @@ export const usePersistField = () => {
isFieldRelation(fieldDefinition) &&
isFieldRelationValue(valueToPersist);
const fieldIsChip =
isFieldChip(fieldDefinition) && isFieldChipValue(valueToPersist);
const fieldIsDoubleText =
isFieldDoubleText(fieldDefinition) &&
isFieldDoubleTextValue(valueToPersist);
const fieldIsDoubleTextChip =
isFieldDoubleTextChip(fieldDefinition) &&
isFieldDoubleTextChipValue(valueToPersist);
const fieldIsText =
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
@ -70,9 +52,6 @@ export const usePersistField = () => {
const fieldIsDate =
isFieldDate(fieldDefinition) && isFieldDateValue(valueToPersist);
const fieldIsURL =
isFieldURL(fieldDefinition) && isFieldURLValue(valueToPersist);
const fieldIsLink =
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
@ -87,13 +66,14 @@ export const usePersistField = () => {
const fieldIsNumber =
isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist);
const fieldIsMoney =
isFieldMoney(fieldDefinition) && isFieldMoneyValue(valueToPersist);
const fieldIsCurrency =
isFieldCurrency(fieldDefinition) &&
isFieldCurrencyValue(valueToPersist);
const fieldIsFullName =
isFieldFullName(fieldDefinition) &&
isFieldFullNameValue(valueToPersist);
const fieldIsPhone =
isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist);
@ -115,62 +95,17 @@ export const usePersistField = () => {
},
},
});
} else if (fieldIsChip) {
const fieldName = fieldDefinition.metadata.contentFieldName;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity?.({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist,
},
},
});
} else if (fieldIsDoubleText || fieldIsDoubleTextChip) {
set(
entityFieldsFamilySelector({
entityId,
fieldName: fieldDefinition.metadata.firstValueFieldName,
}),
valueToPersist.firstValue,
);
set(
entityFieldsFamilySelector({
entityId,
fieldName: fieldDefinition.metadata.secondValueFieldName,
}),
valueToPersist.secondValue,
);
updateEntity?.({
variables: {
where: { id: entityId },
data: {
[fieldDefinition.metadata.firstValueFieldName]:
valueToPersist.firstValue,
[fieldDefinition.metadata.secondValueFieldName]:
valueToPersist.secondValue,
},
},
});
} else if (
fieldIsText ||
fieldIsBoolean ||
fieldIsURL ||
fieldIsEmail ||
fieldIsProbability ||
fieldIsNumber ||
fieldIsMoney ||
fieldIsDate ||
fieldIsPhone ||
fieldIsLink ||
fieldIsCurrency
fieldIsCurrency ||
fieldIsFullName
) {
const fieldName = fieldDefinition.metadata.fieldName;

View File

@ -40,7 +40,7 @@ export const useToggleEditOnlyInput = () => {
});
} else {
throw new Error(
`Invalid value to toggle for type : ${fieldDefinition.type}, type may not be implemented in useToggleEditOnlyInput.`,
`Invalid value to toggle for type : ${fieldDefinition}, type may not be implemented in useToggleEditOnlyInput.`,
);
}
},

View File

@ -1,14 +1,10 @@
import { useChipField } from '../../hooks/useChipField';
import { ChipDisplay } from '../content-display/components/ChipDisplay';
export const ChipFieldDisplay = () => {
const { avatarFieldValue, contentFieldValue, entityId } = useChipField();
return (
<ChipDisplay
displayName={contentFieldValue}
avatarUrlValue={avatarFieldValue}
entityId={entityId}
/>
);
// const { avatarFieldValue, contentFieldValue, entityId } = useChipField();
// return (
// <ChipDisplay
// displayName={contentFieldValue}
// avatarUrlValue={avatarFieldValue}
// entityId={entityId}
// />
// );
};

View File

@ -1,17 +1,11 @@
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
import { ChipDisplay } from '../content-display/components/ChipDisplay';
export const DoubleTextChipFieldDisplay = () => {
const { avatarUrl, firstValue, secondValue, entityId } =
useDoubleTextChipField();
const content = [firstValue, secondValue].filter(Boolean).join(' ');
return (
<ChipDisplay
displayName={content}
avatarUrlValue={avatarUrl}
entityId={entityId}
/>
);
// const {} = useFullNameField();
// const content = [firstValue, secondValue].filter(Boolean).join(' ');
// return (
// <ChipDisplay
// displayName={content}
// avatarUrlValue={avatarUrl}
// entityId={entityId}
// />
// );
};

View File

@ -1,9 +0,0 @@
import { Tag } from '@/ui/display/tag/components/Tag';
import { useEnumField } from '../../hooks/useEnumField';
export const EnumFieldDisplay = () => {
const { fieldValue } = useEnumField();
return <Tag color={fieldValue.color} text={fieldValue.text} />;
};

View File

@ -1,8 +0,0 @@
import { useMoneyField } from '../../hooks/useMoneyField';
import { MoneyDisplay } from '../content-display/components/MoneyDisplay';
export const MoneyFieldDisplay = () => {
const { fieldValue } = useMoneyField();
return <MoneyDisplay value={fieldValue} />;
};

View File

@ -1,9 +0,0 @@
import { URLDisplay } from '@/ui/object/field/meta-types/display/content-display/components/URLDisplay';
import { useURLField } from '../../hooks/useURLField';
export const URLFieldDisplay = () => {
const { fieldValue } = useURLField();
return <URLDisplay value={fieldValue} />;
};

View File

@ -1,57 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { FieldEnumValue } from '../../../../types/FieldMetadata';
import { useEnumField } from '../../../hooks/useEnumField';
import { EnumFieldDisplay } from '../EnumFieldDisplay';
const EnumFieldValueSetterEffect = ({ value }: { value: FieldEnumValue }) => {
const { setFieldValue } = useEnumField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/EnumFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isMainIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'enum',
label: 'Enum',
iconName: 'IconTag',
type: 'ENUM',
metadata: {
fieldName: 'Enum',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<EnumFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: EnumFieldDisplay,
args: {
value: { color: 'purple', text: 'Lorem ipsum' },
},
};
export default meta;
type Story = StoryObj<typeof EnumFieldDisplay>;
export const Default: Story = {};

View File

@ -1,68 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useMoneyField } from '../../../hooks/useMoneyField';
import { MoneyFieldDisplay } from '../MoneyFieldDisplay';
const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useMoneyField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/MoneyFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isMainIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'money',
label: 'Money',
type: 'MONEY_AMOUNT',
iconName: 'Icon123',
metadata: {
fieldName: 'Amount',
placeHolder: 'Amount',
isPositive: true,
},
},
hotkeyScope: 'hotkey-scope',
useUpdateEntityMutation: () => [() => undefined, undefined],
}}
>
<MoneyFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: MoneyFieldDisplay,
args: {
value: 100,
},
};
export default meta;
type Story = StoryObj<typeof MoneyFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
args: {
value: 1e100,
},
parameters: {
container: { width: 100 },
},
};

View File

@ -1,66 +0,0 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useURLField } from '../../../hooks/useURLField';
import { URLFieldDisplay } from '../URLFieldDisplay';
const URLFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useURLField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/URLFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isMainIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'URL',
label: 'URL',
type: 'URL',
iconName: 'IconLink',
metadata: {
fieldName: 'URL',
placeHolder: 'URL',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<MemoryRouter>
<URLFieldValueSetterEffect value={args.value} />
<Story />
</MemoryRouter>
</FieldContext.Provider>
),
ComponentDecorator,
],
component: URLFieldDisplay,
args: {
value: 'https://github.com/twentyhq',
},
};
export default meta;
type Story = StoryObj<typeof URLFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 200 },
},
};

View File

@ -1,55 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldChip } from '../../types/guards/isFieldChip';
export const useChipField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('CHIP', isFieldChip, fieldDefinition);
const contentFieldName = fieldDefinition.metadata.contentFieldName;
const avatarUrlFieldName = fieldDefinition.metadata.urlFieldName;
const [contentFieldValue, setContentFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: contentFieldName,
}),
);
const [avatarFieldValue, setAvatarFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: avatarUrlFieldName,
}),
);
const fieldInitialValue = useFieldInitialValue();
const initialContentValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? contentFieldValue;
const initialAvatarValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value
? ''
: avatarFieldValue;
return {
fieldDefinition,
contentFieldValue,
initialContentValue,
setContentFieldValue,
avatarFieldValue,
initialAvatarValue,
setAvatarFieldValue,
entityId,
hotkeyScope,
};
};

View File

@ -1,75 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldDoubleTextChip } from '../../types/guards/isFieldDoubleTextChip';
export const useDoubleTextChipField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(
'DOUBLE_TEXT_CHIP',
isFieldDoubleTextChip,
fieldDefinition,
);
const [firstValue, setFirstValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldDefinition.metadata.firstValueFieldName,
}),
);
const [secondValue, setSecondValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldDefinition.metadata.secondValueFieldName,
}),
);
const [avatarUrl, setAvatarUrl] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldDefinition.metadata.avatarUrlFieldName,
}),
);
const fullValue = [firstValue, secondValue].filter(Boolean).join(' ');
const fieldInitialValue = useFieldInitialValue();
const initialFirstValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? firstValue;
const initialSecondValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value
? ''
: secondValue;
const initialAvatarUrl = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value
? ''
: avatarUrl;
return {
fieldDefinition,
avatarUrl,
setAvatarUrl,
secondValue,
setSecondValue,
firstValue,
setFirstValue,
fullValue,
entityId,
hotkeyScope,
initialAvatarUrl,
initialFirstValue,
initialSecondValue,
};
};

View File

@ -1,47 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldEnumValue } from '@/ui/object/field/types/FieldMetadata';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldEnum } from '../../types/guards/isFieldEnum';
import { isFieldEnumValue } from '../../types/guards/isFieldEnumValue';
export const useEnumField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('ENUM', isFieldEnum, fieldDefinition);
const { fieldName } = fieldDefinition.metadata;
const [fieldValue, setFieldValue] = useRecoilState<FieldEnumValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldEnumValue = isFieldEnumValue(fieldValue)
? fieldValue
: { color: 'green' as ThemeColor, text: '' };
const fieldInitialValue = useFieldInitialValue();
const initialValue = {
color: 'green' as ThemeColor,
text: fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldEnumValue?.text ?? '',
};
return {
fieldDefinition,
fieldValue: fieldEnumValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};

View File

@ -1,58 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldMoney } from '../../types/guards/isFieldMoney';
export const useMoneyField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('MONEY_AMOUNT', isFieldMoney, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistMoneyField = (newValue: string) => {
if (!canBeCastAsIntegerOrNull(newValue)) {
return;
}
const castedValue = castAsIntegerOrNull(newValue);
persistField(castedValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? null
: !isNaN(Number(fieldInitialValue?.value))
? Number(fieldInitialValue?.value)
: null ?? fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistMoneyField,
};
};

View File

@ -1,53 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { isURL } from '~/utils/is-url';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldURL } from '../../types/guards/isFieldURL';
import { isFieldURLValue } from '../../types/guards/isFieldURLValue';
export const useURLField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('URL', isFieldURL, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldUrlValue = isFieldURLValue(fieldValue) ? fieldValue : '';
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldUrlValue;
const persistField = usePersistField();
const persistURLField = (newValue: string) => {
if (!isURL(newValue) && newValue !== '') {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue: fieldUrlValue,
initialValue,
setFieldValue,
hotkeyScope,
persistURLField,
};
};

View File

@ -1,66 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useChipField } from '../../hooks/useChipField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type ChipFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const ChipFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: ChipFieldInputProps) => {
const { fieldDefinition, initialContentValue, hotkeyScope } = useChipField();
const persistField = usePersistField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialContentValue}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -1,13 +1,13 @@
import { useFullNameField } from '@/ui/object/field/meta-types/hooks/useFullNameField';
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type DoubleTextChipFieldInputProps = {
export type FullNameFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
@ -15,52 +15,53 @@ export type DoubleTextChipFieldInputProps = {
onShiftTab?: FieldInputEvent;
};
export const DoubleTextChipFieldInput = ({
export const FullNameFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: DoubleTextChipFieldInputProps) => {
const {
fieldDefinition,
initialFirstValue,
initialSecondValue,
hotkeyScope,
} = useDoubleTextChipField();
}: FullNameFieldInputProps) => {
const { hotkeyScope, initialValue } = useFullNameField();
const persistField = usePersistField();
const convertToFullName = (newDoubleText: FieldDoubleText) => {
return {
firstName: newDoubleText.firstValue,
lastName: newDoubleText.secondValue,
};
};
const handleEnter = (newDoubleText: FieldDoubleText) => {
onEnter?.(() => persistField(newDoubleText));
onEnter?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleEscape = (newDoubleText: FieldDoubleText) => {
onEscape?.(() => persistField(newDoubleText));
onEscape?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() => persistField(newDoubleText));
onClickOutside?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleTab = (newDoubleText: FieldDoubleText) => {
onTab?.(() => persistField(newDoubleText));
onTab?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
onShiftTab?.(() => persistField(newDoubleText));
onShiftTab?.(() => persistField(convertToFullName(newDoubleText)));
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={initialFirstValue}
secondValue={initialSecondValue}
firstValuePlaceholder={fieldDefinition.metadata.firstValuePlaceholder}
secondValuePlaceholder={fieldDefinition.metadata.secondValuePlaceholder}
firstValue={initialValue.firstName}
secondValue={initialValue.lastName}
firstValuePlaceholder={'First name'}
secondValuePlaceholder={'Last name'}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}

View File

@ -1,65 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useMoneyField } from '../../hooks/useMoneyField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
export type FieldInputEvent = (persist: () => void) => void;
export type MoneyFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const MoneyFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: MoneyFieldInputProps) => {
const { fieldDefinition, hotkeyScope, persistMoneyField, initialValue } =
useMoneyField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistMoneyField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistMoneyField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistMoneyField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistMoneyField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistMoneyField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue?.toLocaleString() ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -1,64 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useURLField } from '../../hooks/useURLField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type URLFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const URLFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: URLFieldInputProps) => {
const { fieldDefinition, initialValue, hotkeyScope, persistURLField } =
useURLField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistURLField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistURLField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistURLField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistURLField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistURLField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -1,176 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useChipField } from '../../../hooks/useChipField';
import { ChipFieldInput, ChipFieldInputProps } from '../ChipFieldInput';
const ChipFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setContentFieldValue } = useChipField();
useEffect(() => {
setContentFieldValue(value);
}, [setContentFieldValue, value]);
return <></>;
};
type ChipFieldInputWithContextProps = ChipFieldInputProps & {
value: string;
entityId?: string;
};
const ChipFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: ChipFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'chip',
label: 'Chip',
type: 'CHIP',
iconName: 'Icon123',
metadata: {
contentFieldName: 'name',
urlFieldName: 'xURL',
placeHolder: 'X URL',
},
}}
entityId={entityId}
>
<ChipFieldValueSetterEffect value={value} />
<ChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/ChipFieldInput',
component: ChipFieldInputWithContext,
args: {
value: 'chip',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
parameters: {
clearMocks: true,
},
decorators: [clearMocksDecorator],
};
export default meta;
type Story = StoryObj<typeof ChipFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,194 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDoubleTextChipField } from '../../../hooks/useDoubleTextChipField';
import {
DoubleTextChipFieldInput,
DoubleTextChipFieldInputProps,
} from '../DoubleTextChipFieldInput';
const DoubleTextChipFieldValueSetterEffect = ({
firstValue,
secondValue,
}: {
firstValue: string;
secondValue: string;
}) => {
const { setFirstValue, setSecondValue } = useDoubleTextChipField();
useEffect(() => {
setFirstValue(firstValue);
setSecondValue(secondValue);
}, [firstValue, secondValue, setFirstValue, setSecondValue]);
return <></>;
};
type DoubleTextChipFieldInputWithContextProps =
DoubleTextChipFieldInputProps & {
firstValue: string;
secondValue: string;
entityId?: string;
};
const DoubleTextChipFieldInputWithContext = ({
entityId,
firstValue,
secondValue,
onClickOutside,
onEnter,
onEscape,
onTab,
onShiftTab,
}: DoubleTextChipFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'double-text-chip',
label: 'Double-Text-Chip',
type: 'DOUBLE_TEXT_CHIP',
iconName: 'IconUser',
metadata: {
firstValueFieldName: 'First-text',
firstValuePlaceholder: 'First-text',
secondValueFieldName: 'Second-text',
secondValuePlaceholder: 'Second-text',
avatarUrlFieldName: 'avatarUrl',
fieldName: '',
},
}}
entityId={entityId}
>
<DoubleTextChipFieldValueSetterEffect
{...{ firstValue, secondValue }}
/>
<DoubleTextChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/DoubleTextChipFieldInput',
component: DoubleTextChipFieldInputWithContext,
args: {
firstValue: 'first value',
secondValue: 'second value',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
parameters: {
clearMocks: true,
},
decorators: [clearMocksDecorator],
};
export default meta;
type Story = StoryObj<typeof DoubleTextChipFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,176 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useMoneyField } from '../../../hooks/useMoneyField';
import { MoneyFieldInput, MoneyFieldInputProps } from '../MoneyFieldInput';
const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useMoneyField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type MoneyFieldInputWithContextProps = MoneyFieldInputProps & {
value: number;
entityId?: string;
};
const MoneyFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: MoneyFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'moneyAmount',
label: 'MoneyAmout',
type: 'MONEY_AMOUNT',
iconName: 'Icon123',
metadata: {
fieldName: 'moneyAmount',
placeHolder: 'Enter Amount',
},
}}
entityId={entityId}
>
<MoneyFieldValueSetterEffect value={value} />
<MoneyFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/MoneyFieldInput',
component: MoneyFieldInputWithContext,
args: {
value: 1000,
isPositive: true,
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof MoneyFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,175 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useURLField } from '../../../hooks/useURLField';
import { URLFieldInput, URLFieldInputProps } from '../URLFieldInput';
const URLFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useURLField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type URLFieldInputWithContextProps = URLFieldInputProps & {
value: string;
entityId?: string;
};
const URLFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: URLFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'url',
label: 'URL',
type: 'URL',
iconName: 'IconLink',
metadata: {
fieldName: 'URL',
placeHolder: 'Enter URL',
},
}}
entityId={entityId}
>
<URLFieldValueSetterEffect value={value} />
<URLFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/URLFieldInput',
component: URLFieldInputWithContext,
args: {
value: 'https://username.domain',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof URLFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -8,22 +8,18 @@ import { assertNotNull } from '~/utils/assert';
import { FieldDefinition } from '../../types/FieldDefinition';
import { FieldMetadata } from '../../types/FieldMetadata';
import { isFieldBoolean } from '../../types/guards/isFieldBoolean';
import { isFieldChip } from '../../types/guards/isFieldChip';
import { isFieldCurrency } from '../../types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '../../types/guards/isFieldCurrencyValue';
import { isFieldDate } from '../../types/guards/isFieldDate';
import { isFieldDoubleTextChip } from '../../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../../types/guards/isFieldEmail';
import { isFieldLink } from '../../types/guards/isFieldLink';
import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue';
import { isFieldMoney } from '../../types/guards/isFieldMoney';
import { isFieldNumber } from '../../types/guards/isFieldNumber';
import { isFieldPhone } from '../../types/guards/isFieldPhone';
import { isFieldProbability } from '../../types/guards/isFieldProbability';
import { isFieldRelation } from '../../types/guards/isFieldRelation';
import { isFieldRelationValue } from '../../types/guards/isFieldRelationValue';
import { isFieldText } from '../../types/guards/isFieldText';
import { isFieldURL } from '../../types/guards/isFieldURL';
import { entityFieldsFamilyState } from '../entityFieldsFamilyState';
const isValueEmpty = (value: unknown) => !assertNotNull(value) || value === '';
@ -32,27 +28,24 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
key: 'isEntityFieldEmptyFamilySelector',
get: ({
fieldDefinition,
fieldName,
entityId,
}: {
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type'> & {
metadata: Omit<FieldMetadata, 'mainIdentifierMapper'>;
};
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type'>;
fieldName: string;
entityId: string;
}) => {
return ({ get }) => {
if (
isFieldUuid(fieldDefinition) ||
isFieldText(fieldDefinition) ||
isFieldURL(fieldDefinition) ||
isFieldDate(fieldDefinition) ||
isFieldNumber(fieldDefinition) ||
isFieldProbability(fieldDefinition) ||
isFieldMoney(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldBoolean(fieldDefinition) ||
isFieldPhone(fieldDefinition)
) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[
fieldName
] as string | number | boolean | null;
@ -61,46 +54,12 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
}
if (isFieldRelation(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue);
}
if (isFieldChip(fieldDefinition)) {
const contentFieldName = fieldDefinition.metadata.contentFieldName;
const contentFieldValue = get(entityFieldsFamilyState(entityId))?.[
contentFieldName
] as string | null;
return isValueEmpty(contentFieldValue);
}
if (isFieldDoubleTextChip(fieldDefinition)) {
const firstValueFieldName =
fieldDefinition.metadata.firstValueFieldName;
const secondValueFieldName =
fieldDefinition.metadata.secondValueFieldName;
const contentFieldFirstValue = get(entityFieldsFamilyState(entityId))?.[
firstValueFieldName
] as string | null;
const contentFieldSecondValue = get(
entityFieldsFamilyState(entityId),
)?.[secondValueFieldName] as string | null;
return (
isValueEmpty(contentFieldFirstValue) &&
isValueEmpty(contentFieldSecondValue)
);
}
if (isFieldCurrency(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
return (
@ -110,7 +69,6 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
}
if (isFieldFullName(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
return (
@ -120,7 +78,6 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
}
if (isFieldLink(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url);

View File

@ -1,9 +1,11 @@
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { MainIdentifierMapper } from '@/ui/object/field/types/MainIdentifierMapper';
import { ThemeColor } from '@/ui/theme/constants/colors';
export type FieldUuidMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldBooleanMetadata = {
fieldName: string;
};
@ -12,22 +14,8 @@ export type FieldTextMetadata = {
fieldName: string;
};
export type FieldPhoneMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldURLMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldLinkMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldDateMetadata = {
placeHolder: string;
fieldName: string;
};
@ -37,10 +25,9 @@ export type FieldNumberMetadata = {
isPositive?: boolean;
};
export type FieldMoneyMetadata = {
fieldName: string;
export type FieldLinkMetadata = {
placeHolder: string;
isPositive?: boolean;
fieldName: string;
};
export type FieldCurrencyMetadata = {
@ -49,13 +36,23 @@ export type FieldCurrencyMetadata = {
isPositive?: boolean;
};
export type FieldFullnameMetadata = {
export type FieldFullNameMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldEmailMetadata = {
fieldName: string;
placeHolder: string;
fieldName: string;
};
export type FieldPhoneMetadata = {
placeHolder: string;
fieldName: string;
};
export type FieldProbabilityMetadata = {
fieldName: string;
};
export type FieldDefinitionRelationType =
@ -74,85 +71,31 @@ export type FieldRelationMetadata = {
objectMetadataNamePlural: string;
};
export type FieldChipMetadata = {
contentFieldName: string;
urlFieldName: string;
placeHolder: string;
};
export type FieldDoubleTextMetadata = {
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
secondValuePlaceholder: string;
};
export type FieldDoubleTextChipMetadata = {
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
secondValuePlaceholder: string;
avatarUrlFieldName: string;
};
export type FieldProbabilityMetadata = {
fieldName: string;
};
export type FieldBooleanMetadata = {
fieldName: string;
};
export type FieldEnumMetadata = {
fieldName: string;
};
export type FieldMetadata =
| FieldBooleanMetadata
| FieldChipMetadata
| FieldCurrencyMetadata
| FieldDateMetadata
| FieldDoubleTextChipMetadata
| FieldDoubleTextMetadata
| FieldEmailMetadata
| FieldLinkMetadata
| FieldMoneyMetadata
| FieldNumberMetadata
| FieldPhoneMetadata
| FieldProbabilityMetadata
| FieldEnumMetadata
| FieldRelationMetadata
| FieldDateMetadata
| FieldTextMetadata
| FieldURLMetadata
| FieldUuidMetadata;
| FieldUuidMetadata
| FieldCurrencyMetadata
| FieldLinkMetadata
| FieldPhoneMetadata
| FieldEmailMetadata
| FieldProbabilityMetadata
| FieldRelationMetadata
| FieldFullNameMetadata;
export type FieldTextValue = string;
export type FieldUUidValue = string;
export type FieldChipValue = string;
export type FieldDateValue = string | null;
export type FieldPhoneValue = string;
export type FieldURLValue = string;
export type FieldLinkValue = { url: string; label: string };
export type FieldNumberValue = number | null;
export type FieldMoneyValue = number | null;
export type FieldCurrencyValue = { currencyCode: string; amountMicros: number };
export type FieldFullNameValue = { firstName: string; lastName: string };
export type FieldEmailValue = string;
export type FieldProbabilityValue = number;
export type FieldBooleanValue = boolean;
export type FieldDoubleTextValue = {
firstValue: string;
secondValue: string;
};
export type FieldDoubleTextChipValue = {
firstValue: string;
secondValue: string;
};
export type FieldPhoneValue = string;
export type FieldEmailValue = string;
export type FieldLinkValue = { url: string; label: string };
export type FieldCurrencyValue = { currencyCode: string; amountMicros: number };
export type FieldFullNameValue = { firstName: string; lastName: string };
export type FieldProbabilityValue = number;
export type FieldRelationValue = EntityForSelect | null;
export type FieldEnumValue = { color: ThemeColor; text: string };

View File

@ -1,23 +1,17 @@
import { FieldDefinition } from '../FieldDefinition';
import {
FieldBooleanMetadata,
FieldChipMetadata,
FieldCurrencyMetadata,
FieldDateMetadata,
FieldDoubleTextChipMetadata,
FieldDoubleTextMetadata,
FieldEmailMetadata,
FieldEnumMetadata,
FieldFullnameMetadata,
FieldFullNameMetadata,
FieldLinkMetadata,
FieldMetadata,
FieldMoneyMetadata,
FieldNumberMetadata,
FieldPhoneMetadata,
FieldProbabilityMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
FieldUuidMetadata,
} from '../FieldMetadata';
import { FieldType } from '../FieldType';
@ -26,26 +20,16 @@ type AssertFieldMetadataFunction = <
E extends FieldType,
T extends E extends 'BOOLEAN'
? FieldBooleanMetadata
: E extends 'CHIP'
? FieldChipMetadata
: E extends 'CURRENCY'
? FieldCurrencyMetadata
: E extends 'FULL_NAME'
? FieldFullnameMetadata
? FieldFullNameMetadata
: E extends 'DATE'
? FieldDateMetadata
: E extends 'DOUBLE_TEXT'
? FieldDoubleTextMetadata
: E extends 'DOUBLE_TEXT_CHIP'
? FieldDoubleTextChipMetadata
: E extends 'EMAIL'
? FieldEmailMetadata
: E extends 'LINK'
? FieldLinkMetadata
: E extends 'MONEY_AMOUNT'
? FieldMoneyMetadata
: E extends 'ENUM'
? FieldEnumMetadata
: E extends 'NUMBER'
? FieldNumberMetadata
: E extends 'PHONE'
@ -56,8 +40,6 @@ type AssertFieldMetadataFunction = <
? FieldRelationMetadata
: E extends 'TEXT'
? FieldTextMetadata
: E extends 'URL'
? FieldURLMetadata
: E extends 'UUID'
? FieldUuidMetadata
: never,

View File

@ -1,6 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldChipMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldChip = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldChipMetadata> => field.type === 'CHIP';

View File

@ -1,8 +0,0 @@
import { isString } from '@sniptt/guards';
import { FieldChipValue } from '../FieldMetadata';
// TODO: add zod
export const isFieldChipValue = (
fieldValue: unknown,
): fieldValue is FieldChipValue => isString(fieldValue);

View File

@ -1,7 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldDoubleTextMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldDoubleText = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldDoubleTextMetadata> =>
field.type === 'DOUBLE_TEXT';

View File

@ -1,7 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldDoubleTextChipMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldDoubleTextChip = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldDoubleTextChipMetadata> =>
field.type === 'DOUBLE_TEXT_CHIP';

View File

@ -1,9 +0,0 @@
import { FieldDoubleTextChipValue } from '../FieldMetadata';
import { DoubleTextTypeResolver } from '../resolvers/DoubleTextTypeResolver';
export const isFieldDoubleTextChipValue = (
fieldValue: unknown,
): fieldValue is FieldDoubleTextChipValue =>
fieldValue !== null &&
fieldValue !== undefined &&
DoubleTextTypeResolver.safeParse(fieldValue).success;

View File

@ -1,10 +0,0 @@
import { FieldDoubleTextValue } from '../FieldMetadata';
import { DoubleTextTypeResolver } from '../resolvers/DoubleTextTypeResolver';
// TODO: add zod
export const isFieldDoubleTextValue = (
fieldValue: unknown,
): fieldValue is FieldDoubleTextValue =>
fieldValue !== null &&
fieldValue !== undefined &&
DoubleTextTypeResolver.safeParse(fieldValue).success;

View File

@ -1,6 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldEnumMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldEnum = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldEnumMetadata> => field.type === 'ENUM';

View File

@ -1,7 +1,7 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldCurrencyMetadata, FieldMetadata } from '../FieldMetadata';
import { FieldFullNameMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldFullName = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldCurrencyMetadata> =>
): field is FieldDefinition<FieldFullNameMetadata> =>
field.type === 'FULL_NAME';

View File

@ -1,7 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldMoneyMetadata } from '../FieldMetadata';
export const isFieldMoney = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldMoneyMetadata> =>
field.type === 'MONEY_AMOUNT';

View File

@ -1,8 +0,0 @@
import { isNull, isNumber } from '@sniptt/guards';
import { FieldMoneyValue } from '../FieldMetadata';
// TODO: add zod
export const isFieldMoneyValue = (
fieldValue: unknown,
): fieldValue is FieldMoneyValue => isNull(fieldValue) || isNumber(fieldValue);

View File

@ -1,6 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldURLMetadata } from '../FieldMetadata';
export const isFieldURL = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldURLMetadata> => field.type === 'URL';

View File

@ -1,8 +0,0 @@
import { isString } from '@sniptt/guards';
import { FieldURLValue } from '../FieldMetadata';
// TODO: add zod
export const isFieldURLValue = (
fieldValue: unknown,
): fieldValue is FieldURLValue => isString(fieldValue);