From 584d90ec897a2772badf2b3d95a2288a4fdf8d2c Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:41:36 +0200 Subject: [PATCH] Create new field type JSON (#4729) ### Description Create new field type JSON ### Refs https://github.com/twentyhq/twenty/issues/3900 ### Demo https://github.com/twentyhq/twenty/assets/140154534/9ebdf4d4-f332-4940-b9d8-d9cf91935b67 Fixes #3900 --------- Co-authored-by: gitstart-twenty Co-authored-by: v1b3m Co-authored-by: Lucas Bordeau Co-authored-by: Marie Stoppa --- .../record-field/components/FieldDisplay.tsx | 5 ++ .../record-field/components/FieldInput.tsx | 10 +++ .../record-field/hooks/usePersistField.ts | 9 +- .../display/components/JsonFieldDisplay.tsx | 13 +++ .../meta-types/hooks/useJsonField.ts | 48 +++++++++++ .../input/components/RawJsonFieldInput.tsx | 83 +++++++++++++++++++ .../record-field/types/FieldMetadata.ts | 2 + .../types/guards/isFieldRawJsonValue.ts | 8 ++ .../record-field/utils/isFieldValueEmpty.ts | 4 +- .../record-field/utils/isFieldValueJson.ts | 12 +++ .../utils/generateEmptyFieldValue.ts | 3 + .../constants/SettingsFieldTypeConfigs.ts | 6 ++ ...SettingsDataModelFieldSettingsFormCard.tsx | 1 + .../types/SettingsSupportedFieldType.ts | 2 +- .../modules/ui/display/tooltip/AppTooltip.tsx | 3 + .../tooltip/OverflowingTextWithTooltip.tsx | 8 +- .../field/display/components/JsonDisplay.tsx | 10 +++ .../field/input/components/TextAreaInput.tsx | 3 + .../graphql-types/scalars/index.ts | 2 + .../graphql-types/scalars/json.scalar.ts | 14 ++++ .../services/type-mapper.service.ts | 4 +- .../open-api/utils/components.utils.ts | 2 +- .../display/icon/components/TablerIcons.ts | 1 + 23 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/JsonFieldDisplay.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJsonValue.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueJson.ts create mode 100644 packages/twenty-front/src/modules/ui/field/display/components/JsonDisplay.tsx create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx index fd24519a27..9096626cf3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx @@ -1,5 +1,8 @@ import { useContext } from 'react'; +import { JsonFieldDisplay } from '@/object-record/record-field/meta-types/display/components/JsonFieldDisplay'; +import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; + import { FieldContext } from '../contexts/FieldContext'; import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay'; @@ -59,5 +62,7 @@ export const FieldDisplay = () => { ) : isFieldAddress(fieldDefinition) ? ( + ) : isFieldRawJson(fieldDefinition) ? ( + ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 63ddafac26..ad2279e824 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -2,9 +2,11 @@ import { useContext } from 'react'; import { AddressFieldInput } from '@/object-record/record-field/meta-types/input/components/AddressFieldInput'; import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput'; +import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; +import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; @@ -137,6 +139,14 @@ export const FieldInput = ({ onTab={onTab} onShiftTab={onShiftTab} /> + ) : isFieldRawJson(fieldDefinition) ? ( + ) : ( <> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index 6757c1bc04..de825b7dfc 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -5,6 +5,8 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue'; +import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; +import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; @@ -88,6 +90,10 @@ export const usePersistField = () => { isFieldAddress(fieldDefinition) && isFieldAddressValue(valueToPersist); + const fieldIsRawJson = + isFieldRawJson(fieldDefinition) && + isFieldRawJsonValue(valueToPersist); + if ( fieldIsRelation || fieldIsText || @@ -101,7 +107,8 @@ export const usePersistField = () => { fieldIsCurrency || fieldIsFullName || fieldIsSelect || - fieldIsAddress + fieldIsAddress || + fieldIsRawJson ) { const fieldName = fieldDefinition.metadata.fieldName; set( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/JsonFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/JsonFieldDisplay.tsx new file mode 100644 index 0000000000..5a0f553cde --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/JsonFieldDisplay.tsx @@ -0,0 +1,13 @@ +import { useJsonField } from '@/object-record/record-field/meta-types/hooks/useJsonField'; +import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay'; + +export const JsonFieldDisplay = () => { + const { fieldValue, maxWidth } = useJsonField(); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts new file mode 100644 index 0000000000..0219eb1739 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonField.ts @@ -0,0 +1,48 @@ +import { useContext } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; +import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata'; +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'; +import { isFieldRawJson } from '../../types/guards/isFieldRawJson'; +import { isFieldTextValue } from '../../types/guards/isFieldTextValue'; + +export const useJsonField = () => { + const { entityId, fieldDefinition, hotkeyScope, maxWidth } = + useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.RawJson, + isFieldRawJson, + fieldDefinition, + ); + + const fieldName = fieldDefinition.metadata.fieldName; + + const [fieldValue, setFieldValue] = useRecoilState( + recordStoreFamilySelector({ + recordId: entityId, + fieldName: fieldName, + }), + ); + const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : ''; + + const { setDraftValue, getDraftValueSelector } = + useRecordFieldInput(`${entityId}-${fieldName}`); + + const draftValue = useRecoilValue(getDraftValueSelector()); + + return { + draftValue, + setDraftValue, + maxWidth, + fieldDefinition, + fieldValue: fieldTextValue, + setFieldValue, + hotkeyScope, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx new file mode 100644 index 0000000000..34a06e8141 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RawJsonFieldInput.tsx @@ -0,0 +1,83 @@ +import { isValidJSON } from '@/object-record/record-field/utils/isFieldValueJson'; +import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay'; +import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useJsonField } from '../../hooks/useJsonField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type RawJsonFieldInputProps = { + onClickOutside?: FieldInputEvent; + onEnter?: FieldInputEvent; + onEscape?: FieldInputEvent; + onTab?: FieldInputEvent; + onShiftTab?: FieldInputEvent; +}; + +export const RawJsonFieldInput = ({ + onEnter, + onEscape, + onClickOutside, + onTab, + onShiftTab, +}: RawJsonFieldInputProps) => { + const { fieldDefinition, draftValue, hotkeyScope, setDraftValue } = + useJsonField(); + + const persistField = usePersistField(); + + const handlePersistField = (newText: string) => { + if (!newText || isValidJSON(newText)) persistField(newText || null); + }; + + const handleEnter = (newText: string) => { + onEnter?.(() => handlePersistField(newText)); + }; + + const handleEscape = (newText: string) => { + onEscape?.(() => handlePersistField(newText)); + }; + + const handleClickOutside = ( + _event: MouseEvent | TouchEvent, + newText: string, + ) => { + onClickOutside?.(() => handlePersistField(newText)); + }; + + const handleTab = (newText: string) => { + onTab?.(() => handlePersistField(newText)); + }; + + const handleShiftTab = (newText: string) => { + onShiftTab?.(() => handlePersistField(newText)); + }; + + const handleChange = (newText: string) => { + setDraftValue(newText); + }; + + const value = + draftValue && isValidJSON(draftValue) + ? JSON.stringify(JSON.parse(draftValue), null, 2) + : draftValue ?? ''; + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index f682bd4229..d65d17bf6c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -78,6 +78,7 @@ export type FieldAddressMetadata = { export type FieldRawJsonMetadata = { objectMetadataNameSingular?: string; fieldName: string; + placeHolder: string; }; export type FieldDefinitionRelationType = @@ -146,3 +147,4 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number]; export type FieldSelectValue = string | null; export type FieldRelationValue = EntityForSelect | null; +export type FieldJsonValue = string; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJsonValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJsonValue.ts new file mode 100644 index 0000000000..8c7657d8dc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJsonValue.ts @@ -0,0 +1,8 @@ +import { isNull, isString } from '@sniptt/guards'; + +import { FieldJsonValue } from '../FieldMetadata'; + +// TODO: add zod +export const isFieldRawJsonValue = ( + fieldValue: unknown, +): fieldValue is FieldJsonValue => isString(fieldValue) || isNull(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index 62ee380f79..c20b06742d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -13,6 +13,7 @@ import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLi import { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating'; +import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue'; @@ -39,7 +40,8 @@ export const isFieldValueEmpty = ({ isFieldRating(fieldDefinition) || isFieldEmail(fieldDefinition) || isFieldBoolean(fieldDefinition) || - isFieldRelation(fieldDefinition) + isFieldRelation(fieldDefinition) || + isFieldRawJson(fieldDefinition) //|| isFieldPhone(fieldDefinition) ) { return isValueEmpty(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueJson.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueJson.ts new file mode 100644 index 0000000000..6e577e7794 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueJson.ts @@ -0,0 +1,12 @@ +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; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts index 2d8b314931..dd5d98f652 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts @@ -75,6 +75,9 @@ export const generateEmptyFieldValue = ( case FieldMetadataType.MultiSelect: { throw new Error('Not implemented yet'); } + case FieldMetadataType.RawJson: { + return null; + } default: { throw new Error('Unhandled FieldMetadataType'); } diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts index d661a355c2..c4e2d798d4 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts @@ -2,6 +2,7 @@ import { IconCalendarEvent, IconCheck, IconCoins, + IconJson, IconKey, IconLink, IconMail, @@ -117,4 +118,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record< addressLng: -118.2437, }, }, + [FieldMetadataType.RawJson]: { + label: 'JSON', + Icon: IconJson, + defaultValue: `{ "key": "value" }`, + }, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index 0cf85a8157..8a344827f4 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -69,6 +69,7 @@ const previewableTypes = [ FieldMetadataType.Relation, FieldMetadataType.Text, FieldMetadataType.Address, + FieldMetadataType.RawJson, ]; export const SettingsDataModelFieldSettingsFormCard = ({ diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts index 5a337be846..0149601685 100644 --- a/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts @@ -2,5 +2,5 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; export type SettingsSupportedFieldType = Exclude< FieldMetadataType, - FieldMetadataType.Position | FieldMetadataType.RawJson + FieldMetadataType.Position >; diff --git a/packages/twenty-front/src/modules/ui/display/tooltip/AppTooltip.tsx b/packages/twenty-front/src/modules/ui/display/tooltip/AppTooltip.tsx index 99fa35032f..ad8abe37af 100644 --- a/packages/twenty-front/src/modules/ui/display/tooltip/AppTooltip.tsx +++ b/packages/twenty-front/src/modules/ui/display/tooltip/AppTooltip.tsx @@ -35,6 +35,7 @@ export type AppTooltipProps = { className?: string; anchorSelect?: string; content?: string; + children?: React.ReactNode; delayHide?: number; offset?: number; noArrow?: boolean; @@ -53,6 +54,7 @@ export const AppTooltip = ({ offset, place, positionStrategy, + children, }: AppTooltipProps) => ( ); diff --git a/packages/twenty-front/src/modules/ui/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-front/src/modules/ui/display/tooltip/OverflowingTextWithTooltip.tsx index 41b266efaf..f8ee4ebcd4 100644 --- a/packages/twenty-front/src/modules/ui/display/tooltip/OverflowingTextWithTooltip.tsx +++ b/packages/twenty-front/src/modules/ui/display/tooltip/OverflowingTextWithTooltip.tsx @@ -22,9 +22,11 @@ const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>` export const OverflowingTextWithTooltip = ({ text, className, + mutliline, }: { text: string | null | undefined; className?: string; + mutliline?: boolean; }) => { const textElementId = `title-id-${uuidV4()}`; @@ -65,13 +67,15 @@ export const OverflowingTextWithTooltip = ({
+ > + {mutliline ?
{text}
: ''} +
, document.body, )} diff --git a/packages/twenty-front/src/modules/ui/field/display/components/JsonDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/JsonDisplay.tsx new file mode 100644 index 0000000000..63614d358c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/field/display/components/JsonDisplay.tsx @@ -0,0 +1,10 @@ +import { EllipsisDisplay } from './EllipsisDisplay'; + +type JsonDisplayProps = { + text: string; + maxWidth?: number; +}; + +export const JsonDisplay = ({ text, maxWidth }: JsonDisplayProps) => ( + {text} +); diff --git a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx index 43f9254bb6..29b7543913 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx @@ -19,6 +19,7 @@ export type TextAreaInputProps = { onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void; hotkeyScope: string; onChange?: (newText: string) => void; + maxRows?: number; }; const StyledTextArea = styled(TextareaAutosize)` @@ -45,6 +46,7 @@ export const TextAreaInput = ({ onShiftTab, onClickOutside, onChange, + maxRows, }: TextAreaInputProps) => { const [internalText, setInternalText] = useState(value); @@ -84,6 +86,7 @@ export const TextAreaInput = ({ onChange={handleChange} autoFocus={autoFocus} value={internalText} + maxRows={maxRows} /> ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts index 32b0466f20..b6c4f05a84 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/index.ts @@ -1,3 +1,4 @@ +import { JsonScalarType } from './json.scalar'; import { PositionScalarType } from './position.scalar'; import { CursorScalarType } from './cursor.scalar'; import { BigFloatScalarType } from './big-float.scalar'; @@ -24,4 +25,5 @@ export const scalars = [ UUIDScalarType, CursorScalarType, PositionScalarType, + JsonScalarType, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts new file mode 100644 index 0000000000..1f03304e71 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar.ts @@ -0,0 +1,14 @@ +import { isString } from 'class-validator'; +import { GraphQLScalarType } from 'graphql'; +import GraphQLJSON from 'graphql-type-json'; + +export const JsonScalarType = new GraphQLScalarType({ + ...GraphQLJSON, + parseValue: (value) => { + if (isString(value) && isString(JSON.parse(value))) { + throw new Error(`Strings are not supported as JSON: ${value}`); + } + + return GraphQLJSON.parseValue(value); + }, +}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index 2739f25949..3d5ca2f1b0 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -14,7 +14,6 @@ import { GraphQLString, GraphQLType, } from 'graphql'; -import GraphQLJSON from 'graphql-type-json'; import { DateScalarMode, @@ -39,6 +38,7 @@ import { UUIDScalarType, } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar'; +import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar'; export interface TypeOptions { nullable?: boolean; @@ -71,7 +71,7 @@ export class TypeMapperService { [FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.RELATION, UUIDScalarType], [FieldMetadataType.POSITION, PositionScalarType], - [FieldMetadataType.RAW_JSON, GraphQLJSON], + [FieldMetadataType.RAW_JSON, JsonScalarType], ]); return typeScalarMapping.get(fieldMetadataType); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 75307797a2..07401cb784 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -72,7 +72,7 @@ const getSchemaComponentsProperties = ( }; break; case FieldMetadataType.RAW_JSON: - type: 'object'; + itemProperty.type = 'object'; break; default: itemProperty.type = 'string'; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 25ec8383fa..f16f99dd20 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -82,6 +82,7 @@ export { IconHierarchy2, IconInbox, IconInfoCircle, + IconJson, IconKey, IconLanguage, IconLayersLinked,