feat: add Links field type (#5176)
Closes #5113 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -305,6 +305,7 @@ export enum FieldMetadataType {
|
|||||||
Email = 'EMAIL',
|
Email = 'EMAIL',
|
||||||
FullName = 'FULL_NAME',
|
FullName = 'FULL_NAME',
|
||||||
Link = 'LINK',
|
Link = 'LINK',
|
||||||
|
Links = 'LINKS',
|
||||||
MultiSelect = 'MULTI_SELECT',
|
MultiSelect = 'MULTI_SELECT',
|
||||||
Number = 'NUMBER',
|
Number = 'NUMBER',
|
||||||
Numeric = 'NUMERIC',
|
Numeric = 'NUMERIC',
|
||||||
|
|||||||
@ -212,6 +212,7 @@ export enum FieldMetadataType {
|
|||||||
Email = 'EMAIL',
|
Email = 'EMAIL',
|
||||||
FullName = 'FULL_NAME',
|
FullName = 'FULL_NAME',
|
||||||
Link = 'LINK',
|
Link = 'LINK',
|
||||||
|
Links = 'LINKS',
|
||||||
MultiSelect = 'MULTI_SELECT',
|
MultiSelect = 'MULTI_SELECT',
|
||||||
Number = 'NUMBER',
|
Number = 'NUMBER',
|
||||||
Numeric = 'NUMERIC',
|
Numeric = 'NUMERIC',
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { useContext } from 'react';
|
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 { FieldContext } from '../contexts/FieldContext';
|
||||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||||
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
|
||||||
@ -62,6 +65,8 @@ export const FieldDisplay = () => {
|
|||||||
<NumberFieldDisplay />
|
<NumberFieldDisplay />
|
||||||
) : isFieldLink(fieldDefinition) ? (
|
) : isFieldLink(fieldDefinition) ? (
|
||||||
<LinkFieldDisplay />
|
<LinkFieldDisplay />
|
||||||
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
|
<LinksFieldDisplay />
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldDisplay />
|
<CurrencyFieldDisplay />
|
||||||
) : isFieldFullName(fieldDefinition) ? (
|
) : isFieldFullName(fieldDefinition) ? (
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import { useContext } from 'react';
|
|||||||
import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput';
|
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 { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
|
||||||
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 { 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 { 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 { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
||||||
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';
|
||||||
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
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 { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
@ -131,6 +133,14 @@ export const FieldInput = ({
|
|||||||
onTab={onTab}
|
onTab={onTab}
|
||||||
onShiftTab={onShiftTab}
|
onShiftTab={onShiftTab}
|
||||||
/>
|
/>
|
||||||
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
|
<LinksFieldInput
|
||||||
|
onEnter={onEnter}
|
||||||
|
onEscape={onEscape}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onTab={onTab}
|
||||||
|
onShiftTab={onShiftTab}
|
||||||
|
/>
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldInput
|
<CurrencyFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
|||||||
@ -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 { isFieldDateValue } from '@/object-record/record-field/types/guards/isFieldDateValue';
|
||||||
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 { 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 { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
@ -69,6 +71,9 @@ export const usePersistField = () => {
|
|||||||
const fieldIsLink =
|
const fieldIsLink =
|
||||||
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
|
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
|
||||||
|
|
||||||
|
const fieldIsLinks =
|
||||||
|
isFieldLinks(fieldDefinition) && isFieldLinksValue(valueToPersist);
|
||||||
|
|
||||||
const fieldIsBoolean =
|
const fieldIsBoolean =
|
||||||
isFieldBoolean(fieldDefinition) &&
|
isFieldBoolean(fieldDefinition) &&
|
||||||
isFieldBooleanValue(valueToPersist);
|
isFieldBooleanValue(valueToPersist);
|
||||||
@ -116,6 +121,7 @@ export const usePersistField = () => {
|
|||||||
fieldIsDate ||
|
fieldIsDate ||
|
||||||
fieldIsPhone ||
|
fieldIsPhone ||
|
||||||
fieldIsLink ||
|
fieldIsLink ||
|
||||||
|
fieldIsLinks ||
|
||||||
fieldIsCurrency ||
|
fieldIsCurrency ||
|
||||||
fieldIsFullName ||
|
fieldIsFullName ||
|
||||||
fieldIsSelect ||
|
fieldIsSelect ||
|
||||||
@ -123,7 +129,7 @@ export const usePersistField = () => {
|
|||||||
fieldIsAddress ||
|
fieldIsAddress ||
|
||||||
fieldIsRawJson;
|
fieldIsRawJson;
|
||||||
|
|
||||||
if (isValuePersistable === true) {
|
if (isValuePersistable) {
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
set(
|
set(
|
||||||
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
recordStoreFamilySelector({ recordId: entityId, fieldName }),
|
||||||
|
|||||||
@ -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} />;
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -6,6 +6,7 @@ import {
|
|||||||
FieldDateTimeValue,
|
FieldDateTimeValue,
|
||||||
FieldEmailValue,
|
FieldEmailValue,
|
||||||
FieldFullNameValue,
|
FieldFullNameValue,
|
||||||
|
FieldLinksValue,
|
||||||
FieldLinkValue,
|
FieldLinkValue,
|
||||||
FieldMultiSelectValue,
|
FieldMultiSelectValue,
|
||||||
FieldNumberValue,
|
FieldNumberValue,
|
||||||
@ -26,6 +27,11 @@ export type FieldSelectDraftValue = string;
|
|||||||
export type FieldMultiSelectDraftValue = string[];
|
export type FieldMultiSelectDraftValue = string[];
|
||||||
export type FieldRelationDraftValue = string;
|
export type FieldRelationDraftValue = string;
|
||||||
export type FieldLinkDraftValue = { url: string; label: string };
|
export type FieldLinkDraftValue = { url: string; label: string };
|
||||||
|
export type FieldLinksDraftValue = {
|
||||||
|
primaryLinkLabel: string;
|
||||||
|
primaryLinkUrl: string;
|
||||||
|
secondaryLinks?: string | null;
|
||||||
|
};
|
||||||
export type FieldCurrencyDraftValue = {
|
export type FieldCurrencyDraftValue = {
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
amount: string;
|
amount: string;
|
||||||
@ -58,18 +64,20 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldEmailDraftValue
|
? FieldEmailDraftValue
|
||||||
: FieldValue extends FieldLinkValue
|
: FieldValue extends FieldLinkValue
|
||||||
? FieldLinkDraftValue
|
? FieldLinkDraftValue
|
||||||
: FieldValue extends FieldCurrencyValue
|
: FieldValue extends FieldLinksValue
|
||||||
? FieldCurrencyDraftValue
|
? FieldLinksDraftValue
|
||||||
: FieldValue extends FieldFullNameValue
|
: FieldValue extends FieldCurrencyValue
|
||||||
? FieldFullNameDraftValue
|
? FieldCurrencyDraftValue
|
||||||
: FieldValue extends FieldRatingValue
|
: FieldValue extends FieldFullNameValue
|
||||||
? FieldRatingValue
|
? FieldFullNameDraftValue
|
||||||
: FieldValue extends FieldSelectValue
|
: FieldValue extends FieldRatingValue
|
||||||
? FieldSelectDraftValue
|
? FieldRatingValue
|
||||||
: FieldValue extends FieldMultiSelectValue
|
: FieldValue extends FieldSelectValue
|
||||||
? FieldMultiSelectDraftValue
|
? FieldSelectDraftValue
|
||||||
: FieldValue extends FieldRelationValue
|
: FieldValue extends FieldMultiSelectValue
|
||||||
? FieldRelationDraftValue
|
? FieldMultiSelectDraftValue
|
||||||
: FieldValue extends FieldAddressValue
|
: FieldValue extends FieldRelationValue
|
||||||
? FieldAddressDraftValue
|
? FieldRelationDraftValue
|
||||||
: never;
|
: FieldValue extends FieldAddressValue
|
||||||
|
? FieldAddressDraftValue
|
||||||
|
: never;
|
||||||
|
|||||||
@ -45,6 +45,11 @@ export type FieldLinkMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldLinksMetadata = {
|
||||||
|
objectMetadataNameSingular?: string;
|
||||||
|
fieldName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldCurrencyMetadata = {
|
export type FieldCurrencyMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -143,6 +148,11 @@ export type FieldBooleanValue = boolean;
|
|||||||
export type FieldPhoneValue = string;
|
export type FieldPhoneValue = string;
|
||||||
export type FieldEmailValue = string;
|
export type FieldEmailValue = string;
|
||||||
export type FieldLinkValue = { url: string; label: string };
|
export type FieldLinkValue = { url: string; label: string };
|
||||||
|
export type FieldLinksValue = {
|
||||||
|
primaryLinkLabel: string;
|
||||||
|
primaryLinkUrl: string;
|
||||||
|
secondaryLinks?: string | null;
|
||||||
|
};
|
||||||
export type FieldCurrencyValue = {
|
export type FieldCurrencyValue = {
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
amountMicros: number | null;
|
amountMicros: number | null;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
FieldEmailMetadata,
|
FieldEmailMetadata,
|
||||||
FieldFullNameMetadata,
|
FieldFullNameMetadata,
|
||||||
FieldLinkMetadata,
|
FieldLinkMetadata,
|
||||||
|
FieldLinksMetadata,
|
||||||
FieldMetadata,
|
FieldMetadata,
|
||||||
FieldMultiSelectMetadata,
|
FieldMultiSelectMetadata,
|
||||||
FieldNumberMetadata,
|
FieldNumberMetadata,
|
||||||
@ -44,23 +45,25 @@ type AssertFieldMetadataFunction = <
|
|||||||
? FieldRatingMetadata
|
? FieldRatingMetadata
|
||||||
: E extends 'LINK'
|
: E extends 'LINK'
|
||||||
? FieldLinkMetadata
|
? FieldLinkMetadata
|
||||||
: E extends 'NUMBER'
|
: E extends 'LINKS'
|
||||||
? FieldNumberMetadata
|
? FieldLinksMetadata
|
||||||
: E extends 'PHONE'
|
: E extends 'NUMBER'
|
||||||
? FieldPhoneMetadata
|
? FieldNumberMetadata
|
||||||
: E extends 'PROBABILITY'
|
: E extends 'PHONE'
|
||||||
? FieldRatingMetadata
|
? FieldPhoneMetadata
|
||||||
: E extends 'RELATION'
|
: E extends 'PROBABILITY'
|
||||||
? FieldRelationMetadata
|
? FieldRatingMetadata
|
||||||
: E extends 'TEXT'
|
: E extends 'RELATION'
|
||||||
? FieldTextMetadata
|
? FieldRelationMetadata
|
||||||
: E extends 'UUID'
|
: E extends 'TEXT'
|
||||||
? FieldUuidMetadata
|
? FieldTextMetadata
|
||||||
: E extends 'ADDRESS'
|
: E extends 'UUID'
|
||||||
? FieldAddressMetadata
|
? FieldUuidMetadata
|
||||||
: E extends 'RAW_JSON'
|
: E extends 'ADDRESS'
|
||||||
? FieldRawJsonMetadata
|
? FieldAddressMetadata
|
||||||
: never,
|
: E extends 'RAW_JSON'
|
||||||
|
? FieldRawJsonMetadata
|
||||||
|
: never,
|
||||||
>(
|
>(
|
||||||
fieldType: E,
|
fieldType: E,
|
||||||
fieldTypeGuard: (
|
fieldTypeGuard: (
|
||||||
|
|||||||
@ -7,7 +7,6 @@ const linkSchema = z.object({
|
|||||||
label: z.string(),
|
label: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: add zod
|
|
||||||
export const isFieldLinkValue = (
|
export const isFieldLinkValue = (
|
||||||
fieldValue: unknown,
|
fieldValue: unknown,
|
||||||
): fieldValue is FieldLinkValue => linkSchema.safeParse(fieldValue).success;
|
): fieldValue is FieldLinkValue => linkSchema.safeParse(fieldValue).success;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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 { 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 { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
|
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 { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
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(
|
throw new Error(
|
||||||
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const generateEmptyFieldValue = (
|
export const generateEmptyFieldValue = (
|
||||||
fieldMetadataItem: FieldMetadataItem,
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
@ -18,6 +18,9 @@ export const generateEmptyFieldValue = (
|
|||||||
url: '',
|
url: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Links: {
|
||||||
|
return { primaryLinkUrl: '', primaryLinkLabel: '' };
|
||||||
|
}
|
||||||
case FieldMetadataType.FullName: {
|
case FieldMetadataType.FullName: {
|
||||||
return {
|
return {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
|
|||||||
@ -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, '')
|
|
||||||
: '';
|
|
||||||
};
|
|
||||||
@ -4,9 +4,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { sanitizeLink } from '@/object-record/utils/sanitizeLinkRecordInput';
|
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||||
|
|
||||||
export const sanitizeRecordInput = ({
|
export const sanitizeRecordInput = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -54,6 +54,6 @@ export const sanitizeRecordInput = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...filteredResultRecord,
|
...filteredResultRecord,
|
||||||
domainName: sanitizeLink(filteredResultRecord.domainName),
|
domainName: getUrlHostName(filteredResultRecord.domainName),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -62,6 +62,11 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
|
|||||||
Icon: IconLink,
|
Icon: IconLink,
|
||||||
defaultValue: { url: 'www.twenty.com', label: '' },
|
defaultValue: { url: 'www.twenty.com', label: '' },
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.Links]: {
|
||||||
|
label: 'Links',
|
||||||
|
Icon: IconLink,
|
||||||
|
defaultValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' },
|
||||||
|
},
|
||||||
[FieldMetadataType.Boolean]: {
|
[FieldMetadataType.Boolean]: {
|
||||||
label: 'True/False',
|
label: 'True/False',
|
||||||
Icon: IconCheck,
|
Icon: IconCheck,
|
||||||
|
|||||||
@ -67,6 +67,7 @@ const previewableTypes = [
|
|||||||
FieldMetadataType.Select,
|
FieldMetadataType.Select,
|
||||||
FieldMetadataType.MultiSelect,
|
FieldMetadataType.MultiSelect,
|
||||||
FieldMetadataType.Link,
|
FieldMetadataType.Link,
|
||||||
|
FieldMetadataType.Links,
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
FieldMetadataType.Relation,
|
FieldMetadataType.Relation,
|
||||||
|
|||||||
@ -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 }} />;
|
||||||
|
};
|
||||||
@ -282,6 +282,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
FieldMetadataType.Email,
|
FieldMetadataType.Email,
|
||||||
FieldMetadataType.FullName,
|
FieldMetadataType.FullName,
|
||||||
FieldMetadataType.Link,
|
FieldMetadataType.Link,
|
||||||
|
FieldMetadataType.Links,
|
||||||
FieldMetadataType.Numeric,
|
FieldMetadataType.Numeric,
|
||||||
FieldMetadataType.Phone,
|
FieldMetadataType.Phone,
|
||||||
FieldMetadataType.Probability,
|
FieldMetadataType.Probability,
|
||||||
|
|||||||
@ -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('');
|
||||||
|
});
|
||||||
|
});
|
||||||
10
packages/twenty-front/src/utils/url/getUrlHostName.ts
Normal file
10
packages/twenty-front/src/utils/url/getUrlHostName.ts
Normal 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 '';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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()),
|
||||||
|
);
|
||||||
@ -69,7 +69,9 @@ export class CompositeInputTypeDefinitionFactory {
|
|||||||
options,
|
options,
|
||||||
{
|
{
|
||||||
nullable: !property.isRequired,
|
nullable: !property.isRequired,
|
||||||
isArray: property.type === FieldMetadataType.MULTI_SELECT,
|
isArray:
|
||||||
|
property.type === FieldMetadataType.MULTI_SELECT ||
|
||||||
|
property.isArray,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,9 @@ export class CompositeObjectTypeDefinitionFactory {
|
|||||||
options,
|
options,
|
||||||
{
|
{
|
||||||
nullable: !property.isRequired,
|
nullable: !property.isRequired,
|
||||||
isArray: property.type === FieldMetadataType.MULTI_SELECT,
|
isArray:
|
||||||
|
property.type === FieldMetadataType.MULTI_SELECT ||
|
||||||
|
property.isArray,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,15 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
} else if (fieldType === FieldMetadataType.LINKS) {
|
||||||
|
return `
|
||||||
|
${field.name}
|
||||||
|
{
|
||||||
|
primaryLinkLabel
|
||||||
|
primaryLinkUrl
|
||||||
|
secondaryLinks
|
||||||
|
}
|
||||||
|
`;
|
||||||
} else if (fieldType === FieldMetadataType.CURRENCY) {
|
} else if (fieldType === FieldMetadataType.CURRENCY) {
|
||||||
return `
|
return `
|
||||||
${field.name}
|
${field.name}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const getSchemaComponentsProperties = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.LINK:
|
case FieldMetadataType.LINK:
|
||||||
|
case FieldMetadataType.LINKS:
|
||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
case FieldMetadataType.FULL_NAME:
|
case FieldMetadataType.FULL_NAME:
|
||||||
case FieldMetadataType.ADDRESS:
|
case FieldMetadataType.ADDRESS:
|
||||||
|
|||||||
@ -15,9 +15,10 @@ import {
|
|||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
} 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 {
|
import {
|
||||||
AddressMetadata,
|
|
||||||
addressCompositeType,
|
addressCompositeType,
|
||||||
|
AddressMetadata,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
} 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 = (
|
export type CompositeFieldsDefinitionFunction = (
|
||||||
fieldMetadata?: FieldMetadataInterface,
|
fieldMetadata?: FieldMetadataInterface,
|
||||||
@ -28,6 +29,7 @@ export const compositeTypeDefintions = new Map<
|
|||||||
CompositeType
|
CompositeType
|
||||||
>([
|
>([
|
||||||
[FieldMetadataType.LINK, linkCompositeType],
|
[FieldMetadataType.LINK, linkCompositeType],
|
||||||
|
[FieldMetadataType.LINKS, linksCompositeType],
|
||||||
[FieldMetadataType.CURRENCY, currencyCompositeType],
|
[FieldMetadataType.CURRENCY, currencyCompositeType],
|
||||||
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
|
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
|
||||||
[FieldMetadataType.ADDRESS, addressCompositeType],
|
[FieldMetadataType.ADDRESS, addressCompositeType],
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -138,3 +138,17 @@ export class FieldMetadataDefaultValueAddress {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
addressLng: number | null;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export enum FieldMetadataType {
|
|||||||
NUMERIC = 'NUMERIC',
|
NUMERIC = 'NUMERIC',
|
||||||
PROBABILITY = 'PROBABILITY',
|
PROBABILITY = 'PROBABILITY',
|
||||||
LINK = 'LINK',
|
LINK = 'LINK',
|
||||||
|
LINKS = 'LINKS',
|
||||||
CURRENCY = 'CURRENCY',
|
CURRENCY = 'CURRENCY',
|
||||||
FULL_NAME = 'FULL_NAME',
|
FULL_NAME = 'FULL_NAME',
|
||||||
RATING = 'RATING',
|
RATING = 'RATING',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface CompositeProperty {
|
|||||||
type: FieldMetadataType;
|
type: FieldMetadataType;
|
||||||
hidden: 'input' | 'output' | true | false;
|
hidden: 'input' | 'output' | true | false;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
|
isArray?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompositeType {
|
export interface CompositeType {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
FieldMetadataDefaultValueString,
|
FieldMetadataDefaultValueString,
|
||||||
FieldMetadataDefaultValueUuidFunction,
|
FieldMetadataDefaultValueUuidFunction,
|
||||||
FieldMetadataDefaultValueNowFunction,
|
FieldMetadataDefaultValueNowFunction,
|
||||||
|
FieldMetadataDefaultValueLinks,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ type FieldMetadataDefaultValueMapping = {
|
|||||||
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber;
|
[FieldMetadataType.PROBABILITY]: FieldMetadataDefaultValueNumber;
|
||||||
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
|
[FieldMetadataType.LINK]: FieldMetadataDefaultValueLink;
|
||||||
|
[FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks;
|
||||||
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
[FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency;
|
||||||
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
[FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName;
|
||||||
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;
|
[FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress;
|
||||||
|
|||||||
@ -36,6 +36,12 @@ export function generateDefaultValue(
|
|||||||
amountMicros: null,
|
amountMicros: null,
|
||||||
currencyCode: "''",
|
currencyCode: "''",
|
||||||
};
|
};
|
||||||
|
case FieldMetadataType.LINKS:
|
||||||
|
return {
|
||||||
|
primaryLinkLabel: "''",
|
||||||
|
primaryLinkUrl: "''",
|
||||||
|
secondaryLinks: null,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,14 @@ export const isCompositeFieldMetadataType = (
|
|||||||
): type is
|
): type is
|
||||||
| FieldMetadataType.LINK
|
| FieldMetadataType.LINK
|
||||||
| FieldMetadataType.CURRENCY
|
| FieldMetadataType.CURRENCY
|
||||||
| FieldMetadataType.FULL_NAME => {
|
| FieldMetadataType.FULL_NAME
|
||||||
return (
|
| FieldMetadataType.ADDRESS
|
||||||
type === FieldMetadataType.LINK ||
|
| FieldMetadataType.LINKS => {
|
||||||
type === FieldMetadataType.CURRENCY ||
|
return [
|
||||||
type === FieldMetadataType.FULL_NAME ||
|
FieldMetadataType.LINK,
|
||||||
type === FieldMetadataType.ADDRESS
|
FieldMetadataType.CURRENCY,
|
||||||
);
|
FieldMetadataType.FULL_NAME,
|
||||||
|
FieldMetadataType.ADDRESS,
|
||||||
|
FieldMetadataType.LINKS,
|
||||||
|
].includes(type);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
FieldMetadataDefaultValueNowFunction,
|
FieldMetadataDefaultValueNowFunction,
|
||||||
FieldMetadataDefaultValueUuidFunction,
|
FieldMetadataDefaultValueUuidFunction,
|
||||||
FieldMetadataDefaultValueDate,
|
FieldMetadataDefaultValueDate,
|
||||||
|
FieldMetadataDefaultValueLinks,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input';
|
} 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';
|
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.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
||||||
[FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress],
|
[FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress],
|
||||||
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
||||||
|
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
|
||||||
};
|
};
|
||||||
|
|
||||||
type ValidationResult = {
|
type ValidationResult = {
|
||||||
|
|||||||
@ -18,7 +18,8 @@ export type CompositeFieldMetadataType =
|
|||||||
| FieldMetadataType.ADDRESS
|
| FieldMetadataType.ADDRESS
|
||||||
| FieldMetadataType.CURRENCY
|
| FieldMetadataType.CURRENCY
|
||||||
| FieldMetadataType.FULL_NAME
|
| FieldMetadataType.FULL_NAME
|
||||||
| FieldMetadataType.LINK;
|
| FieldMetadataType.LINK
|
||||||
|
| FieldMetadataType.LINKS;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
|
export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<CompositeFieldMetadataType> {
|
||||||
@ -51,6 +52,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
|||||||
columnType: fieldMetadataTypeToColumnType(property.type),
|
columnType: fieldMetadataTypeToColumnType(property.type),
|
||||||
isNullable: fieldMetadata.isNullable || !property.isRequired,
|
isNullable: fieldMetadata.isNullable || !property.isRequired,
|
||||||
defaultValue: serializedDefaultValue,
|
defaultValue: serializedDefaultValue,
|
||||||
|
isArray: property.isArray,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +118,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
|||||||
defaultValue: serializeDefaultValue(
|
defaultValue: serializeDefaultValue(
|
||||||
currentFieldMetadata.defaultValue?.[currentProperty.name],
|
currentFieldMetadata.defaultValue?.[currentProperty.name],
|
||||||
),
|
),
|
||||||
|
isArray: currentProperty.isArray,
|
||||||
},
|
},
|
||||||
alteredColumnDefinition: {
|
alteredColumnDefinition: {
|
||||||
columnName: alteredColumnName,
|
columnName: alteredColumnName,
|
||||||
@ -123,6 +126,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
|||||||
isNullable:
|
isNullable:
|
||||||
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
|
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
|
||||||
defaultValue: serializedDefaultValue,
|
defaultValue: serializedDefaultValue,
|
||||||
|
isArray: alteredProperty.isArray,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export class WorkspaceMigrationFactory {
|
|||||||
FieldMetadataType.FULL_NAME,
|
FieldMetadataType.FULL_NAME,
|
||||||
{ factory: this.compositeColumnActionFactory },
|
{ factory: this.compositeColumnActionFactory },
|
||||||
],
|
],
|
||||||
|
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
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';
|
import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user