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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -215,7 +215,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
const backend = currentBackend(state.config);
const integration = selectIntegration(state, null, 'assetStore');
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 editingDraft = selectEditingDraft(state.entryDraft);

View File

@ -1,6 +1,26 @@
import { attempt, flatten, isError, uniq, trim, sortBy, get, set } from 'lodash';
import { List, Map, fromJS, Set } from 'immutable';
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 { selectUseWorkflow } from './reducers/config';
import { selectMediaFilePath, selectEntry } from './reducers/entries';
@ -21,35 +41,14 @@ import { createEntry, EntryValue } from './valueObjects/Entry';
import { sanitizeChar } from './lib/urlHelper';
import { getBackend, invokeEvent } from './lib/registry';
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 { stringTemplate } from 'netlify-cms-lib-widgets';
import {
Collection,
CmsConfig,
EntryMap,
Config,
FilterRule,
Collections,
EntryDraft,
Collection,
Collections,
CollectionFile,
State,
EntryField,
@ -73,7 +72,7 @@ const { extractTemplateVars, dateParsers, expandPath } = stringTemplate;
function updateAssetProxies(
assetProxies: AssetProxy[],
config: Config,
config: CmsConfig,
collection: Collection,
entryDraft: EntryDraft,
path: string,
@ -238,9 +237,9 @@ interface AuthStore {
}
interface BackendOptions {
backendName?: string;
authStore?: AuthStore | null;
config?: Config;
backendName: string;
config: CmsConfig;
authStore?: AuthStore;
}
export interface MediaFile {
@ -263,7 +262,7 @@ interface BackupEntry {
}
interface PersistArgs {
config: Config;
config: CmsConfig;
collection: Collection;
entryDraft: EntryDraft;
assetProxies: AssetProxy[];
@ -279,7 +278,7 @@ interface ImplementationInitOptions {
}
type Implementation = BackendImplementation & {
init: (config: ImplementationConfig, options: ImplementationInitOptions) => Implementation;
init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation;
};
function prepareMetaPath(path: string, collection: Collection) {
@ -305,24 +304,21 @@ function collectionDepth(collection: Collection) {
export class Backend {
implementation: Implementation;
backendName: string;
authStore: AuthStore | null;
config: Config;
config: CmsConfig;
authStore?: AuthStore;
user?: User | null;
backupSync: AsyncLock;
constructor(
implementation: Implementation,
{ backendName, authStore = null, config }: BackendOptions = {},
) {
constructor(implementation: Implementation, { backendName, authStore, config }: BackendOptions) {
// We can't reliably run this on exit, so we do cleanup on load.
this.deleteAnonymousBackup();
this.config = config as Config;
this.implementation = implementation.init(this.config.toJS(), {
this.config = config;
this.implementation = implementation.init(this.config, {
useWorkflow: selectUseWorkflow(this.config),
updateUserCredentials: this.updateUserCredentials,
initialWorkflowStatus: status.first(),
});
this.backendName = backendName as string;
this.backendName = backendName;
this.authStore = authStore;
if (this.implementation === null) {
throw new Error('Cannot instantiate a Backend with no implementation');
@ -436,11 +432,11 @@ export class Backend {
async generateUniqueSlug(
collection: Collection,
entryData: Map<string, unknown>,
config: Config,
config: CmsConfig,
usedSlugs: List<string>,
customPath: string | undefined,
) {
const slugConfig = config.get('slug');
const slugConfig = config.slug;
let slug: string;
if (customPath) {
slug = slugFromCustomPath(collection, customPath);
@ -944,7 +940,7 @@ export class Backend {
async processEntry(state: State, collection: Collection, entry: EntryValue) {
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) {
const files = await Promise.all(
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.
*/
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 {
url: previewUrlFormatter(baseUrl, collection, slug, this.config.get('slug'), entry),
url: previewUrlFormatter(baseUrl, collection, slug, entry, this.config.slug),
status: 'SUCCESS',
};
}
@ -1005,7 +1001,7 @@ export class Backend {
* If the registered backend does not provide a `getDeployPreview` method, or
* `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;
}
@ -1019,7 +1015,7 @@ export class Backend {
count++;
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
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.
*/
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.
*/
@ -1182,7 +1178,7 @@ export class Backend {
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 options = {
commitMessage: commitMessageFormatter(
@ -1233,7 +1229,7 @@ export class Backend {
await this.invokePostUnpublishEvent(entry);
}
async deleteMedia(config: Config, path: string) {
async deleteMedia(config: CmsConfig, path: string) {
const user = (await this.currentUser()) as User;
const commitMessage = commitMessageFormatter(
'deleteMedia',
@ -1310,12 +1306,12 @@ export class Backend {
}
}
export function resolveBackend(config: Config) {
const name = config.getIn(['backend', 'name']);
if (name == null) {
export function resolveBackend(config: CmsConfig) {
if (!config.backend.name) {
throw new Error('No backend defined in configuration');
}
const { name } = config.backend;
const authStore = new LocalStorageAuthStore();
const backend = getBackend(name);
@ -1329,7 +1325,7 @@ export function resolveBackend(config: Config) {
export const currentBackend = (function() {
let backend: Backend;
return (config: Config) => {
return (config: CmsConfig) => {
if (backend) {
return backend;
}

View File

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

View File

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

View File

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

View File

@ -74,7 +74,7 @@ const SuggestionItem = styled.li(
padding: 6px 6px 6px 32px;
cursor: pointer;
position: relative;
&:hover {
color: ${colors.active};
background-color: ${colors.activeBackground};
@ -88,7 +88,7 @@ const SuggestionDivider = styled.div`
class CollectionSearch extends React.Component {
static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired,
collections: ImmutablePropTypes.map.isRequired,
collection: ImmutablePropTypes.map,
searchTerm: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { loadUnpublishedEntry, persistUnpublishedEntry } from 'Actions/editorial
function mapStateToProps(state, ownProps) {
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 returnObj = {
isEditorialWorkflow,

View File

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

View File

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

View File

@ -53,7 +53,7 @@ const WorkflowTopDescription = styled.p`
class Workflow extends Component {
static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired,
collections: ImmutablePropTypes.map.isRequired,
isEditorialWorkflow: PropTypes.bool.isRequired,
isOpenAuthoring: PropTypes.bool,
isFetching: PropTypes.bool,
@ -137,7 +137,7 @@ class Workflow extends Component {
function mapStateToProps(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 returnObj = { collections, isEditorialWorkflow, isOpenAuthoring };

View File

@ -135,7 +135,7 @@ class WorkflowList extends React.Component {
handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
isOpenAuthoring: PropTypes.bool,
collections: ImmutablePropTypes.orderedMap.isRequired,
collections: ImmutablePropTypes.map.isRequired,
};
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';
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 = {
title: {

View File

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

View File

@ -1,4 +1,3 @@
import { Map } from 'immutable';
import { sanitizeURI, sanitizeSlug, sanitizeChar } from '../urlHelper';
describe('sanitizeURI', () => {
@ -60,79 +59,80 @@ describe('sanitizeSlug', () => {
});
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.',
);
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: [] }))).toThrowError(
expect(() => sanitizeSlug('test', { sanitize_replacement: [] })).toThrowError(
'`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.',
);
expect(() => sanitizeSlug('test', Map({ sanitize_replacement: null }))).toThrowError(
expect(() => sanitizeSlug('test', { sanitize_replacement: null })).toThrowError(
'`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.',
);
// 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', Map({ sanitize_replacement: () => {} }))).toThrowError(
expect(() => sanitizeSlug('test', { sanitize_replacement: () => {} })).toThrowError(
'`options.replacement` must be a string.',
);
});
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',
);
});
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', () => {
expect(
sanitizeSlug('ěščřžý日本語のタイトル', Map({ ...slugConfig, encoding: 'ascii' })),
).toEqual('');
expect(sanitizeSlug('ěščřžý日本語のタイトル', { ...slugConfig, encoding: 'ascii' })).toEqual(
'',
);
});
it('should clean accents and strip non-latin chars in "ascii" mode with `clean_accents` set', () => {
expect(
sanitizeSlug(
'ěščřžý日本語のタイトル',
Map({ ...slugConfig, encoding: 'ascii', clean_accents: true }),
),
sanitizeSlug('ěščřžý日本語のタイトル', {
...slugConfig,
encoding: 'ascii',
clean_accents: true,
}),
).toEqual('escrzy');
});
it('removes double replacements', () => {
expect(sanitizeSlug('test--test', Map(slugConfig))).toEqual('test-test');
expect(sanitizeSlug('test test', Map(slugConfig))).toEqual('test-test');
expect(sanitizeSlug('test--test', slugConfig)).toEqual('test-test');
expect(sanitizeSlug('test test', slugConfig)).toEqual('test-test');
});
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', () => {
expect(sanitizeSlug('"test" test', Map(slugConfig))).toEqual('test-test');
expect(sanitizeSlug('"test" test', slugConfig)).toEqual('test-test');
});
it('uses alternate replacements', () => {
expect(
sanitizeSlug('test test ', Map({ ...slugConfig, sanitize_replacement: '_' })),
).toEqual('test_test');
expect(sanitizeSlug('test test ', { ...slugConfig, sanitize_replacement: '_' })).toEqual(
'test_test',
);
});
});
describe('sanitizeChar', () => {
it('should sanitize whitespace with default replacement', () => {
expect(sanitizeChar(' ', Map(slugConfig))).toBe('-');
expect(sanitizeChar(' ', slugConfig)).toBe('-');
});
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 {
selectIdentifier,
selectField,
COMMIT_AUTHOR,
COMMIT_DATE,
selectInferedField,
getFileFromSlug,
} 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 { FILES } from '../constants/collectionTypes';
import { COMMIT_AUTHOR, COMMIT_DATE } from '../constants/commitProps';
const {
compileStringTemplate,
@ -22,14 +21,14 @@ const {
addFileTemplateFields,
} = stringTemplate;
const commitMessageTemplates = Map({
const commitMessageTemplates = {
create: 'Create {{collection}} “{{slug}}”',
update: 'Update {{collection}} “{{slug}}”',
delete: 'Delete {{collection}} “{{slug}}”',
uploadMedia: 'Upload “{{path}}”',
deleteMedia: 'Delete “{{path}}”',
openAuthoring: '{{message}}',
});
} as const;
const variableRegex = /\{\{([^}]+)\}\}/g;
@ -42,16 +41,14 @@ type Options = {
};
export function commitMessageFormatter(
type: string,
config: Config,
type: keyof typeof commitMessageTemplates,
config: CmsConfig,
{ slug, path, collection, authorLogin, authorName }: Options,
isOpenAuthoring?: boolean,
) {
const templates = commitMessageTemplates.merge(
config.getIn(['backend', 'commit_messages'], Map<string, string>()),
);
const templates = { ...commitMessageTemplates, ...(config.backend.commit_messages || {}) };
const commitMessage = templates.get(type).replace(variableRegex, (_, variable) => {
const commitMessage = templates[type].replace(variableRegex, (_, variable) => {
switch (variable) {
case 'slug':
return slug || '';
@ -73,7 +70,7 @@ export function commitMessageFormatter(
return commitMessage;
}
const message = templates.get('openAuthoring').replace(variableRegex, (_, variable) => {
const message = templates.openAuthoring.replace(variableRegex, (_, variable) => {
switch (variable) {
case 'message':
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) =>
ignoreValues.includes(value)
ignoreValues && ignoreValues.includes(value)
? value
: flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
}
@ -115,7 +112,7 @@ export function getProcessSegment(slugConfig: SlugConfig, ignoreValues: string[]
export function slugFormatter(
collection: Collection,
entryData: Map<string, unknown>,
slugConfig: SlugConfig,
slugConfig?: CmsSlug,
) {
const slugTemplate = collection.get('slug') || '{{slug}}';
@ -144,8 +141,8 @@ export function previewUrlFormatter(
baseUrl: string,
collection: Collection,
slug: string,
slugConfig: SlugConfig,
entry: EntryMap,
slugConfig?: CmsSlug,
) {
/**
* Preview URL can't be created without `baseUrl`. This makes preview URLs
@ -239,7 +236,7 @@ export function folderFormatter(
collection: Collection,
defaultFolder: string,
folderKey: string,
slugConfig: SlugConfig,
slugConfig?: CmsSlug,
) {
if (!entry || !entry.get('data')) {
return folderTemplate;

View File

@ -2,7 +2,7 @@ import url from 'url';
import diacritics from 'diacritics';
import sanitizeFilename from 'sanitize-filename';
import { isString, escapeRegExp, flow, partialRight } from 'lodash';
import { SlugConfig } from '../types/redux';
import { CmsSlug } from '../types/redux';
function getUrl(urlString: string, direct?: boolean) {
return `${direct ? '/#' : ''}${urlString}`;
@ -16,7 +16,7 @@ export function getNewEntryUrl(collectionName: string, direct?: boolean) {
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);
parsedUrl.query = { ...parsedUrl.query, ...params };
return url.format(parsedUrl);
@ -64,7 +64,12 @@ export function getCharReplacer(encoding: string, replacement: string) {
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.
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)) {
throw new Error('The input slug must be a string.');
}
@ -79,21 +84,18 @@ export function sanitizeURI(str: string, { replacement = '', encoding = 'unicode
.join('');
}
export function sanitizeChar(char: string, options: SlugConfig) {
const encoding = options.get('encoding');
const replacement = options.get('sanitize_replacement');
export function sanitizeChar(char: string, options?: CmsSlug) {
const { encoding = 'unicode', sanitize_replacement: replacement = '' } = options || {};
return getCharReplacer(encoding, replacement)(char);
}
export function sanitizeSlug(str: string, options: SlugConfig) {
export function sanitizeSlug(str: string, options?: CmsSlug) {
if (!isString(str)) {
throw new Error('The input slug must be a string.');
}
const encoding = options.get('encoding');
const stripDiacritics = options.get('clean_accents');
const replacement = options.get('sanitize_replacement');
const { encoding, clean_accents: stripDiacritics, sanitize_replacement: replacement } =
options || {};
const sanitizedSlug = flow([
...(stripDiacritics ? [diacritics.remove] : []),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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