This commit is contained in:
1009
node_modules/decap-cms-core/src/actions/__tests__/config.spec.js
generated
vendored
Normal file
1009
node_modules/decap-cms-core/src/actions/__tests__/config.spec.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
216
node_modules/decap-cms-core/src/actions/__tests__/editorialWorkflow.spec.js
generated
vendored
Normal file
216
node_modules/decap-cms-core/src/actions/__tests__/editorialWorkflow.spec.js
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { addAssets } from '../media';
|
||||
import * as actions from '../editorialWorkflow';
|
||||
|
||||
jest.mock('../../backend');
|
||||
jest.mock('../../valueObjects/AssetProxy');
|
||||
jest.mock('decap-cms-lib-util');
|
||||
jest.mock('uuid', () => {
|
||||
return { v4: jest.fn().mockReturnValue('000000000000000000000') };
|
||||
});
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore(middlewares);
|
||||
|
||||
describe('editorialWorkflow actions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('loadUnpublishedEntry', () => {
|
||||
it('should load unpublished entry', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
const { createAssetProxy } = require('../../valueObjects/AssetProxy');
|
||||
|
||||
const assetProxy = { name: 'name', path: 'path' };
|
||||
const entry = { mediaFiles: [{ file: { name: 'name' }, id: '1', draft: true }] };
|
||||
const backend = {
|
||||
unpublishedEntry: jest.fn().mockResolvedValue(entry),
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
config: fromJS({}),
|
||||
collections: fromJS({
|
||||
posts: { name: 'posts' },
|
||||
}),
|
||||
mediaLibrary: fromJS({
|
||||
isLoading: false,
|
||||
}),
|
||||
editorialWorkflow: fromJS({
|
||||
pages: { ids: [] },
|
||||
}),
|
||||
});
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
createAssetProxy.mockResolvedValue(assetProxy);
|
||||
|
||||
const slug = 'slug';
|
||||
const collection = store.getState().collections.get('posts');
|
||||
|
||||
return store.dispatch(actions.loadUnpublishedEntry(collection, slug)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(4);
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_REQUEST',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
slug,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual(addAssets([assetProxy]));
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_SUCCESS',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
entry: { ...entry, mediaFiles: [{ file: { name: 'name' }, id: '1', draft: true }] },
|
||||
},
|
||||
});
|
||||
expect(actions[3]).toEqual({
|
||||
type: 'DRAFT_CREATE_FROM_ENTRY',
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishUnpublishedEntry', () => {
|
||||
it('should publish unpublished entry and report success', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
|
||||
const entry = {};
|
||||
const backend = {
|
||||
publishUnpublishedEntry: jest.fn().mockResolvedValue(),
|
||||
getEntry: jest.fn().mockResolvedValue(entry),
|
||||
getMedia: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
config: fromJS({}),
|
||||
integrations: fromJS([]),
|
||||
mediaLibrary: fromJS({
|
||||
isLoading: false,
|
||||
}),
|
||||
collections: fromJS({
|
||||
posts: { name: 'posts' },
|
||||
}),
|
||||
});
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
const slug = 'slug';
|
||||
|
||||
return store.dispatch(actions.publishUnpublishedEntry('posts', slug)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(8);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
slug,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'MEDIA_LOAD_REQUEST',
|
||||
payload: {
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'NOTIFICATION_SEND',
|
||||
payload: {
|
||||
message: { key: 'ui.toast.entryPublished' },
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
},
|
||||
});
|
||||
expect(actions[3]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
slug,
|
||||
},
|
||||
});
|
||||
|
||||
expect(actions[4]).toEqual({
|
||||
type: 'MEDIA_LOAD_SUCCESS',
|
||||
payload: {
|
||||
files: [],
|
||||
},
|
||||
});
|
||||
expect(actions[5]).toEqual({
|
||||
type: 'ENTRY_REQUEST',
|
||||
payload: {
|
||||
slug,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
expect(actions[6]).toEqual({
|
||||
type: 'ENTRY_SUCCESS',
|
||||
payload: {
|
||||
entry,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
expect(actions[7]).toEqual({
|
||||
type: 'DRAFT_CREATE_FROM_ENTRY',
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should publish unpublished entry and report error', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
|
||||
const error = new Error('failed to publish entry');
|
||||
const backend = {
|
||||
publishUnpublishedEntry: jest.fn().mockRejectedValue(error),
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
config: fromJS({}),
|
||||
collections: fromJS({
|
||||
posts: { name: 'posts' },
|
||||
}),
|
||||
});
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
const slug = 'slug';
|
||||
|
||||
return store.dispatch(actions.publishUnpublishedEntry('posts', slug)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(3);
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
slug,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'NOTIFICATION_SEND',
|
||||
payload: {
|
||||
message: { key: 'ui.toast.onFailToPublishEntry', details: error },
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
},
|
||||
});
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE',
|
||||
payload: {
|
||||
collection: 'posts',
|
||||
slug,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
575
node_modules/decap-cms-core/src/actions/__tests__/entries.spec.js
generated
vendored
Normal file
575
node_modules/decap-cms-core/src/actions/__tests__/entries.spec.js
generated
vendored
Normal file
@@ -0,0 +1,575 @@
|
||||
import { fromJS, Map } from 'immutable';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {
|
||||
createEmptyDraft,
|
||||
createEmptyDraftData,
|
||||
retrieveLocalBackup,
|
||||
persistLocalBackup,
|
||||
getMediaAssets,
|
||||
validateMetaField,
|
||||
} from '../entries';
|
||||
import AssetProxy from '../../valueObjects/AssetProxy';
|
||||
|
||||
jest.mock('../../backend');
|
||||
jest.mock('decap-cms-lib-util');
|
||||
jest.mock('../mediaLibrary');
|
||||
jest.mock('../../reducers/entries');
|
||||
jest.mock('../../reducers/entryDraft');
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore(middlewares);
|
||||
|
||||
describe('entries', () => {
|
||||
describe('createEmptyDraft', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
const backend = {
|
||||
processEntry: jest.fn((_state, _collection, entry) => Promise.resolve(entry)),
|
||||
};
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should dispatch draft created action', () => {
|
||||
const store = mockStore({ mediaLibrary: fromJS({ files: [] }) });
|
||||
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'title' }],
|
||||
});
|
||||
|
||||
return store.dispatch(createEmptyDraft(collection, '')).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
payload: {
|
||||
author: '',
|
||||
collection: undefined,
|
||||
data: {},
|
||||
meta: {},
|
||||
i18n: {},
|
||||
isModification: null,
|
||||
label: null,
|
||||
mediaFiles: [],
|
||||
partial: false,
|
||||
path: '',
|
||||
raw: '',
|
||||
slug: '',
|
||||
status: '',
|
||||
updatedOn: '',
|
||||
},
|
||||
type: 'DRAFT_CREATE_EMPTY',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should populate draft entry from URL param', () => {
|
||||
const store = mockStore({ mediaLibrary: fromJS({ files: [] }) });
|
||||
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'title' }, { name: 'boolean' }],
|
||||
});
|
||||
|
||||
return store.dispatch(createEmptyDraft(collection, '?title=title&boolean=True')).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
payload: {
|
||||
author: '',
|
||||
collection: undefined,
|
||||
data: { title: 'title', boolean: true },
|
||||
meta: {},
|
||||
i18n: {},
|
||||
isModification: null,
|
||||
label: null,
|
||||
mediaFiles: [],
|
||||
partial: false,
|
||||
path: '',
|
||||
raw: '',
|
||||
slug: '',
|
||||
status: '',
|
||||
updatedOn: '',
|
||||
},
|
||||
type: 'DRAFT_CREATE_EMPTY',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should html escape URL params', () => {
|
||||
const store = mockStore({ mediaLibrary: fromJS({ files: [] }) });
|
||||
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'title' }],
|
||||
});
|
||||
|
||||
return store
|
||||
.dispatch(createEmptyDraft(collection, "?title=<script>alert('hello')</script>"))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
payload: {
|
||||
author: '',
|
||||
collection: undefined,
|
||||
data: { title: '<script>alert('hello')</script>' },
|
||||
meta: {},
|
||||
i18n: {},
|
||||
isModification: null,
|
||||
label: null,
|
||||
mediaFiles: [],
|
||||
partial: false,
|
||||
path: '',
|
||||
raw: '',
|
||||
slug: '',
|
||||
status: '',
|
||||
updatedOn: '',
|
||||
},
|
||||
type: 'DRAFT_CREATE_EMPTY',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('createEmptyDraftData', () => {
|
||||
it('should allow an empty array as list default for a single field list', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
default: [],
|
||||
field: { name: 'url', widget: 'text' },
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) });
|
||||
});
|
||||
|
||||
it('should allow a complex array as list default for a single field list', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
default: [
|
||||
{
|
||||
url: 'https://image.png',
|
||||
},
|
||||
],
|
||||
field: { name: 'url', widget: 'text' },
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({
|
||||
images: fromJS([
|
||||
{
|
||||
url: 'https://image.png',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow an empty array as list default for a fields list', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
default: [],
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text' },
|
||||
{ name: 'url', widget: 'text' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) });
|
||||
});
|
||||
|
||||
it('should allow a complex array as list default for a fields list', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
default: [
|
||||
{
|
||||
title: 'default image',
|
||||
url: 'https://image.png',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text' },
|
||||
{ name: 'url', widget: 'text' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({
|
||||
images: fromJS([
|
||||
{
|
||||
title: 'default image',
|
||||
url: 'https://image.png',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use field default when no list default is provided', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
field: { name: 'url', widget: 'text', default: 'https://image.png' },
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({ images: [{ url: 'https://image.png' }] });
|
||||
});
|
||||
|
||||
it('should use fields default when no list default is provided', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text', default: 'default image' },
|
||||
{ name: 'url', widget: 'text', default: 'https://image.png' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({
|
||||
images: [{ title: 'default image', url: 'https://image.png' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set empty value for list fields widget', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'images',
|
||||
widget: 'list',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text' },
|
||||
{ name: 'url', widget: 'text' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({});
|
||||
});
|
||||
|
||||
it('should set default value for object field widget', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'post',
|
||||
widget: 'object',
|
||||
field: { name: 'image', widget: 'text', default: 'https://image.png' },
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({ post: { image: 'https://image.png' } });
|
||||
});
|
||||
|
||||
it('should set default values for object fields widget', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'post',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text', default: 'default title' },
|
||||
{ name: 'url', widget: 'text', default: 'https://image.png' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({
|
||||
post: { title: 'default title', url: 'https://image.png' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set empty value for object fields widget', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'post',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ name: 'title', widget: 'text' },
|
||||
{ name: 'url', widget: 'text' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({});
|
||||
});
|
||||
|
||||
it('should populate nested fields', () => {
|
||||
const fields = fromJS([
|
||||
{
|
||||
name: 'names',
|
||||
widget: 'list',
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ name: 'first', widget: 'string', default: 'first' },
|
||||
{ name: 'second', widget: 'string', default: 'second' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(createEmptyDraftData(fields)).toEqual({
|
||||
names: [{ object: { first: 'first', second: 'second' } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistLocalBackup', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should persist local backup with media files', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
|
||||
const backend = {
|
||||
persistLocalDraftBackup: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
config: Map(),
|
||||
});
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
const collection = Map();
|
||||
const mediaFiles = [{ path: 'static/media/image.png' }];
|
||||
const entry = fromJS({ mediaFiles });
|
||||
|
||||
return store.dispatch(persistLocalBackup(entry, collection)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
|
||||
expect(backend.persistLocalDraftBackup).toHaveBeenCalledTimes(1);
|
||||
expect(backend.persistLocalDraftBackup).toHaveBeenCalledWith(entry, collection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('retrieveLocalBackup', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should retrieve media files with local backup', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
const { createAssetProxy } = require('../../valueObjects/AssetProxy');
|
||||
|
||||
const backend = {
|
||||
getLocalDraftBackup: jest.fn((...args) => args),
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
config: Map(),
|
||||
});
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
const collection = Map({
|
||||
name: 'collection',
|
||||
});
|
||||
const slug = 'slug';
|
||||
|
||||
const file = new File([], 'image.png');
|
||||
const mediaFiles = [{ path: 'static/media/image.png', url: 'url', file }];
|
||||
const asset = createAssetProxy(mediaFiles[0]);
|
||||
const entry = { mediaFiles };
|
||||
|
||||
backend.getLocalDraftBackup.mockReturnValue({ entry });
|
||||
|
||||
return store.dispatch(retrieveLocalBackup(collection, slug)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(2);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'ADD_ASSETS',
|
||||
payload: [asset],
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'DRAFT_LOCAL_BACKUP_RETRIEVED',
|
||||
payload: { entry },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMediaAssets', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should map mediaFiles to assets', () => {
|
||||
const mediaFiles = fromJS([{ path: 'path1' }, { path: 'path2', draft: true }]);
|
||||
|
||||
const entry = Map({ mediaFiles });
|
||||
expect(getMediaAssets({ entry })).toEqual([new AssetProxy({ path: 'path2' })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMetaField', () => {
|
||||
const state = {
|
||||
config: {
|
||||
slug: {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
},
|
||||
},
|
||||
entries: fromJS([]),
|
||||
};
|
||||
const collection = fromJS({
|
||||
folder: 'folder',
|
||||
type: 'folder_based_collection',
|
||||
name: 'name',
|
||||
});
|
||||
const t = jest.fn((key, args) => ({ key, args }));
|
||||
|
||||
const { selectCustomPath } = require('../../reducers/entryDraft');
|
||||
const { selectEntryByPath } = require('../../reducers/entries');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not return error on non meta field', () => {
|
||||
expect(validateMetaField(null, null, fromJS({}), null, t)).toEqual({ error: false });
|
||||
});
|
||||
|
||||
it('should not return error on meta path field', () => {
|
||||
expect(validateMetaField(null, null, fromJS({ meta: true, name: 'other' }), null, t)).toEqual(
|
||||
{ error: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error on empty path', () => {
|
||||
expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), null, t)).toEqual({
|
||||
error: {
|
||||
message: {
|
||||
key: 'editor.editorControlPane.widget.invalidPath',
|
||||
args: { path: null },
|
||||
},
|
||||
type: 'CUSTOM',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), undefined, t),
|
||||
).toEqual({
|
||||
error: {
|
||||
message: {
|
||||
key: 'editor.editorControlPane.widget.invalidPath',
|
||||
args: { path: undefined },
|
||||
},
|
||||
type: 'CUSTOM',
|
||||
},
|
||||
});
|
||||
|
||||
expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), '', t)).toEqual({
|
||||
error: {
|
||||
message: {
|
||||
key: 'editor.editorControlPane.widget.invalidPath',
|
||||
args: { path: '' },
|
||||
},
|
||||
type: 'CUSTOM',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error on invalid path', () => {
|
||||
expect(
|
||||
validateMetaField(state, null, fromJS({ meta: true, name: 'path' }), 'invalid path', t),
|
||||
).toEqual({
|
||||
error: {
|
||||
message: {
|
||||
key: 'editor.editorControlPane.widget.invalidPath',
|
||||
args: { path: 'invalid path' },
|
||||
},
|
||||
type: 'CUSTOM',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error on existing path', () => {
|
||||
selectCustomPath.mockReturnValue('existing-path');
|
||||
selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' }));
|
||||
expect(
|
||||
validateMetaField(
|
||||
{
|
||||
...state,
|
||||
entryDraft: fromJS({
|
||||
entry: {},
|
||||
}),
|
||||
},
|
||||
collection,
|
||||
fromJS({ meta: true, name: 'path' }),
|
||||
'existing-path',
|
||||
t,
|
||||
),
|
||||
).toEqual({
|
||||
error: {
|
||||
message: {
|
||||
key: 'editor.editorControlPane.widget.pathExists',
|
||||
args: { path: 'existing-path' },
|
||||
},
|
||||
type: 'CUSTOM',
|
||||
},
|
||||
});
|
||||
|
||||
expect(selectCustomPath).toHaveBeenCalledTimes(1);
|
||||
expect(selectCustomPath).toHaveBeenCalledWith(
|
||||
collection,
|
||||
fromJS({ entry: { meta: { path: 'existing-path' } } }),
|
||||
);
|
||||
|
||||
expect(selectEntryByPath).toHaveBeenCalledTimes(1);
|
||||
expect(selectEntryByPath).toHaveBeenCalledWith(
|
||||
state.entries,
|
||||
collection.get('name'),
|
||||
'existing-path',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return error on non existing path for new entry', () => {
|
||||
selectCustomPath.mockReturnValue('non-existing-path');
|
||||
selectEntryByPath.mockReturnValue(undefined);
|
||||
expect(
|
||||
validateMetaField(
|
||||
{
|
||||
...state,
|
||||
entryDraft: fromJS({
|
||||
entry: {},
|
||||
}),
|
||||
},
|
||||
collection,
|
||||
fromJS({ meta: true, name: 'path' }),
|
||||
'non-existing-path',
|
||||
t,
|
||||
),
|
||||
).toEqual({
|
||||
error: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return error when for existing entry', () => {
|
||||
selectCustomPath.mockReturnValue('existing-path');
|
||||
selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' }));
|
||||
expect(
|
||||
validateMetaField(
|
||||
{
|
||||
...state,
|
||||
entryDraft: fromJS({
|
||||
entry: { path: 'existing-path' },
|
||||
}),
|
||||
},
|
||||
collection,
|
||||
fromJS({ meta: true, name: 'path' }),
|
||||
'existing-path',
|
||||
t,
|
||||
),
|
||||
).toEqual({
|
||||
error: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
171
node_modules/decap-cms-core/src/actions/__tests__/media.spec.ts
generated
vendored
Normal file
171
node_modules/decap-cms-core/src/actions/__tests__/media.spec.ts
generated
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Map } from 'immutable';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import { getAsset, ADD_ASSET, LOAD_ASSET_REQUEST } from '../media';
|
||||
import { selectMediaFilePath } from '../../reducers/entries';
|
||||
import AssetProxy from '../../valueObjects/AssetProxy';
|
||||
|
||||
import type { State } from '../../types/redux';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore<Partial<State>, ThunkDispatch<State, {}, AnyAction>>(
|
||||
middlewares,
|
||||
);
|
||||
const mockedSelectMediaFilePath = mocked(selectMediaFilePath);
|
||||
|
||||
jest.mock('../../reducers/entries');
|
||||
jest.mock('../mediaLibrary');
|
||||
|
||||
describe('media', () => {
|
||||
const emptyAsset = new AssetProxy({
|
||||
path: 'empty.svg',
|
||||
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], 'empty.svg', {
|
||||
type: 'image/svg+xml',
|
||||
}),
|
||||
});
|
||||
|
||||
describe('getAsset', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
global.URL = { createObjectURL: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty asset for null path', () => {
|
||||
const store = mockStore({});
|
||||
|
||||
const payload = { collection: null, entryPath: null, entry: null, path: null };
|
||||
|
||||
// TODO change to proper payload when immutable is removed
|
||||
// from 'collections' and 'entries' state slices
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
expect(result).toEqual(emptyAsset);
|
||||
});
|
||||
|
||||
it('should return asset from medias state', () => {
|
||||
const path = 'static/media/image.png';
|
||||
const asset = new AssetProxy({ file: new File([], 'empty'), path });
|
||||
const store = mockStore({
|
||||
// TODO change to proper store data when immutable is removed
|
||||
// from 'config' state slice
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
config: Map(),
|
||||
medias: {
|
||||
[path]: { asset, isLoading: false, error: null },
|
||||
},
|
||||
});
|
||||
|
||||
mockedSelectMediaFilePath.mockReturnValue(path);
|
||||
const payload = { collection: Map(), entry: Map({ path: 'entryPath' }), path };
|
||||
|
||||
// TODO change to proper payload when immutable is removed
|
||||
// from 'collections' and 'entries' state slices
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
|
||||
expect(result).toBe(asset);
|
||||
expect(mockedSelectMediaFilePath).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSelectMediaFilePath).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
payload.collection,
|
||||
payload.entry,
|
||||
path,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create asset for absolute path when not in medias state', () => {
|
||||
const path = 'https://asset.netlify.com/image.png';
|
||||
|
||||
const asset = new AssetProxy({ url: path, path });
|
||||
const store = mockStore({
|
||||
medias: {},
|
||||
});
|
||||
|
||||
mockedSelectMediaFilePath.mockReturnValue(path);
|
||||
const payload = { collection: null, entryPath: null, path };
|
||||
|
||||
// TODO change to proper payload when immutable is removed
|
||||
// from 'collections' state slice
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
});
|
||||
|
||||
it('should return empty asset and initiate load when not in medias state', () => {
|
||||
const path = 'static/media/image.png';
|
||||
const store = mockStore({
|
||||
medias: {},
|
||||
});
|
||||
|
||||
mockedSelectMediaFilePath.mockReturnValue(path);
|
||||
const payload = { path };
|
||||
|
||||
// TODO change to proper payload when immutable is removed
|
||||
// from 'collections' and 'entries' state slices
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: LOAD_ASSET_REQUEST,
|
||||
payload: { path },
|
||||
});
|
||||
expect(result).toEqual(emptyAsset);
|
||||
});
|
||||
|
||||
it('should return asset with original path on load error', () => {
|
||||
const path = 'static/media/image.png';
|
||||
const resolvePath = 'resolvePath';
|
||||
const store = mockStore({
|
||||
medias: {
|
||||
[resolvePath]: {
|
||||
asset: undefined,
|
||||
error: new Error('test'),
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockedSelectMediaFilePath.mockReturnValue(resolvePath);
|
||||
const payload = { path };
|
||||
|
||||
// TODO change to proper payload when immutable is removed
|
||||
// from 'collections' and 'entries' state slices
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
|
||||
const asset = new AssetProxy({ url: path, path: resolvePath });
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
});
|
||||
});
|
||||
});
|
||||
327
node_modules/decap-cms-core/src/actions/__tests__/mediaLibrary.spec.js
generated
vendored
Normal file
327
node_modules/decap-cms-core/src/actions/__tests__/mediaLibrary.spec.js
generated
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { List, Map } from 'immutable';
|
||||
|
||||
import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary';
|
||||
|
||||
jest.mock('../../backend');
|
||||
jest.mock('../waitUntil');
|
||||
jest.mock('decap-cms-lib-util', () => {
|
||||
const lib = jest.requireActual('decap-cms-lib-util');
|
||||
return {
|
||||
...lib,
|
||||
getBlobSHA: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore(middlewares);
|
||||
|
||||
describe('mediaLibrary', () => {
|
||||
describe('insertMedia', () => {
|
||||
it('should return mediaPath as string when string is given', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
public_folder: '/media',
|
||||
},
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false, collection: 'posts' }),
|
||||
}),
|
||||
});
|
||||
|
||||
store.dispatch(insertMedia('foo.png'));
|
||||
expect(store.getActions()[0]).toEqual({
|
||||
type: 'MEDIA_INSERT',
|
||||
payload: { mediaPath: '/media/foo.png' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return mediaPath as array of strings when array of strings is given', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
public_folder: '/media',
|
||||
},
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false, collection: 'posts' }),
|
||||
}),
|
||||
});
|
||||
|
||||
store.dispatch(insertMedia(['foo.png']));
|
||||
expect(store.getActions()[0]).toEqual({
|
||||
type: 'MEDIA_INSERT',
|
||||
payload: { mediaPath: ['/media/foo.png'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const { currentBackend } = require('../../backend');
|
||||
|
||||
const backend = {
|
||||
persistMedia: jest.fn(() => ({ id: 'id' })),
|
||||
deleteMedia: jest.fn(),
|
||||
};
|
||||
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
describe('persistMedia', () => {
|
||||
global.URL = { createObjectURL: jest.fn().mockReturnValue('displayURL') };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not persist media when editing draft', () => {
|
||||
const { getBlobSHA } = require('decap-cms-lib-util');
|
||||
|
||||
getBlobSHA.mockReturnValue('000000000000000');
|
||||
|
||||
const store = mockStore({
|
||||
config: {
|
||||
media_folder: 'static/media',
|
||||
slug: {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
},
|
||||
},
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false, collection: 'posts' }),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = new File([''], 'name.png');
|
||||
|
||||
return store.dispatch(persistMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(2);
|
||||
expect(actions[0].type).toEqual('ADD_ASSET');
|
||||
expect(actions[0].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
path: 'static/media/name.png',
|
||||
}),
|
||||
);
|
||||
expect(actions[1].type).toEqual('ADD_DRAFT_ENTRY_MEDIA_FILE');
|
||||
expect(actions[1].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
draft: true,
|
||||
id: '000000000000000',
|
||||
path: 'static/media/name.png',
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(getBlobSHA).toHaveBeenCalledTimes(1);
|
||||
expect(getBlobSHA).toHaveBeenCalledWith(file);
|
||||
expect(backend.persistMedia).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media when not editing draft', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
media_folder: 'static/media',
|
||||
slug: {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
},
|
||||
},
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map(),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = new File([''], 'name.png');
|
||||
|
||||
return store.dispatch(persistMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(3);
|
||||
|
||||
expect(actions).toHaveLength(3);
|
||||
expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' });
|
||||
expect(actions[1].type).toEqual('ADD_ASSET');
|
||||
expect(actions[1].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
path: 'static/media/name.png',
|
||||
}),
|
||||
);
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'MEDIA_PERSIST_SUCCESS',
|
||||
payload: {
|
||||
file: { id: 'id' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
|
||||
expect(backend.persistMedia).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
expect.objectContaining({
|
||||
path: 'static/media/name.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize media name if needed when persisting', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
media_folder: 'static/media',
|
||||
slug: {
|
||||
encoding: 'ascii',
|
||||
clean_accents: true,
|
||||
sanitize_replacement: '_',
|
||||
},
|
||||
},
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map(),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = new File([''], 'abc DEF éâçÖ $;, .png');
|
||||
|
||||
return store.dispatch(persistMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(3);
|
||||
|
||||
expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' });
|
||||
|
||||
expect(actions[1].type).toEqual('ADD_ASSET');
|
||||
expect(actions[1].payload).toEqual(
|
||||
expect.objectContaining({
|
||||
path: 'static/media/abc_def_eaco_.png',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'MEDIA_PERSIST_SUCCESS',
|
||||
payload: {
|
||||
file: { id: 'id' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
|
||||
expect(backend.persistMedia).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
expect.objectContaining({
|
||||
path: 'static/media/abc_def_eaco_.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMedia', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should delete non draft file', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
collections: Map(),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false }),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: false };
|
||||
|
||||
return store.dispatch(deleteMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(4);
|
||||
expect(actions[0]).toEqual({ type: 'MEDIA_DELETE_REQUEST' });
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'REMOVE_ASSET',
|
||||
payload: 'static/media/name.png',
|
||||
});
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'MEDIA_DELETE_SUCCESS',
|
||||
payload: { file },
|
||||
});
|
||||
expect(actions[3]).toEqual({
|
||||
type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE',
|
||||
payload: { id: 'id' },
|
||||
});
|
||||
|
||||
expect(backend.deleteMedia).toHaveBeenCalledTimes(1);
|
||||
expect(backend.deleteMedia).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
'static/media/name.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete a draft file', () => {
|
||||
const store = mockStore({
|
||||
config: {
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
collections: Map(),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false }),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: true };
|
||||
|
||||
return store.dispatch(deleteMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(2);
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'REMOVE_ASSET',
|
||||
payload: 'static/media/name.png',
|
||||
});
|
||||
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE',
|
||||
payload: { id: 'id' },
|
||||
});
|
||||
|
||||
expect(backend.deleteMedia).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
209
node_modules/decap-cms-core/src/actions/__tests__/search.spec.js
generated
vendored
Normal file
209
node_modules/decap-cms-core/src/actions/__tests__/search.spec.js
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { searchEntries } from '../search';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureMockStore(middlewares);
|
||||
|
||||
jest.mock('../../reducers');
|
||||
jest.mock('../../backend');
|
||||
jest.mock('../../integrations');
|
||||
|
||||
describe('search', () => {
|
||||
describe('searchEntries', () => {
|
||||
const { currentBackend } = require('../../backend');
|
||||
const { selectIntegration } = require('../../reducers');
|
||||
const { getIntegrationProvider } = require('../../integrations');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should search entries in all collections using integration', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: {},
|
||||
});
|
||||
|
||||
selectIntegration.mockReturnValue('search_integration');
|
||||
currentBackend.mockReturnValue({});
|
||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||
const integration = { search: jest.fn().mockResolvedValue(response) };
|
||||
getIntegrationProvider.mockReturnValue(integration);
|
||||
|
||||
await store.dispatch(searchEntries('find me'));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(2);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_REQUEST',
|
||||
payload: {
|
||||
searchTerm: 'find me',
|
||||
searchCollections: ['posts', 'pages'],
|
||||
page: 0,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||
payload: {
|
||||
entries: response.entries,
|
||||
page: response.pagination,
|
||||
},
|
||||
});
|
||||
|
||||
expect(integration.search).toHaveBeenCalledTimes(1);
|
||||
expect(integration.search).toHaveBeenCalledWith(['posts', 'pages'], 'find me', 0);
|
||||
});
|
||||
|
||||
it('should search entries in a subset of collections using integration', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: {},
|
||||
});
|
||||
|
||||
selectIntegration.mockReturnValue('search_integration');
|
||||
currentBackend.mockReturnValue({});
|
||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||
const integration = { search: jest.fn().mockResolvedValue(response) };
|
||||
getIntegrationProvider.mockReturnValue(integration);
|
||||
|
||||
await store.dispatch(searchEntries('find me', ['pages']));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(2);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_REQUEST',
|
||||
payload: {
|
||||
searchTerm: 'find me',
|
||||
searchCollections: ['pages'],
|
||||
page: 0,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||
payload: {
|
||||
entries: response.entries,
|
||||
page: response.pagination,
|
||||
},
|
||||
});
|
||||
|
||||
expect(integration.search).toHaveBeenCalledTimes(1);
|
||||
expect(integration.search).toHaveBeenCalledWith(['pages'], 'find me', 0);
|
||||
});
|
||||
|
||||
it('should search entries in all collections using backend', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: {},
|
||||
});
|
||||
|
||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||
const backend = { search: jest.fn().mockResolvedValue(response) };
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
await store.dispatch(searchEntries('find me'));
|
||||
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(2);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_REQUEST',
|
||||
payload: {
|
||||
searchTerm: 'find me',
|
||||
searchCollections: ['posts', 'pages'],
|
||||
page: 0,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||
payload: {
|
||||
entries: response.entries,
|
||||
page: response.pagination,
|
||||
},
|
||||
});
|
||||
|
||||
expect(backend.search).toHaveBeenCalledTimes(1);
|
||||
expect(backend.search).toHaveBeenCalledWith(
|
||||
[fromJS({ name: 'posts' }), fromJS({ name: 'pages' })],
|
||||
'find me',
|
||||
);
|
||||
});
|
||||
|
||||
it('should search entries in a subset of collections using backend', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: {},
|
||||
});
|
||||
|
||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||
const backend = { search: jest.fn().mockResolvedValue(response) };
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
await store.dispatch(searchEntries('find me', ['pages']));
|
||||
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(2);
|
||||
|
||||
expect(actions[0]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_REQUEST',
|
||||
payload: {
|
||||
searchTerm: 'find me',
|
||||
searchCollections: ['pages'],
|
||||
page: 0,
|
||||
},
|
||||
});
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||
payload: {
|
||||
entries: response.entries,
|
||||
page: response.pagination,
|
||||
},
|
||||
});
|
||||
|
||||
expect(backend.search).toHaveBeenCalledTimes(1);
|
||||
expect(backend.search).toHaveBeenCalledWith([fromJS({ name: 'pages' })], 'find me');
|
||||
});
|
||||
|
||||
it('should ignore identical search in all collections', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: { isFetching: true, term: 'find me', collections: ['posts', 'pages'] },
|
||||
});
|
||||
|
||||
await store.dispatch(searchEntries('find me'));
|
||||
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should ignore identical search in a subset of collections', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: { isFetching: true, term: 'find me', collections: ['pages'] },
|
||||
});
|
||||
|
||||
await store.dispatch(searchEntries('find me', ['pages']));
|
||||
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not ignore same search term in different search collections', async () => {
|
||||
const store = mockStore({
|
||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||
search: { isFetching: true, term: 'find me', collections: ['pages'] },
|
||||
});
|
||||
const backend = { search: jest.fn().mockResolvedValue({}) };
|
||||
currentBackend.mockReturnValue(backend);
|
||||
|
||||
await store.dispatch(searchEntries('find me', ['posts', 'pages']));
|
||||
|
||||
expect(backend.search).toHaveBeenCalledTimes(1);
|
||||
expect(backend.search).toHaveBeenCalledWith(
|
||||
[fromJS({ name: 'posts' }), fromJS({ name: 'pages' })],
|
||||
'find me',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
127
node_modules/decap-cms-core/src/actions/auth.ts
generated
vendored
Normal file
127
node_modules/decap-cms-core/src/actions/auth.ts
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
import { currentBackend } from '../backend';
|
||||
import { addNotification, clearNotifications } from './notifications';
|
||||
|
||||
import type { Credentials, User } from 'decap-cms-lib-util';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { State } from '../types/redux';
|
||||
|
||||
export const AUTH_REQUEST = 'AUTH_REQUEST';
|
||||
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
|
||||
export const AUTH_FAILURE = 'AUTH_FAILURE';
|
||||
export const AUTH_REQUEST_DONE = 'AUTH_REQUEST_DONE';
|
||||
export const USE_OPEN_AUTHORING = 'USE_OPEN_AUTHORING';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export function authenticating() {
|
||||
return {
|
||||
type: AUTH_REQUEST,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function authenticate(userData: User) {
|
||||
return {
|
||||
type: AUTH_SUCCESS,
|
||||
payload: userData,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function authError(error: Error) {
|
||||
return {
|
||||
type: AUTH_FAILURE,
|
||||
error: 'Failed to authenticate',
|
||||
payload: error,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function doneAuthenticating() {
|
||||
return {
|
||||
type: AUTH_REQUEST_DONE,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function useOpenAuthoring() {
|
||||
return {
|
||||
type: USE_OPEN_AUTHORING,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return {
|
||||
type: LOGOUT,
|
||||
} as const;
|
||||
}
|
||||
|
||||
// Check if user data token is cached and is valid
|
||||
export function authenticateUser() {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(authenticating());
|
||||
return Promise.resolve(backend.currentUser())
|
||||
.then(user => {
|
||||
if (user) {
|
||||
if (user.useOpenAuthoring) {
|
||||
dispatch(useOpenAuthoring());
|
||||
}
|
||||
dispatch(authenticate(user));
|
||||
} else {
|
||||
dispatch(doneAuthenticating());
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(authError(error));
|
||||
dispatch(logoutUser());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loginUser(credentials: Credentials) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
|
||||
dispatch(authenticating());
|
||||
return backend
|
||||
.authenticate(credentials)
|
||||
.then(user => {
|
||||
if (user.useOpenAuthoring) {
|
||||
dispatch(useOpenAuthoring());
|
||||
}
|
||||
dispatch(authenticate(user));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
details: error.message,
|
||||
key: 'ui.toast.onFailToAuth',
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(authError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function logoutUser() {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
Promise.resolve(backend.logout()).then(() => {
|
||||
dispatch(logout());
|
||||
dispatch(clearNotifications());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type AuthAction = ReturnType<
|
||||
| typeof authenticating
|
||||
| typeof authenticate
|
||||
| typeof authError
|
||||
| typeof doneAuthenticating
|
||||
| typeof logout
|
||||
>;
|
||||
18
node_modules/decap-cms-core/src/actions/collections.ts
generated
vendored
Normal file
18
node_modules/decap-cms-core/src/actions/collections.ts
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { history } from '../routing/history';
|
||||
import { getCollectionUrl, getNewEntryUrl } from '../lib/urlHelper';
|
||||
|
||||
export function searchCollections(query: string, collection: string) {
|
||||
if (collection) {
|
||||
history.push(`/collections/${collection}/search/${query}`);
|
||||
} else {
|
||||
history.push(`/search/${query}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function showCollection(collectionName: string) {
|
||||
history.push(getCollectionUrl(collectionName));
|
||||
}
|
||||
|
||||
export function createNewEntry(collectionName: string) {
|
||||
history.push(getNewEntryUrl(collectionName));
|
||||
}
|
||||
538
node_modules/decap-cms-core/src/actions/config.ts
generated
vendored
Normal file
538
node_modules/decap-cms-core/src/actions/config.ts
generated
vendored
Normal file
@@ -0,0 +1,538 @@
|
||||
import yaml from 'yaml';
|
||||
import { fromJS } from 'immutable';
|
||||
import deepmerge from 'deepmerge';
|
||||
import { produce } from 'immer';
|
||||
import { trimStart, trim, isEmpty } from 'lodash';
|
||||
|
||||
import { SIMPLE as SIMPLE_PUBLISH_MODE } from '../constants/publishModes';
|
||||
import { validateConfig } from '../constants/configSchema';
|
||||
import { selectDefaultSortableFields } from '../reducers/collections';
|
||||
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 type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type {
|
||||
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 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 => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function getConfigUrl() {
|
||||
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<T extends CmsField>(field: T) {
|
||||
if ('media_folder' in field && !('public_folder' in field)) {
|
||||
return { ...field, public_folder: field.media_folder };
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
// Mapping between existing camelCase and its snake_case counterpart
|
||||
const WIDGET_KEY_MAP = {
|
||||
dateFormat: 'date_format',
|
||||
timeFormat: 'time_format',
|
||||
pickerUtc: 'picker_utc',
|
||||
editorComponents: 'editor_components',
|
||||
valueType: 'value_type',
|
||||
valueField: 'value_field',
|
||||
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>;
|
||||
|
||||
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 as unknown as Record<string, unknown>)[camel] };
|
||||
});
|
||||
|
||||
return Object.assign({}, field, ...snakeValues) as T;
|
||||
}
|
||||
|
||||
function setI18nField<T extends CmsField>(field: T) {
|
||||
if (field[I18N] === true) {
|
||||
return { ...field, [I18N]: I18N_FIELD.TRANSLATE };
|
||||
} else if (field[I18N] === false || !field[I18N]) {
|
||||
return { ...field, [I18N]: I18N_FIELD.NONE };
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
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: CmsI18nConfig = deepmerge(defaultI18n, collectionOrFileI18n);
|
||||
mergedI18n.locales = locales;
|
||||
mergedI18n.default_locale = defaultLocale;
|
||||
throwOnMissingDefaultLocale(mergedI18n);
|
||||
return mergedI18n;
|
||||
}
|
||||
}
|
||||
|
||||
function setI18nDefaultsForFields(collectionOrFileFields: CmsField[], hasI18n: boolean) {
|
||||
if (hasI18n) {
|
||||
return traverseFieldsJS(collectionOrFileFields, setI18nField);
|
||||
} else {
|
||||
return traverseFieldsJS(collectionOrFileFields, field => {
|
||||
const newField = { ...field };
|
||||
delete newField[I18N];
|
||||
return newField;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 ${
|
||||
i18n.default_locale
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: CmsConfig) {
|
||||
const { collections = [] } = config;
|
||||
|
||||
const normalizedCollections = collections.map(collection => {
|
||||
const { fields, files } = collection;
|
||||
|
||||
let normalizedCollection = collection;
|
||||
if (fields) {
|
||||
const normalizedFields = traverseFieldsJS(fields, setSnakeCaseConfig);
|
||||
normalizedCollection = { ...normalizedCollection, fields: normalizedFields };
|
||||
}
|
||||
|
||||
if (files) {
|
||||
const normalizedFiles = files.map(file => {
|
||||
const normalizedFileFields = traverseFieldsJS(file.fields, setSnakeCaseConfig);
|
||||
return { ...file, fields: normalizedFileFields };
|
||||
});
|
||||
normalizedCollection = { ...normalizedCollection, files: normalizedFiles };
|
||||
}
|
||||
|
||||
if (normalizedCollection.sortableFields) {
|
||||
const { sortableFields, ...rest } = normalizedCollection;
|
||||
normalizedCollection = { ...rest, sortable_fields: sortableFields };
|
||||
|
||||
console.warn(
|
||||
`Collection ${collection.name} is using a deprecated configuration 'sortableFields'. Please use 'sortable_fields'`,
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedCollection;
|
||||
});
|
||||
|
||||
return { ...config, collections: normalizedCollections };
|
||||
}
|
||||
|
||||
export function applyDefaults(originalConfig: CmsConfig) {
|
||||
return produce(originalConfig, config => {
|
||||
config.publish_mode = config.publish_mode || SIMPLE_PUBLISH_MODE;
|
||||
config.slug = config.slug || {};
|
||||
config.collections = config.collections || [];
|
||||
|
||||
// Use `site_url` as default `display_url`.
|
||||
if (!config.display_url && config.site_url) {
|
||||
config.display_url = config.site_url;
|
||||
}
|
||||
|
||||
// Use media_folder as default public_folder.
|
||||
const defaultPublicFolder = `/${trimStart(config.media_folder, '/')}`;
|
||||
if (!('public_folder' in config)) {
|
||||
config.public_folder = defaultPublicFolder;
|
||||
}
|
||||
|
||||
// default values for the slug config
|
||||
if (!('encoding' in config.slug)) {
|
||||
config.slug.encoding = 'unicode';
|
||||
}
|
||||
|
||||
if (!('clean_accents' in config.slug)) {
|
||||
config.slug.clean_accents = false;
|
||||
}
|
||||
|
||||
if (!('sanitize_replacement' in config.slug)) {
|
||||
config.slug.sanitize_replacement = '-';
|
||||
}
|
||||
|
||||
const i18n = config[I18N];
|
||||
|
||||
if (i18n) {
|
||||
i18n.default_locale = i18n.default_locale || i18n.locales[0];
|
||||
}
|
||||
|
||||
throwOnMissingDefaultLocale(i18n);
|
||||
|
||||
const backend = resolveBackend(config);
|
||||
|
||||
for (const collection of config.collections) {
|
||||
if (!('publish' in collection)) {
|
||||
collection.publish = true;
|
||||
}
|
||||
|
||||
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, Boolean(collectionI18n));
|
||||
}
|
||||
|
||||
const { folder, files, view_filters, view_groups, meta } = collection;
|
||||
|
||||
if (folder) {
|
||||
collection.type = FOLDER;
|
||||
|
||||
if (collection.path && !collection.media_folder) {
|
||||
// default value for media folder when using the path config
|
||||
collection.media_folder = '';
|
||||
}
|
||||
|
||||
if ('media_folder' in collection && !('public_folder' in collection)) {
|
||||
collection.public_folder = collection.media_folder;
|
||||
}
|
||||
|
||||
if (collection.fields) {
|
||||
collection.fields = traverseFieldsJS(collection.fields, setDefaultPublicFolderForField);
|
||||
}
|
||||
|
||||
collection.folder = trim(folder, '/');
|
||||
|
||||
if (meta && meta.path) {
|
||||
const metaField = {
|
||||
name: 'path',
|
||||
meta: true,
|
||||
required: true,
|
||||
...meta.path,
|
||||
};
|
||||
collection.fields = [metaField, ...(collection.fields || [])];
|
||||
}
|
||||
}
|
||||
|
||||
if (files) {
|
||||
collection.type = FILES;
|
||||
|
||||
throwOnInvalidFileCollectionStructure(collectionI18n);
|
||||
|
||||
delete collection.nested;
|
||||
delete collection.meta;
|
||||
|
||||
for (const file of files) {
|
||||
file.file = trimStart(file.file, '/');
|
||||
|
||||
if ('media_folder' in file && !('public_folder' in file)) {
|
||||
file.public_folder = file.media_folder;
|
||||
}
|
||||
|
||||
if (file.fields) {
|
||||
file.fields = traverseFieldsJS(file.fields, setDefaultPublicFolderForField);
|
||||
}
|
||||
|
||||
let fileI18n = file[I18N];
|
||||
|
||||
if (fileI18n && collectionI18n) {
|
||||
fileI18n = getI18nDefaults(fileI18n, collectionI18n);
|
||||
file[I18N] = fileI18n;
|
||||
} else {
|
||||
fileI18n = undefined;
|
||||
delete file[I18N];
|
||||
}
|
||||
|
||||
throwOnInvalidFileCollectionStructure(fileI18n);
|
||||
|
||||
if (file.fields) {
|
||||
file.fields = setI18nDefaultsForFields(file.fields, Boolean(fileI18n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!collection.sortable_fields) {
|
||||
collection.sortable_fields = selectDefaultSortableFields(
|
||||
// TODO remove fromJS when Immutable is removed from the collections state slice
|
||||
fromJS(collection),
|
||||
backend,
|
||||
hasIntegration(config, collection),
|
||||
);
|
||||
}
|
||||
|
||||
collection.view_filters = (view_filters || []).map(filter => {
|
||||
return {
|
||||
...filter,
|
||||
id: `${filter.field}__${filter.pattern}`,
|
||||
};
|
||||
});
|
||||
|
||||
collection.view_groups = (view_groups || []).map(group => {
|
||||
return {
|
||||
...group,
|
||||
id: `${group.field}__${group.pattern}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (config.editor && !collection.editor) {
|
||||
collection.editor = { preview: config.editor.preview };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function parseConfig(data: string) {
|
||||
const config = yaml.parse(data, { maxAliasCount: -1, prettyErrors: true, merge: true });
|
||||
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 as Partial<CmsConfig>;
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
return parseConfig(await response.text());
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
type: CONFIG_FAILURE,
|
||||
error: 'Error loading config',
|
||||
payload: err,
|
||||
} as const;
|
||||
}
|
||||
|
||||
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 Decap 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 Decap CMS Proxy Server at '${proxyUrl}' with repo: '${repo}'`);
|
||||
return { proxyUrl, publish_modes, type };
|
||||
} else {
|
||||
console.log(`Decap CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
return {};
|
||||
}
|
||||
} catch {
|
||||
console.log(`Decap CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getPublishMode(config: CmsConfig, publishModes?: CmsPublishMode[], backendType?: string) {
|
||||
if (config.publish_mode && publishModes && !publishModes.includes(config.publish_mode)) {
|
||||
const newPublishMode = publishModes[0];
|
||||
console.log(
|
||||
`'${config.publish_mode}' is not supported by '${backendType}' backend, switching to '${newPublishMode}'`,
|
||||
);
|
||||
return newPublishMode;
|
||||
}
|
||||
|
||||
return config.publish_mode;
|
||||
}
|
||||
|
||||
export async function handleLocalBackend(originalConfig: CmsConfig) {
|
||||
if (!originalConfig.local_backend) {
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
const {
|
||||
proxyUrl,
|
||||
publish_modes: publishModes,
|
||||
type: backendType,
|
||||
} = await detectProxyServer(originalConfig.local_backend);
|
||||
|
||||
if (!proxyUrl) {
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
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: Partial<CmsConfig> = {}, onLoad: () => unknown) {
|
||||
if (window.CMS_CONFIG) {
|
||||
return configLoaded(window.CMS_CONFIG);
|
||||
}
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>) => {
|
||||
dispatch(configLoading());
|
||||
|
||||
try {
|
||||
const configUrl = getConfigUrl();
|
||||
const hasManualConfig = !isEmpty(manualConfig);
|
||||
const configYaml =
|
||||
manualConfig.load_config_file === false
|
||||
? {}
|
||||
: await getConfigYaml(configUrl, hasManualConfig);
|
||||
|
||||
// Merge manual config into the config.yml one
|
||||
const mergedConfig = deepmerge(configYaml, manualConfig);
|
||||
|
||||
validateConfig(mergedConfig);
|
||||
|
||||
const withLocalBackend = await handleLocalBackend(mergedConfig);
|
||||
const normalizedConfig = normalizeConfig(withLocalBackend);
|
||||
|
||||
const config = applyDefaults(normalizedConfig);
|
||||
|
||||
dispatch(configLoaded(config));
|
||||
|
||||
if (typeof onLoad === 'function') {
|
||||
onLoad();
|
||||
}
|
||||
} catch (err) {
|
||||
dispatch(configFailed(err));
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type ConfigAction = ReturnType<
|
||||
typeof configLoading | typeof configLoaded | typeof configFailed
|
||||
>;
|
||||
104
node_modules/decap-cms-core/src/actions/deploys.ts
generated
vendored
Normal file
104
node_modules/decap-cms-core/src/actions/deploys.ts
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
import { currentBackend } from '../backend';
|
||||
import { selectDeployPreview } from '../reducers';
|
||||
import { addNotification } from './notifications';
|
||||
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { Collection, Entry, State } from '../types/redux';
|
||||
|
||||
export const DEPLOY_PREVIEW_REQUEST = 'DEPLOY_PREVIEW_REQUEST';
|
||||
export const DEPLOY_PREVIEW_SUCCESS = 'DEPLOY_PREVIEW_SUCCESS';
|
||||
export const DEPLOY_PREVIEW_FAILURE = 'DEPLOY_PREVIEW_FAILURE';
|
||||
|
||||
function deployPreviewLoading(collection: string, slug: string) {
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_REQUEST,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
function deployPreviewLoaded(
|
||||
collection: string,
|
||||
slug: string,
|
||||
deploy: { url: string | undefined; status: string },
|
||||
) {
|
||||
const { url, status } = deploy;
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
url,
|
||||
status,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
function deployPreviewError(collection: string, slug: string) {
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_FAILURE,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a deploy preview object from the registered backend.
|
||||
*/
|
||||
export function loadDeployPreview(
|
||||
collection: Collection,
|
||||
slug: string,
|
||||
entry: Entry,
|
||||
published: boolean,
|
||||
opts?: { maxAttempts?: number; interval?: number },
|
||||
) {
|
||||
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const collectionName = collection.get('name');
|
||||
|
||||
// Exit if currently fetching
|
||||
const deployState = selectDeployPreview(state, collectionName, slug);
|
||||
if (deployState && deployState.isFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(deployPreviewLoading(collectionName, slug));
|
||||
|
||||
try {
|
||||
/**
|
||||
* `getDeploy` is for published entries, while `getDeployPreview` is for
|
||||
* unpublished entries.
|
||||
*/
|
||||
const deploy = published
|
||||
? backend.getDeploy(collection, slug, entry)
|
||||
: await backend.getDeployPreview(collection, slug, entry, opts);
|
||||
if (deploy) {
|
||||
return dispatch(deployPreviewLoaded(collectionName, slug, deploy));
|
||||
}
|
||||
return dispatch(deployPreviewError(collectionName, slug));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
details: error.message,
|
||||
key: 'ui.toast.onFailToLoadDeployPreview',
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(deployPreviewError(collectionName, slug));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type DeploysAction = ReturnType<
|
||||
typeof deployPreviewLoading | typeof deployPreviewLoaded | typeof deployPreviewError
|
||||
>;
|
||||
567
node_modules/decap-cms-core/src/actions/editorialWorkflow.ts
generated
vendored
Normal file
567
node_modules/decap-cms-core/src/actions/editorialWorkflow.ts
generated
vendored
Normal file
@@ -0,0 +1,567 @@
|
||||
import { get } from 'lodash';
|
||||
import { Map, List } from 'immutable';
|
||||
import { EDITORIAL_WORKFLOW_ERROR } from 'decap-cms-lib-util';
|
||||
|
||||
import { currentBackend, slugFromCustomPath } from '../backend';
|
||||
import {
|
||||
selectPublishedSlugs,
|
||||
selectUnpublishedSlugs,
|
||||
selectEntry,
|
||||
selectUnpublishedEntry,
|
||||
} from '../reducers';
|
||||
import { selectEditingDraft } from '../reducers/entries';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../constants/publishModes';
|
||||
import {
|
||||
loadEntry,
|
||||
entryDeleted,
|
||||
getMediaAssets,
|
||||
createDraftFromEntry,
|
||||
loadEntries,
|
||||
getSerializedEntry,
|
||||
} from './entries';
|
||||
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import { addAssets } from './media';
|
||||
import { loadMedia } from './mediaLibrary';
|
||||
import ValidationErrorTypes from '../constants/validationErrorTypes';
|
||||
import { navigateToEntry } from '../routing/history';
|
||||
import { addNotification } from './notifications';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
EntryMap,
|
||||
State,
|
||||
Collections,
|
||||
EntryDraft,
|
||||
MediaFile,
|
||||
} from '../types/redux';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { EntryValue } from '../valueObjects/Entry';
|
||||
import type { Status } from '../constants/publishModes';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
|
||||
/*
|
||||
* Constant Declarations
|
||||
*/
|
||||
export const UNPUBLISHED_ENTRY_REQUEST = 'UNPUBLISHED_ENTRY_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_SUCCESS = 'UNPUBLISHED_ENTRY_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_REDIRECT = 'UNPUBLISHED_ENTRY_REDIRECT';
|
||||
|
||||
export const UNPUBLISHED_ENTRIES_REQUEST = 'UNPUBLISHED_ENTRIES_REQUEST';
|
||||
export const UNPUBLISHED_ENTRIES_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRIES_FAILURE = 'UNPUBLISHED_ENTRIES_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_REQUEST = 'UNPUBLISHED_ENTRY_PERSIST_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_SUCCESS = 'UNPUBLISHED_ENTRY_PERSIST_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_FAILURE = 'UNPUBLISHED_ENTRY_PERSIST_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_DELETE_REQUEST = 'UNPUBLISHED_ENTRY_DELETE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_DELETE_SUCCESS = 'UNPUBLISHED_ENTRY_DELETE_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_DELETE_FAILURE = 'UNPUBLISHED_ENTRY_DELETE_FAILURE';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
*/
|
||||
|
||||
function unpublishedEntryLoading(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_REQUEST,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryLoaded(
|
||||
collection: Collection,
|
||||
entry: EntryValue & { mediaFiles: MediaFile[] },
|
||||
) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_SUCCESS,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryRedirected(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_REDIRECT,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesLoading() {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesLoaded(entries: EntryValue[], pagination: number) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
entries,
|
||||
pages: pagination,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesFailed(error: Error) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_FAILURE,
|
||||
error: 'Failed to load entries',
|
||||
payload: error,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisting(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisted(collection: Collection, entry: EntryMap) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersistedFail(error: Error, collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangeRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangePersisted(
|
||||
collection: string,
|
||||
slug: string,
|
||||
newStatus: Status,
|
||||
) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
newStatus,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangeError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublished(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleteRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_REQUEST,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleted(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_SUCCESS,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleteError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported Thunk Action Creators
|
||||
*/
|
||||
|
||||
export function loadUnpublishedEntry(collection: Collection, slug: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
//run possible unpublishedEntries migration
|
||||
if (!entriesLoaded) {
|
||||
try {
|
||||
const { entries, pagination } = await backend.unpublishedEntries(state.collections);
|
||||
dispatch(unpublishedEntriesLoaded(entries, pagination));
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
dispatch(unpublishedEntryLoading(collection, slug));
|
||||
|
||||
try {
|
||||
const entry = (await backend.unpublishedEntry(state, collection, slug)) as EntryValue;
|
||||
const assetProxies = await Promise.all(
|
||||
entry.mediaFiles
|
||||
.filter(file => file.draft)
|
||||
.map(({ url, file, path }) =>
|
||||
createAssetProxy({
|
||||
path,
|
||||
url,
|
||||
file,
|
||||
}),
|
||||
),
|
||||
);
|
||||
dispatch(addAssets(assetProxies));
|
||||
dispatch(unpublishedEntryLoaded(collection, entry));
|
||||
dispatch(createDraftFromEntry(entry));
|
||||
} catch (error) {
|
||||
if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
|
||||
dispatch(unpublishedEntryRedirected(collection, slug));
|
||||
dispatch(loadEntry(collection, slug));
|
||||
} else {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.onFailToLoadEntries',
|
||||
details: error,
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadUnpublishedEntries(collections: Collections) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
|
||||
if (state.config.publish_mode !== EDITORIAL_WORKFLOW || entriesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(unpublishedEntriesLoading());
|
||||
backend
|
||||
.unpublishedEntries(collections)
|
||||
.then(response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)))
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.onFailToLoadEntries',
|
||||
details: error,
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntriesFailed(error));
|
||||
Promise.reject(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const entryDraft = state.entryDraft;
|
||||
const fieldsErrors = entryDraft.get('fieldsErrors');
|
||||
const unpublishedSlugs = selectUnpublishedSlugs(state, collection.get('name'));
|
||||
const publishedSlugs = selectPublishedSlugs(state, collection.get('name'));
|
||||
const usedSlugs = publishedSlugs.concat(unpublishedSlugs) as List<string>;
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
|
||||
//load unpublishedEntries
|
||||
!entriesLoaded && dispatch(loadUnpublishedEntries(state.collections));
|
||||
|
||||
// Early return if draft contains validation errors
|
||||
if (!fieldsErrors.isEmpty()) {
|
||||
const hasPresenceErrors = fieldsErrors.some(errors =>
|
||||
errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE),
|
||||
);
|
||||
|
||||
if (hasPresenceErrors) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.missingRequiredField',
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = entryDraft.get('entry');
|
||||
const assetProxies = getMediaAssets({
|
||||
entry,
|
||||
});
|
||||
|
||||
const serializedEntry = getSerializedEntry(collection, entry);
|
||||
const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
|
||||
|
||||
dispatch(unpublishedEntryPersisting(collection, entry.get('slug')));
|
||||
const persistAction = existingUnpublishedEntry
|
||||
? backend.persistUnpublishedEntry
|
||||
: backend.persistEntry;
|
||||
|
||||
try {
|
||||
const newSlug = await persistAction.call(backend, {
|
||||
config: state.config,
|
||||
collection,
|
||||
entryDraft: serializedEntryDraft,
|
||||
assetProxies,
|
||||
usedSlugs,
|
||||
});
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.entrySaved',
|
||||
},
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPersisted(collection, serializedEntry));
|
||||
|
||||
if (entry.get('slug') !== newSlug) {
|
||||
await dispatch(loadUnpublishedEntry(collection, newSlug));
|
||||
navigateToEntry(collection.get('name'), newSlug);
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.onFailToPersist',
|
||||
details: error,
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
return Promise.reject(
|
||||
dispatch(unpublishedEntryPersistedFail(error, collection, entry.get('slug'))),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUnpublishedEntryStatus(
|
||||
collection: string,
|
||||
slug: string,
|
||||
oldStatus: Status,
|
||||
newStatus: Status,
|
||||
) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
if (oldStatus === newStatus) return;
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryStatusChangeRequest(collection, slug));
|
||||
backend
|
||||
.updateUnpublishedEntryStatus(collection, slug, newStatus)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.entryUpdated',
|
||||
},
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryStatusChangePersisted(collection, slug, newStatus));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.onFailToUpdateStatus',
|
||||
details: error,
|
||||
},
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryStatusChangeError(collection, slug));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryDeleteRequest(collection, slug));
|
||||
return backend
|
||||
.deleteUnpublishedEntry(collection, slug)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.onDeleteUnpublishedChanges' },
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryDeleted(collection, slug));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.onDeleteUnpublishedChanges', details: error },
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryDeleteError(collection, slug));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const collections = state.collections;
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = selectUnpublishedEntry(state, collectionName, slug);
|
||||
dispatch(unpublishedEntryPublishRequest(collectionName, slug));
|
||||
try {
|
||||
await backend.publishUnpublishedEntry(entry);
|
||||
// re-load media after entry was published
|
||||
dispatch(loadMedia());
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.entryPublished' },
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPublished(collectionName, slug));
|
||||
const collection = collections.get(collectionName);
|
||||
if (collection.has('nested')) {
|
||||
dispatch(loadEntries(collection));
|
||||
const newSlug = slugFromCustomPath(collection, entry.get('path'));
|
||||
loadEntry(collection, newSlug);
|
||||
if (slug !== newSlug && selectEditingDraft(state.entryDraft)) {
|
||||
navigateToEntry(collection.get('name'), newSlug);
|
||||
}
|
||||
} else {
|
||||
return dispatch(loadEntry(collection, slug));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.onFailToPublishEntry', details: error },
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPublishError(collectionName, slug));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function unpublishPublishedEntry(collection: Collection, slug: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = selectEntry(state, collection.get('name'), slug);
|
||||
const entryDraft = Map().set('entry', entry) as unknown as EntryDraft;
|
||||
dispatch(unpublishedEntryPersisting(collection, slug));
|
||||
return backend
|
||||
.deleteEntry(state, collection, slug)
|
||||
.then(() =>
|
||||
backend.persistEntry({
|
||||
config: state.config,
|
||||
collection,
|
||||
entryDraft,
|
||||
assetProxies: [],
|
||||
usedSlugs: List(),
|
||||
status: status.get('PENDING_PUBLISH'),
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryPersisted(collection, entry));
|
||||
dispatch(entryDeleted(collection, slug));
|
||||
dispatch(loadUnpublishedEntry(collection, slug));
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.entryUnpublished' },
|
||||
type: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: { key: 'ui.toast.onFailToUnpublishEntry', details: error },
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPersistedFail(error, collection, entry.get('slug')));
|
||||
});
|
||||
};
|
||||
}
|
||||
1041
node_modules/decap-cms-core/src/actions/entries.ts
generated
vendored
Normal file
1041
node_modules/decap-cms-core/src/actions/entries.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
139
node_modules/decap-cms-core/src/actions/media.ts
generated
vendored
Normal file
139
node_modules/decap-cms-core/src/actions/media.ts
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
import { isAbsolutePath } from 'decap-cms-lib-util';
|
||||
|
||||
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import { selectMediaFilePath } from '../reducers/entries';
|
||||
import { selectMediaFileByPath } from '../reducers/mediaLibrary';
|
||||
import { getMediaFile, waitForMediaLibraryToLoad, getMediaDisplayURL } from './mediaLibrary';
|
||||
|
||||
import type AssetProxy from '../valueObjects/AssetProxy';
|
||||
import type { Collection, State, EntryMap, EntryField } from '../types/redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
export const ADD_ASSETS = 'ADD_ASSETS';
|
||||
export const ADD_ASSET = 'ADD_ASSET';
|
||||
export const REMOVE_ASSET = 'REMOVE_ASSET';
|
||||
|
||||
export const LOAD_ASSET_REQUEST = 'LOAD_ASSET_REQUEST';
|
||||
export const LOAD_ASSET_SUCCESS = 'LOAD_ASSET_SUCCESS';
|
||||
export const LOAD_ASSET_FAILURE = 'LOAD_ASSET_FAILURE';
|
||||
|
||||
export function addAssets(assets: AssetProxy[]) {
|
||||
return { type: ADD_ASSETS, payload: assets } as const;
|
||||
}
|
||||
|
||||
export function addAsset(assetProxy: AssetProxy) {
|
||||
return { type: ADD_ASSET, payload: assetProxy } as const;
|
||||
}
|
||||
|
||||
export function removeAsset(path: string) {
|
||||
return { type: REMOVE_ASSET, payload: path } as const;
|
||||
}
|
||||
|
||||
export function loadAssetRequest(path: string) {
|
||||
return { type: LOAD_ASSET_REQUEST, payload: { path } } as const;
|
||||
}
|
||||
|
||||
export function loadAssetSuccess(path: string) {
|
||||
return { type: LOAD_ASSET_SUCCESS, payload: { path } } as const;
|
||||
}
|
||||
|
||||
export function loadAssetFailure(path: string, error: Error) {
|
||||
return { type: LOAD_ASSET_FAILURE, payload: { path, error } } as const;
|
||||
}
|
||||
|
||||
export function loadAsset(resolvedPath: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
try {
|
||||
dispatch(loadAssetRequest(resolvedPath));
|
||||
// load asset url from backend
|
||||
await waitForMediaLibraryToLoad(dispatch, getState());
|
||||
const file = selectMediaFileByPath(getState(), resolvedPath);
|
||||
|
||||
if (file) {
|
||||
const url = await getMediaDisplayURL(dispatch, getState(), file);
|
||||
const asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
|
||||
dispatch(addAsset(asset));
|
||||
} else {
|
||||
const { url } = await getMediaFile(getState(), resolvedPath);
|
||||
const asset = createAssetProxy({ path: resolvedPath, url });
|
||||
dispatch(addAsset(asset));
|
||||
}
|
||||
dispatch(loadAssetSuccess(resolvedPath));
|
||||
} catch (e) {
|
||||
dispatch(loadAssetFailure(resolvedPath, e));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAssetArgs {
|
||||
collection: Collection;
|
||||
entry: EntryMap;
|
||||
path: string;
|
||||
field?: EntryField;
|
||||
}
|
||||
|
||||
const emptyAsset = createAssetProxy({
|
||||
path: 'empty.svg',
|
||||
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], 'empty.svg', {
|
||||
type: 'image/svg+xml',
|
||||
}),
|
||||
});
|
||||
|
||||
export function boundGetAsset(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
collection: Collection,
|
||||
entry: EntryMap,
|
||||
) {
|
||||
function bound(path: string, field: EntryField) {
|
||||
const asset = dispatch(getAsset({ collection, entry, path, field }));
|
||||
return asset;
|
||||
}
|
||||
|
||||
return bound;
|
||||
}
|
||||
|
||||
export function getAsset({ collection, entry, path, field }: GetAssetArgs) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
if (!path) return emptyAsset;
|
||||
|
||||
const state = getState();
|
||||
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, field);
|
||||
|
||||
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
|
||||
if (isLoading) {
|
||||
return emptyAsset;
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
// There is already an AssetProxy in memory for this path. Use it.
|
||||
return asset;
|
||||
}
|
||||
|
||||
if (isAbsolutePath(resolvedPath)) {
|
||||
// asset path is a public url so we can just use it as is
|
||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
||||
dispatch(addAsset(asset));
|
||||
} else {
|
||||
if (error) {
|
||||
// on load error default back to original path
|
||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
||||
dispatch(addAsset(asset));
|
||||
} else {
|
||||
dispatch(loadAsset(resolvedPath));
|
||||
asset = emptyAsset;
|
||||
}
|
||||
}
|
||||
|
||||
return asset;
|
||||
};
|
||||
}
|
||||
|
||||
export type MediasAction = ReturnType<
|
||||
| typeof addAssets
|
||||
| typeof addAsset
|
||||
| typeof removeAsset
|
||||
| typeof loadAssetRequest
|
||||
| typeof loadAssetSuccess
|
||||
| typeof loadAssetFailure
|
||||
>;
|
||||
574
node_modules/decap-cms-core/src/actions/mediaLibrary.ts
generated
vendored
Normal file
574
node_modules/decap-cms-core/src/actions/mediaLibrary.ts
generated
vendored
Normal file
@@ -0,0 +1,574 @@
|
||||
import { Map } from 'immutable';
|
||||
import { basename, getBlobSHA } from 'decap-cms-lib-util';
|
||||
|
||||
import { currentBackend } from '../backend';
|
||||
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import { selectIntegration } from '../reducers';
|
||||
import {
|
||||
selectMediaFilePath,
|
||||
selectMediaFilePublicPath,
|
||||
selectEditingDraft,
|
||||
} from '../reducers/entries';
|
||||
import { selectMediaDisplayURL, selectMediaFiles } from '../reducers/mediaLibrary';
|
||||
import { getIntegrationProvider } from '../integrations';
|
||||
import { addAsset, removeAsset } from './media';
|
||||
import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries';
|
||||
import { sanitizeSlug } from '../lib/urlHelper';
|
||||
import { waitUntilWithTimeout } from './waitUntil';
|
||||
import { addNotification } from './notifications';
|
||||
|
||||
import type {
|
||||
State,
|
||||
MediaFile,
|
||||
DisplayURLState,
|
||||
MediaLibraryInstance,
|
||||
EntryField,
|
||||
} from '../types/redux';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type AssetProxy from '../valueObjects/AssetProxy';
|
||||
import type { ImplementationMediaFile } from 'decap-cms-lib-util';
|
||||
|
||||
export const MEDIA_LIBRARY_OPEN = 'MEDIA_LIBRARY_OPEN';
|
||||
export const MEDIA_LIBRARY_CLOSE = 'MEDIA_LIBRARY_CLOSE';
|
||||
export const MEDIA_LIBRARY_CREATE = 'MEDIA_LIBRARY_CREATE';
|
||||
export const MEDIA_INSERT = 'MEDIA_INSERT';
|
||||
export const MEDIA_REMOVE_INSERTED = 'MEDIA_REMOVE_INSERTED';
|
||||
export const MEDIA_LOAD_REQUEST = 'MEDIA_LOAD_REQUEST';
|
||||
export const MEDIA_LOAD_SUCCESS = 'MEDIA_LOAD_SUCCESS';
|
||||
export const MEDIA_LOAD_FAILURE = 'MEDIA_LOAD_FAILURE';
|
||||
export const MEDIA_PERSIST_REQUEST = 'MEDIA_PERSIST_REQUEST';
|
||||
export const MEDIA_PERSIST_SUCCESS = 'MEDIA_PERSIST_SUCCESS';
|
||||
export const MEDIA_PERSIST_FAILURE = 'MEDIA_PERSIST_FAILURE';
|
||||
export const MEDIA_DELETE_REQUEST = 'MEDIA_DELETE_REQUEST';
|
||||
export const MEDIA_DELETE_SUCCESS = 'MEDIA_DELETE_SUCCESS';
|
||||
export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE';
|
||||
export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST';
|
||||
export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS';
|
||||
export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE';
|
||||
|
||||
export function createMediaLibrary(instance: MediaLibraryInstance) {
|
||||
const api = {
|
||||
show: instance.show || (() => undefined),
|
||||
hide: instance.hide || (() => undefined),
|
||||
onClearControl: instance.onClearControl || (() => undefined),
|
||||
onRemoveControl: instance.onRemoveControl || (() => undefined),
|
||||
enableStandalone: instance.enableStandalone || (() => undefined),
|
||||
};
|
||||
return { type: MEDIA_LIBRARY_CREATE, payload: api } as const;
|
||||
}
|
||||
|
||||
export function clearMediaControl(id: string) {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.onClearControl({ id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeMediaControl(id: string) {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.onRemoveControl({ id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function openMediaLibrary(
|
||||
payload: {
|
||||
controlID?: string;
|
||||
forImage?: boolean;
|
||||
privateUpload?: boolean;
|
||||
value?: string;
|
||||
allowMultiple?: boolean;
|
||||
config?: Map<string, unknown>;
|
||||
field?: EntryField;
|
||||
} = {},
|
||||
) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
const { controlID: id, value, config = Map(), allowMultiple, forImage } = payload;
|
||||
mediaLibrary.show({ id, value, config: config.toJS(), allowMultiple, imagesOnly: forImage });
|
||||
}
|
||||
dispatch(mediaLibraryOpened(payload));
|
||||
};
|
||||
}
|
||||
|
||||
export function closeMediaLibrary() {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.hide();
|
||||
}
|
||||
dispatch(mediaLibraryClosed());
|
||||
};
|
||||
}
|
||||
|
||||
export function insertMedia(mediaPath: string | string[], field: EntryField | undefined) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const config = state.config;
|
||||
const entry = state.entryDraft.get('entry');
|
||||
const collectionName = state.entryDraft.getIn(['entry', 'collection']);
|
||||
const collection = state.collections.get(collectionName);
|
||||
if (Array.isArray(mediaPath)) {
|
||||
mediaPath = mediaPath.map(path =>
|
||||
selectMediaFilePublicPath(config, collection, path, entry, field),
|
||||
);
|
||||
} else {
|
||||
mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry, field);
|
||||
}
|
||||
dispatch(mediaInserted(mediaPath));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeInsertedMedia(controlID: string) {
|
||||
return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } } as const;
|
||||
}
|
||||
|
||||
export function loadMedia(
|
||||
opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {},
|
||||
) {
|
||||
const { delay = 0, query = '', page = 1, privateUpload } = opts;
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const integration = selectIntegration(state, null, 'assetStore');
|
||||
if (integration) {
|
||||
const provider = getIntegrationProvider(state.integrations, backend.getToken, integration);
|
||||
dispatch(mediaLoading(page));
|
||||
try {
|
||||
const files = await provider.retrieve(query, page, privateUpload);
|
||||
const mediaLoadedOpts = {
|
||||
page,
|
||||
canPaginate: true,
|
||||
dynamicSearch: true,
|
||||
dynamicSearchQuery: query,
|
||||
privateUpload,
|
||||
};
|
||||
return dispatch(mediaLoaded(files, mediaLoadedOpts));
|
||||
} catch (error) {
|
||||
return dispatch(mediaLoadFailed({ privateUpload }));
|
||||
}
|
||||
}
|
||||
dispatch(mediaLoading(page));
|
||||
|
||||
function loadFunction() {
|
||||
return backend
|
||||
.getMedia()
|
||||
.then(files => dispatch(mediaLoaded(files)))
|
||||
.catch((error: { status?: number }) => {
|
||||
console.error(error);
|
||||
if (error.status === 404) {
|
||||
console.log('This 404 was expected and handled appropriately.');
|
||||
dispatch(mediaLoaded([]));
|
||||
} else {
|
||||
dispatch(mediaLoadFailed());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (delay > 0) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(loadFunction()), delay);
|
||||
});
|
||||
} else {
|
||||
return loadFunction();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createMediaFileFromAsset({
|
||||
id,
|
||||
file,
|
||||
assetProxy,
|
||||
draft,
|
||||
}: {
|
||||
id: string;
|
||||
file: File;
|
||||
assetProxy: AssetProxy;
|
||||
draft: boolean;
|
||||
}): ImplementationMediaFile {
|
||||
const mediaFile = {
|
||||
id,
|
||||
name: basename(assetProxy.path),
|
||||
displayURL: assetProxy.url,
|
||||
draft,
|
||||
file,
|
||||
size: file.size,
|
||||
url: assetProxy.url,
|
||||
path: assetProxy.path,
|
||||
field: assetProxy.field,
|
||||
};
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
export function persistMedia(file: File, opts: MediaOptions = {}) {
|
||||
const { privateUpload, field } = opts;
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
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.slug);
|
||||
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);
|
||||
|
||||
const editingDraft = selectEditingDraft(state.entryDraft);
|
||||
|
||||
/**
|
||||
* Check for existing files of the same name before persisting. If no asset
|
||||
* store integration is used, files are being stored in Git, so we can
|
||||
* expect file names to be unique. If an asset store is in use, file names
|
||||
* may not be unique, so we forego this check.
|
||||
*/
|
||||
if (!integration && existingFile) {
|
||||
if (!window.confirm(`${existingFile.name} already exists. Do you want to replace it?`)) {
|
||||
return;
|
||||
} else {
|
||||
await dispatch(deleteMedia(existingFile, { privateUpload }));
|
||||
}
|
||||
}
|
||||
|
||||
if (integration || !editingDraft) {
|
||||
dispatch(mediaPersisting());
|
||||
}
|
||||
|
||||
try {
|
||||
let assetProxy: AssetProxy;
|
||||
if (integration) {
|
||||
try {
|
||||
const provider = getIntegrationProvider(
|
||||
state.integrations,
|
||||
backend.getToken,
|
||||
integration,
|
||||
);
|
||||
const response = await provider.upload(file, privateUpload);
|
||||
assetProxy = createAssetProxy({
|
||||
url: response.asset.url,
|
||||
path: response.asset.url,
|
||||
});
|
||||
} catch (error) {
|
||||
assetProxy = createAssetProxy({
|
||||
file,
|
||||
path: fileName,
|
||||
});
|
||||
}
|
||||
} else if (privateUpload) {
|
||||
throw new Error('The Private Upload option is only available for Asset Store Integration');
|
||||
} else {
|
||||
const entry = state.entryDraft.get('entry');
|
||||
const collection = state.collections.get(entry?.get('collection'));
|
||||
const path = selectMediaFilePath(state.config, collection, entry, fileName, field);
|
||||
assetProxy = createAssetProxy({
|
||||
file,
|
||||
path,
|
||||
field,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(addAsset(assetProxy));
|
||||
|
||||
let mediaFile: ImplementationMediaFile;
|
||||
if (integration) {
|
||||
const id = await getBlobSHA(file);
|
||||
// integration assets are persisted immediately, thus draft is false
|
||||
mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: false });
|
||||
} else if (editingDraft) {
|
||||
const id = await getBlobSHA(file);
|
||||
mediaFile = createMediaFileFromAsset({
|
||||
id,
|
||||
file,
|
||||
assetProxy,
|
||||
draft: editingDraft,
|
||||
});
|
||||
return dispatch(addDraftEntryMediaFile(mediaFile));
|
||||
} else {
|
||||
mediaFile = await backend.persistMedia(state.config, assetProxy);
|
||||
}
|
||||
|
||||
return dispatch(mediaPersisted(mediaFile, { privateUpload }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: `Failed to persist media: ${error}`,
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
return dispatch(mediaPersistFailed({ privateUpload }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const integration = selectIntegration(state, null, 'assetStore');
|
||||
if (integration) {
|
||||
const provider = getIntegrationProvider(state.integrations, backend.getToken, integration);
|
||||
dispatch(mediaDeleting());
|
||||
|
||||
try {
|
||||
await provider.delete(file.id);
|
||||
return dispatch(mediaDeleted(file, { privateUpload }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: `Failed to delete media: ${error.message}`,
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
return dispatch(mediaDeleteFailed({ privateUpload }));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (file.draft) {
|
||||
dispatch(removeAsset(file.path));
|
||||
dispatch(removeDraftEntryMediaFile({ id: file.id }));
|
||||
} else {
|
||||
const editingDraft = selectEditingDraft(state.entryDraft);
|
||||
|
||||
dispatch(mediaDeleting());
|
||||
dispatch(removeAsset(file.path));
|
||||
|
||||
await backend.deleteMedia(state.config, file.path);
|
||||
|
||||
dispatch(mediaDeleted(file));
|
||||
if (editingDraft) {
|
||||
dispatch(removeDraftEntryMediaFile({ id: file.id }));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: `Failed to delete media: ${error.message}`,
|
||||
type: 'error',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
return dispatch(mediaDeleteFailed());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMediaFile(state: State, path: string) {
|
||||
const backend = currentBackend(state.config);
|
||||
const { url } = await backend.getMediaFile(path);
|
||||
return { url };
|
||||
}
|
||||
|
||||
export function loadMediaDisplayURL(file: MediaFile) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const { displayURL, id } = file;
|
||||
const state = getState();
|
||||
const displayURLState: DisplayURLState = selectMediaDisplayURL(state, id);
|
||||
if (
|
||||
!id ||
|
||||
!displayURL ||
|
||||
displayURLState.get('url') ||
|
||||
displayURLState.get('isFetching') ||
|
||||
displayURLState.get('err')
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (typeof displayURL === 'string') {
|
||||
dispatch(mediaDisplayURLRequest(id));
|
||||
dispatch(mediaDisplayURLSuccess(id, displayURL));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(mediaDisplayURLRequest(id));
|
||||
const newURL = await backend.getMediaDisplayURL(displayURL);
|
||||
if (newURL) {
|
||||
dispatch(mediaDisplayURLSuccess(id, newURL));
|
||||
} else {
|
||||
throw new Error('No display URL was returned!');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(mediaDisplayURLFailure(id, err));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mediaLibraryOpened(payload: {
|
||||
controlID?: string;
|
||||
forImage?: boolean;
|
||||
privateUpload?: boolean;
|
||||
value?: string;
|
||||
replaceIndex?: number;
|
||||
allowMultiple?: boolean;
|
||||
config?: Map<string, unknown>;
|
||||
field?: EntryField;
|
||||
}) {
|
||||
return { type: MEDIA_LIBRARY_OPEN, payload } as const;
|
||||
}
|
||||
|
||||
function mediaLibraryClosed() {
|
||||
return { type: MEDIA_LIBRARY_CLOSE } as const;
|
||||
}
|
||||
|
||||
function mediaInserted(mediaPath: string | string[]) {
|
||||
return { type: MEDIA_INSERT, payload: { mediaPath } } as const;
|
||||
}
|
||||
|
||||
export function mediaLoading(page: number) {
|
||||
return {
|
||||
type: MEDIA_LOAD_REQUEST,
|
||||
payload: { page },
|
||||
} as const;
|
||||
}
|
||||
|
||||
interface MediaOptions {
|
||||
privateUpload?: boolean;
|
||||
field?: EntryField;
|
||||
page?: number;
|
||||
canPaginate?: boolean;
|
||||
dynamicSearch?: boolean;
|
||||
dynamicSearchQuery?: string;
|
||||
}
|
||||
|
||||
export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
|
||||
return {
|
||||
type: MEDIA_LOAD_SUCCESS,
|
||||
payload: { files, ...opts },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function mediaLoadFailed(opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return { type: MEDIA_LOAD_FAILURE, payload: { privateUpload } } as const;
|
||||
}
|
||||
|
||||
export function mediaPersisting() {
|
||||
return { type: MEDIA_PERSIST_REQUEST } as const;
|
||||
}
|
||||
|
||||
export function mediaPersisted(file: ImplementationMediaFile, opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return {
|
||||
type: MEDIA_PERSIST_SUCCESS,
|
||||
payload: { file, privateUpload },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function mediaPersistFailed(opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return { type: MEDIA_PERSIST_FAILURE, payload: { privateUpload } } as const;
|
||||
}
|
||||
|
||||
export function mediaDeleting() {
|
||||
return { type: MEDIA_DELETE_REQUEST } as const;
|
||||
}
|
||||
|
||||
export function mediaDeleted(file: MediaFile, opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return {
|
||||
type: MEDIA_DELETE_SUCCESS,
|
||||
payload: { file, privateUpload },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function mediaDeleteFailed(opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return { type: MEDIA_DELETE_FAILURE, payload: { privateUpload } } as const;
|
||||
}
|
||||
|
||||
export function mediaDisplayURLRequest(key: string) {
|
||||
return { type: MEDIA_DISPLAY_URL_REQUEST, payload: { key } } as const;
|
||||
}
|
||||
|
||||
export function mediaDisplayURLSuccess(key: string, url: string) {
|
||||
return {
|
||||
type: MEDIA_DISPLAY_URL_SUCCESS,
|
||||
payload: { key, url },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function mediaDisplayURLFailure(key: string, err: Error) {
|
||||
return {
|
||||
type: MEDIA_DISPLAY_URL_FAILURE,
|
||||
payload: { key, err },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export async function waitForMediaLibraryToLoad(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
state: State,
|
||||
) {
|
||||
if (state.mediaLibrary.get('isLoading') !== false && !state.mediaLibrary.get('externalLibrary')) {
|
||||
await waitUntilWithTimeout(dispatch, resolve => ({
|
||||
predicate: ({ type }) => type === MEDIA_LOAD_SUCCESS || type === MEDIA_LOAD_FAILURE,
|
||||
run: () => resolve(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMediaDisplayURL(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
state: State,
|
||||
file: MediaFile,
|
||||
) {
|
||||
const displayURLState: DisplayURLState = selectMediaDisplayURL(state, file.id);
|
||||
|
||||
let url: string | null | undefined;
|
||||
if (displayURLState.get('url')) {
|
||||
// url was already loaded
|
||||
url = displayURLState.get('url');
|
||||
} else if (displayURLState.get('err')) {
|
||||
// url loading had an error
|
||||
url = null;
|
||||
} else {
|
||||
const key = file.id;
|
||||
const promise = waitUntilWithTimeout<string>(dispatch, resolve => ({
|
||||
predicate: ({ type, payload }) =>
|
||||
(type === MEDIA_DISPLAY_URL_SUCCESS || type === MEDIA_DISPLAY_URL_FAILURE) &&
|
||||
payload.key === key,
|
||||
run: (_dispatch, _getState, action) => resolve(action.payload.url),
|
||||
}));
|
||||
|
||||
if (!displayURLState.get('isFetching')) {
|
||||
// load display url
|
||||
dispatch(loadMediaDisplayURL(file));
|
||||
}
|
||||
|
||||
url = (await promise) ?? null;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export type MediaLibraryAction = ReturnType<
|
||||
| typeof createMediaLibrary
|
||||
| typeof mediaLibraryOpened
|
||||
| typeof mediaLibraryClosed
|
||||
| typeof mediaInserted
|
||||
| typeof removeInsertedMedia
|
||||
| typeof mediaLoading
|
||||
| typeof mediaLoaded
|
||||
| typeof mediaLoadFailed
|
||||
| typeof mediaPersisting
|
||||
| typeof mediaPersisted
|
||||
| typeof mediaPersistFailed
|
||||
| typeof mediaDeleting
|
||||
| typeof mediaDeleted
|
||||
| typeof mediaDeleteFailed
|
||||
| typeof mediaDisplayURLRequest
|
||||
| typeof mediaDisplayURLSuccess
|
||||
| typeof mediaDisplayURLFailure
|
||||
>;
|
||||
36
node_modules/decap-cms-core/src/actions/notifications.ts
generated
vendored
Normal file
36
node_modules/decap-cms-core/src/actions/notifications.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { TypeOptions } from 'react-toastify';
|
||||
|
||||
export interface NotificationMessage {
|
||||
details?: unknown;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface NotificationPayload {
|
||||
message: string | NotificationMessage;
|
||||
dismissAfter?: number;
|
||||
type: TypeOptions | undefined;
|
||||
}
|
||||
|
||||
export const NOTIFICATION_SEND = 'NOTIFICATION_SEND';
|
||||
export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS';
|
||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATION_CLEAR';
|
||||
|
||||
function addNotification(notification: NotificationPayload) {
|
||||
return { type: NOTIFICATION_SEND, payload: notification };
|
||||
}
|
||||
|
||||
function dismissNotification(id: string) {
|
||||
return { type: NOTIFICATION_DISMISS, id };
|
||||
}
|
||||
|
||||
function clearNotifications() {
|
||||
return { type: NOTIFICATIONS_CLEAR };
|
||||
}
|
||||
|
||||
export type NotificationsAction = {
|
||||
type: typeof NOTIFICATION_DISMISS | typeof NOTIFICATION_SEND | typeof NOTIFICATIONS_CLEAR;
|
||||
payload?: NotificationPayload;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export { addNotification, dismissNotification, clearNotifications };
|
||||
221
node_modules/decap-cms-core/src/actions/search.ts
generated
vendored
Normal file
221
node_modules/decap-cms-core/src/actions/search.ts
generated
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { currentBackend } from '../backend';
|
||||
import { getIntegrationProvider } from '../integrations';
|
||||
import { selectIntegration } from '../reducers';
|
||||
|
||||
import type { QueryRequest } from '../reducers/search';
|
||||
import type { State } from '../types/redux';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { EntryValue } from '../valueObjects/Entry';
|
||||
|
||||
/*
|
||||
* Constant Declarations
|
||||
*/
|
||||
export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
|
||||
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
||||
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
||||
|
||||
export const QUERY_REQUEST = 'QUERY_REQUEST';
|
||||
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||
export const QUERY_FAILURE = 'QUERY_FAILURE';
|
||||
|
||||
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||
export const CLEAR_REQUESTS = 'CLEAR_REQUESTS';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
* We still need to export them for tests
|
||||
*/
|
||||
export function searchingEntries(searchTerm: string, searchCollections: string[], page: number) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_REQUEST,
|
||||
payload: { searchTerm, searchCollections, page },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function searchSuccess(entries: EntryValue[], page: number) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
entries,
|
||||
page,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function searchFailure(error: Error) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_FAILURE,
|
||||
payload: { error },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function querying(searchTerm: string, request?: QueryRequest) {
|
||||
return {
|
||||
type: QUERY_REQUEST,
|
||||
payload: {
|
||||
searchTerm,
|
||||
request,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
type SearchResponse = {
|
||||
entries: EntryValue[];
|
||||
pagination: number;
|
||||
};
|
||||
|
||||
type QueryResponse = {
|
||||
hits: EntryValue[];
|
||||
query: string;
|
||||
};
|
||||
|
||||
export function querySuccess(namespace: string, hits: EntryValue[]) {
|
||||
return {
|
||||
type: QUERY_SUCCESS,
|
||||
payload: {
|
||||
namespace,
|
||||
hits,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function queryFailure(error: Error) {
|
||||
return {
|
||||
type: QUERY_FAILURE,
|
||||
payload: { error },
|
||||
} as const;
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported simple Action Creators
|
||||
*/
|
||||
|
||||
export function clearSearch() {
|
||||
return { type: SEARCH_CLEAR } as const;
|
||||
}
|
||||
|
||||
export function clearRequests() {
|
||||
return { type: CLEAR_REQUESTS } as const;
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported Thunk Action Creators
|
||||
*/
|
||||
|
||||
// SearchEntries will search for complete entries in all collections.
|
||||
export function searchEntries(searchTerm: string, searchCollections: string[], page = 0) {
|
||||
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const { search } = state;
|
||||
const backend = currentBackend(state.config);
|
||||
const allCollections = searchCollections || state.collections.keySeq().toArray();
|
||||
const collections = allCollections.filter(collection =>
|
||||
selectIntegration(state, collection, 'search'),
|
||||
);
|
||||
const integration = selectIntegration(state, collections[0], 'search');
|
||||
|
||||
// avoid duplicate searches
|
||||
if (
|
||||
search.isFetching &&
|
||||
search.term === searchTerm &&
|
||||
isEqual(allCollections, search.collections) &&
|
||||
// if an integration doesn't exist, 'page' is not used
|
||||
(search.page === page || !integration)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(searchingEntries(searchTerm, allCollections, page));
|
||||
|
||||
const searchPromise = integration
|
||||
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(
|
||||
collections,
|
||||
searchTerm,
|
||||
page,
|
||||
)
|
||||
: backend.search(
|
||||
state.collections
|
||||
.filter((_, key: string) => allCollections.indexOf(key) !== -1)
|
||||
.valueSeq()
|
||||
.toArray(),
|
||||
searchTerm,
|
||||
);
|
||||
|
||||
try {
|
||||
const response: SearchResponse = await searchPromise;
|
||||
return dispatch(searchSuccess(response.entries, response.pagination));
|
||||
} catch (error) {
|
||||
return dispatch(searchFailure(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instead of searching for complete entries, query will search for specific fields
|
||||
// in specific collections and return raw data (no entries).
|
||||
export function query(
|
||||
namespace: string,
|
||||
collectionName: string,
|
||||
searchFields: string[],
|
||||
searchTerm: string,
|
||||
file?: string,
|
||||
limit?: number,
|
||||
) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const integration = selectIntegration(state, collectionName, 'search');
|
||||
const collection = state.collections.find(
|
||||
collection => collection.get('name') === collectionName,
|
||||
);
|
||||
|
||||
dispatch(clearRequests());
|
||||
|
||||
const queryIdentifier = `${collectionName}-${searchFields.join()}-${searchTerm}-${file}-${limit}`;
|
||||
|
||||
const queuedQueryPromise = state.search.requests.find(({ id }) => id == queryIdentifier);
|
||||
|
||||
const queryPromise = queuedQueryPromise
|
||||
? queuedQueryPromise.queryResponse
|
||||
: integration
|
||||
? getIntegrationProvider(state.integrations, backend.getToken, integration).searchBy(
|
||||
searchFields.map(f => `data.${f}`),
|
||||
collectionName,
|
||||
searchTerm,
|
||||
)
|
||||
: backend.query(collection, searchFields, searchTerm, file, limit);
|
||||
|
||||
dispatch(
|
||||
querying(
|
||||
searchTerm,
|
||||
queuedQueryPromise
|
||||
? undefined
|
||||
: {
|
||||
id: queryIdentifier,
|
||||
expires: new Date(new Date().getTime() + 10 * 1000),
|
||||
queryResponse: queryPromise,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const response: QueryResponse = await queryPromise;
|
||||
return dispatch(querySuccess(namespace, response.hits));
|
||||
} catch (error) {
|
||||
return dispatch(queryFailure(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SearchAction = ReturnType<
|
||||
| typeof searchingEntries
|
||||
| typeof searchSuccess
|
||||
| typeof searchFailure
|
||||
| typeof querying
|
||||
| typeof querySuccess
|
||||
| typeof queryFailure
|
||||
| typeof clearSearch
|
||||
| typeof clearRequests
|
||||
>;
|
||||
99
node_modules/decap-cms-core/src/actions/status.ts
generated
vendored
Normal file
99
node_modules/decap-cms-core/src/actions/status.ts
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
import { currentBackend } from '../backend';
|
||||
import { addNotification, dismissNotification } from './notifications';
|
||||
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { State } from '../types/redux';
|
||||
|
||||
export const STATUS_REQUEST = 'STATUS_REQUEST';
|
||||
export const STATUS_SUCCESS = 'STATUS_SUCCESS';
|
||||
export const STATUS_FAILURE = 'STATUS_FAILURE';
|
||||
|
||||
export function statusRequest() {
|
||||
return {
|
||||
type: STATUS_REQUEST,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function statusSuccess(status: {
|
||||
auth: { status: boolean };
|
||||
api: { status: boolean; statusPage: string };
|
||||
}) {
|
||||
return {
|
||||
type: STATUS_SUCCESS,
|
||||
payload: { status },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function statusFailure(error: Error) {
|
||||
return {
|
||||
type: STATUS_FAILURE,
|
||||
payload: { error },
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function checkBackendStatus() {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
try {
|
||||
const state = getState();
|
||||
if (state.status.isFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(statusRequest());
|
||||
const backend = currentBackend(state.config);
|
||||
const status = await backend.status();
|
||||
|
||||
const backendDownKey = 'ui.toast.onBackendDown';
|
||||
const previousBackendDownNotifications = state.notifications.notifications.filter(
|
||||
n => typeof n.message != 'string' && n.message?.key === backendDownKey,
|
||||
);
|
||||
|
||||
if (status.api.status === false) {
|
||||
if (previousBackendDownNotifications.length === 0) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
details: status.api.statusPage,
|
||||
key: 'ui.toast.onBackendDown',
|
||||
},
|
||||
type: 'error',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return dispatch(statusSuccess(status));
|
||||
} else if (status.api.status === true && previousBackendDownNotifications.length > 0) {
|
||||
// If backend is up, clear all the danger messages
|
||||
previousBackendDownNotifications.forEach(notification => {
|
||||
dispatch(dismissNotification(notification.id));
|
||||
});
|
||||
}
|
||||
|
||||
const authError = status.auth.status === false;
|
||||
if (authError) {
|
||||
const key = 'ui.toast.onLoggedOut';
|
||||
const existingNotification = state.notifications.notifications.find(
|
||||
n => typeof n.message != 'string' && n.message?.key === key,
|
||||
);
|
||||
if (!existingNotification) {
|
||||
dispatch(
|
||||
addNotification({
|
||||
message: {
|
||||
key: 'ui.toast.onLoggedOut',
|
||||
},
|
||||
type: 'error',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(statusSuccess(status));
|
||||
} catch (error) {
|
||||
dispatch(statusFailure(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type StatusAction = ReturnType<
|
||||
typeof statusRequest | typeof statusSuccess | typeof statusFailure
|
||||
>;
|
||||
49
node_modules/decap-cms-core/src/actions/waitUntil.ts
generated
vendored
Normal file
49
node_modules/decap-cms-core/src/actions/waitUntil.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { WAIT_UNTIL_ACTION } from '../redux/middleware/waitUntilAction';
|
||||
|
||||
import type { WaitActionArgs } from '../redux/middleware/waitUntilAction';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { State } from '../types/redux';
|
||||
|
||||
export function waitUntil({ predicate, run }: WaitActionArgs) {
|
||||
return {
|
||||
type: WAIT_UNTIL_ACTION,
|
||||
predicate,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
export async function waitUntilWithTimeout<T>(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
waitActionArgs: (resolve: (value?: T) => void) => WaitActionArgs,
|
||||
timeout = 30000,
|
||||
): Promise<T | null | void> {
|
||||
let waitDone = false;
|
||||
|
||||
const waitPromise = new Promise<T>(resolve => {
|
||||
dispatch(waitUntil(waitActionArgs(resolve as (value?: T | undefined) => void)));
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise<T | null | void>(resolve => {
|
||||
setTimeout(() => {
|
||||
if (waitDone) {
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('Wait Action timed out');
|
||||
resolve(null);
|
||||
}
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
const result = await Promise.race([
|
||||
waitPromise
|
||||
.then(result => {
|
||||
waitDone = true;
|
||||
return result;
|
||||
})
|
||||
.catch(null),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user