2282 Rename components to use the new naming convention part 1 (#2293)

renaming in progress
This commit is contained in:
bosiraphael
2023-10-31 12:12:52 +01:00
committed by GitHub
parent 7fe569ec6a
commit ec8389cecf
403 changed files with 458 additions and 455 deletions

View File

@ -0,0 +1,67 @@
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 { MoneyAmountV2FieldDisplay } from '../meta-types/display/components/MoneyAmountV2FieldDisplay';
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 { URLV2FieldDisplay } from '../meta-types/display/components/URLV2FieldDisplay';
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 { isFieldMoneyAmountV2 } from '../types/guards/isFieldMoneyAmountV2';
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';
import { isFieldURLV2 } from '../types/guards/isFieldURLV2';
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 />
) : isFieldURLV2(fieldDefinition) ? (
<URLV2FieldDisplay />
) : isFieldMoneyAmountV2(fieldDefinition) ? (
<MoneyAmountV2FieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldDisplay />
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldDisplay />
) : isFieldDoubleText(fieldDefinition) ? (
<DoubleTextFieldDisplay />
) : (
<></>
)}
</>
);
};

View File

@ -0,0 +1,170 @@
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 { MoneyAmountV2FieldInput } from '../meta-types/input/components/MoneyAmountV2FieldInput';
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 { URLV2FieldInput } from '../meta-types/input/components/URLV2FieldInput';
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 { isFieldMoneyAmountV2 } from '../types/guards/isFieldMoneyAmountV2';
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';
import { isFieldURLV2 } from '../types/guards/isFieldURLV2';
type FieldInputProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const FieldInput = ({
onCancel,
onSubmit,
onEnter,
onEscape,
onShiftTab,
onTab,
onClickOutside,
}: FieldInputProps) => {
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}
/>
) : isFieldURLV2(fieldDefinition) ? (
<URLV2FieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldMoneyAmountV2(fieldDefinition) ? (
<MoneyAmountV2FieldInput
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';
export type GenericFieldContextType = {
fieldDefinition: FieldDefinition<FieldMetadata>;
// TODO: add better typing for mutation web-hook
useUpdateEntityMutation: () => [(params: any) => void, any];
entityId: string;
recoilScopeId: string;
hotkeyScope: string;
};
export const FieldContext = createContext<GenericFieldContextType>(
{} as GenericFieldContextType,
);

View File

@ -0,0 +1,22 @@
import { useContext } from 'react';
import { IconPencil } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldURL } from '../types/guards/isFieldURL';
export const useGetButtonIcon = (): IconComponent | undefined => {
const { fieldDefinition } = useContext(FieldContext);
if (
isFieldURL(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition) ||
isFieldRelation(fieldDefinition)
) {
return IconPencil;
}
};

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: {
fieldId: fieldDefinition.fieldId,
label: fieldDefinition.label,
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,201 @@
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 { isFieldMoneyAmountV2 } from '../types/guards/isFieldMoneyAmountV2';
import { isFieldMoneyAmountV2Value } from '../types/guards/isFieldMoneyAmountV2Value';
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 { isFieldURLV2 } from '../types/guards/isFieldURLV2';
import { isFieldURLV2Value } from '../types/guards/isFieldURLV2Value';
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 fieldIsURLV2 =
isFieldURLV2(fieldDefinition) && isFieldURLV2Value(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 fieldIsMoneyAmountV2 =
isFieldMoneyAmountV2(fieldDefinition) &&
isFieldMoneyAmountV2Value(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 ||
fieldIsURLV2 ||
fieldIsMoneyAmountV2
) {
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: ${JSON.stringify(
valueToPersist,
)} for type : ${
fieldDefinition.type
}, type may not be implemented in usePersistField.`,
);
}
},
[entityId, fieldDefinition, updateEntity],
);
return persistField;
};

View File

@ -0,0 +1,48 @@
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';
export const useToggleEditOnlyInput = () => {
const { entityId, fieldDefinition, useUpdateEntityMutation } =
useContext(FieldContext);
const [updateEntity] = useUpdateEntityMutation();
const toggleField = useRecoilCallback(
({ set, snapshot }) =>
() => {
const fieldIsBoolean = isFieldBoolean(fieldDefinition);
if (fieldIsBoolean) {
const fieldName = fieldDefinition.metadata.fieldName;
const oldValue = snapshot
.getLoadable(entityFieldsFamilySelector({ entityId, fieldName }))
.valueOrThrow();
const valueToPersist = !oldValue;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist,
},
},
});
} else {
throw new Error(
`Invalid value to toggle for type : ${fieldDefinition.type}, type may not be implemented in useToggleEditOnlyInput.`,
);
}
},
[entityId, fieldDefinition, updateEntity],
);
return toggleField;
};

View File

@ -0,0 +1,35 @@
import {
FieldContext,
GenericFieldContextType,
} from '@/ui/object/field/contexts/FieldContext';
type FieldContextProviderProps = {
children: React.ReactNode;
fieldDefinition: GenericFieldContextType['fieldDefinition'];
entityId?: string;
};
export const FieldContextProvider = ({
children,
fieldDefinition,
entityId,
}: FieldContextProviderProps) => {
return (
<FieldContext.Provider
value={{
entityId: entityId ?? '1',
recoilScopeId: '1',
hotkeyScope: 'hotkey-scope',
fieldDefinition,
useUpdateEntityMutation: () => [
() => {
return;
},
{},
],
}}
>
{children}
</FieldContext.Provider>
);
};

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/object/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 { useMoneyAmountV2Field } from '../../hooks/useMoneyAmountV2Field';
import { MoneyAmountV2Display } from '../content-display/components/MoneyAmountV2Display';
export const MoneyAmountV2FieldDisplay = () => {
const { fieldValue } = useMoneyAmountV2Field();
return <MoneyAmountV2Display 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/object/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/object/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,24 @@
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationField();
const { entityChipDisplayMapper } = fieldDefinition;
if (!entityChipDisplayMapper) {
throw new Error(
"Missing entityChipDisplayMapper in FieldContext. Please provide it in the FieldContextProvider's value prop.",
);
}
const { name, pictureUrl, avatarType } =
entityChipDisplayMapper?.(fieldValue);
return (
<EntityChip
entityId={fieldValue?.id}
name={name}
pictureUrl={pictureUrl}
avatarType={avatarType}
/>
);
};

View File

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

View File

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

View File

@ -0,0 +1,77 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDateField } from '../../../hooks/useDateField';
import { DateFieldDisplay } from '../DateFieldDisplay';
const formattedDate = new Date();
const DateFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useDateField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type DateFieldDisplayWithContextProps = {
value: string;
entityId?: string;
};
const DateFieldDisplayWithContext = ({
value,
entityId,
}: DateFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'date',
label: 'Date',
type: 'date',
metadata: {
fieldName: 'Date',
},
}}
entityId={entityId}
>
<DateFieldValueSetterEffect value={value} />
<DateFieldDisplay />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/DateFieldDisplay',
component: DateFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof DateFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: formattedDate.toISOString(),
},
};
export const Elipsis: Story = {
args: {
value: formattedDate.toISOString(),
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 50,
},
},
decorators: [ComponentDecorator],
};

View File

@ -0,0 +1,101 @@
import React, { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDoubleTextField } from '../../../hooks/useDoubleTextField';
import { DoubleTextFieldDisplay } from '../DoubleTextFieldDisplay'; // Import your component
const DoubleTextFieldDisplayValueSetterEffect = ({
firstValue,
secondValue,
}: {
firstValue: string;
secondValue: string;
}) => {
const { setFirstValue, setSecondValue } = useDoubleTextField();
useEffect(() => {
setFirstValue(firstValue);
setSecondValue(secondValue);
}, [setFirstValue, setSecondValue, firstValue, secondValue]);
return <></>;
};
type DoubleTextFieldDisplayWithContextProps = {
firstValue: string;
secondValue: string;
entityId?: string;
};
const DoubleTextFieldDisplayWithContext = ({
firstValue,
secondValue,
entityId,
}: DoubleTextFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'double-text',
label: 'Double-Text',
type: 'double-text',
metadata: {
firstValueFieldName: 'First-text',
firstValuePlaceholder: 'First-text',
secondValueFieldName: 'Second-text',
secondValuePlaceholder: 'Second-text',
},
}}
entityId={entityId}
>
<DoubleTextFieldDisplayValueSetterEffect
firstValue={firstValue}
secondValue={secondValue}
/>
<DoubleTextFieldDisplay />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/DoubleTextFieldDisplay',
component: DoubleTextFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof DoubleTextFieldDisplayWithContext>;
export const Default: Story = {
args: {
firstValue: 'Lorem',
secondValue: 'ipsum',
},
};
export const CustomValues: Story = {
args: {
firstValue: 'Lorem',
secondValue: 'ipsum',
},
};
export const Elipsis: Story = {
args: {
firstValue:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
secondValue: 'ipsum dolor sit amet, consectetur adipiscing elit.',
},
argTypes: {
firstValue: { control: true },
secondValue: { control: true },
},
parameters: {
container: {
width: 100,
},
},
decorators: [ComponentDecorator],
};

View File

@ -0,0 +1,79 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useEmailField } from '../../../hooks/useEmailField';
import { EmailFieldDisplay } from '../EmailFieldDisplay';
const EmailFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useEmailField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type EmailFieldDisplayWithContextProps = {
value: string;
entityId?: string;
};
const EmailFieldDisplayWithContext = ({
value,
entityId,
}: EmailFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'email',
label: 'Email',
type: 'email',
metadata: {
fieldName: 'Email',
placeHolder: 'Email',
},
}}
entityId={entityId}
>
<MemoryRouter>
<EmailFieldValueSetterEffect value={value} />
<EmailFieldDisplay />
</MemoryRouter>
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/EmailFieldDisplay',
component: EmailFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof EmailFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: 'Test@Test.test',
},
};
export const Elipsis: Story = {
args: {
value: 'Test@Test.test',
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 50,
},
},
decorators: [ComponentDecorator],
};

View File

@ -0,0 +1,111 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { v4 } from 'uuid';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useMoneyField } from '../../../hooks/useMoneyField';
import { MoneyFieldDisplay } from '../MoneyFieldDisplay';
const MoneyFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useMoneyField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type MoneyFieldDisplayWithContextProps = {
value: number;
entityId?: string;
};
const MoneyFieldDisplayWithContext = ({
value,
entityId,
}: MoneyFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'money',
label: 'Money',
type: 'moneyAmount',
metadata: {
fieldName: 'Amount',
placeHolder: 'Amount',
isPositive: true,
},
}}
entityId={entityId}
>
<MoneyFieldValueSetterEffect value={value} />
<MoneyFieldDisplay />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/MoneyFieldDisplay',
component: MoneyFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof MoneyFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: 100,
},
};
export const Elipsis: Story = {
args: {
value: 1e100,
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 100,
},
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MoneyFieldDisplayWithContext> =
{
argTypes: {
value: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'currency',
values: ['$'] satisfies string[],
props: (_value: string) => ({}),
},
{
name: 'value',
values: [
100, 1000, -1000, 1e10, 1.357802, -1.283, 0,
] satisfies number[],
props: (value: number) => ({ value, entityId: v4() }),
},
],
options: {
elementContainer: {
width: 100,
},
},
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,108 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { v4 } from 'uuid';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useNumberField } from '../../../hooks/useNumberField';
import { NumberFieldDisplay } from '../NumberFieldDisplay';
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useNumberField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type NumberFieldDisplayWithContextProps = {
value: number;
entityId?: string;
};
const NumberFieldDisplayWithContext = ({
value,
entityId,
}: NumberFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'number',
label: 'Number',
type: 'number',
metadata: {
fieldName: 'Number',
placeHolder: 'Number',
isPositive: true,
},
}}
entityId={entityId}
>
<NumberFieldValueSetterEffect value={value} />
<NumberFieldDisplay />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/NumberFieldDisplay',
component: NumberFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof NumberFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: 100,
},
};
export const Elipsis: Story = {
args: {
value: 1e100,
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 100,
},
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<
Story,
typeof NumberFieldDisplayWithContext
> = {
argTypes: {
value: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'value',
values: [
100, 1000, -1000, 1e10, 1.357802, -1.283, 0,
] satisfies number[],
props: (value: number) => ({ value, entityId: v4() }),
},
],
options: {
elementContainer: {
width: 100,
},
},
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,79 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { usePhoneField } from '../../../hooks/usePhoneField';
import { PhoneFieldDisplay } from '../PhoneFieldDisplay';
const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = usePhoneField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type PhoneFieldDisplayWithContextProps = {
value: string;
entityId?: string;
};
const PhoneFieldDisplayWithContext = ({
value,
entityId,
}: PhoneFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'phone',
label: 'Phone',
type: 'phone',
metadata: {
fieldName: 'Phone',
placeHolder: 'Phone',
},
}}
entityId={entityId}
>
<MemoryRouter>
<PhoneFieldValueSetterEffect value={value} />
<PhoneFieldDisplay />
</MemoryRouter>
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/PhoneFieldDisplay',
component: PhoneFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof PhoneFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: '362763872687362',
},
};
export const Elipsis: Story = {
args: {
value: '362763872687362',
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 50,
},
},
decorators: [ComponentDecorator],
};

View File

@ -0,0 +1,110 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { v4 } from 'uuid';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldDisplay } from '../TextFieldDisplay';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type TextFieldDisplayWithContextProps = {
value: string;
entityId?: string;
};
const TextFieldDisplayWithContext = ({
value,
entityId,
}: TextFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'text',
label: 'Text',
type: 'text',
metadata: {
fieldName: 'Text',
placeHolder: 'Text',
},
}}
entityId={entityId}
>
<TextFieldValueSetterEffect value={value} />
<TextFieldDisplay />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/TextFieldDisplay',
component: TextFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof TextFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: 'Lorem ipsum',
},
};
export const Elipsis: Story = {
args: {
value:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
},
argTypes: {
value: { control: false },
},
parameters: {
container: {
width: 100,
},
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof TextFieldDisplayWithContext> =
{
argTypes: {
value: { control: false },
},
parameters: {
catalog: {
dimensions: [
{
name: 'value',
values: [
'Hello world',
'Test',
'1234567890',
' ',
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
] satisfies string[],
props: (value: string) => ({ value, entityId: v4() }),
},
],
options: {
elementContainer: {
width: 100,
},
},
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,79 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useURLField } from '../../../hooks/useURLField';
import { URLFieldDisplay } from '../URLFieldDisplay';
const URLFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useURLField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type URLFieldDisplayWithContextProps = {
value: string;
entityId?: string;
};
const URLFieldDisplayWithContext = ({
value,
entityId,
}: URLFieldDisplayWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'URL',
label: 'URL',
type: 'url',
metadata: {
fieldName: 'URL',
placeHolder: 'URL',
},
}}
entityId={entityId}
>
<MemoryRouter>
<URLFieldValueSetterEffect value={value} />
<URLFieldDisplay />
</MemoryRouter>
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Display/URLFieldDisplay',
component: URLFieldDisplayWithContext,
};
export default meta;
type Story = StoryObj<typeof URLFieldDisplayWithContext>;
export const Default: Story = {
args: {
value: 'https://github.com/GitStartHQ',
},
};
export const Elipsis: Story = {
args: {
value: 'https://www.instagram.com/gitstart/',
},
argTypes: {
value: { control: true },
},
parameters: {
container: {
width: 200,
},
},
decorators: [ComponentDecorator],
};

View File

@ -0,0 +1,45 @@
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';
import { logError } from '~/utils/logError';
type ChipDisplayProps = {
entityType: Entity;
displayName: string;
entityId: string | null;
avatarUrlValue?: string;
};
export const ChipDisplay = ({
entityType,
displayName,
entityId,
avatarUrlValue,
}: ChipDisplayProps) => {
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:
logError(
`Unknown relation type: "${entityType}" in DoubleTextChipDisplay`,
);
return <> </>;
}
};

View File

@ -0,0 +1,11 @@
import { formatToHumanReadableDate } from '~/utils';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateDisplayProps = {
value: Date | string | null | undefined;
};
export const DateDisplay = ({ value }: DateDisplayProps) => (
<EllipsisDisplay>{value && formatToHumanReadableDate(value)}</EllipsisDisplay>
);

View File

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

View File

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
const StyledEllipsisDisplay = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export { StyledEllipsisDisplay as EllipsisDisplay };

View File

@ -0,0 +1,31 @@
import { MouseEvent } from 'react';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { EllipsisDisplay } from './EllipsisDisplay';
const validateEmail = (email: string) => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email.trim());
};
type EmailDisplayProps = {
value: string | null;
};
export const EmailDisplay = ({ value }: EmailDisplayProps) => (
<EllipsisDisplay>
{value && validateEmail(value) ? (
<ContactLink
href={`mailto:${value}`}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{value}
</ContactLink>
) : (
<ContactLink href="#">{value}</ContactLink>
)}
</EllipsisDisplay>
);

View File

@ -0,0 +1,15 @@
import { FieldMoneyAmountV2Value } from '@/ui/object/field/types/FieldMetadata';
import { EllipsisDisplay } from './EllipsisDisplay';
type MoneyAmountV2DisplayProps = {
value?: FieldMoneyAmountV2Value;
};
export const MoneyAmountV2Display = ({ value }: MoneyAmountV2DisplayProps) => {
return (
<EllipsisDisplay>
{value?.amount} {value?.currency}
</EllipsisDisplay>
);
};

View File

@ -0,0 +1,11 @@
import { formatNumber } from '~/utils/format/number';
import { EllipsisDisplay } from './EllipsisDisplay';
type MoneyDisplayProps = {
value: number | null;
};
export const MoneyDisplay = ({ value }: MoneyDisplayProps) => (
<EllipsisDisplay>{value ? `$${formatNumber(value)}` : ''}</EllipsisDisplay>
);

View File

@ -0,0 +1,11 @@
import { formatNumber } from '~/utils/format/number';
import { EllipsisDisplay } from './EllipsisDisplay';
type NumberDisplayProps = {
value: string | number | null;
};
export const NumberDisplay = ({ value }: NumberDisplayProps) => (
<EllipsisDisplay>{value && formatNumber(Number(value))}</EllipsisDisplay>
);

View File

@ -0,0 +1,27 @@
import { MouseEvent } from 'react';
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { EllipsisDisplay } from './EllipsisDisplay';
type PhoneDisplayProps = {
value: string | null;
};
export const PhoneDisplay = ({ value }: PhoneDisplayProps) => (
<EllipsisDisplay>
{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>
)}
</EllipsisDisplay>
);

View File

@ -0,0 +1,9 @@
import { EllipsisDisplay } from './EllipsisDisplay';
type TextDisplayProps = {
text: string;
};
export const TextDisplay = ({ text }: TextDisplayProps) => (
<EllipsisDisplay>{text}</EllipsisDisplay>
);

View File

@ -0,0 +1,72 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { EllipsisDisplay } from './EllipsisDisplay';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type URLDisplayProps = {
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 }: URLDisplayProps) => {
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 (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
</EllipsisDisplay>
);
};

View File

@ -0,0 +1,73 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { FieldURLV2Value } from '@/ui/object/field/types/FieldMetadata';
import { EllipsisDisplay } from './EllipsisDisplay';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type URLV2DisplayProps = {
value?: FieldURLV2Value;
};
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 URLV2Display = ({ value }: URLV2DisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
};
const absoluteUrl = value?.link
? value.link.startsWith('http')
? value.link
: 'https://' + value.link
: '';
const displayedValue = value?.text ?? '';
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
</EllipsisDisplay>
);
};

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/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,43 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldMoneyAmountV2Value } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldMoneyAmountV2 } from '../../types/guards/isFieldMoneyAmountV2';
import { isFieldMoneyAmountV2Value } from '../../types/guards/isFieldMoneyAmountV2Value';
export const useMoneyAmountV2Field = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('moneyAmountV2', isFieldMoneyAmountV2, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldMoneyAmountV2Value>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistMoneyAmountV2Field = (newValue: FieldMoneyAmountV2Value) => {
if (!isFieldMoneyAmountV2Value(newValue)) {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
persistMoneyAmountV2Field,
};
};

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) && 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) && newValue !== '') {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
persistURLField,
};
};

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldURLV2Value } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldURLV2 } from '../../types/guards/isFieldURLV2';
import { isFieldURLV2Value } from '../../types/guards/isFieldURLV2Value';
export const useURLV2Field = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('urlV2', isFieldURLV2, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldURLV2Value>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistURLField = (newValue: FieldURLV2Value) => {
if (!isFieldURLV2Value(newValue)) {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
persistURLField,
};
};

View File

@ -0,0 +1,32 @@
import { BooleanInput } from '@/ui/object/field/meta-types/input/components/internal/BooleanInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useBooleanField } from '../../hooks/useBooleanField';
import { FieldInputEvent } from './DateFieldInput';
export type BooleanFieldInputProps = {
onSubmit?: FieldInputEvent;
testId?: string;
};
export const BooleanFieldInput = ({
onSubmit,
testId,
}: BooleanFieldInputProps) => {
const { fieldValue } = useBooleanField();
const persistField = usePersistField();
const handleToggle = (newValue: boolean) => {
onSubmit?.(() => persistField(newValue));
};
return (
<BooleanInput
value={fieldValue ?? ''}
onToggle={handleToggle}
testId={testId}
/>
);
};

View File

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

View File

@ -0,0 +1,62 @@
import { DateInput } from '@/ui/object/field/meta-types/input/components/internal/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateField } from '../../hooks/useDateField';
export type FieldInputEvent = (persist: () => void) => void;
export type DateFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const DateFieldInput = ({
onEnter,
onEscape,
onClickOutside,
}: DateFieldInputProps) => {
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,69 @@
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type DoubleTextChipFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const DoubleTextChipFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: DoubleTextChipFieldInputProps) => {
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 (
<FieldInputOverlay>
<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}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,69 @@
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type DoubleTextFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const DoubleTextFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: DoubleTextFieldInputProps) => {
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 (
<FieldInputOverlay>
<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}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,66 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useEmailField } from '../../hooks/useEmailField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type EmailFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const EmailFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: EmailFieldInputProps) => {
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 (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,93 @@
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldDoubleText } from '../../../types/FieldDoubleText';
import { useMoneyAmountV2Field } from '../../hooks/useMoneyAmountV2Field';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type MoneyAmountV2FieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const MoneyAmountV2FieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: MoneyAmountV2FieldInputProps) => {
const { fieldValue, hotkeyScope } = useMoneyAmountV2Field();
const persistField = usePersistField();
const handleEnter = (newDoubleText: FieldDoubleText) => {
onEnter?.(() =>
persistField({
amount: parseFloat(newDoubleText.firstValue),
currency: newDoubleText.secondValue,
}),
);
};
const handleEscape = (newDoubleText: FieldDoubleText) => {
onEscape?.(() =>
persistField({
amount: parseFloat(newDoubleText.firstValue),
currency: newDoubleText.secondValue,
}),
);
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() =>
persistField({
amount: parseFloat(newDoubleText.firstValue),
currency: newDoubleText.secondValue,
}),
);
};
const handleTab = (newDoubleText: FieldDoubleText) => {
onTab?.(() =>
persistField({
amount: parseFloat(newDoubleText.firstValue),
currency: newDoubleText.secondValue,
}),
);
};
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
onShiftTab?.(() =>
persistField({
amount: parseFloat(newDoubleText.firstValue),
currency: newDoubleText.secondValue,
}),
);
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={fieldValue.amount?.toString() ?? ''}
secondValue={fieldValue.currency ?? ''}
firstValuePlaceholder="Amount"
secondValuePlaceholder="Currency"
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

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

View File

@ -0,0 +1,65 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useNumberField } from '../../hooks/useNumberField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
export type FieldInputEvent = (persist: () => void) => void;
export type NumberFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const NumberFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: NumberFieldInputProps) => {
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 (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue?.toString() ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,64 @@
import { PhoneInput } from '@/ui/object/field/meta-types/input/components/internal/PhoneInput';
import { usePhoneField } from '../../hooks/usePhoneField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type PhoneFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const PhoneFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: PhoneFieldInputProps) => {
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 (
<FieldInputOverlay>
<PhoneInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,29 @@
import { ProbabilityInput } from '@/ui/object/field/meta-types/input/components/internal/ProbabilityInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useProbabilityField } from '../../hooks/useProbabilityField';
import { FieldInputEvent } from './DateFieldInput';
export type ProbabilityFieldInputProps = {
onSubmit?: FieldInputEvent;
};
export const ProbabilityFieldInput = ({
onSubmit,
}: ProbabilityFieldInputProps) => {
const { probabilityIndex } = useProbabilityField();
const persistField = usePersistField();
const handleChange = (newValue: number) => {
onSubmit?.(() => persistField(newValue));
};
return (
<ProbabilityInput
probabilityIndex={probabilityIndex}
onChange={handleChange}
/>
);
};

View File

@ -0,0 +1,61 @@
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: -1px;
position: absolute;
top: -1px;
`;
export type RelationFieldInputProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;
};
export const RelationFieldInput = ({
onSubmit,
onCancel,
}: RelationFieldInputProps) => {
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,66 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type TextFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const TextFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: TextFieldInputProps) => {
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 (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

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

View File

@ -0,0 +1,89 @@
import { FieldDoubleText } from '../../../types/FieldDoubleText';
import { useURLV2Field } from '../../hooks/useURLV2Field';
import { DoubleTextInput } from './internal/DoubleTextInput';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type URLV2FieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const URLV2FieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: URLV2FieldInputProps) => {
const { fieldValue, hotkeyScope, persistURLField } = useURLV2Field();
const handleEnter = (newURL: FieldDoubleText) => {
onEnter?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleEscape = (newURL: FieldDoubleText) => {
onEscape?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newURL: FieldDoubleText,
) => {
onClickOutside?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleTab = (newURL: FieldDoubleText) => {
onTab?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleShiftTab = (newURL: FieldDoubleText) => {
onShiftTab?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={fieldValue.link}
secondValue={fieldValue.text}
firstValuePlaceholder={'Link'}
secondValuePlaceholder={'Label'}
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
/>
</FieldInputOverlay>
);
};

View File

@ -0,0 +1,100 @@
import { useEffect } from 'react';
import { jest } from '@storybook/jest';
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useBooleanField } from '../../../hooks/useBooleanField';
import {
BooleanFieldInput,
BooleanFieldInputProps,
} from '../BooleanFieldInput';
const BooleanFieldValueSetterEffect = ({ value }: { value: boolean }) => {
const { setFieldValue } = useBooleanField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type BooleanFieldInputWithContextProps = BooleanFieldInputProps & {
value: boolean;
entityId?: string;
};
const BooleanFieldInputWithContext = ({
value,
entityId,
onSubmit,
}: BooleanFieldInputWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'boolean',
label: 'Boolean',
type: 'boolean',
metadata: {
fieldName: 'Boolean',
},
}}
entityId={entityId}
>
<BooleanFieldValueSetterEffect value={value} />
<BooleanFieldInput onSubmit={onSubmit} testId="boolean-field-input" />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Input/BooleanFieldInput',
component: BooleanFieldInputWithContext,
args: {
value: true,
},
};
export default meta;
type Story = StoryObj<typeof BooleanFieldInputWithContext>;
const submitJestFn = jest.fn();
export const Default: Story = {};
export const Toggle: Story = {
args: {
onSubmit: submitJestFn,
},
argTypes: {
onSubmit: {
control: false,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByTestId('boolean-field-input');
const trueText = await within(input).findByText('True');
await expect(trueText).toBeInTheDocument();
await expect(submitJestFn).toHaveBeenCalledTimes(0);
await userEvent.click(input);
await expect(input).toHaveTextContent('False');
await expect(submitJestFn).toHaveBeenCalledTimes(1);
await userEvent.click(input);
await expect(input).toHaveTextContent('True');
await expect(submitJestFn).toHaveBeenCalledTimes(2);
},
};

View File

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

View File

@ -0,0 +1,131 @@
import { useEffect } from 'react';
import { expect } from '@storybook/jest';
import { jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDateField } from '../../../hooks/useDateField';
import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput';
const formattedDate = new Date();
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
const { setFieldValue } = useDateField();
useEffect(() => {
setFieldValue(value.toISOString());
}, [setFieldValue, value]);
return <></>;
};
type DateFieldInputWithContextProps = DateFieldInputProps & {
value: Date;
entityId?: string;
};
const DateFieldInputWithContext = ({
value,
entityId,
onEscape,
onEnter,
onClickOutside,
}: DateFieldInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {
setHotkeyScope('hotkey-scope');
}, [setHotkeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldId: 'date',
label: 'Date',
type: 'date',
metadata: {
fieldName: 'Date',
},
}}
entityId={entityId}
>
<DateFieldValueSetterEffect value={value} />
<DateFieldInput
onEscape={onEscape}
onEnter={onEnter}
onClickOutside={onClickOutside}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div"></div>
</div>
);
};
const escapeJestFn = jest.fn();
const enterJestFn = jest.fn();
const clickOutsideJestFn = jest.fn();
const meta: Meta = {
title: 'UI/Data/Field/Input/DateFieldInput',
component: DateFieldInputWithContext,
args: {
value: formattedDate,
onEscape: escapeJestFn,
onEnter: enterJestFn,
onClickOutside: clickOutsideJestFn,
},
argTypes: {
onEscape: {
control: false,
},
onEnter: {
control: false,
},
onClickOutside: {
control: false,
},
},
};
export default meta;
type Story = StoryObj<typeof DateFieldInputWithContext>;
export const Default: Story = {};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await userEvent.click(emptyDiv);
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
},
};
export const Escape: Story = {
play: async () => {
await expect(escapeJestFn).toHaveBeenCalledTimes(0);
await userEvent.keyboard('{esc}');
await expect(escapeJestFn).toHaveBeenCalledTimes(1);
},
};
export const Enter: Story = {
play: async () => {
await expect(enterJestFn).toHaveBeenCalledTimes(0);
await userEvent.keyboard('{enter}');
await expect(enterJestFn).toHaveBeenCalledTimes(1);
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,106 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useProbabilityField } from '../../../hooks/useProbabilityField';
import {
ProbabilityFieldInput,
ProbabilityFieldInputProps,
} from '../ProbabilityFieldInput';
const ProbabilityFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useProbabilityField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type ProbabilityFieldInputWithContextProps = ProbabilityFieldInputProps & {
value: number;
entityId?: string;
};
const ProbabilityFieldInputWithContext = ({
entityId,
value,
onSubmit,
}: ProbabilityFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<FieldContextProvider
fieldDefinition={{
fieldId: 'probability',
label: 'Probability',
type: 'probability',
metadata: {
fieldName: 'Probability',
},
}}
entityId={entityId}
>
<ProbabilityFieldValueSetterEffect value={value} />
<ProbabilityFieldInput onSubmit={onSubmit} />
</FieldContextProvider>
);
};
const submitJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
submitJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/ProbabilityFieldInput',
component: ProbabilityFieldInputWithContext,
args: {
value: 25,
isPositive: true,
onSubmit: submitJestFn,
},
argTypes: {
onSubmit: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof ProbabilityFieldInputWithContext>;
export const Default: Story = {};
export const Submit: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(submitJestFn).toHaveBeenCalledTimes(0);
const item = (await canvas.findByText('25%'))?.nextElementSibling
?.firstElementChild;
if (item) {
userEvent.click(item);
}
expect(submitJestFn).toHaveBeenCalledTimes(1);
},
};

View File

@ -0,0 +1,134 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useRelationField } from '../../../hooks/useRelationField';
import {
RelationFieldInput,
RelationFieldInputProps,
} from '../RelationFieldInput';
const RelationFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useRelationField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type RelationFieldInputWithContextProps = RelationFieldInputProps & {
value: number;
entityId?: string;
};
const RelationFieldInputWithContext = ({
entityId,
value,
onSubmit,
onCancel,
}: RelationFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldId: 'relation',
label: 'Relation',
type: 'relation',
metadata: {
fieldName: 'Relation',
relationType: Entity.Person,
},
}}
entityId={entityId}
>
<RelationFieldValueSetterEffect value={value} />
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const submitJestFn = jest.fn();
const cancelJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
submitJestFn.mockClear();
cancelJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/RelationFieldInput',
component: RelationFieldInputWithContext,
args: {
useEditButton: true,
onSubmit: submitJestFn,
onCancel: cancelJestFn,
},
argTypes: {
onSubmit: { control: false },
onCancel: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RelationFieldInputWithContext>;
export const Default: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
};
export const Submit: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(submitJestFn).toHaveBeenCalledTimes(0);
const item = await canvas.findByText('Jane Doe');
userEvent.click(item);
expect(submitJestFn).toHaveBeenCalledTimes(1);
},
};
export const Cancel: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(cancelJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(cancelJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

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

View File

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

View File

@ -0,0 +1,59 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck, IconX } from '@/ui/display/icon';
const StyledEditableBooleanFieldContainer = styled.div`
align-items: center;
cursor: pointer;
display: flex;
height: 100%;
width: 100%;
`;
const StyledEditableBooleanFieldValue = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
`;
type BooleanInputProps = {
value: boolean;
onToggle?: (newValue: boolean) => void;
testId?: string;
};
export const BooleanInput = ({
value,
onToggle,
testId,
}: BooleanInputProps) => {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
const handleClick = () => {
setInternalValue(!internalValue);
onToggle?.(!internalValue);
};
const theme = useTheme();
return (
<StyledEditableBooleanFieldContainer
onClick={handleClick}
data-testid={testId}
>
{internalValue ? (
<IconCheck size={theme.icon.size.sm} />
) : (
<IconX size={theme.icon.size.sm} />
)}
<StyledEditableBooleanFieldValue>
{internalValue ? 'True' : 'False'}
</StyledEditableBooleanFieldValue>
</StyledEditableBooleanFieldContainer>
);
};

View File

@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { DateDisplay } from '@/ui/object/field/meta-types/display/content-display/components/DateDisplay';
import { Nullable } from '~/types/Nullable';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
`;
const StyledInputContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0)} ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export type DateInputProps = {
value: Nullable<Date>;
onEnter: (newDate: Nullable<Date>) => void;
onEscape: (newDate: Nullable<Date>) => void;
onClickOutside: (
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => void;
hotkeyScope: string;
};
export const DateInput = ({
value,
hotkeyScope,
onEnter,
onEscape,
onClickOutside,
}: DateInputProps) => {
const theme = useTheme();
const [internalValue, setInternalValue] = useState(value);
const wrapperRef = useRef<HTMLDivElement>(null);
const { refs, floatingStyles } = useFloating({
placement: 'bottom-start',
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * 2,
}),
],
});
const handleChange = (newDate: Date) => {
setInternalValue(newDate);
};
useEffect(() => {
setInternalValue(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalValue,
onEnter,
onEscape,
onClickOutside,
hotkeyScope,
});
return (
<div ref={wrapperRef}>
<div ref={refs.setReference}>
<StyledInputContainer>
<DateDisplay value={internalValue ?? new Date()} />
</StyledInputContainer>
</div>
<div ref={refs.setFloating} style={floatingStyles}>
<StyledCalendarContainer>
<InternalDatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date) => {
onEnter(newDate);
}}
/>
</StyledCalendarContainer>
</div>
</div>
);
};

View File

@ -0,0 +1,171 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { StyledInput } from './TextInput';
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
input {
width: ${({ theme }) => theme.spacing(24)};
}
& > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
}
`;
type DoubleTextInputProps = {
firstValue: string;
secondValue: string;
firstValuePlaceholder: string;
secondValuePlaceholder: string;
hotkeyScope: string;
onEnter: (newDoubleTextValue: FieldDoubleText) => void;
onEscape: (newDoubleTextValue: FieldDoubleText) => void;
onTab?: (newDoubleTextValue: FieldDoubleText) => void;
onShiftTab?: (newDoubleTextValue: FieldDoubleText) => void;
onClickOutside: (
event: MouseEvent | TouchEvent,
newDoubleTextValue: FieldDoubleText,
) => void;
};
export const DoubleTextInput = ({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
hotkeyScope,
onClickOutside,
onEnter,
onEscape,
onShiftTab,
onTab,
}: DoubleTextInputProps) => {
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
const firstValueInputRef = useRef<HTMLInputElement>(null);
const secondValueInputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
}, [firstValue, secondValue]);
const handleChange = (
newFirstValue: string,
newSecondValue: string,
): void => {
setFirstInternalValue(newFirstValue);
setSecondInternalValue(newSecondValue);
};
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
useScopedHotkeys(
Key.Enter,
() => {
onEnter({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
},
hotkeyScope,
[onEnter, firstInternalValue, secondInternalValue],
);
useScopedHotkeys(
Key.Escape,
() => {
onEscape({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
},
hotkeyScope,
[onEscape, firstInternalValue, secondInternalValue],
);
useScopedHotkeys(
'tab',
() => {
if (focusPosition === 'left') {
setFocusPosition('right');
secondValueInputRef.current?.focus();
} else {
onTab?.({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
}
},
hotkeyScope,
[onTab, firstInternalValue, secondInternalValue, focusPosition],
);
useScopedHotkeys(
'shift+tab',
() => {
if (focusPosition === 'right') {
setFocusPosition('left');
firstValueInputRef.current?.focus();
} else {
onShiftTab?.({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
}
},
hotkeyScope,
[onShiftTab, firstInternalValue, secondInternalValue, focusPosition],
);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
onClickOutside?.(event, {
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
},
enabled: isDefined(onClickOutside),
});
return (
<StyledContainer ref={containerRef}>
<StyledInput
autoComplete="off"
autoFocus
onFocus={() => setFocusPosition('left')}
ref={firstValueInputRef}
placeholder={firstValuePlaceholder}
value={firstInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleChange(event.target.value, secondInternalValue);
}}
/>
<StyledInput
autoComplete="off"
onFocus={() => setFocusPosition('right')}
ref={secondValueInputRef}
placeholder={secondValuePlaceholder}
value={secondInternalValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleChange(firstInternalValue, event.target.value);
}}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,14 @@
import styled from '@emotion/styled';
import { overlayBackground } from '@/ui/theme/constants/effects';
const StyledFieldInputOverlay = styled.div`
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
border-radius: ${({ theme }) => theme.border.radius.sm};
${overlayBackground}
display: flex;
height: 32px;
width: 100%;
`;
export const FieldInputOverlay = StyledFieldInputOverlay;

View File

@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from 'react';
import ReactPhoneNumberInput from 'react-phone-number-input';
import styled from '@emotion/styled';
import { CountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/CountryPickerDropdownButton';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
import 'react-phone-number-input/style.css';
const StyledContainer = styled.div`
align-items: center;
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
justify-content: center;
`;
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
font-family: ${({ theme }) => theme.font.family};
height: 32px;
.PhoneInputInput {
background: ${({ theme }) => theme.background.transparent.secondary};
border: none;
color: ${({ theme }) => theme.font.color.primary};
&::placeholder,
&::-webkit-input-placeholder {
color: ${({ theme }) => theme.font.color.light};
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
:focus {
outline: none;
}
}
& svg {
border-radius: ${({ theme }) => theme.border.radius.xs};
height: 12px;
}
`;
export type PhoneInputProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
onEnter: (newText: string) => void;
onEscape: (newText: string) => void;
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
hotkeyScope: string;
};
export const PhoneInput = ({
autoFocus,
value,
onEnter,
onEscape,
onTab,
onShiftTab,
onClickOutside,
hotkeyScope,
}: PhoneInputProps) => {
const [internalValue, setInternalValue] = useState<string | undefined>(value);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setInternalValue(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalValue ?? '',
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
hotkeyScope,
});
return (
<StyledContainer ref={wrapperRef}>
<StyledCustomPhoneInput
autoFocus={autoFocus}
placeholder="Phone number"
value={value}
onChange={setInternalValue}
international={true}
withCountryCallingCode={true}
countrySelectComponent={CountryPickerDropdownButton}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,108 @@
import { useState } from 'react';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
`;
const StyledProgressBarItemContainer = styled.div`
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledProgressBarItem = styled.div<{
isFirst: boolean;
isLast: boolean;
isActive: boolean;
}>`
background-color: ${({ theme, isActive }) =>
isActive
? theme.font.color.secondary
: theme.background.transparent.medium};
border-bottom-left-radius: ${({ theme, isFirst }) =>
isFirst ? theme.border.radius.sm : theme.border.radius.xs};
border-bottom-right-radius: ${({ theme, isLast }) =>
isLast ? theme.border.radius.sm : theme.border.radius.xs};
border-top-left-radius: ${({ theme, isFirst }) =>
isFirst ? theme.border.radius.sm : theme.border.radius.xs};
border-top-right-radius: ${({ theme, isLast }) =>
isLast ? theme.border.radius.sm : theme.border.radius.xs};
height: ${({ theme }) => theme.spacing(2)};
width: ${({ theme }) => theme.spacing(3)};
`;
const StyledProgressBarContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
`;
const StyledLabel = styled.div`
width: ${({ theme }) => theme.spacing(12)};
`;
const PROBABILITY_VALUES = [
{ label: '0%', value: 0 },
{ label: '25%', value: 25 },
{ label: '50%', value: 50 },
{ label: '75%', value: 75 },
{ label: '100%', value: 100 },
];
type ProbabilityInputProps = {
probabilityIndex: number | null;
onChange: (newValue: number) => void;
};
export const ProbabilityInput = ({
onChange,
probabilityIndex,
}: ProbabilityInputProps) => {
const [hoveredProbabilityIndex, setHoveredProbabilityIndex] = useState<
number | null
>(null);
const probabilityIndexToShow =
hoveredProbabilityIndex ?? probabilityIndex ?? 0;
return (
<StyledContainer>
<StyledLabel>
{PROBABILITY_VALUES[probabilityIndexToShow].label}
</StyledLabel>
<StyledProgressBarContainer>
{PROBABILITY_VALUES.map((probability, probabilityIndexToSelect) => (
<StyledProgressBarItemContainer
key={probabilityIndexToSelect}
onClick={() => onChange(probability.value)}
onMouseEnter={() =>
setHoveredProbabilityIndex(probabilityIndexToSelect)
}
onMouseLeave={() => setHoveredProbabilityIndex(null)}
>
<StyledProgressBarItem
isActive={
hoveredProbabilityIndex || hoveredProbabilityIndex === 0
? probabilityIndexToSelect <= hoveredProbabilityIndex
: probabilityIndexToSelect <= probabilityIndexToShow
}
key={probability.label}
isFirst={probabilityIndexToSelect === 0}
isLast={
probabilityIndexToSelect === PROBABILITY_VALUES.length - 1
}
/>
</StyledProgressBarItemContainer>
))}
</StyledProgressBarContainer>
</StyledContainer>
);
};

View File

@ -0,0 +1,70 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/theme/constants/effects';
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
export const StyledInput = styled.input`
margin: 0;
${textInputStyle}
width: 100%;
`;
type TextInputProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
onEnter: (newText: string) => void;
onEscape: (newText: string) => void;
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
hotkeyScope: string;
};
export const TextInput = ({
placeholder,
autoFocus,
value,
hotkeyScope,
onEnter,
onEscape,
onTab,
onShiftTab,
onClickOutside,
}: TextInputProps) => {
const [internalText, setInternalText] = useState(value);
const wrapperRef = useRef<HTMLInputElement>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
};
useEffect(() => {
setInternalText(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalText,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
hotkeyScope,
});
return (
<StyledInput
autoComplete="off"
ref={wrapperRef}
placeholder={placeholder}
onChange={handleChange}
autoFocus={autoFocus}
value={internalText}
/>
);
};

View File

@ -0,0 +1,69 @@
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
export const useRegisterInputEvents = <T>({
inputRef,
inputValue,
onEscape,
onEnter,
onTab,
onShiftTab,
onClickOutside,
hotkeyScope,
}: {
inputRef: React.RefObject<any>;
inputValue: T;
onEscape: (inputValue: T) => void;
onEnter: (inputValue: T) => void;
onTab?: (inputValue: T) => void;
onShiftTab?: (inputValue: T) => void;
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
hotkeyScope: string;
}) => {
useListenClickOutside({
refs: [inputRef],
callback: (event) => {
event.stopImmediatePropagation();
onClickOutside?.(event, inputValue);
},
enabled: isDefined(onClickOutside),
});
useScopedHotkeys(
'enter',
() => {
onEnter?.(inputValue);
},
hotkeyScope,
[onEnter, inputValue],
);
useScopedHotkeys(
'esc',
() => {
onEscape?.(inputValue);
},
hotkeyScope,
[onEscape, inputValue],
);
useScopedHotkeys(
'tab',
() => {
onTab?.(inputValue);
},
hotkeyScope,
[onTab, inputValue],
);
useScopedHotkeys(
'shift+tab',
() => {
onShiftTab?.(inputValue);
},
hotkeyScope,
[onShiftTab, inputValue],
);
};

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,96 @@
import { selectorFamily } from 'recoil';
import { FieldDefinition } from '../../types/FieldDefinition';
import { FieldMetadata } from '../../types/FieldMetadata';
import { isFieldChip } from '../../types/guards/isFieldChip';
import { isFieldDate } from '../../types/guards/isFieldDate';
import { isFieldDoubleTextChip } from '../../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../../types/guards/isFieldEmail';
import { isFieldMoney } from '../../types/guards/isFieldMoney';
import { 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' | 'fieldId' | 'label'
>;
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;
}
} else if (isFieldChip(fieldDefinition)) {
const contentFieldName = fieldDefinition.metadata.contentFieldName;
const contentFieldValue = get(entityFieldsFamilyState(entityId))?.[
contentFieldName
] as string | null;
return (
contentFieldValue === null ||
contentFieldValue === undefined ||
contentFieldValue === ''
);
} else if (isFieldDoubleTextChip(fieldDefinition)) {
const firstValueFieldName =
fieldDefinition.metadata.firstValueFieldName;
const secondValueFieldName =
fieldDefinition.metadata.secondValueFieldName;
const contentFieldFirstValue = get(entityFieldsFamilyState(entityId))?.[
firstValueFieldName
] as string | null;
const contentFieldSecondValue = get(
entityFieldsFamilyState(entityId),
)?.[secondValueFieldName] as string | null;
return (
(contentFieldFirstValue === null ||
contentFieldFirstValue === undefined ||
contentFieldFirstValue === '') &&
(contentFieldSecondValue === null ||
contentFieldSecondValue === undefined ||
contentFieldSecondValue === '')
);
}
return false;
};
},
});

View File

@ -0,0 +1,20 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { AvatarType } from '@/users/components/Avatar';
import { FieldMetadata } from './FieldMetadata';
import { FieldType } from './FieldType';
export type FieldDefinition<T extends FieldMetadata> = {
fieldId: string;
label: string;
Icon?: IconComponent;
type: FieldType;
metadata: T;
basePathToShowPage?: string;
infoTooltipContent?: string;
entityChipDisplayMapper?: (dataObject: any) => {
name: string;
pictureUrl?: string;
avatarType: AvatarType;
};
};

View File

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

Some files were not shown because too many files have changed in this diff Show More