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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 488 additions and 451 deletions

View File

@ -175,7 +175,7 @@ describe('gitlab backend', () => {
}, },
{ {
backendName: 'gitlab', backendName: 'gitlab',
config: fromJS(config), config,
authStore, authStore,
}, },
); );
@ -401,7 +401,7 @@ describe('gitlab backend', () => {
const entry = await backend.getEntry( const entry = await backend.getEntry(
{ {
config: fromJS({}), config: {},
integrations: fromJS([]), integrations: fromJS([]),
entryDraft: fromJS({}), entryDraft: fromJS({}),
mediaLibrary: fromJS({}), mediaLibrary: fromJS({}),

View File

@ -355,6 +355,14 @@ declare module 'netlify-cms-core' {
cms_label_prefix?: string; cms_label_prefix?: string;
squash_merges?: boolean; squash_merges?: boolean;
proxy_url?: string; proxy_url?: string;
commit_messages?: {
create?: string;
update?: string;
delete?: string;
uploadMedia?: string;
deleteMedia?: string;
openAuthoring?: string;
};
} }
export interface CmsSlug { export interface CmsSlug {
@ -382,6 +390,14 @@ declare module 'netlify-cms-core' {
media_library?: CmsMediaLibrary; media_library?: CmsMediaLibrary;
publish_mode?: CmsPublishMode; publish_mode?: CmsPublishMode;
load_config_file?: boolean; load_config_file?: boolean;
integrations?: {
hooks: string[];
provider: string;
collections?: '*' | string[];
applicationID?: string;
apiKey?: string;
getSignedFormURL?: string;
}[];
slug?: CmsSlug; slug?: CmsSlug;
i18n?: CmsI18nConfig; i18n?: CmsI18nConfig;
local_backend?: boolean | CmsLocalBackend; local_backend?: boolean | CmsLocalBackend;

View File

@ -1,3 +1,4 @@
import { Map, List, fromJS } from 'immutable';
import { import {
resolveBackend, resolveBackend,
Backend, Backend,
@ -5,12 +6,10 @@ import {
expandSearchEntries, expandSearchEntries,
mergeExpandedEntries, mergeExpandedEntries,
} from '../backend'; } from '../backend';
import registry from 'Lib/registry'; import registry from '../lib/registry';
import { FOLDER } from 'Constants/collectionTypes'; import { FOLDER, FILES } from '../constants/collectionTypes';
import { Map, List, fromJS } from 'immutable';
import { FILES } from '../constants/collectionTypes';
jest.mock('Lib/registry'); jest.mock('../lib/registry');
jest.mock('netlify-cms-lib-util'); jest.mock('netlify-cms-lib-util');
jest.mock('../lib/urlHelper'); jest.mock('../lib/urlHelper');
@ -22,13 +21,11 @@ describe('Backend', () => {
registry.getBackend.mockReturnValue({ registry.getBackend.mockReturnValue({
init: jest.fn(), init: jest.fn(),
}); });
backend = resolveBackend( backend = resolveBackend({
Map({ backend: {
backend: Map({ name: 'git-gateway',
name: 'git-gateway', },
}), });
}),
);
}); });
it('filters string values', () => { it('filters string values', () => {
@ -133,9 +130,8 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
const collection = Map({ const collection = Map({
name: 'posts', name: 'posts',
@ -155,9 +151,7 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({}); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
const backend = new Backend(implementation, { config, backendName: 'github' });
const collection = Map({ const collection = Map({
name: 'posts', name: 'posts',
@ -177,9 +171,8 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
const collection = Map({ const collection = Map({
name: 'posts', name: 'posts',
@ -218,9 +211,8 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
const collection = Map({ const collection = Map({
name: 'posts', name: 'posts',
@ -268,9 +260,8 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
backend.entryToRaw = jest.fn().mockReturnValue(''); backend.entryToRaw = jest.fn().mockReturnValue('');
@ -295,9 +286,8 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
backend.entryToRaw = jest.fn().mockReturnValue('content'); backend.entryToRaw = jest.fn().mockReturnValue('content');
@ -334,10 +324,10 @@ describe('Backend', () => {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
persistMedia: jest.fn().mockResolvedValue(persistMediaResult), persistMedia: jest.fn().mockResolvedValue(persistMediaResult),
}; };
const config = Map({}); const config = { backend: { name: 'github' } };
const backend = new Backend(implementation, { config, backendName: config.backend.name });
const user = { login: 'login', name: 'name' }; const user = { login: 'login', name: 'name' };
const backend = new Backend(implementation, { config, backendName: 'github' });
backend.currentUser = jest.fn().mockResolvedValue(user); backend.currentUser = jest.fn().mockResolvedValue(user);
const file = { path: 'static/media/image.png' }; const file = { path: 'static/media/image.png' };
@ -365,7 +355,9 @@ describe('Backend', () => {
.mockResolvedValueOnce('---\ntitle: "Hello World"\n---\n'), .mockResolvedValueOnce('---\ntitle: "Hello World"\n---\n'),
unpublishedEntryMediaFile: jest.fn().mockResolvedValueOnce({ id: '1' }), unpublishedEntryMediaFile: jest.fn().mockResolvedValueOnce({ id: '1' }),
}; };
const config = Map({ media_folder: 'static/images' }); const config = {
media_folder: 'static/images',
};
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config, backendName: 'github' });
@ -412,8 +404,6 @@ describe('Backend', () => {
const { sanitizeSlug } = require('../lib/urlHelper'); const { sanitizeSlug } = require('../lib/urlHelper');
sanitizeSlug.mockReturnValue('some-post-title'); sanitizeSlug.mockReturnValue('some-post-title');
const config = Map({});
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
getEntry: jest.fn(() => Promise.resolve()), getEntry: jest.fn(() => Promise.resolve()),
@ -436,7 +426,7 @@ describe('Backend', () => {
title: 'some post title', title: 'some post title',
}); });
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe( await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
'sub_dir/some-post-title', 'sub_dir/some-post-title',
@ -448,8 +438,6 @@ describe('Backend', () => {
sanitizeSlug.mockReturnValue('some-post-title'); sanitizeSlug.mockReturnValue('some-post-title');
sanitizeChar.mockReturnValue('-'); sanitizeChar.mockReturnValue('-');
const config = Map({});
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
getEntry: jest.fn(), getEntry: jest.fn(),
@ -475,7 +463,7 @@ describe('Backend', () => {
title: 'some post title', title: 'some post title',
}); });
const backend = new Backend(implementation, { config, backendName: 'github' }); const backend = new Backend(implementation, { config: {}, backendName: 'github' });
await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe( await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
'sub_dir/some-post-title-1', 'sub_dir/some-post-title-1',
@ -585,11 +573,10 @@ describe('Backend', () => {
const implementation = { const implementation = {
init: jest.fn(() => implementation), init: jest.fn(() => implementation),
}; };
const config = Map({});
let backend; let backend;
beforeEach(() => { beforeEach(() => {
backend = new Backend(implementation, { config, backendName: 'github' }); backend = new Backend(implementation, { config: {}, backendName: 'github' });
backend.listAllEntries = jest.fn(collection => { backend.listAllEntries = jest.fn(collection => {
if (collection.get('name') === 'posts') { if (collection.get('name') === 'posts') {
return Promise.resolve(posts); return Promise.resolve(posts);

View File

@ -1,5 +1,4 @@
import { stripIndent } from 'common-tags'; import { stripIndent } from 'common-tags';
import { fromJS } from 'immutable';
import { import {
loadConfig, loadConfig,
parseConfig, parseConfig,
@ -932,13 +931,13 @@ describe('config', () => {
expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' }); expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' });
expect(dispatch).toHaveBeenCalledWith({ expect(dispatch).toHaveBeenCalledWith({
type: 'CONFIG_SUCCESS', type: 'CONFIG_SUCCESS',
payload: fromJS({ payload: {
backend: { repo: 'test-repo' }, backend: { repo: 'test-repo' },
collections: [], collections: [],
publish_mode: 'simple', publish_mode: 'simple',
slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' }, slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' },
public_folder: '/', public_folder: '/',
}), },
}); });
}); });
@ -965,13 +964,13 @@ describe('config', () => {
expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' }); expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' });
expect(dispatch).toHaveBeenCalledWith({ expect(dispatch).toHaveBeenCalledWith({
type: 'CONFIG_SUCCESS', type: 'CONFIG_SUCCESS',
payload: fromJS({ payload: {
backend: { repo: 'github' }, backend: { repo: 'github' },
collections: [], collections: [],
publish_mode: 'simple', publish_mode: 'simple',
slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' }, slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' },
public_folder: '/', public_folder: '/',
}), },
}); });
}); });

View File

@ -395,13 +395,13 @@ describe('entries', () => {
describe('validateMetaField', () => { describe('validateMetaField', () => {
const state = { const state = {
config: fromJS({ config: {
slug: { slug: {
encoding: 'unicode', encoding: 'unicode',
clean_accents: false, clean_accents: false,
sanitize_replacement: '-', sanitize_replacement: '-',
}, },
}), },
entries: fromJS([]), entries: fromJS([]),
}; };
const collection = fromJS({ const collection = fromJS({

View File

@ -3,7 +3,7 @@ import thunk from 'redux-thunk';
import { List, Map } from 'immutable'; import { List, Map } from 'immutable';
import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary'; import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary';
jest.mock('coreSrc/backend'); jest.mock('../../backend');
jest.mock('../waitUntil'); jest.mock('../waitUntil');
jest.mock('netlify-cms-lib-util', () => { jest.mock('netlify-cms-lib-util', () => {
const lib = jest.requireActual('netlify-cms-lib-util'); const lib = jest.requireActual('netlify-cms-lib-util');
@ -20,9 +20,9 @@ describe('mediaLibrary', () => {
describe('insertMedia', () => { describe('insertMedia', () => {
it('should return mediaPath as string when string is given', () => { it('should return mediaPath as string when string is given', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
public_folder: '/media', public_folder: '/media',
}), },
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
}), }),
@ -40,9 +40,9 @@ describe('mediaLibrary', () => {
it('should return mediaPath as array of strings when array of strings is given', () => { it('should return mediaPath as array of strings when array of strings is given', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
public_folder: '/media', public_folder: '/media',
}), },
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
}), }),
@ -81,14 +81,14 @@ describe('mediaLibrary', () => {
getBlobSHA.mockReturnValue('000000000000000'); getBlobSHA.mockReturnValue('000000000000000');
const store = mockStore({ const store = mockStore({
config: Map({ config: {
media_folder: 'static/media', media_folder: 'static/media',
slug: Map({ slug: {
encoding: 'unicode', encoding: 'unicode',
clean_accents: false, clean_accents: false,
sanitize_replacement: '-', sanitize_replacement: '-',
}), },
}), },
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
}), }),
@ -132,14 +132,14 @@ describe('mediaLibrary', () => {
it('should persist media when not editing draft', () => { it('should persist media when not editing draft', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
media_folder: 'static/media', media_folder: 'static/media',
slug: Map({ slug: {
encoding: 'unicode', encoding: 'unicode',
clean_accents: false, clean_accents: false,
sanitize_replacement: '-', sanitize_replacement: '-',
}), },
}), },
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
}), }),
@ -186,14 +186,14 @@ describe('mediaLibrary', () => {
it('should sanitize media name if needed when persisting', () => { it('should sanitize media name if needed when persisting', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
media_folder: 'static/media', media_folder: 'static/media',
slug: Map({ slug: {
encoding: 'ascii', encoding: 'ascii',
clean_accents: true, clean_accents: true,
sanitize_replacement: '_', sanitize_replacement: '_',
}), },
}), },
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
}), }),
@ -247,9 +247,9 @@ describe('mediaLibrary', () => {
it('should delete non draft file', () => { it('should delete non draft file', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
publish_mode: 'editorial_workflow', publish_mode: 'editorial_workflow',
}), },
collections: Map(), collections: Map(),
integrations: Map(), integrations: Map(),
mediaLibrary: Map({ mediaLibrary: Map({
@ -290,9 +290,9 @@ describe('mediaLibrary', () => {
it('should not delete a draft file', () => { it('should not delete a draft file', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: {
publish_mode: 'editorial_workflow', publish_mode: 'editorial_workflow',
}), },
collections: Map(), collections: Map(),
integrations: Map(), integrations: Map(),
mediaLibrary: Map({ mediaLibrary: Map({

View File

@ -240,8 +240,7 @@ export function applyDefaults(originalConfig: CmsConfig) {
throwOnMissingDefaultLocale(i18n); throwOnMissingDefaultLocale(i18n);
// TODO remove fromJS when Immutable is removed from backend const backend = resolveBackend(config);
const backend = resolveBackend(fromJS(config));
for (const collection of config.collections) { for (const collection of config.collections) {
if (!('publish' in collection)) { if (!('publish' in collection)) {
@ -399,13 +398,13 @@ export function configLoaded(config: CmsConfig) {
return { return {
type: CONFIG_SUCCESS, type: CONFIG_SUCCESS,
payload: config, payload: config,
}; } as const;
} }
export function configLoading() { export function configLoading() {
return { return {
type: CONFIG_REQUEST, type: CONFIG_REQUEST,
}; } as const;
} }
export function configFailed(err: Error) { export function configFailed(err: Error) {
@ -413,7 +412,7 @@ export function configFailed(err: Error) {
type: CONFIG_FAILURE, type: CONFIG_FAILURE,
error: 'Error loading config', error: 'Error loading config',
payload: err, payload: err,
}; } as const;
} }
export async function detectProxyServer(localBackend?: boolean | CmsLocalBackend) { export async function detectProxyServer(localBackend?: boolean | CmsLocalBackend) {
@ -495,7 +494,7 @@ export async function handleLocalBackend(originalConfig: CmsConfig) {
export function loadConfig(manualConfig: Partial<CmsConfig> = {}, onLoad: () => unknown) { export function loadConfig(manualConfig: Partial<CmsConfig> = {}, onLoad: () => unknown) {
if (window.CMS_CONFIG) { if (window.CMS_CONFIG) {
return configLoaded(fromJS(window.CMS_CONFIG)); return configLoaded(window.CMS_CONFIG);
} }
return async (dispatch: ThunkDispatch<State, {}, AnyAction>) => { return async (dispatch: ThunkDispatch<State, {}, AnyAction>) => {
dispatch(configLoading()); dispatch(configLoading());
@ -518,7 +517,7 @@ export function loadConfig(manualConfig: Partial<CmsConfig> = {}, onLoad: () =>
const config = applyDefaults(normalizedConfig); const config = applyDefaults(normalizedConfig);
dispatch(configLoaded(fromJS(config))); dispatch(configLoaded(config));
if (typeof onLoad === 'function') { if (typeof onLoad === 'function') {
onLoad(); onLoad();
@ -529,3 +528,7 @@ export function loadConfig(manualConfig: Partial<CmsConfig> = {}, onLoad: () =>
} }
}; };
} }
export type ConfigAction = ReturnType<
typeof configLoading | typeof configLoaded | typeof configFailed
>;

View File

@ -286,7 +286,10 @@ export function loadUnpublishedEntries(collections: Collections) {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false); const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
if (state.config.get('publish_mode') !== EDITORIAL_WORKFLOW || entriesLoaded) return;
if (state.config.publish_mode !== EDITORIAL_WORKFLOW || entriesLoaded) {
return;
}
dispatch(unpublishedEntriesLoading()); dispatch(unpublishedEntriesLoading());
backend backend

View File

@ -1020,7 +1020,7 @@ export function validateMetaField(
} }
const sanitizedPath = (value as string) const sanitizedPath = (value as string)
.split('/') .split('/')
.map(getProcessSegment(state.config.get('slug'))) .map(getProcessSegment(state.config.slug))
.join('/'); .join('/');
if (value !== sanitizedPath) { if (value !== sanitizedPath) {

View File

@ -215,7 +215,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
const integration = selectIntegration(state, null, 'assetStore'); const integration = selectIntegration(state, null, 'assetStore');
const files: MediaFile[] = selectMediaFiles(state, field); const files: MediaFile[] = selectMediaFiles(state, field);
const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug')); const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.slug);
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName); const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);
const editingDraft = selectEditingDraft(state.entryDraft); const editingDraft = selectEditingDraft(state.entryDraft);

View File

@ -1,6 +1,26 @@
import { attempt, flatten, isError, uniq, trim, sortBy, get, set } from 'lodash'; import { attempt, flatten, isError, uniq, trim, sortBy, get, set } from 'lodash';
import { List, Map, fromJS, Set } from 'immutable'; import { List, Map, fromJS, Set } from 'immutable';
import * as fuzzy from 'fuzzy'; import * as fuzzy from 'fuzzy';
import {
localForage,
Cursor,
CURSOR_COMPATIBILITY_SYMBOL,
EditorialWorkflowError,
Implementation as BackendImplementation,
DisplayURL,
ImplementationEntry,
Credentials,
User,
getPathDepth,
blobToFileObj,
asyncLock,
AsyncLock,
UnpublishedEntry,
DataFile,
UnpublishedEntryDiff,
} from 'netlify-cms-lib-util';
import { basename, join, extname, dirname } from 'path';
import { stringTemplate } from 'netlify-cms-lib-widgets';
import { resolveFormat } from './formats/formats'; import { resolveFormat } from './formats/formats';
import { selectUseWorkflow } from './reducers/config'; import { selectUseWorkflow } from './reducers/config';
import { selectMediaFilePath, selectEntry } from './reducers/entries'; import { selectMediaFilePath, selectEntry } from './reducers/entries';
@ -21,35 +41,14 @@ import { createEntry, EntryValue } from './valueObjects/Entry';
import { sanitizeChar } from './lib/urlHelper'; import { sanitizeChar } from './lib/urlHelper';
import { getBackend, invokeEvent } from './lib/registry'; import { getBackend, invokeEvent } from './lib/registry';
import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters'; import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
import {
localForage,
Cursor,
CURSOR_COMPATIBILITY_SYMBOL,
EditorialWorkflowError,
Implementation as BackendImplementation,
DisplayURL,
ImplementationEntry,
Credentials,
User,
getPathDepth,
Config as ImplementationConfig,
blobToFileObj,
asyncLock,
AsyncLock,
UnpublishedEntry,
DataFile,
UnpublishedEntryDiff,
} from 'netlify-cms-lib-util';
import { basename, join, extname, dirname } from 'path';
import { status } from './constants/publishModes'; import { status } from './constants/publishModes';
import { stringTemplate } from 'netlify-cms-lib-widgets';
import { import {
Collection, CmsConfig,
EntryMap, EntryMap,
Config,
FilterRule, FilterRule,
Collections,
EntryDraft, EntryDraft,
Collection,
Collections,
CollectionFile, CollectionFile,
State, State,
EntryField, EntryField,
@ -73,7 +72,7 @@ const { extractTemplateVars, dateParsers, expandPath } = stringTemplate;
function updateAssetProxies( function updateAssetProxies(
assetProxies: AssetProxy[], assetProxies: AssetProxy[],
config: Config, config: CmsConfig,
collection: Collection, collection: Collection,
entryDraft: EntryDraft, entryDraft: EntryDraft,
path: string, path: string,
@ -238,9 +237,9 @@ interface AuthStore {
} }
interface BackendOptions { interface BackendOptions {
backendName?: string; backendName: string;
authStore?: AuthStore | null; config: CmsConfig;
config?: Config; authStore?: AuthStore;
} }
export interface MediaFile { export interface MediaFile {
@ -263,7 +262,7 @@ interface BackupEntry {
} }
interface PersistArgs { interface PersistArgs {
config: Config; config: CmsConfig;
collection: Collection; collection: Collection;
entryDraft: EntryDraft; entryDraft: EntryDraft;
assetProxies: AssetProxy[]; assetProxies: AssetProxy[];
@ -279,7 +278,7 @@ interface ImplementationInitOptions {
} }
type Implementation = BackendImplementation & { type Implementation = BackendImplementation & {
init: (config: ImplementationConfig, options: ImplementationInitOptions) => Implementation; init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation;
}; };
function prepareMetaPath(path: string, collection: Collection) { function prepareMetaPath(path: string, collection: Collection) {
@ -305,24 +304,21 @@ function collectionDepth(collection: Collection) {
export class Backend { export class Backend {
implementation: Implementation; implementation: Implementation;
backendName: string; backendName: string;
authStore: AuthStore | null; config: CmsConfig;
config: Config; authStore?: AuthStore;
user?: User | null; user?: User | null;
backupSync: AsyncLock; backupSync: AsyncLock;
constructor( constructor(implementation: Implementation, { backendName, authStore, config }: BackendOptions) {
implementation: Implementation,
{ backendName, authStore = null, config }: BackendOptions = {},
) {
// We can't reliably run this on exit, so we do cleanup on load. // We can't reliably run this on exit, so we do cleanup on load.
this.deleteAnonymousBackup(); this.deleteAnonymousBackup();
this.config = config as Config; this.config = config;
this.implementation = implementation.init(this.config.toJS(), { this.implementation = implementation.init(this.config, {
useWorkflow: selectUseWorkflow(this.config), useWorkflow: selectUseWorkflow(this.config),
updateUserCredentials: this.updateUserCredentials, updateUserCredentials: this.updateUserCredentials,
initialWorkflowStatus: status.first(), initialWorkflowStatus: status.first(),
}); });
this.backendName = backendName as string; this.backendName = backendName;
this.authStore = authStore; this.authStore = authStore;
if (this.implementation === null) { if (this.implementation === null) {
throw new Error('Cannot instantiate a Backend with no implementation'); throw new Error('Cannot instantiate a Backend with no implementation');
@ -436,11 +432,11 @@ export class Backend {
async generateUniqueSlug( async generateUniqueSlug(
collection: Collection, collection: Collection,
entryData: Map<string, unknown>, entryData: Map<string, unknown>,
config: Config, config: CmsConfig,
usedSlugs: List<string>, usedSlugs: List<string>,
customPath: string | undefined, customPath: string | undefined,
) { ) {
const slugConfig = config.get('slug'); const slugConfig = config.slug;
let slug: string; let slug: string;
if (customPath) { if (customPath) {
slug = slugFromCustomPath(collection, customPath); slug = slugFromCustomPath(collection, customPath);
@ -944,7 +940,7 @@ export class Backend {
async processEntry(state: State, collection: Collection, entry: EntryValue) { async processEntry(state: State, collection: Collection, entry: EntryValue) {
const integration = selectIntegration(state.integrations, null, 'assetStore'); const integration = selectIntegration(state.integrations, null, 'assetStore');
const mediaFolders = selectMediaFolders(state, collection, fromJS(entry)); const mediaFolders = selectMediaFolders(state.config, collection, fromJS(entry));
if (mediaFolders.length > 0 && !integration) { if (mediaFolders.length > 0 && !integration) {
const files = await Promise.all( const files = await Promise.all(
mediaFolders.map(folder => this.implementation.getMedia(folder)), mediaFolders.map(folder => this.implementation.getMedia(folder)),
@ -978,14 +974,14 @@ export class Backend {
* If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing. * If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing.
*/ */
const baseUrl = this.config.get('site_url'); const baseUrl = this.config.site_url;
if (!baseUrl || this.config.get('show_preview_links') === false) { if (!baseUrl || this.config.show_preview_links === false) {
return; return;
} }
return { return {
url: previewUrlFormatter(baseUrl, collection, slug, this.config.get('slug'), entry), url: previewUrlFormatter(baseUrl, collection, slug, entry, this.config.slug),
status: 'SUCCESS', status: 'SUCCESS',
}; };
} }
@ -1005,7 +1001,7 @@ export class Backend {
* If the registered backend does not provide a `getDeployPreview` method, or * If the registered backend does not provide a `getDeployPreview` method, or
* `show_preview_links` in the config is set to false, do nothing. * `show_preview_links` in the config is set to false, do nothing.
*/ */
if (!this.implementation.getDeployPreview || this.config.get('show_preview_links') === false) { if (!this.implementation.getDeployPreview || this.config.show_preview_links === false) {
return; return;
} }
@ -1019,7 +1015,7 @@ export class Backend {
count++; count++;
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug); deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
if (!deployPreview) { if (!deployPreview) {
await new Promise(resolve => setTimeout(() => resolve(), interval)); await new Promise(resolve => setTimeout(() => resolve(undefined), interval));
} }
} }
@ -1034,7 +1030,7 @@ export class Backend {
/** /**
* Create a URL using the collection `preview_path`, if provided. * Create a URL using the collection `preview_path`, if provided.
*/ */
url: previewUrlFormatter(deployPreview.url, collection, slug, this.config.get('slug'), entry), url: previewUrlFormatter(deployPreview.url, collection, slug, entry, this.config.slug),
/** /**
* Always capitalize the status for consistency. * Always capitalize the status for consistency.
*/ */
@ -1182,7 +1178,7 @@ export class Backend {
await this.invokeEventWithEntry('postSave', entry); await this.invokeEventWithEntry('postSave', entry);
} }
async persistMedia(config: Config, file: AssetProxy) { async persistMedia(config: CmsConfig, file: AssetProxy) {
const user = (await this.currentUser()) as User; const user = (await this.currentUser()) as User;
const options = { const options = {
commitMessage: commitMessageFormatter( commitMessage: commitMessageFormatter(
@ -1233,7 +1229,7 @@ export class Backend {
await this.invokePostUnpublishEvent(entry); await this.invokePostUnpublishEvent(entry);
} }
async deleteMedia(config: Config, path: string) { async deleteMedia(config: CmsConfig, path: string) {
const user = (await this.currentUser()) as User; const user = (await this.currentUser()) as User;
const commitMessage = commitMessageFormatter( const commitMessage = commitMessageFormatter(
'deleteMedia', 'deleteMedia',
@ -1310,12 +1306,12 @@ export class Backend {
} }
} }
export function resolveBackend(config: Config) { export function resolveBackend(config: CmsConfig) {
const name = config.getIn(['backend', 'name']); if (!config.backend.name) {
if (name == null) {
throw new Error('No backend defined in configuration'); throw new Error('No backend defined in configuration');
} }
const { name } = config.backend;
const authStore = new LocalStorageAuthStore(); const authStore = new LocalStorageAuthStore();
const backend = getBackend(name); const backend = getBackend(name);
@ -1329,7 +1325,7 @@ export function resolveBackend(config: Config) {
export const currentBackend = (function() { export const currentBackend = (function() {
let backend: Backend; let backend: Backend;
return (config: Config) => { return (config: CmsConfig) => {
if (backend) { if (backend) {
return backend; return backend;
} }

View File

@ -73,8 +73,8 @@ function RouteInCollection({ collections, render, ...props }) {
class App extends React.Component { class App extends React.Component {
static propTypes = { static propTypes = {
auth: PropTypes.object.isRequired, auth: PropTypes.object.isRequired,
config: ImmutablePropTypes.map, config: PropTypes.object.isRequired,
collections: ImmutablePropTypes.orderedMap, collections: ImmutablePropTypes.map.isRequired,
loginUser: PropTypes.func.isRequired, loginUser: PropTypes.func.isRequired,
logoutUser: PropTypes.func.isRequired, logoutUser: PropTypes.func.isRequired,
user: PropTypes.object, user: PropTypes.object,
@ -94,7 +94,7 @@ class App extends React.Component {
<h1>{t('app.app.errorHeader')}</h1> <h1>{t('app.app.errorHeader')}</h1>
<div> <div>
<strong>{t('app.app.configErrors')}:</strong> <strong>{t('app.app.configErrors')}:</strong>
<ErrorCodeBlock>{config.get('error')}</ErrorCodeBlock> <ErrorCodeBlock>{config.error}</ErrorCodeBlock>
<span>{t('app.app.checkConfigYml')}</span> <span>{t('app.app.checkConfigYml')}</span>
</div> </div>
</ErrorContainer> </ErrorContainer>
@ -124,10 +124,10 @@ class App extends React.Component {
onLogin: this.handleLogin.bind(this), onLogin: this.handleLogin.bind(this),
error: auth.error, error: auth.error,
inProgress: auth.isFetching, inProgress: auth.isFetching,
siteId: this.props.config.getIn(['backend', 'site_domain']), siteId: this.props.config.backend.site_domain,
base_url: this.props.config.getIn(['backend', 'base_url'], null), base_url: this.props.config.backend.base_url,
authEndpoint: this.props.config.getIn(['backend', 'auth_endpoint']), authEndpoint: this.props.config.backend.auth_endpoint,
config: this.props.config.toJS(), config: this.props.config,
clearHash: () => history.replace('/'), clearHash: () => history.replace('/'),
t, t,
})} })}
@ -158,11 +158,11 @@ class App extends React.Component {
return null; return null;
} }
if (config.get('error')) { if (config.error) {
return this.configError(config); return this.configError(config);
} }
if (config.get('isFetching')) { if (config.isFetching) {
return <Loader active>{t('app.app.loadingConfig')}</Loader>; return <Loader active>{t('app.app.loadingConfig')}</Loader>;
} }
@ -183,8 +183,8 @@ class App extends React.Component {
onLogoutClick={logoutUser} onLogoutClick={logoutUser}
openMediaLibrary={openMediaLibrary} openMediaLibrary={openMediaLibrary}
hasWorkflow={hasWorkflow} hasWorkflow={hasWorkflow}
displayUrl={config.get('display_url')} displayUrl={config.display_url}
isTestRepo={config.getIn(['backend', 'name']) === 'test-repo'} isTestRepo={config.backend.name === 'test-repo'}
showMediaButton={showMediaButton} showMediaButton={showMediaButton}
/> />
<AppMainContainer> <AppMainContainer>
@ -256,7 +256,7 @@ function mapStateToProps(state) {
const { auth, config, collections, globalUI, mediaLibrary } = state; const { auth, config, collections, globalUI, mediaLibrary } = state;
const user = auth.user; const user = auth.user;
const isFetching = globalUI.get('isFetching'); const isFetching = globalUI.get('isFetching');
const publishMode = config && config.get('publish_mode'); const publishMode = config.publish_mode;
const useMediaLibrary = !mediaLibrary.get('externalLibrary'); const useMediaLibrary = !mediaLibrary.get('externalLibrary');
const showMediaButton = mediaLibrary.get('showMediaButton'); const showMediaButton = mediaLibrary.get('showMediaButton');
return { return {

View File

@ -116,7 +116,7 @@ const AppHeaderNavList = styled.ul`
class Header extends React.Component { class Header extends React.Component {
static propTypes = { static propTypes = {
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
onCreateEntryClick: PropTypes.func.isRequired, onCreateEntryClick: PropTypes.func.isRequired,
onLogoutClick: PropTypes.func.isRequired, onLogoutClick: PropTypes.func.isRequired,
openMediaLibrary: PropTypes.func.isRequired, openMediaLibrary: PropTypes.func.isRequired,

View File

@ -48,7 +48,7 @@ export class Collection extends React.Component {
isSearchResults: PropTypes.bool, isSearchResults: PropTypes.bool,
isSingleSearchResult: PropTypes.bool, isSingleSearchResult: PropTypes.bool,
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
sortableFields: PropTypes.array, sortableFields: PropTypes.array,
sort: ImmutablePropTypes.orderedMap, sort: ImmutablePropTypes.orderedMap,
onSortClick: PropTypes.func.isRequired, onSortClick: PropTypes.func.isRequired,

View File

@ -88,7 +88,7 @@ const SuggestionDivider = styled.div`
class CollectionSearch extends React.Component { class CollectionSearch extends React.Component {
static propTypes = { static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
collection: ImmutablePropTypes.map, collection: ImmutablePropTypes.map,
searchTerm: PropTypes.string.isRequired, searchTerm: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,

View File

@ -67,7 +67,7 @@ const SidebarNavLink = styled(NavLink)`
export class Sidebar extends React.Component { export class Sidebar extends React.Component {
static propTypes = { static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
collection: ImmutablePropTypes.map, collection: ImmutablePropTypes.map,
searchTerm: PropTypes.string, searchTerm: PropTypes.string,
filterTerm: PropTypes.string, filterTerm: PropTypes.string,

View File

@ -434,8 +434,8 @@ function mapStateToProps(state, ownProps) {
const entry = newEntry ? null : selectEntry(state, collectionName, slug); const entry = newEntry ? null : selectEntry(state, collectionName, slug);
const user = auth.user; const user = auth.user;
const hasChanged = entryDraft.get('hasChanged'); const hasChanged = entryDraft.get('hasChanged');
const displayUrl = config.get('display_url'); const displayUrl = config.display_url;
const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW; const hasWorkflow = config.publish_mode === EDITORIAL_WORKFLOW;
const useOpenAuthoring = globalUI.get('useOpenAuthoring', false); const useOpenAuthoring = globalUI.get('useOpenAuthoring', false);
const isModification = entryDraft.getIn(['entry', 'isModification']); const isModification = entryDraft.getIn(['entry', 'isModification']);
const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]); const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]);

View File

@ -7,7 +7,7 @@ import { loadUnpublishedEntry, persistUnpublishedEntry } from 'Actions/editorial
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {
const { collections } = state; const { collections } = state;
const isEditorialWorkflow = state.config.get('publish_mode') === EDITORIAL_WORKFLOW; const isEditorialWorkflow = state.config.publish_mode === EDITORIAL_WORKFLOW;
const collection = collections.get(ownProps.match.params.name); const collection = collections.get(ownProps.match.params.name);
const returnObj = { const returnObj = {
isEditorialWorkflow, isEditorialWorkflow,

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { translate } from 'react-polyglot'; import { translate } from 'react-polyglot';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import yaml from 'yaml'; import yaml from 'yaml';
@ -44,9 +43,9 @@ function buildIssueTemplate({ config }) {
} }
const template = getIssueTemplate({ const template = getIssueTemplate({
version, version,
provider: config.getIn(['backend', 'name']), provider: config.backend.name,
browser: navigator.userAgent, browser: navigator.userAgent,
config: yaml.stringify(config.toJS()), config: yaml.stringify(config),
}); });
return template; return template;
@ -131,7 +130,7 @@ export class ErrorBoundary extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
config: ImmutablePropTypes.map.isRequired, config: PropTypes.object.isRequired,
}; };
state = { state = {

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { ErrorBoundary } from '../ErrorBoundary'; import { ErrorBoundary } from '../ErrorBoundary';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { fromJS } from 'immutable';
import { oneLineTrim } from 'common-tags'; import { oneLineTrim } from 'common-tags';
function WithError() { function WithError() {
@ -24,7 +23,7 @@ Object.defineProperty(
); );
describe('Editor', () => { describe('Editor', () => {
const config = fromJS({ backend: { name: 'github' } }); const config = { backend: { name: 'github' } };
const props = { t: jest.fn(key => key), config }; const props = { t: jest.fn(key => key), config };

View File

@ -53,7 +53,7 @@ const WorkflowTopDescription = styled.p`
class Workflow extends Component { class Workflow extends Component {
static propTypes = { static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
isEditorialWorkflow: PropTypes.bool.isRequired, isEditorialWorkflow: PropTypes.bool.isRequired,
isOpenAuthoring: PropTypes.bool, isOpenAuthoring: PropTypes.bool,
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
@ -137,7 +137,7 @@ class Workflow extends Component {
function mapStateToProps(state) { function mapStateToProps(state) {
const { collections, config, globalUI } = state; const { collections, config, globalUI } = state;
const isEditorialWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW; const isEditorialWorkflow = config.publish_mode === EDITORIAL_WORKFLOW;
const isOpenAuthoring = globalUI.get('useOpenAuthoring', false); const isOpenAuthoring = globalUI.get('useOpenAuthoring', false);
const returnObj = { collections, isEditorialWorkflow, isOpenAuthoring }; const returnObj = { collections, isEditorialWorkflow, isOpenAuthoring };

View File

@ -135,7 +135,7 @@ class WorkflowList extends React.Component {
handleDelete: PropTypes.func.isRequired, handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
isOpenAuthoring: PropTypes.bool, isOpenAuthoring: PropTypes.bool,
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.map.isRequired,
}; };
handleChangeStatus = (newStatus, dragProps) => { handleChangeStatus = (newStatus, dragProps) => {

View File

@ -0,0 +1,2 @@
export const COMMIT_AUTHOR = 'commit_author';
export const COMMIT_DATE = 'commit_date';

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
export const IDENTIFIER_FIELDS = ['title', 'path']; export const IDENTIFIER_FIELDS = ['title', 'path'] as const;
export const SORTABLE_FIELDS = ['title', 'date', 'author', 'description']; export const SORTABLE_FIELDS = ['title', 'date', 'author', 'description'] as const;
export const INFERABLE_FIELDS = { export const INFERABLE_FIELDS = {
title: { title: {

View File

@ -14,18 +14,18 @@ jest.mock('../../reducers/collections');
describe('formatters', () => { describe('formatters', () => {
describe('commitMessageFormatter', () => { describe('commitMessageFormatter', () => {
const config = { const config = {
getIn: jest.fn(), backend: {
}; name: 'git-gateway',
},
const collection = {
get: jest.fn().mockReturnValue('Collection'),
}; };
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('should return default commit message on create', () => { it('should return default commit message on create, label_singular', () => {
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter('create', config, { commitMessageFormatter('create', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -35,9 +35,8 @@ describe('formatters', () => {
).toEqual('Create Collection “doc-slug”'); ).toEqual('Create Collection “doc-slug”');
}); });
it('should return default commit message on create', () => { it('should return default commit message on create, label', () => {
collection.get.mockReturnValueOnce(undefined); const collection = Map({ label: 'Collections' });
collection.get.mockReturnValueOnce('Collections');
expect( expect(
commitMessageFormatter('update', config, { commitMessageFormatter('update', config, {
@ -49,6 +48,8 @@ describe('formatters', () => {
}); });
it('should return default commit message on delete', () => { it('should return default commit message on delete', () => {
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter('delete', config, { commitMessageFormatter('delete', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -59,6 +60,8 @@ describe('formatters', () => {
}); });
it('should return default commit message on uploadMedia', () => { it('should return default commit message on uploadMedia', () => {
const collection = Map({});
expect( expect(
commitMessageFormatter('uploadMedia', config, { commitMessageFormatter('uploadMedia', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -69,6 +72,8 @@ describe('formatters', () => {
}); });
it('should return default commit message on deleteMedia', () => { it('should return default commit message on deleteMedia', () => {
const collection = Map({});
expect( expect(
commitMessageFormatter('deleteMedia', config, { commitMessageFormatter('deleteMedia', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -79,11 +84,14 @@ describe('formatters', () => {
}); });
it('should log warning on unknown variable', () => { it('should log warning on unknown variable', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
create: 'Create {{collection}} “{{slug}}” with "{{unknown variable}}"', commit_messages: {
}), create: 'Create {{collection}} “{{slug}}” with "{{unknown variable}}"',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter('create', config, { commitMessageFormatter('create', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -98,12 +106,14 @@ describe('formatters', () => {
}); });
it('should return custom commit message on update', () => { it('should return custom commit message on update', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
update: 'Custom commit message', commit_messages: {
}), update: 'Custom commit message',
); },
},
};
const collection = Map({});
expect( expect(
commitMessageFormatter('update', config, { commitMessageFormatter('update', config, {
slug: 'doc-slug', slug: 'doc-slug',
@ -114,12 +124,14 @@ describe('formatters', () => {
}); });
it('should use empty values if "authorLogin" and "authorName" are missing in commit message', () => { it('should use empty values if "authorLogin" and "authorName" are missing in commit message', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
update: '{{author-login}} - {{author-name}}: Create {{collection}} “{{slug}}”', commit_messages: {
}), update: '{{author-login}} - {{author-name}}: Create {{collection}} “{{slug}}”',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter( commitMessageFormatter(
'update', 'update',
@ -135,12 +147,14 @@ describe('formatters', () => {
}); });
it('should return custom create message with author information', () => { it('should return custom create message with author information', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
create: '{{author-login}} - {{author-name}}: Create {{collection}} “{{slug}}”', commit_messages: {
}), create: '{{author-login}} - {{author-name}}: Create {{collection}} “{{slug}}”',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter( commitMessageFormatter(
'create', 'create',
@ -158,12 +172,14 @@ describe('formatters', () => {
}); });
it('should return custom open authoring message', () => { it('should return custom open authoring message', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
openAuthoring: '{{author-login}} - {{author-name}}: {{message}}', commit_messages: {
}), openAuthoring: '{{author-login}} - {{author-name}}: {{message}}',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter( commitMessageFormatter(
'create', 'create',
@ -181,12 +197,14 @@ describe('formatters', () => {
}); });
it('should use empty values if "authorLogin" and "authorName" are missing in open authoring message', () => { it('should use empty values if "authorLogin" and "authorName" are missing in open authoring message', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
openAuthoring: '{{author-login}} - {{author-name}}: {{message}}', commit_messages: {
}), openAuthoring: '{{author-login}} - {{author-name}}: {{message}}',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
expect( expect(
commitMessageFormatter( commitMessageFormatter(
'create', 'create',
@ -202,12 +220,14 @@ describe('formatters', () => {
}); });
it('should log warning on unknown variable in open authoring template', () => { it('should log warning on unknown variable in open authoring template', () => {
config.getIn.mockReturnValueOnce( const config = {
Map({ backend: {
openAuthoring: '{{author-email}}: {{message}}', commit_messages: {
}), openAuthoring: '{{author-email}}: {{message}}',
); },
},
};
const collection = Map({ label_singular: 'Collection' });
commitMessageFormatter( commitMessageFormatter(
'create', 'create',
config, config,
@ -246,11 +266,11 @@ describe('formatters', () => {
}); });
}); });
const slugConfig = Map({ const slugConfig = {
encoding: 'unicode', encoding: 'unicode',
clean_accents: false, clean_accents: false,
sanitize_replacement: '-', sanitize_replacement: '-',
}); };
describe('slugFormatter', () => { describe('slugFormatter', () => {
const date = new Date('2020-01-01'); const date = new Date('2020-01-01');
@ -363,8 +383,8 @@ describe('formatters', () => {
preview_path_date_field: 'customDateField', preview_path_date_field: 'customDateField',
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({ customDateField: date, slug: 'entrySlug', title: 'title' }) }), Map({ data: Map({ customDateField: date, slug: 'entrySlug', title: 'title' }) }),
slugConfig,
), ),
).toBe('https://www.example.com/2020/backendslug/title/entryslug'); ).toBe('https://www.example.com/2020/backendslug/title/entryslug');
}); });
@ -384,8 +404,8 @@ describe('formatters', () => {
files: List([file]), files: List([file]),
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }), Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }),
slugConfig,
), ),
).toBe('https://www.example.com/backendslug/about-the-project/title'); ).toBe('https://www.example.com/backendslug/about-the-project/title');
}); });
@ -404,8 +424,8 @@ describe('formatters', () => {
files: List([file]), files: List([file]),
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }), Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }),
slugConfig,
), ),
).toBe('https://www.example.com/backendslug/about-the-project/title'); ).toBe('https://www.example.com/backendslug/about-the-project/title');
}); });
@ -425,8 +445,8 @@ describe('formatters', () => {
files: List([file]), files: List([file]),
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }), Map({ data: Map({ slug: 'about-the-project', title: 'title' }), slug: 'about-file' }),
slugConfig,
), ),
).toBe('https://www.example.com/backendslug/title/about-the-project'); ).toBe('https://www.example.com/backendslug/title/about-the-project');
}); });
@ -444,8 +464,8 @@ describe('formatters', () => {
preview_path: '{{year}}/{{month}}/{{slug}}/{{title}}/{{fields.slug}}', preview_path: '{{year}}/{{month}}/{{slug}}/{{title}}/{{fields.slug}}',
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({ date, slug: 'entrySlug', title: 'title' }) }), Map({ data: Map({ date, slug: 'entrySlug', title: 'title' }) }),
slugConfig,
), ),
).toBe('https://www.example.com/2020/01/backendslug/title/entryslug'); ).toBe('https://www.example.com/2020/01/backendslug/title/entryslug');
}); });
@ -458,8 +478,8 @@ describe('formatters', () => {
preview_path: 'posts/{{filename}}.{{extension}}', preview_path: 'posts/{{filename}}.{{extension}}',
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({}), path: 'src/content/posts/title.md' }), Map({ data: Map({}), path: 'src/content/posts/title.md' }),
slugConfig,
), ),
).toBe('https://www.example.com/posts/title.md'); ).toBe('https://www.example.com/posts/title.md');
}); });
@ -473,8 +493,8 @@ describe('formatters', () => {
preview_path: 'portfolio/{{dirname}}', preview_path: 'portfolio/{{dirname}}',
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({}), path: '_portfolio/i-am-the-slug.md' }), Map({ data: Map({}), path: '_portfolio/i-am-the-slug.md' }),
slugConfig,
), ),
).toBe('https://www.example.com/portfolio/'); ).toBe('https://www.example.com/portfolio/');
}); });
@ -490,8 +510,8 @@ describe('formatters', () => {
meta: { path: { widget: 'string', label: 'Path', index_file: 'index' } }, meta: { path: { widget: 'string', label: 'Path', index_file: 'index' } },
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({}), path: '_portfolio/drawing/i-am-the-slug/index.md' }), Map({ data: Map({}), path: '_portfolio/drawing/i-am-the-slug/index.md' }),
slugConfig,
), ),
).toBe('https://www.example.com/portfolio/drawing/i-am-the-slug'); ).toBe('https://www.example.com/portfolio/drawing/i-am-the-slug');
}); });
@ -507,8 +527,8 @@ describe('formatters', () => {
preview_path_date_field: 'date', preview_path_date_field: 'date',
}), }),
'backendSlug', 'backendSlug',
slugConfig,
Map({ data: Map({}) }), Map({ data: Map({}) }),
slugConfig,
), ),
).toBe('https://www.example.com'); ).toBe('https://www.example.com');

View File

@ -1,4 +1,3 @@
import { Map } from 'immutable';
import { sanitizeURI, sanitizeSlug, sanitizeChar } from '../urlHelper'; import { sanitizeURI, sanitizeSlug, sanitizeChar } from '../urlHelper';
describe('sanitizeURI', () => { describe('sanitizeURI', () => {
@ -60,79 +59,80 @@ describe('sanitizeSlug', () => {
}); });
it('throws an error for non-string replacements', () => { it('throws an error for non-string replacements', () => {
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: {} }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: {} })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: [] }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: [] })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: false }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: false })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: null }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: null })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: 11232 }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: 11232 })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
// do not test undefined for this variant since a default is set in the constructor. // do not test undefined for this variant since a default is set in the constructor.
//expect(() => sanitizeSlug('test', { sanitize_replacement: undefined })).toThrowError("`options.replacement` must be a string."); //expect(() => sanitizeSlug('test', { sanitize_replacement: undefined })).toThrowError("`options.replacement` must be a string.");
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: () => {} }))).toThrowError( expect(() => sanitizeSlug('test', { sanitize_replacement: () => {} })).toThrowError(
'`options.replacement` must be a string.', '`options.replacement` must be a string.',
); );
}); });
it('should keep valid URI chars (letters digits _ - . ~)', () => { it('should keep valid URI chars (letters digits _ - . ~)', () => {
expect(sanitizeSlug('This, that-one_or.the~other 123!', Map(slugConfig))).toEqual( expect(sanitizeSlug('This, that-one_or.the~other 123!', slugConfig)).toEqual(
'This-that-one_or.the~other-123', 'This-that-one_or.the~other-123',
); );
}); });
it('should remove accents with `clean_accents` set', () => { it('should remove accents with `clean_accents` set', () => {
expect(sanitizeSlug('ěščřžý', Map({ ...slugConfig, clean_accents: true }))).toEqual('escrzy'); expect(sanitizeSlug('ěščřžý', { ...slugConfig, clean_accents: true })).toEqual('escrzy');
}); });
it('should remove non-latin chars in "ascii" mode', () => { it('should remove non-latin chars in "ascii" mode', () => {
expect( expect(sanitizeSlug('ěščřžý日本語のタイトル', { ...slugConfig, encoding: 'ascii' })).toEqual(
sanitizeSlug('ěščřžý日本語のタイトル', Map({ ...slugConfig, encoding: 'ascii' })), '',
).toEqual(''); );
}); });
it('should clean accents and strip non-latin chars in "ascii" mode with `clean_accents` set', () => { it('should clean accents and strip non-latin chars in "ascii" mode with `clean_accents` set', () => {
expect( expect(
sanitizeSlug( sanitizeSlug('ěščřžý日本語のタイトル', {
'ěščřžý日本語のタイトル', ...slugConfig,
Map({ ...slugConfig, encoding: 'ascii', clean_accents: true }), encoding: 'ascii',
), clean_accents: true,
}),
).toEqual('escrzy'); ).toEqual('escrzy');
}); });
it('removes double replacements', () => { it('removes double replacements', () => {
expect(sanitizeSlug('test--test', Map(slugConfig))).toEqual('test-test'); expect(sanitizeSlug('test--test', slugConfig)).toEqual('test-test');
expect(sanitizeSlug('test test', Map(slugConfig))).toEqual('test-test'); expect(sanitizeSlug('test test', slugConfig)).toEqual('test-test');
}); });
it('removes trailing replacements', () => { it('removes trailing replacements', () => {
expect(sanitizeSlug('test test ', Map(slugConfig))).toEqual('test-test'); expect(sanitizeSlug('test test ', slugConfig)).toEqual('test-test');
}); });
it('removes leading replacements', () => { it('removes leading replacements', () => {
expect(sanitizeSlug('"test" test', Map(slugConfig))).toEqual('test-test'); expect(sanitizeSlug('"test" test', slugConfig)).toEqual('test-test');
}); });
it('uses alternate replacements', () => { it('uses alternate replacements', () => {
expect( expect(sanitizeSlug('test test ', { ...slugConfig, sanitize_replacement: '_' })).toEqual(
sanitizeSlug('test test ', Map({ ...slugConfig, sanitize_replacement: '_' })), 'test_test',
).toEqual('test_test'); );
}); });
}); });
describe('sanitizeChar', () => { describe('sanitizeChar', () => {
it('should sanitize whitespace with default replacement', () => { it('should sanitize whitespace with default replacement', () => {
expect(sanitizeChar(' ', Map(slugConfig))).toBe('-'); expect(sanitizeChar(' ', slugConfig)).toBe('-');
}); });
it('should sanitize whitespace with custom replacement', () => { it('should sanitize whitespace with custom replacement', () => {
expect(sanitizeChar(' ', Map({ ...slugConfig, sanitize_replacement: '_' }))).toBe('_'); expect(sanitizeChar(' ', { ...slugConfig, sanitize_replacement: '_' })).toBe('_');
}); });
}); });

View File

@ -5,14 +5,13 @@ import { stringTemplate } from 'netlify-cms-lib-widgets';
import { import {
selectIdentifier, selectIdentifier,
selectField, selectField,
COMMIT_AUTHOR,
COMMIT_DATE,
selectInferedField, selectInferedField,
getFileFromSlug, getFileFromSlug,
} from '../reducers/collections'; } from '../reducers/collections';
import { Collection, SlugConfig, Config, EntryMap } from '../types/redux'; import { Collection, CmsConfig, CmsSlug, EntryMap } from '../types/redux';
import { stripIndent } from 'common-tags'; import { stripIndent } from 'common-tags';
import { FILES } from '../constants/collectionTypes'; import { FILES } from '../constants/collectionTypes';
import { COMMIT_AUTHOR, COMMIT_DATE } from '../constants/commitProps';
const { const {
compileStringTemplate, compileStringTemplate,
@ -22,14 +21,14 @@ const {
addFileTemplateFields, addFileTemplateFields,
} = stringTemplate; } = stringTemplate;
const commitMessageTemplates = Map({ const commitMessageTemplates = {
create: 'Create {{collection}} “{{slug}}”', create: 'Create {{collection}} “{{slug}}”',
update: 'Update {{collection}} “{{slug}}”', update: 'Update {{collection}} “{{slug}}”',
delete: 'Delete {{collection}} “{{slug}}”', delete: 'Delete {{collection}} “{{slug}}”',
uploadMedia: 'Upload “{{path}}”', uploadMedia: 'Upload “{{path}}”',
deleteMedia: 'Delete “{{path}}”', deleteMedia: 'Delete “{{path}}”',
openAuthoring: '{{message}}', openAuthoring: '{{message}}',
}); } as const;
const variableRegex = /\{\{([^}]+)\}\}/g; const variableRegex = /\{\{([^}]+)\}\}/g;
@ -42,16 +41,14 @@ type Options = {
}; };
export function commitMessageFormatter( export function commitMessageFormatter(
type: string, type: keyof typeof commitMessageTemplates,
config: Config, config: CmsConfig,
{ slug, path, collection, authorLogin, authorName }: Options, { slug, path, collection, authorLogin, authorName }: Options,
isOpenAuthoring?: boolean, isOpenAuthoring?: boolean,
) { ) {
const templates = commitMessageTemplates.merge( const templates = { ...commitMessageTemplates, ...(config.backend.commit_messages || {}) };
config.getIn(['backend', 'commit_messages'], Map<string, string>()),
);
const commitMessage = templates.get(type).replace(variableRegex, (_, variable) => { const commitMessage = templates[type].replace(variableRegex, (_, variable) => {
switch (variable) { switch (variable) {
case 'slug': case 'slug':
return slug || ''; return slug || '';
@ -73,7 +70,7 @@ export function commitMessageFormatter(
return commitMessage; return commitMessage;
} }
const message = templates.get('openAuthoring').replace(variableRegex, (_, variable) => { const message = templates.openAuthoring.replace(variableRegex, (_, variable) => {
switch (variable) { switch (variable) {
case 'message': case 'message':
return commitMessage; return commitMessage;
@ -105,9 +102,9 @@ export function prepareSlug(slug: string) {
); );
} }
export function getProcessSegment(slugConfig: SlugConfig, ignoreValues: string[] = []) { export function getProcessSegment(slugConfig?: CmsSlug, ignoreValues?: string[]) {
return (value: string) => return (value: string) =>
ignoreValues.includes(value) ignoreValues && ignoreValues.includes(value)
? value ? value
: flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value); : flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
} }
@ -115,7 +112,7 @@ export function getProcessSegment(slugConfig: SlugConfig, ignoreValues: string[]
export function slugFormatter( export function slugFormatter(
collection: Collection, collection: Collection,
entryData: Map<string, unknown>, entryData: Map<string, unknown>,
slugConfig: SlugConfig, slugConfig?: CmsSlug,
) { ) {
const slugTemplate = collection.get('slug') || '{{slug}}'; const slugTemplate = collection.get('slug') || '{{slug}}';
@ -144,8 +141,8 @@ export function previewUrlFormatter(
baseUrl: string, baseUrl: string,
collection: Collection, collection: Collection,
slug: string, slug: string,
slugConfig: SlugConfig,
entry: EntryMap, entry: EntryMap,
slugConfig?: CmsSlug,
) { ) {
/** /**
* Preview URL can't be created without `baseUrl`. This makes preview URLs * Preview URL can't be created without `baseUrl`. This makes preview URLs
@ -239,7 +236,7 @@ export function folderFormatter(
collection: Collection, collection: Collection,
defaultFolder: string, defaultFolder: string,
folderKey: string, folderKey: string,
slugConfig: SlugConfig, slugConfig?: CmsSlug,
) { ) {
if (!entry || !entry.get('data')) { if (!entry || !entry.get('data')) {
return folderTemplate; return folderTemplate;

View File

@ -2,7 +2,7 @@ import url from 'url';
import diacritics from 'diacritics'; import diacritics from 'diacritics';
import sanitizeFilename from 'sanitize-filename'; import sanitizeFilename from 'sanitize-filename';
import { isString, escapeRegExp, flow, partialRight } from 'lodash'; import { isString, escapeRegExp, flow, partialRight } from 'lodash';
import { SlugConfig } from '../types/redux'; import { CmsSlug } from '../types/redux';
function getUrl(urlString: string, direct?: boolean) { function getUrl(urlString: string, direct?: boolean) {
return `${direct ? '/#' : ''}${urlString}`; return `${direct ? '/#' : ''}${urlString}`;
@ -16,7 +16,7 @@ export function getNewEntryUrl(collectionName: string, direct?: boolean) {
return getUrl(`/collections/${collectionName}/new`, direct); return getUrl(`/collections/${collectionName}/new`, direct);
} }
export function addParams(urlString: string, params: {}) { export function addParams(urlString: string, params: Record<string, string>) {
const parsedUrl = url.parse(urlString, true); const parsedUrl = url.parse(urlString, true);
parsedUrl.query = { ...parsedUrl.query, ...params }; parsedUrl.query = { ...parsedUrl.query, ...params };
return url.format(parsedUrl); return url.format(parsedUrl);
@ -64,7 +64,12 @@ export function getCharReplacer(encoding: string, replacement: string) {
return (char: string) => (validChar(char) ? char : replacement); return (char: string) => (validChar(char) ? char : replacement);
} }
// `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed. // `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed.
export function sanitizeURI(str: string, { replacement = '', encoding = 'unicode' } = {}) { export function sanitizeURI(
str: string,
options?: { replacement: CmsSlug['sanitize_replacement']; encoding: CmsSlug['encoding'] },
) {
const { replacement = '', encoding = 'unicode' } = options || {};
if (!isString(str)) { if (!isString(str)) {
throw new Error('The input slug must be a string.'); throw new Error('The input slug must be a string.');
} }
@ -79,21 +84,18 @@ export function sanitizeURI(str: string, { replacement = '', encoding = 'unicode
.join(''); .join('');
} }
export function sanitizeChar(char: string, options: SlugConfig) { export function sanitizeChar(char: string, options?: CmsSlug) {
const encoding = options.get('encoding'); const { encoding = 'unicode', sanitize_replacement: replacement = '' } = options || {};
const replacement = options.get('sanitize_replacement');
return getCharReplacer(encoding, replacement)(char); return getCharReplacer(encoding, replacement)(char);
} }
export function sanitizeSlug(str: string, options: SlugConfig) { export function sanitizeSlug(str: string, options?: CmsSlug) {
if (!isString(str)) { if (!isString(str)) {
throw new Error('The input slug must be a string.'); throw new Error('The input slug must be a string.');
} }
const encoding = options.get('encoding'); const { encoding, clean_accents: stripDiacritics, sanitize_replacement: replacement } =
const stripDiacritics = options.get('clean_accents'); options || {};
const replacement = options.get('sanitize_replacement');
const sanitizedSlug = flow([ const sanitizedSlug = flow([
...(stripDiacritics ? [diacritics.remove] : []), ...(stripDiacritics ? [diacritics.remove] : []),

View File

@ -7,7 +7,7 @@ import { getMediaLibrary } from './lib/registry';
import store from './redux'; import store from './redux';
import { configFailed } from './actions/config'; import { configFailed } from './actions/config';
import { createMediaLibrary, insertMedia } from './actions/mediaLibrary'; import { createMediaLibrary, insertMedia } from './actions/mediaLibrary';
import { MediaLibraryInstance, State } from './types/redux'; import { MediaLibraryInstance } from './types/redux';
type MediaLibraryOptions = {}; type MediaLibraryOptions = {};
@ -38,10 +38,12 @@ const initializeMediaLibrary = once(async function initializeMediaLibrary(name,
}); });
store.subscribe(() => { store.subscribe(() => {
const state = store.getState() as State; const state = store.getState();
const mediaLibraryName = state.config.getIn(['media_library', 'name']); if (state) {
if (mediaLibraryName && !state.mediaLibrary.get('externalLibrary')) { const mediaLibraryName = state.config.media_library?.name;
const mediaLibraryConfig = state.config.get('media_library').toJS(); if (mediaLibraryName && !state.mediaLibrary.get('externalLibrary')) {
initializeMediaLibrary(mediaLibraryName, mediaLibraryConfig); const mediaLibraryConfig = state.config.media_library;
initializeMediaLibrary(mediaLibraryName, mediaLibraryConfig);
}
} }
}); });

View File

@ -1,5 +1,5 @@
import { OrderedMap, fromJS } from 'immutable'; import { fromJS, Map } from 'immutable';
import { configLoaded } from 'Actions/config'; import { configLoaded } from '../../actions/config';
import collections, { import collections, {
selectAllowDeletion, selectAllowDeletion,
selectEntryPath, selectEntryPath,
@ -11,39 +11,51 @@ import collections, {
selectField, selectField,
updateFieldByKey, updateFieldByKey,
} from '../collections'; } from '../collections';
import { FILES, FOLDER } from 'Constants/collectionTypes'; import { FILES, FOLDER } from '../../constants/collectionTypes';
describe('collections', () => { describe('collections', () => {
it('should handle an empty state', () => { it('should handle an empty state', () => {
expect(collections(undefined, {})).toEqual(null); expect(collections(undefined, {})).toEqual(Map());
}); });
it('should load the collections from the config', () => { it('should load the collections from the config', () => {
expect( expect(
collections( collections(
undefined, undefined,
configLoaded( configLoaded({
fromJS({ collections: [
collections: [ {
{ name: 'posts',
name: 'posts', folder: '_posts',
folder: '_posts', fields: [{ name: 'title', widget: 'string' }],
fields: [{ name: 'title', widget: 'string' }], },
}, ],
],
}),
),
),
).toEqual(
OrderedMap({
posts: fromJS({
name: 'posts',
folder: '_posts',
fields: [{ name: 'title', widget: 'string' }],
type: FOLDER,
}), }),
).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', () => { describe('selectAllowDeletions', () => {
@ -234,11 +246,11 @@ describe('collections', () => {
sanitize_replacement: '-', sanitize_replacement: '-',
}; };
const config = fromJS({ slug, media_folder: '/static/img' }); const config = { slug, media_folder: '/static/img' };
it('should return fields and collection folders', () => { it('should return fields and collection folders', () => {
expect( expect(
selectMediaFolders( selectMediaFolders(
{ config }, config,
fromJS({ fromJS({
folder: 'posts', folder: 'posts',
media_folder: '{{media_folder}}/general/', media_folder: '{{media_folder}}/general/',
@ -265,7 +277,7 @@ describe('collections', () => {
it('should return fields, file and collection folders', () => { it('should return fields, file and collection folders', () => {
expect( expect(
selectMediaFolders( selectMediaFolders(
{ config }, config,
fromJS({ fromJS({
media_folder: '{{media_folder}}/general/', media_folder: '{{media_folder}}/general/',
files: [ files: [

View File

@ -1,29 +1,38 @@
import { Map } from 'immutable'; import { configLoaded, configLoading, configFailed } from '../../actions/config';
import { configLoaded, configLoading, configFailed } from 'Actions/config'; import config, { selectLocale } from '../config';
import config, { selectLocale } from 'Reducers/config';
describe('config', () => { describe('config', () => {
it('should handle an empty state', () => { 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', () => { it('should handle an update', () => {
expect(config(Map({ a: 'b', c: 'd' }), configLoaded(Map({ a: 'changed', e: 'new' })))).toEqual( expect(
Map({ a: 'changed', e: 'new' }), 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', () => { 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', () => { it('should handle an error', () => {
expect(config(Map(), configFailed(new Error('Config could not be loaded')))).toEqual( expect(
Map({ error: 'Error: Config could not be loaded' }), 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', () => { 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 { OrderedMap, fromJS } from 'immutable';
import * as actions from 'Actions/entries'; import * as actions from '../../actions/entries';
import reducer, { import reducer, {
selectMediaFolder, selectMediaFolder,
selectMediaFilePath, selectMediaFilePath,
@ -76,7 +76,7 @@ describe('entries', () => {
it("should return global media folder when collection doesn't specify media_folder", () => { it("should return global media folder when collection doesn't specify media_folder", () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts' }), fromJS({ name: 'posts' }),
undefined, undefined,
undefined, undefined,
@ -87,7 +87,7 @@ describe('entries', () => {
it('should return draft media folder when collection specifies media_folder and entry is undefined', () => { it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined, undefined,
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', () => { it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
fromJS({ path: 'posts/title/index.md' }), fromJS({ path: 'posts/title/index.md' }),
undefined, undefined,
@ -121,7 +121,7 @@ describe('entries', () => {
const field = fromJS({ media_folder: '' }); const field = fromJS({ media_folder: '' });
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: '/static/img' }), { media_folder: '/static/img' },
fromJS({ fromJS({
name: 'other', name: 'other',
folder: 'other', folder: 'other',
@ -137,7 +137,7 @@ describe('entries', () => {
it('should return collection absolute media folder without leading slash', () => { it('should return collection absolute media folder without leading slash', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: '/static/Images' }), { media_folder: '/static/Images' },
fromJS({ fromJS({
name: 'getting-started', name: 'getting-started',
folder: 'src/docs/getting-started', folder: 'src/docs/getting-started',
@ -169,7 +169,7 @@ describe('entries', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media', slug: slugConfig }), { media_folder: 'static/media', slug: slugConfig },
collection, collection,
entry, entry,
undefined, undefined,
@ -196,7 +196,7 @@ describe('entries', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: '/static/images', slug: slugConfig }), { media_folder: '/static/images', slug: slugConfig },
collection, collection,
entry, entry,
undefined, undefined,
@ -229,7 +229,7 @@ describe('entries', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media', slug: slugConfig }), { media_folder: 'static/media', slug: slugConfig },
collection, collection,
entry, entry,
collection.get('fields').get(0), collection.get('fields').get(0),
@ -258,7 +258,7 @@ describe('entries', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: '/static/img/', slug: slugConfig }), { media_folder: '/static/img/', slug: slugConfig },
collection, collection,
entry, entry,
undefined, undefined,
@ -267,7 +267,7 @@ describe('entries', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/img/', slug: slugConfig }), { media_folder: 'static/img/', slug: slugConfig },
collection, collection,
entry, entry,
undefined, undefined,
@ -278,7 +278,7 @@ describe('entries', () => {
it('should handle file media_folder', () => { it('should handle file media_folder', () => {
expect( expect(
selectMediaFolder( selectMediaFolder(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }), fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }),
fromJS({ path: 'posts/title/index.md', slug: 'index' }), fromJS({ path: 'posts/title/index.md', slug: 'index' }),
undefined, undefined,
@ -302,7 +302,7 @@ describe('entries', () => {
}); });
const args = [ const args = [
fromJS({ media_folder: '/static/img' }), { media_folder: '/static/img' },
fromJS({ fromJS({
name: 'general', name: 'general',
media_folder: '{{media_folder}}/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', () => { it('should resolve path from global media folder for collection with no media folder', () => {
expect( expect(
selectMediaFilePath( selectMediaFilePath(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts' }), fromJS({ name: 'posts', folder: 'posts' }),
undefined, undefined,
'image.png', 'image.png',
@ -357,7 +357,7 @@ describe('entries', () => {
it('should resolve path from collection media folder for collection with media folder', () => { it('should resolve path from collection media folder for collection with media folder', () => {
expect( expect(
selectMediaFilePath( selectMediaFilePath(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
undefined, undefined,
'image.png', 'image.png',
@ -369,7 +369,7 @@ describe('entries', () => {
it('should handle relative media_folder', () => { it('should handle relative media_folder', () => {
expect( expect(
selectMediaFilePath( selectMediaFilePath(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }), fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
fromJS({ path: 'posts/title/index.md' }), fromJS({ path: 'posts/title/index.md' }),
'image.png', 'image.png',
@ -382,7 +382,7 @@ describe('entries', () => {
const field = fromJS({ media_folder: '../../static/media/' }); const field = fromJS({ media_folder: '../../static/media/' });
expect( expect(
selectMediaFilePath( selectMediaFilePath(
fromJS({ media_folder: 'static/media' }), { media_folder: 'static/media' },
fromJS({ name: 'posts', folder: 'posts', fields: [field] }), fromJS({ name: 'posts', folder: 'posts', fields: [field] }),
fromJS({ path: 'posts/title/index.md' }), fromJS({ path: 'posts/title/index.md' }),
'image.png', 'image.png',
@ -402,7 +402,7 @@ describe('entries', () => {
it('should resolve path from public folder for collection with no media folder', () => { it('should resolve path from public folder for collection with no media folder', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }), { public_folder: '/media' },
null, null,
'/media/image.png', '/media/image.png',
undefined, undefined,
@ -414,7 +414,7 @@ describe('entries', () => {
it('should resolve path from collection public folder for collection with public folder', () => { it('should resolve path from collection public folder for collection with public folder', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }), { public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '' }), fromJS({ name: 'posts', folder: 'posts', public_folder: '' }),
'image.png', 'image.png',
undefined, undefined,
@ -426,7 +426,7 @@ describe('entries', () => {
it('should handle relative public_folder', () => { it('should handle relative public_folder', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: '/media' }), { public_folder: '/media' },
fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }), fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
'image.png', 'image.png',
undefined, undefined,
@ -455,7 +455,7 @@ describe('entries', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media', slug: slugConfig }), { public_folder: 'static/media', slug: slugConfig },
collection, collection,
'image.png', 'image.png',
entry, entry,
@ -489,7 +489,7 @@ describe('entries', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media', slug: slugConfig }), { public_folder: 'static/media', slug: slugConfig },
collection, collection,
'image.png', 'image.png',
entry, entry,
@ -523,7 +523,7 @@ describe('entries', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/', slug: slugConfig }), { public_folder: 'static/media/', slug: slugConfig },
collection, collection,
'image.png', 'image.png',
entry, entry,
@ -551,7 +551,7 @@ describe('entries', () => {
expect( expect(
selectMediaFilePublicPath( selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/' }), { public_folder: 'static/media/' },
collection, collection,
'image.png', 'image.png',
entry, entry,

View File

@ -1,13 +1,13 @@
import { fromJS } from 'immutable';
import integrations from '../integrations'; import integrations from '../integrations';
import { CONFIG_SUCCESS } from '../../actions/config'; import { CONFIG_SUCCESS, ConfigAction } from '../../actions/config';
import { FOLDER } from '../../constants/collectionTypes';
describe('integrations', () => { describe('integrations', () => {
it('should return default state when no integrations', () => { it('should return default state when no integrations', () => {
const result = integrations(null, { const result = integrations(null, {
type: CONFIG_SUCCESS, type: CONFIG_SUCCESS,
payload: fromJS({ integrations: [] }), payload: { integrations: [] },
}); } as ConfigAction);
expect(result && result.toJS()).toEqual({ expect(result && result.toJS()).toEqual({
providers: {}, providers: {},
hooks: {}, hooks: {},
@ -17,7 +17,7 @@ describe('integrations', () => {
it('should return hooks and providers map when has integrations', () => { it('should return hooks and providers map when has integrations', () => {
const result = integrations(null, { const result = integrations(null, {
type: CONFIG_SUCCESS, type: CONFIG_SUCCESS,
payload: fromJS({ payload: {
integrations: [ integrations: [
{ {
hooks: ['listEntries'], hooks: ['listEntries'],
@ -39,9 +39,13 @@ describe('integrations', () => {
getSignedFormURL: 'https://asset.store.com/signedUrl', 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({ expect(result && result.toJS()).toEqual({
providers: { 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 { get, escapeRegExp } from 'lodash';
import consoleError from '../lib/consoleError'; 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 { FILES, FOLDER } from '../constants/collectionTypes';
import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference'; import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
import { formatExtensions } from '../formats/formats'; import { formatExtensions } from '../formats/formats';
import { import {
CollectionsAction,
Collection, Collection,
Collections,
CollectionFiles, CollectionFiles,
EntryField, EntryField,
State,
EntryMap, EntryMap,
ViewFilter, ViewFilter,
ViewGroup, ViewGroup,
CmsConfig,
} from '../types/redux'; } from '../types/redux';
import { selectMediaFolder } from './entries'; import { selectMediaFolder } from './entries';
import { stringTemplate } from 'netlify-cms-lib-widgets'; import { stringTemplate } from 'netlify-cms-lib-widgets';
@ -22,29 +23,17 @@ import { Backend } from '../backend';
const { keyToPathArray } = stringTemplate; const { keyToPathArray } = stringTemplate;
function collections(state = null, action: CollectionsAction) { const defaultState: Collections = fromJS({});
function collections(state = defaultState, action: ConfigAction) {
switch (action.type) { switch (action.type) {
case CONFIG_SUCCESS: { case CONFIG_SUCCESS: {
const configCollections = action.payload const collections = action.payload.collections;
? action.payload.get('collections') let newState = OrderedMap({});
: List<Collection>(); collections.forEach(collection => {
newState = newState.set(collection.name, fromJS(collection));
return ( });
configCollections return newState;
.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'))
);
} }
default: default:
return state; return state;
@ -165,19 +154,19 @@ export function selectFieldsWithMediaFolders(collection: Collection, slug: strin
return []; 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 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')) { if (collection.has('files')) {
const file = getFileFromSlug(collection, entry.get('slug')); const file = getFileFromSlug(collection, entry.get('slug'));
if (file) { if (file) {
folders.unshift(selectMediaFolder(state.config, collection, entry, undefined)); folders.unshift(selectMediaFolder(config, collection, entry, undefined));
} }
} }
if (collection.has('media_folder')) { if (collection.has('media_folder')) {
// stop evaluating media folders at collection level // stop evaluating media folders at collection level
collection = collection.delete('files'); collection = collection.delete('files');
folders.unshift(selectMediaFolder(state.config, collection, entry, undefined)); folders.unshift(selectMediaFolder(config, collection, entry, undefined));
} }
return Set(folders).toArray(); return Set(folders).toArray();
@ -317,10 +306,10 @@ export function updateFieldByKey(
export function selectIdentifier(collection: Collection) { export function selectIdentifier(collection: Collection) {
const identifier = collection.get('identifier_field'); const identifier = collection.get('identifier_field');
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS; const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : [...IDENTIFIER_FIELDS];
const fieldNames = getFieldsNames(collection.get('fields', List<EntryField>()).toArray()); const fieldNames = getFieldsNames(collection.get('fields', List()).toArray());
return identifierFields.find(id => 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)); return titleField && entryData.getIn(keyToPathArray(titleField));
} }
export const COMMIT_AUTHOR = 'commit_author';
export const COMMIT_DATE = 'commit_date';
export function selectDefaultSortableFields( export function selectDefaultSortableFields(
collection: Collection, collection: Collection,
backend: Backend, backend: Backend,

View File

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

View File

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

View File

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

View File

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

View File

@ -362,6 +362,14 @@ export interface CmsBackend {
cms_label_prefix?: string; cms_label_prefix?: string;
squash_merges?: boolean; squash_merges?: boolean;
proxy_url?: string; proxy_url?: string;
commit_messages?: {
create?: string;
update?: string;
delete?: string;
uploadMedia?: string;
deleteMedia?: string;
openAuthoring?: string;
};
} }
export interface CmsSlug { export interface CmsSlug {
@ -389,12 +397,22 @@ export interface CmsConfig {
media_library?: CmsMediaLibrary; media_library?: CmsMediaLibrary;
publish_mode?: CmsPublishMode; publish_mode?: CmsPublishMode;
load_config_file?: boolean; load_config_file?: boolean;
integrations?: {
hooks: string[];
provider: string;
collections?: '*' | string[];
applicationID?: string;
apiKey?: string;
getSignedFormURL?: string;
}[];
slug?: CmsSlug; slug?: CmsSlug;
i18n?: CmsI18nConfig; i18n?: CmsI18nConfig;
local_backend?: boolean | CmsLocalBackend; local_backend?: boolean | CmsLocalBackend;
editor?: { editor?: {
preview?: boolean; preview?: boolean;
}; };
error: string | undefined;
isFetching: boolean;
} }
export type CmsMediaLibraryOptions = unknown; // TODO: type properly export type CmsMediaLibraryOptions = unknown; // TODO: type properly
@ -675,7 +693,7 @@ export type Cursors = StaticallyTypedRecord<{}>;
export interface State { export interface State {
auth: Auth; auth: Auth;
config: Config; config: CmsConfig;
cursors: Cursors; cursors: Cursors;
collections: Collections; collections: Collections;
deploys: Deploys; deploys: Deploys;
@ -690,20 +708,12 @@ export interface State {
status: Status; status: Status;
} }
export interface ConfigAction extends Action<string> {
payload: Map<string, boolean>;
}
export interface Integration { export interface Integration {
hooks: string[]; hooks: string[];
collections?: string | string[]; collections?: string | string[];
provider: string; provider: string;
} }
export interface IntegrationsAction extends Action<string> {
payload: Config;
}
interface EntryPayload { interface EntryPayload {
collection: string; collection: string;
} }
@ -785,12 +795,8 @@ export interface EntriesAction extends Action<string> {
}; };
} }
export interface CollectionsAction extends Action<string> {
payload?: StaticallyTypedRecord<{ collections: List<Collection> }>;
}
export interface EditorialWorkflowAction extends Action<string> { export interface EditorialWorkflowAction extends Action<string> {
payload?: StaticallyTypedRecord<{ publish_mode: string }> & { payload?: CmsConfig & {
collection: string; collection: string;
entry: { slug: string }; entry: { slug: string };
} & { } & {