fix: fix several field bugs (#5339)
After discussing with @charlesBochet, several fixes are needed on fields: - [x] Disable Boolean field `defaultValue` edition for now (On `defaultValue` update, newly created records are not taking the updated `defaultValue` into account. Setting the `defaultValue` on creation is fine.) - [x] Disable Phone field creation for now - [x] For the Person object, display the "Phone" field as a field of type Phone (right now its type is Text; later we'll migrate it to a proper Phone field). - [x] Fix RawJson field display (displaying `[object Object]` in Record Table cells). - [x] In Settings/Data Model, on Relation field creation/edition, "Object destination" select is not working properly if an object was not manually selected (displays Companies by default but creates a relation to another random object than Companies).
This commit is contained in:
@ -1,3 +1,5 @@
|
|||||||
|
import pick from 'lodash.pick';
|
||||||
|
|
||||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
@ -12,15 +14,11 @@ export const getRecordFromRecordNode = <T extends ObjectRecord>({
|
|||||||
return {
|
return {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(recordNode).map(([fieldName, value]) => {
|
Object.entries(recordNode).map(([fieldName, value]) => {
|
||||||
if (isUndefinedOrNull(value)) {
|
if (
|
||||||
return [fieldName, value];
|
isUndefinedOrNull(value) ||
|
||||||
}
|
Array.isArray(value) ||
|
||||||
|
typeof value !== 'object'
|
||||||
if (Array.isArray(value)) {
|
) {
|
||||||
return [fieldName, value];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return [fieldName, value];
|
return [fieldName, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +30,10 @@ export const getRecordFromRecordNode = <T extends ObjectRecord>({
|
|||||||
: [fieldName, getRecordFromRecordNode<T>({ recordNode: value })];
|
: [fieldName, getRecordFromRecordNode<T>({ recordNode: value })];
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
id: recordNode.id,
|
// Only adds `id` and `__typename` if they exist.
|
||||||
__typename: recordNode.__typename,
|
// RawJson field value passes through this method and does not have `id` or `__typename`.
|
||||||
|
// This prevents adding an undefined `id` and `__typename` to the RawJson field value,
|
||||||
|
// which is invalid JSON.
|
||||||
|
...pick(recordNode, ['id', '__typename'] as const),
|
||||||
} as T;
|
} as T;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
|
||||||
@ -56,7 +57,8 @@ export const FieldDisplay = ({
|
|||||||
<ChipFieldDisplay />
|
<ChipFieldDisplay />
|
||||||
) : isFieldRelation(fieldDefinition) ? (
|
) : isFieldRelation(fieldDefinition) ? (
|
||||||
<RelationFieldDisplay />
|
<RelationFieldDisplay />
|
||||||
) : isFieldPhone(fieldDefinition) ? (
|
) : isFieldPhone(fieldDefinition) ||
|
||||||
|
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||||
<PhoneFieldDisplay />
|
<PhoneFieldDisplay />
|
||||||
) : isFieldText(fieldDefinition) ? (
|
) : isFieldText(fieldDefinition) ? (
|
||||||
<TextFieldDisplay />
|
<TextFieldDisplay />
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input
|
|||||||
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 { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
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 { 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';
|
||||||
@ -71,7 +72,8 @@ export const FieldInput = ({
|
|||||||
>
|
>
|
||||||
{isFieldRelation(fieldDefinition) ? (
|
{isFieldRelation(fieldDefinition) ? (
|
||||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
) : isFieldPhone(fieldDefinition) ? (
|
) : isFieldPhone(fieldDefinition) ||
|
||||||
|
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||||
<PhoneFieldInput
|
<PhoneFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
onEscape={onEscape}
|
onEscape={onEscape}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { IconComponent, IconPencil } from 'twenty-ui';
|
import { IconComponent, IconPencil } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
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 { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
@ -20,6 +21,7 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
|
|||||||
isFieldLink(fieldDefinition) ||
|
isFieldLink(fieldDefinition) ||
|
||||||
isFieldEmail(fieldDefinition) ||
|
isFieldEmail(fieldDefinition) ||
|
||||||
isFieldPhone(fieldDefinition) ||
|
isFieldPhone(fieldDefinition) ||
|
||||||
|
isFieldDisplayedAsPhone(fieldDefinition) ||
|
||||||
isFieldMultiSelect(fieldDefinition) ||
|
isFieldMultiSelect(fieldDefinition) ||
|
||||||
(isFieldRelation(fieldDefinition) &&
|
(isFieldRelation(fieldDefinition) &&
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField';
|
import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField';
|
||||||
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
|
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
|
||||||
|
|
||||||
export const JsonFieldDisplay = () => {
|
export const JsonFieldDisplay = () => {
|
||||||
@ -6,7 +7,7 @@ export const JsonFieldDisplay = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<JsonDisplay
|
<JsonDisplay
|
||||||
text={fieldValue ? JSON.stringify(JSON.parse(fieldValue), null, 2) : ''}
|
text={isFieldRawJsonValue(fieldValue) ? JSON.stringify(fieldValue) : ''}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||||
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
@ -9,7 +10,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
import { isFieldRawJson } from '../../types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '../../types/guards/isFieldRawJson';
|
||||||
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
|
|
||||||
|
|
||||||
export const useJsonField = () => {
|
export const useJsonField = () => {
|
||||||
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
|
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
|
||||||
@ -29,7 +29,18 @@ export const useJsonField = () => {
|
|||||||
fieldName: fieldName,
|
fieldName: fieldName,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
|
|
||||||
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
const persistJsonField = (nextValue: string) => {
|
||||||
|
if (!nextValue) persistField(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
persistField(JSON.parse(nextValue));
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { setDraftValue, getDraftValueSelector } =
|
const { setDraftValue, getDraftValueSelector } =
|
||||||
useRecordFieldInput<FieldJsonValue>(`${entityId}-${fieldName}`);
|
useRecordFieldInput<FieldJsonValue>(`${entityId}-${fieldName}`);
|
||||||
@ -41,8 +52,9 @@ export const useJsonField = () => {
|
|||||||
setDraftValue,
|
setDraftValue,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue: fieldTextValue,
|
fieldValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
|
persistJsonField,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput';
|
||||||
import { FieldPhoneValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldPhoneValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
@ -15,7 +16,17 @@ import { isFieldPhone } from '../../types/guards/isFieldPhone';
|
|||||||
export const usePhoneField = () => {
|
export const usePhoneField = () => {
|
||||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||||
|
|
||||||
assertFieldMetadata(FieldMetadataType.Phone, isFieldPhone, fieldDefinition);
|
try {
|
||||||
|
// TODO: temporary - remove when 'Phone' field in 'Person' object
|
||||||
|
// is migrated to use FieldMetadataType.Phone as type.
|
||||||
|
assertFieldMetadata(
|
||||||
|
FieldMetadataType.Text,
|
||||||
|
isFieldDisplayedAsPhone,
|
||||||
|
fieldDefinition,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
assertFieldMetadata(FieldMetadataType.Phone, isFieldPhone, fieldDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { isValidJSON } from '@/object-record/record-field/utils/isFieldValueJson';
|
|
||||||
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
|
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
|
||||||
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
|
||||||
import { useJsonField } from '../../hooks/useJsonField';
|
import { useJsonField } from '../../hooks/useJsonField';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateFieldInput';
|
import { FieldInputEvent } from './DateFieldInput';
|
||||||
@ -22,53 +20,47 @@ export const RawJsonFieldInput = ({
|
|||||||
onTab,
|
onTab,
|
||||||
onShiftTab,
|
onShiftTab,
|
||||||
}: RawJsonFieldInputProps) => {
|
}: RawJsonFieldInputProps) => {
|
||||||
const { fieldDefinition, draftValue, hotkeyScope, setDraftValue } =
|
const {
|
||||||
useJsonField();
|
fieldDefinition,
|
||||||
|
draftValue,
|
||||||
const persistField = usePersistField();
|
hotkeyScope,
|
||||||
|
setDraftValue,
|
||||||
const handlePersistField = (newText: string) => {
|
persistJsonField,
|
||||||
if (!newText || isValidJSON(newText)) persistField(newText || null);
|
} = useJsonField();
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnter = (newText: string) => {
|
const handleEnter = (newText: string) => {
|
||||||
onEnter?.(() => handlePersistField(newText));
|
onEnter?.(() => persistJsonField(newText));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (newText: string) => {
|
const handleEscape = (newText: string) => {
|
||||||
onEscape?.(() => handlePersistField(newText));
|
onEscape?.(() => persistJsonField(newText));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = (
|
const handleClickOutside = (
|
||||||
_event: MouseEvent | TouchEvent,
|
_event: MouseEvent | TouchEvent,
|
||||||
newText: string,
|
newText: string,
|
||||||
) => {
|
) => {
|
||||||
onClickOutside?.(() => handlePersistField(newText));
|
onClickOutside?.(() => persistJsonField(newText));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTab = (newText: string) => {
|
const handleTab = (newText: string) => {
|
||||||
onTab?.(() => handlePersistField(newText));
|
onTab?.(() => persistJsonField(newText));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShiftTab = (newText: string) => {
|
const handleShiftTab = (newText: string) => {
|
||||||
onShiftTab?.(() => handlePersistField(newText));
|
onShiftTab?.(() => persistJsonField(newText));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (newText: string) => {
|
const handleChange = (newText: string) => {
|
||||||
setDraftValue(newText);
|
setDraftValue(newText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const value =
|
|
||||||
draftValue && isValidJSON(draftValue)
|
|
||||||
? JSON.stringify(JSON.parse(draftValue), null, 2)
|
|
||||||
: draftValue ?? '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldTextAreaOverlay>
|
<FieldTextAreaOverlay>
|
||||||
<TextAreaInput
|
<TextAreaInput
|
||||||
placeholder={fieldDefinition.metadata.placeHolder}
|
placeholder={fieldDefinition.metadata.placeHolder}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={draftValue ?? ''}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
onEnter={handleEnter}
|
onEnter={handleEnter}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
FieldDateTimeValue,
|
FieldDateTimeValue,
|
||||||
FieldEmailValue,
|
FieldEmailValue,
|
||||||
FieldFullNameValue,
|
FieldFullNameValue,
|
||||||
|
FieldJsonValue,
|
||||||
FieldLinksValue,
|
FieldLinksValue,
|
||||||
FieldLinkValue,
|
FieldLinkValue,
|
||||||
FieldMultiSelectValue,
|
FieldMultiSelectValue,
|
||||||
@ -47,6 +48,7 @@ export type FieldAddressDraftValue = {
|
|||||||
addressLat: number | null;
|
addressLat: number | null;
|
||||||
addressLng: number | null;
|
addressLng: number | null;
|
||||||
};
|
};
|
||||||
|
export type FieldJsonDraftValue = string;
|
||||||
|
|
||||||
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
||||||
? FieldTextDraftValue
|
? FieldTextDraftValue
|
||||||
@ -80,4 +82,6 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldRelationDraftValue
|
? FieldRelationDraftValue
|
||||||
: FieldValue extends FieldAddressValue
|
: FieldValue extends FieldAddressValue
|
||||||
? FieldAddressDraftValue
|
? FieldAddressDraftValue
|
||||||
: never;
|
: FieldValue extends FieldJsonValue
|
||||||
|
? FieldJsonDraftValue
|
||||||
|
: never;
|
||||||
|
|||||||
@ -173,4 +173,8 @@ export type FieldSelectValue = string | null;
|
|||||||
export type FieldMultiSelectValue = string[] | null;
|
export type FieldMultiSelectValue = string[] | null;
|
||||||
|
|
||||||
export type FieldRelationValue = EntityForSelect | null;
|
export type FieldRelationValue = EntityForSelect | null;
|
||||||
export type FieldJsonValue = string;
|
|
||||||
|
// See https://zod.dev/?id=json-type
|
||||||
|
type Literal = string | number | boolean | null;
|
||||||
|
export type Json = Literal | { [key: string]: Json } | Json[];
|
||||||
|
export type FieldJsonValue = Record<string, Json> | Json[] | null;
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldMetadata, FieldTextMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
// TODO: temporary - remove when 'Phone' field in 'Person' object
|
||||||
|
// is migrated to use FieldMetadataType.Phone as type.
|
||||||
|
export const isFieldDisplayedAsPhone = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||||
|
): field is FieldDefinition<FieldTextMetadata> =>
|
||||||
|
field.metadata.objectMetadataNameSingular === CoreObjectNameSingular.Person &&
|
||||||
|
field.type === FieldMetadataType.Text &&
|
||||||
|
field.metadata.fieldName === 'phone';
|
||||||
@ -1,8 +1,20 @@
|
|||||||
import { isNull, isString } from '@sniptt/guards';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FieldJsonValue } from '../FieldMetadata';
|
import { FieldJsonValue, Json } from '../FieldMetadata';
|
||||||
|
|
||||||
|
// See https://zod.dev/?id=json-type
|
||||||
|
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||||
|
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||||
|
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const jsonWithoutLiteralsSchema: z.ZodType<FieldJsonValue> = z.union([
|
||||||
|
z.null(), // Exclude literal values other than null
|
||||||
|
z.array(jsonSchema),
|
||||||
|
z.record(jsonSchema),
|
||||||
|
]);
|
||||||
|
|
||||||
// TODO: add zod
|
|
||||||
export const isFieldRawJsonValue = (
|
export const isFieldRawJsonValue = (
|
||||||
fieldValue: unknown,
|
fieldValue: unknown,
|
||||||
): fieldValue is FieldJsonValue => isString(fieldValue) || isNull(fieldValue);
|
): fieldValue is FieldJsonValue =>
|
||||||
|
jsonWithoutLiteralsSchema.safeParse(fieldValue).success;
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldIn
|
|||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
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';
|
||||||
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { computeEmptyDraftValue } from '@/object-record/record-field/utils/computeEmptyDraftValue';
|
import { computeEmptyDraftValue } from '@/object-record/record-field/utils/computeEmptyDraftValue';
|
||||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||||
@ -33,9 +35,20 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
|
|||||||
currencyCode: fieldValue?.currencyCode ?? '',
|
currencyCode: fieldValue?.currencyCode ?? '',
|
||||||
} as unknown as FieldInputDraftValue<FieldValue>;
|
} as unknown as FieldInputDraftValue<FieldValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFieldRelation(fieldDefinition)) {
|
if (isFieldRelation(fieldDefinition)) {
|
||||||
return computeEmptyDraftValue<FieldValue>({ fieldDefinition });
|
return computeEmptyDraftValue<FieldValue>({ fieldDefinition });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldRawJson(fieldDefinition)) {
|
||||||
|
return isFieldRawJsonValue(fieldValue)
|
||||||
|
? (JSON.stringify(
|
||||||
|
fieldValue,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) as FieldInputDraftValue<FieldValue>)
|
||||||
|
: computeEmptyDraftValue<FieldValue>({ fieldDefinition });
|
||||||
|
}
|
||||||
|
|
||||||
return fieldValue as FieldInputDraftValue<FieldValue>;
|
return fieldValue as FieldInputDraftValue<FieldValue>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,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 { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
|
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||||
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
||||||
|
|
||||||
@ -26,7 +27,8 @@ export const computeEmptyDraftValue = <FieldValue>({
|
|||||||
isFieldDateTime(fieldDefinition) ||
|
isFieldDateTime(fieldDefinition) ||
|
||||||
isFieldNumber(fieldDefinition) ||
|
isFieldNumber(fieldDefinition) ||
|
||||||
isFieldEmail(fieldDefinition) ||
|
isFieldEmail(fieldDefinition) ||
|
||||||
isFieldRelationValue(fieldDefinition)
|
isFieldRelation(fieldDefinition) ||
|
||||||
|
isFieldRawJson(fieldDefinition)
|
||||||
) {
|
) {
|
||||||
return '' as FieldInputDraftValue<FieldValue>;
|
return '' as FieldInputDraftValue<FieldValue>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { isString } from '@sniptt/guards';
|
|
||||||
|
|
||||||
export const isValidJSON = (str: string) => {
|
|
||||||
try {
|
|
||||||
if (isString(JSON.parse(str))) {
|
|
||||||
throw new Error(`Strings are not supported as JSON: ${str}`);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -6,6 +6,7 @@ import { z } from 'zod';
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
// TODO: rename to SettingsDataModelFieldBooleanForm and move to settings/data-model/fields/forms/components
|
// TODO: rename to SettingsDataModelFieldBooleanForm and move to settings/data-model/fields/forms/components
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ export const SettingsDataModelFieldBooleanForm = ({
|
|||||||
}: SettingsDataModelFieldBooleanFormProps) => {
|
}: SettingsDataModelFieldBooleanFormProps) => {
|
||||||
const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
|
const { control } = useFormContext<SettingsDataModelFieldBooleanFormValues>();
|
||||||
|
|
||||||
|
const isEditMode = isDefined(fieldMetadataItem?.defaultValue);
|
||||||
const initialValue = fieldMetadataItem?.defaultValue ?? true;
|
const initialValue = fieldMetadataItem?.defaultValue ?? true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -54,6 +56,9 @@ export const SettingsDataModelFieldBooleanForm = ({
|
|||||||
<Select
|
<Select
|
||||||
className={className}
|
className={className}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
// TODO: temporary fix - disabling edition because after editing the defaultValue,
|
||||||
|
// newly created records are not taking into account the updated defaultValue properly.
|
||||||
|
disabled={isEditMode}
|
||||||
dropdownId="object-field-default-value-select"
|
dropdownId="object-field-default-value-select"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||||
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
|
||||||
|
import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/hooks/useRelationSettingsFormInitialValues';
|
||||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import { RELATION_TYPES } from '../constants/RelationTypes';
|
import { RELATION_TYPES } from '../constants/RelationTypes';
|
||||||
import { RelationType } from '../types/RelationType';
|
import { RelationType } from '../types/RelationType';
|
||||||
@ -32,7 +30,7 @@ export const settingsDataModelFieldRelationFormSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type SettingsDataModelFieldRelationFormValues = z.infer<
|
export type SettingsDataModelFieldRelationFormValues = z.infer<
|
||||||
typeof settingsDataModelFieldRelationFormSchema
|
typeof settingsDataModelFieldRelationFormSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -79,30 +77,23 @@ const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES)
|
|||||||
export const SettingsDataModelFieldRelationForm = ({
|
export const SettingsDataModelFieldRelationForm = ({
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
}: SettingsDataModelFieldRelationFormProps) => {
|
}: SettingsDataModelFieldRelationFormProps) => {
|
||||||
const { control } =
|
const { control, watch: watchFormValue } =
|
||||||
useFormContext<SettingsDataModelFieldRelationFormValues>();
|
useFormContext<SettingsDataModelFieldRelationFormValues>();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const { objectMetadataItems } = useFilteredObjectMetadataItems();
|
const { objectMetadataItems, findObjectMetadataItemById } =
|
||||||
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
const getRelationMetadata = useGetRelationMetadata();
|
|
||||||
const {
|
const {
|
||||||
relationFieldMetadataItem,
|
disableFieldEdition,
|
||||||
relationType,
|
disableRelationEdition,
|
||||||
relationObjectMetadataItem,
|
initialRelationFieldMetadataItem,
|
||||||
} =
|
initialRelationObjectMetadataItem,
|
||||||
useMemo(
|
initialRelationType,
|
||||||
() =>
|
} = useRelationSettingsFormInitialValues({ fieldMetadataItem });
|
||||||
fieldMetadataItem ? getRelationMetadata({ fieldMetadataItem }) : null,
|
|
||||||
[fieldMetadataItem, getRelationMetadata],
|
|
||||||
) ?? {};
|
|
||||||
|
|
||||||
const disableFieldEdition =
|
const selectedObjectMetadataItem = findObjectMetadataItemById(
|
||||||
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom;
|
watchFormValue('relation.objectMetadataId'),
|
||||||
|
);
|
||||||
const disableRelationEdition = !!relationFieldMetadataItem;
|
|
||||||
|
|
||||||
const selectedObjectMetadataItem =
|
|
||||||
relationObjectMetadataItem ?? objectMetadataItems[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
@ -110,7 +101,7 @@ export const SettingsDataModelFieldRelationForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="relation.type"
|
name="relation.type"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={relationType ?? RelationMetadataType.OneToMany}
|
defaultValue={initialRelationType}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Select
|
<Select
|
||||||
label="Relation type"
|
label="Relation type"
|
||||||
@ -126,7 +117,7 @@ export const SettingsDataModelFieldRelationForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="relation.objectMetadataId"
|
name="relation.objectMetadataId"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={selectedObjectMetadataItem?.id}
|
defaultValue={initialRelationObjectMetadataItem.id}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<Select
|
<Select
|
||||||
label="Object destination"
|
label="Object destination"
|
||||||
@ -153,11 +144,7 @@ export const SettingsDataModelFieldRelationForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="relation.field.icon"
|
name="relation.field.icon"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={
|
defaultValue={initialRelationFieldMetadataItem.icon}
|
||||||
relationFieldMetadataItem?.icon ??
|
|
||||||
relationObjectMetadataItem?.icon ??
|
|
||||||
'IconUsers'
|
|
||||||
}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<IconPicker
|
<IconPicker
|
||||||
disabled={disableFieldEdition}
|
disabled={disableFieldEdition}
|
||||||
@ -171,7 +158,7 @@ export const SettingsDataModelFieldRelationForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="relation.field.label"
|
name="relation.field.label"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={relationFieldMetadataItem?.label}
|
defaultValue={initialRelationFieldMetadataItem.label}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled={disableFieldEdition}
|
disabled={disableFieldEdition}
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||||
|
import {
|
||||||
|
SettingsDataModelFieldRelationForm,
|
||||||
|
SettingsDataModelFieldRelationFormValues,
|
||||||
|
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
||||||
|
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||||
|
import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/hooks/useRelationSettingsFormInitialValues';
|
||||||
|
import {
|
||||||
|
SettingsDataModelFieldPreviewCard,
|
||||||
|
SettingsDataModelFieldPreviewCardProps,
|
||||||
|
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
type SettingsDataModelFieldRelationSettingsFormCardProps = {
|
||||||
|
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||||
|
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||||
|
relationFieldMetadataItem?: FieldMetadataItem;
|
||||||
|
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||||
|
|
||||||
|
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||||
|
display: grid;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledPreviewContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
||||||
|
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
||||||
|
width: 54px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsDataModelFieldRelationSettingsFormCard = ({
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
}: SettingsDataModelFieldRelationSettingsFormCardProps) => {
|
||||||
|
const { watch: watchFormValue } =
|
||||||
|
useFormContext<SettingsDataModelFieldRelationFormValues>();
|
||||||
|
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const {
|
||||||
|
initialRelationObjectMetadataItem,
|
||||||
|
initialRelationType,
|
||||||
|
initialRelationFieldMetadataItem,
|
||||||
|
} = useRelationSettingsFormInitialValues({ fieldMetadataItem });
|
||||||
|
|
||||||
|
const relationObjectMetadataId = watchFormValue(
|
||||||
|
'relation.objectMetadataId',
|
||||||
|
initialRelationObjectMetadataItem?.id,
|
||||||
|
);
|
||||||
|
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||||
|
relationObjectMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relationObjectMetadataItem) return null;
|
||||||
|
|
||||||
|
const relationType = watchFormValue('relation.type', initialRelationType);
|
||||||
|
const relationTypeConfig = RELATION_TYPES[relationType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsDataModelPreviewFormCard
|
||||||
|
preview={
|
||||||
|
<StyledPreviewContent>
|
||||||
|
<StyledFieldPreviewCard
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
shrink
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||||
|
/>
|
||||||
|
<StyledRelationImage
|
||||||
|
src={relationTypeConfig.imageSrc}
|
||||||
|
flip={relationTypeConfig.isImageFlipped}
|
||||||
|
alt={relationTypeConfig.label}
|
||||||
|
/>
|
||||||
|
<StyledFieldPreviewCard
|
||||||
|
fieldMetadataItem={{
|
||||||
|
...initialRelationFieldMetadataItem,
|
||||||
|
icon: watchFormValue(
|
||||||
|
'relation.field.icon',
|
||||||
|
initialRelationFieldMetadataItem.icon,
|
||||||
|
),
|
||||||
|
label:
|
||||||
|
watchFormValue(
|
||||||
|
'relation.field.label',
|
||||||
|
initialRelationFieldMetadataItem.label,
|
||||||
|
) || 'Field name',
|
||||||
|
type: FieldMetadataType.Relation,
|
||||||
|
}}
|
||||||
|
shrink
|
||||||
|
objectMetadataItem={relationObjectMetadataItem}
|
||||||
|
relationObjectMetadataItem={objectMetadataItem}
|
||||||
|
/>
|
||||||
|
</StyledPreviewContent>
|
||||||
|
}
|
||||||
|
form={
|
||||||
|
<SettingsDataModelFieldRelationForm
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,7 +3,6 @@ import styled from '@emotion/styled';
|
|||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import {
|
import {
|
||||||
SettingsDataModelFieldBooleanForm,
|
SettingsDataModelFieldBooleanForm,
|
||||||
@ -14,16 +13,13 @@ import {
|
|||||||
SettingsDataModelFieldCurrencyForm,
|
SettingsDataModelFieldCurrencyForm,
|
||||||
settingsDataModelFieldCurrencyFormSchema,
|
settingsDataModelFieldCurrencyFormSchema,
|
||||||
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||||
import {
|
import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
||||||
SettingsDataModelFieldRelationForm,
|
|
||||||
settingsDataModelFieldRelationFormSchema,
|
|
||||||
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
|
||||||
import {
|
import {
|
||||||
SettingsDataModelFieldSelectForm,
|
SettingsDataModelFieldSelectForm,
|
||||||
settingsDataModelFieldSelectFormSchema,
|
settingsDataModelFieldSelectFormSchema,
|
||||||
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
|
||||||
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||||
|
import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldRelationSettingsFormCard';
|
||||||
import {
|
import {
|
||||||
SettingsDataModelFieldPreviewCard,
|
SettingsDataModelFieldPreviewCard,
|
||||||
SettingsDataModelFieldPreviewCardProps,
|
SettingsDataModelFieldPreviewCardProps,
|
||||||
@ -81,7 +77,6 @@ type SettingsDataModelFieldSettingsFormCardProps = {
|
|||||||
disableCurrencyForm?: boolean;
|
disableCurrencyForm?: boolean;
|
||||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||||
relationFieldMetadataItem?: FieldMetadataItem;
|
|
||||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||||
|
|
||||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||||
@ -94,11 +89,6 @@ const StyledPreviewContent = styled.div`
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
|
||||||
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
|
||||||
width: 54px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const previewableTypes = [
|
const previewableTypes = [
|
||||||
FieldMetadataType.Boolean,
|
FieldMetadataType.Boolean,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
@ -121,23 +111,20 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
|||||||
disableCurrencyForm,
|
disableCurrencyForm,
|
||||||
fieldMetadataItem,
|
fieldMetadataItem,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
relationFieldMetadataItem,
|
|
||||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||||
const { watch: watchFormValue } =
|
const { watch: watchFormValue } =
|
||||||
useFormContext<SettingsDataModelFieldSettingsFormValues>();
|
useFormContext<SettingsDataModelFieldSettingsFormValues>();
|
||||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
|
||||||
|
|
||||||
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
||||||
|
|
||||||
const relationObjectMetadataId = watchFormValue('relation.objectMetadataId');
|
if (fieldMetadataItem.type === FieldMetadataType.Relation) {
|
||||||
const relationObjectMetadataItem = relationObjectMetadataId
|
return (
|
||||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
<SettingsDataModelFieldRelationSettingsFormCard
|
||||||
: undefined;
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
const relationType = watchFormValue('relation.type');
|
/>
|
||||||
const relationTypeConfig = relationType
|
);
|
||||||
? RELATION_TYPES[relationType]
|
}
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsDataModelPreviewFormCard
|
<SettingsDataModelPreviewFormCard
|
||||||
@ -145,34 +132,9 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
|||||||
<StyledPreviewContent>
|
<StyledPreviewContent>
|
||||||
<StyledFieldPreviewCard
|
<StyledFieldPreviewCard
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
|
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
|
||||||
selectOptions={watchFormValue('options')}
|
selectOptions={watchFormValue('options')}
|
||||||
/>
|
/>
|
||||||
{fieldMetadataItem.type === FieldMetadataType.Relation &&
|
|
||||||
!!relationObjectMetadataItem &&
|
|
||||||
!!relationTypeConfig && (
|
|
||||||
<>
|
|
||||||
<StyledRelationImage
|
|
||||||
src={relationTypeConfig.imageSrc}
|
|
||||||
flip={relationTypeConfig.isImageFlipped}
|
|
||||||
alt={relationTypeConfig.label}
|
|
||||||
/>
|
|
||||||
<StyledFieldPreviewCard
|
|
||||||
fieldMetadataItem={{
|
|
||||||
...relationFieldMetadataItem,
|
|
||||||
icon: watchFormValue('relation.field.icon'),
|
|
||||||
label:
|
|
||||||
watchFormValue('relation.field.label') || 'Field name',
|
|
||||||
type: FieldMetadataType.Relation,
|
|
||||||
}}
|
|
||||||
shrink
|
|
||||||
objectMetadataItem={relationObjectMetadataItem}
|
|
||||||
relationObjectMetadataItem={objectMetadataItem}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</StyledPreviewContent>
|
</StyledPreviewContent>
|
||||||
}
|
}
|
||||||
form={
|
form={
|
||||||
@ -185,10 +147,6 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
|||||||
disabled={disableCurrencyForm}
|
disabled={disableCurrencyForm}
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
/>
|
/>
|
||||||
) : fieldMetadataItem.type === FieldMetadataType.Relation ? (
|
|
||||||
<SettingsDataModelFieldRelationForm
|
|
||||||
fieldMetadataItem={fieldMetadataItem}
|
|
||||||
/>
|
|
||||||
) : fieldMetadataItem.type === FieldMetadataType.Select ||
|
) : fieldMetadataItem.type === FieldMetadataType.Select ||
|
||||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
||||||
<SettingsDataModelFieldSelectForm
|
<SettingsDataModelFieldSelectForm
|
||||||
|
|||||||
@ -7,10 +7,7 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato
|
|||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import {
|
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';
|
||||||
mockedCompanyObjectMetadataItem,
|
|
||||||
mockedPersonObjectMetadataItem,
|
|
||||||
} from '~/testing/mock-data/metadata';
|
|
||||||
|
|
||||||
import { SettingsDataModelFieldSettingsFormCard } from '../SettingsDataModelFieldSettingsFormCard';
|
import { SettingsDataModelFieldSettingsFormCard } from '../SettingsDataModelFieldSettingsFormCard';
|
||||||
|
|
||||||
@ -49,9 +46,6 @@ export const WithRelationForm: Story = {
|
|||||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||||
({ name }) => name === 'people',
|
({ name }) => name === 'people',
|
||||||
),
|
),
|
||||||
relationFieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
|
||||||
({ name }) => name === 'company',
|
|
||||||
)!,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||||
|
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useRelationSettingsFormInitialValues = ({
|
||||||
|
fieldMetadataItem,
|
||||||
|
}: {
|
||||||
|
fieldMetadataItem?: Pick<
|
||||||
|
FieldMetadataItem,
|
||||||
|
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
|
||||||
|
>;
|
||||||
|
}) => {
|
||||||
|
const { objectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
const {
|
||||||
|
relationFieldMetadataItem,
|
||||||
|
relationObjectMetadataItem: relationObjectMetadataItemFromFieldMetadata,
|
||||||
|
relationType: relationTypeFromFieldMetadata,
|
||||||
|
} = useMemo(
|
||||||
|
() =>
|
||||||
|
fieldMetadataItem ? getRelationMetadata({ fieldMetadataItem }) : null,
|
||||||
|
[fieldMetadataItem, getRelationMetadata],
|
||||||
|
) ?? {};
|
||||||
|
|
||||||
|
const initialRelationObjectMetadataItem = useMemo(
|
||||||
|
() =>
|
||||||
|
relationObjectMetadataItemFromFieldMetadata ??
|
||||||
|
objectMetadataItems.find(
|
||||||
|
({ nameSingular }) => nameSingular === CoreObjectNameSingular.Person,
|
||||||
|
) ??
|
||||||
|
objectMetadataItems.filter(isObjectMetadataAvailableForRelation)[0],
|
||||||
|
[objectMetadataItems, relationObjectMetadataItemFromFieldMetadata],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialRelationType =
|
||||||
|
relationTypeFromFieldMetadata ?? RelationMetadataType.OneToMany;
|
||||||
|
|
||||||
|
return {
|
||||||
|
disableFieldEdition:
|
||||||
|
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom,
|
||||||
|
disableRelationEdition: !!relationFieldMetadataItem,
|
||||||
|
initialRelationFieldMetadataItem: relationFieldMetadataItem ?? {
|
||||||
|
icon: initialRelationObjectMetadataItem.icon ?? 'IconUsers',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
initialRelationObjectMetadataItem,
|
||||||
|
initialRelationType,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import { IconArchive, IconSettings } from 'twenty-ui';
|
import { IconArchive, IconSettings } from 'twenty-ui';
|
||||||
@ -33,6 +32,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
|
|||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
type SettingsDataModelFieldEditFormValues = z.infer<
|
type SettingsDataModelFieldEditFormValues = z.infer<
|
||||||
typeof settingsFieldFormSchema
|
typeof settingsFieldFormSchema
|
||||||
@ -72,15 +72,6 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getRelationMetadata = useGetRelationMetadata();
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
const { relationFieldMetadataItem } =
|
|
||||||
useMemo(
|
|
||||||
() =>
|
|
||||||
activeMetadataField
|
|
||||||
? getRelationMetadata({ fieldMetadataItem: activeMetadataField })
|
|
||||||
: null,
|
|
||||||
[activeMetadataField, getRelationMetadata],
|
|
||||||
) ?? {};
|
|
||||||
|
|
||||||
const { updateOneFieldMetadataItem } = useUpdateOneFieldMetadataItem();
|
const { updateOneFieldMetadataItem } = useUpdateOneFieldMetadataItem();
|
||||||
|
|
||||||
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
|
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
|
||||||
@ -111,13 +102,19 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
if (
|
if (
|
||||||
formValues.type === FieldMetadataType.Relation &&
|
formValues.type === FieldMetadataType.Relation &&
|
||||||
'relation' in formValues &&
|
'relation' in formValues &&
|
||||||
'relation' in dirtyFields &&
|
'relation' in dirtyFields
|
||||||
isNonEmptyString(relationFieldMetadataItem?.id)
|
|
||||||
) {
|
) {
|
||||||
await updateOneFieldMetadataItem({
|
const { relationFieldMetadataItem } =
|
||||||
fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
|
getRelationMetadata({
|
||||||
updatePayload: formValues.relation.field,
|
fieldMetadataItem: activeMetadataField,
|
||||||
});
|
}) ?? {};
|
||||||
|
|
||||||
|
if (isDefined(relationFieldMetadataItem)) {
|
||||||
|
await updateOneFieldMetadataItem({
|
||||||
|
fieldMetadataIdToUpdate: relationFieldMetadataItem.id,
|
||||||
|
updatePayload: formValues.relation.field,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherDirtyFields = omit(dirtyFields, 'relation');
|
const otherDirtyFields = omit(dirtyFields, 'relation');
|
||||||
@ -202,7 +199,6 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
disableCurrencyForm
|
disableCurrencyForm
|
||||||
fieldMetadataItem={activeMetadataField}
|
fieldMetadataItem={activeMetadataField}
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
relationFieldMetadataItem={relationFieldMetadataItem}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
{!isLabelIdentifier && (
|
{!isLabelIdentifier && (
|
||||||
|
|||||||
@ -271,6 +271,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
FieldMetadataType.Numeric,
|
FieldMetadataType.Numeric,
|
||||||
FieldMetadataType.Probability,
|
FieldMetadataType.Probability,
|
||||||
FieldMetadataType.Uuid,
|
FieldMetadataType.Uuid,
|
||||||
|
FieldMetadataType.Phone,
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user