Skip to content

Commit

Permalink
feat: add Domain field type
Browse files Browse the repository at this point in the history
Closes #5113
  • Loading branch information
thaisguigon committed Apr 26, 2024
1 parent 5e143f1 commit c9a222d
Show file tree
Hide file tree
Showing 34 changed files with 332 additions and 14 deletions.
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export enum FieldMetadataType {
Currency = 'CURRENCY',
Date = 'DATE',
DateTime = 'DATE_TIME',
Domain = 'DOMAIN',
Email = 'EMAIL',
FullName = 'FULL_NAME',
Link = 'LINK',
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export enum FieldMetadataType {
Currency = 'CURRENCY',
Date = 'DATE',
DateTime = 'DATE_TIME',
Domain = 'DOMAIN',
Email = 'EMAIL',
FullName = 'FULL_NAME',
Link = 'LINK',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useContext } from 'react';

import { DomainFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DomainFieldDisplay.tsx';
import { isFieldDomain } from '@/object-record/record-field/types/guards/isFieldDomain.ts';

import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
Expand Down Expand Up @@ -74,5 +77,7 @@ export const FieldDisplay = () => {
<AddressFieldDisplay />
) : isFieldRawJson(fieldDefinition) ? (
<JsonFieldDisplay />
) : isFieldDomain(fieldDefinition) ? (
<DomainFieldDisplay />
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { useContext } from 'react';

import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput';
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
import { DomainFieldInput } from '@/object-record/record-field/meta-types/input/components/DomainFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDomain } from '@/object-record/record-field/types/guards/isFieldDomain';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
Expand Down Expand Up @@ -163,6 +165,14 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDomain(fieldDefinition) ? (
<DomainFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : (
<></>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFieldDateValue';
import { isFieldDomain } from '@/object-record/record-field/types/guards/isFieldDomain';
import { isFieldDomainValue } from '@/object-record/record-field/types/guards/isFieldDomainValue';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
Expand Down Expand Up @@ -105,6 +107,9 @@ export const usePersistField = () => {
isFieldRawJson(fieldDefinition) &&
isFieldRawJsonValue(valueToPersist);

const fieldIsDomain =
isFieldDomain(fieldDefinition) && isFieldDomainValue(valueToPersist);

const isValuePersistable =
fieldIsRelation ||
fieldIsText ||
Expand All @@ -121,9 +126,10 @@ export const usePersistField = () => {
fieldIsSelect ||
fieldIsMultiSelect ||
fieldIsAddress ||
fieldIsRawJson;
fieldIsRawJson ||
fieldIsDomain;

if (isValuePersistable === true) {
if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;
set(
recordStoreFamilySelector({ recordId: entityId, fieldName }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useDomainField } from '@/object-record/record-field/meta-types/hooks/useDomainField';
import { DomainDisplay } from '@/ui/field/display/components/DomainDisplay';

export const DomainFieldDisplay = () => {
const { fieldValue } = useDomainField();

return <DomainDisplay value={fieldValue} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useContext } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';

import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
import { FieldDomainValue } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldDomain } from '@/object-record/record-field/types/guards/isFieldDomain';
import { isFieldDomainValue } from '@/object-record/record-field/types/guards/isFieldDomainValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';

import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';

export const useDomainField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);

assertFieldMetadata(FieldMetadataType.Domain, isFieldDomain, fieldDefinition);

const fieldName = fieldDefinition.metadata.fieldName;

const [fieldValue, setFieldValue] = useRecoilState<FieldDomainValue>(
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldName,
}),
);

const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldDomainValue>(`${entityId}-${fieldName}`);

const draftValue = useRecoilValue(getDraftValueSelector());

const persistField = usePersistField();

const persistDomainField = (newValue: FieldDomainValue) => {
if (!isFieldDomainValue(newValue)) return;

persistField(newValue);
};

return {
fieldDefinition,
fieldValue,
draftValue,
setDraftValue,
setFieldValue,
hotkeyScope,
persistDomainField,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useDomainField } from '@/object-record/record-field/meta-types/hooks/useDomainField';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { TextInput } from '@/ui/field/input/components/TextInput';

import { FieldInputEvent } from './DateTimeFieldInput';

export type DomainFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};

export const DomainFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: DomainFieldInputProps) => {
const { draftValue, setDraftValue, hotkeyScope, persistDomainField } =
useDomainField();

const handleEnter = (domain: string) => {
onEnter?.(() =>
persistDomainField({
primaryLink: domain,
}),
);
};

const handleEscape = (domain: string) => {
onEscape?.(() =>
persistDomainField({
primaryLink: domain,
}),
);
};

const handleClickOutside = (
event: MouseEvent | TouchEvent,
domain: string,
) => {
onClickOutside?.(() =>
persistDomainField({
primaryLink: domain,
}),
);
};

const handleTab = (domain: string) => {
onTab?.(() =>
persistDomainField({
primaryLink: domain,
}),
);
};

const handleShiftTab = (domain: string) => {
onShiftTab?.(() =>
persistDomainField({
primaryLink: domain,
}),
);
};

const handleChange = (domain: string) => {
setDraftValue({
primaryLink: domain,
});
};

return (
<FieldInputOverlay>
<TextInput
value={draftValue?.primaryLink ?? ''}
autoFocus
placeholder="Domain"
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
onChange={handleChange}
/>
</FieldInputOverlay>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FieldBooleanValue,
FieldCurrencyValue,
FieldDateTimeValue,
FieldDomainValue,
FieldEmailValue,
FieldFullNameValue,
FieldLinkValue,
Expand All @@ -26,6 +27,10 @@ export type FieldSelectDraftValue = string;
export type FieldMultiSelectDraftValue = string[];
export type FieldRelationDraftValue = string;
export type FieldLinkDraftValue = { url: string; label: string };
export type FieldDomainDraftValue = {
primaryLink: string;
secondaryLinks?: string[] | null;
};
export type FieldCurrencyDraftValue = {
currencyCode: CurrencyCode;
amount: string;
Expand Down Expand Up @@ -72,4 +77,6 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
? FieldRelationDraftValue
: FieldValue extends FieldAddressValue
? FieldAddressDraftValue
: never;
: FieldValue extends FieldDomainValue
? FieldDomainDraftValue
: never;
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export type FieldRawJsonMetadata = {
placeHolder: string;
};

export type FieldDomainMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
};

export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
Expand Down Expand Up @@ -143,6 +148,10 @@ export type FieldBooleanValue = boolean;
export type FieldPhoneValue = string;
export type FieldEmailValue = string;
export type FieldLinkValue = { url: string; label: string };
export type FieldDomainValue = {
primaryLink: string;
secondaryLinks?: string[] | null;
};
export type FieldCurrencyValue = {
currencyCode: CurrencyCode;
amountMicros: number | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FieldCurrencyMetadata,
FieldDateMetadata,
FieldDateTimeMetadata,
FieldDomainMetadata,
FieldEmailMetadata,
FieldFullNameMetadata,
FieldLinkMetadata,
Expand Down Expand Up @@ -60,7 +61,9 @@ type AssertFieldMetadataFunction = <
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never,
: E extends 'DOMAIN'
? FieldDomainMetadata
: never,
>(
fieldType: E,
fieldTypeGuard: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';

import { FieldDefinition } from '../FieldDefinition';
import { FieldDomainMetadata, FieldMetadata } from '../FieldMetadata';

export const isFieldDomain = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldDomainMetadata> =>
field.type === FieldMetadataType.Domain;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

import { FieldDomainValue } from '../FieldMetadata';

const domainSchema = z.object({
primaryLink: z.string(),
secondaryLinks: z.array(z.string()).optional().nullable(),
}) satisfies z.ZodType<FieldDomainValue>;

export const isFieldDomainValue = (
fieldValue: unknown,
): fieldValue is FieldDomainValue => domainSchema.safeParse(fieldValue).success;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFie
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
import { isFieldDomain } from '@/object-record/record-field/types/guards/isFieldDomain';
import { isFieldDomainValue } from '@/object-record/record-field/types/guards/isFieldDomainValue';
import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
Expand Down Expand Up @@ -95,6 +97,12 @@ export const isFieldValueEmpty = ({
);
}

if (isFieldDomain(fieldDefinition)) {
return (
!isFieldDomainValue(fieldValue) || isValueEmpty(fieldValue.primaryLink)
);
}

throw new Error(
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export const generateEmptyFieldValue = (
case FieldMetadataType.RawJson: {
return null;
}
case FieldMetadataType.Domain: {
return { primaryLink: '' };
}
default: {
throw new Error('Unhandled FieldMetadataType');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
IconTextSize,
IconTwentyStar,
IconUser,
IconWorldWww,
} from 'twenty-ui';

import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
Expand Down Expand Up @@ -130,4 +131,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
Icon: IconJson,
defaultValue: `{ "key": "value" }`,
},
[FieldMetadataType.Domain]: {
label: 'Domain',
Icon: IconWorldWww,
defaultValue: { primaryLink: 'twenty.com' },
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const previewableTypes = [
FieldMetadataType.Text,
FieldMetadataType.Address,
FieldMetadataType.RawJson,
FieldMetadataType.Domain,
];

export const SettingsDataModelFieldSettingsFormCard = ({
Expand Down
Loading

0 comments on commit c9a222d

Please sign in to comment.