refactor: convert config actions to TypeScript (#4950)

This commit is contained in:
Vladislav Shkodin 2021-02-14 18:41:02 +01:00 committed by GitHub
parent 41e82c2280
commit 0ac17bfc25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 707 additions and 121 deletions

View File

@ -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 },

View File

@ -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 {

View File

@ -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)'),
});
});
});
});

View File

@ -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 {

View File

@ -0,0 +1,8 @@
import { CmsConfig } from './redux';
declare global {
interface Window {
CMS_CONFIG?: CmsConfig;
CMS_ENV?: string;
}
}

View File

@ -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>;