feat: adds initial json field
This commit is contained in:
1
components/fields/Json.ts
Normal file
1
components/fields/Json.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type { Props } from '../../dist/admin/components/forms/field-types/JSON/types';
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
|
||||
90
src/admin/components/forms/field-types/JSON/JSON.tsx
Normal file
90
src/admin/components/forms/field-types/JSON/JSON.tsx
Normal 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);
|
||||
12
src/admin/components/forms/field-types/JSON/index.scss
Normal file
12
src/admin/components/forms/field-types/JSON/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/admin/components/forms/field-types/JSON/index.tsx
Normal file
13
src/admin/components/forms/field-types/JSON/index.tsx
Normal 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;
|
||||
5
src/admin/components/forms/field-types/JSON/types.ts
Normal file
5
src/admin/components/forms/field-types/JSON/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { JSONField } from '../../../../../fields/config/types';
|
||||
|
||||
export type Props = Omit<JSONField, 'type'> & {
|
||||
path?: string
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
number: Text,
|
||||
email: Text,
|
||||
code: Text,
|
||||
json: Text,
|
||||
checkbox: Text,
|
||||
radio: Select,
|
||||
row: Nested,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -74,6 +74,11 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'json': {
|
||||
fieldSchema = { type: 'object' };
|
||||
break;
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
fieldSchema = {
|
||||
type: 'array',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ export default [
|
||||
'text',
|
||||
'textarea',
|
||||
'code',
|
||||
'json',
|
||||
'number',
|
||||
'email',
|
||||
'radio',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
36
test/fields/collections/JSON/index.tsx
Normal file
36
test/fields/collections/JSON/index.tsx
Normal 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;
|
||||
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user