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:
Lucas Bordeau
2023-09-27 18:18:02 +02:00
committed by GitHub
parent d9feabbc63
commit cbadcba188
290 changed files with 3152 additions and 4481 deletions

View 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 />
) : (
<></>
)}
</>
);
};

View 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}
/>
) : (
<></>
)}
</>
);
};

View 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,
);

View 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;
};

View 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;
};

View 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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { TextDisplay } from './TextDisplay';
export const DoubleTextDisplay = TextDisplay;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {};

View 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 { 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,
};
};

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

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

View File

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

View File

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

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

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

View 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 { 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,
};
};

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

View File

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

View 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 { 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,
};
};

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const entityFieldsFamilyState = atomFamily<
Record<string, unknown> | null,
string
>({
key: 'entityFieldsFamilyState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const isFieldEmptyScopedState = atomFamily<boolean, string>({
key: 'isFieldEmptyScopedState',
default: false,
});

View File

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

View File

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

View 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;
};

View File

@ -0,0 +1,7 @@
import { FieldDefinition } from './FieldDefinition';
import { FieldMetadata } from './FieldMetadata';
export type FieldDefinitionSerializable = Omit<
FieldDefinition<FieldMetadata>,
'Icon'
>;

View File

@ -0,0 +1,5 @@
import { z } from 'zod';
import { DoubleTextTypeResolver } from './resolvers/DoubleTextTypeResolver';
export type FieldDoubleText = z.infer<typeof DoubleTextTypeResolver>;

View File

@ -0,0 +1 @@
export type FieldInputEvent = (persist: () => void) => void;

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

View File

@ -0,0 +1,14 @@
export type FieldType =
| 'text'
| 'relation'
| 'chip'
| 'double-text-chip'
| 'double-text'
| 'number'
| 'email'
| 'boolean'
| 'date'
| 'phone'
| 'url'
| 'probability'
| 'moneyAmount';

View File

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

View File

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

View File

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

View 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';

View File

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

View 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';

View File

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

View File

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

View File

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

View File

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

View File

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

View 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';

View File

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

View 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';

View File

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

View 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';

View File

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

View 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';

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { FieldRelationValue } from '../FieldMetadata';
// TODO: add yup
export const isFieldRelationValue = (
fieldValue: unknown,
): fieldValue is FieldRelationValue =>
fieldValue !== undefined && typeof fieldValue === 'object';

View 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';

View File

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

View 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';

View File

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

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
export const DoubleTextTypeResolver = z.object({
firstValue: z.string(),
secondValue: z.string(),
});