diff --git a/demo/collections/AllFields.js b/demo/collections/AllFields.js index 8b439277ee..fe14e4cbaf 100644 --- a/demo/collections/AllFields.js +++ b/demo/collections/AllFields.js @@ -11,6 +11,9 @@ const AllFields = { return null; }, + policies: { + read: () => true, + }, fields: [ { name: 'text', @@ -65,6 +68,41 @@ const AllFields = { label: 'Checkbox', position: 'sidebar', }, + { + type: 'row', + fields: [ + { + name: 'email', + label: 'Email', + type: 'email', + }, { + name: 'number', + label: 'Number', + type: 'number', + }, + ], + }, + { + type: 'group', + label: 'Group', + name: 'group', + fields: [ + { + type: 'row', + fields: [ + { + name: 'nestedText1', + label: 'Nested Text 1', + type: 'text', + }, { + name: 'nestedText2', + label: 'Nested Text 2', + type: 'text', + }, + ], + }, + ], + }, ], timestamps: true, }; diff --git a/demo/server.js b/demo/server.js index 82a8ac1b68..2aa8ffa2f5 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,7 +1,6 @@ const path = require('path'); const express = require('express'); const Payload = require('../src'); -const publicConfig = require('./payload.public.config'); const privateConfig = require('./payload.private.config'); const expressApp = express(); diff --git a/src/client/components/elements/ColumnSelector/index.js b/src/client/components/elements/ColumnSelector/index.js index ca3393c02f..f9e855b83c 100644 --- a/src/client/components/elements/ColumnSelector/index.js +++ b/src/client/components/elements/ColumnSelector/index.js @@ -54,13 +54,13 @@ const ColumnSelector = (props) => { return (
- {fields && fields.map((field) => { + {fields && fields.map((field, i) => { const isEnabled = columns.find(column => column === field.name); return ( dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })} alignIcon="left" - key={field.name} + key={field.name || i} icon={isEnabled ? : } pillStyle={isEnabled ? 'dark' : undefined} className={`${baseClass}__active-column`} diff --git a/src/client/components/forms/RenderFields/index.js b/src/client/components/forms/RenderFields/index.js index cf696b944e..07816a5c20 100644 --- a/src/client/components/forms/RenderFields/index.js +++ b/src/client/components/forms/RenderFields/index.js @@ -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 ( field.validate(value, field) : undefined} - defaultValue={initialData[field.name] || defaultValue} + defaultValue={defaultValue} /> ); } diff --git a/src/client/components/forms/field-types/Checkbox/index.js b/src/client/components/forms/field-types/Checkbox/index.js index 5f96fcaaf1..3eddaab8d8 100644 --- a/src/client/components/forms/field-types/Checkbox/index.js +++ b/src/client/components/forms/field-types/Checkbox/index.js @@ -39,14 +39,12 @@ const Checkbox = (props) => { showError && 'error', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ formProcessing && 'processing', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ showError && 'error', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ 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, }; })} /> diff --git a/src/client/components/forms/field-types/Number/index.js b/src/client/components/forms/field-types/Number/index.js index 1dd59eecb5..40c89e20bd 100644 --- a/src/client/components/forms/field-types/Number/index.js +++ b/src/client/components/forms/field-types/Number/index.js @@ -41,14 +41,12 @@ const NumberField = (props) => { showError && 'error', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ 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, }} > { + const { + fields, fieldTypes, name, defaultValue, + } = props; + + return ( +
+ { + return { + ...field, + name: `${name ? `${name}.` : ''}${field.name}`, + defaultValue: defaultValue ? defaultValue[field.name] : null, + }; + })} + /> +
+ ); +}; + +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); diff --git a/src/client/components/forms/field-types/Row/index.scss b/src/client/components/forms/field-types/Row/index.scss new file mode 100644 index 0000000000..eca7669aa5 --- /dev/null +++ b/src/client/components/forms/field-types/Row/index.scss @@ -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); + } +} diff --git a/src/client/components/forms/field-types/Select/index.js b/src/client/components/forms/field-types/Select/index.js index e2de794599..2e1b0c0717 100644 --- a/src/client/components/forms/field-types/Select/index.js +++ b/src/client/components/forms/field-types/Select/index.js @@ -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 (
{ showError && 'error', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ showError && 'error', ].filter(Boolean).join(' '); - const fieldWidth = width ? `${width}%` : undefined; - return (
{ 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' }); diff --git a/src/client/components/forms/withCondition/index.js b/src/client/components/forms/withCondition/index.js index 798c77899f..f5f68f2274 100644 --- a/src/client/components/forms/withCondition/index.js +++ b/src/client/components/forms/withCondition/index.js @@ -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; diff --git a/src/client/components/views/collections/Edit/index.scss b/src/client/components/views/collections/Edit/index.scss index 94201ddb37..c049b4e998 100644 --- a/src/client/components/views/collections/Edit/index.scss +++ b/src/client/components/views/collections/Edit/index.scss @@ -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; } } diff --git a/src/fields/schemaMap.js b/src/fields/schemaMap.js deleted file mode 100644 index b6ecec1393..0000000000 --- a/src/fields/schemaMap.js +++ /dev/null @@ -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; diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index 924e2cb2cb..6975e3165d 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -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) diff --git a/src/graphql/schema/buildMutationInputType.js b/src/graphql/schema/buildMutationInputType.js index 7aa33be22e..ca0ddc8273 100644 --- a/src/graphql/schema/buildMutationInputType.js +++ b/src/graphql/schema/buildMutationInputType.js @@ -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, diff --git a/src/mongoose/schema/baseFields.js b/src/mongoose/schema/baseFields.js deleted file mode 100644 index 74cce815b7..0000000000 --- a/src/mongoose/schema/baseFields.js +++ /dev/null @@ -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; diff --git a/src/mongoose/schema/buildSchema.js b/src/mongoose/schema/buildSchema.js index 49a6dfb06b..ff6eb3880b 100644 --- a/src/mongoose/schema/buildSchema.js +++ b/src/mongoose/schema/buildSchema.js @@ -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; diff --git a/src/webpack/getWebpackDevConfig.js b/src/webpack/getWebpackDevConfig.js index 708a6fc9bb..9e7de4293a 100644 --- a/src/webpack/getWebpackDevConfig.js +++ b/src/webpack/getWebpackDevConfig.js @@ -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')],