feat: adds initial json field

This commit is contained in:
Jessica Boezwinkle
2022-12-15 18:32:11 +00:00
parent a0b41eb83b
commit 28d9f9009c
22 changed files with 267 additions and 2 deletions

View File

@@ -0,0 +1 @@
export type { Props } from '../../dist/admin/components/forms/field-types/JSON/types';

View File

@@ -96,6 +96,7 @@
"@faceless-ui/modal": "^2.0.1",
"@faceless-ui/scroll-info": "^1.2.3",
"@faceless-ui/window-info": "^2.0.2",
"@monaco-editor/react": "^4.4.6",
"@types/is-plain-object": "^2.0.4",
"@types/sharp": "^0.26.1",
"babel-jest": "^26.3.0",

View File

@@ -84,6 +84,10 @@ const fieldTypeConditions = {
component: 'Text',
operators: [...base, like, contains],
},
json: {
component: 'Text',
operators: [...base, like, contains],
},
richText: {
component: 'Text',
operators: [...base, like, contains],

View File

@@ -0,0 +1,90 @@
import React, { useCallback } from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import Editor from '@monaco-editor/react';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
import { json } from '../../../../../fields/validations';
import { Props } from './types';
import './index.scss';
const baseClass = 'json-field';
const JSONField: React.FC<Props> = (props) => {
const {
path: pathFromProps,
name,
required,
validate = json,
admin: {
readOnly,
style,
className,
width,
description,
condition,
} = {},
label,
} = props;
const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => {
return validate(value, { ...options, required });
}, [validate, required]);
const {
value,
showError,
setValue,
errorMessage,
} = useField<string>({
path,
validate: memoizedValidate,
condition,
});
const classes = [
baseClass,
'field-type',
className,
showError && 'error',
readOnly && 'read-only',
].filter(Boolean).join(' ');
return (
<div
className={classes}
style={{
...style,
width,
}}
>
<Error
showError={showError}
message={errorMessage}
/>
<Label
htmlFor={`field-${path}`}
label={label}
required={required}
/>
<Editor
height="50vh"
defaultLanguage="json"
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
onChange={readOnly ? () => null : (val) => setValue(val)}
/>
<FieldDescription
value={value}
description={description}
/>
</div>
);
};
export default withCondition(JSONField);

View File

@@ -0,0 +1,12 @@
@import '../../../../scss/styles.scss';
.json-field {
position: relative;
margin-bottom: $baseline;
&.error {
textarea {
border: 1px solid var(--theme-error-500) !important;
}
}
}

View File

@@ -0,0 +1,13 @@
import React, { Suspense, lazy } from 'react';
import Loading from '../../../elements/Loading';
import { Props } from './types';
const JSON = lazy(() => import('./JSON'));
const JSONField: React.FC<Props> = (props) => (
<Suspense fallback={<Loading />}>
<JSON {...props} />
</Suspense>
);
export default JSONField;

View File

@@ -0,0 +1,5 @@
import { JSONField } from '../../../../../fields/config/types';
export type Props = Omit<JSONField, 'type'> & {
path?: string
}

View File

@@ -1,4 +1,5 @@
import code from './Code';
import json from './JSON';
import email from './Email';
import hidden from './HiddenInput';
import text from './Text';
@@ -26,6 +27,7 @@ import ui from './UI';
export type FieldTypes = {
code: React.ComponentType<any>
json: React.ComponentType<any>
email: React.ComponentType<any>
hidden: React.ComponentType<any>
text: React.ComponentType<any>
@@ -52,6 +54,7 @@ export type FieldTypes = {
const fieldTypes: FieldTypes = {
code,
json,
email,
hidden,
text,

View File

@@ -11,6 +11,7 @@ export default {
number: Text,
email: Text,
code: Text,
json: Text,
checkbox: Text,
radio: Select,
row: Nested,

View File

@@ -0,0 +1,17 @@
@import '../../../../../../../scss/styles.scss';
.json-cell {
font-size: 1rem;
line-height: base(1);
border: 0;
display: inline-flex;
vertical-align: middle;
background: var(--theme-elevation-150);
color: var(--theme-elevation-800);
border-radius: $style-radius-s;
padding: 0 base(0.25);
padding-left: base(0.0875 + 0.25);
background: var(--theme-elevation-100);
color: var(--theme-elevation-800);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import './index.scss';
const JSONCell = ({ data }) => {
const textToShow = data.length > 100 ? `${data.substring(0, 100)}\u2026` : data;
return (
<code className="json-cell">
<span>
{JSON.stringify(textToShow)}
</span>
</code>
);
};
export default JSONCell;

View File

@@ -3,6 +3,7 @@ import blocks from './Blocks';
import checkbox from './Checkbox';
import code from './Code';
import date from './Date';
import json from './JSON';
import relationship from './Relationship';
import richText from './Richtext';
import select from './Select';
@@ -15,6 +16,7 @@ export default {
code,
checkbox,
date,
json,
relationship,
richText,
select,

View File

@@ -74,6 +74,11 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
break;
}
case 'json': {
fieldSchema = { type: 'object' };
break;
}
case 'richText': {
fieldSchema = {
type: 'array',

View File

@@ -136,6 +136,15 @@ export const code = baseField.keys({
}),
});
export const json = baseField.keys({
type: joi.string().valid('json').required(),
name: joi.string().required(),
defaultValue: joi.alternatives().try(
joi.array(),
joi.object(),
),
});
export const select = baseField.keys({
type: joi.string().valid('select').required(),
name: joi.string().required(),
@@ -448,6 +457,7 @@ const fieldSchema = joi.alternatives()
textarea,
email,
code,
json,
select,
group,
array,

View File

@@ -259,6 +259,11 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
type: 'code';
}
export type JSONField = Omit<FieldBase, 'admin'> & {
admin?: Admin
type: 'json';
}
export type SelectField = FieldBase & {
type: 'select'
options: Option[]
@@ -399,6 +404,7 @@ export type Field =
| SelectField
| UploadField
| CodeField
| JSONField
| PointField
| RowField
| CollapsibleField
@@ -421,6 +427,7 @@ export type FieldAffectingData =
| SelectField
| UploadField
| CodeField
| JSONField
| PointField
| TabAsField
@@ -440,6 +447,7 @@ export type NonPresentationalField =
| SelectField
| UploadField
| CodeField
| JSONField
| PointField
| RowField
| TabsField
@@ -467,7 +475,7 @@ export type FieldWithMaxDepth =
| RelationshipField
export function fieldHasSubFields(field: Field): field is FieldWithSubFields {
return (field.type === 'group' || field.type === 'array' || field.type === 'row' || field.type === 'collapsible');
return (field.type === 'group' || field.type === 'array' || field.type === 'row' || field.type === 'collapsible' || field.type === 'relationship');
}
export function fieldIsArrayType(field: Field): field is ArrayField {

View File

@@ -2,6 +2,7 @@ export default [
'text',
'textarea',
'code',
'json',
'number',
'email',
'radio',

View File

@@ -6,6 +6,7 @@ import {
CodeField,
DateField,
EmailField,
JSONField,
NumberField,
PointField,
RadioField,
@@ -132,6 +133,23 @@ export const code: Validate<unknown, unknown, CodeField> = (value: string, { t,
return true;
};
export const json: Validate<unknown, unknown, JSONField> = (value: string, {
t, required,
}) => {
if (required && !value) {
return t('validation:required');
}
try {
const incomingJSON = typeof value === 'object' ? JSON.stringify(value) : value;
const validJSON = JSON.parse(incomingJSON);
} catch (err) {
return `Invalid JSON: ${err}`;
}
return true;
};
export const richText: Validate<unknown, unknown, RichTextField> = (value, { t, required }) => {
if (required) {
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue);
@@ -398,4 +416,5 @@ export default {
radio,
blocks,
point,
json,
};

View File

@@ -16,7 +16,7 @@ import { GraphQLJSON } from 'graphql-type-json';
import withNullableType from './withNullableType';
import formatName from '../utilities/formatName';
import combineParentName from '../utilities/combineParentName';
import { ArrayField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField, tabHasName } from '../../fields/config/types';
import { ArrayField, CodeField, JSONField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField, tabHasName } from '../../fields/config/types';
import { toWords } from '../../utilities/formatLabels';
import { Payload } from '../../index';
import { SanitizedCollectionConfig } from '../../collections/config/types';
@@ -66,6 +66,10 @@ function buildMutationInputType(payload: Payload, name: string, fields: Field[],
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
}),
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },

View File

@@ -30,6 +30,7 @@ import {
EmailField,
TextareaField,
CodeField,
JSONField,
DateField,
PointField,
CheckboxField,
@@ -104,6 +105,10 @@ function buildObjectType({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
}),
date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, DateTimeResolver, forceNullable) },

View File

@@ -18,6 +18,7 @@ import {
fieldAffectsData, fieldIsLocalized,
fieldIsPresentationalOnly,
GroupField,
JSONField,
NonPresentationalField,
NumberField,
PointField,
@@ -149,6 +150,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
[field.name]: localizeSchema(field, baseSchema, config.localization),
});
},
json: (field: JSONField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed };
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
});
},
point: (field: PointField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
type: {

View File

@@ -0,0 +1,36 @@
import type { CollectionConfig } from '../../../../src/collections/config/types';
type JSONField = {
id: string;
json?: any;
createdAt: string;
updatedAt: string;
}
const JSON: CollectionConfig = {
slug: 'json-fields',
fields: [
{
name: 'json',
type: 'json',
// maxLength: 100,
},
],
};
export const jsonDoc: Partial<JSONField> = {
json: {
property: 'value',
arr: [
'val1',
'val2',
'val3',
],
nested: {
value: 'nested value',
},
},
};
export default JSON;

View File

@@ -19,6 +19,7 @@ import Uploads, { uploadsDoc } from './collections/Upload';
import IndexedFields from './collections/Indexed';
import NumberFields, { numberDoc } from './collections/Number';
import CodeFields, { codeDoc } from './collections/Code';
import JSONFields, { jsonDoc } from './collections/JSON';
import RelationshipFields from './collections/Relationship';
import RadioFields, { radiosDoc } from './collections/Radio';
@@ -45,6 +46,7 @@ export default buildConfig({
RadioFields,
GroupFields,
IndexedFields,
JSONFields,
NumberFields,
PointFields,
RelationshipFields,
@@ -77,6 +79,7 @@ export default buildConfig({
await payload.create({ collection: 'point-fields', data: pointDoc });
await payload.create({ collection: 'date-fields', data: dateDoc });
await payload.create({ collection: 'code-fields', data: codeDoc });
await payload.create({ collection: 'json-fields', data: jsonDoc });
const createdTextDoc = await payload.create({ collection: textFieldsSlug, data: textDoc });