FieldDisplay & FieldInput (#1708)
* Removed view field duplicate types * wip * wip 2 * wip 3 * Unified state for fields * Renaming * Wip * Post merge * Post post merge * wip * Delete unused file * Boolean and Probability * Finished InlineCell * Renamed EditableCell to TableCell * Finished double texts * Finished MoneyField * Fixed bug inline cell click outside * Fixed hotkey scope * Final fixes * Phone * Fix url and number input validation * Fix * Fix position * wip refactor activity editor * Fixed activity editor --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
59
front/src/modules/ui/field/components/FieldDisplay.tsx
Normal file
59
front/src/modules/ui/field/components/FieldDisplay.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
||||
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
|
||||
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
|
||||
import { DoubleTextFieldDisplay } from '../meta-types/display/components/DoubleTextFieldDisplay';
|
||||
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
|
||||
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 { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
|
||||
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
||||
import { URLFieldDisplay } from '../meta-types/display/components/URLFieldDisplay';
|
||||
import { isFieldChip } from '../types/guards/isFieldChip';
|
||||
import { isFieldDate } from '../types/guards/isFieldDate';
|
||||
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
|
||||
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
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);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldRelation(fieldDefinition) ? (
|
||||
<RelationFieldDisplay />
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldDisplay />
|
||||
) : isFieldEmail(fieldDefinition) ? (
|
||||
<EmailFieldDisplay />
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldDisplay />
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldDisplay />
|
||||
) : isFieldMoney(fieldDefinition) ? (
|
||||
<MoneyFieldDisplay />
|
||||
) : isFieldURL(fieldDefinition) ? (
|
||||
<URLFieldDisplay />
|
||||
) : isFieldPhone(fieldDefinition) ? (
|
||||
<PhoneFieldDisplay />
|
||||
) : isFieldChip(fieldDefinition) ? (
|
||||
<ChipFieldDisplay />
|
||||
) : isFieldDoubleTextChip(fieldDefinition) ? (
|
||||
<DoubleTextChipFieldDisplay />
|
||||
) : isFieldDoubleText(fieldDefinition) ? (
|
||||
<DoubleTextFieldDisplay />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
150
front/src/modules/ui/field/components/FieldInput.tsx
Normal file
150
front/src/modules/ui/field/components/FieldInput.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
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 { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
|
||||
import { DoubleTextChipFieldInput } from '../meta-types/input/components/DoubleTextChipFieldInput';
|
||||
import { DoubleTextFieldInput } from '../meta-types/input/components/DoubleTextFieldInput';
|
||||
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
|
||||
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 { isFieldDate } from '../types/guards/isFieldDate';
|
||||
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
|
||||
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
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 OwnProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const FieldInput = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onShiftTab,
|
||||
onTab,
|
||||
onClickOutside,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldRelation(fieldDefinition) ? (
|
||||
<RecoilScope>
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</RecoilScope>
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldEmail(fieldDefinition) ? (
|
||||
<EmailFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldURL(fieldDefinition) ? (
|
||||
<URLFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldPhone(fieldDefinition) ? (
|
||||
<PhoneFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<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}
|
||||
/>
|
||||
) : isFieldDoubleText(fieldDefinition) ? (
|
||||
<DoubleTextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldMoney(fieldDefinition) ? (
|
||||
<MoneyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
front/src/modules/ui/field/contexts/FieldContext.ts
Normal file
17
front/src/modules/ui/field/contexts/FieldContext.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { FieldDefinition } from '../types/FieldDefinition';
|
||||
import { FieldMetadata } from '../types/FieldMetadata';
|
||||
|
||||
type GenericFieldContextType = {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
// TODO: add better typing for mutation hook
|
||||
useUpdateEntityMutation: () => [(params: any) => void, any];
|
||||
entityId: string;
|
||||
recoilScopeId: string;
|
||||
hotkeyScope: string;
|
||||
};
|
||||
|
||||
export const FieldContext = createContext<GenericFieldContextType>(
|
||||
{} as GenericFieldContextType,
|
||||
);
|
||||
23
front/src/modules/ui/field/hooks/useIsFieldEmpty.ts
Normal file
23
front/src/modules/ui/field/hooks/useIsFieldEmpty.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isEntityFieldEmptyFamilySelector } from '../states/selectors/isEntityFieldEmptyFamilySelector';
|
||||
|
||||
export const useIsFieldEmpty = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const isFieldEmpty = useRecoilValue(
|
||||
isEntityFieldEmptyFamilySelector({
|
||||
fieldDefinition: {
|
||||
key: fieldDefinition.key,
|
||||
name: fieldDefinition.name,
|
||||
type: fieldDefinition.type,
|
||||
metadata: fieldDefinition.metadata,
|
||||
},
|
||||
entityId,
|
||||
}),
|
||||
);
|
||||
|
||||
return isFieldEmpty;
|
||||
};
|
||||
15
front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts
Normal file
15
front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldProbability } from '../types/guards/isFieldProbability';
|
||||
|
||||
export const useIsFieldInputOnly = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (isFieldBoolean(fieldDefinition) || isFieldProbability(fieldDefinition)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
184
front/src/modules/ui/field/hooks/usePersistField.ts
Normal file
184
front/src/modules/ui/field/hooks/usePersistField.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
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 { 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 { 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';
|
||||
import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue';
|
||||
import { isFieldProbability } from '../types/guards/isFieldProbability';
|
||||
import { isFieldProbabilityValue } from '../types/guards/isFieldProbabilityValue';
|
||||
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 { entityId, fieldDefinition, useUpdateEntityMutation } =
|
||||
useContext(FieldContext);
|
||||
|
||||
const [updateEntity] = useUpdateEntityMutation();
|
||||
|
||||
const persistField = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(valueToPersist: unknown) => {
|
||||
const fieldIsRelation =
|
||||
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);
|
||||
|
||||
const fieldIsEmail =
|
||||
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);
|
||||
|
||||
const fieldIsDate =
|
||||
isFieldDate(fieldDefinition) && isFieldDateValue(valueToPersist);
|
||||
|
||||
const fieldIsURL =
|
||||
isFieldURL(fieldDefinition) && isFieldURLValue(valueToPersist);
|
||||
|
||||
const fieldIsBoolean =
|
||||
isFieldBoolean(fieldDefinition) &&
|
||||
isFieldBooleanValue(valueToPersist);
|
||||
|
||||
const fieldIsProbability =
|
||||
isFieldProbability(fieldDefinition) &&
|
||||
isFieldProbabilityValue(valueToPersist);
|
||||
|
||||
const fieldIsNumber =
|
||||
isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist);
|
||||
|
||||
const fieldIsMoney =
|
||||
isFieldMoney(fieldDefinition) && isFieldMoneyValue(valueToPersist);
|
||||
|
||||
const fieldIsPhone =
|
||||
isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist);
|
||||
|
||||
if (fieldIsRelation) {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
set(
|
||||
entityFieldsFamilySelector({ entityId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: entityId },
|
||||
data: {
|
||||
[fieldName]: valueToPersist
|
||||
? { connect: { id: valueToPersist.id } }
|
||||
: { disconnect: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
} 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
|
||||
) {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
set(
|
||||
entityFieldsFamilySelector({ entityId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: entityId },
|
||||
data: {
|
||||
[fieldName]: valueToPersist,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid value to persist: ${valueToPersist} for type : ${fieldDefinition.type}, type may not be implemented in usePersistField.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
[entityId, fieldDefinition, updateEntity],
|
||||
);
|
||||
|
||||
return persistField;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { useChipField } from '../../hooks/useChipField';
|
||||
import { ChipDisplay } from '../content-display/components/ChipDisplay';
|
||||
|
||||
export const ChipFieldDisplay = () => {
|
||||
const { avatarFieldValue, contentFieldValue, entityType, entityId } =
|
||||
useChipField();
|
||||
|
||||
return (
|
||||
<ChipDisplay
|
||||
displayName={contentFieldValue}
|
||||
avatarUrlValue={avatarFieldValue}
|
||||
entityType={entityType}
|
||||
entityId={entityId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
|
||||
|
||||
import { useDateField } from '../../hooks/useDateField';
|
||||
|
||||
export const DateFieldDisplay = () => {
|
||||
const { fieldValue } = useDateField();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
|
||||
import { ChipDisplay } from '../content-display/components/ChipDisplay';
|
||||
|
||||
export const DoubleTextChipFieldDisplay = () => {
|
||||
const { avatarUrl, firstValue, secondValue, entityType, entityId } =
|
||||
useDoubleTextChipField();
|
||||
|
||||
const content = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<ChipDisplay
|
||||
displayName={content}
|
||||
avatarUrlValue={avatarUrl}
|
||||
entityType={entityType}
|
||||
entityId={entityId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
|
||||
import { TextDisplay } from '../content-display/components/TextDisplay';
|
||||
|
||||
export const DoubleTextFieldDisplay = () => {
|
||||
const { firstValue, secondValue } = useDoubleTextField();
|
||||
|
||||
const content = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return <TextDisplay text={content} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useEmailField } from '../../hooks/useEmailField';
|
||||
import { EmailDisplay } from '../content-display/components/EmailDisplay';
|
||||
|
||||
export const EmailFieldDisplay = () => {
|
||||
const { fieldValue } = useEmailField();
|
||||
|
||||
return <EmailDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useMoneyField } from '../../hooks/useMoneyField';
|
||||
import { MoneyDisplay } from '../content-display/components/MoneyDisplay';
|
||||
|
||||
export const MoneyFieldDisplay = () => {
|
||||
const { fieldValue } = useMoneyField();
|
||||
|
||||
return <MoneyDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { NumberDisplay } from '@/ui/field/meta-types/display/content-display/components/NumberDisplay';
|
||||
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export const NumberFieldDisplay = () => {
|
||||
const { fieldValue } = useNumberField();
|
||||
|
||||
return <NumberDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { PhoneDisplay } from '@/ui/field/meta-types/display/content-display/components/PhoneDisplay';
|
||||
|
||||
import { usePhoneField } from '../../hooks/usePhoneField';
|
||||
|
||||
export const PhoneFieldDisplay = () => {
|
||||
const { fieldValue } = usePhoneField();
|
||||
|
||||
return <PhoneDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { CompanyChip } from '@/companies/components/CompanyChip';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||
import { UserChip } from '@/users/components/UserChip';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
export const RelationFieldDisplay = () => {
|
||||
const { fieldDefinition, fieldValue } = useRelationField();
|
||||
|
||||
switch (fieldDefinition.metadata.relationType) {
|
||||
case Entity.Person: {
|
||||
return (
|
||||
<PersonChip
|
||||
id={fieldValue?.id ?? ''}
|
||||
name={fieldValue?.displayName ?? ''}
|
||||
pictureUrl={fieldValue?.avatarUrl ?? ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case Entity.User: {
|
||||
return (
|
||||
<UserChip
|
||||
id={fieldValue?.id ?? ''}
|
||||
name={fieldValue?.displayName ?? ''}
|
||||
pictureUrl={fieldValue?.avatarUrl ?? ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case Entity.Company: {
|
||||
return (
|
||||
<CompanyChip
|
||||
id={fieldValue?.id ?? ''}
|
||||
name={fieldValue?.name ?? ''}
|
||||
pictureUrl={
|
||||
fieldValue?.domainName
|
||||
? getLogoUrlFromDomainName(fieldValue.domainName)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
console.warn(
|
||||
`Unknown relation type: "${fieldDefinition.metadata.relationType}"
|
||||
in RelationFieldDisplay`,
|
||||
);
|
||||
return <> </>;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { TextDisplay } from '@/ui/field/meta-types/display/content-display/components/TextDisplay';
|
||||
|
||||
import { useTextField } from '../../hooks/useTextField';
|
||||
|
||||
export const TextFieldDisplay = () => {
|
||||
const { fieldValue } = useTextField();
|
||||
|
||||
return <TextDisplay text={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { URLDisplay } from '@/ui/field/meta-types/display/content-display/components/URLDisplay';
|
||||
|
||||
import { useURLField } from '../../hooks/useURLField';
|
||||
|
||||
export const URLFieldDisplay = () => {
|
||||
const { fieldValue } = useURLField();
|
||||
|
||||
return <URLDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import { CompanyChip } from '@/companies/components/CompanyChip';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
type OwnProps = {
|
||||
entityType: Entity;
|
||||
displayName: string;
|
||||
entityId: string | null;
|
||||
avatarUrlValue?: string;
|
||||
};
|
||||
|
||||
export const ChipDisplay = ({
|
||||
entityType,
|
||||
displayName,
|
||||
entityId,
|
||||
avatarUrlValue,
|
||||
}: OwnProps) => {
|
||||
switch (entityType) {
|
||||
case Entity.Company: {
|
||||
return (
|
||||
<CompanyChip
|
||||
id={entityId ?? ''}
|
||||
name={displayName}
|
||||
pictureUrl={getLogoUrlFromDomainName(avatarUrlValue)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case Entity.Person: {
|
||||
return (
|
||||
<PersonChip
|
||||
id={entityId ?? ''}
|
||||
name={displayName}
|
||||
pictureUrl={avatarUrlValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
console.warn(
|
||||
`Unknown relation type: "${entityType}" in DoubleTextChipDisplay`,
|
||||
);
|
||||
return <> </>;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
|
||||
type OwnProps = {
|
||||
value: Date | string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateDisplay = ({ value }: OwnProps) => (
|
||||
<div>{value && formatToHumanReadableDate(value)}</div>
|
||||
);
|
||||
@ -0,0 +1,3 @@
|
||||
import { TextDisplay } from './TextDisplay';
|
||||
|
||||
export const DoubleTextDisplay = TextDisplay;
|
||||
@ -0,0 +1,26 @@
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import { ContactLink } from '@/ui/link/components/ContactLink';
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailPattern.test(email.trim());
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export const EmailDisplay = ({ value }: OwnProps) =>
|
||||
value && validateEmail(value) ? (
|
||||
<ContactLink
|
||||
href={`mailto:${value}`}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</ContactLink>
|
||||
) : (
|
||||
<ContactLink href="#">{value}</ContactLink>
|
||||
);
|
||||
@ -0,0 +1,20 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
|
||||
const StyledTextInputDisplay = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
value: number | null;
|
||||
};
|
||||
|
||||
export const MoneyDisplay = ({ value }: OwnProps) => (
|
||||
<StyledTextInputDisplay>
|
||||
{value ? `$${formatNumber(value)}` : ''}
|
||||
</StyledTextInputDisplay>
|
||||
);
|
||||
@ -0,0 +1,20 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
|
||||
const StyledNumberDisplay = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
value: string | number | null;
|
||||
};
|
||||
|
||||
export const NumberDisplay = ({ value }: OwnProps) => (
|
||||
<StyledNumberDisplay>
|
||||
{value && formatNumber(Number(value))}
|
||||
</StyledNumberDisplay>
|
||||
);
|
||||
@ -0,0 +1,22 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
import { ContactLink } from '@/ui/link/components/ContactLink';
|
||||
|
||||
type OwnProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export const PhoneDisplay = ({ value }: OwnProps) =>
|
||||
value && isValidPhoneNumber(value) ? (
|
||||
<ContactLink
|
||||
href={parsePhoneNumber(value, 'FR')?.getURI()}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{parsePhoneNumber(value, 'FR')?.formatInternational() || value}
|
||||
</ContactLink>
|
||||
) : (
|
||||
<ContactLink href="#">{value}</ContactLink>
|
||||
);
|
||||
@ -0,0 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledTextInputDisplay = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const TextDisplay = ({ text }: OwnProps) => (
|
||||
<StyledTextInputDisplay>{text}</StyledTextInputDisplay>
|
||||
);
|
||||
@ -0,0 +1,63 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RoundedLink } from '@/ui/link/components/RoundedLink';
|
||||
import { LinkType, SocialLink } from '@/ui/link/components/SocialLink';
|
||||
|
||||
const StyledRawLink = styled(RoundedLink)`
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
const checkUrlType = (url: string) => {
|
||||
if (
|
||||
/^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(
|
||||
url,
|
||||
)
|
||||
) {
|
||||
return LinkType.LinkedIn;
|
||||
}
|
||||
if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?/i)) {
|
||||
return LinkType.Twitter;
|
||||
}
|
||||
|
||||
return LinkType.Url;
|
||||
};
|
||||
|
||||
export const URLDisplay = ({ value }: OwnProps) => {
|
||||
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const absoluteUrl = value
|
||||
? value.startsWith('http')
|
||||
? value
|
||||
: 'https://' + value
|
||||
: '';
|
||||
|
||||
const displayedValue = value ?? '';
|
||||
|
||||
const type = checkUrlType(absoluteUrl);
|
||||
|
||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
||||
return (
|
||||
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
|
||||
{displayedValue}
|
||||
</SocialLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
|
||||
{displayedValue}
|
||||
</StyledRawLink>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
|
||||
import { PhoneDisplay } from '../PhoneDisplay'; // Adjust the import path as needed
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Input/PhoneInputDisplay',
|
||||
component: PhoneDisplay,
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
value: '+33788901234',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PhoneDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldBoolean } from '../../types/guards/isFieldBoolean';
|
||||
|
||||
export const useBooleanField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('boolean', isFieldBoolean, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<boolean>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
43
front/src/modules/ui/field/meta-types/hooks/useChipField.ts
Normal file
43
front/src/modules/ui/field/meta-types/hooks/useChipField.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
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 entityType = fieldDefinition.metadata.relationType;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
contentFieldValue,
|
||||
setContentFieldValue,
|
||||
avatarFieldValue,
|
||||
setAvatarFieldValue,
|
||||
entityType,
|
||||
entityId,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
29
front/src/modules/ui/field/meta-types/hooks/useDateField.ts
Normal file
29
front/src/modules/ui/field/meta-types/hooks/useDateField.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldDate } from '../../types/guards/isFieldDate';
|
||||
|
||||
export const useDateField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('date', isFieldDate, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
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 entityType = fieldDefinition.metadata.entityType;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
avatarUrl,
|
||||
setAvatarUrl,
|
||||
secondValue,
|
||||
setSecondValue,
|
||||
firstValue,
|
||||
setFirstValue,
|
||||
fullValue,
|
||||
entityType,
|
||||
entityId,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldDoubleText } from '../../types/guards/isFieldDoubleText';
|
||||
|
||||
export const useDoubleTextField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('double-text', isFieldDoubleText, 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 fullValue = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
secondValue,
|
||||
setSecondValue,
|
||||
firstValue,
|
||||
setFirstValue,
|
||||
fullValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
29
front/src/modules/ui/field/meta-types/hooks/useEmailField.ts
Normal file
29
front/src/modules/ui/field/meta-types/hooks/useEmailField.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldEmail } from '../../types/guards/isFieldEmail';
|
||||
|
||||
export const useEmailField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('email', isFieldEmail, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
48
front/src/modules/ui/field/meta-types/hooks/useMoneyField.ts
Normal file
48
front/src/modules/ui/field/meta-types/hooks/useMoneyField.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
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('moneyAmount', 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);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistMoneyField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldNumber } from '../../types/guards/isFieldNumber';
|
||||
|
||||
export const useNumberField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('number', isFieldNumber, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistNumberField = (newValue: string) => {
|
||||
if (!canBeCastAsIntegerOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const castedValue = castAsIntegerOrNull(newValue);
|
||||
|
||||
persistField(castedValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistNumberField,
|
||||
};
|
||||
};
|
||||
40
front/src/modules/ui/field/meta-types/hooks/usePhoneField.ts
Normal file
40
front/src/modules/ui/field/meta-types/hooks/usePhoneField.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { useContext } from 'react';
|
||||
import { isPossiblePhoneNumber } from 'libphonenumber-js';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldPhone } from '../../types/guards/isFieldPhone';
|
||||
|
||||
export const usePhoneField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('phone', isFieldPhone, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistPhoneField = (newPhoneValue: string) => {
|
||||
if (!isPossiblePhoneNumber(newPhoneValue)) return;
|
||||
|
||||
persistField(newPhoneValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistPhoneField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldProbability } from '../../types/guards/isFieldProbability';
|
||||
|
||||
export const useProbabilityField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('probability', isFieldProbability, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const probabilityIndex = Math.ceil((fieldValue ?? 0) / 25);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
probabilityIndex,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
// TODO: we will be able to type more precisely when we will have custom field and custom entities support
|
||||
export const useRelationField = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('relation', isFieldRelation, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
};
|
||||
};
|
||||
29
front/src/modules/ui/field/meta-types/hooks/useTextField.ts
Normal file
29
front/src/modules/ui/field/meta-types/hooks/useTextField.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldText } from '../../types/guards/isFieldText';
|
||||
|
||||
export const useTextField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('text', isFieldText, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
43
front/src/modules/ui/field/meta-types/hooks/useURLField.ts
Normal file
43
front/src/modules/ui/field/meta-types/hooks/useURLField.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isURL } from '~/utils/is-url';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldURL } from '../../types/guards/isFieldURL';
|
||||
|
||||
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 persistField = usePersistField();
|
||||
|
||||
const persistURLField = (newValue: string) => {
|
||||
if (!isURL(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistField(newValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistURLField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import { BooleanInput } from '@/ui/input/components/BooleanInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useBooleanField } from '../../hooks/useBooleanField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const BooleanFieldInput = ({ onSubmit }: OwnProps) => {
|
||||
const { fieldValue } = useBooleanField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleToggle = (newValue: boolean) => {
|
||||
onSubmit?.(() => persistField(newValue));
|
||||
};
|
||||
|
||||
return <BooleanInput value={fieldValue ?? ''} onToggle={handleToggle} />;
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useChipField } from '../../hooks/useChipField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const ChipFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, contentFieldValue, 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 (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={contentFieldValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { DateInput } from '@/ui/input/components/DateInput';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useDateField } from '../../hooks/useDateField';
|
||||
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DateFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
}: OwnProps) => {
|
||||
const { fieldValue, hotkeyScope } = useDateField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistDate = (newDate: Nullable<Date>) => {
|
||||
if (!newDate) {
|
||||
persistField('');
|
||||
} else {
|
||||
const newDateISO = newDate?.toISOString();
|
||||
|
||||
persistField(newDateISO);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (newDate: Nullable<Date>) => {
|
||||
onEnter?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const handleEscape = (newDate: Nullable<Date>) => {
|
||||
onEscape?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
newDate: Nullable<Date>,
|
||||
) => {
|
||||
onClickOutside?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
value={dateValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
||||
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DoubleTextChipFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, firstValue, secondValue, hotkeyScope } =
|
||||
useDoubleTextChipField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
return (
|
||||
<DoubleTextInput
|
||||
firstValue={firstValue ?? ''}
|
||||
secondValue={secondValue ?? ''}
|
||||
firstValuePlaceholder={fieldDefinition.metadata.firstValuePlaceholder}
|
||||
secondValuePlaceholder={fieldDefinition.metadata.secondValuePlaceholder}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
||||
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DoubleTextFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, firstValue, secondValue, hotkeyScope } =
|
||||
useDoubleTextField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistField(newDoubleText));
|
||||
};
|
||||
|
||||
return (
|
||||
<DoubleTextInput
|
||||
firstValue={firstValue ?? ''}
|
||||
secondValue={secondValue ?? ''}
|
||||
firstValuePlaceholder={fieldDefinition.metadata.firstValuePlaceholder}
|
||||
secondValuePlaceholder={fieldDefinition.metadata.secondValuePlaceholder}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useEmailField } from '../../hooks/useEmailField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const EmailFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, hotkeyScope } = useEmailField();
|
||||
|
||||
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 (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { useMoneyField } from '../../hooks/useMoneyField';
|
||||
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const MoneyFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, hotkeyScope, persistMoneyField } =
|
||||
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 (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue?.toLocaleString() ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const NumberFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, hotkeyScope, persistNumberField } =
|
||||
useNumberField();
|
||||
|
||||
const handleEnter = (newText: string) => {
|
||||
onEnter?.(() => persistNumberField(newText));
|
||||
};
|
||||
|
||||
const handleEscape = (newText: string) => {
|
||||
onEscape?.(() => persistNumberField(newText));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newText: string,
|
||||
) => {
|
||||
onClickOutside?.(() => persistNumberField(newText));
|
||||
};
|
||||
|
||||
const handleTab = (newText: string) => {
|
||||
onTab?.(() => persistNumberField(newText));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newText: string) => {
|
||||
onShiftTab?.(() => persistNumberField(newText));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue?.toString() ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { PhoneInput } from '@/ui/input/components/PhoneInput';
|
||||
|
||||
import { usePhoneField } from '../../hooks/usePhoneField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const PhoneFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, hotkeyScope, persistPhoneField } =
|
||||
usePhoneField();
|
||||
|
||||
const handleEnter = (newText: string) => {
|
||||
onEnter?.(() => persistPhoneField(newText));
|
||||
};
|
||||
|
||||
const handleEscape = (newText: string) => {
|
||||
onEscape?.(() => persistPhoneField(newText));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newText: string,
|
||||
) => {
|
||||
onClickOutside?.(() => persistPhoneField(newText));
|
||||
};
|
||||
|
||||
const handleTab = (newText: string) => {
|
||||
onTab?.(() => persistPhoneField(newText));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newText: string) => {
|
||||
onShiftTab?.(() => persistPhoneField(newText));
|
||||
};
|
||||
|
||||
return (
|
||||
<PhoneInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import { ProbabilityInput } from '@/ui/input/components/ProbabilityInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useProbabilityField } from '../../hooks/useProbabilityField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const ProbabilityFieldInput = ({ onSubmit }: OwnProps) => {
|
||||
const { probabilityIndex } = useProbabilityField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleChange = (newValue: number) => {
|
||||
onSubmit?.(() => persistField(newValue));
|
||||
};
|
||||
|
||||
return (
|
||||
<ProbabilityInput
|
||||
probabilityIndex={probabilityIndex}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CompanyPicker } from '@/companies/components/CompanyPicker';
|
||||
import { PeoplePicker } from '@/people/components/PeoplePicker';
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||
import { UserPicker } from '@/users/components/UserPicker';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
const StyledRelationPickerContainer = styled.div`
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export const RelationFieldInput = ({ onSubmit, onCancel }: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue } = useRelationField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleSubmit = (newEntity: EntityForSelect | null) => {
|
||||
onSubmit?.(() => persistField(newEntity?.originalEntity ?? null));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRelationPickerContainer>
|
||||
{fieldDefinition.metadata.relationType === Entity.Person ? (
|
||||
<PeoplePicker
|
||||
personId={fieldValue?.id ?? ''}
|
||||
companyId={fieldValue?.companyId ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : fieldDefinition.metadata.relationType === Entity.User ? (
|
||||
<UserPicker
|
||||
userId={fieldValue?.id ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : fieldDefinition.metadata.relationType === Entity.Company ? (
|
||||
<CompanyPicker
|
||||
companyId={fieldValue?.id ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : null}
|
||||
</StyledRelationPickerContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useTextField } from '../../hooks/useTextField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const TextFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, hotkeyScope } = useTextField();
|
||||
|
||||
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 (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { useURLField } from '../../hooks/useURLField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
type OwnProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const URLFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: OwnProps) => {
|
||||
const { fieldDefinition, fieldValue, 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 (
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const entityFieldsFamilyState = atomFamily<
|
||||
Record<string, unknown> | null,
|
||||
string
|
||||
>({
|
||||
key: 'entityFieldsFamilyState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isFieldEmptyScopedState = atomFamily<boolean, string>({
|
||||
key: 'isFieldEmptyScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '../entityFieldsFamilyState';
|
||||
|
||||
export const entityFieldsFamilySelector = selectorFamily({
|
||||
key: 'entityFieldsFamilySelector',
|
||||
get:
|
||||
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
|
||||
({ get }) =>
|
||||
get(entityFieldsFamilyState(entityId))?.[fieldName] as T,
|
||||
set:
|
||||
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
|
||||
({ set }, newValue: T) =>
|
||||
set(entityFieldsFamilyState(entityId), (prevState) => ({
|
||||
...prevState,
|
||||
[fieldName]: newValue,
|
||||
})),
|
||||
});
|
||||
@ -0,0 +1,59 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { FieldDefinition } from '../../types/FieldDefinition';
|
||||
import { FieldMetadata } from '../../types/FieldMetadata';
|
||||
import { isFieldDate } from '../../types/guards/isFieldDate';
|
||||
import { isFieldEmail } from '../../types/guards/isFieldEmail';
|
||||
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 { isFieldRelationValue } from '../../types/guards/isFieldRelationValue';
|
||||
import { isFieldText } from '../../types/guards/isFieldText';
|
||||
import { isFieldURL } from '../../types/guards/isFieldURL';
|
||||
import { entityFieldsFamilyState } from '../entityFieldsFamilyState';
|
||||
|
||||
export const isEntityFieldEmptyFamilySelector = selectorFamily({
|
||||
key: 'isEntityFieldEmptyFamilySelector',
|
||||
get: ({
|
||||
fieldDefinition,
|
||||
entityId,
|
||||
}: {
|
||||
fieldDefinition: Pick<
|
||||
FieldDefinition<FieldMetadata>,
|
||||
'type' | 'metadata' | 'key' | 'name'
|
||||
>;
|
||||
entityId: string;
|
||||
}) => {
|
||||
return ({ get }) => {
|
||||
if (
|
||||
isFieldText(fieldDefinition) ||
|
||||
isFieldURL(fieldDefinition) ||
|
||||
isFieldDate(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition) ||
|
||||
isFieldMoney(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldPhone(fieldDefinition)
|
||||
) {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[
|
||||
fieldName
|
||||
] as string | null;
|
||||
|
||||
return (
|
||||
fieldValue === null || fieldValue === undefined || fieldValue === ''
|
||||
);
|
||||
} else if (isFieldRelation(fieldDefinition)) {
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
|
||||
|
||||
if (isFieldRelationValue(fieldValue)) {
|
||||
return fieldValue === null || fieldValue === undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
},
|
||||
});
|
||||
13
front/src/modules/ui/field/types/FieldDefinition.ts
Normal file
13
front/src/modules/ui/field/types/FieldDefinition.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
|
||||
import { FieldMetadata } from './FieldMetadata';
|
||||
import { FieldType } from './FieldType';
|
||||
|
||||
export type FieldDefinition<T extends FieldMetadata> = {
|
||||
key: string;
|
||||
name: string;
|
||||
Icon?: IconComponent;
|
||||
type: FieldType;
|
||||
metadata: T;
|
||||
useEditButton?: boolean;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldDefinition } from './FieldDefinition';
|
||||
import { FieldMetadata } from './FieldMetadata';
|
||||
|
||||
export type FieldDefinitionSerializable = Omit<
|
||||
FieldDefinition<FieldMetadata>,
|
||||
'Icon'
|
||||
>;
|
||||
5
front/src/modules/ui/field/types/FieldDoubleText.ts
Normal file
5
front/src/modules/ui/field/types/FieldDoubleText.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DoubleTextTypeResolver } from './resolvers/DoubleTextTypeResolver';
|
||||
|
||||
export type FieldDoubleText = z.infer<typeof DoubleTextTypeResolver>;
|
||||
1
front/src/modules/ui/field/types/FieldInputEvent.ts
Normal file
1
front/src/modules/ui/field/types/FieldInputEvent.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
114
front/src/modules/ui/field/types/FieldMetadata.ts
Normal file
114
front/src/modules/ui/field/types/FieldMetadata.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||
|
||||
export type FieldTextMetadata = {
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldPhoneMetadata = {
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldURLMetadata = {
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldDateMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldNumberMetadata = {
|
||||
fieldName: string;
|
||||
placeHolder: string;
|
||||
isPositive?: boolean;
|
||||
};
|
||||
|
||||
export type FieldMoneyMetadata = {
|
||||
fieldName: string;
|
||||
placeHolder: string;
|
||||
isPositive?: boolean;
|
||||
};
|
||||
|
||||
export type FieldEmailMetadata = {
|
||||
fieldName: string;
|
||||
placeHolder: string;
|
||||
};
|
||||
|
||||
export type FieldRelationMetadata = {
|
||||
relationType: Entity;
|
||||
fieldName: string;
|
||||
useEditButton?: boolean;
|
||||
};
|
||||
|
||||
export type FieldChipMetadata = {
|
||||
relationType: Entity;
|
||||
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;
|
||||
entityType: Entity;
|
||||
};
|
||||
|
||||
export type FieldProbabilityMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldBooleanMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldMetadata =
|
||||
| FieldTextMetadata
|
||||
| FieldRelationMetadata
|
||||
| FieldChipMetadata
|
||||
| FieldDoubleTextChipMetadata
|
||||
| FieldDoubleTextMetadata
|
||||
| FieldPhoneMetadata
|
||||
| FieldURLMetadata
|
||||
| FieldNumberMetadata
|
||||
| FieldMoneyMetadata
|
||||
| FieldEmailMetadata
|
||||
| FieldDateMetadata
|
||||
| FieldProbabilityMetadata
|
||||
| FieldBooleanMetadata;
|
||||
|
||||
export type FieldTextValue = string;
|
||||
|
||||
export type FieldChipValue = string;
|
||||
export type FieldDateValue = string | null;
|
||||
export type FieldPhoneValue = string;
|
||||
export type FieldURLValue = string;
|
||||
export type FieldNumberValue = number | null;
|
||||
export type FieldMoneyValue = number | null;
|
||||
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 FieldRelationValue = EntityForSelect | null;
|
||||
14
front/src/modules/ui/field/types/FieldType.ts
Normal file
14
front/src/modules/ui/field/types/FieldType.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export type FieldType =
|
||||
| 'text'
|
||||
| 'relation'
|
||||
| 'chip'
|
||||
| 'double-text-chip'
|
||||
| 'double-text'
|
||||
| 'number'
|
||||
| 'email'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'phone'
|
||||
| 'url'
|
||||
| 'probability'
|
||||
| 'moneyAmount';
|
||||
@ -0,0 +1,71 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import {
|
||||
FieldBooleanMetadata,
|
||||
FieldChipMetadata,
|
||||
FieldDateMetadata,
|
||||
FieldDoubleTextChipMetadata,
|
||||
FieldDoubleTextMetadata,
|
||||
FieldEmailMetadata,
|
||||
FieldMetadata,
|
||||
FieldMoneyMetadata,
|
||||
FieldNumberMetadata,
|
||||
FieldPhoneMetadata,
|
||||
FieldProbabilityMetadata,
|
||||
FieldRelationMetadata,
|
||||
FieldTextMetadata,
|
||||
FieldURLMetadata,
|
||||
} from '../FieldMetadata';
|
||||
import { FieldType } from '../FieldType';
|
||||
|
||||
type AssertFieldMetadataFunction = <
|
||||
E extends FieldType,
|
||||
T extends E extends 'text'
|
||||
? FieldTextMetadata
|
||||
: E extends 'relation'
|
||||
? FieldRelationMetadata
|
||||
: E extends 'chip'
|
||||
? FieldChipMetadata
|
||||
: E extends 'double-text-chip'
|
||||
? FieldDoubleTextChipMetadata
|
||||
: E extends 'double-text'
|
||||
? FieldDoubleTextMetadata
|
||||
: E extends 'number'
|
||||
? FieldNumberMetadata
|
||||
: E extends 'email'
|
||||
? FieldEmailMetadata
|
||||
: E extends 'boolean'
|
||||
? FieldBooleanMetadata
|
||||
: E extends 'date'
|
||||
? FieldDateMetadata
|
||||
: E extends 'phone'
|
||||
? FieldPhoneMetadata
|
||||
: E extends 'url'
|
||||
? FieldURLMetadata
|
||||
: E extends 'probability'
|
||||
? FieldProbabilityMetadata
|
||||
: E extends 'moneyAmount'
|
||||
? FieldMoneyMetadata
|
||||
: never,
|
||||
>(
|
||||
fieldType: E,
|
||||
fieldTypeGuard: (
|
||||
a: FieldDefinition<FieldMetadata>,
|
||||
) => a is FieldDefinition<T>,
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>,
|
||||
) => asserts fieldDefinition is FieldDefinition<T>;
|
||||
|
||||
export const assertFieldMetadata: AssertFieldMetadataFunction = (
|
||||
fieldType,
|
||||
fieldTypeGuard,
|
||||
fieldDefinition,
|
||||
) => {
|
||||
const fieldDefinitionType = fieldDefinition.type;
|
||||
|
||||
if (!fieldTypeGuard(fieldDefinition) || fieldDefinitionType !== fieldType) {
|
||||
throw new Error(
|
||||
`Trying to use a "${fieldDefinitionType}" field as a "${fieldType}" field. Verify that the field is defined as a type "${fieldDefinitionType}" field in assertFieldMetadata.ts.`,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldBooleanMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldBoolean = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldBooleanMetadata> => field.type === 'boolean';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldBooleanValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldBooleanValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldBooleanValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'boolean';
|
||||
6
front/src/modules/ui/field/types/guards/isFieldChip.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldChip.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldChipMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldChip = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldChipMetadata> => field.type === 'chip';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldChipValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldChipValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldChipValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string';
|
||||
6
front/src/modules/ui/field/types/guards/isFieldDate.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldDate.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldDateMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldDate = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldDateMetadata> => field.type === 'date';
|
||||
@ -0,0 +1,8 @@
|
||||
import { FieldDateValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldDateValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldDateValue =>
|
||||
fieldValue === null ||
|
||||
(fieldValue !== undefined && typeof fieldValue === 'string');
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldDoubleTextMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldDoubleText = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldDoubleTextMetadata> =>
|
||||
field.type === 'double-text';
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldDoubleTextChipMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldDoubleTextChip = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldDoubleTextChipMetadata> =>
|
||||
field.type === 'double-text-chip';
|
||||
@ -0,0 +1,9 @@
|
||||
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;
|
||||
@ -0,0 +1,10 @@
|
||||
import { FieldDoubleTextValue } from '../FieldMetadata';
|
||||
import { DoubleTextTypeResolver } from '../resolvers/DoubleTextTypeResolver';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldDoubleTextValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldDoubleTextValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
DoubleTextTypeResolver.safeParse(fieldValue).success;
|
||||
6
front/src/modules/ui/field/types/guards/isFieldEmail.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldEmail.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldEmailMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldEmail = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldEmailMetadata> => field.type === 'email';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldEmailValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldEmailValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldEmailValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string';
|
||||
6
front/src/modules/ui/field/types/guards/isFieldMoney.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldMoney.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldMoneyMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldMoney = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldMoneyMetadata> => field.type === 'moneyAmount';
|
||||
@ -0,0 +1,8 @@
|
||||
import { FieldMoneyValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldMoneyValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldMoneyValue =>
|
||||
fieldValue === null ||
|
||||
(fieldValue !== undefined && typeof fieldValue === 'number');
|
||||
6
front/src/modules/ui/field/types/guards/isFieldNumber.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldNumber.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldNumberMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldNumber = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldNumberMetadata> => field.type === 'number';
|
||||
@ -0,0 +1,8 @@
|
||||
import { FieldNumberValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldNumberValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldNumberValue =>
|
||||
fieldValue === null ||
|
||||
(fieldValue !== undefined && typeof fieldValue === 'number');
|
||||
6
front/src/modules/ui/field/types/guards/isFieldPhone.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldPhone.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldPhoneMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldPhone = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldPhoneMetadata> => field.type === 'phone';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldPhoneValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldPhoneValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldPhoneValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string';
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldProbabilityMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldProbability = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldProbabilityMetadata> =>
|
||||
field.type === 'probability';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldProbabilityValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldProbabilityValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldProbabilityValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'number';
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelation = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldRelationMetadata> => field.type === 'relation';
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldRelationValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldRelationValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldRelationValue =>
|
||||
fieldValue !== undefined && typeof fieldValue === 'object';
|
||||
6
front/src/modules/ui/field/types/guards/isFieldText.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldText.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldTextMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldText = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldTextMetadata> => field.type === 'text';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldTextValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldTextValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldTextValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string';
|
||||
6
front/src/modules/ui/field/types/guards/isFieldURL.ts
Normal file
6
front/src/modules/ui/field/types/guards/isFieldURL.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldURLMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldURL = (
|
||||
field: FieldDefinition<FieldMetadata>,
|
||||
): field is FieldDefinition<FieldURLMetadata> => field.type === 'url';
|
||||
@ -0,0 +1,9 @@
|
||||
import { FieldURLValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add yup
|
||||
export const isFieldURLValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldURLValue =>
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string';
|
||||
@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DoubleTextTypeResolver = z.object({
|
||||
firstValue: z.string(),
|
||||
secondValue: z.string(),
|
||||
});
|
||||
Reference in New Issue
Block a user