Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new field type JSON #4729

Merged
merged 8 commits into from
Apr 11, 2024
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -59,5 +62,7 @@ export const FieldDisplay = () => {
<SelectFieldDisplay />
) : isFieldAddress(fieldDefinition) ? (
<AddressFieldDisplay />
) : isFieldRawJson(fieldDefinition) ? (
<JsonFieldDisplay />
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -137,6 +139,14 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldRawJson(fieldDefinition) ? (
<RawJsonFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : (
<></>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { isFieldAddress } from '@/object-record/record-field/types/guards/isFiel
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
import { 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';
Expand Down Expand Up @@ -88,6 +90,10 @@ export const usePersistField = () => {
isFieldAddress(fieldDefinition) &&
isFieldAddressValue(valueToPersist);

const fieldIsRawJson =
isFieldRawJson(fieldDefinition) &&
isFieldRawJsonValue(valueToPersist);

if (
fieldIsRelation ||
fieldIsText ||
Expand All @@ -101,7 +107,8 @@ export const usePersistField = () => {
fieldIsCurrency ||
fieldIsFullName ||
fieldIsSelect ||
fieldIsAddress
fieldIsAddress ||
fieldIsRawJson
) {
const fieldName = fieldDefinition.metadata.fieldName;
set(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<JsonDisplay
text={fieldValue ? JSON.stringify(JSON.parse(fieldValue), null, 2) : ''}
maxWidth={maxWidth}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<FieldJsonValue>(
recordStoreFamilySelector({
recordId: entityId,
fieldName: fieldName,
}),
);
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gitstart-twenty what is this useful for?


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

const draftValue = useRecoilValue(getDraftValueSelector());

return {
draftValue,
setDraftValue,
maxWidth,
fieldDefinition,
fieldValue: fieldTextValue,
setFieldValue,
hotkeyScope,
};
};
Original file line number Diff line number Diff line change
@@ -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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the behavior for other fields, right now I'm unable to escape / enter if invalid json. Instead I should be able to escape and it should come back to the previous value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

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 (
<FieldTextAreaOverlay>
<TextAreaInput
placeholder={fieldDefinition.metadata.placeHolder}
FelixMalfait marked this conversation as resolved.
Show resolved Hide resolved
autoFocus
value={value}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
maxRows={25}
/>
</FieldTextAreaOverlay>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type FieldAddressMetadata = {
export type FieldRawJsonMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
placeHolder: string;
};

export type FieldDefinitionRelationType =
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -39,7 +40,8 @@ export const isFieldValueEmpty = ({
isFieldRating(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldBoolean(fieldDefinition) ||
isFieldRelation(fieldDefinition)
isFieldRelation(fieldDefinition) ||
isFieldRawJson(fieldDefinition)
//|| isFieldPhone(fieldDefinition)
) {
return isValueEmpty(fieldValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
IconCalendarEvent,
IconCheck,
IconCoins,
IconJson,
IconKey,
IconLink,
IconMail,
Expand Down Expand Up @@ -117,4 +118,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
addressLng: -118.2437,
},
},
[FieldMetadataType.RawJson]: {
label: 'JSON',
Icon: IconJson,
defaultValue: `{ "key": "value" }`,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const previewableTypes = [
FieldMetadataType.Relation,
FieldMetadataType.Text,
FieldMetadataType.Address,
FieldMetadataType.RawJson,
];

export const SettingsDataModelFieldSettingsFormCard = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';

export type SettingsSupportedFieldType = Exclude<
FieldMetadataType,
FieldMetadataType.Position | FieldMetadataType.RawJson
FieldMetadataType.Position
>;
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type AppTooltipProps = {
className?: string;
anchorSelect?: string;
content?: string;
children?: React.ReactNode;
delayHide?: number;
offset?: number;
noArrow?: boolean;
Expand All @@ -53,6 +54,7 @@ export const AppTooltip = ({
offset,
place,
positionStrategy,
children,
}: AppTooltipProps) => (
<StyledAppTooltip
{...{
Expand All @@ -65,6 +67,7 @@ export const AppTooltip = ({
offset,
place,
positionStrategy,
children,
}}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`;

Expand Down Expand Up @@ -65,13 +67,15 @@ export const OverflowingTextWithTooltip = ({
<div onClick={handleTooltipClick}>
<AppTooltip
anchorSelect={`#${textElementId}`}
content={text ?? ''}
content={mutliline ? undefined : text ?? ''}
delayHide={0}
offset={5}
noArrow
place="bottom"
positionStrategy="absolute"
/>
>
{mutliline ? <pre>{text}</pre> : ''}
</AppTooltip>
</div>,
document.body,
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { EllipsisDisplay } from './EllipsisDisplay';

type JsonDisplayProps = {
text: string;
maxWidth?: number;
};

export const JsonDisplay = ({ text, maxWidth }: JsonDisplayProps) => (
<EllipsisDisplay maxWidth={maxWidth}>{text}</EllipsisDisplay>
);
Loading
Loading