refactor: convert config actions to TypeScript (#4950)
This commit is contained in:
parent
41e82c2280
commit
0ac17bfc25
@ -69,7 +69,8 @@ module.exports = {
|
||||
'require-atomic-updates': [0],
|
||||
'import/no-unresolved': [0],
|
||||
'@typescript-eslint/no-non-null-assertion': [0],
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/camelcase': [0],
|
||||
'@typescript-eslint/explicit-function-return-type': [0],
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
'error',
|
||||
{ functions: false, classes: true, variables: true },
|
||||
|
52
packages/netlify-cms-core/index.d.ts
vendored
52
packages/netlify-cms-core/index.d.ts
vendored
@ -2,6 +2,7 @@
|
||||
declare module 'netlify-cms-core' {
|
||||
import React, { ComponentType } from 'react';
|
||||
import { List, Map } from 'immutable';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
|
||||
export type CmsBackendType =
|
||||
| 'azure'
|
||||
@ -9,7 +10,8 @@ declare module 'netlify-cms-core' {
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'bitbucket'
|
||||
| 'test-repo';
|
||||
| 'test-repo'
|
||||
| 'proxy';
|
||||
|
||||
export type CmsMapWidgetType = 'Point' | 'LineString' | 'Polygon';
|
||||
|
||||
@ -62,7 +64,10 @@ declare module 'netlify-cms-core' {
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
pattern?: [string, string];
|
||||
i18n?: boolean | 'translate' | 'duplicate';
|
||||
i18n?: boolean | 'translate' | 'duplicate' | 'none';
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface CmsFieldBoolean {
|
||||
@ -236,6 +241,15 @@ declare module 'netlify-cms-core' {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface CmsFieldMeta {
|
||||
name: string;
|
||||
label: string;
|
||||
widget: string;
|
||||
required: boolean;
|
||||
index_file: string;
|
||||
meta: boolean;
|
||||
}
|
||||
|
||||
export type CmsField = CmsFieldBase &
|
||||
(
|
||||
| CmsFieldBoolean
|
||||
@ -252,6 +266,7 @@ declare module 'netlify-cms-core' {
|
||||
| CmsFieldSelect
|
||||
| CmsFieldHidden
|
||||
| CmsFieldStringOrText
|
||||
| CmsFieldMeta
|
||||
);
|
||||
|
||||
export interface CmsCollectionFile {
|
||||
@ -261,6 +276,25 @@ declare module 'netlify-cms-core' {
|
||||
fields: CmsField[];
|
||||
label_singular?: string;
|
||||
description?: string;
|
||||
preview_path?: string;
|
||||
preview_path_date_field?: string;
|
||||
i18n?: boolean | CmsI18nConfig;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
}
|
||||
|
||||
export interface ViewFilter {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ViewGroup {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CmsCollection {
|
||||
@ -280,6 +314,12 @@ declare module 'netlify-cms-core' {
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
publish?: boolean;
|
||||
nested?: {
|
||||
depth: number;
|
||||
};
|
||||
type: typeof FOLDER | typeof FILES;
|
||||
meta?: { path?: { label: string; widget: string; index_file: string } };
|
||||
|
||||
/**
|
||||
* It accepts the following values: yml, yaml, toml, json, md, markdown, html
|
||||
@ -296,6 +336,8 @@ declare module 'netlify-cms-core' {
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
sortable_fields?: string[];
|
||||
view_filters?: ViewFilter[];
|
||||
view_groups?: ViewGroup[];
|
||||
i18n?: boolean | CmsI18nConfig;
|
||||
|
||||
/**
|
||||
@ -316,11 +358,13 @@ declare module 'netlify-cms-core' {
|
||||
auth_endpoint?: string;
|
||||
cms_label_prefix?: string;
|
||||
squash_merges?: boolean;
|
||||
proxy_url?: string;
|
||||
}
|
||||
|
||||
export interface CmsSlug {
|
||||
encoding?: CmsSlugEncoding;
|
||||
clean_accents?: boolean;
|
||||
sanitize_replacement?: string;
|
||||
}
|
||||
|
||||
export interface CmsLocalBackend {
|
||||
@ -341,9 +385,13 @@ declare module 'netlify-cms-core' {
|
||||
media_folder_relative?: boolean;
|
||||
media_library?: CmsMediaLibrary;
|
||||
publish_mode?: CmsPublishMode;
|
||||
load_config_file?: boolean;
|
||||
slug?: CmsSlug;
|
||||
i18n?: CmsI18nConfig;
|
||||
local_backend?: boolean | CmsLocalBackend;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { fromJS } from 'immutable';
|
||||
import {
|
||||
loadConfig,
|
||||
parseConfig,
|
||||
normalizeConfig,
|
||||
applyDefaults,
|
||||
@ -7,6 +9,8 @@ import {
|
||||
handleLocalBackend,
|
||||
} from '../config';
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
jest.mock('../../backend', () => {
|
||||
@ -14,6 +18,7 @@ jest.mock('../../backend', () => {
|
||||
resolveBackend: jest.fn(() => ({ isGitBackend: jest.fn(() => true) })),
|
||||
};
|
||||
});
|
||||
jest.mock('../../constants/configSchema');
|
||||
|
||||
describe('config', () => {
|
||||
describe('parseConfig', () => {
|
||||
@ -903,4 +908,88 @@ describe('config', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig', () => {
|
||||
beforeEach(() => {
|
||||
document.querySelector = jest.fn();
|
||||
global.fetch = jest.fn();
|
||||
});
|
||||
|
||||
test(`should fetch default 'config.yml'`, async () => {
|
||||
const dispatch = jest.fn();
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve(yaml.dump({ backend: { repo: 'test-repo' } })),
|
||||
headers: new Headers(),
|
||||
});
|
||||
await loadConfig()(dispatch);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('config.yml', { credentials: 'same-origin' });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' });
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'CONFIG_SUCCESS',
|
||||
payload: fromJS({
|
||||
backend: { repo: 'test-repo' },
|
||||
collections: [],
|
||||
publish_mode: 'simple',
|
||||
slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' },
|
||||
public_folder: '/',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test(`should fetch from custom 'config.yml'`, async () => {
|
||||
const dispatch = jest.fn();
|
||||
|
||||
document.querySelector.mockReturnValue({ type: 'text/yaml', href: 'custom-config.yml' });
|
||||
global.fetch.mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve(yaml.dump({ backend: { repo: 'github' } })),
|
||||
headers: new Headers(),
|
||||
});
|
||||
await loadConfig()(dispatch);
|
||||
|
||||
expect(document.querySelector).toHaveBeenCalledTimes(1);
|
||||
expect(document.querySelector).toHaveBeenCalledWith('link[rel="cms-config-url"]');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('custom-config.yml', {
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' });
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'CONFIG_SUCCESS',
|
||||
payload: fromJS({
|
||||
backend: { repo: 'github' },
|
||||
collections: [],
|
||||
publish_mode: 'simple',
|
||||
slug: { encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' },
|
||||
public_folder: '/',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test(`should throw on failure to fetch 'config.yml'`, async () => {
|
||||
const dispatch = jest.fn();
|
||||
|
||||
global.fetch.mockRejectedValue(new Error('Failed to fetch'));
|
||||
await expect(() => loadConfig()(dispatch)).rejects.toEqual(
|
||||
new Error('Failed to load config.yml (Failed to fetch)'),
|
||||
);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: 'CONFIG_REQUEST' });
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'CONFIG_FAILURE',
|
||||
error: 'Error loading config',
|
||||
payload: new Error('Failed to load config.yml (Failed to fetch)'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,9 @@ import yaml from 'yaml';
|
||||
import { fromJS } from 'immutable';
|
||||
import deepmerge from 'deepmerge';
|
||||
import { produce } from 'immer';
|
||||
import { trimStart, trim, get, isPlainObject, isEmpty } from 'lodash';
|
||||
import { trimStart, trim, isEmpty } from 'lodash';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { SIMPLE as SIMPLE_PUBLISH_MODE } from '../constants/publishModes';
|
||||
import { validateConfig } from '../constants/configSchema';
|
||||
import { selectDefaultSortableFields } from '../reducers/collections';
|
||||
@ -10,20 +12,43 @@ import { getIntegrations, selectIntegration } from '../reducers/integrations';
|
||||
import { resolveBackend } from '../backend';
|
||||
import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
import {
|
||||
CmsCollection,
|
||||
CmsConfig,
|
||||
CmsField,
|
||||
CmsFieldBase,
|
||||
CmsFieldObject,
|
||||
CmsFieldList,
|
||||
CmsI18nConfig,
|
||||
CmsPublishMode,
|
||||
CmsLocalBackend,
|
||||
State,
|
||||
} from '../types/redux';
|
||||
|
||||
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
|
||||
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
|
||||
export const CONFIG_FAILURE = 'CONFIG_FAILURE';
|
||||
|
||||
function traverseFieldsJS(fields, updater) {
|
||||
function isObjectField(field: CmsField): field is CmsFieldBase & CmsFieldObject {
|
||||
return 'fields' in (field as CmsFieldObject);
|
||||
}
|
||||
|
||||
function isFieldList(field: CmsField): field is CmsFieldBase & CmsFieldList {
|
||||
return 'types' in (field as CmsFieldList) || 'field' in (field as CmsFieldList);
|
||||
}
|
||||
|
||||
function traverseFieldsJS<Field extends CmsField>(
|
||||
fields: Field[],
|
||||
updater: <T extends CmsField>(field: T) => T,
|
||||
): Field[] {
|
||||
return fields.map(field => {
|
||||
let newField = updater(field);
|
||||
if (newField.fields) {
|
||||
newField = { ...newField, fields: traverseFieldsJS(newField.fields, updater) };
|
||||
} else if (newField.field) {
|
||||
newField = { ...newField, field: traverseFieldsJS([newField.field], updater)[0] };
|
||||
} else if (newField.types) {
|
||||
newField = { ...newField, types: traverseFieldsJS(newField.types, updater) };
|
||||
const newField = updater(field);
|
||||
if (isObjectField(newField)) {
|
||||
return { ...newField, fields: traverseFieldsJS(newField.fields, updater) };
|
||||
} else if (isFieldList(newField) && newField.field) {
|
||||
return { ...newField, field: traverseFieldsJS([newField.field], updater)[0] };
|
||||
} else if (isFieldList(newField) && newField.types) {
|
||||
return { ...newField, types: traverseFieldsJS(newField.types, updater) };
|
||||
}
|
||||
|
||||
return newField;
|
||||
@ -31,18 +56,19 @@ function traverseFieldsJS(fields, updater) {
|
||||
}
|
||||
|
||||
function getConfigUrl() {
|
||||
const validTypes = { 'text/yaml': 'yaml', 'application/x-yaml': 'yaml' };
|
||||
const configLinkEl = document.querySelector('link[rel="cms-config-url"]');
|
||||
const isValidLink = configLinkEl && validTypes[configLinkEl.type] && get(configLinkEl, 'href');
|
||||
if (isValidLink) {
|
||||
const link = get(configLinkEl, 'href');
|
||||
console.log(`Using config file path: "${link}"`);
|
||||
return link;
|
||||
const validTypes: { [type: string]: string } = {
|
||||
'text/yaml': 'yaml',
|
||||
'application/x-yaml': 'yaml',
|
||||
};
|
||||
const configLinkEl = document.querySelector<HTMLLinkElement>('link[rel="cms-config-url"]');
|
||||
if (configLinkEl && validTypes[configLinkEl.type] && configLinkEl.href) {
|
||||
console.log(`Using config file path: "${configLinkEl.href}"`);
|
||||
return configLinkEl.href;
|
||||
}
|
||||
return 'config.yml';
|
||||
}
|
||||
|
||||
function setDefaultPublicFolderForField(field) {
|
||||
function setDefaultPublicFolderForField<T extends CmsField>(field: T) {
|
||||
if ('media_folder' in field && !field.public_folder) {
|
||||
return { ...field, public_folder: field.media_folder };
|
||||
}
|
||||
@ -60,22 +86,25 @@ const WIDGET_KEY_MAP = {
|
||||
searchFields: 'search_fields',
|
||||
displayFields: 'display_fields',
|
||||
optionsLength: 'options_length',
|
||||
};
|
||||
} as const;
|
||||
|
||||
function setSnakeCaseConfig<T extends CmsField>(field: T) {
|
||||
const deprecatedKeys = Object.keys(WIDGET_KEY_MAP).filter(
|
||||
camel => camel in field,
|
||||
) as ReadonlyArray<keyof typeof WIDGET_KEY_MAP>;
|
||||
|
||||
function setSnakeCaseConfig(field) {
|
||||
const deprecatedKeys = Object.keys(WIDGET_KEY_MAP).filter(camel => camel in field);
|
||||
const snakeValues = deprecatedKeys.map(camel => {
|
||||
const snake = WIDGET_KEY_MAP[camel];
|
||||
console.warn(
|
||||
`Field ${field.name} is using a deprecated configuration '${camel}'. Please use '${snake}'`,
|
||||
);
|
||||
return { [snake]: field[camel] };
|
||||
return { [snake]: (field as Record<string, unknown>)[camel] };
|
||||
});
|
||||
|
||||
return Object.assign({}, field, ...snakeValues);
|
||||
return Object.assign({}, field, ...snakeValues) as T;
|
||||
}
|
||||
|
||||
function setI18nField(field) {
|
||||
function setI18nField<T extends CmsField>(field: T) {
|
||||
if (field[I18N] === true) {
|
||||
return { ...field, [I18N]: I18N_FIELD.TRANSLATE };
|
||||
} else if (field[I18N] === false || !field[I18N]) {
|
||||
@ -84,13 +113,16 @@ function setI18nField(field) {
|
||||
return field;
|
||||
}
|
||||
|
||||
function getI18nDefaults(collectionOrFileI18n, defaultI18n) {
|
||||
function getI18nDefaults(
|
||||
collectionOrFileI18n: boolean | CmsI18nConfig,
|
||||
defaultI18n: CmsI18nConfig,
|
||||
) {
|
||||
if (typeof collectionOrFileI18n === 'boolean') {
|
||||
return defaultI18n;
|
||||
} else {
|
||||
const locales = collectionOrFileI18n.locales || defaultI18n.locales;
|
||||
const defaultLocale = collectionOrFileI18n.default_locale || locales[0];
|
||||
const mergedI18n = deepmerge(defaultI18n, collectionOrFileI18n);
|
||||
const mergedI18n: CmsI18nConfig = deepmerge(defaultI18n, collectionOrFileI18n);
|
||||
mergedI18n.locales = locales;
|
||||
mergedI18n.default_locale = defaultLocale;
|
||||
throwOnMissingDefaultLocale(mergedI18n);
|
||||
@ -98,7 +130,7 @@ function getI18nDefaults(collectionOrFileI18n, defaultI18n) {
|
||||
}
|
||||
}
|
||||
|
||||
function setI18nDefaultsForFields(collectionOrFileFields, hasI18n) {
|
||||
function setI18nDefaultsForFields(collectionOrFileFields: CmsField[], hasI18n: boolean) {
|
||||
if (hasI18n) {
|
||||
return traverseFieldsJS(collectionOrFileFields, setI18nField);
|
||||
} else {
|
||||
@ -110,7 +142,7 @@ function setI18nDefaultsForFields(collectionOrFileFields, hasI18n) {
|
||||
}
|
||||
}
|
||||
|
||||
function throwOnInvalidFileCollectionStructure(i18n) {
|
||||
function throwOnInvalidFileCollectionStructure(i18n?: CmsI18nConfig) {
|
||||
if (i18n && i18n.structure !== I18N_STRUCTURE.SINGLE_FILE) {
|
||||
throw new Error(
|
||||
`i18n configuration for files collections is limited to ${I18N_STRUCTURE.SINGLE_FILE} structure`,
|
||||
@ -118,7 +150,7 @@ function throwOnInvalidFileCollectionStructure(i18n) {
|
||||
}
|
||||
}
|
||||
|
||||
function throwOnMissingDefaultLocale(i18n) {
|
||||
function throwOnMissingDefaultLocale(i18n?: CmsI18nConfig) {
|
||||
if (i18n && i18n.default_locale && !i18n.locales.includes(i18n.default_locale)) {
|
||||
throw new Error(
|
||||
`i18n locales '${i18n.locales.join(', ')}' are missing the default locale ${
|
||||
@ -128,14 +160,14 @@ function throwOnMissingDefaultLocale(i18n) {
|
||||
}
|
||||
}
|
||||
|
||||
function hasIntegration(config, collection) {
|
||||
function hasIntegration(config: CmsConfig, collection: CmsCollection) {
|
||||
// TODO remove fromJS when Immutable is removed from the integrations state slice
|
||||
const integrations = getIntegrations(fromJS(config));
|
||||
const integration = selectIntegration(integrations, collection.name, 'listEntries');
|
||||
return !!integration;
|
||||
}
|
||||
|
||||
export function normalizeConfig(config) {
|
||||
export function normalizeConfig(config: CmsConfig) {
|
||||
const { collections = [] } = config;
|
||||
|
||||
const normalizedCollections = collections.map(collection => {
|
||||
@ -170,7 +202,7 @@ export function normalizeConfig(config) {
|
||||
return { ...config, collections: normalizedCollections };
|
||||
}
|
||||
|
||||
export function applyDefaults(originalConfig) {
|
||||
export function applyDefaults(originalConfig: CmsConfig) {
|
||||
return produce(originalConfig, config => {
|
||||
config.publish_mode = config.publish_mode || SIMPLE_PUBLISH_MODE;
|
||||
config.slug = config.slug || {};
|
||||
@ -201,13 +233,14 @@ export function applyDefaults(originalConfig) {
|
||||
}
|
||||
|
||||
const i18n = config[I18N];
|
||||
const hasI18n = Boolean(i18n);
|
||||
if (hasI18n) {
|
||||
|
||||
if (i18n) {
|
||||
i18n.default_locale = i18n.default_locale || i18n.locales[0];
|
||||
}
|
||||
|
||||
throwOnMissingDefaultLocale(i18n);
|
||||
|
||||
// TODO remove fromJS when Immutable is removed from backend
|
||||
const backend = resolveBackend(fromJS(config));
|
||||
|
||||
for (const collection of config.collections) {
|
||||
@ -215,15 +248,18 @@ export function applyDefaults(originalConfig) {
|
||||
collection.publish = true;
|
||||
}
|
||||
|
||||
const collectionHasI18n = Boolean(collection[I18N]);
|
||||
if (hasI18n && collectionHasI18n) {
|
||||
collection[I18N] = getI18nDefaults(collection[I18N], i18n);
|
||||
let collectionI18n = collection[I18N];
|
||||
|
||||
if (i18n && collectionI18n) {
|
||||
collectionI18n = getI18nDefaults(collectionI18n, i18n);
|
||||
collection[I18N] = collectionI18n;
|
||||
} else {
|
||||
collectionI18n = undefined;
|
||||
delete collection[I18N];
|
||||
}
|
||||
|
||||
if (collection.fields) {
|
||||
collection.fields = setI18nDefaultsForFields(collection.fields, collectionHasI18n);
|
||||
collection.fields = setI18nDefaultsForFields(collection.fields, Boolean(collectionI18n));
|
||||
}
|
||||
|
||||
const { folder, files, view_filters, view_groups, meta } = collection;
|
||||
@ -260,9 +296,6 @@ export function applyDefaults(originalConfig) {
|
||||
if (files) {
|
||||
collection.type = FILES;
|
||||
|
||||
// after we invoked setI18nDefaults,
|
||||
// i18n property can't be boolean anymore
|
||||
const collectionI18n = collection[I18N];
|
||||
throwOnInvalidFileCollectionStructure(collectionI18n);
|
||||
|
||||
delete collection.nested;
|
||||
@ -279,24 +312,21 @@ export function applyDefaults(originalConfig) {
|
||||
file.fields = traverseFieldsJS(file.fields, setDefaultPublicFolderForField);
|
||||
}
|
||||
|
||||
const fileHasI18n = Boolean(file[I18N]);
|
||||
let fileI18n = file[I18N];
|
||||
|
||||
if (fileHasI18n) {
|
||||
if (collectionI18n) {
|
||||
file[I18N] = getI18nDefaults(file[I18N], collectionI18n);
|
||||
}
|
||||
if (fileI18n && collectionI18n) {
|
||||
fileI18n = getI18nDefaults(fileI18n, collectionI18n);
|
||||
file[I18N] = fileI18n;
|
||||
} else {
|
||||
fileI18n = undefined;
|
||||
delete file[I18N];
|
||||
}
|
||||
|
||||
if (file.fields) {
|
||||
file.fields = setI18nDefaultsForFields(file.fields, fileHasI18n);
|
||||
}
|
||||
|
||||
// after we invoked setI18nDefaults,
|
||||
// i18n property can't be boolean anymore
|
||||
const fileI18n = file[I18N];
|
||||
throwOnInvalidFileCollectionStructure(fileI18n);
|
||||
|
||||
if (file.fields) {
|
||||
file.fields = setI18nDefaultsForFields(file.fields, Boolean(fileI18n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,32 +360,42 @@ export function applyDefaults(originalConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
export function parseConfig(data) {
|
||||
export function parseConfig(data: string) {
|
||||
const config = yaml.parse(data, { maxAliasCount: -1, prettyErrors: true, merge: true });
|
||||
if (typeof CMS_ENV === 'string' && config[CMS_ENV]) {
|
||||
Object.keys(config[CMS_ENV]).forEach(key => {
|
||||
config[key] = config[CMS_ENV][key];
|
||||
});
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.CMS_ENV === 'string' &&
|
||||
config[window.CMS_ENV]
|
||||
) {
|
||||
const configKeys = Object.keys(config[window.CMS_ENV]) as ReadonlyArray<keyof CmsConfig>;
|
||||
for (const key of configKeys) {
|
||||
config[key] = config[window.CMS_ENV][key] as CmsConfig[keyof CmsConfig];
|
||||
}
|
||||
}
|
||||
return config;
|
||||
return config as Partial<CmsConfig>;
|
||||
}
|
||||
|
||||
async function getConfigYaml(file, hasManualConfig) {
|
||||
const response = await fetch(file, { credentials: 'same-origin' }).catch(err => err);
|
||||
async function getConfigYaml(file: string, hasManualConfig: boolean) {
|
||||
const response = await fetch(file, { credentials: 'same-origin' }).catch(error => error as Error);
|
||||
if (response instanceof Error || response.status !== 200) {
|
||||
if (hasManualConfig) return parseConfig('');
|
||||
throw new Error(`Failed to load config.yml (${response.status || response})`);
|
||||
if (hasManualConfig) {
|
||||
return {};
|
||||
}
|
||||
const message = response instanceof Error ? response.message : response.status;
|
||||
throw new Error(`Failed to load config.yml (${message})`);
|
||||
}
|
||||
const contentType = response.headers.get('Content-Type') || 'Not-Found';
|
||||
const isYaml = contentType.indexOf('yaml') !== -1;
|
||||
if (!isYaml) {
|
||||
console.log(`Response for ${file} was not yaml. (Content-Type: ${contentType})`);
|
||||
if (hasManualConfig) return parseConfig('');
|
||||
if (hasManualConfig) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return parseConfig(await response.text());
|
||||
}
|
||||
|
||||
export function configLoaded(config) {
|
||||
export function configLoaded(config: CmsConfig) {
|
||||
return {
|
||||
type: CONFIG_SUCCESS,
|
||||
payload: config,
|
||||
@ -368,7 +408,7 @@ export function configLoading() {
|
||||
};
|
||||
}
|
||||
|
||||
export function configFailed(err) {
|
||||
export function configFailed(err: Error) {
|
||||
return {
|
||||
type: CONFIG_FAILURE,
|
||||
error: 'Error loading config',
|
||||
@ -376,35 +416,49 @@ export function configFailed(err) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function detectProxyServer(localBackend) {
|
||||
const allowedHosts = ['localhost', '127.0.0.1', ...(localBackend?.allowed_hosts || [])];
|
||||
if (allowedHosts.includes(location.hostname)) {
|
||||
let proxyUrl;
|
||||
const defaultUrl = 'http://localhost:8081/api/v1';
|
||||
if (localBackend === true) {
|
||||
proxyUrl = defaultUrl;
|
||||
} else if (isPlainObject(localBackend)) {
|
||||
proxyUrl = localBackend.url || defaultUrl.replace('localhost', location.hostname);
|
||||
}
|
||||
try {
|
||||
console.log(`Looking for Netlify CMS Proxy Server at '${proxyUrl}'`);
|
||||
const { repo, publish_modes, type } = await fetch(`${proxyUrl}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'info' }),
|
||||
}).then(res => res.json());
|
||||
if (typeof repo === 'string' && Array.isArray(publish_modes) && typeof type === 'string') {
|
||||
console.log(`Detected Netlify CMS Proxy Server at '${proxyUrl}' with repo: '${repo}'`);
|
||||
return { proxyUrl, publish_modes, type };
|
||||
}
|
||||
} catch {
|
||||
console.log(`Netlify CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
}
|
||||
export async function detectProxyServer(localBackend?: boolean | CmsLocalBackend) {
|
||||
const allowedHosts = [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
...(typeof localBackend === 'boolean' ? [] : localBackend?.allowed_hosts || []),
|
||||
];
|
||||
|
||||
if (!allowedHosts.includes(location.hostname) || !localBackend) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const defaultUrl = 'http://localhost:8081/api/v1';
|
||||
const proxyUrl =
|
||||
localBackend === true
|
||||
? defaultUrl
|
||||
: localBackend.url || defaultUrl.replace('localhost', location.hostname);
|
||||
|
||||
try {
|
||||
console.log(`Looking for Netlify CMS Proxy Server at '${proxyUrl}'`);
|
||||
const res = await fetch(`${proxyUrl}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'info' }),
|
||||
});
|
||||
const { repo, publish_modes, type } = (await res.json()) as {
|
||||
repo?: string;
|
||||
publish_modes?: CmsPublishMode[];
|
||||
type?: string;
|
||||
};
|
||||
if (typeof repo === 'string' && Array.isArray(publish_modes) && typeof type === 'string') {
|
||||
console.log(`Detected Netlify CMS Proxy Server at '${proxyUrl}' with repo: '${repo}'`);
|
||||
return { proxyUrl, publish_modes, type };
|
||||
} else {
|
||||
console.log(`Netlify CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
return {};
|
||||
}
|
||||
} catch {
|
||||
console.log(`Netlify CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function getPublishMode(config, publishModes, backendType) {
|
||||
function getPublishMode(config: CmsConfig, publishModes?: CmsPublishMode[], backendType?: string) {
|
||||
if (config.publish_mode && publishModes && !publishModes.includes(config.publish_mode)) {
|
||||
const newPublishMode = publishModes[0];
|
||||
console.log(
|
||||
@ -416,32 +470,34 @@ function getPublishMode(config, publishModes, backendType) {
|
||||
return config.publish_mode;
|
||||
}
|
||||
|
||||
export async function handleLocalBackend(config) {
|
||||
if (!config.local_backend) {
|
||||
return config;
|
||||
export async function handleLocalBackend(originalConfig: CmsConfig) {
|
||||
if (!originalConfig.local_backend) {
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
const { proxyUrl, publish_modes: publishModes, type: backendType } = await detectProxyServer(
|
||||
config.local_backend,
|
||||
originalConfig.local_backend,
|
||||
);
|
||||
|
||||
if (!proxyUrl) {
|
||||
return config;
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
const publishMode = getPublishMode(config, publishModes, backendType);
|
||||
return {
|
||||
...config,
|
||||
...(publishMode && { publish_mode: publishMode }),
|
||||
backend: { ...config.backend, name: 'proxy', proxy_url: proxyUrl },
|
||||
};
|
||||
return produce(originalConfig, config => {
|
||||
config.backend.name = 'proxy';
|
||||
config.backend.proxy_url = proxyUrl;
|
||||
|
||||
if (config.publish_mode) {
|
||||
config.publish_mode = getPublishMode(config, publishModes, backendType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function loadConfig(manualConfig = {}, onLoad) {
|
||||
export function loadConfig(manualConfig: Partial<CmsConfig> = {}, onLoad: () => unknown) {
|
||||
if (window.CMS_CONFIG) {
|
||||
return configLoaded(fromJS(window.CMS_CONFIG));
|
||||
}
|
||||
return async dispatch => {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>) => {
|
||||
dispatch(configLoading());
|
||||
|
||||
try {
|
8
packages/netlify-cms-core/src/types/global.d.ts
vendored
Normal file
8
packages/netlify-cms-core/src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { CmsConfig } from './redux';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CMS_CONFIG?: CmsConfig;
|
||||
CMS_ENV?: string;
|
||||
}
|
||||
}
|
@ -1,11 +1,409 @@
|
||||
import { Action } from 'redux';
|
||||
import { StaticallyTypedRecord } from './immutable';
|
||||
import { Map, List, OrderedMap, Set } from 'immutable';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
import { MediaFile as BackendMediaFile } from '../backend';
|
||||
import { Auth } from '../reducers/auth';
|
||||
import { Status } from '../reducers/status';
|
||||
import { Medias } from '../reducers/medias';
|
||||
|
||||
export type CmsBackendType =
|
||||
| 'azure'
|
||||
| 'git-gateway'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'bitbucket'
|
||||
| 'test-repo'
|
||||
| 'proxy';
|
||||
|
||||
export type CmsMapWidgetType = 'Point' | 'LineString' | 'Polygon';
|
||||
|
||||
export type CmsMarkdownWidgetButton =
|
||||
| 'bold'
|
||||
| 'italic'
|
||||
| 'code'
|
||||
| 'link'
|
||||
| 'heading-one'
|
||||
| 'heading-two'
|
||||
| 'heading-three'
|
||||
| 'heading-four'
|
||||
| 'heading-five'
|
||||
| 'heading-six'
|
||||
| 'quote'
|
||||
| 'code-block'
|
||||
| 'bulleted-list'
|
||||
| 'numbered-list';
|
||||
|
||||
export interface CmsSelectWidgetOptionObject {
|
||||
label: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export type CmsCollectionFormatType =
|
||||
| 'yml'
|
||||
| 'yaml'
|
||||
| 'toml'
|
||||
| 'json'
|
||||
| 'frontmatter'
|
||||
| 'yaml-frontmatter'
|
||||
| 'toml-frontmatter'
|
||||
| 'json-frontmatter';
|
||||
|
||||
export type CmsAuthScope = 'repo' | 'public_repo';
|
||||
|
||||
export type CmsPublishMode = 'simple' | 'editorial_workflow';
|
||||
|
||||
export type CmsSlugEncoding = 'unicode' | 'ascii';
|
||||
|
||||
export interface CmsI18nConfig {
|
||||
structure: 'multiple_folders' | 'multiple_files' | 'single_file';
|
||||
locales: string[];
|
||||
default_locale?: string;
|
||||
}
|
||||
|
||||
export interface CmsFieldBase {
|
||||
name: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
pattern?: [string, string];
|
||||
i18n?: boolean | 'translate' | 'duplicate' | 'none';
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface CmsFieldBoolean {
|
||||
widget: 'boolean';
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export interface CmsFieldCode {
|
||||
widget: 'code';
|
||||
default?: unknown;
|
||||
|
||||
default_language?: string;
|
||||
allow_language_selection?: boolean;
|
||||
keys?: { code: string; lang: string };
|
||||
output_code_only?: boolean;
|
||||
}
|
||||
|
||||
export interface CmsFieldColor {
|
||||
widget: 'color';
|
||||
default?: string;
|
||||
|
||||
allowInput?: boolean;
|
||||
enableAlpha?: boolean;
|
||||
}
|
||||
|
||||
export interface CmsFieldDateTime {
|
||||
widget: 'datetime';
|
||||
default?: string;
|
||||
|
||||
format?: string;
|
||||
date_format?: boolean | string;
|
||||
time_format?: boolean | string;
|
||||
picker_utc?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated Use date_format instead
|
||||
*/
|
||||
dateFormat?: boolean | string;
|
||||
/**
|
||||
* @deprecated Use time_format instead
|
||||
*/
|
||||
timeFormat?: boolean | string;
|
||||
/**
|
||||
* @deprecated Use picker_utc instead
|
||||
*/
|
||||
pickerUtc?: boolean;
|
||||
}
|
||||
|
||||
export interface CmsFieldFileOrImage {
|
||||
widget: 'file' | 'image';
|
||||
default?: string;
|
||||
|
||||
media_library?: CmsMediaLibrary;
|
||||
allow_multiple?: boolean;
|
||||
config?: unknown;
|
||||
}
|
||||
|
||||
export interface CmsFieldObject {
|
||||
widget: 'object';
|
||||
default?: unknown;
|
||||
|
||||
collapsed?: boolean;
|
||||
summary?: string;
|
||||
fields: CmsField[];
|
||||
}
|
||||
|
||||
export interface CmsFieldList {
|
||||
widget: 'list';
|
||||
default?: unknown;
|
||||
|
||||
allow_add?: boolean;
|
||||
collapsed?: boolean;
|
||||
summary?: string;
|
||||
minimize_collapsed?: boolean;
|
||||
label_singular?: string;
|
||||
field?: CmsField;
|
||||
fields?: CmsField[];
|
||||
max?: number;
|
||||
min?: number;
|
||||
add_to_top?: boolean;
|
||||
types?: (CmsFieldBase & CmsFieldObject)[];
|
||||
}
|
||||
|
||||
export interface CmsFieldMap {
|
||||
widget: 'map';
|
||||
default?: string;
|
||||
|
||||
decimals?: number;
|
||||
type?: CmsMapWidgetType;
|
||||
}
|
||||
|
||||
export interface CmsFieldMarkdown {
|
||||
widget: 'markdown';
|
||||
default?: string;
|
||||
|
||||
minimal?: boolean;
|
||||
buttons?: CmsMarkdownWidgetButton[];
|
||||
editor_components?: string[];
|
||||
modes?: ('raw' | 'rich_text')[];
|
||||
|
||||
/**
|
||||
* @deprecated Use editor_components instead
|
||||
*/
|
||||
editorComponents?: string[];
|
||||
}
|
||||
|
||||
export interface CmsFieldNumber {
|
||||
widget: 'number';
|
||||
default?: string | number;
|
||||
|
||||
value_type?: 'int' | 'float' | string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
step?: number;
|
||||
|
||||
/**
|
||||
* @deprecated Use valueType instead
|
||||
*/
|
||||
valueType?: 'int' | 'float' | string;
|
||||
}
|
||||
|
||||
export interface CmsFieldSelect {
|
||||
widget: 'select';
|
||||
default?: string | string[];
|
||||
|
||||
options: string[] | CmsSelectWidgetOptionObject[];
|
||||
multiple?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export interface CmsFieldRelation {
|
||||
widget: 'relation';
|
||||
default?: string | string[];
|
||||
|
||||
collection: string;
|
||||
value_field: string;
|
||||
search_fields: string[];
|
||||
file?: string;
|
||||
display_fields?: string[];
|
||||
multiple?: boolean;
|
||||
options_length?: number;
|
||||
|
||||
/**
|
||||
* @deprecated Use value_field instead
|
||||
*/
|
||||
valueField?: string;
|
||||
/**
|
||||
* @deprecated Use search_fields instead
|
||||
*/
|
||||
searchFields?: string[];
|
||||
/**
|
||||
* @deprecated Use display_fields instead
|
||||
*/
|
||||
displayFields?: string[];
|
||||
/**
|
||||
* @deprecated Use options_length instead
|
||||
*/
|
||||
optionsLength?: number;
|
||||
}
|
||||
|
||||
export interface CmsFieldHidden {
|
||||
widget: 'hidden';
|
||||
default?: unknown;
|
||||
}
|
||||
|
||||
export interface CmsFieldStringOrText {
|
||||
// This is the default widget, so declaring its type is optional.
|
||||
widget?: 'string' | 'text';
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface CmsFieldMeta {
|
||||
name: string;
|
||||
label: string;
|
||||
widget: string;
|
||||
required: boolean;
|
||||
index_file: string;
|
||||
meta: boolean;
|
||||
}
|
||||
|
||||
export type CmsField = CmsFieldBase &
|
||||
(
|
||||
| CmsFieldBoolean
|
||||
| CmsFieldCode
|
||||
| CmsFieldColor
|
||||
| CmsFieldDateTime
|
||||
| CmsFieldFileOrImage
|
||||
| CmsFieldList
|
||||
| CmsFieldMap
|
||||
| CmsFieldMarkdown
|
||||
| CmsFieldNumber
|
||||
| CmsFieldObject
|
||||
| CmsFieldRelation
|
||||
| CmsFieldSelect
|
||||
| CmsFieldHidden
|
||||
| CmsFieldStringOrText
|
||||
| CmsFieldMeta
|
||||
);
|
||||
|
||||
export interface CmsCollectionFile {
|
||||
name: string;
|
||||
label: string;
|
||||
file: string;
|
||||
fields: CmsField[];
|
||||
label_singular?: string;
|
||||
description?: string;
|
||||
preview_path?: string;
|
||||
preview_path_date_field?: string;
|
||||
i18n?: boolean | CmsI18nConfig;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
}
|
||||
|
||||
export interface ViewFilter {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ViewGroup {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CmsCollection {
|
||||
name: string;
|
||||
label: string;
|
||||
label_singular?: string;
|
||||
description?: string;
|
||||
folder?: string;
|
||||
files?: CmsCollectionFile[];
|
||||
identifier_field?: string;
|
||||
summary?: string;
|
||||
slug?: string;
|
||||
preview_path?: string;
|
||||
preview_path_date_field?: string;
|
||||
create?: boolean;
|
||||
delete?: boolean;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
publish?: boolean;
|
||||
nested?: {
|
||||
depth: number;
|
||||
};
|
||||
type: typeof FOLDER | typeof FILES;
|
||||
meta?: { path?: { label: string; widget: string; index_file: string } };
|
||||
|
||||
/**
|
||||
* It accepts the following values: yml, yaml, toml, json, md, markdown, html
|
||||
*
|
||||
* You may also specify a custom extension not included in the list above, by specifying the format value.
|
||||
*/
|
||||
extension?: string;
|
||||
format?: CmsCollectionFormatType;
|
||||
|
||||
frontmatter_delimiter?: string[] | string;
|
||||
fields?: CmsField[];
|
||||
filter?: { field: string; value: unknown };
|
||||
path?: string;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
sortable_fields?: string[];
|
||||
view_filters?: ViewFilter[];
|
||||
view_groups?: ViewGroup[];
|
||||
i18n?: boolean | CmsI18nConfig;
|
||||
|
||||
/**
|
||||
* @deprecated Use sortable_fields instead
|
||||
*/
|
||||
sortableFields?: string[];
|
||||
}
|
||||
|
||||
export interface CmsBackend {
|
||||
name: CmsBackendType;
|
||||
auth_scope?: CmsAuthScope;
|
||||
open_authoring?: boolean;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
site_domain?: string;
|
||||
base_url?: string;
|
||||
auth_endpoint?: string;
|
||||
cms_label_prefix?: string;
|
||||
squash_merges?: boolean;
|
||||
proxy_url?: string;
|
||||
}
|
||||
|
||||
export interface CmsSlug {
|
||||
encoding?: CmsSlugEncoding;
|
||||
clean_accents?: boolean;
|
||||
sanitize_replacement?: string;
|
||||
}
|
||||
|
||||
export interface CmsLocalBackend {
|
||||
url?: string;
|
||||
allowed_hosts?: string[];
|
||||
}
|
||||
|
||||
export interface CmsConfig {
|
||||
backend: CmsBackend;
|
||||
collections: CmsCollection[];
|
||||
locale?: string;
|
||||
site_url?: string;
|
||||
display_url?: string;
|
||||
logo_url?: string;
|
||||
show_preview_links?: boolean;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
media_folder_relative?: boolean;
|
||||
media_library?: CmsMediaLibrary;
|
||||
publish_mode?: CmsPublishMode;
|
||||
load_config_file?: boolean;
|
||||
slug?: CmsSlug;
|
||||
i18n?: CmsI18nConfig;
|
||||
local_backend?: boolean | CmsLocalBackend;
|
||||
editor?: {
|
||||
preview?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type CmsMediaLibraryOptions = unknown; // TODO: type properly
|
||||
|
||||
export interface CmsMediaLibrary {
|
||||
name: string;
|
||||
config?: CmsMediaLibraryOptions;
|
||||
}
|
||||
|
||||
export type SlugConfig = StaticallyTypedRecord<{
|
||||
encoding: string;
|
||||
clean_accents: boolean;
|
||||
@ -162,20 +560,6 @@ export type CollectionFile = StaticallyTypedRecord<{
|
||||
|
||||
export type CollectionFiles = List<CollectionFile>;
|
||||
|
||||
export type ViewFilter = {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ViewGroup = {
|
||||
label: string;
|
||||
field: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type NestedObject = { depth: number };
|
||||
|
||||
type Nested = StaticallyTypedRecord<NestedObject>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user