feat: add Links field type (#5176)

Closes #5113

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Thaïs 2024-05-01 11:56:14 +02:00 committed by GitHub
parent e0ece3c917
commit 8853226d17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 465 additions and 61 deletions

View File

@ -305,6 +305,7 @@ export enum FieldMetadataType {
Email = 'EMAIL',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
Numeric = 'NUMERIC',

View File

@ -212,6 +212,7 @@ export enum FieldMetadataType {
Email = 'EMAIL',
FullName = 'FULL_NAME',
Link = 'LINK',
Links = 'LINKS',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
Numeric = 'NUMERIC',

View File

@ -1,5 +1,8 @@
import { useContext } from 'react';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
@ -62,6 +65,8 @@ export const FieldDisplay = () => {
<NumberFieldDisplay />
) : isFieldLink(fieldDefinition) ? (
<LinkFieldDisplay />
) : isFieldLinks(fieldDefinition) ? (
<LinksFieldDisplay />
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldDisplay />
) : isFieldFullName(fieldDefinition) ? (

View File

@ -3,12 +3,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 { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
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 { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
@ -131,6 +133,14 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldLinks(fieldDefinition) ? (
<LinksFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldInput
onEnter={onEnter}

View File

@ -7,6 +7,8 @@ import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDa
import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFieldDateValue';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
@ -69,6 +71,9 @@ export const usePersistField = () => {
const fieldIsLink =
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
const fieldIsLinks =
isFieldLinks(fieldDefinition) && isFieldLinksValue(valueToPersist);
const fieldIsBoolean =
isFieldBoolean(fieldDefinition) &&
isFieldBooleanValue(valueToPersist);
@ -116,6 +121,7 @@ export const usePersistField = () => {
fieldIsDate ||
fieldIsPhone ||
fieldIsLink ||
fieldIsLinks ||
fieldIsCurrency ||
fieldIsFullName ||
fieldIsSelect ||
@ -123,7 +129,7 @@ export const usePersistField = () => {
fieldIsAddress ||
fieldIsRawJson;
if (isValuePersistable === true) {
if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;
set(
recordStoreFamilySelector({ recordId: entityId, fieldName }),

View File

@ -0,0 +1,8 @@
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { LinksDisplay } from '@/ui/field/display/components/LinksDisplay';
export const LinksFieldDisplay = () => {
const { fieldValue } = useLinksField();
return <LinksDisplay value={fieldValue} />;
};

View File

@ -0,0 +1,53 @@
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 { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { linksSchema } from '@/object-record/record-field/types/guards/isFieldLinksValue';
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 useLinksField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(FieldMetadataType.Links, isFieldLinks, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldLinksValue>(
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldName,
}),
);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldLinksValue>(`${entityId}-${fieldName}`);
const draftValue = useRecoilValue(getDraftValueSelector());
const persistField = usePersistField();
const persistLinksField = (nextValue: FieldLinksValue) => {
try {
persistField(linksSchema.parse(nextValue));
} catch {
return;
}
};
return {
fieldDefinition,
fieldValue,
draftValue,
setDraftValue,
setFieldValue,
hotkeyScope,
persistLinksField,
};
};

View File

@ -0,0 +1,93 @@
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { TextInput } from '@/ui/field/input/components/TextInput';
import { FieldInputEvent } from './DateTimeFieldInput';
export type LinksFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const LinksFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: LinksFieldInputProps) => {
const { draftValue, setDraftValue, hotkeyScope, persistLinksField } =
useLinksField();
const handleEnter = (url: string) => {
onEnter?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
}),
);
};
const handleEscape = (url: string) => {
onEscape?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
}),
);
};
const handleClickOutside = (event: MouseEvent | TouchEvent, url: string) => {
onClickOutside?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
}),
);
};
const handleTab = (url: string) => {
onTab?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
}),
);
};
const handleShiftTab = (url: string) => {
onShiftTab?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
}),
);
};
const handleChange = (url: string) => {
setDraftValue({
primaryLinkUrl: url,
primaryLinkLabel: '',
});
};
return (
<FieldInputOverlay>
<TextInput
value={draftValue?.primaryLinkUrl ?? ''}
autoFocus
placeholder="Links"
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
onChange={handleChange}
/>
</FieldInputOverlay>
);
};

View File

@ -6,6 +6,7 @@ import {
FieldDateTimeValue,
FieldEmailValue,
FieldFullNameValue,
FieldLinksValue,
FieldLinkValue,
FieldMultiSelectValue,
FieldNumberValue,
@ -26,6 +27,11 @@ export type FieldSelectDraftValue = string;
export type FieldMultiSelectDraftValue = string[];
export type FieldRelationDraftValue = string;
export type FieldLinkDraftValue = { url: string; label: string };
export type FieldLinksDraftValue = {
primaryLinkLabel: string;
primaryLinkUrl: string;
secondaryLinks?: string | null;
};
export type FieldCurrencyDraftValue = {
currencyCode: CurrencyCode;
amount: string;
@ -58,18 +64,20 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
? FieldEmailDraftValue
: FieldValue extends FieldLinkValue
? FieldLinkDraftValue
: FieldValue extends FieldCurrencyValue
? FieldCurrencyDraftValue
: FieldValue extends FieldFullNameValue
? FieldFullNameDraftValue
: FieldValue extends FieldRatingValue
? FieldRatingValue
: FieldValue extends FieldSelectValue
? FieldSelectDraftValue
: FieldValue extends FieldMultiSelectValue
? FieldMultiSelectDraftValue
: FieldValue extends FieldRelationValue
? FieldRelationDraftValue
: FieldValue extends FieldAddressValue
? FieldAddressDraftValue
: never;
: FieldValue extends FieldLinksValue
? FieldLinksDraftValue
: FieldValue extends FieldCurrencyValue
? FieldCurrencyDraftValue
: FieldValue extends FieldFullNameValue
? FieldFullNameDraftValue
: FieldValue extends FieldRatingValue
? FieldRatingValue
: FieldValue extends FieldSelectValue
? FieldSelectDraftValue
: FieldValue extends FieldMultiSelectValue
? FieldMultiSelectDraftValue
: FieldValue extends FieldRelationValue
? FieldRelationDraftValue
: FieldValue extends FieldAddressValue
? FieldAddressDraftValue
: never;

View File

@ -45,6 +45,11 @@ export type FieldLinkMetadata = {
fieldName: string;
};
export type FieldLinksMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
};
export type FieldCurrencyMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
@ -143,6 +148,11 @@ export type FieldBooleanValue = boolean;
export type FieldPhoneValue = string;
export type FieldEmailValue = string;
export type FieldLinkValue = { url: string; label: string };
export type FieldLinksValue = {
primaryLinkLabel: string;
primaryLinkUrl: string;
secondaryLinks?: string | null;
};
export type FieldCurrencyValue = {
currencyCode: CurrencyCode;
amountMicros: number | null;

View File

@ -10,6 +10,7 @@ import {
FieldEmailMetadata,
FieldFullNameMetadata,
FieldLinkMetadata,
FieldLinksMetadata,
FieldMetadata,
FieldMultiSelectMetadata,
FieldNumberMetadata,
@ -44,23 +45,25 @@ type AssertFieldMetadataFunction = <
? FieldRatingMetadata
: E extends 'LINK'
? FieldLinkMetadata
: E extends 'NUMBER'
? FieldNumberMetadata
: E extends 'PHONE'
? FieldPhoneMetadata
: E extends 'PROBABILITY'
? FieldRatingMetadata
: E extends 'RELATION'
? FieldRelationMetadata
: E extends 'TEXT'
? FieldTextMetadata
: E extends 'UUID'
? FieldUuidMetadata
: E extends 'ADDRESS'
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never,
: E extends 'LINKS'
? FieldLinksMetadata
: E extends 'NUMBER'
? FieldNumberMetadata
: E extends 'PHONE'
? FieldPhoneMetadata
: E extends 'PROBABILITY'
? FieldRatingMetadata
: E extends 'RELATION'
? FieldRelationMetadata
: E extends 'TEXT'
? FieldTextMetadata
: E extends 'UUID'
? FieldUuidMetadata
: E extends 'ADDRESS'
? FieldAddressMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never,
>(
fieldType: E,
fieldTypeGuard: (

View File

@ -7,7 +7,6 @@ const linkSchema = z.object({
label: z.string(),
});
// TODO: add zod
export const isFieldLinkValue = (
fieldValue: unknown,
): fieldValue is FieldLinkValue => linkSchema.safeParse(fieldValue).success;

View File

@ -0,0 +1,9 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldLinksMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldLinks = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldLinksMetadata> =>
field.type === FieldMetadataType.Links;

View File

@ -0,0 +1,15 @@
import { z } from 'zod';
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
import { FieldLinksValue } from '../FieldMetadata';
export const linksSchema = z.object({
primaryLinkLabel: z.string(),
primaryLinkUrl: absoluteUrlSchema,
secondaryLinks: z.string().optional().nullable(),
}) satisfies z.ZodType<FieldLinksValue>;
export const isFieldLinksValue = (
fieldValue: unknown,
): fieldValue is FieldLinksValue => linksSchema.safeParse(fieldValue).success;

View File

@ -11,6 +11,8 @@ import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldE
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
import { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
@ -95,6 +97,12 @@ export const isFieldValueEmpty = ({
);
}
if (isFieldLinks(fieldDefinition)) {
return (
!isFieldLinksValue(fieldValue) || isValueEmpty(fieldValue.primaryLinkUrl)
);
}
throw new Error(
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
);

View File

@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const generateEmptyFieldValue = (
fieldMetadataItem: FieldMetadataItem,
@ -18,6 +18,9 @@ export const generateEmptyFieldValue = (
url: '',
};
}
case FieldMetadataType.Links: {
return { primaryLinkUrl: '', primaryLinkLabel: '' };
}
case FieldMetadataType.FullName: {
return {
firstName: '',

View File

@ -1,13 +0,0 @@
import { z } from 'zod';
export const sanitizeLink = (url: string) =>
getUrlHostName(url) || getUrlHostName(`https://${url}`);
const getUrlHostName = (url: string) => {
const urlSchema = z.string().url();
const validation = urlSchema.safeParse(url);
return validation.success
? new URL(validation.data).hostname.replace(/^www\./i, '')
: '';
};

View File

@ -4,9 +4,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeLink } from '@/object-record/utils/sanitizeLinkRecordInput';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { getUrlHostName } from '~/utils/url/getUrlHostName';
export const sanitizeRecordInput = ({
objectMetadataItem,
@ -54,6 +54,6 @@ export const sanitizeRecordInput = ({
return {
...filteredResultRecord,
domainName: sanitizeLink(filteredResultRecord.domainName),
domainName: getUrlHostName(filteredResultRecord.domainName),
};
};

View File

@ -62,6 +62,11 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
Icon: IconLink,
defaultValue: { url: 'www.twenty.com', label: '' },
},
[FieldMetadataType.Links]: {
label: 'Links',
Icon: IconLink,
defaultValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' },
},
[FieldMetadataType.Boolean]: {
label: 'True/False',
Icon: IconCheck,

View File

@ -67,6 +67,7 @@ const previewableTypes = [
FieldMetadataType.Select,
FieldMetadataType.MultiSelect,
FieldMetadataType.Link,
FieldMetadataType.Links,
FieldMetadataType.Number,
FieldMetadataType.Rating,
FieldMetadataType.Relation,

View File

@ -0,0 +1,14 @@
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
import { getUrlHostName } from '~/utils/url/getUrlHostName';
type LinksDisplayProps = {
value?: FieldLinksValue;
};
export const LinksDisplay = ({ value }: LinksDisplayProps) => {
const url = value?.primaryLinkUrl || '';
const label = value?.primaryLinkLabel || getUrlHostName(url);
return <LinkDisplay value={{ url, label }} />;
};

View File

@ -282,6 +282,7 @@ export const SettingsObjectNewFieldStep2 = () => {
FieldMetadataType.Email,
FieldMetadataType.FullName,
FieldMetadataType.Link,
FieldMetadataType.Links,
FieldMetadataType.Numeric,
FieldMetadataType.Phone,
FieldMetadataType.Probability,

View File

@ -0,0 +1,25 @@
import { getUrlHostName } from '~/utils/url/getUrlHostName';
describe('getUrlHostName', () => {
it("returns the URL's hostname", () => {
expect(getUrlHostName('https://www.example.com')).toBe('example.com');
expect(getUrlHostName('http://subdomain.example.com')).toBe(
'subdomain.example.com',
);
expect(getUrlHostName('https://www.example.com/path')).toBe('example.com');
expect(getUrlHostName('https://www.example.com?query=123')).toBe(
'example.com',
);
expect(getUrlHostName('http://localhost:3000')).toBe('localhost');
expect(getUrlHostName('example.com')).toBe('example.com');
expect(getUrlHostName('www.subdomain.example.com')).toBe(
'subdomain.example.com',
);
});
it('returns an empty string for invalid URLs', () => {
expect(getUrlHostName('?o')).toBe('');
expect(getUrlHostName('')).toBe('');
expect(getUrlHostName('\\')).toBe('');
});
});

View File

@ -0,0 +1,10 @@
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
export const getUrlHostName = (url: string) => {
try {
const absoluteUrl = absoluteUrlSchema.parse(url);
return new URL(absoluteUrl).hostname.replace(/^www\./i, '');
} catch {
return '';
}
};

View File

@ -0,0 +1,34 @@
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
describe('absoluteUrlSchema', () => {
it('validates an absolute url', () => {
expect(absoluteUrlSchema.parse('https://www.example.com')).toBe(
'https://www.example.com',
);
expect(absoluteUrlSchema.parse('http://subdomain.example.com')).toBe(
'http://subdomain.example.com',
);
expect(absoluteUrlSchema.parse('https://www.example.com/path')).toBe(
'https://www.example.com/path',
);
expect(absoluteUrlSchema.parse('https://www.example.com?query=123')).toBe(
'https://www.example.com?query=123',
);
expect(absoluteUrlSchema.parse('http://localhost:3000')).toBe(
'http://localhost:3000',
);
});
it('transforms a non-absolute URL to an absolute URL', () => {
expect(absoluteUrlSchema.parse('example.com')).toBe('https://example.com');
expect(absoluteUrlSchema.parse('www.subdomain.example.com')).toBe(
'https://www.subdomain.example.com',
);
});
it('fails for invalid urls', () => {
expect(absoluteUrlSchema.safeParse('?o').success).toBe(false);
expect(absoluteUrlSchema.safeParse('').success).toBe(false);
expect(absoluteUrlSchema.safeParse('\\').success).toBe(false);
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const absoluteUrlSchema = z
.string()
.url()
.or(
z
.string()
.transform((value) => `https://${value}`)
.pipe(z.string().url()),
);

View File

@ -69,7 +69,9 @@ export class CompositeInputTypeDefinitionFactory {
options,
{
nullable: !property.isRequired,
isArray: property.type === FieldMetadataType.MULTI_SELECT,
isArray:
property.type === FieldMetadataType.MULTI_SELECT ||
property.isArray,
},
);

View File

@ -69,7 +69,9 @@ export class CompositeObjectTypeDefinitionFactory {
options,
{
nullable: !property.isRequired,
isArray: property.type === FieldMetadataType.MULTI_SELECT,
isArray:
property.type === FieldMetadataType.MULTI_SELECT ||
property.isArray,
},
);

View File

@ -92,6 +92,15 @@ export const mapFieldMetadataToGraphqlQuery = (
url
}
`;
} else if (fieldType === FieldMetadataType.LINKS) {
return `
${field.name}
{
primaryLinkLabel
primaryLinkUrl
secondaryLinks
}
`;
} else if (fieldType === FieldMetadataType.CURRENCY) {
return `
${field.name}

View File

@ -57,6 +57,7 @@ const getSchemaComponentsProperties = (
}
break;
case FieldMetadataType.LINK:
case FieldMetadataType.LINKS:
case FieldMetadataType.CURRENCY:
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.ADDRESS:

View File

@ -15,9 +15,10 @@ import {
} 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 {
AddressMetadata,
addressCompositeType,
AddressMetadata,
} from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
export type CompositeFieldsDefinitionFunction = (
fieldMetadata?: FieldMetadataInterface,
@ -28,6 +29,7 @@ export const compositeTypeDefintions = new Map<
CompositeType
>([
[FieldMetadataType.LINK, linkCompositeType],
[FieldMetadataType.LINKS, linksCompositeType],
[FieldMetadataType.CURRENCY, currencyCompositeType],
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
[FieldMetadataType.ADDRESS, addressCompositeType],

View File

@ -0,0 +1,33 @@
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const linksCompositeType: CompositeType = {
type: FieldMetadataType.LINKS,
properties: [
{
name: 'primaryLinkLabel',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
name: 'primaryLinkUrl',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
name: 'secondaryLinks',
type: FieldMetadataType.RAW_JSON,
hidden: false,
isRequired: false,
},
],
};
export type LinksMetadata = {
primaryLinkLabel: string;
primaryLinkUrl: string;
secondaryLinks: JSON | null;
};

View File

@ -138,3 +138,17 @@ export class FieldMetadataDefaultValueAddress {
@IsNumber()
addressLng: number | null;
}
export class FieldMetadataDefaultValueLinks {
@ValidateIf((_object, value) => value !== null)
@IsQuotedString()
primaryLinkLabel: string | null;
@ValidateIf((_object, value) => value !== null)
@IsQuotedString()
primaryLinkUrl: string | null;
@ValidateIf((_object, value) => value !== null)
@IsJSON()
secondaryLinks: JSON | null;
}

View File

@ -31,6 +31,7 @@ export enum FieldMetadataType {
NUMERIC = 'NUMERIC',
PROBABILITY = 'PROBABILITY',
LINK = 'LINK',
LINKS = 'LINKS',
CURRENCY = 'CURRENCY',
FULL_NAME = 'FULL_NAME',
RATING = 'RATING',

View File

@ -6,6 +6,7 @@ export interface CompositeProperty {
type: FieldMetadataType;
hidden: 'input' | 'output' | true | false;
isRequired: boolean;
isArray?: boolean;
}
export interface CompositeType {

View File

@ -10,6 +10,7 @@ import {
FieldMetadataDefaultValueString,
FieldMetadataDefaultValueUuidFunction,
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueLinks,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -36,6 +37,7 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
[FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber;
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
[FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks;
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;

View File

@ -36,6 +36,12 @@ export function generateDefaultValue(
amountMicros: null,
currencyCode: "''",
};
case FieldMetadataType.LINKS:
return {
primaryLinkLabel: "''",
primaryLinkUrl: "''",
secondaryLinks: null,
};
default:
return null;
}

View File

@ -5,11 +5,14 @@ export const isCompositeFieldMetadataType = (
): type is
| FieldMetadataType.LINK
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME => {
return (
type === FieldMetadataType.LINK ||
type === FieldMetadataType.CURRENCY ||
type === FieldMetadataType.FULL_NAME ||
type === FieldMetadataType.ADDRESS
);
| FieldMetadataType.FULL_NAME
| FieldMetadataType.ADDRESS
| FieldMetadataType.LINKS => {
return [
FieldMetadataType.LINK,
FieldMetadataType.CURRENCY,
FieldMetadataType.FULL_NAME,
FieldMetadataType.ADDRESS,
FieldMetadataType.LINKS,
].includes(type);
};

View File

@ -21,6 +21,7 @@ import {
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueUuidFunction,
FieldMetadataDefaultValueDate,
FieldMetadataDefaultValueLinks,
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
@ -49,6 +50,7 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
[FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress],
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
};
type ValidationResult = {

View File

@ -18,7 +18,8 @@ export type CompositeFieldMetadataType =
| FieldMetadataType.ADDRESS
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME
| FieldMetadataType.LINK;
| FieldMetadataType.LINK
| FieldMetadataType.LINKS;
@Injectable()
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
@ -51,6 +52,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
columnType: fieldMetadataTypeToColumnType(property.type),
isNullable: fieldMetadata.isNullable || !property.isRequired,
defaultValue: serializedDefaultValue,
isArray: property.isArray,
});
}
@ -116,6 +118,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue?.[currentProperty.name],
),
isArray: currentProperty.isArray,
},
alteredColumnDefinition: {
columnName: alteredColumnName,
@ -123,6 +126,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
isNullable:
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
defaultValue: serializedDefaultValue,
isArray: alteredProperty.isArray,
},
});
}

View File

@ -94,6 +94,7 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.FULL_NAME,
{ factory: this.compositeColumnActionFactory },
],
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
]);
}

View File

@ -8,6 +8,7 @@ import {
ObjectLiteral,
} from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { DeepPartial } from 'typeorm/common/DeepPartial';
import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';