Chore/move records related to record folder (#2859)
* WIP * Finished multi select filter * Cleaned console log * Fix naming * Fixed naming * Moved RelationPicker folder * Moved EntitySelect components * Moved story * Moved RelationPicker non component folders * Moved everything else
This commit is contained in:
@ -5,9 +5,14 @@ import { CompanyTeam } from '@/companies/components/CompanyTeam';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { filterAvailableFieldMetadataItem } from '@/object-record/utils/filterAvailableFieldMetadataItem';
|
||||
import { IconBuildingSkyscraper } from '@/ui/display/icon';
|
||||
import { useRelationPicker } from '@/ui/input/components/internal/relation-picker/hooks/useRelationPicker';
|
||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
|
||||
@ -18,11 +23,6 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
|
||||
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
|
||||
import { RecordInlineCell } from '@/ui/object/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/ui/object/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/ui/object/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { FileFolder, useUploadImageMutation } from '~/generated/graphql';
|
||||
|
||||
@ -4,10 +4,10 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordTable } from '@/ui/object/record-table/components/RecordTable';
|
||||
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
|
||||
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||
import { TableOptionsDropdown } from '@/ui/object/record-table/options/components/TableOptionsDropdown';
|
||||
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
|
||||
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { TableOptionsDropdown } from '@/object-record/record-table/options/components/TableOptionsDropdown';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
|
||||
@ -4,8 +4,8 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
|
||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
|
||||
|
||||
@ -8,14 +8,14 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
|
||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||
import { PageBody } from '@/ui/layout/page/PageBody';
|
||||
import { PageContainer } from '@/ui/layout/page/PageContainer';
|
||||
import { PageHeader } from '@/ui/layout/page/PageHeader';
|
||||
import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect';
|
||||
import { RecordTableActionBar } from '@/ui/object/record-table/action-bar/components/RecordTableActionBar';
|
||||
import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu';
|
||||
|
||||
import { RecordTableContainer } from './RecordTableContainer';
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
||||
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
|
||||
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
|
||||
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
|
||||
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
|
||||
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
|
||||
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 { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
|
||||
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
||||
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
|
||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldFullName } from '../types/guards/isFieldFullName';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '../types/guards/isFieldSelect';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
|
||||
export const FieldDisplay = () => {
|
||||
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
if (
|
||||
isLabelIdentifier &&
|
||||
(isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition))
|
||||
) {
|
||||
return <ChipFieldDisplay />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isFieldRelation(fieldDefinition) ? (
|
||||
<RelationFieldDisplay />
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldDisplay />
|
||||
) : isFieldUuid(fieldDefinition) ? (
|
||||
<UuidFieldDisplay />
|
||||
) : isFieldEmail(fieldDefinition) ? (
|
||||
<EmailFieldDisplay />
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateFieldDisplay />
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldDisplay />
|
||||
) : isFieldLink(fieldDefinition) ? (
|
||||
<LinkFieldDisplay />
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldDisplay />
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldDisplay />
|
||||
) : isFieldPhone(fieldDefinition) ? (
|
||||
<PhoneFieldDisplay />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldDisplay />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
130
front/src/modules/object-record/field/components/FieldInput.tsx
Normal file
130
front/src/modules/object-record/field/components/FieldInput.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FullNameFieldInput } from '@/object-record/field/meta-types/input/components/FullNameFieldInput';
|
||||
import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
|
||||
import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput';
|
||||
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
|
||||
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
|
||||
import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
|
||||
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
|
||||
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
|
||||
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
|
||||
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
|
||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||
import { FieldInputEvent } from '../types/FieldInputEvent';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
|
||||
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>
|
||||
) : isFieldPhone(fieldDefinition) ? (
|
||||
<PhoneFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : 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}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDateTime(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}
|
||||
/>
|
||||
) : isFieldLink(fieldDefinition) ? (
|
||||
<LinkFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
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;
|
||||
isLabelIdentifier: boolean;
|
||||
basePathToShowPage?: string;
|
||||
};
|
||||
|
||||
export const FieldContext = createContext<GenericFieldContextType>(
|
||||
{} as GenericFieldContextType,
|
||||
);
|
||||
@ -0,0 +1,18 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { entityFieldInitialValueFamilyState } from '../states/entityFieldInitialValueFamilyState';
|
||||
|
||||
export const useFieldInitialValue = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const fieldInitialValue = useRecoilValue(
|
||||
entityFieldInitialValueFamilyState({
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
entityId,
|
||||
}),
|
||||
);
|
||||
|
||||
return fieldInitialValue;
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { isFieldRelation } from '@/object-record/field/types/guards/isFieldRelation';
|
||||
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 { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
|
||||
export const useGetButtonIcon = (): IconComponent | undefined => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (!fieldDefinition) return undefined;
|
||||
|
||||
if (
|
||||
isFieldLink(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldPhone(fieldDefinition)
|
||||
) {
|
||||
return IconPencil;
|
||||
}
|
||||
|
||||
if (isFieldRelation(fieldDefinition)) {
|
||||
if (
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||
'workspaceMember'
|
||||
) {
|
||||
return IconPencil;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
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: {
|
||||
type: fieldDefinition.type,
|
||||
},
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
entityId,
|
||||
}),
|
||||
);
|
||||
|
||||
return isFieldEmpty;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||
|
||||
export const useIsFieldInputOnly = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
if (isFieldBoolean(fieldDefinition) || isFieldRating(fieldDefinition)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
138
front/src/modules/object-record/field/hooks/usePersistField.ts
Normal file
138
front/src/modules/object-record/field/hooks/usePersistField.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName';
|
||||
import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../states/selectors/entityFieldsFamilySelector';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||
import { isFieldCurrencyValue } from '../types/guards/isFieldCurrencyValue';
|
||||
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
||||
import { isFieldDateTimeValue } from '../types/guards/isFieldDateTimeValue';
|
||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||
import { isFieldEmailValue } from '../types/guards/isFieldEmailValue';
|
||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldLinkValue } from '../types/guards/isFieldLinkValue';
|
||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||
import { isFieldNumberValue } from '../types/guards/isFieldNumberValue';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue';
|
||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||
import { isFieldRatingValue } from '../types/guards/isFieldRatingValue';
|
||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
||||
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
|
||||
|
||||
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 fieldIsText =
|
||||
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
||||
|
||||
const fieldIsEmail =
|
||||
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);
|
||||
|
||||
const fieldIsDateTime =
|
||||
isFieldDateTime(fieldDefinition) &&
|
||||
isFieldDateTimeValue(valueToPersist);
|
||||
|
||||
const fieldIsLink =
|
||||
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
|
||||
|
||||
const fieldIsBoolean =
|
||||
isFieldBoolean(fieldDefinition) &&
|
||||
isFieldBooleanValue(valueToPersist);
|
||||
|
||||
const fieldIsProbability =
|
||||
isFieldRating(fieldDefinition) && isFieldRatingValue(valueToPersist);
|
||||
|
||||
const fieldIsNumber =
|
||||
isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist);
|
||||
|
||||
const fieldIsCurrency =
|
||||
isFieldCurrency(fieldDefinition) &&
|
||||
isFieldCurrencyValue(valueToPersist);
|
||||
|
||||
const fieldIsFullName =
|
||||
isFieldFullName(fieldDefinition) &&
|
||||
isFieldFullNameValue(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: {
|
||||
// TODO: find a more elegant way to do this ?
|
||||
// Maybe have a link between the RELATION field and the UUID field ?
|
||||
[`${fieldName}Id`]: valueToPersist?.id ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
fieldIsText ||
|
||||
fieldIsBoolean ||
|
||||
fieldIsEmail ||
|
||||
fieldIsProbability ||
|
||||
fieldIsNumber ||
|
||||
fieldIsDateTime ||
|
||||
fieldIsPhone ||
|
||||
fieldIsLink ||
|
||||
fieldIsCurrency ||
|
||||
fieldIsFullName
|
||||
) {
|
||||
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,51 @@
|
||||
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 may not be implemented in useToggleEditOnlyInput.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
[entityId, fieldDefinition, updateEntity],
|
||||
);
|
||||
|
||||
return toggleField;
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import {
|
||||
FieldContext,
|
||||
GenericFieldContextType,
|
||||
} from '@/object-record/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',
|
||||
isLabelIdentifier: false,
|
||||
recoilScopeId: '1',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
fieldDefinition,
|
||||
useUpdateEntityMutation: () => [() => undefined, {}],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { useChipField } from '@/object-record/field/meta-types/hooks/useChipField';
|
||||
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
|
||||
|
||||
export const ChipFieldDisplay = () => {
|
||||
const {
|
||||
record,
|
||||
entityId,
|
||||
identifiersMapper,
|
||||
objectNameSingular,
|
||||
basePathToShowPage,
|
||||
} = useChipField();
|
||||
|
||||
const identifiers = identifiersMapper?.(record, objectNameSingular ?? '');
|
||||
|
||||
return (
|
||||
<EntityChip
|
||||
name={identifiers?.name ?? ''}
|
||||
avatarUrl={identifiers?.avatarUrl}
|
||||
avatarType={identifiers?.avatarType}
|
||||
entityId={entityId}
|
||||
linkToEntity={basePathToShowPage + entityId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||
import { CurrencyDisplay } from '../content-display/components/CurrencyDisplay';
|
||||
|
||||
export const CurrencyFieldDisplay = () => {
|
||||
const { initialAmount } = useCurrencyField();
|
||||
|
||||
return <CurrencyDisplay amount={initialAmount} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { DateDisplay } from '@/object-record/field/meta-types/display/content-display/components/DateDisplay';
|
||||
|
||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||
|
||||
export const DateFieldDisplay = () => {
|
||||
const { fieldValue } = useDateTimeField();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -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,13 @@
|
||||
import { useFullNameField } from '@/object-record/field/meta-types/hooks/useFullNameField';
|
||||
|
||||
import { TextDisplay } from '../content-display/components/TextDisplay';
|
||||
|
||||
export const FullNameFieldDisplay = () => {
|
||||
const { fieldValue } = useFullNameField();
|
||||
|
||||
const content = [fieldValue.firstName, fieldValue.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return <TextDisplay text={content} />;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { useLinkField } from '../../hooks/useLinkField';
|
||||
import { LinkDisplay } from '../content-display/components/LinkDisplay';
|
||||
|
||||
export const LinkFieldDisplay = () => {
|
||||
const { fieldValue } = useLinkField();
|
||||
|
||||
return <LinkDisplay value={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { NumberDisplay } from '@/object-record/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 '@/object-record/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,28 @@
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
|
||||
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
export const RelationFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition } = useRelationField();
|
||||
|
||||
const { identifiersMapper } = useRelationPicker();
|
||||
|
||||
if (!fieldValue || !fieldDefinition || !identifiersMapper) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const objectIdentifiers = identifiersMapper(
|
||||
fieldValue,
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityChip
|
||||
entityId={fieldValue.id}
|
||||
name={objectIdentifiers?.name ?? ''}
|
||||
avatarUrl={objectIdentifiers?.avatarUrl}
|
||||
avatarType={objectIdentifiers?.avatarType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
|
||||
import { useSelectField } from '../../hooks/useSelectField';
|
||||
|
||||
export const SelectFieldDisplay = () => {
|
||||
const { fieldValue } = useSelectField();
|
||||
|
||||
return <Tag color={fieldValue.color} text={fieldValue.label} />;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { TextDisplay } from '@/object-record/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,8 @@
|
||||
import { TextDisplay } from '@/object-record/field/meta-types/display/content-display/components/TextDisplay';
|
||||
import { useUuidField } from '@/object-record/field/meta-types/hooks/useUuidField';
|
||||
|
||||
export const UuidFieldDisplay = () => {
|
||||
const { fieldValue } = useUuidField();
|
||||
|
||||
return <TextDisplay text={fieldValue} />;
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { useDateTimeField } from '../../../hooks/useDateTimeField';
|
||||
import { DateFieldDisplay } from '../DateFieldDisplay';
|
||||
|
||||
const formattedDate = new Date('2023-04-01');
|
||||
|
||||
const DateFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = useDateTimeField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/DateFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'date',
|
||||
label: 'Date',
|
||||
type: 'DATE_TIME',
|
||||
iconName: 'IconCalendarEvent',
|
||||
metadata: {
|
||||
fieldName: 'Date',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<DateFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: DateFieldDisplay,
|
||||
argTypes: { value: { control: 'date' } },
|
||||
args: {
|
||||
value: formattedDate,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DateFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { useEmailField } from '../../../hooks/useEmailField';
|
||||
import { EmailFieldDisplay } from '../EmailFieldDisplay';
|
||||
|
||||
const EmailFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = useEmailField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/EmailFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'email',
|
||||
label: 'Email',
|
||||
type: 'EMAIL',
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Email',
|
||||
placeHolder: 'Email',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<MemoryRouter>
|
||||
<EmailFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: EmailFieldDisplay,
|
||||
args: {
|
||||
value: 'Test@Test.test',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EmailFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { useNumberField } from '../../../hooks/useNumberField';
|
||||
import { NumberFieldDisplay } from '../NumberFieldDisplay';
|
||||
|
||||
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
|
||||
const { setFieldValue } = useNumberField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/NumberFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'number',
|
||||
label: 'Number',
|
||||
type: 'NUMBER',
|
||||
iconName: 'Icon123',
|
||||
metadata: {
|
||||
fieldName: 'Number',
|
||||
placeHolder: 'Number',
|
||||
isPositive: true,
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
useUpdateEntityMutation: () => [() => undefined, undefined],
|
||||
}}
|
||||
>
|
||||
<NumberFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: NumberFieldDisplay,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof NumberFieldDisplay>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 100,
|
||||
},
|
||||
};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
args: {
|
||||
value: 1e100,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export const Negative: Story = {
|
||||
args: {
|
||||
value: -1000,
|
||||
},
|
||||
};
|
||||
|
||||
export const Float: Story = {
|
||||
args: {
|
||||
value: 1.357802,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,68 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { usePhoneField } from '../../../hooks/usePhoneField';
|
||||
import { PhoneFieldDisplay } from '../PhoneFieldDisplay';
|
||||
|
||||
const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = usePhoneField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/PhoneFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'phone',
|
||||
label: 'Phone',
|
||||
type: 'TEXT',
|
||||
iconName: 'IconPhone',
|
||||
metadata: {
|
||||
fieldName: 'phone',
|
||||
placeHolder: 'Phone',
|
||||
objectMetadataNameSingular: 'person',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
useUpdateEntityMutation: () => [() => undefined, undefined],
|
||||
}}
|
||||
>
|
||||
<MemoryRouter>
|
||||
<PhoneFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: PhoneFieldDisplay,
|
||||
args: {
|
||||
value: '362763872687362',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PhoneFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Elipsis: Story = {
|
||||
parameters: {
|
||||
container: { width: 50 },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { FieldContext } from '../../../../contexts/FieldContext';
|
||||
import { useTextField } from '../../../hooks/useTextField';
|
||||
import { TextFieldDisplay } from '../TextFieldDisplay';
|
||||
|
||||
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
|
||||
const { setFieldValue } = useTextField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Display/TextFieldDisplay',
|
||||
decorators: [
|
||||
(Story, { args }) => (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: '',
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'text',
|
||||
label: 'Text',
|
||||
type: 'TEXT',
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Text',
|
||||
placeHolder: 'Text',
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
}}
|
||||
>
|
||||
<TextFieldValueSetterEffect value={args.value} />
|
||||
<Story />
|
||||
</FieldContext.Provider>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: TextFieldDisplay,
|
||||
args: {
|
||||
value: 'Lorem ipsum',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof TextFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
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.',
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type CurrencyDisplayProps = {
|
||||
amount?: number | null;
|
||||
};
|
||||
|
||||
// TODO: convert currencyCode to currency symbol
|
||||
export const CurrencyDisplay = ({ amount }: CurrencyDisplayProps) => {
|
||||
return <EllipsisDisplay>{amount}</EllipsisDisplay>;
|
||||
};
|
||||
@ -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,73 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldLinkValue } from '@/object-record/field/types/FieldMetadata';
|
||||
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 LinkDisplayProps = {
|
||||
value?: FieldLinkValue;
|
||||
};
|
||||
|
||||
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 LinkDisplay = ({ value }: LinkDisplayProps) => {
|
||||
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const absoluteUrl = value?.url
|
||||
? value.url.startsWith('http')
|
||||
? value.url
|
||||
: 'https://' + value.url
|
||||
: '';
|
||||
|
||||
const displayedValue = value?.label || value?.url || '';
|
||||
|
||||
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,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,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,31 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName';
|
||||
import { isFieldText } from '@/object-record/field/types/guards/isFieldText';
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
|
||||
export const useChipField = () => {
|
||||
const { entityId, fieldDefinition, basePathToShowPage } =
|
||||
useContext(FieldContext);
|
||||
|
||||
const objectNameSingular =
|
||||
isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition)
|
||||
? fieldDefinition.metadata.objectMetadataNameSingular
|
||||
: undefined;
|
||||
|
||||
const record = useRecoilValue<any | null>(entityFieldsFamilyState(entityId));
|
||||
|
||||
const { identifiersMapper } = useRelationPicker();
|
||||
|
||||
return {
|
||||
basePathToShowPage,
|
||||
entityId,
|
||||
objectNameSingular,
|
||||
record,
|
||||
identifiersMapper,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,101 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldInitialValue } from '@/object-record/field/types/FieldInitialValue';
|
||||
import { canBeCastAsIntegerOrNull } from '~/utils/cast-as-integer-or-null';
|
||||
import {
|
||||
convertCurrencyMicrosToCurrency,
|
||||
convertCurrencyToCurrencyMicros,
|
||||
} from '~/utils/convert-currency-amount';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldCurrencyValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldCurrency } from '../../types/guards/isFieldCurrency';
|
||||
import { isFieldCurrencyValue } from '../../types/guards/isFieldCurrencyValue';
|
||||
|
||||
const initializeValue = (
|
||||
fieldInitialValue: FieldInitialValue | undefined,
|
||||
fieldValue: FieldCurrencyValue,
|
||||
) => {
|
||||
if (fieldInitialValue?.isEmpty) {
|
||||
return { amount: null, currencyCode: 'USD' };
|
||||
}
|
||||
if (!isNaN(Number(fieldInitialValue?.value))) {
|
||||
return {
|
||||
amount: Number(fieldInitialValue?.value),
|
||||
currencyCode: 'USD',
|
||||
};
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return { amount: null, currencyCode: 'USD' };
|
||||
}
|
||||
|
||||
return {
|
||||
amount: convertCurrencyMicrosToCurrency(fieldValue.amountMicros),
|
||||
currencyCode: fieldValue.currencyCode,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCurrencyField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('CURRENCY', isFieldCurrency, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldCurrencyValue>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistCurrencyField = ({
|
||||
amountText,
|
||||
currencyCode,
|
||||
}: {
|
||||
amountText: string;
|
||||
currencyCode: string;
|
||||
}) => {
|
||||
if (!canBeCastAsIntegerOrNull(amountText)) {
|
||||
return;
|
||||
}
|
||||
const amount = parseFloat(amountText);
|
||||
|
||||
const newCurrencyValue = {
|
||||
amountMicros: isNaN(amount)
|
||||
? null
|
||||
: convertCurrencyToCurrencyMicros(amount),
|
||||
currencyCode: currencyCode,
|
||||
};
|
||||
|
||||
if (!isFieldCurrencyValue(newCurrencyValue)) {
|
||||
return;
|
||||
}
|
||||
persistField(newCurrencyValue);
|
||||
};
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = initializeValue(fieldInitialValue, fieldValue);
|
||||
|
||||
const initialAmount = initialValue.amount;
|
||||
const initialCurrencyCode = initialValue.currencyCode;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialAmount,
|
||||
initialCurrencyCode,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistCurrencyField,
|
||||
};
|
||||
};
|
||||
@ -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 { isFieldDateTime } from '../../types/guards/isFieldDateTime';
|
||||
|
||||
export const useDateTimeField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('DATE_TIME', isFieldDateTime, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { 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,
|
||||
}),
|
||||
);
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldFullNameValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldFullName } from '../../types/guards/isFieldFullName';
|
||||
import { isFieldFullNameValue } from '../../types/guards/isFieldFullNameValue';
|
||||
|
||||
export const useFullNameField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('FULL_NAME', isFieldFullName, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldFullNameValue>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistFullNameField = (newValue: FieldFullNameValue) => {
|
||||
if (!isFieldFullNameValue(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistField(newValue);
|
||||
};
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue: FieldFullNameValue = fieldInitialValue?.isEmpty
|
||||
? { firstName: '', lastName: '' }
|
||||
: fieldValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistFullNameField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldLinkValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldLink } from '../../types/guards/isFieldLink';
|
||||
import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue';
|
||||
|
||||
export const useLinkField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('LINK', isFieldLink, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldLinkValue>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue: FieldLinkValue = fieldInitialValue?.isEmpty
|
||||
? { url: '', label: '' }
|
||||
: fieldInitialValue?.value
|
||||
? { url: fieldInitialValue.value, label: '' }
|
||||
: fieldValue;
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistLinkField = (newValue: FieldLinkValue) => {
|
||||
if (!isFieldLinkValue(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistField(newValue);
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistLinkField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
} from '~/utils/cast-as-integer-or-null';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { 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);
|
||||
};
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty
|
||||
? null
|
||||
: !isNaN(Number(fieldInitialValue?.value))
|
||||
? Number(fieldInitialValue?.value)
|
||||
: null ?? fieldValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistNumberField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { useContext } from 'react';
|
||||
import { isPossiblePhoneNumber } from 'libphonenumber-js';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { usePersistField } from '../../hooks/usePersistField';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldPhone } from '../../types/guards/isFieldPhone';
|
||||
|
||||
export const usePhoneField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
//assertFieldMetadata('PHONE', isFieldPhone, fieldDefinition);
|
||||
assertFieldMetadata('TEXT', 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);
|
||||
};
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
persistPhoneField,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldRatingValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRating } from '../../types/guards/isFieldRating';
|
||||
|
||||
export const useRatingField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.Probability,
|
||||
isFieldRating,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldRatingValue | null>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const rating = +(fieldValue ?? 0);
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
rating,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { 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,
|
||||
}),
|
||||
);
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialSearchValue = fieldInitialValue?.isEmpty
|
||||
? null
|
||||
: fieldInitialValue?.value;
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty ? null : fieldValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue,
|
||||
initialValue,
|
||||
initialSearchValue,
|
||||
setFieldValue,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldSelectValue } from '../../types/FieldMetadata';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldSelect } from '../../types/guards/isFieldSelect';
|
||||
import { isFieldSelectValue } from '../../types/guards/isFieldSelectValue';
|
||||
|
||||
export const useSelectField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('ENUM', isFieldSelect, fieldDefinition);
|
||||
|
||||
const { fieldName } = fieldDefinition.metadata;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<FieldSelectValue>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
const fieldSelectValue = isFieldSelectValue(fieldValue)
|
||||
? fieldValue
|
||||
: { color: 'green' as ThemeColor, label: '' };
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = {
|
||||
color: 'green' as ThemeColor,
|
||||
label: fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldSelectValue?.label ?? '',
|
||||
};
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: fieldSelectValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldText } from '../../types/guards/isFieldText';
|
||||
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldTextValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: fieldTextValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid';
|
||||
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
|
||||
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
|
||||
|
||||
export const useUuidField = () => {
|
||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata('UUID', isFieldUuid, fieldDefinition);
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: entityId,
|
||||
fieldName: fieldName,
|
||||
}),
|
||||
);
|
||||
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
|
||||
|
||||
const fieldInitialValue = useFieldInitialValue();
|
||||
|
||||
const initialValue = fieldInitialValue?.isEmpty
|
||||
? ''
|
||||
: fieldInitialValue?.value ?? fieldTextValue;
|
||||
|
||||
return {
|
||||
fieldDefinition,
|
||||
fieldValue: fieldTextValue,
|
||||
initialValue,
|
||||
setFieldValue,
|
||||
hotkeyScope,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { BooleanInput } from '@/object-record/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;
|
||||
readonly?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const BooleanFieldInput = ({
|
||||
onSubmit,
|
||||
readonly,
|
||||
testId,
|
||||
}: BooleanFieldInputProps) => {
|
||||
const { fieldValue } = useBooleanField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleToggle = (newValue: boolean) => {
|
||||
onSubmit?.(() => persistField(newValue));
|
||||
};
|
||||
|
||||
return (
|
||||
<BooleanInput
|
||||
value={fieldValue ?? ''}
|
||||
onToggle={handleToggle}
|
||||
readonly={readonly}
|
||||
testId={testId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
import { TextInput } from '@/object-record/field/meta-types/input/components/internal/TextInput';
|
||||
|
||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||
|
||||
import { FieldInputOverlay } from './internal/FieldInputOverlay';
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
export type CurrencyFieldInputProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const CurrencyFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: CurrencyFieldInputProps) => {
|
||||
const {
|
||||
hotkeyScope,
|
||||
initialAmount,
|
||||
initialCurrencyCode,
|
||||
persistCurrencyField,
|
||||
} = useCurrencyField();
|
||||
|
||||
const handleEnter = (newValue: string) => {
|
||||
onEnter?.(() => {
|
||||
persistCurrencyField({
|
||||
amountText: newValue,
|
||||
currencyCode: initialCurrencyCode,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEscape = (newValue: string) => {
|
||||
onEscape?.(() => {
|
||||
persistCurrencyField({
|
||||
amountText: newValue,
|
||||
currencyCode: initialCurrencyCode,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newValue: string,
|
||||
) => {
|
||||
onClickOutside?.(() => {
|
||||
persistCurrencyField({
|
||||
amountText: newValue,
|
||||
currencyCode: initialCurrencyCode,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleTab = (newValue: string) => {
|
||||
onTab?.(() => {
|
||||
persistCurrencyField({
|
||||
amountText: newValue,
|
||||
currencyCode: initialCurrencyCode,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleShiftTab = (newValue: string) => {
|
||||
onShiftTab?.(() =>
|
||||
persistCurrencyField({
|
||||
amountText: newValue,
|
||||
currencyCode: initialCurrencyCode,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<TextInput
|
||||
value={initialAmount?.toString() ?? ''}
|
||||
autoFocus
|
||||
placeholder="Currency"
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { DateInput } from '@/object-record/field/meta-types/input/components/internal/DateInput';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||
|
||||
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 } = useDateTimeField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistDate = (newDate: Nullable<Date>) => {
|
||||
if (!newDate) {
|
||||
persistField('');
|
||||
} else {
|
||||
const newDateISO = newDate?.toISOString();
|
||||
|
||||
persistField(newDateISO);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (newDate: Nullable<Date>) => {
|
||||
onEnter?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const handleEscape = (newDate: Nullable<Date>) => {
|
||||
onEscape?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
newDate: Nullable<Date>,
|
||||
) => {
|
||||
onClickOutside?.(() => persistDate(newDate));
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
value={dateValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { TextInput } from '@/object-record/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, initialValue, 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={initialValue}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { useFullNameField } from '@/object-record/field/meta-types/hooks/useFullNameField';
|
||||
import { DoubleTextInput } from '@/object-record/field/meta-types/input/components/internal/DoubleTextInput';
|
||||
import { FieldDoubleText } from '@/object-record/field/types/FieldDoubleText';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
|
||||
import { FieldInputOverlay } from './internal/FieldInputOverlay';
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
export type FullNameFieldInputProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const FullNameFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: FullNameFieldInputProps) => {
|
||||
const { hotkeyScope, initialValue } = useFullNameField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
const convertToFullName = (newDoubleText: FieldDoubleText) => {
|
||||
return {
|
||||
firstName: newDoubleText.firstValue,
|
||||
lastName: newDoubleText.secondValue,
|
||||
};
|
||||
};
|
||||
|
||||
const handleEnter = (newDoubleText: FieldDoubleText) => {
|
||||
onEnter?.(() => persistField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleEscape = (newDoubleText: FieldDoubleText) => {
|
||||
onEscape?.(() => persistField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDoubleText: FieldDoubleText,
|
||||
) => {
|
||||
onClickOutside?.(() => persistField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleTab = (newDoubleText: FieldDoubleText) => {
|
||||
onTab?.(() => persistField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
|
||||
onShiftTab?.(() => persistField(convertToFullName(newDoubleText)));
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<DoubleTextInput
|
||||
firstValue={initialValue.firstName}
|
||||
secondValue={initialValue.lastName}
|
||||
firstValuePlaceholder={'First name'}
|
||||
secondValuePlaceholder={'Last name'}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,88 @@
|
||||
import { TextInput } from '@/object-record/field/meta-types/input/components/internal/TextInput';
|
||||
|
||||
import { useLinkField } from '../../hooks/useLinkField';
|
||||
|
||||
import { FieldInputOverlay } from './internal/FieldInputOverlay';
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
export type LinkFieldInputProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onTab?: FieldInputEvent;
|
||||
onShiftTab?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const LinkFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
}: LinkFieldInputProps) => {
|
||||
const { initialValue, hotkeyScope, persistLinkField } = useLinkField();
|
||||
|
||||
const handleEnter = (newURL: string) => {
|
||||
onEnter?.(() =>
|
||||
persistLinkField({
|
||||
url: newURL,
|
||||
label: newURL,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEscape = (newURL: string) => {
|
||||
onEscape?.(() =>
|
||||
persistLinkField({
|
||||
url: newURL,
|
||||
label: newURL,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent,
|
||||
newURL: string,
|
||||
) => {
|
||||
onClickOutside?.(() =>
|
||||
persistLinkField({
|
||||
url: newURL,
|
||||
label: newURL,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTab = (newURL: string) => {
|
||||
onTab?.(() =>
|
||||
persistLinkField({
|
||||
url: newURL,
|
||||
label: newURL,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleShiftTab = (newURL: string) => {
|
||||
onShiftTab?.(() =>
|
||||
persistLinkField({
|
||||
url: newURL,
|
||||
label: newURL,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<TextInput
|
||||
value={initialValue.url}
|
||||
autoFocus
|
||||
placeholder="URL"
|
||||
hotkeyScope={hotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import { TextInput } from '@/object-record/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, initialValue, 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={initialValue?.toString() ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
import { PhoneInput } from '@/object-record/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, initialValue, 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={initialValue}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { RatingInput } from '@/object-record/field/meta-types/input/components/internal/RatingInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useRatingField } from '../../hooks/useRatingField';
|
||||
|
||||
import { FieldInputEvent } from './DateFieldInput';
|
||||
|
||||
export type RatingFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const RatingFieldInput = ({
|
||||
onSubmit,
|
||||
readonly,
|
||||
}: RatingFieldInputProps) => {
|
||||
const { rating } = useRatingField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleChange = (newRating: number) => {
|
||||
onSubmit?.(() => persistField(`${newRating}`));
|
||||
};
|
||||
|
||||
return (
|
||||
<RatingInput value={rating} onChange={handleChange} readonly={readonly} />
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
|
||||
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, initialValue, initialSearchValue } =
|
||||
useRelationField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleSubmit = (newEntity: EntityForSelect | null) => {
|
||||
onSubmit?.(() => persistField(newEntity?.record ?? null));
|
||||
};
|
||||
|
||||
useEffect(() => {}, [initialSearchValue]);
|
||||
|
||||
return (
|
||||
<StyledRelationPickerContainer>
|
||||
<RelationPicker
|
||||
fieldDefinition={fieldDefinition}
|
||||
recordId={initialValue?.id ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
/>
|
||||
{/* {fieldDefinition.metadata.fieldName === 'person' ? (
|
||||
<PeoplePicker
|
||||
personId={initialValue?.id ?? ''}
|
||||
companyId={initialValue?.companyId ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
/>
|
||||
) : fieldDefinition.metadata.fieldName === 'company' ? (
|
||||
<CompanyPicker
|
||||
companyId={initialValue?.id ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
/>
|
||||
) : null} */}
|
||||
</StyledRelationPickerContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { TextInput } from '@/object-record/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, initialValue, 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={initialValue}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,100 @@
|
||||
import { useEffect } from 'react';
|
||||
import { expect, jest } 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={{
|
||||
fieldMetadataId: 'boolean',
|
||||
label: 'Boolean',
|
||||
iconName: 'Icon123',
|
||||
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,131 @@
|
||||
import { useEffect } from 'react';
|
||||
import { expect, 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 { useDateTimeField } from '../../../hooks/useDateTimeField';
|
||||
import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput';
|
||||
|
||||
const formattedDate = new Date();
|
||||
|
||||
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
|
||||
const { setFieldValue } = useDateTimeField();
|
||||
|
||||
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={{
|
||||
fieldMetadataId: 'date',
|
||||
label: 'Date',
|
||||
type: 'DATE_TIME',
|
||||
iconName: 'IconCalendarEvent',
|
||||
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,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 { 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={{
|
||||
fieldMetadataId: 'email',
|
||||
label: 'Email',
|
||||
type: 'EMAIL',
|
||||
iconName: 'IconLink',
|
||||
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,176 @@
|
||||
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={{
|
||||
fieldMetadataId: 'number',
|
||||
label: 'Number',
|
||||
iconName: 'Icon123',
|
||||
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,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 { 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={{
|
||||
fieldMetadataId: 'phone',
|
||||
label: 'Phone',
|
||||
type: 'TEXT',
|
||||
iconName: 'IconPhone',
|
||||
metadata: {
|
||||
fieldName: 'phone',
|
||||
placeHolder: 'Enter phone number',
|
||||
objectMetadataNameSingular: 'person',
|
||||
},
|
||||
}}
|
||||
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,107 @@
|
||||
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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldRatingValue } from '../../../../types/FieldMetadata';
|
||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||
import { useRatingField } from '../../../hooks/useRatingField';
|
||||
import { RatingFieldInput, RatingFieldInputProps } from '../RatingFieldInput';
|
||||
|
||||
const RatingFieldValueSetterEffect = ({
|
||||
value,
|
||||
}: {
|
||||
value: FieldRatingValue;
|
||||
}) => {
|
||||
const { setFieldValue } = useRatingField();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [setFieldValue, value]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type RatingFieldInputWithContextProps = RatingFieldInputProps & {
|
||||
value: FieldRatingValue;
|
||||
entityId?: string;
|
||||
};
|
||||
|
||||
const RatingFieldInputWithContext = ({
|
||||
entityId,
|
||||
value,
|
||||
onSubmit,
|
||||
}: RatingFieldInputWithContextProps) => {
|
||||
const setHotKeyScope = useSetHotkeyScope();
|
||||
|
||||
useEffect(() => {
|
||||
setHotKeyScope('hotkey-scope');
|
||||
}, [setHotKeyScope]);
|
||||
|
||||
return (
|
||||
<FieldContextProvider
|
||||
fieldDefinition={{
|
||||
fieldMetadataId: 'rating',
|
||||
label: 'Rating',
|
||||
type: FieldMetadataType.Probability,
|
||||
iconName: 'Icon123',
|
||||
metadata: {
|
||||
fieldName: 'Rating',
|
||||
},
|
||||
}}
|
||||
entityId={entityId}
|
||||
>
|
||||
<RatingFieldValueSetterEffect value={value} />
|
||||
<RatingFieldInput 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/RatingFieldInput',
|
||||
component: RatingFieldInputWithContext,
|
||||
args: {
|
||||
value: '3',
|
||||
onSubmit: submitJestFn,
|
||||
},
|
||||
argTypes: {
|
||||
onSubmit: { control: false },
|
||||
},
|
||||
decorators: [clearMocksDecorator],
|
||||
parameters: {
|
||||
clearMocks: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RatingFieldInputWithContext>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Submit: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(0);
|
||||
|
||||
const input = canvas.getByRole('slider', { name: 'Rating' });
|
||||
const firstStar = input.firstElementChild;
|
||||
|
||||
if (firstStar) userEvent.click(firstStar);
|
||||
|
||||
expect(submitJestFn).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,137 @@
|
||||
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 { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
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>
|
||||
<RelationPickerScope relationPickerScopeId="relation-picker">
|
||||
<FieldContextProvider
|
||||
fieldDefinition={{
|
||||
fieldMetadataId: 'relation',
|
||||
label: 'Relation',
|
||||
type: 'RELATION',
|
||||
iconName: 'IconLink',
|
||||
metadata: {
|
||||
fieldName: 'Relation',
|
||||
},
|
||||
}}
|
||||
entityId={entityId}
|
||||
>
|
||||
<RelationFieldValueSetterEffect value={value} />
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</FieldContextProvider>
|
||||
</RelationPickerScope>
|
||||
<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);
|
||||
|
||||
// FIXME: Failing because the picker is not fetching any items
|
||||
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,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 { 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={{
|
||||
fieldMetadataId: 'text',
|
||||
label: 'Text',
|
||||
type: 'TEXT',
|
||||
iconName: 'IconTag',
|
||||
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,61 @@
|
||||
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: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
|
||||
display: flex;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEditableBooleanFieldValue = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type BooleanInputProps = {
|
||||
value: boolean;
|
||||
onToggle?: (newValue: boolean) => void;
|
||||
readonly?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const BooleanInput = ({
|
||||
value,
|
||||
onToggle,
|
||||
readonly,
|
||||
testId,
|
||||
}: BooleanInputProps) => {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleClick = () => {
|
||||
setInternalValue(!internalValue);
|
||||
onToggle?.(!internalValue);
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledEditableBooleanFieldContainer
|
||||
onClick={readonly ? undefined : 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 { DateDisplay } from '@/object-record/field/meta-types/display/content-display/components/DateDisplay';
|
||||
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||
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 '@/object-record/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,15 @@
|
||||
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;
|
||||
margin: -1px;
|
||||
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,61 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconTwentyStarFilled } from '@/ui/display/icon/components/IconTwentyStarFilled';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledRatingIconContainer = styled.div<{ isActive?: boolean }>`
|
||||
color: ${({ isActive, theme }) =>
|
||||
isActive ? theme.font.color.secondary : theme.background.quaternary};
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
type RatingInputProps = {
|
||||
onChange: (newValue: number) => void;
|
||||
value: number;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
const RATING_LEVELS_NB = 5;
|
||||
|
||||
export const RatingInput = ({
|
||||
onChange,
|
||||
value,
|
||||
readonly,
|
||||
}: RatingInputProps) => {
|
||||
const theme = useTheme();
|
||||
const [hoveredValue, setHoveredValue] = useState<number | null>(null);
|
||||
const currentValue = hoveredValue ?? value;
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
role="slider"
|
||||
aria-label="Rating"
|
||||
aria-valuemax={RATING_LEVELS_NB}
|
||||
aria-valuemin={1}
|
||||
aria-valuenow={value}
|
||||
tabIndex={0}
|
||||
>
|
||||
{Array.from({ length: RATING_LEVELS_NB }, (_, index) => {
|
||||
const rating = index + 1;
|
||||
|
||||
return (
|
||||
<StyledRatingIconContainer
|
||||
key={index}
|
||||
isActive={rating <= currentValue}
|
||||
onClick={readonly ? undefined : () => onChange(rating)}
|
||||
onMouseEnter={readonly ? undefined : () => setHoveredValue(rating)}
|
||||
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
|
||||
>
|
||||
<IconTwentyStarFilled size={theme.icon.size.md} />
|
||||
</StyledRatingIconContainer>
|
||||
);
|
||||
})}
|
||||
</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,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FieldInitialValue } from '../types/FieldInitialValue';
|
||||
|
||||
export const entityFieldInitialValueFamilyState = atomFamily<
|
||||
FieldInitialValue | undefined,
|
||||
{ entityId: string; fieldMetadataId: string }
|
||||
>({
|
||||
key: 'entityFieldInitialValueFamilyState',
|
||||
default: undefined,
|
||||
});
|
||||
@ -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,90 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName';
|
||||
import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue';
|
||||
import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { FieldDefinition } from '../../types/FieldDefinition';
|
||||
import { FieldMetadata } from '../../types/FieldMetadata';
|
||||
import { isFieldBoolean } from '../../types/guards/isFieldBoolean';
|
||||
import { isFieldCurrency } from '../../types/guards/isFieldCurrency';
|
||||
import { isFieldCurrencyValue } from '../../types/guards/isFieldCurrencyValue';
|
||||
import { isFieldDateTime } from '../../types/guards/isFieldDateTime';
|
||||
import { isFieldEmail } from '../../types/guards/isFieldEmail';
|
||||
import { isFieldLink } from '../../types/guards/isFieldLink';
|
||||
import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue';
|
||||
import { isFieldNumber } from '../../types/guards/isFieldNumber';
|
||||
import { isFieldRating } from '../../types/guards/isFieldRating';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
import { isFieldRelationValue } from '../../types/guards/isFieldRelationValue';
|
||||
import { isFieldText } from '../../types/guards/isFieldText';
|
||||
import { entityFieldsFamilyState } from '../entityFieldsFamilyState';
|
||||
|
||||
const isValueEmpty = (value: unknown) => !assertNotNull(value) || value === '';
|
||||
|
||||
export const isEntityFieldEmptyFamilySelector = selectorFamily({
|
||||
key: 'isEntityFieldEmptyFamilySelector',
|
||||
get: ({
|
||||
fieldDefinition,
|
||||
fieldName,
|
||||
entityId,
|
||||
}: {
|
||||
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type'>;
|
||||
fieldName: string;
|
||||
entityId: string;
|
||||
}) => {
|
||||
return ({ get }) => {
|
||||
if (
|
||||
isFieldUuid(fieldDefinition) ||
|
||||
isFieldText(fieldDefinition) ||
|
||||
isFieldDateTime(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition) ||
|
||||
isFieldRating(fieldDefinition) ||
|
||||
isFieldEmail(fieldDefinition) ||
|
||||
isFieldBoolean(fieldDefinition)
|
||||
//|| isFieldPhone(fieldDefinition)
|
||||
) {
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[
|
||||
fieldName
|
||||
] as string | number | boolean | null;
|
||||
|
||||
return isValueEmpty(fieldValue);
|
||||
}
|
||||
|
||||
if (isFieldRelation(fieldDefinition)) {
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
|
||||
|
||||
return isFieldRelationValue(fieldValue) && isValueEmpty(fieldValue);
|
||||
}
|
||||
|
||||
if (isFieldCurrency(fieldDefinition)) {
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
|
||||
|
||||
return (
|
||||
!isFieldCurrencyValue(fieldValue) ||
|
||||
isValueEmpty(fieldValue?.amountMicros)
|
||||
);
|
||||
}
|
||||
|
||||
if (isFieldFullName(fieldDefinition)) {
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
|
||||
|
||||
return (
|
||||
!isFieldFullNameValue(fieldValue) ||
|
||||
isValueEmpty(fieldValue?.firstName + fieldValue?.lastName)
|
||||
);
|
||||
}
|
||||
|
||||
if (isFieldLink(fieldDefinition)) {
|
||||
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
|
||||
|
||||
return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Entity field type not supported in isEntityFieldEmptyFamilySelector : ${fieldDefinition.type}}`,
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { FieldMetadata } from './FieldMetadata';
|
||||
import { FieldType } from './FieldType';
|
||||
|
||||
export type FieldDefinitionRelationType =
|
||||
| 'FROM_MANY_OBJECTS'
|
||||
| 'FROM_ONE_OBJECT'
|
||||
| 'TO_MANY_OBJECTS'
|
||||
| 'TO_ONE_OBJECT';
|
||||
|
||||
export type FieldDefinition<T extends FieldMetadata> = {
|
||||
fieldMetadataId: string;
|
||||
label: string;
|
||||
iconName: string;
|
||||
type: FieldType;
|
||||
metadata: T;
|
||||
infoTooltipContent?: string;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldDefinition } from './FieldDefinition';
|
||||
import { FieldMetadata } from './FieldMetadata';
|
||||
|
||||
export type FieldDefinitionSerializable = Omit<
|
||||
FieldDefinition<FieldMetadata>,
|
||||
'Icon'
|
||||
>;
|
||||
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DoubleTextTypeResolver } from './resolvers/DoubleTextTypeResolver';
|
||||
|
||||
export type FieldDoubleText = z.infer<typeof DoubleTextTypeResolver>;
|
||||
@ -0,0 +1,4 @@
|
||||
export type FieldInitialValue = {
|
||||
isEmpty?: boolean;
|
||||
value?: string;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
121
front/src/modules/object-record/field/types/FieldMetadata.ts
Normal file
121
front/src/modules/object-record/field/types/FieldMetadata.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
|
||||
export type FieldUuidMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldBooleanMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldTextMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldDateTimeMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldNumberMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
placeHolder: string;
|
||||
isPositive?: boolean;
|
||||
};
|
||||
|
||||
export type FieldLinkMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldCurrencyMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
placeHolder: string;
|
||||
isPositive?: boolean;
|
||||
};
|
||||
|
||||
export type FieldFullNameMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldEmailMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldPhoneMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldRatingMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldDefinitionRelationType =
|
||||
| 'FROM_MANY_OBJECTS'
|
||||
| 'FROM_ONE_OBJECT'
|
||||
| 'TO_MANY_OBJECTS'
|
||||
| 'TO_ONE_OBJECT';
|
||||
|
||||
export type FieldRelationMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
useEditButton?: boolean;
|
||||
relationType?: FieldDefinitionRelationType;
|
||||
relationObjectMetadataNameSingular: string;
|
||||
relationObjectMetadataNamePlural: string;
|
||||
};
|
||||
|
||||
export type FieldSelectMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldMetadata =
|
||||
| FieldBooleanMetadata
|
||||
| FieldCurrencyMetadata
|
||||
| FieldDateTimeMetadata
|
||||
| FieldEmailMetadata
|
||||
| FieldFullNameMetadata
|
||||
| FieldLinkMetadata
|
||||
| FieldNumberMetadata
|
||||
| FieldPhoneMetadata
|
||||
| FieldRatingMetadata
|
||||
| FieldRelationMetadata
|
||||
| FieldSelectMetadata
|
||||
| FieldTextMetadata
|
||||
| FieldUuidMetadata;
|
||||
|
||||
export type FieldTextValue = string;
|
||||
export type FieldUUidValue = string;
|
||||
export type FieldDateTimeValue = string | null;
|
||||
export type FieldNumberValue = number | null;
|
||||
export type FieldBooleanValue = boolean;
|
||||
|
||||
export type FieldPhoneValue = string;
|
||||
export type FieldEmailValue = string;
|
||||
export type FieldLinkValue = { url: string; label: string };
|
||||
export type FieldCurrencyValue = {
|
||||
currencyCode: string;
|
||||
amountMicros: number | null;
|
||||
};
|
||||
export type FieldFullNameValue = { firstName: string; lastName: string };
|
||||
export type FieldRatingValue = '1' | '2' | '3' | '4' | '5';
|
||||
export type FieldSelectValue = { color: ThemeColor; label: string };
|
||||
|
||||
export type FieldRelationValue = EntityForSelect | null;
|
||||
18
front/src/modules/object-record/field/types/FieldType.ts
Normal file
18
front/src/modules/object-record/field/types/FieldType.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type FieldType =
|
||||
| 'BOOLEAN'
|
||||
| 'UUID'
|
||||
| 'TEXT'
|
||||
| 'RELATION'
|
||||
| 'CHIP'
|
||||
| 'DATE_TIME'
|
||||
| 'DOUBLE_TEXT_CHIP'
|
||||
| 'DOUBLE_TEXT'
|
||||
| 'EMAIL'
|
||||
| 'ENUM'
|
||||
| 'NUMBER'
|
||||
| 'PHONE'
|
||||
| 'PROBABILITY'
|
||||
| 'URL'
|
||||
| 'LINK'
|
||||
| 'CURRENCY'
|
||||
| 'FULL_NAME';
|
||||
@ -0,0 +1,71 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import {
|
||||
FieldBooleanMetadata,
|
||||
FieldCurrencyMetadata,
|
||||
FieldDateTimeMetadata,
|
||||
FieldEmailMetadata,
|
||||
FieldFullNameMetadata,
|
||||
FieldLinkMetadata,
|
||||
FieldMetadata,
|
||||
FieldNumberMetadata,
|
||||
FieldPhoneMetadata,
|
||||
FieldRatingMetadata,
|
||||
FieldRelationMetadata,
|
||||
FieldSelectMetadata,
|
||||
FieldTextMetadata,
|
||||
FieldUuidMetadata,
|
||||
} from '../FieldMetadata';
|
||||
import { FieldType } from '../FieldType';
|
||||
|
||||
type AssertFieldMetadataFunction = <
|
||||
E extends FieldType,
|
||||
T extends E extends 'BOOLEAN'
|
||||
? FieldBooleanMetadata
|
||||
: E extends 'CURRENCY'
|
||||
? FieldCurrencyMetadata
|
||||
: E extends 'FULL_NAME'
|
||||
? FieldFullNameMetadata
|
||||
: E extends 'DATE_TIME'
|
||||
? FieldDateTimeMetadata
|
||||
: E extends 'EMAIL'
|
||||
? FieldEmailMetadata
|
||||
: E extends 'ENUM'
|
||||
? FieldSelectMetadata
|
||||
: E extends 'LINK'
|
||||
? FieldLinkMetadata
|
||||
: E extends 'NUMBER'
|
||||
? FieldNumberMetadata
|
||||
: E extends 'PHONE'
|
||||
? FieldPhoneMetadata
|
||||
: E extends 'PROBABILITY'
|
||||
? FieldRatingMetadata
|
||||
: E extends 'RELATION'
|
||||
? FieldRelationMetadata
|
||||
: E extends 'TEXT'
|
||||
? FieldTextMetadata
|
||||
: E extends 'UUID'
|
||||
? FieldUuidMetadata
|
||||
: never,
|
||||
>(
|
||||
fieldType: E,
|
||||
fieldTypeGuard: (
|
||||
a: FieldDefinition<FieldMetadata>,
|
||||
) => a is FieldDefinition<T>,
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>,
|
||||
) => asserts fieldDefinition is FieldDefinition<T>;
|
||||
|
||||
export const assertFieldMetadata: AssertFieldMetadataFunction = (
|
||||
fieldType,
|
||||
fieldTypeGuard,
|
||||
fieldDefinition,
|
||||
) => {
|
||||
const fieldDefinitionType = fieldDefinition.type;
|
||||
|
||||
if (!fieldTypeGuard(fieldDefinition) || fieldDefinitionType !== fieldType) {
|
||||
throw new Error(
|
||||
`Trying to use a "${fieldDefinitionType}" field as a "${fieldType}" field. Verify that the field is defined as a type "${fieldDefinitionType}" field in assertFieldMetadata.ts.`,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldBooleanMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldBoolean = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||
): field is FieldDefinition<FieldBooleanMetadata> => field.type === 'BOOLEAN';
|
||||
@ -0,0 +1,8 @@
|
||||
import { isBoolean } from '@sniptt/guards';
|
||||
|
||||
import { FieldBooleanValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add zod
|
||||
export const isFieldBooleanValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldBooleanValue => isBoolean(fieldValue);
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldCurrencyMetadata, FieldMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldCurrency = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||
): field is FieldDefinition<FieldCurrencyMetadata> => field.type === 'CURRENCY';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user