diff --git a/demo/collections/Localized.js b/demo/collections/Localized.js index 7ba13b6013..af04354eeb 100644 --- a/demo/collections/Localized.js +++ b/demo/collections/Localized.js @@ -32,6 +32,12 @@ module.exports = { unique: true, localized: true, }, + { + name: 'summary', + label: 'Summary', + type: 'text', + index: true, + }, { name: 'description', label: 'Description', diff --git a/src/client/components/elements/DatePicker/index.scss b/src/client/components/elements/DatePicker/index.scss index 80f7ccbae8..a0be6d0afe 100644 --- a/src/client/components/elements/DatePicker/index.scss +++ b/src/client/components/elements/DatePicker/index.scss @@ -3,13 +3,35 @@ $cal-icon-width: 18px; .date-time-picker { + .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box, + .react-datepicker__time-container { + width: 120px; + } &--hide-dates { .react-datepicker { - &__month-container { + width: 100%; + + &__month-container, + &__navigation--previous, + &__navigation--next { display: none; visibility: hidden; } + + &-popper, + &__time-container, + &__time-box { + width: 100%; + } + + &__time-container { + .react-datepicker__time { + .react-datepicker__time-box { + width: 100%; + } + } + } } } @@ -69,7 +91,7 @@ $cal-icon-width: 18px; &--time { padding: 10px 0; border-bottom: 1px solid $color-light-gray; - font-weight: bold; + font-weight: 600; } } @@ -111,10 +133,15 @@ $cal-icon-width: 18px; &__current-month { padding: 10px 0; + font-weight: 600; + } + + &__month-container { + border-right: 1px solid $color-light-gray; } &__time-container { - border-left: 1px solid $color-light-gray; + border-left: none; } &__day-names { @@ -131,38 +158,54 @@ $cal-icon-width: 18px; } &--selected { + font-weight: 600; + &:focus { - background-color: $color-dark-gray; + background-color: $color-light-gray; } } &--keyboard-selected { color: white; + font-weight: 600; &:focus { background-color: $color-light-gray; box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray; } } + + &--today { + font-weight: 600; + } + } + + &__day, + &__day-name { + width: base(1.5); + margin: base(.15); + line-height: base(1.25); } } .react-datepicker-popper { z-index: 10; + border: 1px solid $color-light-gray; } .react-datepicker__day--keyboard-selected, .react-datepicker__month-text--keyboard-selected, .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected { - background: $color-dark-gray; + box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray; + background-color: $color-light-gray; color: $color-dark-gray; - font-weight: normal; border-radius: 0; } .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--selected, .react-datepicker__day--selected, .react-datepicker__day--in-selecting-range, .react-datepicker__day--in-range, .react-datepicker__month-text--selected, .react-datepicker__month-text--in-selecting-range, .react-datepicker__month-text--in-range { - background: $color-dark-gray; + box-shadow: inset 0px 0px 0px 1px $color-dark-gray, 0px 0px 0px 1px $color-dark-gray; + background-color: $color-light-gray; color: $color-dark-gray; border-radius: 0; } @@ -171,17 +214,14 @@ $cal-icon-width: 18px; border-radius: 0; } - .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box, - .react-datepicker__time-container { - width: 120px; - } - .react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) { right: 130px; } .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item { line-height: 20px; + font-size: base(.5); + font-weight: 500; } @include small-break { diff --git a/src/collections/sanitize.js b/src/collections/sanitize.js index 8012b78f03..98e203205f 100644 --- a/src/collections/sanitize.js +++ b/src/collections/sanitize.js @@ -257,7 +257,8 @@ const sanitizeCollection = (collections, collection) => { // Sanitize fields // ///////////////////////////////// - sanitized.fields = sanitizeFields(sanitized.fields); + const validRelationships = collections.map((c) => c.slug); + sanitized.fields = sanitizeFields(sanitized.fields, validRelationships); return sanitized; }; diff --git a/src/errors/InvalidFieldRelationship.js b/src/errors/InvalidFieldRelationship.js new file mode 100644 index 0000000000..7599342bac --- /dev/null +++ b/src/errors/InvalidFieldRelationship.js @@ -0,0 +1,9 @@ +const APIError = require('./APIError'); + +class InvalidFieldRelationship extends APIError { + constructor(field, relationship) { + super(`Field ${field.label} has invalid relationship '${relationship}'.`); + } +} + +module.exports = InvalidFieldRelationship; diff --git a/src/errors/index.js b/src/errors/index.js index 1457a74995..9b74a13124 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -2,28 +2,32 @@ const APIError = require('./APIError'); const AuthenticationError = require('./AuthenticationError'); const DuplicateCollection = require('./DuplicateCollection'); const DuplicateGlobal = require('./DuplicateGlobal'); +const ErrorDeletingFile = require('./ErrorDeletingFile'); +const errorHandler = require('../express/middleware/errorHandler'); +const Forbidden = require('./Forbidden'); +const InvalidFieldRelationship = require('./InvalidFieldRelationship'); const MissingCollectionLabel = require('./MissingCollectionLabel'); +const MissingFieldInputOptions = require('./MissingFieldInputOptions'); +const MissingFieldType = require('./MissingFieldType'); +const MissingFile = require('./MissingFile'); const MissingGlobalLabel = require('./MissingGlobalLabel'); const NotFound = require('./NotFound'); -const Forbidden = require('./Forbidden'); const ValidationError = require('./ValidationError'); -const errorHandler = require('../express/middleware/errorHandler'); -const MissingFile = require('./MissingFile'); -const MissingFieldInputOptions = require('./MissingFieldInputOptions'); -const ErrorDeletingFile = require('./ErrorDeletingFile'); module.exports = { - errorHandler, - ErrorDeletingFile, APIError, AuthenticationError, DuplicateCollection, DuplicateGlobal, + ErrorDeletingFile, + errorHandler, + Forbidden, + InvalidFieldRelationship, MissingCollectionLabel, + MissingFieldInputOptions, + MissingFieldType, + MissingFile, MissingGlobalLabel, NotFound, - Forbidden, ValidationError, - MissingFile, - MissingFieldInputOptions, }; diff --git a/src/fields/sanitize.js b/src/fields/sanitize.js index 16576b1364..4e5d8ca156 100644 --- a/src/fields/sanitize.js +++ b/src/fields/sanitize.js @@ -1,7 +1,7 @@ -const { MissingFieldType } = require('../errors'); +const { MissingFieldType, InvalidFieldRelationship } = require('../errors'); const validations = require('./validations'); -const sanitizeFields = (fields) => { +const sanitizeFields = (fields, validRelationships) => { if (!fields) return []; return fields.map((unsanitizedField) => { @@ -9,6 +9,15 @@ const sanitizeFields = (fields) => { if (!field.type) throw new MissingFieldType(field); + if (field.type === 'relationship') { + const relationships = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]; + relationships.forEach((relationship) => { + if (!validRelationships.includes(relationship)) { + throw new InvalidFieldRelationship(field, relationship); + } + }); + } + if (typeof field.validate === 'undefined') { const defaultValidate = validations[field.type]; const noValidate = () => true; @@ -24,7 +33,7 @@ const sanitizeFields = (fields) => { if (field.blocks) { field.blocks = field.blocks.map((block) => { const unsanitizedBlock = { ...block }; - unsanitizedBlock.fields = sanitizeFields(block.fields); + unsanitizedBlock.fields = sanitizeFields(block.fields, validRelationships); return unsanitizedBlock; }); } diff --git a/src/fields/sanitize.spec.js b/src/fields/sanitize.spec.js new file mode 100644 index 0000000000..c19c085b5b --- /dev/null +++ b/src/fields/sanitize.spec.js @@ -0,0 +1,111 @@ +/* eslint-disable jest/require-to-throw-message */ +const sanitizeFields = require('./sanitize'); +const { MissingFieldType, InvalidFieldRelationship } = require('../errors'); + +describe('sanitizeFields', () => { + it('should throw on missing type field', () => { + const fields = [{ + label: 'some-collection', + name: 'Some Collection', + }]; + expect(() => { + sanitizeFields(fields, []); + }).toThrow(MissingFieldType); + }); + + describe('relationships', () => { + it('should not throw on valid relationship', () => { + const validRelationships = ['some-collection']; + const fields = [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: 'some-collection', + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).not.toThrow(); + }); + + it('should not throw on valid relationship - multiple', () => { + const validRelationships = ['some-collection', 'another-collection']; + const fields = [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: ['some-collection', 'another-collection'], + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).not.toThrow(); + }); + + it('should not throw on valid relationship inside blocks', () => { + const validRelationships = ['some-collection']; + const fields = [{ + name: 'layout', + label: 'Layout Blocks', + singularLabel: 'Block', + type: 'blocks', + blocks: [{ + fields: [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: 'some-collection', + }], + }], + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).not.toThrow(); + }); + + it('should throw on invalid relationship', () => { + const validRelationships = ['some-collection']; + const fields = [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: 'not-valid', + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).toThrow(InvalidFieldRelationship); + }); + + it('should throw on invalid relationship - multiple', () => { + const validRelationships = ['some-collection', 'another-collection']; + const fields = [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: ['some-collection', 'not-valid'], + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).toThrow(InvalidFieldRelationship); + }); + + it('should throw on invalid relationship inside blocks', () => { + const validRelationships = ['some-collection']; + const fields = [{ + name: 'layout', + label: 'Layout Blocks', + singularLabel: 'Block', + type: 'blocks', + blocks: [{ + fields: [{ + type: 'relationship', + label: 'my-relationship', + name: 'My Relationship', + relationTo: 'not-valid', + }], + }], + }]; + expect(() => { + sanitizeFields(fields, validRelationships); + }).toThrow(InvalidFieldRelationship); + }); + }); +}); diff --git a/src/fields/validations.js b/src/fields/validations.js index c2b9373753..04995ab9df 100644 --- a/src/fields/validations.js +++ b/src/fields/validations.js @@ -115,7 +115,7 @@ const optionsToValidatorMap = { return true; }, date: (value, options = {}) => { - if (value && value instanceof Date) { + if (value && !isNaN(Date.parse(value.toString()))) { /* eslint-disable-line */ return true; } diff --git a/src/globals/sanitize.js b/src/globals/sanitize.js index 3792610b88..9486814724 100644 --- a/src/globals/sanitize.js +++ b/src/globals/sanitize.js @@ -1,7 +1,7 @@ const { MissingGlobalLabel } = require('../errors'); const sanitizeFields = require('../fields/sanitize'); -const sanitizeGlobals = (globals) => { +const sanitizeGlobals = (collections, globals) => { // ///////////////////////////////// // Ensure globals are valid // ///////////////////////////////// @@ -33,7 +33,8 @@ const sanitizeGlobals = (globals) => { // Sanitize fields // ///////////////////////////////// - sanitizedGlobal.fields = sanitizeFields(global.fields); + const validRelationships = collections.map((c) => c.slug); + sanitizedGlobal.fields = sanitizeFields(global.fields, validRelationships); return sanitizedGlobal; }); diff --git a/src/mongoose/buildSchema.js b/src/mongoose/buildSchema.js index 04af3df75a..1d57115915 100644 --- a/src/mongoose/buildSchema.js +++ b/src/mongoose/buildSchema.js @@ -35,6 +35,7 @@ const formatBaseSchema = (field) => { unique: field.unique || false, required: (field.required && !field.localized && !condition && !createAccess) || false, default: field.defaultValue || undefined, + index: field.index || field.unique || false, }; }; diff --git a/src/mongoose/connect.js b/src/mongoose/connect.js index 33fc7b65b9..fb220238ee 100644 --- a/src/mongoose/connect.js +++ b/src/mongoose/connect.js @@ -19,6 +19,7 @@ const connectMongoose = async (url) => { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, + autoIndex: false, }); logger.info(successfulConnectionMessage); } catch (err) { diff --git a/src/utilities/sanitizeConfig.js b/src/utilities/sanitizeConfig.js index ee2228e6c0..d34574a82f 100644 --- a/src/utilities/sanitizeConfig.js +++ b/src/utilities/sanitizeConfig.js @@ -10,7 +10,7 @@ const sanitizeConfig = (config) => { sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig.collections, collection)); if (sanitizedConfig.globals) { - sanitizedConfig.globals = sanitizeGlobals(sanitizedConfig.globals); + sanitizedConfig.globals = sanitizeGlobals(sanitizedConfig.collections, sanitizedConfig.globals); } else { sanitizedConfig.globals = []; } diff --git a/src/webpack/getWebpackDevConfig.js b/src/webpack/getWebpackDevConfig.js index c004c73047..43156e83f0 100644 --- a/src/webpack/getWebpackDevConfig.js +++ b/src/webpack/getWebpackDevConfig.js @@ -39,7 +39,7 @@ module.exports = (config) => { loader: 'babel-loader', options: { plugins: [ - [removeObjectProperties, { values: ['graphQL', 'hooks', 'webpack', 'access', 'hooks'] }], + [removeObjectProperties, { values: ['graphQL', 'hooks', 'webpack', 'access'] }], ], }, },