introduces Row, modifies buildSchema to support fields that need to modify top-level schemas

This commit is contained in:
James
2020-06-02 13:57:55 -04:00
parent 15dd6e2887
commit f8369ffb10
26 changed files with 360 additions and 209 deletions

View File

@@ -54,13 +54,13 @@ const ColumnSelector = (props) => {
return (
<div className={baseClass}>
{fields && fields.map((field) => {
{fields && fields.map((field, i) => {
const isEnabled = columns.find(column => column === field.name);
return (
<Pill
onClick={() => dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })}
alignIcon="left"
key={field.name}
key={field.name || i}
icon={isEnabled ? <X /> : <Plus />}
pillStyle={isEnabled ? 'dark' : undefined}
className={`${baseClass}__active-column`}

View File

@@ -10,7 +10,6 @@ const RenderFields = ({
return (
<>
{fieldSchema.map((field, i) => {
const { defaultValue } = field;
if (field?.hidden !== 'api' && field?.hidden !== true) {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
let FieldComponent = field?.hidden === 'admin' ? fieldTypes.hidden : fieldTypes[field.type];
@@ -19,14 +18,22 @@ const RenderFields = ({
FieldComponent = customComponents[field.name].field;
}
let { defaultValue } = field;
if (!field.name) {
defaultValue = initialData;
} else if (initialData[field.name]) {
defaultValue = initialData[field.name];
}
if (FieldComponent) {
return (
<FieldComponent
fieldTypes={fieldTypes}
key={field.name}
key={field.name || `field-${i}`}
{...field}
validate={field.validate ? value => field.validate(value, field) : undefined}
defaultValue={initialData[field.name] || defaultValue}
defaultValue={defaultValue}
/>
);
}

View File

@@ -39,14 +39,12 @@ const Checkbox = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -72,7 +70,7 @@ Checkbox.defaultProps = {
defaultValue: false,
validate: null,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
};
@@ -82,7 +80,7 @@ Checkbox.propTypes = {
defaultValue: PropTypes.bool,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string,
};

View File

@@ -44,14 +44,12 @@ const DateTime = (props) => {
formProcessing && 'processing',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -80,7 +78,7 @@ DateTime.defaultProps = {
defaultValue: null,
validate: null,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
};
@@ -91,7 +89,7 @@ DateTime.propTypes = {
defaultValue: PropTypes.string,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
};

View File

@@ -50,14 +50,12 @@ const Email = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -90,7 +88,7 @@ Email.defaultProps = {
placeholder: undefined,
validate: defaultValidate,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
autoComplete: undefined,
};
@@ -102,7 +100,7 @@ Email.propTypes = {
defaultValue: PropTypes.string,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string,
autoComplete: PropTypes.string,

View File

@@ -17,8 +17,8 @@ const Group = (props) => {
fieldSchema={fields.map((subField) => {
return {
...subField,
name: `${name}.${subField.name}`,
defaultValue: defaultValue[subField.name],
name: `${name}${subField.name ? `.${subField.name}` : ''}`,
defaultValue: subField.name ? defaultValue[subField.name] : defaultValue,
};
})}
/>

View File

@@ -41,14 +41,12 @@ const NumberField = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -80,7 +78,7 @@ NumberField.defaultProps = {
placeholder: undefined,
validate: defaultValidate,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
};
@@ -91,7 +89,7 @@ NumberField.propTypes = {
defaultValue: PropTypes.number,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string,
};

View File

@@ -34,8 +34,6 @@ const Password = (props) => {
validate,
});
const fieldWidth = width ? `${width}%` : null;
const classes = [
'field-type',
'password',
@@ -47,7 +45,7 @@ const Password = (props) => {
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -77,7 +75,7 @@ Password.defaultProps = {
defaultValue: null,
validate: defaultValidate,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
};
@@ -86,7 +84,7 @@ Password.propTypes = {
required: PropTypes.bool,
defaultValue: PropTypes.string,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string.isRequired,
validate: PropTypes.func,

View File

@@ -210,9 +210,6 @@ class Relationship extends Component {
showError && 'error',
].filter(Boolean).join(' ');
// eslint-disable-next-line prefer-template
const fieldWidth = width ? width + '%' : null;
const valueToRender = this.findValueInOptions(options, value);
// ///////////////////////////////////////////
@@ -224,7 +221,7 @@ class Relationship extends Component {
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -258,7 +255,7 @@ Relationship.defaultProps = {
required: false,
errorMessage: defaultError,
hasMany: false,
width: 100,
width: undefined,
showError: false,
value: null,
formProcessing: false,
@@ -282,7 +279,7 @@ Relationship.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
formProcessing: PropTypes.bool,
width: PropTypes.number,
width: PropTypes.string,
hasMany: PropTypes.bool,
onFieldChange: PropTypes.func.isRequired,
hasMultipleRelations: PropTypes.bool.isRequired,

View File

@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import './index.scss';
const Row = (props) => {
const {
fields, fieldTypes, name, defaultValue,
} = props;
return (
<div className="field-type row">
<RenderFields
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => {
return {
...field,
name: `${name ? `${name}.` : ''}${field.name}`,
defaultValue: defaultValue ? defaultValue[field.name] : null,
};
})}
/>
</div>
);
};
Row.defaultProps = {
name: '',
defaultValue: null,
};
Row.propTypes = {
fields: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
name: PropTypes.string,
defaultValue: PropTypes.shape({}),
};
export default withCondition(Row);

View File

@@ -0,0 +1,13 @@
@import '../../../../scss/styles.scss';
.field-type.row {
display: flex;
margin-left: - base(.5);
margin-right: - base(.5);
width: calc(100% + #{$baseline});
> * {
margin-left: base(.5);
margin-right: base(.5);
}
}

View File

@@ -22,7 +22,7 @@ const formatFormValue = (value) => {
});
}
if (typeof value === 'object' && value.value) {
if (typeof value === 'object' && value !== null && value.value) {
return value.value;
}
@@ -85,14 +85,12 @@ const Select = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -124,7 +122,7 @@ Select.defaultProps = {
validate: defaultValidate,
defaultValue: null,
hasMany: false,
width: 100,
width: undefined,
};
Select.propTypes = {
@@ -138,7 +136,7 @@ Select.propTypes = {
]),
validate: PropTypes.func,
name: PropTypes.string.isRequired,
width: PropTypes.number,
width: PropTypes.string,
options: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.string,

View File

@@ -41,14 +41,12 @@ const Text = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -80,7 +78,7 @@ Text.defaultProps = {
placeholder: undefined,
validate: defaultValidate,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
};
@@ -91,7 +89,7 @@ Text.propTypes = {
defaultValue: PropTypes.string,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string,
};

View File

@@ -41,14 +41,12 @@ const Textarea = (props) => {
showError && 'error',
].filter(Boolean).join(' ');
const fieldWidth = width ? `${width}%` : undefined;
return (
<div
className={classes}
style={{
...style,
width: fieldWidth,
width,
}}
>
<Error
@@ -78,7 +76,7 @@ Textarea.defaultProps = {
defaultValue: null,
validate: defaultValidate,
errorMessage: defaultError,
width: 100,
width: undefined,
style: {},
placeholder: null,
};
@@ -89,7 +87,7 @@ Textarea.propTypes = {
defaultValue: PropTypes.string,
validate: PropTypes.func,
errorMessage: PropTypes.string,
width: PropTypes.number,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string,
placeholder: PropTypes.string,

View File

@@ -13,3 +13,4 @@ export { default as checkbox } from './Checkbox';
export { default as flexible } from './Flexible';
export { default as group } from './Group';
export { default as repeater } from './Repeater';
export { default as row } from './Row';

View File

@@ -1,5 +1,6 @@
import { useContext, useCallback, useEffect } from 'react';
import FormContext from '../Form/Context';
import { useLocale } from '../../utilities/Locale';
import './index.scss';
@@ -12,6 +13,7 @@ const useFieldType = (options) => {
validate,
} = options;
const locale = useLocale();
const formContext = useContext(FormContext);
const { dispatchFields, submitted, processing } = formContext;
let mountValue = formContext.fields[name]?.value;
@@ -33,6 +35,10 @@ const useFieldType = (options) => {
sendField(mountValue);
}, [sendField, mountValue]);
useEffect(() => {
sendField(null);
}, [locale, sendField]);
// Remove field from state on "unmount"
useEffect(() => {
return () => dispatchFields({ name, type: 'REMOVE' });

View File

@@ -41,11 +41,12 @@ const withCondition = (Field) => {
WithCondition.defaultProps = {
condition: null,
name: '',
};
WithCondition.propTypes = {
condition: PropTypes.func,
name: PropTypes.string.isRequired,
name: PropTypes.string,
};
return WithCondition;

View File

@@ -8,10 +8,15 @@
align-items: flex-start;
}
&__main {
min-width: 0;
}
&__header {
h1 {
word-break: break-all;
hyphens: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@@ -1,118 +0,0 @@
const { Schema } = require('mongoose');
const formatBaseSchema = (field) => {
return {
hide: field.hidden === 'api' || field.hidden === true,
localized: field.localized || false,
unique: field.unique || false,
required: (field.required && !field.localized && !field.hidden && !field.condition) || false,
default: field.defaultValue || undefined,
};
};
const fieldToSchemaMap = {
number: (field) => {
return { ...formatBaseSchema(field), type: Number };
},
text: (field) => {
return { ...formatBaseSchema(field), type: String };
},
email: (field) => {
return { ...formatBaseSchema(field), type: String };
},
textarea: (field) => {
return { ...formatBaseSchema(field), type: String };
},
wysiwyg: (field) => {
return { ...formatBaseSchema(field), type: String };
},
code: (field) => {
return { ...formatBaseSchema(field), type: String };
},
checkbox: (field) => {
return { ...formatBaseSchema(field), type: Boolean };
},
date: (field) => {
return {
...formatBaseSchema(field),
type: Date,
};
},
upload: (field) => {
const schema = {
...formatBaseSchema(field),
type: Schema.Types.ObjectId,
autopopulate: true,
ref: field.type,
};
return schema;
},
relationship: (field) => {
let schema = {};
if (Array.isArray(field.relationTo)) {
schema.value = {
type: Schema.Types.ObjectId,
autopopulate: true,
refPath: `${field.name}${field.localized ? '.{{LOCALE}}' : ''}.relationTo`,
};
schema.relationTo = { type: String, enum: field.relationTo };
} else {
schema = {
...formatBaseSchema(field),
};
schema.type = Schema.Types.ObjectId;
schema.autopopulate = true;
schema.ref = field.relationTo;
}
if (field.hasMany) {
return {
type: [schema],
localized: field.localized,
};
}
return schema;
},
repeater: (field) => {
const schema = {};
field.fields.forEach((subField) => {
schema[subField.name] = fieldToSchemaMap[subField.type](subField);
});
return [schema];
},
group: (field) => {
// Localization for groups not supported
const schema = {};
field.fields.forEach((subField) => {
schema[subField.name] = fieldToSchemaMap[subField.type](subField);
});
return schema;
},
select: (field) => {
const schema = {
...formatBaseSchema(field),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') return option.value;
return option;
}),
};
return field.hasMany ? [schema] : schema;
},
flexible: (field) => {
const flexibleSchema = new Schema({ blockName: String }, { discriminatorKey: 'blockType', _id: false });
return {
type: [flexibleSchema],
localized: field.localized || false,
};
},
};
module.exports = fieldToSchemaMap;

View File

@@ -14,7 +14,7 @@ const buildModel = (config) => {
const Globals = mongoose.model('globals', globalsSchema);
Object.values(config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(globalConfig.fields, config);
const globalSchema = buildSchema(globalConfig.fields);
globalSchema
.plugin(localizationPlugin, config.localization)

View File

@@ -100,6 +100,22 @@ function buildMutationInputType(name, fields, parentName) {
return { type };
},
flexible: () => ({ type: GraphQLJSON }),
row: (field) => {
return field.fields.reduce((acc, rowField) => {
const getFieldSchema = fieldToSchemaMap[rowField.type];
if (getFieldSchema) {
const fieldSchema = getFieldSchema(rowField);
return [
...acc,
fieldSchema,
];
}
return null;
}, []);
},
};
const fieldTypes = fields.reduce((schema, field) => {
@@ -108,6 +124,15 @@ function buildMutationInputType(name, fields, parentName) {
if (getFieldSchema) {
const fieldSchema = getFieldSchema(field);
if (Array.isArray(fieldSchema)) {
return fieldSchema.reduce((acc, subField, i) => {
return {
...acc,
[field.fields[i].name]: subField,
};
}, schema);
}
return {
...schema,
[formatName(field.name)]: fieldSchema,

View File

@@ -1,7 +0,0 @@
const schemaBaseFields = {
// TODO: What is status being used for? It is probable that people are going to try to add their own status field for their own purposes. Is there a safe way we can house payload level fields in the future to avoid collisions?
status: String,
publishedAt: Date,
};
module.exports = schemaBaseFields;

View File

@@ -1,43 +1,198 @@
/* eslint-disable no-use-before-define */
const { Schema } = require('mongoose');
const fieldToSchemaMap = require('../../fields/schemaMap');
const baseFields = require('./baseFields');
const buildSchema = (configFields, config, options = {}, additionalBaseFields = {}) => {
const fields = { ...baseFields, ...additionalBaseFields };
const flexiblefields = [];
const formatBaseSchema = (field) => {
return {
hide: field.hidden === 'api' || field.hidden === true,
localized: field.localized || false,
unique: field.unique || false,
required: (field.required && !field.localized && !field.hidden && !field.condition) || false,
default: field.defaultValue || undefined,
};
};
const buildSchema = (configFields, options = {}) => {
let fields = {};
configFields.forEach((field) => {
const fieldSchema = fieldToSchemaMap[field.type];
if (field.type === 'flexible') {
flexiblefields.push(field);
}
if (fieldSchema) {
fields[field.name] = fieldSchema(field, config);
fields = fieldSchema(field, fields);
}
});
const schema = new Schema(fields, options);
if (flexiblefields.length > 0) {
flexiblefields.forEach((field) => {
if (field.blocks && field.blocks.length > 0) {
field.blocks.forEach((block) => {
const blockSchemaFields = {};
configFields.forEach((field) => {
if (field.type === 'flexible' && field.blocks && field.blocks.length > 0) {
field.blocks.forEach((block) => {
let blockSchemaFields = {};
block.fields.forEach((blockField) => {
const fieldSchema = fieldToSchemaMap[blockField.type];
if (fieldSchema) blockSchemaFields[blockField.name] = fieldSchema(blockField, config);
});
const blockSchema = new Schema(blockSchemaFields, { _id: false });
schema.path(field.name).discriminator(block.slug, blockSchema);
block.fields.forEach((blockField) => {
const fieldSchema = fieldToSchemaMap[blockField.type];
if (fieldSchema) {
blockSchemaFields = fieldSchema(blockField, blockSchemaFields);
}
});
}
});
}
const blockSchema = new Schema(blockSchemaFields, { _id: false });
schema.path(field.name).discriminator(block.slug, blockSchema);
});
}
});
return schema;
};
const fieldToSchemaMap = {
number: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: Number },
};
},
text: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: String },
};
},
email: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: String },
};
},
textarea: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: String },
};
},
wysiwyg: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: String },
};
},
code: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: String },
};
},
checkbox: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: Boolean },
};
},
date: (field, fields) => {
return {
...fields,
[field.name]: { ...formatBaseSchema(field), type: Date },
};
},
upload: (field, fields) => {
return {
...fields,
[field.name]: {
...formatBaseSchema(field),
type: Schema.Types.ObjectId,
autopopulate: true,
ref: field.type,
},
};
},
relationship: (field, fields) => {
let schema = {};
if (Array.isArray(field.relationTo)) {
schema.value = {
type: Schema.Types.ObjectId,
autopopulate: true,
refPath: `${field.name}${field.localized ? '.{{LOCALE}}' : ''}.relationTo`,
};
schema.relationTo = { type: String, enum: field.relationTo };
} else {
schema = {
...formatBaseSchema(field),
};
schema.type = Schema.Types.ObjectId;
schema.autopopulate = true;
schema.ref = field.relationTo;
}
if (field.hasMany) {
return {
type: [schema],
localized: field.localized,
};
}
return {
...fields,
[field.name]: schema,
};
},
row: (field, fields) => {
const newFields = { ...fields };
field.fields.forEach((rowField) => {
const fieldSchemaMap = fieldToSchemaMap[rowField.type];
if (fieldSchemaMap) {
const fieldSchema = fieldSchemaMap(rowField, fields);
newFields[rowField.name] = fieldSchema[rowField.name];
}
});
return newFields;
},
repeater: (field, fields) => {
const schema = buildSchema(field.fields);
return {
...fields,
[field.name]: [schema],
};
},
group: (field, fields) => {
const schema = buildSchema(field.fields, { _id: false });
return {
...fields,
[field.name]: schema,
};
},
select: (field, fields) => {
const schema = {
...formatBaseSchema(field),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') return option.value;
return option;
}),
};
return {
...fields,
[field.name]: field.hasMany ? [schema] : schema,
};
},
flexible: (field, fields) => {
const flexibleSchema = new Schema({ blockName: String }, { discriminatorKey: 'blockType', _id: false });
return {
...fields,
[field.name]: {
type: [flexibleSchema],
localized: field.localized || false,
},
};
},
};
module.exports = buildSchema;

View File

@@ -111,7 +111,9 @@ module.exports = (config) => {
filename: './index.html',
}),
new webpack.HotModuleReplacementPlugin(),
new Dotenv(),
new Dotenv({
silent: true,
}),
],
resolve: {
modules: ['node_modules', path.resolve(__dirname, '../../node_modules')],