feat: address composite field (#4492)
Added new Address field input type. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -265,6 +265,7 @@ export type FieldDeleteResponse = {
|
|||||||
|
|
||||||
/** Type of the field */
|
/** Type of the field */
|
||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
|
|||||||
@ -179,6 +179,7 @@ export type FieldDeleteResponse = {
|
|||||||
|
|
||||||
/** Type of the field */
|
/** Type of the field */
|
||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Link,
|
FieldMetadataType.Link,
|
||||||
FieldMetadataType.FullName,
|
FieldMetadataType.FullName,
|
||||||
|
FieldMetadataType.Address,
|
||||||
FieldMetadataType.Relation,
|
FieldMetadataType.Relation,
|
||||||
FieldMetadataType.Select,
|
FieldMetadataType.Select,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
@ -52,24 +53,32 @@ export const formatFieldMetadataItemAsFilterDefinition = ({
|
|||||||
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
||||||
relationObjectMetadataNameSingular:
|
relationObjectMetadataNameSingular:
|
||||||
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
||||||
type:
|
type: getFilterType(field.type),
|
||||||
field.type === FieldMetadataType.DateTime
|
|
||||||
? 'DATE_TIME'
|
|
||||||
: field.type === FieldMetadataType.Link
|
|
||||||
? 'LINK'
|
|
||||||
: field.type === FieldMetadataType.FullName
|
|
||||||
? 'FULL_NAME'
|
|
||||||
: field.type === FieldMetadataType.Number
|
|
||||||
? 'NUMBER'
|
|
||||||
: field.type === FieldMetadataType.Currency
|
|
||||||
? 'CURRENCY'
|
|
||||||
: field.type === FieldMetadataType.Email
|
|
||||||
? 'TEXT'
|
|
||||||
: field.type === FieldMetadataType.Phone
|
|
||||||
? 'TEXT'
|
|
||||||
: field.type === FieldMetadataType.Relation
|
|
||||||
? 'RELATION'
|
|
||||||
: field.type === FieldMetadataType.Select
|
|
||||||
? 'SELECT'
|
|
||||||
: 'TEXT',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFilterType = (fieldType: FieldMetadataType) => {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldMetadataType.DateTime:
|
||||||
|
return 'DATE_TIME';
|
||||||
|
case FieldMetadataType.Link:
|
||||||
|
return 'LINK';
|
||||||
|
case FieldMetadataType.FullName:
|
||||||
|
return 'FULL_NAME';
|
||||||
|
case FieldMetadataType.Number:
|
||||||
|
return 'NUMBER';
|
||||||
|
case FieldMetadataType.Currency:
|
||||||
|
return 'CURRENCY';
|
||||||
|
case FieldMetadataType.Email:
|
||||||
|
return 'EMAIL';
|
||||||
|
case FieldMetadataType.Phone:
|
||||||
|
return 'PHONE';
|
||||||
|
case FieldMetadataType.Relation:
|
||||||
|
return 'RELATION';
|
||||||
|
case FieldMetadataType.Select:
|
||||||
|
return 'SELECT';
|
||||||
|
case FieldMetadataType.Address:
|
||||||
|
return 'ADDRESS';
|
||||||
|
default:
|
||||||
|
return 'TEXT';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -106,6 +106,18 @@ ${mapObjectMetadataToGraphQLQuery({
|
|||||||
{
|
{
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
|
}`;
|
||||||
|
} else if (fieldType === 'ADDRESS') {
|
||||||
|
return `${field.name}
|
||||||
|
{
|
||||||
|
addressStreet1
|
||||||
|
addressStreet2
|
||||||
|
addressCity
|
||||||
|
addressState
|
||||||
|
addressCountry
|
||||||
|
addressPostcode
|
||||||
|
addressLat
|
||||||
|
addressLng
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,4 +8,5 @@ export type FilterType =
|
|||||||
| 'FULL_NAME'
|
| 'FULL_NAME'
|
||||||
| 'LINK'
|
| 'LINK'
|
||||||
| 'RELATION'
|
| 'RELATION'
|
||||||
|
| 'ADDRESS'
|
||||||
| 'SELECT';
|
| 'SELECT';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
|
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||||
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
||||||
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
|
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
|
||||||
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
|
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
|
||||||
@ -13,6 +14,7 @@ import { RelationFieldDisplay } from '../meta-types/display/components/RelationF
|
|||||||
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
|
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
|
||||||
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
||||||
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
|
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
|
||||||
|
import { isFieldAddress } from '../types/guards/isFieldAddress';
|
||||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||||
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
||||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||||
@ -55,5 +57,7 @@ export const FieldDisplay = () => {
|
|||||||
<PhoneFieldDisplay />
|
<PhoneFieldDisplay />
|
||||||
) : isFieldSelect(fieldDefinition) ? (
|
) : isFieldSelect(fieldDefinition) ? (
|
||||||
<SelectFieldDisplay />
|
<SelectFieldDisplay />
|
||||||
|
) : isFieldAddress(fieldDefinition) ? (
|
||||||
|
<AddressFieldDisplay />
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput';
|
||||||
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
|
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
|
||||||
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
||||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||||
@ -19,6 +20,7 @@ import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInpu
|
|||||||
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
|
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
|
||||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||||
import { FieldInputEvent } from '../types/FieldInputEvent';
|
import { FieldInputEvent } from '../types/FieldInputEvent';
|
||||||
|
import { isFieldAddress } from '../types/guards/isFieldAddress';
|
||||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||||
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
|
||||||
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
|
||||||
@ -127,6 +129,14 @@ export const FieldInput = ({
|
|||||||
<RatingFieldInput onSubmit={onSubmit} />
|
<RatingFieldInput onSubmit={onSubmit} />
|
||||||
) : isFieldSelect(fieldDefinition) ? (
|
) : isFieldSelect(fieldDefinition) ? (
|
||||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
|
) : isFieldAddress(fieldDefinition) ? (
|
||||||
|
<AddressFieldInput
|
||||||
|
onEnter={onEnter}
|
||||||
|
onEscape={onEscape}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onTab={onTab}
|
||||||
|
onShiftTab={onShiftTab}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
@ -82,6 +84,10 @@ export const usePersistField = () => {
|
|||||||
const fieldIsSelect =
|
const fieldIsSelect =
|
||||||
isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist);
|
isFieldSelect(fieldDefinition) && isFieldSelectValue(valueToPersist);
|
||||||
|
|
||||||
|
const fieldIsAddress =
|
||||||
|
isFieldAddress(fieldDefinition) &&
|
||||||
|
isFieldAddressValue(valueToPersist);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fieldIsRelation ||
|
fieldIsRelation ||
|
||||||
fieldIsText ||
|
fieldIsText ||
|
||||||
@ -94,7 +100,8 @@ export const usePersistField = () => {
|
|||||||
fieldIsLink ||
|
fieldIsLink ||
|
||||||
fieldIsCurrency ||
|
fieldIsCurrency ||
|
||||||
fieldIsFullName ||
|
fieldIsFullName ||
|
||||||
fieldIsSelect
|
fieldIsSelect ||
|
||||||
|
fieldIsAddress
|
||||||
) {
|
) {
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
set(
|
set(
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
|
||||||
|
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
||||||
|
|
||||||
|
export const AddressFieldDisplay = () => {
|
||||||
|
const { fieldValue } = useAddressField();
|
||||||
|
|
||||||
|
const content = [
|
||||||
|
fieldValue?.addressStreet1,
|
||||||
|
fieldValue?.addressStreet2,
|
||||||
|
fieldValue?.addressCity,
|
||||||
|
fieldValue?.addressCountry,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return <TextDisplay text={content} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||||
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
import { usePersistField } from '../../hooks/usePersistField';
|
||||||
|
import { FieldAddressValue } from '../../types/FieldMetadata';
|
||||||
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
|
import { isFieldAddress } from '../../types/guards/isFieldAddress';
|
||||||
|
|
||||||
|
export const useAddressField = () => {
|
||||||
|
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||||
|
|
||||||
|
assertFieldMetadata(
|
||||||
|
FieldMetadataType.Address,
|
||||||
|
isFieldAddress,
|
||||||
|
fieldDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const [fieldValue, setFieldValue] = useRecoilState<FieldAddressValue>(
|
||||||
|
recordStoreFamilySelector({
|
||||||
|
recordId: entityId,
|
||||||
|
fieldName: fieldName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
const persistAddressField = (newValue: FieldAddressValue) => {
|
||||||
|
if (!isFieldAddressValue(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
persistField(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setDraftValue, getDraftValueSelector } =
|
||||||
|
useRecordFieldInput<FieldAddressValue>(`${entityId}-${fieldName}`);
|
||||||
|
|
||||||
|
const draftValue = useRecoilValue(getDraftValueSelector());
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition,
|
||||||
|
fieldValue,
|
||||||
|
setFieldValue,
|
||||||
|
draftValue,
|
||||||
|
setDraftValue,
|
||||||
|
hotkeyScope,
|
||||||
|
persistAddressField,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
|
||||||
|
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||||
|
import { AddressInput } from '@/ui/field/input/components/AddressInput';
|
||||||
|
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
|
||||||
|
|
||||||
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|
||||||
|
import { FieldInputEvent } from './DateFieldInput';
|
||||||
|
|
||||||
|
export type AddressFieldInputProps = {
|
||||||
|
onClickOutside?: FieldInputEvent;
|
||||||
|
onEnter?: FieldInputEvent;
|
||||||
|
onEscape?: FieldInputEvent;
|
||||||
|
onTab?: FieldInputEvent;
|
||||||
|
onShiftTab?: FieldInputEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddressFieldInput = ({
|
||||||
|
onEnter,
|
||||||
|
onEscape,
|
||||||
|
onClickOutside,
|
||||||
|
onTab,
|
||||||
|
onShiftTab,
|
||||||
|
}: AddressFieldInputProps) => {
|
||||||
|
const { hotkeyScope, draftValue, setDraftValue } = useAddressField();
|
||||||
|
|
||||||
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
const convertToAddress = (
|
||||||
|
newAddress: FieldAddressDraftValue | undefined,
|
||||||
|
): FieldAddressDraftValue => {
|
||||||
|
return {
|
||||||
|
addressStreet1: newAddress?.addressStreet1 ?? '',
|
||||||
|
addressStreet2: newAddress?.addressStreet2 ?? null,
|
||||||
|
addressCity: newAddress?.addressCity ?? null,
|
||||||
|
addressState: newAddress?.addressState ?? null,
|
||||||
|
addressCountry: newAddress?.addressCountry ?? null,
|
||||||
|
addressPostcode: newAddress?.addressPostcode ?? null,
|
||||||
|
addressLat: newAddress?.addressLat ?? null,
|
||||||
|
addressLng: newAddress?.addressLng ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
onEnter?.(() => persistField(convertToAddress(newAddress)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTab = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
onTab?.(() => persistField(convertToAddress(newAddress)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShiftTab = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
onShiftTab?.(() => persistField(convertToAddress(newAddress)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEscape = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
onEscape?.(() => persistField(convertToAddress(newAddress)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = (
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
newAddress: FieldAddressDraftValue,
|
||||||
|
) => {
|
||||||
|
onClickOutside?.(() => persistField(convertToAddress(newAddress)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (newAddress: FieldAddressDraftValue) => {
|
||||||
|
setDraftValue(convertToAddress(newAddress));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldInputOverlay>
|
||||||
|
<AddressInput
|
||||||
|
value={convertToAddress(draftValue)}
|
||||||
|
onClickOutside={handleClickOutside}
|
||||||
|
onEnter={handleEnter}
|
||||||
|
onEscape={handleEscape}
|
||||||
|
hotkeyScope={hotkeyScope}
|
||||||
|
onChange={handleChange}
|
||||||
|
onTab={handleTab}
|
||||||
|
onShiftTab={handleShiftTab}
|
||||||
|
/>
|
||||||
|
</FieldInputOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, fn, userEvent, waitFor } from '@storybook/test';
|
||||||
|
|
||||||
|
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
|
||||||
|
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||||
|
import {
|
||||||
|
AddressInput,
|
||||||
|
AddressInputProps,
|
||||||
|
} from '@/ui/field/input/components/AddressInput';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
|
|
||||||
|
const AddressValueSetterEffect = ({
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
value: FieldAddressDraftValue;
|
||||||
|
}) => {
|
||||||
|
const { setFieldValue } = useAddressField();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFieldValue(value);
|
||||||
|
}, [setFieldValue, value]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddressInputWithContextProps = AddressInputProps & {
|
||||||
|
value: string;
|
||||||
|
entityId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddressInputWithContext = ({
|
||||||
|
entityId,
|
||||||
|
value,
|
||||||
|
onEnter,
|
||||||
|
onEscape,
|
||||||
|
onClickOutside,
|
||||||
|
onTab,
|
||||||
|
onShiftTab,
|
||||||
|
}: AddressInputWithContextProps) => {
|
||||||
|
const setHotKeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHotKeyScope('hotkey-scope');
|
||||||
|
}, [setHotKeyScope]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldContextProvider
|
||||||
|
fieldDefinition={{
|
||||||
|
fieldMetadataId: 'text',
|
||||||
|
label: 'Address',
|
||||||
|
type: FieldMetadataType.Address,
|
||||||
|
iconName: 'IconTag',
|
||||||
|
metadata: {
|
||||||
|
fieldName: 'Address',
|
||||||
|
placeHolder: 'Enter text',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
entityId={entityId}
|
||||||
|
>
|
||||||
|
<AddressValueSetterEffect value={value} />
|
||||||
|
<AddressInput
|
||||||
|
onEnter={onEnter}
|
||||||
|
onEscape={onEscape}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
value={value}
|
||||||
|
hotkeyScope=""
|
||||||
|
onTab={onTab}
|
||||||
|
onShiftTab={onShiftTab}
|
||||||
|
/>
|
||||||
|
</FieldContextProvider>
|
||||||
|
<div data-testid="data-field-input-click-outside-div" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enterJestFn = fn();
|
||||||
|
const escapeJestfn = fn();
|
||||||
|
const clickOutsideJestFn = fn();
|
||||||
|
const tabJestFn = fn();
|
||||||
|
const shiftTabJestFn = fn();
|
||||||
|
|
||||||
|
const clearMocksDecorator: Decorator = (Story, context) => {
|
||||||
|
if (context.parameters.clearMocks === true) {
|
||||||
|
enterJestFn.mockClear();
|
||||||
|
escapeJestfn.mockClear();
|
||||||
|
clickOutsideJestFn.mockClear();
|
||||||
|
tabJestFn.mockClear();
|
||||||
|
shiftTabJestFn.mockClear();
|
||||||
|
}
|
||||||
|
return <Story />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'UI/Data/Field/Input/AddressInput',
|
||||||
|
component: AddressInputWithContext,
|
||||||
|
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 AddressInputWithContext>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Enter: Story = {
|
||||||
|
play: async () => {
|
||||||
|
expect(enterJestFn).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
userEvent.keyboard('{enter}');
|
||||||
|
expect(enterJestFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
||||||
import {
|
import {
|
||||||
|
FieldAddressValue,
|
||||||
FieldBooleanValue,
|
FieldBooleanValue,
|
||||||
FieldCurrencyValue,
|
FieldCurrencyValue,
|
||||||
FieldDateTimeValue,
|
FieldDateTimeValue,
|
||||||
@ -28,6 +29,16 @@ export type FieldCurrencyDraftValue = {
|
|||||||
amount: string;
|
amount: string;
|
||||||
};
|
};
|
||||||
export type FieldFullNameDraftValue = { firstName: string; lastName: string };
|
export type FieldFullNameDraftValue = { firstName: string; lastName: string };
|
||||||
|
export type FieldAddressDraftValue = {
|
||||||
|
addressStreet1: string;
|
||||||
|
addressStreet2: string | null;
|
||||||
|
addressCity: string | null;
|
||||||
|
addressState: string | null;
|
||||||
|
addressPostcode: string | null;
|
||||||
|
addressCountry: string | null;
|
||||||
|
addressLat: number | null;
|
||||||
|
addressLng: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
||||||
? FieldTextDraftValue
|
? FieldTextDraftValue
|
||||||
@ -55,4 +66,6 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldSelectDraftValue
|
? FieldSelectDraftValue
|
||||||
: FieldValue extends FieldRelationValue
|
: FieldValue extends FieldRelationValue
|
||||||
? FieldRelationDraftValue
|
? FieldRelationDraftValue
|
||||||
: never;
|
: FieldValue extends FieldAddressValue
|
||||||
|
? FieldAddressDraftValue
|
||||||
|
: never;
|
||||||
|
|||||||
@ -69,6 +69,12 @@ export type FieldRatingMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldAddressMetadata = {
|
||||||
|
objectMetadataNameSingular?: string;
|
||||||
|
placeHolder: string;
|
||||||
|
fieldName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldRawJsonMetadata = {
|
export type FieldRawJsonMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -109,7 +115,8 @@ export type FieldMetadata =
|
|||||||
| FieldRelationMetadata
|
| FieldRelationMetadata
|
||||||
| FieldSelectMetadata
|
| FieldSelectMetadata
|
||||||
| FieldTextMetadata
|
| FieldTextMetadata
|
||||||
| FieldUuidMetadata;
|
| FieldUuidMetadata
|
||||||
|
| FieldAddressMetadata;
|
||||||
|
|
||||||
export type FieldTextValue = string;
|
export type FieldTextValue = string;
|
||||||
export type FieldUUidValue = string;
|
export type FieldUUidValue = string;
|
||||||
@ -125,6 +132,16 @@ export type FieldCurrencyValue = {
|
|||||||
amountMicros: number | null;
|
amountMicros: number | null;
|
||||||
};
|
};
|
||||||
export type FieldFullNameValue = { firstName: string; lastName: string };
|
export type FieldFullNameValue = { firstName: string; lastName: string };
|
||||||
|
export type FieldAddressValue = {
|
||||||
|
addressStreet1: string;
|
||||||
|
addressStreet2: string | null;
|
||||||
|
addressCity: string | null;
|
||||||
|
addressState: string | null;
|
||||||
|
addressPostcode: string | null;
|
||||||
|
addressCountry: string | null;
|
||||||
|
addressLat: number | null;
|
||||||
|
addressLng: number | null;
|
||||||
|
};
|
||||||
export type FieldRatingValue = (typeof RATING_VALUES)[number];
|
export type FieldRatingValue = (typeof RATING_VALUES)[number];
|
||||||
export type FieldSelectValue = string | null;
|
export type FieldSelectValue = string | null;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
import { FieldDefinition } from '../FieldDefinition';
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
import {
|
import {
|
||||||
|
FieldAddressMetadata,
|
||||||
FieldBooleanMetadata,
|
FieldBooleanMetadata,
|
||||||
FieldCurrencyMetadata,
|
FieldCurrencyMetadata,
|
||||||
FieldDateTimeMetadata,
|
FieldDateTimeMetadata,
|
||||||
@ -49,9 +50,11 @@ type AssertFieldMetadataFunction = <
|
|||||||
? FieldTextMetadata
|
? FieldTextMetadata
|
||||||
: E extends 'UUID'
|
: E extends 'UUID'
|
||||||
? FieldUuidMetadata
|
? FieldUuidMetadata
|
||||||
: E extends 'RAW_JSON'
|
: E extends 'ADDRESS'
|
||||||
? FieldRawJsonMetadata
|
? FieldAddressMetadata
|
||||||
: never,
|
: E extends 'RAW_JSON'
|
||||||
|
? FieldRawJsonMetadata
|
||||||
|
: never,
|
||||||
>(
|
>(
|
||||||
fieldType: E,
|
fieldType: E,
|
||||||
fieldTypeGuard: (
|
fieldTypeGuard: (
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldAddressMetadata, FieldMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldAddress = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||||
|
): field is FieldDefinition<FieldAddressMetadata> => field.type === 'ADDRESS';
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FieldAddressValue } from '../FieldMetadata';
|
||||||
|
|
||||||
|
const addressSchema = z.object({
|
||||||
|
addressStreet1: z.string(),
|
||||||
|
addressStreet2: z.string().nullable(),
|
||||||
|
addressCity: z.string().nullable(),
|
||||||
|
addressState: z.string().nullable(),
|
||||||
|
addressPostcode: z.string().nullable(),
|
||||||
|
addressCountry: z.string().nullable(),
|
||||||
|
addressLat: z.number().nullable(),
|
||||||
|
addressLng: z.number().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isFieldAddressValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldAddressValue =>
|
||||||
|
addressSchema.safeParse(fieldValue).success;
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
||||||
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
||||||
@ -68,6 +70,18 @@ export const isFieldValueEmpty = ({
|
|||||||
return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url);
|
return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldAddress(fieldDefinition)) {
|
||||||
|
return (
|
||||||
|
!isFieldAddressValue(fieldValue) ||
|
||||||
|
(isValueEmpty(fieldValue?.addressStreet1) &&
|
||||||
|
isValueEmpty(fieldValue?.addressStreet2) &&
|
||||||
|
isValueEmpty(fieldValue?.addressCity) &&
|
||||||
|
isValueEmpty(fieldValue?.addressState) &&
|
||||||
|
isValueEmpty(fieldValue?.addressPostcode) &&
|
||||||
|
isValueEmpty(fieldValue?.addressCountry))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -71,6 +71,15 @@ export type FullNameFilter = {
|
|||||||
lastName?: StringFilter;
|
lastName?: StringFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AddressFilter = {
|
||||||
|
addressStreet1?: StringFilter;
|
||||||
|
addressStreet2?: StringFilter;
|
||||||
|
addressCity?: StringFilter;
|
||||||
|
addressState?: StringFilter;
|
||||||
|
addressCountry?: StringFilter;
|
||||||
|
addressPostcode?: StringFilter;
|
||||||
|
};
|
||||||
|
|
||||||
export type LeafFilter =
|
export type LeafFilter =
|
||||||
| UUIDFilter
|
| UUIDFilter
|
||||||
| StringFilter
|
| StringFilter
|
||||||
@ -80,6 +89,7 @@ export type LeafFilter =
|
|||||||
| URLFilter
|
| URLFilter
|
||||||
| FullNameFilter
|
| FullNameFilter
|
||||||
| BooleanFilter
|
| BooleanFilter
|
||||||
|
| AddressFilter
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type AndObjectRecordFilter = {
|
export type AndObjectRecordFilter = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { isObject } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import {
|
import {
|
||||||
|
AddressFilter,
|
||||||
AndObjectRecordFilter,
|
AndObjectRecordFilter,
|
||||||
BooleanFilter,
|
BooleanFilter,
|
||||||
CurrencyFilter,
|
CurrencyFilter,
|
||||||
@ -180,6 +181,30 @@ export const isRecordMatchingFilter = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Address: {
|
||||||
|
const addressFilter = filterValue as AddressFilter;
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
'addressStreet1',
|
||||||
|
'addressStreet2',
|
||||||
|
'addressCity',
|
||||||
|
'addressState',
|
||||||
|
'addressCountry',
|
||||||
|
'addressPostcode',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return keys.some((key) => {
|
||||||
|
const value = addressFilter[key];
|
||||||
|
if (value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isMatchingStringFilter({
|
||||||
|
stringFilter: value,
|
||||||
|
value: record[filterKey][key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
case FieldMetadataType.DateTime: {
|
case FieldMetadataType.DateTime: {
|
||||||
return isMatchingDateFilter({
|
return isMatchingDateFilter({
|
||||||
dateFilter: filterValue as DateFilter,
|
dateFilter: filterValue as DateFilter,
|
||||||
|
|||||||
@ -25,6 +25,18 @@ export const generateEmptyFieldValue = (
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Address: {
|
||||||
|
return {
|
||||||
|
addressStreet1: '',
|
||||||
|
addressStreet2: '',
|
||||||
|
addressCity: '',
|
||||||
|
addressState: '',
|
||||||
|
addressCountry: '',
|
||||||
|
addressPostcode: '',
|
||||||
|
addressLat: null,
|
||||||
|
addressLng: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
case FieldMetadataType.DateTime: {
|
case FieldMetadataType.DateTime: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
IconKey,
|
IconKey,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconMail,
|
IconMail,
|
||||||
|
IconMap,
|
||||||
IconNumbers,
|
IconNumbers,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconRelationManyToMany,
|
IconRelationManyToMany,
|
||||||
@ -101,4 +102,18 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
|
|||||||
Icon: IconUser,
|
Icon: IconUser,
|
||||||
defaultValue: { firstName: 'John', lastName: 'Doe' },
|
defaultValue: { firstName: 'John', lastName: 'Doe' },
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.Address]: {
|
||||||
|
label: 'Address',
|
||||||
|
Icon: IconMap,
|
||||||
|
defaultValue: {
|
||||||
|
addressStreet1: '456 Oak Street',
|
||||||
|
addressStreet2: 'Unit 3B',
|
||||||
|
addressCity: 'Springfield',
|
||||||
|
addressState: 'California',
|
||||||
|
addressCountry: 'United States',
|
||||||
|
addressPostcode: '90210',
|
||||||
|
addressLat: 34.0522,
|
||||||
|
addressLng: -118.2437,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const previewableTypes = [
|
|||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
FieldMetadataType.Relation,
|
FieldMetadataType.Relation,
|
||||||
FieldMetadataType.Text,
|
FieldMetadataType.Text,
|
||||||
|
FieldMetadataType.Address,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SettingsDataModelFieldSettingsFormCard = ({
|
export const SettingsDataModelFieldSettingsFormCard = ({
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
|
|
||||||
export type IconComponent = FunctionComponent<{
|
export type IconComponentProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
stroke?: number;
|
stroke?: number;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
export type IconComponent = FunctionComponent<IconComponentProps>;
|
||||||
|
|||||||
@ -0,0 +1,254 @@
|
|||||||
|
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { flip, offset, useFloating } from '@floating-ui/react';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
|
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
|
||||||
|
import { FieldAddressValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { CountrySelect } from '@/ui/input/components/internal/country/components/CountrySelect';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledAddressContainer = 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};
|
||||||
|
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-width: 260px;
|
||||||
|
> div {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledHalfRowContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type AddressInputProps = {
|
||||||
|
value: FieldAddressValue;
|
||||||
|
onTab: (newAddress: FieldAddressDraftValue) => void;
|
||||||
|
onShiftTab: (newAddress: FieldAddressDraftValue) => void;
|
||||||
|
onEnter: (newAddress: FieldAddressDraftValue) => void;
|
||||||
|
onEscape: (newAddress: FieldAddressDraftValue) => void;
|
||||||
|
onClickOutside: (
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
newAddress: FieldAddressDraftValue,
|
||||||
|
) => void;
|
||||||
|
hotkeyScope: string;
|
||||||
|
clearable?: boolean;
|
||||||
|
onChange?: (updatedValue: FieldAddressDraftValue) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddressInput = ({
|
||||||
|
value,
|
||||||
|
hotkeyScope,
|
||||||
|
onTab,
|
||||||
|
onShiftTab,
|
||||||
|
onEnter,
|
||||||
|
onEscape,
|
||||||
|
onClickOutside,
|
||||||
|
onChange,
|
||||||
|
}: AddressInputProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [internalValue, setInternalValue] = useState(value);
|
||||||
|
const addressStreet1InputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const addressStreet2InputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const addressCityInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const addressStateInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const addressPostCodeInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const inputRefs: {
|
||||||
|
[key in keyof FieldAddressDraftValue]?: RefObject<HTMLInputElement>;
|
||||||
|
} = {
|
||||||
|
addressStreet1: addressStreet1InputRef,
|
||||||
|
addressStreet2: addressStreet2InputRef,
|
||||||
|
addressCity: addressCityInputRef,
|
||||||
|
addressState: addressStateInputRef,
|
||||||
|
addressPostcode: addressPostCodeInputRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [focusPosition, setFocusPosition] =
|
||||||
|
useState<keyof FieldAddressDraftValue>('addressStreet1');
|
||||||
|
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { refs, floatingStyles } = useFloating({
|
||||||
|
placement: 'top-start',
|
||||||
|
middleware: [
|
||||||
|
flip(),
|
||||||
|
offset({
|
||||||
|
mainAxis: theme.spacingMultiplicator * 2,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getChangeHandler =
|
||||||
|
(field: keyof FieldAddressDraftValue) => (updatedAddressPart: string) => {
|
||||||
|
const updatedAddress = { ...value, [field]: updatedAddressPart };
|
||||||
|
setInternalValue(updatedAddress);
|
||||||
|
onChange?.(updatedAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFocusHandler = (fieldName: keyof FieldAddressDraftValue) => () => {
|
||||||
|
setFocusPosition(fieldName);
|
||||||
|
|
||||||
|
inputRefs[fieldName]?.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'tab',
|
||||||
|
() => {
|
||||||
|
const currentFocusPosition = Object.keys(inputRefs).findIndex(
|
||||||
|
(key) => key === focusPosition,
|
||||||
|
);
|
||||||
|
const maxFocusPosition = Object.keys(inputRefs).length - 1;
|
||||||
|
|
||||||
|
const nextFocusPosition = currentFocusPosition + 1;
|
||||||
|
|
||||||
|
const isFocusPositionAfterLast = nextFocusPosition > maxFocusPosition;
|
||||||
|
|
||||||
|
if (isFocusPositionAfterLast) {
|
||||||
|
onTab?.(internalValue);
|
||||||
|
} else {
|
||||||
|
const nextFocusFieldName = Object.keys(inputRefs)[
|
||||||
|
nextFocusPosition
|
||||||
|
] as keyof FieldAddressDraftValue;
|
||||||
|
|
||||||
|
setFocusPosition(nextFocusFieldName);
|
||||||
|
inputRefs[nextFocusFieldName]?.current?.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
[onTab, internalValue, focusPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'shift+tab',
|
||||||
|
() => {
|
||||||
|
const currentFocusPosition = Object.keys(inputRefs).findIndex(
|
||||||
|
(key) => key === focusPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextFocusPosition = currentFocusPosition - 1;
|
||||||
|
|
||||||
|
const isFocusPositionBeforeFirst = nextFocusPosition < 0;
|
||||||
|
|
||||||
|
if (isFocusPositionBeforeFirst) {
|
||||||
|
onShiftTab?.(internalValue);
|
||||||
|
} else {
|
||||||
|
const nextFocusFieldName = Object.keys(inputRefs)[
|
||||||
|
nextFocusPosition
|
||||||
|
] as keyof FieldAddressDraftValue;
|
||||||
|
|
||||||
|
setFocusPosition(nextFocusFieldName);
|
||||||
|
inputRefs[nextFocusFieldName]?.current?.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
[onTab, internalValue, focusPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.Enter,
|
||||||
|
() => {
|
||||||
|
onEnter(internalValue);
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
[onEnter, internalValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Escape],
|
||||||
|
() => {
|
||||||
|
onEscape(internalValue);
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
[onEscape, internalValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { useListenClickOutside } = useClickOutsideListener('addressInput');
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [wrapperRef],
|
||||||
|
callback: (event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
onClickOutside?.(event, internalValue);
|
||||||
|
},
|
||||||
|
enabled: isDefined(onClickOutside),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={refs.setFloating} style={floatingStyles}>
|
||||||
|
<StyledAddressContainer ref={wrapperRef}>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
value={internalValue.addressStreet1 ?? ''}
|
||||||
|
ref={inputRefs['addressStreet1']}
|
||||||
|
label="ADDRESS 1"
|
||||||
|
fullWidth
|
||||||
|
onChange={getChangeHandler('addressStreet1')}
|
||||||
|
onFocus={getFocusHandler('addressStreet1')}
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={internalValue.addressStreet2 ?? ''}
|
||||||
|
ref={inputRefs['addressStreet2']}
|
||||||
|
label="ADDRESS 2"
|
||||||
|
fullWidth
|
||||||
|
onChange={getChangeHandler('addressStreet2')}
|
||||||
|
onFocus={getFocusHandler('addressStreet2')}
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
<StyledHalfRowContainer>
|
||||||
|
<TextInput
|
||||||
|
value={internalValue.addressCity ?? ''}
|
||||||
|
ref={inputRefs['addressCity']}
|
||||||
|
label="CITY"
|
||||||
|
fullWidth
|
||||||
|
onChange={getChangeHandler('addressCity')}
|
||||||
|
onFocus={getFocusHandler('addressCity')}
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={internalValue.addressState ?? ''}
|
||||||
|
ref={inputRefs['addressState']}
|
||||||
|
label="STATE"
|
||||||
|
fullWidth
|
||||||
|
onChange={getChangeHandler('addressState')}
|
||||||
|
onFocus={getFocusHandler('addressState')}
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
</StyledHalfRowContainer>
|
||||||
|
<StyledHalfRowContainer>
|
||||||
|
<TextInput
|
||||||
|
value={internalValue.addressPostcode ?? ''}
|
||||||
|
ref={inputRefs['addressPostcode']}
|
||||||
|
label="POST CODE"
|
||||||
|
fullWidth
|
||||||
|
onChange={getChangeHandler('addressPostcode')}
|
||||||
|
onFocus={getFocusHandler('addressPostcode')}
|
||||||
|
disableHotkeys
|
||||||
|
/>
|
||||||
|
<CountrySelect
|
||||||
|
onChange={getChangeHandler('addressCountry')}
|
||||||
|
selectedCountryName={internalValue.addressCountry ?? ''}
|
||||||
|
/>
|
||||||
|
</StyledHalfRowContainer>
|
||||||
|
</StyledAddressContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -13,7 +13,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { StyledInput } from './TextInput';
|
import { StyledTextInput } from './TextInput';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -174,7 +174,7 @@ export const DoubleTextInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer ref={containerRef}>
|
<StyledContainer ref={containerRef}>
|
||||||
<StyledInput
|
<StyledTextInput
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus
|
autoFocus
|
||||||
onFocus={() => setFocusPosition('left')}
|
onFocus={() => setFocusPosition('left')}
|
||||||
@ -188,7 +188,7 @@ export const DoubleTextInput = ({
|
|||||||
handleOnPaste(event)
|
handleOnPaste(event)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StyledInput
|
<StyledTextInput
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onFocus={() => setFocusPosition('right')}
|
onFocus={() => setFocusPosition('right')}
|
||||||
ref={secondValueInputRef}
|
ref={secondValueInputRef}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import ReactPhoneNumberInput from 'react-phone-number-input';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { CountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/CountryPickerDropdownButton';
|
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||||
|
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ export const PhoneInput = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
international={true}
|
international={true}
|
||||||
withCountryCallingCode={true}
|
withCountryCallingCode={true}
|
||||||
countrySelectComponent={CountryPickerDropdownButton}
|
countrySelectComponent={PhoneCountryPickerDropdownButton}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
||||||
|
|
||||||
export const StyledInput = styled.input`
|
export const StyledTextInput = styled.input`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
${TEXT_INPUT_STYLE}
|
${TEXT_INPUT_STYLE}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -60,7 +60,7 @@ export const TextInput = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInput
|
<StyledTextInput
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { StyledInput } from '@/ui/field/input/components/TextInput';
|
import { StyledTextInput as UIStyledTextInput } from '@/ui/field/input/components/TextInput';
|
||||||
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
|
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ const StyledDoubleTextContainer = styled.div`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTextInput = styled(StyledInput)`
|
const StyledTextInput = styled(UIStyledTextInput)`
|
||||||
margin: 0 ${({ theme }) => theme.spacing(0.5)};
|
margin: 0 ${({ theme }) => theme.spacing(0.5)};
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: ${({ width }) => (width ? `${width}px` : 'auto')};
|
width: ${({ width }) => (width ? `${width}px` : 'auto')};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
|||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
|
|
||||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
box-sizing: border-box;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
color: ${({ disabled, theme }) =>
|
color: ${({ disabled, theme }) =>
|
||||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||||
@ -88,6 +90,8 @@ export const Select = <Value extends string | number | null>({
|
|||||||
value,
|
value,
|
||||||
withSearchInput,
|
withSearchInput,
|
||||||
}: SelectProps<Value>) => {
|
}: SelectProps<Value>) => {
|
||||||
|
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [searchInputValue, setSearchInputValue] = useState('');
|
const [searchInputValue, setSearchInputValue] = useState('');
|
||||||
|
|
||||||
@ -109,6 +113,15 @@ export const Select = <Value extends string | number | null>({
|
|||||||
|
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
const { useListenClickOutside } = useClickOutsideListener(dropdownId);
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [selectContainerRef],
|
||||||
|
callback: () => {
|
||||||
|
closeDropdown();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const selectControl = (
|
const selectControl = (
|
||||||
<StyledControlContainer disabled={isDisabled}>
|
<StyledControlContainer disabled={isDisabled}>
|
||||||
<StyledControlLabel>
|
<StyledControlLabel>
|
||||||
@ -133,6 +146,7 @@ export const Select = <Value extends string | number | null>({
|
|||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
ref={selectContainerRef}
|
||||||
>
|
>
|
||||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
|
|||||||
@ -20,20 +20,6 @@ import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
|||||||
|
|
||||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
export type TextInputComponentProps = Omit<
|
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
|
||||||
'onChange' | 'onKeyDown'
|
|
||||||
> & {
|
|
||||||
className?: string;
|
|
||||||
label?: string;
|
|
||||||
onChange?: (text: string) => void;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
disableHotkeys?: boolean;
|
|
||||||
error?: string;
|
|
||||||
RightIcon?: IconComponent;
|
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -110,6 +96,21 @@ const StyledTrailingIcon = styled.div`
|
|||||||
|
|
||||||
const INPUT_TYPE_PASSWORD = 'password';
|
const INPUT_TYPE_PASSWORD = 'password';
|
||||||
|
|
||||||
|
export type TextInputComponentProps = Omit<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
'onChange' | 'onKeyDown'
|
||||||
|
> & {
|
||||||
|
className?: string;
|
||||||
|
label?: string;
|
||||||
|
onChange?: (text: string) => void;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
disableHotkeys?: boolean;
|
||||||
|
error?: string;
|
||||||
|
RightIcon?: IconComponent;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
const TextInputComponent = (
|
const TextInputComponent = (
|
||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
@ -163,6 +164,7 @@ const TextInputComponent = (
|
|||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
},
|
},
|
||||||
InputHotkeyScope.TextInput,
|
InputHotkeyScope.TextInput,
|
||||||
|
{ enabled: !disableHotkeys },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { IconComponentProps } from '@/ui/display/icon/types/IconComponent';
|
||||||
|
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
|
||||||
|
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
|
||||||
|
export const CountrySelect = ({
|
||||||
|
selectedCountryName,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
selectedCountryName: string;
|
||||||
|
onChange: (countryCode: string) => void;
|
||||||
|
}) => {
|
||||||
|
const countries = useCountries();
|
||||||
|
|
||||||
|
const options: SelectOption<string>[] = useMemo(() => {
|
||||||
|
return countries.map<SelectOption<string>>(({ countryName, Flag }) => ({
|
||||||
|
label: countryName,
|
||||||
|
value: countryName,
|
||||||
|
Icon: (props: IconComponentProps) =>
|
||||||
|
Flag({ width: props.size, height: props.size }), // TODO : improve this ?
|
||||||
|
}));
|
||||||
|
}, [countries]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
fullWidth
|
||||||
|
dropdownId={SELECT_COUNTRY_DROPDOWN_ID}
|
||||||
|
options={options}
|
||||||
|
label="COUNTRY"
|
||||||
|
withSearchInput
|
||||||
|
onChange={onChange}
|
||||||
|
value={selectedCountryName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const SELECT_COUNTRY_DROPDOWN_ID = 'select-country-picker';
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { hasFlag } from 'country-flag-icons';
|
||||||
|
import * as Flags from 'country-flag-icons/react/3x2';
|
||||||
|
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||||
|
|
||||||
|
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||||
|
|
||||||
|
export const useCountries = () => {
|
||||||
|
return useMemo<Country[]>(() => {
|
||||||
|
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
||||||
|
type: 'region',
|
||||||
|
});
|
||||||
|
|
||||||
|
const countryCodes = getCountries();
|
||||||
|
|
||||||
|
return countryCodes.reduce<Country[]>((result, countryCode) => {
|
||||||
|
const countryName = regionNamesInEnglish.of(countryCode);
|
||||||
|
|
||||||
|
if (!countryName) return result;
|
||||||
|
|
||||||
|
if (!hasFlag(countryCode)) return result;
|
||||||
|
|
||||||
|
const Flag = Flags[countryCode];
|
||||||
|
|
||||||
|
const callingCode = getCountryCallingCode(countryCode);
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
countryCode,
|
||||||
|
countryName,
|
||||||
|
callingCode,
|
||||||
|
Flag,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getCountries, getCountryCallingCode } from 'react-phone-number-input';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { hasFlag } from 'country-flag-icons';
|
|
||||||
import * as Flags from 'country-flag-icons/react/3x2';
|
|
||||||
import { CountryCallingCode } from 'libphonenumber-js';
|
|
||||||
|
|
||||||
import { IconChevronDown, IconWorld } from '@/ui/display/icon';
|
import { IconChevronDown, IconWorld } from '@/ui/display/icon';
|
||||||
|
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||||
|
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
||||||
|
|
||||||
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSelect';
|
||||||
|
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
@ -57,14 +55,7 @@ const StyledIconContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type Country = {
|
export const PhoneCountryPickerDropdownButton = ({
|
||||||
countryCode: string;
|
|
||||||
countryName: string;
|
|
||||||
callingCode: CountryCallingCode;
|
|
||||||
Flag: Flags.FlagComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CountryPickerDropdownButton = ({
|
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
@ -82,34 +73,7 @@ export const CountryPickerDropdownButton = ({
|
|||||||
closeDropdown();
|
closeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const countries = useMemo<Country[]>(() => {
|
const countries = useCountries();
|
||||||
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
|
||||||
type: 'region',
|
|
||||||
});
|
|
||||||
|
|
||||||
const countryCodes = getCountries();
|
|
||||||
|
|
||||||
return countryCodes.reduce<Country[]>((result, countryCode) => {
|
|
||||||
const countryName = regionNamesInEnglish.of(countryCode);
|
|
||||||
|
|
||||||
if (!countryName) return result;
|
|
||||||
|
|
||||||
if (!hasFlag(countryCode)) return result;
|
|
||||||
|
|
||||||
const Flag = Flags[countryCode];
|
|
||||||
|
|
||||||
const callingCode = getCountryCallingCode(countryCode);
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
countryCode,
|
|
||||||
countryName,
|
|
||||||
callingCode,
|
|
||||||
Flag,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const country = countries.find(({ countryCode }) => countryCode === value);
|
const country = countries.find(({ countryCode }) => countryCode === value);
|
||||||
@ -132,7 +96,7 @@ export const CountryPickerDropdownButton = ({
|
|||||||
</StyledDropdownButtonContainer>
|
</StyledDropdownButtonContainer>
|
||||||
}
|
}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<CountryPickerDropdownSelect
|
<PhoneCountryPickerDropdownSelect
|
||||||
countries={countries}
|
countries={countries}
|
||||||
selectedCountry={selectedCountry}
|
selectedCountry={selectedCountry}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
@ -8,8 +9,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
|||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
||||||
|
|
||||||
import { Country } from './CountryPickerDropdownButton';
|
|
||||||
|
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
const StyledIconContainer = styled.div`
|
||||||
@ -27,7 +26,7 @@ const StyledIconContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CountryPickerDropdownSelect = ({
|
export const PhoneCountryPickerDropdownSelect = ({
|
||||||
countries,
|
countries,
|
||||||
selectedCountry,
|
selectedCountry,
|
||||||
onChange,
|
onChange,
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import * as Flags from 'country-flag-icons/react/3x2';
|
||||||
|
import { CountryCallingCode } from 'libphonenumber-js';
|
||||||
|
|
||||||
|
export type Country = {
|
||||||
|
countryCode: string;
|
||||||
|
countryName: string;
|
||||||
|
callingCode: CountryCallingCode;
|
||||||
|
Flag: Flags.FlagComponent;
|
||||||
|
};
|
||||||
@ -29,7 +29,6 @@ export const seedCompanies = async (
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'domainName',
|
'domainName',
|
||||||
'address',
|
|
||||||
'position',
|
'position',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
@ -38,91 +37,78 @@ export const seedCompanies = async (
|
|||||||
id: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
id: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
||||||
name: 'Linkedin',
|
name: 'Linkedin',
|
||||||
domainName: 'linkedin.com',
|
domainName: 'linkedin.com',
|
||||||
address: '',
|
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
id: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
||||||
name: 'Facebook',
|
name: 'Facebook',
|
||||||
domainName: 'facebook.com',
|
domainName: 'facebook.com',
|
||||||
address: '',
|
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.QONTO,
|
id: DEV_SEED_COMPANY_IDS.QONTO,
|
||||||
name: 'Qonto',
|
name: 'Qonto',
|
||||||
domainName: 'qonto.com',
|
domainName: 'qonto.com',
|
||||||
address: '',
|
|
||||||
position: 3,
|
position: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
id: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
name: 'Microsoft',
|
name: 'Microsoft',
|
||||||
domainName: 'microsoft.com',
|
domainName: 'microsoft.com',
|
||||||
address: '',
|
|
||||||
position: 4,
|
position: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.AIRBNB,
|
id: DEV_SEED_COMPANY_IDS.AIRBNB,
|
||||||
name: 'Airbnb',
|
name: 'Airbnb',
|
||||||
domainName: 'airbnb.com',
|
domainName: 'airbnb.com',
|
||||||
address: '',
|
|
||||||
position: 5,
|
position: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.GOOGLE,
|
id: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
name: 'Google',
|
name: 'Google',
|
||||||
domainName: 'google.com',
|
domainName: 'google.com',
|
||||||
address: '',
|
|
||||||
position: 6,
|
position: 6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.NETFLIX,
|
id: DEV_SEED_COMPANY_IDS.NETFLIX,
|
||||||
name: 'Netflix',
|
name: 'Netflix',
|
||||||
domainName: 'netflix.com',
|
domainName: 'netflix.com',
|
||||||
address: '',
|
|
||||||
position: 7,
|
position: 7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.LIBEO,
|
id: DEV_SEED_COMPANY_IDS.LIBEO,
|
||||||
name: 'Libeo',
|
name: 'Libeo',
|
||||||
domainName: 'libeo.io',
|
domainName: 'libeo.io',
|
||||||
address: '',
|
|
||||||
position: 8,
|
position: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.CLAAP,
|
id: DEV_SEED_COMPANY_IDS.CLAAP,
|
||||||
name: 'Claap',
|
name: 'Claap',
|
||||||
domainName: 'claap.io',
|
domainName: 'claap.io',
|
||||||
address: '',
|
|
||||||
position: 9,
|
position: 9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.HASURA,
|
id: DEV_SEED_COMPANY_IDS.HASURA,
|
||||||
name: 'Hasura',
|
name: 'Hasura',
|
||||||
domainName: 'hasura.io',
|
domainName: 'hasura.io',
|
||||||
address: '',
|
|
||||||
position: 10,
|
position: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.WEWORK,
|
id: DEV_SEED_COMPANY_IDS.WEWORK,
|
||||||
name: 'Wework',
|
name: 'Wework',
|
||||||
domainName: 'wework.com',
|
domainName: 'wework.com',
|
||||||
address: '',
|
|
||||||
position: 11,
|
position: 11,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.SAMSUNG,
|
id: DEV_SEED_COMPANY_IDS.SAMSUNG,
|
||||||
name: 'Samsung',
|
name: 'Samsung',
|
||||||
domainName: 'samsung.com',
|
domainName: 'samsung.com',
|
||||||
address: '',
|
|
||||||
position: 12,
|
position: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.ALGOLIA,
|
id: DEV_SEED_COMPANY_IDS.ALGOLIA,
|
||||||
name: 'Algolia',
|
name: 'Algolia',
|
||||||
domainName: 'algolia.com',
|
domainName: 'algolia.com',
|
||||||
address: '',
|
|
||||||
position: 13,
|
position: 13,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { fullNameObjectDefinition } from 'src/engine/metadata-modules/field-meta
|
|||||||
import { currencyObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
import { currencyObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||||
import { linkObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
import { linkObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||||
|
import { addressObjectDefinition } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||||
|
|
||||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||||
import {
|
import {
|
||||||
@ -68,7 +69,8 @@ export class TypeDefinitionsGenerator {
|
|||||||
currencyObjectDefinition,
|
currencyObjectDefinition,
|
||||||
linkObjectDefinition,
|
linkObjectDefinition,
|
||||||
fullNameObjectDefinition,
|
fullNameObjectDefinition,
|
||||||
];
|
addressObjectDefinition,
|
||||||
|
] satisfies ObjectMetadataInterface[];
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Generating staticObjects: [${staticObjectMetadataCollection
|
`Generating staticObjects: [${staticObjectMetadataCollection
|
||||||
|
|||||||
@ -104,5 +104,19 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
} else if (fieldType === FieldMetadataType.ADDRESS) {
|
||||||
|
return `
|
||||||
|
${field.name}
|
||||||
|
{
|
||||||
|
addressStreet1
|
||||||
|
addressStreet2
|
||||||
|
addressCity
|
||||||
|
addressPostcode
|
||||||
|
addressState
|
||||||
|
addressCountry
|
||||||
|
addressLat
|
||||||
|
addressLng
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const getSchemaComponentsProperties = (
|
|||||||
case FieldMetadataType.LINK:
|
case FieldMetadataType.LINK:
|
||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
case FieldMetadataType.FULL_NAME:
|
case FieldMetadataType.FULL_NAME:
|
||||||
|
case FieldMetadataType.ADDRESS:
|
||||||
itemProperty = {
|
itemProperty = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: Object.keys(field.targetColumnMap).reduce(
|
properties: Object.keys(field.targetColumnMap).reduce(
|
||||||
|
|||||||
@ -0,0 +1,195 @@
|
|||||||
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util';
|
||||||
|
|
||||||
|
export const addressFields = (
|
||||||
|
fieldMetadata?: FieldMetadataInterface,
|
||||||
|
): FieldMetadataInterface[] => {
|
||||||
|
const inferredFieldMetadata = fieldMetadata as
|
||||||
|
| FieldMetadataInterface<FieldMetadataType.ADDRESS>
|
||||||
|
| undefined;
|
||||||
|
const targetColumnMap = inferredFieldMetadata
|
||||||
|
? generateTargetColumnMap(
|
||||||
|
inferredFieldMetadata.type,
|
||||||
|
inferredFieldMetadata.isCustom ?? false,
|
||||||
|
inferredFieldMetadata.name,
|
||||||
|
)
|
||||||
|
: {
|
||||||
|
addressStreet1: 'addressStreet1',
|
||||||
|
addressStreet2: 'addressStreet2',
|
||||||
|
addressCity: 'addressCity',
|
||||||
|
addressPostcode: 'addressPostcode',
|
||||||
|
addressState: 'addressState',
|
||||||
|
addressCountry: 'addressCountry',
|
||||||
|
addressLat: 'addressLat',
|
||||||
|
addressLng: 'addressLng',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'addressStreet1',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressStreet1',
|
||||||
|
label: 'Address',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressStreet1,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressStreet1 ?? undefined,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressStreet2',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressStreet2',
|
||||||
|
label: 'Address 2',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressStreet2,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressStreet2 ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressCity',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressCity',
|
||||||
|
label: 'City',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressCity,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressCity ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressPostcode',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressPostcode',
|
||||||
|
label: 'Postcode',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressPostcode,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressPostcode ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressState',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressState',
|
||||||
|
label: 'State',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressState,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressState ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressCountry',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressCountry',
|
||||||
|
label: 'Country',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressCountry,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressCountry ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.TEXT>,
|
||||||
|
{
|
||||||
|
id: 'addressLat',
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressLat',
|
||||||
|
label: 'Latitude',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressLat,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressLat ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
|
||||||
|
{
|
||||||
|
id: 'addressLng',
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
objectMetadataId: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
name: 'addressLng',
|
||||||
|
label: 'Longitude',
|
||||||
|
targetColumnMap: {
|
||||||
|
value: targetColumnMap.addressLng,
|
||||||
|
},
|
||||||
|
isNullable: true,
|
||||||
|
...(inferredFieldMetadata
|
||||||
|
? {
|
||||||
|
defaultValue:
|
||||||
|
inferredFieldMetadata.defaultValue?.addressLng ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
} satisfies FieldMetadataInterface<FieldMetadataType.NUMBER>,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addressObjectDefinition = {
|
||||||
|
id: FieldMetadataType.ADDRESS.toString(),
|
||||||
|
nameSingular: 'address',
|
||||||
|
namePlural: 'address',
|
||||||
|
labelSingular: 'Address',
|
||||||
|
labelPlural: 'Addresses',
|
||||||
|
targetTableName: '',
|
||||||
|
fields: addressFields(),
|
||||||
|
fromRelations: [],
|
||||||
|
toRelations: [],
|
||||||
|
isActive: true,
|
||||||
|
isSystem: true,
|
||||||
|
isCustom: false,
|
||||||
|
isRemote: false,
|
||||||
|
} satisfies ObjectMetadataInterface;
|
||||||
|
|
||||||
|
export type AddressMetadata = {
|
||||||
|
addressStreet1: string;
|
||||||
|
addressStreet2: string;
|
||||||
|
addressCity: string;
|
||||||
|
addressState: string;
|
||||||
|
addressZipCode: string;
|
||||||
|
addressCountry: string;
|
||||||
|
addressLat: number;
|
||||||
|
addressLng: number;
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import { currencyFields } from 'src/engine/metadata-modules/field-metadata/compo
|
|||||||
import { fullNameFields } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
import { fullNameFields } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||||
import { linkFields } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
import { linkFields } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { addressFields } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||||
|
|
||||||
export type CompositeFieldsDefinitionFunction = (
|
export type CompositeFieldsDefinitionFunction = (
|
||||||
fieldMetadata?: FieldMetadataInterface,
|
fieldMetadata?: FieldMetadataInterface,
|
||||||
@ -16,4 +17,5 @@ export const compositeDefinitions = new Map<
|
|||||||
[FieldMetadataType.LINK, linkFields],
|
[FieldMetadataType.LINK, linkFields],
|
||||||
[FieldMetadataType.CURRENCY, currencyFields],
|
[FieldMetadataType.CURRENCY, currencyFields],
|
||||||
[FieldMetadataType.FULL_NAME, fullNameFields],
|
[FieldMetadataType.FULL_NAME, fullNameFields],
|
||||||
|
[FieldMetadataType.ADDRESS, addressFields],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsNumberString,
|
IsNumberString,
|
||||||
|
IsString,
|
||||||
Matches,
|
Matches,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -98,3 +99,36 @@ export class FieldMetadataDefaultValueNowFunction {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
value: typeof fieldMetadataDefaultValueFunctionName.NOW;
|
value: typeof fieldMetadataDefaultValueFunctionName.NOW;
|
||||||
}
|
}
|
||||||
|
export class FieldMetadataDefaultValueAddress {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressStreet1: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressStreet2: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressCity: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressPostcode: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressState: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
addressCountry: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsNumber()
|
||||||
|
addressLat: number | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsNumber()
|
||||||
|
addressLng: number | null;
|
||||||
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export enum FieldMetadataType {
|
|||||||
MULTI_SELECT = 'MULTI_SELECT',
|
MULTI_SELECT = 'MULTI_SELECT',
|
||||||
RELATION = 'RELATION',
|
RELATION = 'RELATION',
|
||||||
POSITION = 'POSITION',
|
POSITION = 'POSITION',
|
||||||
|
ADDRESS = 'ADDRESS',
|
||||||
|
JSON = 'JSON',
|
||||||
RAW_JSON = 'RAW_JSON',
|
RAW_JSON = 'RAW_JSON',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
FieldMetadataDefaultValueAddress,
|
||||||
FieldMetadataDefaultValueBoolean,
|
FieldMetadataDefaultValueBoolean,
|
||||||
FieldMetadataDefaultValueCurrency,
|
FieldMetadataDefaultValueCurrency,
|
||||||
FieldMetadataDefaultValueDateTime,
|
FieldMetadataDefaultValueDateTime,
|
||||||
@ -35,6 +36,7 @@ type FieldMetadataDefaultValueMapping = {
|
|||||||
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
|
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
|
||||||
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
||||||
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
||||||
|
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;
|
||||||
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
|
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
|
||||||
|
|||||||
@ -19,6 +19,17 @@ export interface FieldMetadataTargetColumnMapFullName {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FieldMetadataTargetColumnMapAddress = {
|
||||||
|
addressStreet1: string;
|
||||||
|
addressStreet2: string;
|
||||||
|
addressCity: string;
|
||||||
|
addressState: string;
|
||||||
|
addressZipCode: string;
|
||||||
|
addressCountry: string;
|
||||||
|
addressLat: number;
|
||||||
|
addressLng: number;
|
||||||
|
};
|
||||||
|
|
||||||
type AllFieldMetadataTypes = {
|
type AllFieldMetadataTypes = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
@ -27,6 +38,7 @@ type FieldMetadataTypeMapping = {
|
|||||||
[FieldMetadataType.LINK]: FieldMetadataTargetColumnMapLink;
|
[FieldMetadataType.LINK]: FieldMetadataTargetColumnMapLink;
|
||||||
[FieldMetadataType.CURRENCY]: FieldMetadataTargetColumnMapCurrency;
|
[FieldMetadataType.CURRENCY]: FieldMetadataTargetColumnMapCurrency;
|
||||||
[FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName;
|
[FieldMetadataType.FULL_NAME]: FieldMetadataTargetColumnMapFullName;
|
||||||
|
[FieldMetadataType.ADDRESS]: FieldMetadataTargetColumnMapAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
|
type TypeByFieldMetadata<T extends FieldMetadataType | 'default'> = [
|
||||||
|
|||||||
@ -15,6 +15,17 @@ export function generateDefaultValue(
|
|||||||
firstName: "''",
|
firstName: "''",
|
||||||
lastName: "''",
|
lastName: "''",
|
||||||
};
|
};
|
||||||
|
case FieldMetadataType.ADDRESS:
|
||||||
|
return {
|
||||||
|
addressStreet1: "''",
|
||||||
|
addressStreet2: "''",
|
||||||
|
addressCity: "''",
|
||||||
|
addressState: "''",
|
||||||
|
addressCountry: "''",
|
||||||
|
addressPostcode: "''",
|
||||||
|
addressLat: null,
|
||||||
|
addressLng: null,
|
||||||
|
};
|
||||||
case FieldMetadataType.LINK:
|
case FieldMetadataType.LINK:
|
||||||
return {
|
return {
|
||||||
url: "''",
|
url: "''",
|
||||||
|
|||||||
@ -54,6 +54,17 @@ export function generateTargetColumnMap(
|
|||||||
firstName: `${columnName}FirstName`,
|
firstName: `${columnName}FirstName`,
|
||||||
lastName: `${columnName}LastName`,
|
lastName: `${columnName}LastName`,
|
||||||
};
|
};
|
||||||
|
case FieldMetadataType.ADDRESS:
|
||||||
|
return {
|
||||||
|
addressStreet1: `${columnName}AddressStreet1`,
|
||||||
|
addressStreet2: `${columnName}AddressStreet2`,
|
||||||
|
addressCity: `${columnName}AddressCity`,
|
||||||
|
addressPostcode: `${columnName}AddressPostcode`,
|
||||||
|
addressState: `${columnName}AddressState`,
|
||||||
|
addressCountry: `${columnName}AddressCountry`,
|
||||||
|
addressLat: `${columnName}AddressLat`,
|
||||||
|
addressLng: `${columnName}AddressLng`,
|
||||||
|
};
|
||||||
case FieldMetadataType.RELATION:
|
case FieldMetadataType.RELATION:
|
||||||
return {};
|
return {};
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export const isCompositeFieldMetadataType = (
|
|||||||
return (
|
return (
|
||||||
type === FieldMetadataType.LINK ||
|
type === FieldMetadataType.LINK ||
|
||||||
type === FieldMetadataType.CURRENCY ||
|
type === FieldMetadataType.CURRENCY ||
|
||||||
type === FieldMetadataType.FULL_NAME
|
type === FieldMetadataType.FULL_NAME ||
|
||||||
|
type === FieldMetadataType.ADDRESS
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import {
|
import {
|
||||||
|
FieldMetadataDefaultValueAddress,
|
||||||
FieldMetadataDefaultValueBoolean,
|
FieldMetadataDefaultValueBoolean,
|
||||||
FieldMetadataDefaultValueCurrency,
|
FieldMetadataDefaultValueCurrency,
|
||||||
FieldMetadataDefaultValueDateTime,
|
FieldMetadataDefaultValueDateTime,
|
||||||
@ -44,6 +45,7 @@ export const defaultValueValidatorsMap = {
|
|||||||
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
|
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
|
||||||
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
|
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
|
||||||
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
||||||
|
[FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress],
|
||||||
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@ export const companyPrefillDemoData = async (
|
|||||||
.into(`${schemaName}.company`, [
|
.into(`${schemaName}.company`, [
|
||||||
'name',
|
'name',
|
||||||
'domainName',
|
'domainName',
|
||||||
'address',
|
'addressAddressCity',
|
||||||
'employees',
|
'employees',
|
||||||
'linkedinLinkUrl',
|
'linkedinLinkUrl',
|
||||||
'position',
|
'position',
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export class CompanyObjectMetadata extends BaseObjectMetadata {
|
|||||||
|
|
||||||
@FieldMetadata({
|
@FieldMetadata({
|
||||||
standardId: companyStandardFieldIds.address,
|
standardId: companyStandardFieldIds.address,
|
||||||
type: FieldMetadataType.TEXT,
|
type: FieldMetadataType.ADDRESS,
|
||||||
label: 'Address',
|
label: 'Address',
|
||||||
description: 'The company address',
|
description: 'The company address',
|
||||||
icon: 'IconMap',
|
icon: 'IconMap',
|
||||||
|
|||||||
@ -89,6 +89,57 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
|
|||||||
};
|
};
|
||||||
return [amountMicros, currencyCode];
|
return [amountMicros, currencyCode];
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.ADDRESS: {
|
||||||
|
const address1: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressStreet1',
|
||||||
|
label: 'Address',
|
||||||
|
description: 'Address',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
const address2: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressStreet2',
|
||||||
|
label: 'Address 2',
|
||||||
|
description: 'Address 2',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
const city: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressCity',
|
||||||
|
label: 'City',
|
||||||
|
description: 'City',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
const state: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressState',
|
||||||
|
label: 'State',
|
||||||
|
description: 'State',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
const postalCode: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressPostalCode',
|
||||||
|
label: 'Postal Code',
|
||||||
|
description: 'Postal Code',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
const country: NodeField = {
|
||||||
|
type: 'TEXT',
|
||||||
|
name: 'addressCountry',
|
||||||
|
label: 'Country',
|
||||||
|
description: 'Country',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
return [address1, address2, city, state, postalCode, country];
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown nodeField type: ${nodeField.type}`);
|
throw new Error(`Unknown nodeField type: ${nodeField.type}`);
|
||||||
}
|
}
|
||||||
@ -109,6 +160,7 @@ export const computeInputFields = (
|
|||||||
case FieldMetadataType.FULL_NAME:
|
case FieldMetadataType.FULL_NAME:
|
||||||
case FieldMetadataType.LINK:
|
case FieldMetadataType.LINK:
|
||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
|
case FieldMetadataType.ADDRESS:
|
||||||
for (const subNodeField of get_subfieldsFromField(nodeField)) {
|
for (const subNodeField of get_subfieldsFromField(nodeField)) {
|
||||||
const field = {
|
const field = {
|
||||||
key: `${nodeField.name}__${subNodeField.name}`,
|
key: `${nodeField.name}__${subNodeField.name}`,
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export enum FieldMetadataType {
|
|||||||
SELECT = 'SELECT',
|
SELECT = 'SELECT',
|
||||||
MULTI_SELECT = 'MULTI_SELECT',
|
MULTI_SELECT = 'MULTI_SELECT',
|
||||||
RELATION = 'RELATION',
|
RELATION = 'RELATION',
|
||||||
|
ADDRESS = 'ADDRESS',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Schema = {
|
export type Schema = {
|
||||||
|
|||||||
Reference in New Issue
Block a user