2282 Rename components to use the new naming convention part 1 (#2293)
renaming in progress
This commit is contained in:
@ -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 />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
170
front/src/modules/ui/object/field/components/FieldInput.tsx
Normal file
170
front/src/modules/ui/object/field/components/FieldInput.tsx
Normal 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}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
front/src/modules/ui/object/field/contexts/FieldContext.ts
Normal file
17
front/src/modules/ui/object/field/contexts/FieldContext.ts
Normal 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,
|
||||
);
|
||||
22
front/src/modules/ui/object/field/hooks/useGetButtonIcon.ts
Normal file
22
front/src/modules/ui/object/field/hooks/useGetButtonIcon.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
23
front/src/modules/ui/object/field/hooks/useIsFieldEmpty.ts
Normal file
23
front/src/modules/ui/object/field/hooks/useIsFieldEmpty.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isEntityFieldEmptyFamilySelector } from '../states/selectors/isEntityFieldEmptyFamilySelector';
|
||||
|
||||
export const useIsFieldEmpty = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const isFieldEmpty = useRecoilValue(
|
||||
isEntityFieldEmptyFamilySelector({
|
||||
fieldDefinition: {
|
||||
fieldId: fieldDefinition.fieldId,
|
||||
label: fieldDefinition.label,
|
||||
type: fieldDefinition.type,
|
||||
metadata: fieldDefinition.metadata,
|
||||
},
|
||||
entityId,
|
||||
}),
|
||||
);
|
||||
|
||||
return isFieldEmpty;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
201
front/src/modules/ui/object/field/hooks/usePersistField.ts
Normal file
201
front/src/modules/ui/object/field/hooks/usePersistField.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { useChipField } from '../../hooks/useChipField';
|
||||
import { ChipDisplay } from '../content-display/components/ChipDisplay';
|
||||
|
||||
export const ChipFieldDisplay = () => {
|
||||
const { avatarFieldValue, contentFieldValue, entityType, entityId } =
|
||||
useChipField();
|
||||
|
||||
return (
|
||||
<ChipDisplay
|
||||
displayName={contentFieldValue}
|
||||
avatarUrlValue={avatarFieldValue}
|
||||
entityType={entityType}
|
||||
entityId={entityId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { DateDisplay } from '@/ui/object/field/meta-types/display/content-display/components/DateDisplay';
|
||||
|
||||
import { useDateField } from '../../hooks/useDateField';
|
||||
|
||||
export const DateFieldDisplay = () => {
|
||||
const { fieldValue } = useDateField();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
|
||||
import { ChipDisplay } from '../content-display/components/ChipDisplay';
|
||||
|
||||
export const DoubleTextChipFieldDisplay = () => {
|
||||
const { avatarUrl, firstValue, secondValue, entityType, entityId } =
|
||||
useDoubleTextChipField();
|
||||
|
||||
const content = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<ChipDisplay
|
||||
displayName={content}
|
||||
avatarUrlValue={avatarUrl}
|
||||
entityType={entityType}
|
||||
entityId={entityId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
|
||||
import { TextDisplay } from '../content-display/components/TextDisplay';
|
||||
|
||||
export const DoubleTextFieldDisplay = () => {
|
||||
const { firstValue, secondValue } = useDoubleTextField();
|
||||
|
||||
const content = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return <TextDisplay text={content} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useEmailField } from '../../hooks/useEmailField';
|
||||
import { EmailDisplay } from '../content-display/components/EmailDisplay';
|
||||
|
||||
export const EmailFieldDisplay = () => {
|
||||
const { fieldValue } = useEmailField();
|
||||
|
||||
return <EmailDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useMoneyAmountV2Field } from '../../hooks/useMoneyAmountV2Field';
|
||||
import { MoneyAmountV2Display } from '../content-display/components/MoneyAmountV2Display';
|
||||
|
||||
export const MoneyAmountV2FieldDisplay = () => {
|
||||
const { fieldValue } = useMoneyAmountV2Field();
|
||||
|
||||
return <MoneyAmountV2Display value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useMoneyField } from '../../hooks/useMoneyField';
|
||||
import { MoneyDisplay } from '../content-display/components/MoneyDisplay';
|
||||
|
||||
export const MoneyFieldDisplay = () => {
|
||||
const { fieldValue } = useMoneyField();
|
||||
|
||||
return <MoneyDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { NumberDisplay } from '@/ui/object/field/meta-types/display/content-display/components/NumberDisplay';
|
||||
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export const NumberFieldDisplay = () => {
|
||||
const { fieldValue } = useNumberField();
|
||||
|
||||
return <NumberDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { PhoneDisplay } from '@/ui/object/field/meta-types/display/content-display/components/PhoneDisplay';
|
||||
|
||||
import { usePhoneField } from '../../hooks/usePhoneField';
|
||||
|
||||
export const PhoneFieldDisplay = () => {
|
||||
const { fieldValue } = usePhoneField();
|
||||
|
||||
return <PhoneDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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 <> </>;
|
||||
}
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
@ -0,0 +1,3 @@
|
||||
import { TextDisplay } from './TextDisplay';
|
||||
|
||||
export const DoubleTextDisplay = TextDisplay;
|
||||
@ -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 };
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
@ -0,0 +1,9 @@
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type TextDisplayProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const TextDisplay = ({ text }: TextDisplayProps) => (
|
||||
<EllipsisDisplay>{text}</EllipsisDisplay>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldDate } from '../../types/guards/isFieldDate';
|
||||
|
||||
export const useDateField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('date', isFieldDate, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldDoubleTextChip } from '../../types/guards/isFieldDoubleTextChip';
|
||||
|
||||
export const useDoubleTextChipField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
'double-text-chip',
|
||||
isFieldDoubleTextChip,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const [firstValue, setFirstValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldDefinition.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [secondValue, setSecondValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldDefinition.metadata.secondValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [avatarUrl, setAvatarUrl] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldDefinition.metadata.avatarUrlFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const fullValue = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
const entityType = fieldDefinition.metadata.entityType;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
avatarUrl,
|
||||
setAvatarUrl,
|
||||
secondValue,
|
||||
setSecondValue,
|
||||
firstValue,
|
||||
setFirstValue,
|
||||
fullValue,
|
||||
entityType,
|
||||
entityId,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldDoubleText } from '../../types/guards/isFieldDoubleText';
|
||||
|
||||
export const useDoubleTextField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('double-text', isFieldDoubleText, fieldDefinition);
|
||||
|
||||
const [firstValue, setFirstValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldDefinition.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [secondValue, setSecondValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldDefinition.metadata.secondValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const fullValue = [firstValue, secondValue].filter(Boolean).join(' ');
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
secondValue,
|
||||
setSecondValue,
|
||||
firstValue,
|
||||
setFirstValue,
|
||||
fullValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldMoney } from '../../types/guards/isFieldMoney';
|
||||
|
||||
export const useMoneyField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('moneyAmount', isFieldMoney, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistMoneyField = (newValue: string) => {
|
||||
if (!canBeCastAsIntegerOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const castedValue = castAsIntegerOrNull(newValue);
|
||||
|
||||
persistField(castedValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistMoneyField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldNumber } from '../../types/guards/isFieldNumber';
|
||||
|
||||
export const useNumberField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('number', isFieldNumber, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistNumberField = (newValue: string) => {
|
||||
if (!canBeCastAsIntegerOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const castedValue = castAsIntegerOrNull(newValue);
|
||||
|
||||
persistField(castedValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistNumberField,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldProbability } from '../../types/guards/isFieldProbability';
|
||||
|
||||
export const useProbabilityField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('probability', isFieldProbability, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const probabilityIndex = Math.ceil((fieldValue ?? 0) / 25);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
probabilityIndex,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
// TODO: we will be able to type more precisely when we will have custom field and custom entities support
|
||||
export const useRelationField = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('relation', isFieldRelation, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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],
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const entityFieldsFamilyState = atomFamily<
|
||||
Record<string, unknown> | null,
|
||||
string
|
||||
>({
|
||||
key: 'entityFieldsFamilyState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isFieldEmptyScopedState = atomFamily<boolean, string>({
|
||||
key: 'isFieldEmptyScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '../entityFieldsFamilyState';
|
||||
|
||||
export const entityFieldsFamilySelector = selectorFamily({
|
||||
key: 'entityFieldsFamilySelector',
|
||||
get:
|
||||
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
|
||||
({ get }) =>
|
||||
get(entityFieldsFamilyState(entityId))?.[fieldName] as T,
|
||||
set:
|
||||
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
|
||||
({ set }, newValue: T) =>
|
||||
set(entityFieldsFamilyState(entityId), (prevState) => ({
|
||||
...prevState,
|
||||
[fieldName]: newValue,
|
||||
})),
|
||||
});
|
||||
@ -0,0 +1,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;
|
||||
};
|
||||
},
|
||||
});
|
||||
20
front/src/modules/ui/object/field/types/FieldDefinition.ts
Normal file
20
front/src/modules/ui/object/field/types/FieldDefinition.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
@ -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
Reference in New Issue
Block a user