diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js index b4a98f9d..ef3f2778 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js @@ -1,4 +1,3 @@ -import { fromJS } from 'immutable'; import { stripIndent } from 'common-tags'; import { parseConfig, @@ -96,95 +95,83 @@ describe('config', () => { describe('applyDefaults', () => { describe('publish_mode', () => { it('should set publish_mode if not set', () => { - const config = fromJS({ + const config = { foo: 'bar', media_folder: 'path/to/media', public_folder: '/path/to/media', collections: [], - }); - expect(applyDefaults(config).get('publish_mode')).toEqual('simple'); + }; + expect(applyDefaults(config).publish_mode).toEqual('simple'); }); it('should set publish_mode from config', () => { - const config = fromJS({ + const config = { foo: 'bar', publish_mode: 'complex', media_folder: 'path/to/media', public_folder: '/path/to/media', collections: [], - }); - expect(applyDefaults(config).get('publish_mode')).toEqual('complex'); + }; + expect(applyDefaults(config).publish_mode).toEqual('complex'); }); }); describe('public_folder', () => { it('should set public_folder based on media_folder if not set', () => { expect( - applyDefaults( - fromJS({ - foo: 'bar', - media_folder: 'path/to/media', - collections: [], - }), - ).get('public_folder'), + applyDefaults({ + foo: 'bar', + media_folder: 'path/to/media', + collections: [], + }).public_folder, ).toEqual('/path/to/media'); }); it('should not overwrite public_folder if set', () => { expect( - applyDefaults( - fromJS({ - foo: 'bar', - media_folder: 'path/to/media', - public_folder: '/publib/path', - collections: [], - }), - ).get('public_folder'), + applyDefaults({ + foo: 'bar', + media_folder: 'path/to/media', + public_folder: '/publib/path', + collections: [], + }).public_folder, ).toEqual('/publib/path'); expect( - applyDefaults( - fromJS({ - foo: 'bar', - media_folder: 'path/to/media', - public_folder: '', - collections: [], - }), - ).get('public_folder'), + applyDefaults({ + foo: 'bar', + media_folder: 'path/to/media', + public_folder: '', + collections: [], + }).public_folder, ).toEqual(''); }); }); describe('slug', () => { it('should set default slug config if not set', () => { - expect(applyDefaults(fromJS({ collections: [] })).get('slug')).toEqual( - fromJS({ encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' }), - ); + expect(applyDefaults({ collections: [] }).slug).toEqual({ + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }); }); it('should not overwrite slug encoding if set', () => { expect( - applyDefaults(fromJS({ collections: [], slug: { encoding: 'ascii' } })).getIn([ - 'slug', - 'encoding', - ]), + applyDefaults({ collections: [], slug: { encoding: 'ascii' } }).slug.encoding, ).toEqual('ascii'); }); it('should not overwrite slug clean_accents if set', () => { expect( - applyDefaults(fromJS({ collections: [], slug: { clean_accents: true } })).getIn([ - 'slug', - 'clean_accents', - ]), + applyDefaults({ collections: [], slug: { clean_accents: true } }).slug.clean_accents, ).toEqual(true); }); it('should not overwrite slug sanitize_replacement if set', () => { expect( - applyDefaults(fromJS({ collections: [], slug: { sanitize_replacement: '_' } })).getIn([ - 'slug', - 'sanitize_replacement', - ]), + applyDefaults({ collections: [], slug: { sanitize_replacement: '_' } }).slug + .sanitize_replacement, ).toEqual('_'); }); }); @@ -192,259 +179,233 @@ describe('config', () => { describe('collections', () => { it('should strip leading slashes from collection folder', () => { expect( - applyDefaults( - fromJS({ - collections: [{ folder: '/foo', fields: [{ name: 'title', widget: 'string' }] }], - }), - ).getIn(['collections', 0, 'folder']), + applyDefaults({ + collections: [{ folder: '/foo', fields: [{ name: 'title', widget: 'string' }] }], + }).collections[0].folder, ).toEqual('foo'); }); it('should strip leading slashes from collection files', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { files: [{ file: '/foo', fields: [{ name: 'title', widget: 'string' }] }] }, - ], - }), - ).getIn(['collections', 0, 'files', 0, 'file']), + applyDefaults({ + collections: [ + { files: [{ file: '/foo', fields: [{ name: 'title', widget: 'string' }] }] }, + ], + }).collections[0].files[0].file, ).toEqual('foo'); }); describe('public_folder and media_folder', () => { it('should set collection public_folder based on media_folder if not set', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - media_folder: 'static/images/docs', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ).getIn(['collections', 0, 'public_folder']), + applyDefaults({ + collections: [ + { + folder: 'foo', + media_folder: 'static/images/docs', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }).collections[0].public_folder, ).toEqual('static/images/docs'); }); it('should not overwrite collection public_folder if set', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - media_folder: 'static/images/docs', - public_folder: 'images/docs', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ).getIn(['collections', 0, 'public_folder']), + applyDefaults({ + collections: [ + { + folder: 'foo', + media_folder: 'static/images/docs', + public_folder: 'images/docs', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }).collections[0].public_folder, ).toEqual('images/docs'); }); it("should set collection media_folder and public_folder to an empty string when collection path exists, but collection media_folder doesn't", () => { - const result = applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - path: '{{slug}}/index', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ); - expect(result.getIn(['collections', 0, 'media_folder'])).toEqual(''); - expect(result.getIn(['collections', 0, 'public_folder'])).toEqual(''); + const result = applyDefaults({ + collections: [ + { + folder: 'foo', + path: '{{slug}}/index', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }); + expect(result.collections[0].media_folder).toEqual(''); + expect(result.collections[0].public_folder).toEqual(''); }); it('should set file public_folder based on media_folder if not set', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - files: [ - { - file: 'foo', - media_folder: 'static/images/docs', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }, - ], - }), - ).getIn(['collections', 0, 'files', 0, 'public_folder']), + applyDefaults({ + collections: [ + { + files: [ + { + file: 'foo', + media_folder: 'static/images/docs', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }, + ], + }).collections[0].files[0].public_folder, ).toEqual('static/images/docs'); }); it('should not overwrite file public_folder if set', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - files: [ - { - file: 'foo', - media_folder: 'static/images/docs', - public_folder: 'images/docs', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }, - ], - }), - ).getIn(['collections', 0, 'files', 0, 'public_folder']), + applyDefaults({ + collections: [ + { + files: [ + { + file: 'foo', + media_folder: 'static/images/docs', + public_folder: 'images/docs', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }, + ], + }).collections[0].files[0].public_folder, ).toEqual('images/docs'); }); it('should set nested field public_folder based on media_folder if not set', () => { - const config = applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - path: '{{slug}}/index', - fields: [ - { - name: 'title', - widget: 'string', - media_folder: 'collection/static/images/docs', - }, - ], - }, - { - files: [ - { - file: 'foo', - fields: [ - { - name: 'title', - widget: 'string', - media_folder: 'file/static/images/docs', - }, - ], - }, - ], - }, - ], - }), - ); - expect(config.getIn(['collections', 0, 'fields', 0, 'public_folder'])).toEqual( + const config = applyDefaults({ + collections: [ + { + folder: 'foo', + path: '{{slug}}/index', + fields: [ + { + name: 'title', + widget: 'string', + media_folder: 'collection/static/images/docs', + }, + ], + }, + { + files: [ + { + file: 'foo', + fields: [ + { + name: 'title', + widget: 'string', + media_folder: 'file/static/images/docs', + }, + ], + }, + ], + }, + ], + }); + expect(config.collections[0].fields[0].public_folder).toEqual( 'collection/static/images/docs', ); - expect( - config.getIn(['collections', 1, 'files', 0, 'fields', 0, 'public_folder']), - ).toEqual('file/static/images/docs'); + expect(config.collections[1].files[0].fields[0].public_folder).toEqual( + 'file/static/images/docs', + ); }); it('should not overwrite nested field public_folder if set', () => { - const config = applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - path: '{{slug}}/index', - fields: [ - { - name: 'title', - widget: 'string', - media_folder: 'collection/static/images/docs', - public_folder: 'collection/public_folder', - }, - ], - }, - { - files: [ - { - file: 'foo', - fields: [ - { - name: 'title', - widget: 'string', - public_folder: 'file/public_folder', - }, - ], - }, - ], - }, - ], - }), + const config = applyDefaults({ + collections: [ + { + folder: 'foo', + path: '{{slug}}/index', + fields: [ + { + name: 'title', + widget: 'string', + media_folder: 'collection/static/images/docs', + public_folder: 'collection/public_folder', + }, + ], + }, + { + files: [ + { + file: 'foo', + fields: [ + { + name: 'title', + widget: 'string', + public_folder: 'file/public_folder', + }, + ], + }, + ], + }, + ], + }); + expect(config.collections[0].fields[0].public_folder).toEqual('collection/public_folder'); + expect(config.collections[1].files[0].fields[0].public_folder).toEqual( + 'file/public_folder', ); - expect(config.getIn(['collections', 0, 'fields', 0, 'public_folder'])).toEqual( - 'collection/public_folder', - ); - expect( - config.getIn(['collections', 1, 'files', 0, 'fields', 0, 'public_folder']), - ).toEqual('file/public_folder'); }); }); describe('publish', () => { it('should set publish to true if not set', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - media_folder: 'static/images/docs', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ).getIn(['collections', 0, 'publish']), + applyDefaults({ + collections: [ + { + folder: 'foo', + media_folder: 'static/images/docs', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }).collections[0].publish, ).toEqual(true); }); it('should not override existing publish config', () => { expect( - applyDefaults( - fromJS({ - collections: [ - { - folder: 'foo', - media_folder: 'static/images/docs', - publish: false, - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ).getIn(['collections', 0, 'publish']), + applyDefaults({ + collections: [ + { + folder: 'foo', + media_folder: 'static/images/docs', + publish: false, + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }).collections[0].publish, ).toEqual(false); }); }); describe('editor preview', () => { it('should set editor preview honoring global config before and specific config after', () => { - const config = applyDefaults( - fromJS({ - editor: { - preview: false, + const config = applyDefaults({ + editor: { + preview: false, + }, + collections: [ + { + fields: [{ name: 'title' }], + folder: 'foo', }, - collections: [ - { - fields: [{ name: 'title' }], - folder: 'foo', + { + editor: { + preview: true, }, - { - editor: { - preview: true, - }, - fields: [{ name: 'title' }], - folder: 'bar', - }, - ], - }), - ); + fields: [{ name: 'title' }], + folder: 'bar', + }, + ], + }); - expect(config.getIn(['collections', 0, 'editor', 'preview'])).toEqual(false); - expect(config.getIn(['collections', 1, 'editor', 'preview'])).toEqual(true); + expect(config.collections[0].editor.preview).toEqual(false); + expect(config.collections[1].editor.preview).toEqual(true); }); }); }); @@ -452,61 +413,59 @@ describe('config', () => { test('should convert camel case to snake case', () => { expect( applyDefaults( - fromJS( - normalizeConfig({ - collections: [ - { - sortableFields: ['title'], - folder: 'src', - identifier_field: 'datetime', - fields: [ - { - name: 'datetime', - widget: 'datetime', - dateFormat: 'YYYY/MM/DD', - timeFormat: 'HH:mm', - pickerUtc: true, - }, - { - widget: 'number', - valueType: 'float', - }, - ], - }, - { - sortableFields: [], - files: [ - { - name: 'file', - file: 'src/file.json', - fields: [ - { - widget: 'markdown', - editorComponents: ['code'], - }, - { - widget: 'relation', - valueField: 'title', - searchFields: ['title'], - displayFields: ['title'], - optionsLength: 5, - }, - ], - }, - ], - }, - ], - }), - ), - ).toJS(), + normalizeConfig({ + collections: [ + { + sortableFields: ['title'], + folder: 'src', + identifier_field: 'datetime', + fields: [ + { + name: 'datetime', + widget: 'datetime', + dateFormat: 'YYYY/MM/DD', + timeFormat: 'HH:mm', + pickerUtc: true, + }, + { + widget: 'number', + valueType: 'float', + }, + ], + }, + { + sortableFields: [], + files: [ + { + name: 'file', + file: 'src/file.json', + fields: [ + { + widget: 'markdown', + editorComponents: ['code'], + }, + { + widget: 'relation', + valueField: 'title', + searchFields: ['title'], + displayFields: ['title'], + optionsLength: 5, + }, + ], + }, + ], + }, + ], + }), + ), ).toEqual({ - public_folder: '/', - publish_mode: 'simple', - slug: { clean_accents: false, encoding: 'unicode', sanitize_replacement: '-' }, collections: [ { sortable_fields: ['title'], folder: 'src', + type: 'folder_based_collection', + view_filters: [], + view_groups: [], identifier_field: 'datetime', fields: [ { @@ -525,10 +484,7 @@ describe('config', () => { valueType: 'float', }, ], - meta: {}, publish: true, - view_filters: [], - view_groups: [], }, { sortable_fields: [], @@ -556,261 +512,237 @@ describe('config', () => { ], }, ], - publish: true, + type: 'file_based_collection', view_filters: [], view_groups: [], + publish: true, }, ], + public_folder: '/', + publish_mode: 'simple', + slug: { clean_accents: false, encoding: 'unicode', sanitize_replacement: '-' }, }); }); describe('i18n', () => { it('should set root i18n on collection when collection i18n is set to true', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - }, - collections: [ - { folder: 'foo', i18n: true, fields: [{ name: 'title', widget: 'string' }] }, - ], - }), - ) - .getIn(['collections', 0, 'i18n']) - .toJS(), + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { folder: 'foo', i18n: true, fields: [{ name: 'title', widget: 'string' }] }, + ], + }).collections[0].i18n, ).toEqual({ structure: 'multiple_folders', locales: ['en', 'de'], default_locale: 'en' }); }); it('should not set root i18n on collection when collection i18n is not set', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - }, - collections: [{ folder: 'foo', fields: [{ name: 'title', widget: 'string' }] }], - }), - ).getIn(['collections', 0, 'i18n']), + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [{ folder: 'foo', fields: [{ name: 'title', widget: 'string' }] }], + }).collections[0].i18n, ).toBeUndefined(); }); it('should not set root i18n on collection when collection i18n is set to false', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - }, - collections: [ - { folder: 'foo', i18n: false, fields: [{ name: 'title', widget: 'string' }] }, - ], - }), - ).getIn(['collections', 0, 'i18n']), + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { folder: 'foo', i18n: false, fields: [{ name: 'title', widget: 'string' }] }, + ], + }).collections[0].i18n, ).toBeUndefined(); }); it('should merge root i18n on collection when collection i18n is set to an object', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - default_locale: 'en', + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + default_locale: 'en', + }, + collections: [ + { + folder: 'foo', + i18n: { locales: ['en', 'fr'], default_locale: 'fr' }, + fields: [{ name: 'title', widget: 'string' }], }, - collections: [ - { - folder: 'foo', - i18n: { locales: ['en', 'fr'], default_locale: 'fr' }, - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ) - .getIn(['collections', 0, 'i18n']) - .toJS(), + ], + }).collections[0].i18n, ).toEqual({ structure: 'multiple_folders', locales: ['en', 'fr'], default_locale: 'fr' }); }); it('should throw when i18n structure is not single_file on files collection', () => { expect(() => - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + files: [ + { + name: 'file', + file: 'file', + i18n: true, + fields: [{ name: 'title', widget: 'string', i18n: true }], + }, + ], + i18n: true, }, - collections: [ - { - files: [ - { - name: 'file', - file: 'file', - i18n: true, - fields: [{ name: 'title', widget: 'string', i18n: true }], - }, - ], - i18n: true, - }, - ], - }), - ), + ], + }), ).toThrow('i18n configuration for files collections is limited to single_file structure'); }); it('should throw when i18n structure is set to multiple_folders and contains a single file collection', () => { expect(() => - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + files: [ + { name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] }, + ], + i18n: true, }, - collections: [ - { - files: [ - { name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] }, - ], - i18n: true, - }, - ], - }), - ), + ], + }), ).toThrow('i18n configuration for files collections is limited to single_file structure'); }); it('should throw when i18n structure is set to multiple_files and contains a single file collection', () => { expect(() => - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_files', - locales: ['en', 'de'], + applyDefaults({ + i18n: { + structure: 'multiple_files', + locales: ['en', 'de'], + }, + collections: [ + { + files: [ + { name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] }, + ], + i18n: true, }, - collections: [ - { - files: [ - { name: 'file', file: 'file', fields: [{ name: 'title', widget: 'string' }] }, - ], - i18n: true, - }, - ], - }), - ), + ], + }), ).toThrow('i18n configuration for files collections is limited to single_file structure'); }); it('should set i18n value to translate on field when i18n=true for field in files collection', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - }, - collections: [ - { - files: [ - { - name: 'file', - file: 'file', - i18n: true, - fields: [{ name: 'title', widget: 'string', i18n: true }], - }, - ], - i18n: { - structure: 'single_file', + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + files: [ + { + name: 'file', + file: 'file', + i18n: true, + fields: [{ name: 'title', widget: 'string', i18n: true }], }, + ], + i18n: { + structure: 'single_file', }, - ], - }), - ).getIn(['collections', 0, 'files', 0, 'fields', 0, 'i18n']), + }, + ], + }).collections[0].files[0].fields[0].i18n, ).toEqual('translate'); }); it('should set i18n value to translate on field when i18n=true for field', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + folder: 'foo', + i18n: true, + fields: [{ name: 'title', widget: 'string', i18n: true }], }, - collections: [ - { - folder: 'foo', - i18n: true, - fields: [{ name: 'title', widget: 'string', i18n: true }], - }, - ], - }), - ).getIn(['collections', 0, 'fields', 0, 'i18n']), + ], + }).collections[0].fields[0].i18n, ).toEqual('translate'); }); it('should set i18n value to none on field when i18n=false for field', () => { expect( - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + folder: 'foo', + i18n: true, + fields: [{ name: 'title', widget: 'string', i18n: false }], }, - collections: [ - { - folder: 'foo', - i18n: true, - fields: [{ name: 'title', widget: 'string', i18n: false }], - }, - ], - }), - ).getIn(['collections', 0, 'fields', 0, 'i18n']), + ], + }).collections[0].fields[0].i18n, ).toEqual('none'); }); it('should throw is default locale is missing from root i18n config', () => { expect(() => - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - default_locale: 'fr', + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + default_locale: 'fr', + }, + collections: [ + { + folder: 'foo', + fields: [{ name: 'title', widget: 'string' }], }, - collections: [ - { - folder: 'foo', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }), - ), + ], + }), ).toThrow("i18n locales 'en, de' are missing the default locale fr"); }); it('should throw is default locale is missing from collection i18n config', () => { expect(() => - applyDefaults( - fromJS({ - i18n: { - structure: 'multiple_folders', - locales: ['en', 'de'], - }, - collections: [ - { - folder: 'foo', - i18n: { - default_locale: 'fr', - }, - fields: [{ name: 'title', widget: 'string' }], + applyDefaults({ + i18n: { + structure: 'multiple_folders', + locales: ['en', 'de'], + }, + collections: [ + { + folder: 'foo', + i18n: { + default_locale: 'fr', }, - ], - }), - ), + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }), ).toThrow("i18n locales 'en, de' are missing the default locale fr"); }); }); diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js index f38545ab..534d4c1c 100644 --- a/packages/netlify-cms-core/src/actions/config.js +++ b/packages/netlify-cms-core/src/actions/config.js @@ -1,13 +1,15 @@ import yaml from 'yaml'; -import { Map, fromJS } from 'immutable'; +import { fromJS } from 'immutable'; import deepmerge from 'deepmerge'; +import { produce } from 'immer'; import { trimStart, trim, get, isPlainObject, isEmpty } from 'lodash'; import { SIMPLE as SIMPLE_PUBLISH_MODE } from '../constants/publishModes'; import { validateConfig } from '../constants/configSchema'; -import { selectDefaultSortableFields, traverseFields } from '../reducers/collections'; +import { selectDefaultSortableFields } from '../reducers/collections'; import { getIntegrations, selectIntegration } from '../reducers/integrations'; import { resolveBackend } from '../backend'; import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n'; +import { FILES, FOLDER } from '../constants/collectionTypes'; export const CONFIG_REQUEST = 'CONFIG_REQUEST'; export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; @@ -40,11 +42,11 @@ function getConfigUrl() { return 'config.yml'; } -function setDefaultPublicFolder(map) { - if (map.has('media_folder') && !map.has('public_folder')) { - map = map.set('public_folder', map.get('media_folder')); +function setDefaultPublicFolderForField(field) { + if ('media_folder' in field && !field.public_folder) { + return { ...field, public_folder: field.media_folder }; } - return map; + return field; } // Mapping between existing camelCase and its snake_case counterpart @@ -74,57 +76,42 @@ function setSnakeCaseConfig(field) { } function setI18nField(field) { - if (field.get(I18N) === true) { - field = field.set(I18N, I18N_FIELD.TRANSLATE); - } else if (field.get(I18N) === false || !field.has(I18N)) { - field = field.set(I18N, I18N_FIELD.NONE); + if (field[I18N] === true) { + return { ...field, [I18N]: I18N_FIELD.TRANSLATE }; + } else if (field[I18N] === false || !field[I18N]) { + return { ...field, [I18N]: I18N_FIELD.NONE }; } return field; } -function setI18nDefaults(defaultI18n, collectionOrFile) { - if (defaultI18n && collectionOrFile.has(I18N)) { - const collectionOrFileI18n = collectionOrFile.get(I18N); - if (collectionOrFileI18n === true) { - collectionOrFile = collectionOrFile.set(I18N, defaultI18n); - } else if (collectionOrFileI18n === false) { - collectionOrFile = collectionOrFile.delete(I18N); - } else { - const locales = collectionOrFileI18n.get('locales', defaultI18n.get('locales')); - const defaultLocale = collectionOrFileI18n.get( - 'default_locale', - collectionOrFileI18n.has('locales') ? locales.first() : defaultI18n.get('default_locale'), - ); - collectionOrFile = collectionOrFile.set(I18N, defaultI18n.merge(collectionOrFileI18n)); - collectionOrFile = collectionOrFile.setIn([I18N, 'locales'], locales); - collectionOrFile = collectionOrFile.setIn([I18N, 'default_locale'], defaultLocale); - - throwOnMissingDefaultLocale(collectionOrFile.get(I18N)); - } - - if (collectionOrFileI18n !== false) { - // set default values for i18n fields - if (collectionOrFile.has('fields')) { - collectionOrFile = collectionOrFile.set( - 'fields', - traverseFields(collectionOrFile.get('fields'), setI18nField), - ); - } - } +function getI18nDefaults(collectionOrFileI18n, defaultI18n) { + if (typeof collectionOrFileI18n === 'boolean') { + return defaultI18n; } else { - collectionOrFile = collectionOrFile.delete(I18N); - if (collectionOrFile.has('fields')) { - collectionOrFile = collectionOrFile.set( - 'fields', - traverseFields(collectionOrFile.get('fields'), field => field.delete(I18N)), - ); - } + const locales = collectionOrFileI18n.locales || defaultI18n.locales; + const defaultLocale = collectionOrFileI18n.default_locale || locales[0]; + const mergedI18n = deepmerge(defaultI18n, collectionOrFileI18n); + mergedI18n.locales = locales; + mergedI18n.default_locale = defaultLocale; + throwOnMissingDefaultLocale(mergedI18n); + return mergedI18n; + } +} + +function setI18nDefaultsForFields(collectionOrFileFields, hasI18n) { + if (hasI18n) { + return traverseFieldsJS(collectionOrFileFields, setI18nField); + } else { + return traverseFieldsJS(collectionOrFileFields, field => { + const newField = { ...field }; + delete newField[I18N]; + return newField; + }); } - return collectionOrFile; } function throwOnInvalidFileCollectionStructure(i18n) { - if (i18n && i18n.get('structure') !== I18N_STRUCTURE.SINGLE_FILE) { + if (i18n && i18n.structure !== I18N_STRUCTURE.SINGLE_FILE) { throw new Error( `i18n configuration for files collections is limited to ${I18N_STRUCTURE.SINGLE_FILE} structure`, ); @@ -132,35 +119,19 @@ function throwOnInvalidFileCollectionStructure(i18n) { } function throwOnMissingDefaultLocale(i18n) { - if (i18n && !i18n.get('locales').includes(i18n.get('default_locale'))) { + if (i18n && i18n.default_locale && !i18n.locales.includes(i18n.default_locale)) { throw new Error( - `i18n locales '${i18n.get('locales').join(', ')}' are missing the default locale ${i18n.get( - 'default_locale', - )}`, + `i18n locales '${i18n.locales.join(', ')}' are missing the default locale ${ + i18n.default_locale + }`, ); } } -function setViewPatternsDefaults(key, collection) { - if (!collection.has(key)) { - collection = collection.set(key, fromJS([])); - } else { - collection = collection.set( - key, - collection.get(key).map(v => v.set('id', `${v.get('field')}__${v.get('pattern')}`)), - ); - } - - return collection; -} - -const defaults = { - publish_mode: SIMPLE_PUBLISH_MODE, -}; - function hasIntegration(config, collection) { - const integrations = getIntegrations(config); - const integration = selectIntegration(integrations, collection.get('name'), 'listEntries'); + // TODO remove fromJS when Immutable is removed from the integrations state slice + const integrations = getIntegrations(fromJS(config)); + const integration = selectIntegration(integrations, collection.name, 'listEntries'); return !!integration; } @@ -199,121 +170,164 @@ export function normalizeConfig(config) { return { ...config, collections: normalizedCollections }; } -export function applyDefaults(config) { - return Map(defaults) - .mergeDeep(config) - .withMutations(map => { - // Use `site_url` as default `display_url`. - if (!map.get('display_url') && map.get('site_url')) { - map.set('display_url', map.get('site_url')); +export function applyDefaults(originalConfig) { + return produce(originalConfig, config => { + config.publish_mode = config.publish_mode || SIMPLE_PUBLISH_MODE; + config.slug = config.slug || {}; + config.collections = config.collections || []; + + // Use `site_url` as default `display_url`. + if (!config.display_url && config.site_url) { + config.display_url = config.site_url; + } + + // Use media_folder as default public_folder. + const defaultPublicFolder = `/${trimStart(config.media_folder, '/')}`; + if (!('public_folder' in config)) { + config.public_folder = defaultPublicFolder; + } + + // default values for the slug config + if (!('encoding' in config.slug)) { + config.slug.encoding = 'unicode'; + } + + if (!('clean_accents' in config.slug)) { + config.slug.clean_accents = false; + } + + if (!('sanitize_replacement' in config.slug)) { + config.slug.sanitize_replacement = '-'; + } + + const i18n = config[I18N]; + const hasI18n = Boolean(i18n); + if (hasI18n) { + i18n.default_locale = i18n.default_locale || i18n.locales[0]; + } + + throwOnMissingDefaultLocale(i18n); + + const backend = resolveBackend(config); + + for (const collection of config.collections) { + if (!('publish' in collection)) { + collection.publish = true; } - // Use media_folder as default public_folder. - const defaultPublicFolder = `/${trimStart(map.get('media_folder'), '/')}`; - if (!map.has('public_folder')) { - map.set('public_folder', defaultPublicFolder); + const collectionHasI18n = Boolean(collection[I18N]); + if (hasI18n && collectionHasI18n) { + collection[I18N] = getI18nDefaults(collection[I18N], i18n); + } else { + delete collection[I18N]; } - // default values for the slug config - if (!map.getIn(['slug', 'encoding'])) { - map.setIn(['slug', 'encoding'], 'unicode'); + if (collection.fields) { + collection.fields = setI18nDefaultsForFields(collection.fields, collectionHasI18n); } - if (!map.getIn(['slug', 'clean_accents'])) { - map.setIn(['slug', 'clean_accents'], false); + const { folder, files, view_filters, view_groups, meta } = collection; + + if (folder) { + collection.type = FOLDER; + + if (collection.path && !collection.media_folder) { + // default value for media folder when using the path config + collection.media_folder = ''; + } + + if ('media_folder' in collection && !collection.public_folder) { + collection.public_folder = collection.media_folder; + } + + if (collection.fields) { + collection.fields = traverseFieldsJS(collection.fields, setDefaultPublicFolderForField); + } + + collection.folder = trim(folder, '/'); + + if (meta && meta.path) { + const metaField = { + name: 'path', + meta: true, + required: true, + ...meta.path, + }; + collection.fields = [metaField, ...(collection.fields || [])]; + } } - if (!map.getIn(['slug', 'sanitize_replacement'])) { - map.setIn(['slug', 'sanitize_replacement'], '-'); - } + if (files) { + collection.type = FILES; - let i18n = config.get(I18N); - i18n = i18n?.set('default_locale', i18n.get('default_locale', i18n.get('locales').first())); - throwOnMissingDefaultLocale(i18n); + // after we invoked setI18nDefaults, + // i18n property can't be boolean anymore + const collectionI18n = collection[I18N]; + throwOnInvalidFileCollectionStructure(collectionI18n); - // Strip leading slash from collection folders and files - map.set( - 'collections', - map.get('collections').map(collection => { - if (!collection.has('publish')) { - collection = collection.set('publish', true); + delete collection.nested; + delete collection.meta; + + for (const file of files) { + file.file = trimStart(file.file, '/'); + + if ('media_folder' in file && !file.public_folder) { + file.public_folder = file.media_folder; } - collection = setI18nDefaults(i18n, collection); + if (file.fields) { + file.fields = traverseFieldsJS(file.fields, setDefaultPublicFolderForField); + } - const folder = collection.get('folder'); - if (folder) { - if (collection.has('path') && !collection.has('media_folder')) { - // default value for media folder when using the path config - collection = collection.set('media_folder', ''); - } - collection = setDefaultPublicFolder(collection); - collection = collection.set( - 'fields', - traverseFields(collection.get('fields'), setDefaultPublicFolder), - ); - collection = collection.set('folder', trim(folder, '/')); - if (collection.has('meta')) { - const fields = collection.get('fields'); - const metaFields = []; - collection.get('meta').forEach((value, key) => { - const field = value.withMutations(map => { - map.set('name', key); - map.set('meta', true); - map.set('required', true); - }); - metaFields.push(field); - }); - collection = collection.set('fields', fromJS([]).concat(metaFields, fields)); - } else { - collection = collection.set('meta', Map()); + const fileHasI18n = Boolean(file[I18N]); + + if (fileHasI18n) { + if (collectionI18n) { + file[I18N] = getI18nDefaults(file[I18N], collectionI18n); } + } else { + delete file[I18N]; } - const files = collection.get('files'); - if (files) { - const collectionI18n = collection.get(I18N); - throwOnInvalidFileCollectionStructure(collectionI18n); - - collection = collection.delete('nested'); - collection = collection.delete('meta'); - collection = collection.set( - 'files', - files.map(file => { - file = file.set('file', trimStart(file.get('file'), '/')); - file = setDefaultPublicFolder(file); - file = file.set( - 'fields', - traverseFields(file.get('fields'), setDefaultPublicFolder), - ); - file = setI18nDefaults(collectionI18n, file); - throwOnInvalidFileCollectionStructure(file.get(I18N)); - return file; - }), - ); + if (file.fields) { + file.fields = setI18nDefaultsForFields(file.fields, fileHasI18n); } - if (!collection.has('sortable_fields')) { - const backend = resolveBackend(config); - const defaultSortable = selectDefaultSortableFields( - collection, - backend, - hasIntegration(map, collection), - ); - collection = collection.set('sortable_fields', fromJS(defaultSortable)); - } + // after we invoked setI18nDefaults, + // i18n property can't be boolean anymore + const fileI18n = file[I18N]; + throwOnInvalidFileCollectionStructure(fileI18n); + } + } - collection = setViewPatternsDefaults('view_filters', collection); - collection = setViewPatternsDefaults('view_groups', collection); + if (!collection.sortable_fields) { + collection.sortable_fields = selectDefaultSortableFields( + // TODO remove fromJS when Immutable is removed from the collections state slice + fromJS(collection), + backend, + hasIntegration(config, collection), + ); + } - if (map.hasIn(['editor', 'preview']) && !collection.has('editor')) { - collection = collection.setIn(['editor', 'preview'], map.getIn(['editor', 'preview'])); - } + collection.view_filters = (view_filters || []).map(filter => { + return { + ...filter, + id: `${filter.field}__${filter.pattern}`, + }; + }); - return collection; - }), - ); - }); + collection.view_groups = (view_groups || []).map(group => { + return { + ...group, + id: `${group.field}__${group.pattern}`, + }; + }); + + if (config.editor && !collection.editor) { + collection.editor = { preview: config.editor.preview }; + } + } + }); } export function parseConfig(data) { @@ -446,9 +460,9 @@ export function loadConfig(manualConfig = {}, onLoad) { const withLocalBackend = await handleLocalBackend(mergedConfig); const normalizedConfig = normalizeConfig(withLocalBackend); - const config = applyDefaults(fromJS(normalizedConfig)); + const config = applyDefaults(normalizedConfig); - dispatch(configLoaded(config)); + dispatch(configLoaded(fromJS(config))); if (typeof onLoad === 'function') { onLoad();