refactor: remove immutable from 'config' state slice (#4960)

This commit is contained in:
Vladislav Shkodin
2021-03-11 12:08:46 +02:00
committed by GitHub
parent 133689247b
commit 6623740a8c
39 changed files with 488 additions and 451 deletions

View File

@ -1,5 +1,5 @@
import { OrderedMap, fromJS } from 'immutable';
import { configLoaded } from 'Actions/config';
import { fromJS, Map } from 'immutable';
import { configLoaded } from '../../actions/config';
import collections, {
selectAllowDeletion,
selectEntryPath,
@ -11,39 +11,51 @@ import collections, {
selectField,
updateFieldByKey,
} from '../collections';
import { FILES, FOLDER } from 'Constants/collectionTypes';
import { FILES, FOLDER } from '../../constants/collectionTypes';
describe('collections', () => {
it('should handle an empty state', () => {
expect(collections(undefined, {})).toEqual(null);
expect(collections(undefined, {})).toEqual(Map());
});
it('should load the collections from the config', () => {
expect(
collections(
undefined,
configLoaded(
fromJS({
collections: [
{
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
},
],
}),
),
),
).toEqual(
OrderedMap({
posts: fromJS({
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
type: FOLDER,
configLoaded({
collections: [
{
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
},
],
}),
).toJS(),
).toEqual({
posts: {
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
},
});
});
it('should maintain config collections order', () => {
const collectionsData = new Array(1000).fill(0).map((_, index) => ({
name: `collection_${index}`,
folder: `collection_${index}`,
fields: [{ name: 'title', widget: 'string' }],
}));
const newState = collections(
undefined,
configLoaded({
collections: collectionsData,
}),
);
const keyArray = newState.keySeq().toArray();
expect(keyArray).toEqual(collectionsData.map(({ name }) => name));
});
describe('selectAllowDeletions', () => {
@ -234,11 +246,11 @@ describe('collections', () => {
sanitize_replacement: '-',
};
const config = fromJS({ slug, media_folder: '/static/img' });
const config = { slug, media_folder: '/static/img' };
it('should return fields and collection folders', () => {
expect(
selectMediaFolders(
{ config },
config,
fromJS({
folder: 'posts',
media_folder: '{{media_folder}}/general/',
@ -265,7 +277,7 @@ describe('collections', () => {
it('should return fields, file and collection folders', () => {
expect(
selectMediaFolders(
{ config },
config,
fromJS({
media_folder: '{{media_folder}}/general/',
files: [

View File

@ -1,29 +1,38 @@
import { Map } from 'immutable';
import { configLoaded, configLoading, configFailed } from 'Actions/config';
import config, { selectLocale } from 'Reducers/config';
import { configLoaded, configLoading, configFailed } from '../../actions/config';
import config, { selectLocale } from '../config';
describe('config', () => {
it('should handle an empty state', () => {
expect(config(undefined, {})).toEqual(Map({ isFetching: true }));
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore config reducer doesn't accept empty action
expect(config(undefined, {})).toEqual({ isFetching: true });
});
it('should handle an update', () => {
expect(config(Map({ a: 'b', c: 'd' }), configLoaded(Map({ a: 'changed', e: 'new' })))).toEqual(
Map({ a: 'changed', e: 'new' }),
);
expect(
config({ isFetching: true }, configLoaded({ locale: 'fr', backend: { name: 'proxy' } })),
).toEqual({
locale: 'fr',
backend: { name: 'proxy' },
isFetching: false,
error: undefined,
});
});
it('should mark the config as loading', () => {
expect(config(undefined, configLoading())).toEqual(Map({ isFetching: true }));
expect(config({ isFetching: false }, configLoading())).toEqual({ isFetching: true });
});
it('should handle an error', () => {
expect(config(Map(), configFailed(new Error('Config could not be loaded')))).toEqual(
Map({ error: 'Error: Config could not be loaded' }),
);
expect(
config({ isFetching: true }, configFailed(new Error('Config could not be loaded'))),
).toEqual({
error: 'Error: Config could not be loaded',
isFetching: false,
});
});
it('should default to "en" locale', () => {
expect(selectLocale(Map())).toEqual('en');
expect(selectLocale({})).toEqual('en');
});
});

View File

@ -1,5 +1,5 @@
import { OrderedMap, fromJS } from 'immutable';
import * as actions from 'Actions/entries';
import * as actions from '../../actions/entries';
import reducer, {
selectMediaFolder,
selectMediaFilePath,
@ -76,7 +76,7 @@ describe('entries', () => {
it("should return global media folder when collection doesn't specify media_folder", () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts' }),
undefined,
undefined,
@ -87,7 +87,7 @@ describe('entries', () => {
it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined,
undefined,
@ -98,7 +98,7 @@ describe('entries', () => {
it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
fromJS({ path: 'posts/title/index.md' }),
undefined,
@ -121,7 +121,7 @@ describe('entries', () => {
const field = fromJS({ media_folder: '' });
expect(
selectMediaFolder(
fromJS({ media_folder: '/static/img' }),
{ media_folder: '/static/img' },
fromJS({
name: 'other',
folder: 'other',
@ -137,7 +137,7 @@ describe('entries', () => {
it('should return collection absolute media folder without leading slash', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: '/static/Images' }),
{ media_folder: '/static/Images' },
fromJS({
name: 'getting-started',
folder: 'src/docs/getting-started',
@ -169,7 +169,7 @@ describe('entries', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media', slug: slugConfig }),
{ media_folder: 'static/media', slug: slugConfig },
collection,
entry,
undefined,
@ -196,7 +196,7 @@ describe('entries', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: '/static/images', slug: slugConfig }),
{ media_folder: '/static/images', slug: slugConfig },
collection,
entry,
undefined,
@ -229,7 +229,7 @@ describe('entries', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media', slug: slugConfig }),
{ media_folder: 'static/media', slug: slugConfig },
collection,
entry,
collection.get('fields').get(0),
@ -258,7 +258,7 @@ describe('entries', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: '/static/img/', slug: slugConfig }),
{ media_folder: '/static/img/', slug: slugConfig },
collection,
entry,
undefined,
@ -267,7 +267,7 @@ describe('entries', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/img/', slug: slugConfig }),
{ media_folder: 'static/img/', slug: slugConfig },
collection,
entry,
undefined,
@ -278,7 +278,7 @@ describe('entries', () => {
it('should handle file media_folder', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }),
fromJS({ path: 'posts/title/index.md', slug: 'index' }),
undefined,
@ -302,7 +302,7 @@ describe('entries', () => {
});
const args = [
fromJS({ media_folder: '/static/img' }),
{ media_folder: '/static/img' },
fromJS({
name: 'general',
media_folder: '{{media_folder}}/general/',
@ -345,7 +345,7 @@ describe('entries', () => {
it('should resolve path from global media folder for collection with no media folder', () => {
expect(
selectMediaFilePath(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts' }),
undefined,
'image.png',
@ -357,7 +357,7 @@ describe('entries', () => {
it('should resolve path from collection media folder for collection with media folder', () => {
expect(
selectMediaFilePath(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined,
'image.png',
@ -369,7 +369,7 @@ describe('entries', () => {
it('should handle relative media_folder', () => {
expect(
selectMediaFilePath(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
fromJS({ path: 'posts/title/index.md' }),
'image.png',
@ -382,7 +382,7 @@ describe('entries', () => {
const field = fromJS({ media_folder: '../../static/media/' });
expect(
selectMediaFilePath(
fromJS({ media_folder: 'static/media' }),
{ media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', fields: [field] }),
fromJS({ path: 'posts/title/index.md' }),
'image.png',
@ -402,7 +402,7 @@ describe('entries', () => {
it('should resolve path from public folder for collection with no media folder', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }),
{ public_folder: '/media' },
null,
'/media/image.png',
undefined,
@ -414,7 +414,7 @@ describe('entries', () => {
it('should resolve path from collection public folder for collection with public folder', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }),
{ public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '' }),
'image.png',
undefined,
@ -426,7 +426,7 @@ describe('entries', () => {
it('should handle relative public_folder', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }),
{ public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
'image.png',
undefined,
@ -455,7 +455,7 @@ describe('entries', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media', slug: slugConfig }),
{ public_folder: 'static/media', slug: slugConfig },
collection,
'image.png',
entry,
@ -489,7 +489,7 @@ describe('entries', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media', slug: slugConfig }),
{ public_folder: 'static/media', slug: slugConfig },
collection,
'image.png',
entry,
@ -523,7 +523,7 @@ describe('entries', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/', slug: slugConfig }),
{ public_folder: 'static/media/', slug: slugConfig },
collection,
'image.png',
entry,
@ -551,7 +551,7 @@ describe('entries', () => {
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/' }),
{ public_folder: 'static/media/' },
collection,
'image.png',
entry,

View File

@ -1,13 +1,13 @@
import { fromJS } from 'immutable';
import integrations from '../integrations';
import { CONFIG_SUCCESS } from '../../actions/config';
import { CONFIG_SUCCESS, ConfigAction } from '../../actions/config';
import { FOLDER } from '../../constants/collectionTypes';
describe('integrations', () => {
it('should return default state when no integrations', () => {
const result = integrations(null, {
type: CONFIG_SUCCESS,
payload: fromJS({ integrations: [] }),
});
payload: { integrations: [] },
} as ConfigAction);
expect(result && result.toJS()).toEqual({
providers: {},
hooks: {},
@ -17,7 +17,7 @@ describe('integrations', () => {
it('should return hooks and providers map when has integrations', () => {
const result = integrations(null, {
type: CONFIG_SUCCESS,
payload: fromJS({
payload: {
integrations: [
{
hooks: ['listEntries'],
@ -39,9 +39,13 @@ describe('integrations', () => {
getSignedFormURL: 'https://asset.store.com/signedUrl',
},
],
collections: [{ name: 'posts' }, { name: 'pages' }, { name: 'faq' }],
}),
});
collections: [
{ name: 'posts', label: 'Posts', type: FOLDER },
{ name: 'pages', label: 'Pages', type: FOLDER },
{ name: 'faq', label: 'FAQ', type: FOLDER },
],
},
} as ConfigAction);
expect(result && result.toJS()).toEqual({
providers: {

View File

@ -1,19 +1,20 @@
import { List, Set } from 'immutable';
import { List, Set, fromJS, OrderedMap } from 'immutable';
import { get, escapeRegExp } from 'lodash';
import consoleError from '../lib/consoleError';
import { CONFIG_SUCCESS } from '../actions/config';
import { CONFIG_SUCCESS, ConfigAction } from '../actions/config';
import { FILES, FOLDER } from '../constants/collectionTypes';
import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
import { formatExtensions } from '../formats/formats';
import {
CollectionsAction,
Collection,
Collections,
CollectionFiles,
EntryField,
State,
EntryMap,
ViewFilter,
ViewGroup,
CmsConfig,
} from '../types/redux';
import { selectMediaFolder } from './entries';
import { stringTemplate } from 'netlify-cms-lib-widgets';
@ -22,29 +23,17 @@ import { Backend } from '../backend';
const { keyToPathArray } = stringTemplate;
function collections(state = null, action: CollectionsAction) {
const defaultState: Collections = fromJS({});
function collections(state = defaultState, action: ConfigAction) {
switch (action.type) {
case CONFIG_SUCCESS: {
const configCollections = action.payload
? action.payload.get('collections')
: List<Collection>();
return (
configCollections
.toOrderedMap()
.map(item => {
const collection = item as Collection;
if (collection.has('folder')) {
return collection.set('type', FOLDER);
}
if (collection.has('files')) {
return collection.set('type', FILES);
}
})
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
.mapKeys((key: string, collection: Collection) => collection.get('name'))
);
const collections = action.payload.collections;
let newState = OrderedMap({});
collections.forEach(collection => {
newState = newState.set(collection.name, fromJS(collection));
});
return newState;
}
default:
return state;
@ -165,19 +154,19 @@ export function selectFieldsWithMediaFolders(collection: Collection, slug: strin
return [];
}
export function selectMediaFolders(state: State, collection: Collection, entry: EntryMap) {
export function selectMediaFolders(config: CmsConfig, collection: Collection, entry: EntryMap) {
const fields = selectFieldsWithMediaFolders(collection, entry.get('slug'));
const folders = fields.map(f => selectMediaFolder(state.config, collection, entry, f));
const folders = fields.map(f => selectMediaFolder(config, collection, entry, f));
if (collection.has('files')) {
const file = getFileFromSlug(collection, entry.get('slug'));
if (file) {
folders.unshift(selectMediaFolder(state.config, collection, entry, undefined));
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
}
}
if (collection.has('media_folder')) {
// stop evaluating media folders at collection level
collection = collection.delete('files');
folders.unshift(selectMediaFolder(state.config, collection, entry, undefined));
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
}
return Set(folders).toArray();
@ -317,10 +306,10 @@ export function updateFieldByKey(
export function selectIdentifier(collection: Collection) {
const identifier = collection.get('identifier_field');
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS;
const fieldNames = getFieldsNames(collection.get('fields', List<EntryField>()).toArray());
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : [...IDENTIFIER_FIELDS];
const fieldNames = getFieldsNames(collection.get('fields', List()).toArray());
return identifierFields.find(id =>
fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()),
fieldNames.find(name => name.toLowerCase().trim() === id.toLowerCase().trim()),
);
}
@ -390,9 +379,6 @@ export function selectEntryCollectionTitle(collection: Collection, entry: EntryM
return titleField && entryData.getIn(keyToPathArray(titleField));
}
export const COMMIT_AUTHOR = 'commit_author';
export const COMMIT_DATE = 'commit_date';
export function selectDefaultSortableFields(
collection: Collection,
backend: Backend,

View File

@ -1,37 +1,35 @@
import { Map } from 'immutable';
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
import { Config, ConfigAction } from '../types/redux';
import { produce } from 'immer';
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE, ConfigAction } from '../actions/config';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
import { CmsConfig } from '../types/redux';
const defaultState: Map<string, boolean | string> = Map({ isFetching: true });
const defaultState = {
isFetching: true,
};
function config(state = defaultState, action: ConfigAction) {
const config = produce((state: CmsConfig, action: ConfigAction) => {
switch (action.type) {
case CONFIG_REQUEST:
return state.set('isFetching', true);
state.isFetching = true;
break;
case CONFIG_SUCCESS:
/**
* The loadConfig action merges any existing config into the loaded config
* before firing this action (so the resulting config can be validated),
* so we don't have to merge it here.
*/
return action.payload;
return {
...action.payload,
isFetching: false,
error: undefined,
};
case CONFIG_FAILURE:
return state.withMutations(s => {
s.delete('isFetching');
s.set('error', action.payload.toString());
});
default:
return state;
state.isFetching = false;
state.error = action.payload.toString();
}
}, defaultState);
export function selectLocale(state: CmsConfig) {
return state.locale || 'en';
}
export function selectLocale(state: Config) {
return state.get('locale', 'en') as string;
}
export function selectUseWorkflow(state: Config) {
return state.get('publish_mode') === EDITORIAL_WORKFLOW;
export function selectUseWorkflow(state: CmsConfig) {
return state.publish_mode === EDITORIAL_WORKFLOW;
}
export default config;

View File

@ -24,7 +24,7 @@ import { EditorialWorkflowAction, EditorialWorkflow, Entities } from '../types/r
function unpublishedEntries(state = Map(), action: EditorialWorkflowAction) {
switch (action.type) {
case CONFIG_SUCCESS: {
const publishMode = action.payload && action.payload.get('publish_mode');
const publishMode = action.payload && action.payload.publish_mode;
if (publishMode === EDITORIAL_WORKFLOW) {
// Editorial workflow state is explicitly initiated after the config.
return Map({ entities: Map(), pages: Map() });

View File

@ -27,7 +27,7 @@ import {
EntriesSuccessPayload,
EntryObject,
Entries,
Config,
CmsConfig,
Collection,
EntryFailurePayload,
EntryDeletePayload,
@ -564,7 +564,7 @@ function hasCustomFolder(
function traverseFields(
folderKey: 'media_folder' | 'public_folder',
config: Config,
config: CmsConfig,
collection: Collection,
entryMap: EntryMap | undefined,
field: EntryField,
@ -579,7 +579,7 @@ function traverseFields(
collection,
currentFolder,
folderKey,
config.get('slug'),
config.slug,
);
}
@ -594,7 +594,7 @@ function traverseFields(
collection,
currentFolder,
folderKey,
config.get('slug'),
config.slug,
);
let fieldFolder = null;
if (f.has('fields')) {
@ -638,12 +638,12 @@ function traverseFields(
function evaluateFolder(
folderKey: 'media_folder' | 'public_folder',
config: Config,
config: CmsConfig,
collection: Collection,
entryMap: EntryMap | undefined,
field: EntryField | undefined,
) {
let currentFolder = config.get(folderKey);
let currentFolder = config[folderKey]!;
// add identity template if doesn't exist
if (!collection.has(folderKey)) {
@ -659,7 +659,7 @@ function evaluateFolder(
collection,
currentFolder,
folderKey,
config.get('slug'),
config.slug,
);
let file = getFileField(collection.get('files')!, entryMap?.get('slug'));
@ -676,7 +676,7 @@ function evaluateFolder(
collection,
currentFolder,
folderKey,
config.get('slug'),
config.slug,
);
if (field) {
@ -704,7 +704,7 @@ function evaluateFolder(
collection,
currentFolder,
folderKey,
config.get('slug'),
config.slug,
);
if (field) {
@ -728,13 +728,13 @@ function evaluateFolder(
}
export function selectMediaFolder(
config: Config,
config: CmsConfig,
collection: Collection | null,
entryMap: EntryMap | undefined,
field: EntryField | undefined,
) {
const name = 'media_folder';
let mediaFolder = config.get(name);
let mediaFolder = config[name];
const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);
@ -755,7 +755,7 @@ export function selectMediaFolder(
}
export function selectMediaFilePath(
config: Config,
config: CmsConfig,
collection: Collection | null,
entryMap: EntryMap | undefined,
mediaPath: string,
@ -771,7 +771,7 @@ export function selectMediaFilePath(
}
export function selectMediaFilePublicPath(
config: Config,
config: CmsConfig,
collection: Collection | null,
mediaPath: string,
entryMap: EntryMap | undefined,
@ -782,7 +782,7 @@ export function selectMediaFilePublicPath(
}
const name = 'public_folder';
let publicFolder = config.get(name);
let publicFolder = config[name]!;
const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);

View File

@ -1,14 +1,14 @@
import { fromJS, List } from 'immutable';
import { CONFIG_SUCCESS } from '../actions/config';
import { Integrations, IntegrationsAction, Integration, Config } from '../types/redux';
import { fromJS } from 'immutable';
import { CONFIG_SUCCESS, ConfigAction } from '../actions/config';
import { Integrations, CmsConfig } from '../types/redux';
interface Acc {
providers: Record<string, {}>;
hooks: Record<string, string | Record<string, string>>;
}
export function getIntegrations(config: Config) {
const integrations: Integration[] = config.get('integrations', List()).toJS() || [];
export function getIntegrations(config: CmsConfig) {
const integrations = config.integrations || [];
const newState = integrations.reduce(
(acc, integration) => {
const { hooks, collections, provider, ...providerData } = integration;
@ -20,12 +20,7 @@ export function getIntegrations(config: Config) {
return acc;
}
const integrationCollections =
collections === '*'
? config
.get('collections')
.map(collection => collection!.get('name'))
.toArray()
: (collections as string[]);
collections === '*' ? config.collections.map(collection => collection.name) : collections;
integrationCollections.forEach(collection => {
hooks.forEach(hook => {
acc.hooks[collection]
@ -40,7 +35,9 @@ export function getIntegrations(config: Config) {
return fromJS(newState);
}
function integrations(state = null, action: IntegrationsAction): Integrations | null {
const defaultState = fromJS({ providers: {}, hooks: {} });
function integrations(state = defaultState, action: ConfigAction): Integrations | null {
switch (action.type) {
case CONFIG_SUCCESS: {
return getIntegrations(action.payload);