This commit is contained in:
38
node_modules/decap-cms-core/src/reducers/__tests__/auth.spec.ts
generated
vendored
Normal file
38
node_modules/decap-cms-core/src/reducers/__tests__/auth.spec.ts
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { authenticating, authenticate, authError, logout } from '../../actions/auth';
|
||||
import auth, { defaultState } from '../auth';
|
||||
|
||||
describe('auth', () => {
|
||||
it('should handle an empty state', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore auth reducer doesn't accept empty action
|
||||
expect(auth(undefined, {})).toEqual(defaultState);
|
||||
});
|
||||
|
||||
it('should handle an authentication request', () => {
|
||||
expect(auth(undefined, authenticating())).toEqual({
|
||||
...defaultState,
|
||||
isFetching: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle authentication', () => {
|
||||
const user = { name: 'joe', token: 'token' };
|
||||
expect(auth(undefined, authenticate(user))).toEqual({
|
||||
...defaultState,
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an authentication error', () => {
|
||||
expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual({
|
||||
...defaultState,
|
||||
error: 'Error: Bad credentials',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle logout', () => {
|
||||
const user = { name: 'joe', token: 'token' };
|
||||
const newState = auth({ ...defaultState, user }, logout());
|
||||
expect(newState.user).toBeUndefined();
|
||||
});
|
||||
});
|
||||
571
node_modules/decap-cms-core/src/reducers/__tests__/collections.spec.js
generated
vendored
Normal file
571
node_modules/decap-cms-core/src/reducers/__tests__/collections.spec.js
generated
vendored
Normal file
@@ -0,0 +1,571 @@
|
||||
import { fromJS, Map } from 'immutable';
|
||||
|
||||
import { configLoaded } from '../../actions/config';
|
||||
import collections, {
|
||||
selectAllowDeletion,
|
||||
selectEntryPath,
|
||||
selectEntrySlug,
|
||||
selectFieldsWithMediaFolders,
|
||||
selectMediaFolders,
|
||||
selectEntryCollectionTitle,
|
||||
getFieldsNames,
|
||||
selectField,
|
||||
updateFieldByKey,
|
||||
} from '../collections';
|
||||
import { FILES, FOLDER } from '../../constants/collectionTypes';
|
||||
|
||||
describe('collections', () => {
|
||||
it('should handle an empty state', () => {
|
||||
expect(collections(undefined, {})).toEqual(Map());
|
||||
});
|
||||
|
||||
it('should load the collections from the config', () => {
|
||||
expect(
|
||||
collections(
|
||||
undefined,
|
||||
configLoaded({
|
||||
collections: [
|
||||
{
|
||||
name: 'posts',
|
||||
folder: '_posts',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toJS(),
|
||||
).toEqual({
|
||||
posts: {
|
||||
name: 'posts',
|
||||
folder: '_posts',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should maintain config collections order', () => {
|
||||
const collectionsData = new Array(1000).fill(0).map((_, index) => ({
|
||||
name: `collection_${index}`,
|
||||
folder: `collection_${index}`,
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
}));
|
||||
|
||||
const newState = collections(
|
||||
undefined,
|
||||
configLoaded({
|
||||
collections: collectionsData,
|
||||
}),
|
||||
);
|
||||
const keyArray = newState.keySeq().toArray();
|
||||
expect(keyArray).toEqual(collectionsData.map(({ name }) => name));
|
||||
});
|
||||
|
||||
describe('selectAllowDeletions', () => {
|
||||
it('should not allow deletions for file collections', () => {
|
||||
expect(
|
||||
selectAllowDeletion(
|
||||
fromJS({
|
||||
name: 'pages',
|
||||
type: FILES,
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEntryPath', () => {
|
||||
it('should return path', () => {
|
||||
expect(
|
||||
selectEntryPath(
|
||||
fromJS({
|
||||
type: FOLDER,
|
||||
folder: 'posts',
|
||||
}),
|
||||
'dir1/dir2/slug',
|
||||
),
|
||||
).toBe('posts/dir1/dir2/slug.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEntrySlug', () => {
|
||||
it('should return slug', () => {
|
||||
expect(
|
||||
selectEntrySlug(
|
||||
fromJS({
|
||||
type: FOLDER,
|
||||
folder: 'posts',
|
||||
}),
|
||||
'posts/dir1/dir2/slug.md',
|
||||
),
|
||||
).toBe('dir1/dir2/slug');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectFieldsMediaFolders', () => {
|
||||
it('should return empty array for invalid collection', () => {
|
||||
expect(selectFieldsWithMediaFolders(fromJS({}))).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return configs for folder collection', () => {
|
||||
expect(
|
||||
selectFieldsWithMediaFolders(
|
||||
fromJS({
|
||||
folder: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
media_folder: 'image_media_folder',
|
||||
},
|
||||
{
|
||||
name: 'body',
|
||||
media_folder: 'body_media_folder',
|
||||
},
|
||||
{
|
||||
name: 'list_1',
|
||||
field: {
|
||||
name: 'list_1_item',
|
||||
media_folder: 'list_1_item_media_folder',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_2',
|
||||
fields: [
|
||||
{
|
||||
name: 'list_2_item',
|
||||
media_folder: 'list_2_item_media_folder',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list_3',
|
||||
types: [
|
||||
{
|
||||
name: 'list_3_type',
|
||||
media_folder: 'list_3_type_media_folder',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
fromJS({
|
||||
name: 'image',
|
||||
media_folder: 'image_media_folder',
|
||||
}),
|
||||
fromJS({ name: 'body', media_folder: 'body_media_folder' }),
|
||||
fromJS({ name: 'list_1_item', media_folder: 'list_1_item_media_folder' }),
|
||||
fromJS({
|
||||
name: 'list_2_item',
|
||||
media_folder: 'list_2_item_media_folder',
|
||||
}),
|
||||
fromJS({
|
||||
name: 'list_3_type',
|
||||
media_folder: 'list_3_type_media_folder',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return configs for files collection', () => {
|
||||
expect(
|
||||
selectFieldsWithMediaFolders(
|
||||
fromJS({
|
||||
files: [
|
||||
{
|
||||
name: 'file1',
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
media_folder: 'image_media_folder',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'file2',
|
||||
fields: [
|
||||
{
|
||||
name: 'body',
|
||||
media_folder: 'body_media_folder',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'file3',
|
||||
fields: [
|
||||
{
|
||||
name: 'list_1',
|
||||
field: {
|
||||
name: 'list_1_item',
|
||||
media_folder: 'list_1_item_media_folder',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'file4',
|
||||
fields: [
|
||||
{
|
||||
name: 'list_2',
|
||||
fields: [
|
||||
{
|
||||
name: 'list_2_item',
|
||||
media_folder: 'list_2_item_media_folder',
|
||||
},
|
||||
{
|
||||
name: 'list_3',
|
||||
types: [
|
||||
{
|
||||
name: 'list_3_type',
|
||||
media_folder: 'list_3_type_media_folder',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
'file4',
|
||||
),
|
||||
).toEqual([
|
||||
fromJS({
|
||||
name: 'list_2_item',
|
||||
media_folder: 'list_2_item_media_folder',
|
||||
}),
|
||||
fromJS({
|
||||
name: 'list_3_type',
|
||||
media_folder: 'list_3_type_media_folder',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMediaFolders', () => {
|
||||
const slug = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const config = { slug, media_folder: '/static/img' };
|
||||
it('should return fields and collection folders', () => {
|
||||
expect(
|
||||
selectMediaFolders(
|
||||
config,
|
||||
fromJS({
|
||||
folder: 'posts',
|
||||
media_folder: '{{media_folder}}/general/',
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
media_folder: '{{media_folder}}/customers/',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
types: [{ name: 'widget', media_folder: '{{media_folder}}/widgets' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }),
|
||||
),
|
||||
).toEqual([
|
||||
'static/img/general',
|
||||
'static/img/general/customers',
|
||||
'static/img/general/widgets',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fields, file and collection folders', () => {
|
||||
expect(
|
||||
selectMediaFolders(
|
||||
config,
|
||||
fromJS({
|
||||
media_folder: '{{media_folder}}/general/',
|
||||
files: [
|
||||
{
|
||||
name: 'name',
|
||||
file: 'src/post/post1.md',
|
||||
media_folder: '{{media_folder}}/customers/',
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
media_folder: '{{media_folder}}/logos/',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
types: [{ name: 'widget', media_folder: '{{media_folder}}/widgets' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }),
|
||||
),
|
||||
).toEqual([
|
||||
'static/img/general',
|
||||
'static/img/general/customers',
|
||||
'static/img/general/customers/logos',
|
||||
'static/img/general/customers/widgets',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldsNames', () => {
|
||||
it('should get flat fields names', () => {
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'en' }, { name: 'es' }],
|
||||
});
|
||||
expect(getFieldsNames(collection.get('fields').toArray())).toEqual(['en', 'es']);
|
||||
});
|
||||
|
||||
it('should get nested fields names', () => {
|
||||
const collection = fromJS({
|
||||
fields: [
|
||||
{ name: 'en', fields: [{ name: 'title' }, { name: 'body' }] },
|
||||
{ name: 'es', fields: [{ name: 'title' }, { name: 'body' }] },
|
||||
{ name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } },
|
||||
{
|
||||
name: 'fr',
|
||||
fields: [{ name: 'title', widget: 'list', types: [{ name: 'variableType' }] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getFieldsNames(collection.get('fields').toArray())).toEqual([
|
||||
'en',
|
||||
'es',
|
||||
'it',
|
||||
'fr',
|
||||
'en.title',
|
||||
'en.body',
|
||||
'es.title',
|
||||
'es.body',
|
||||
'it.title',
|
||||
'it.title.subTitle',
|
||||
'fr.title',
|
||||
'fr.title.variableType',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectField', () => {
|
||||
it('should return top field by key', () => {
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'en' }, { name: 'es' }],
|
||||
});
|
||||
expect(selectField(collection, 'en')).toBe(collection.get('fields').get(0));
|
||||
});
|
||||
|
||||
it('should return nested field by key', () => {
|
||||
const collection = fromJS({
|
||||
fields: [
|
||||
{ name: 'en', fields: [{ name: 'title' }, { name: 'body' }] },
|
||||
{ name: 'es', fields: [{ name: 'title' }, { name: 'body' }] },
|
||||
{ name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } },
|
||||
{
|
||||
name: 'fr',
|
||||
fields: [{ name: 'title', widget: 'list', types: [{ name: 'variableType' }] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(selectField(collection, 'en.title')).toBe(
|
||||
collection.get('fields').get(0).get('fields').get(0),
|
||||
);
|
||||
|
||||
expect(selectField(collection, 'it.title.subTitle')).toBe(
|
||||
collection.get('fields').get(2).get('field').get('fields').get(0),
|
||||
);
|
||||
|
||||
expect(selectField(collection, 'fr.title.variableType')).toBe(
|
||||
collection.get('fields').get(3).get('fields').get(0).get('types').get(0),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEntryCollectionTitle', () => {
|
||||
const entry = fromJS({
|
||||
data: { title: 'entry title', otherField: 'other field', emptyLinkTitle: '' },
|
||||
});
|
||||
|
||||
it('should return the entry title if set', () => {
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'title' }, { name: 'otherField' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
|
||||
});
|
||||
|
||||
it('should return some other inferreable title if set', () => {
|
||||
const headlineEntry = fromJS({
|
||||
data: { headline: 'entry headline', otherField: 'other field' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
fields: [{ name: 'headline' }, { name: 'otherField' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, headlineEntry)).toEqual('entry headline');
|
||||
});
|
||||
|
||||
it('should return the identifier_field content if defined in collection', () => {
|
||||
const collection = fromJS({
|
||||
identifier_field: 'otherField',
|
||||
fields: [{ name: 'title' }, { name: 'otherField' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, entry)).toEqual('other field');
|
||||
});
|
||||
|
||||
it('should return the entry title if identifier_field content is not defined in collection', () => {
|
||||
const collection = fromJS({
|
||||
identifier_field: 'missingLinkTitle',
|
||||
fields: [{ name: 'title' }, { name: 'otherField' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
|
||||
});
|
||||
|
||||
it('should return the entry title if identifier_field content is empty', () => {
|
||||
const collection = fromJS({
|
||||
identifier_field: 'emptyLinkTitle',
|
||||
fields: [{ name: 'title' }, { name: 'otherField' }, { name: 'emptyLinkTitle' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title');
|
||||
});
|
||||
|
||||
it('should return the entry label of a file collection', () => {
|
||||
const labelEntry = fromJS({
|
||||
slug: 'entry-name',
|
||||
data: { title: 'entry title', otherField: 'other field' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
type: FILES,
|
||||
files: [
|
||||
{
|
||||
name: 'entry-name',
|
||||
label: 'entry label',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, labelEntry)).toEqual('entry label');
|
||||
});
|
||||
|
||||
it('should return a formatted summary before everything else', () => {
|
||||
const collection = fromJS({
|
||||
summary: '{{title}} -- {{otherField}}',
|
||||
identifier_field: 'otherField',
|
||||
fields: [{ name: 'title' }, { name: 'otherField' }],
|
||||
});
|
||||
|
||||
expect(selectEntryCollectionTitle(collection, entry)).toEqual('entry title -- other field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFieldByKey', () => {
|
||||
it('should update field by key', () => {
|
||||
const collection = fromJS({
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget' }] },
|
||||
],
|
||||
});
|
||||
|
||||
function updater(field) {
|
||||
return field.set('default', 'default');
|
||||
}
|
||||
|
||||
expect(updateFieldByKey(collection, 'non-existent', updater)).toBe(collection);
|
||||
expect(updateFieldByKey(collection, 'title', updater)).toEqual(
|
||||
fromJS({
|
||||
fields: [
|
||||
{ name: 'title', default: 'default' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget' }] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(updateFieldByKey(collection, 'object.title', updater)).toEqual(
|
||||
fromJS({
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [
|
||||
{ name: 'title', default: 'default' },
|
||||
{ name: 'gallery', fields: [{ name: 'image' }] },
|
||||
],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget' }] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updateFieldByKey(collection, 'object.gallery.image', updater)).toEqual(
|
||||
fromJS({
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'gallery', fields: [{ name: 'image', default: 'default' }] },
|
||||
],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget' }] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(updateFieldByKey(collection, 'list.image', updater)).toEqual(
|
||||
fromJS({
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image', default: 'default' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget' }] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(updateFieldByKey(collection, 'widgetList.widget', updater)).toEqual(
|
||||
fromJS({
|
||||
fields: [
|
||||
{ name: 'title' },
|
||||
{ name: 'image' },
|
||||
{
|
||||
name: 'object',
|
||||
fields: [{ name: 'title' }, { name: 'gallery', fields: [{ name: 'image' }] }],
|
||||
},
|
||||
{ name: 'list', field: { name: 'image' } },
|
||||
{ name: 'body' },
|
||||
{ name: 'widgetList', types: [{ name: 'widget', default: 'default' }] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
38
node_modules/decap-cms-core/src/reducers/__tests__/config.spec.js
generated
vendored
Normal file
38
node_modules/decap-cms-core/src/reducers/__tests__/config.spec.js
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { configLoaded, configLoading, configFailed } from '../../actions/config';
|
||||
import config, { selectLocale } from '../config';
|
||||
|
||||
describe('config', () => {
|
||||
it('should handle an empty state', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore config reducer doesn't accept empty action
|
||||
expect(config(undefined, {})).toEqual({ isFetching: true });
|
||||
});
|
||||
|
||||
it('should handle an update', () => {
|
||||
expect(
|
||||
config({ isFetching: true }, configLoaded({ locale: 'fr', backend: { name: 'proxy' } })),
|
||||
).toEqual({
|
||||
locale: 'fr',
|
||||
backend: { name: 'proxy' },
|
||||
isFetching: false,
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark the config as loading', () => {
|
||||
expect(config({ isFetching: false }, configLoading())).toEqual({ isFetching: true });
|
||||
});
|
||||
|
||||
it('should handle an error', () => {
|
||||
expect(
|
||||
config({ isFetching: true }, configFailed(new Error('Config could not be loaded'))),
|
||||
).toEqual({
|
||||
error: 'Error: Config could not be loaded',
|
||||
isFetching: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should default to "en" locale', () => {
|
||||
expect(selectLocale({})).toEqual('en');
|
||||
});
|
||||
});
|
||||
694
node_modules/decap-cms-core/src/reducers/__tests__/entries.spec.js
generated
vendored
Normal file
694
node_modules/decap-cms-core/src/reducers/__tests__/entries.spec.js
generated
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
import { OrderedMap, fromJS } from 'immutable';
|
||||
|
||||
import * as actions from '../../actions/entries';
|
||||
import reducer, {
|
||||
selectMediaFolder,
|
||||
selectMediaFilePath,
|
||||
selectMediaFilePublicPath,
|
||||
selectEntries,
|
||||
} from '../entries';
|
||||
|
||||
const initialState = OrderedMap({
|
||||
posts: fromJS({ name: 'posts' }),
|
||||
});
|
||||
|
||||
describe('entries', () => {
|
||||
describe('reducer', () => {
|
||||
it('should mark entries as fetching', () => {
|
||||
expect(reducer(initialState, actions.entriesLoading(fromJS({ name: 'posts' })))).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
pages: {
|
||||
posts: { isFetching: true },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle loaded entries', () => {
|
||||
const entries = [
|
||||
{ slug: 'a', path: '' },
|
||||
{ slug: 'b', title: 'B' },
|
||||
];
|
||||
expect(
|
||||
reducer(initialState, actions.entriesLoaded(fromJS({ name: 'posts' }), entries, 0)),
|
||||
).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
entities: {
|
||||
'posts.a': { slug: 'a', path: '', isFetching: false },
|
||||
'posts.b': { slug: 'b', title: 'B', isFetching: false },
|
||||
},
|
||||
pages: {
|
||||
posts: {
|
||||
page: 0,
|
||||
ids: ['a', 'b'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle loaded entry', () => {
|
||||
const entry = { slug: 'a', path: '' };
|
||||
expect(reducer(initialState, actions.entryLoaded(fromJS({ name: 'posts' }), entry))).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
entities: {
|
||||
'posts.a': { slug: 'a', path: '' },
|
||||
},
|
||||
pages: {
|
||||
posts: {
|
||||
ids: ['a'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMediaFolder', () => {
|
||||
it("should return global media folder when collection doesn't specify media_folder", () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts' }),
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('static/media');
|
||||
});
|
||||
|
||||
it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('posts/DRAFT_MEDIA_FILES');
|
||||
});
|
||||
|
||||
it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
fromJS({ path: 'posts/title/index.md' }),
|
||||
undefined,
|
||||
),
|
||||
).toEqual('posts/title');
|
||||
});
|
||||
|
||||
it('should resolve collection relative media folder', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
fromJS({ media_folder: 'static/media' }),
|
||||
fromJS({ name: 'posts', folder: 'posts', media_folder: '../' }),
|
||||
fromJS({ path: 'posts/title/index.md' }),
|
||||
undefined,
|
||||
),
|
||||
).toEqual('posts');
|
||||
});
|
||||
|
||||
it('should resolve field relative media folder', () => {
|
||||
const field = fromJS({ media_folder: '' });
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: '/static/img' },
|
||||
fromJS({
|
||||
name: 'other',
|
||||
folder: 'other',
|
||||
fields: [field],
|
||||
media_folder: '../',
|
||||
}),
|
||||
fromJS({ path: 'src/other/other.md', data: {} }),
|
||||
field,
|
||||
),
|
||||
).toEqual('src/other');
|
||||
});
|
||||
|
||||
it('should return collection absolute media folder without leading slash', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: '/static/Images' },
|
||||
fromJS({
|
||||
name: 'getting-started',
|
||||
folder: 'src/docs/getting-started',
|
||||
media_folder: '/static/images/docs/getting-started',
|
||||
}),
|
||||
fromJS({}),
|
||||
undefined,
|
||||
),
|
||||
).toEqual('static/images/docs/getting-started');
|
||||
});
|
||||
|
||||
it('should compile relative media folder template', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
media_folder: '../../../{{media_folder}}/{{category}}/{{slug}}',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media', slug: slugConfig },
|
||||
collection,
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
|
||||
});
|
||||
|
||||
it('should compile absolute media folder template', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
data: { title: 'Overview' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'extending',
|
||||
folder: 'src/docs/extending',
|
||||
media_folder: '{{media_folder}}/docs/extending',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: '/static/images', slug: slugConfig },
|
||||
collection,
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('static/images/docs/extending');
|
||||
});
|
||||
|
||||
it('should compile field media folder template', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
media_folder: '../../../{{media_folder}}/{{category}}/{{slug}}',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media', slug: slugConfig },
|
||||
collection,
|
||||
entry,
|
||||
collection.get('fields').get(0),
|
||||
),
|
||||
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
|
||||
});
|
||||
|
||||
it('should handle double slashes', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
media_folder: '{{media_folder}}/blog',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: '/static/img/', slug: slugConfig },
|
||||
collection,
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('static/img/blog');
|
||||
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/img/', slug: slugConfig },
|
||||
collection,
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('content/en/hosting-and-deployment/static/img/blog');
|
||||
});
|
||||
|
||||
it('should handle file media_folder', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }),
|
||||
fromJS({ path: 'posts/title/index.md', slug: 'index' }),
|
||||
undefined,
|
||||
),
|
||||
).toBe('static/images');
|
||||
});
|
||||
|
||||
it('should cascade media_folders', () => {
|
||||
const mainImageField = fromJS({ name: 'main_image' });
|
||||
const logoField = fromJS({ name: 'logo', media_folder: '{{media_folder}}/logos/' });
|
||||
const nestedField3 = fromJS({ name: 'nested', media_folder: '{{media_folder}}/nested3/' });
|
||||
const nestedField2 = fromJS({
|
||||
name: 'nested',
|
||||
media_folder: '{{media_folder}}/nested2/',
|
||||
types: [nestedField3],
|
||||
});
|
||||
const nestedField1 = fromJS({
|
||||
name: 'nested',
|
||||
media_folder: '{{media_folder}}/nested1/',
|
||||
fields: [nestedField2],
|
||||
});
|
||||
|
||||
const args = [
|
||||
{ media_folder: '/static/img' },
|
||||
fromJS({
|
||||
name: 'general',
|
||||
media_folder: '{{media_folder}}/general/',
|
||||
files: [
|
||||
{
|
||||
name: 'customers',
|
||||
media_folder: '{{media_folder}}/customers/',
|
||||
fields: [
|
||||
mainImageField,
|
||||
logoField,
|
||||
{ media_folder: '{{media_folder}}/nested', field: nestedField1 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
fromJS({ path: 'src/customers/customers.md', slug: 'customers', data: { title: 'title' } }),
|
||||
];
|
||||
|
||||
expect(selectMediaFolder(...args, mainImageField)).toBe('static/img/general/customers');
|
||||
expect(selectMediaFolder(...args, logoField)).toBe('static/img/general/customers/logos');
|
||||
expect(selectMediaFolder(...args, nestedField1)).toBe(
|
||||
'static/img/general/customers/nested/nested1',
|
||||
);
|
||||
expect(selectMediaFolder(...args, nestedField2)).toBe(
|
||||
'static/img/general/customers/nested/nested1/nested2',
|
||||
);
|
||||
expect(selectMediaFolder(...args, nestedField3)).toBe(
|
||||
'static/img/general/customers/nested/nested1/nested2/nested3',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMediaFilePath', () => {
|
||||
it('should return absolute URL as is', () => {
|
||||
expect(selectMediaFilePath(null, null, null, 'https://www.netlify.com/image.png')).toBe(
|
||||
'https://www.netlify.com/image.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve path from global media folder for collection with no media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts' }),
|
||||
undefined,
|
||||
'image.png',
|
||||
undefined,
|
||||
),
|
||||
).toBe('static/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from collection media folder for collection with media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
undefined,
|
||||
'image.png',
|
||||
undefined,
|
||||
),
|
||||
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
|
||||
});
|
||||
|
||||
it('should handle relative media_folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
|
||||
fromJS({ path: 'posts/title/index.md' }),
|
||||
'image.png',
|
||||
undefined,
|
||||
),
|
||||
).toBe('static/media/image.png');
|
||||
});
|
||||
|
||||
it('should handle field media_folder', () => {
|
||||
const field = fromJS({ media_folder: '../../static/media/' });
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
{ media_folder: 'static/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', fields: [field] }),
|
||||
fromJS({ path: 'posts/title/index.md' }),
|
||||
'image.png',
|
||||
field,
|
||||
),
|
||||
).toBe('static/media/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMediaFilePublicPath', () => {
|
||||
it('should return absolute URL as is', () => {
|
||||
expect(selectMediaFilePublicPath(null, null, 'https://www.netlify.com/image.png')).toBe(
|
||||
'https://www.netlify.com/image.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve path from public folder for collection with no media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: '/media' },
|
||||
null,
|
||||
'/media/image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from collection public folder for collection with public folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: '/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', public_folder: '' }),
|
||||
'image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('image.png');
|
||||
});
|
||||
|
||||
it('should handle relative public_folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: '/media' },
|
||||
fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
|
||||
'image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('../../static/media/image.png');
|
||||
});
|
||||
|
||||
it('should handle absolute public_folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: 'https://www.netlify.com/media' },
|
||||
fromJS({
|
||||
name: 'posts',
|
||||
folder: 'posts',
|
||||
public_folder: 'https://www.netlify.com/media',
|
||||
}),
|
||||
'image.png',
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
).toBe('https://www.netlify.com/media/image.png');
|
||||
});
|
||||
|
||||
it('should compile collection public folder template', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: 'static/media', slug: slugConfig },
|
||||
collection,
|
||||
'image.png',
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
|
||||
});
|
||||
|
||||
it('should compile field public folder template', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
|
||||
const field = fromJS({
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
fields: [field],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: 'static/media', slug: slugConfig },
|
||||
collection,
|
||||
'image.png',
|
||||
entry,
|
||||
field,
|
||||
),
|
||||
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
|
||||
});
|
||||
|
||||
it('should handle double slashes', () => {
|
||||
const slugConfig = {
|
||||
encoding: 'unicode',
|
||||
clean_accents: false,
|
||||
sanitize_replacement: '-',
|
||||
};
|
||||
|
||||
const entry = fromJS({
|
||||
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
|
||||
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
|
||||
});
|
||||
|
||||
const field = fromJS({
|
||||
name: 'title',
|
||||
widget: 'string',
|
||||
public_folder: '/{{public_folder}}/{{category}}/{{slug}}',
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
folder: 'content',
|
||||
fields: [field],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: 'static/media/', slug: slugConfig },
|
||||
collection,
|
||||
'image.png',
|
||||
entry,
|
||||
field,
|
||||
),
|
||||
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
|
||||
});
|
||||
|
||||
it('should handle file public_folder', () => {
|
||||
const entry = fromJS({
|
||||
path: 'src/posts/index.md',
|
||||
slug: 'index',
|
||||
});
|
||||
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
files: [
|
||||
{
|
||||
name: 'index',
|
||||
public_folder: '/images',
|
||||
fields: [{ name: 'title', widget: 'string' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
{ public_folder: 'static/media/' },
|
||||
collection,
|
||||
'image.png',
|
||||
entry,
|
||||
undefined,
|
||||
),
|
||||
).toBe('/images/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEntries', () => {
|
||||
it('should return all entries', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1' },
|
||||
'posts.2': { slug: '2' },
|
||||
'posts.3': { slug: '3' },
|
||||
'posts.4': { slug: '4' },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([{ slug: '1' }, { slug: '2' }, { slug: '3' }, { slug: '4' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return sorted entries entries by field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1' } },
|
||||
'posts.2': { slug: '2', data: { title: '2' } },
|
||||
'posts.3': { slug: '3', data: { title: '3' } },
|
||||
'posts.4': { slug: '4', data: { title: '4' } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
sort: { posts: { title: { key: 'title', direction: 'Descending' } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '4', data: { title: '4' } },
|
||||
{ slug: '3', data: { title: '3' } },
|
||||
{ slug: '2', data: { title: '2' } },
|
||||
{ slug: '1', data: { title: '1' } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sorted entries entries by nested field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1', nested: { date: 4 } } },
|
||||
'posts.2': { slug: '2', data: { title: '2', nested: { date: 3 } } },
|
||||
'posts.3': { slug: '3', data: { title: '3', nested: { date: 2 } } },
|
||||
'posts.4': { slug: '4', data: { title: '4', nested: { date: 1 } } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
sort: { posts: { title: { key: 'nested.date', direction: 'Ascending' } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '4', data: { title: '4', nested: { date: 1 } } },
|
||||
{ slug: '3', data: { title: '3', nested: { date: 2 } } },
|
||||
{ slug: '2', data: { title: '2', nested: { date: 3 } } },
|
||||
{ slug: '1', data: { title: '1', nested: { date: 4 } } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return filtered entries entries by field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1' } },
|
||||
'posts.2': { slug: '2', data: { title: '2' } },
|
||||
'posts.3': { slug: '3', data: { title: '3' } },
|
||||
'posts.4': { slug: '4', data: { title: '4' } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
filter: { posts: { title__1: { field: 'title', pattern: '4', active: true } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(fromJS([{ slug: '4', data: { title: '4' } }]));
|
||||
});
|
||||
|
||||
it('should return filtered entries entries by nested field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1', nested: { draft: true } } },
|
||||
'posts.2': { slug: '2', data: { title: '2', nested: { draft: true } } },
|
||||
'posts.3': { slug: '3', data: { title: '3', nested: { draft: false } } },
|
||||
'posts.4': { slug: '4', data: { title: '4', nested: { draft: false } } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
filter: {
|
||||
posts: { 'nested.draft__false': { field: 'nested.draft', pattern: false, active: true } },
|
||||
},
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '3', data: { title: '3', nested: { draft: false } } },
|
||||
{ slug: '4', data: { title: '4', nested: { draft: false } } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
198
node_modules/decap-cms-core/src/reducers/__tests__/entryDraft.spec.js
generated
vendored
Normal file
198
node_modules/decap-cms-core/src/reducers/__tests__/entryDraft.spec.js
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
|
||||
import * as actions from '../../actions/entries';
|
||||
import reducer from '../entryDraft';
|
||||
|
||||
jest.mock('uuid', () => ({ v4: jest.fn(() => '1') }));
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
|
||||
const entry = {
|
||||
collection: 'posts',
|
||||
slug: 'slug',
|
||||
path: 'content/blog/art-and-wine-festival.md',
|
||||
partial: false,
|
||||
raw: '',
|
||||
data: {},
|
||||
metaData: null,
|
||||
};
|
||||
|
||||
describe('entryDraft reducer', () => {
|
||||
describe('DRAFT_CREATE_FROM_ENTRY', () => {
|
||||
it('should create draft from the entry', () => {
|
||||
const state = reducer(initialState, actions.createDraftFromEntry(fromJS(entry)));
|
||||
expect(state).toEqual(
|
||||
fromJS({
|
||||
entry: {
|
||||
...entry,
|
||||
newRecord: false,
|
||||
},
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_CREATE_EMPTY', () => {
|
||||
it('should create a new draft ', () => {
|
||||
const state = reducer(initialState, actions.emptyDraftCreated(fromJS(entry)));
|
||||
expect(state).toEqual(
|
||||
fromJS({
|
||||
entry: {
|
||||
...entry,
|
||||
newRecord: true,
|
||||
},
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_DISCARD', () => {
|
||||
it('should discard the draft and return initial state', () => {
|
||||
expect(reducer(initialState, actions.discardDraft())).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persisting', () => {
|
||||
let initialState;
|
||||
|
||||
beforeEach(() => {
|
||||
initialState = fromJS({
|
||||
entities: {
|
||||
'posts.slug': {
|
||||
collection: 'posts',
|
||||
slug: 'slug',
|
||||
path: 'content/blog/art-and-wine-festival.md',
|
||||
partial: false,
|
||||
raw: '',
|
||||
data: {},
|
||||
metaData: null,
|
||||
},
|
||||
},
|
||||
pages: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle persisting request', () => {
|
||||
const newState = reducer(
|
||||
initialState,
|
||||
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
|
||||
);
|
||||
expect(newState.getIn(['entry', 'isPersisting'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle persisting success', () => {
|
||||
let newState = reducer(
|
||||
initialState,
|
||||
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
|
||||
);
|
||||
newState = reducer(
|
||||
newState,
|
||||
actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })),
|
||||
);
|
||||
expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle persisting error', () => {
|
||||
let newState = reducer(
|
||||
initialState,
|
||||
actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })),
|
||||
);
|
||||
newState = reducer(
|
||||
newState,
|
||||
actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message'),
|
||||
);
|
||||
expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('REMOVE_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should remove a media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.setIn(['entry', 'mediaFiles'], fromJS([{ id: '1' }, { id: '2' }])),
|
||||
actions.removeDraftEntryMediaFile({ id: '1' }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: { mediaFiles: [{ id: '2' }] },
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADD_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should overwrite an existing media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.setIn(['entry', 'mediaFiles'], fromJS([{ id: '1', name: 'old' }])),
|
||||
actions.addDraftEntryMediaFile({ id: '1', name: 'new' }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: { mediaFiles: [{ id: '1', name: 'new' }] },
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_CREATE_FROM_LOCAL_BACKUP', () => {
|
||||
it('should create draft from local backup', () => {
|
||||
const localBackup = Map({ entry: fromJS({ ...entry, mediaFiles: [{ id: '1' }] }) });
|
||||
|
||||
const actualState = reducer(initialState.set('localBackup', localBackup), {
|
||||
type: actions.DRAFT_CREATE_FROM_LOCAL_BACKUP,
|
||||
});
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {
|
||||
...entry,
|
||||
mediaFiles: [{ id: '1' }],
|
||||
newRecord: false,
|
||||
},
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
key: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_LOCAL_BACKUP_RETRIEVED', () => {
|
||||
it('should set local backup', () => {
|
||||
const mediaFiles = [{ id: '1' }];
|
||||
|
||||
const actualState = reducer(
|
||||
initialState,
|
||||
actions.localBackupRetrieved({ ...entry, mediaFiles }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
localBackup: {
|
||||
entry: { ...entry, mediaFiles: [{ id: '1' }] },
|
||||
},
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
43
node_modules/decap-cms-core/src/reducers/__tests__/globalUI.js
generated
vendored
Normal file
43
node_modules/decap-cms-core/src/reducers/__tests__/globalUI.js
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { USE_OPEN_AUTHORING } from '../../actions/auth';
|
||||
import {
|
||||
DEPLOY_PREVIEW_REQUEST,
|
||||
DEPLOY_PREVIEW_SUCCESS,
|
||||
DEPLOY_PREVIEW_FAILURE,
|
||||
} from '../../actions/deploys';
|
||||
import { ENTRY_REQUEST, ENTRY_SUCCESS, ENTRY_FAILURE } from '../../actions/entries';
|
||||
import reducer from '../globalUI';
|
||||
|
||||
describe('globalUI', () => {
|
||||
it('should set isFetching to true on entry request', () => {
|
||||
expect(reducer({ isFetching: false }, { type: ENTRY_REQUEST })).toEqual({ isFetching: true });
|
||||
});
|
||||
|
||||
it('should set isFetching to false on entry success', () => {
|
||||
expect(reducer({ isFetching: true }, { type: ENTRY_SUCCESS })).toEqual({ isFetching: false });
|
||||
});
|
||||
|
||||
it('should set isFetching to false on entry failure', () => {
|
||||
expect(reducer({ isFetching: true }, { type: ENTRY_FAILURE })).toEqual({ isFetching: false });
|
||||
});
|
||||
|
||||
it('should not change state on deploy preview request', () => {
|
||||
const state = { isFetching: false };
|
||||
expect(reducer(state, { type: DEPLOY_PREVIEW_REQUEST })).toBe(state);
|
||||
});
|
||||
|
||||
it('should not change state on deploy preview success', () => {
|
||||
const state = { isFetching: true };
|
||||
expect(reducer(state, { type: DEPLOY_PREVIEW_SUCCESS })).toBe(state);
|
||||
});
|
||||
|
||||
it('should not change state on deploy preview failure', () => {
|
||||
const state = { isFetching: true };
|
||||
expect(reducer(state, { type: DEPLOY_PREVIEW_FAILURE })).toBe(state);
|
||||
});
|
||||
|
||||
it('should set useOpenAuthoring to true on USE_OPEN_AUTHORING', () => {
|
||||
expect(reducer({ useOpenAuthoring: false }, { type: USE_OPEN_AUTHORING })).toEqual({
|
||||
useOpenAuthoring: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
76
node_modules/decap-cms-core/src/reducers/__tests__/integrations.spec.ts
generated
vendored
Normal file
76
node_modules/decap-cms-core/src/reducers/__tests__/integrations.spec.ts
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
import integrations from '../integrations';
|
||||
import { CONFIG_SUCCESS } from '../../actions/config';
|
||||
import { FOLDER } from '../../constants/collectionTypes';
|
||||
|
||||
import type { ConfigAction } from '../../actions/config';
|
||||
|
||||
describe('integrations', () => {
|
||||
it('should return default state when no integrations', () => {
|
||||
const result = integrations(null, {
|
||||
type: CONFIG_SUCCESS,
|
||||
payload: { integrations: [] },
|
||||
} as ConfigAction);
|
||||
expect(result && result.toJS()).toEqual({
|
||||
providers: {},
|
||||
hooks: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return hooks and providers map when has integrations', () => {
|
||||
const result = integrations(null, {
|
||||
type: CONFIG_SUCCESS,
|
||||
payload: {
|
||||
integrations: [
|
||||
{
|
||||
hooks: ['listEntries'],
|
||||
collections: '*',
|
||||
provider: 'algolia',
|
||||
applicationID: 'applicationID',
|
||||
apiKey: 'apiKey',
|
||||
},
|
||||
{
|
||||
hooks: ['listEntries'],
|
||||
collections: ['posts'],
|
||||
provider: 'algolia',
|
||||
applicationID: 'applicationID',
|
||||
apiKey: 'apiKey',
|
||||
},
|
||||
{
|
||||
hooks: ['assetStore'],
|
||||
provider: 'assetStore',
|
||||
getSignedFormURL: 'https://asset.store.com/signedUrl',
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{ name: 'posts', label: 'Posts', type: FOLDER },
|
||||
{ name: 'pages', label: 'Pages', type: FOLDER },
|
||||
{ name: 'faq', label: 'FAQ', type: FOLDER },
|
||||
],
|
||||
},
|
||||
} as ConfigAction);
|
||||
|
||||
expect(result && result.toJS()).toEqual({
|
||||
providers: {
|
||||
algolia: {
|
||||
applicationID: 'applicationID',
|
||||
apiKey: 'apiKey',
|
||||
},
|
||||
assetStore: {
|
||||
getSignedFormURL: 'https://asset.store.com/signedUrl',
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
posts: {
|
||||
listEntries: 'algolia',
|
||||
},
|
||||
pages: {
|
||||
listEntries: 'algolia',
|
||||
},
|
||||
faq: {
|
||||
listEntries: 'algolia',
|
||||
},
|
||||
assetStore: 'assetStore',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
154
node_modules/decap-cms-core/src/reducers/__tests__/mediaLibrary.spec.js
generated
vendored
Normal file
154
node_modules/decap-cms-core/src/reducers/__tests__/mediaLibrary.spec.js
generated
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
|
||||
import { mediaDeleted } from '../../actions/mediaLibrary';
|
||||
import mediaLibrary, {
|
||||
selectMediaFiles,
|
||||
selectMediaFileByPath,
|
||||
selectMediaDisplayURL,
|
||||
} from '../mediaLibrary';
|
||||
|
||||
jest.mock('uuid');
|
||||
jest.mock('../entries');
|
||||
jest.mock('../');
|
||||
|
||||
describe('mediaLibrary', () => {
|
||||
it('should remove media file by key', () => {
|
||||
expect(
|
||||
mediaLibrary(
|
||||
Map({
|
||||
files: [{ key: 'key1' }, { key: 'key2' }],
|
||||
}),
|
||||
mediaDeleted({ key: 'key1' }),
|
||||
),
|
||||
).toEqual(
|
||||
Map({
|
||||
isDeleting: false,
|
||||
files: [{ key: 'key2' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove media file by id', () => {
|
||||
expect(
|
||||
mediaLibrary(
|
||||
Map({
|
||||
files: [{ id: 'id1' }, { id: 'id2' }],
|
||||
}),
|
||||
mediaDeleted({ id: 'id1' }),
|
||||
),
|
||||
).toEqual(
|
||||
Map({
|
||||
isDeleting: false,
|
||||
files: [{ id: 'id2' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should select draft media files from field when editing a draft', () => {
|
||||
const { selectEditingDraft, selectMediaFolder } = require('../../reducers/entries');
|
||||
|
||||
selectEditingDraft.mockReturnValue(true);
|
||||
selectMediaFolder.mockReturnValue('/static/images/posts/logos');
|
||||
|
||||
const imageField = fromJS({ name: 'image' });
|
||||
const collection = fromJS({ fields: [imageField] });
|
||||
const entry = fromJS({
|
||||
collection: 'posts',
|
||||
mediaFiles: [
|
||||
{ id: 1, path: '/static/images/posts/logos/logo.png' },
|
||||
{ id: 2, path: '/static/images/posts/general/image.png' },
|
||||
{ id: 3, path: '/static/images/posts/index.png' },
|
||||
],
|
||||
data: {},
|
||||
});
|
||||
const state = {
|
||||
config: {},
|
||||
collections: fromJS({ posts: collection }),
|
||||
entryDraft: fromJS({
|
||||
entry,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(selectMediaFiles(state, imageField)).toEqual([
|
||||
{ id: 1, key: 1, path: '/static/images/posts/logos/logo.png' },
|
||||
]);
|
||||
|
||||
expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField);
|
||||
});
|
||||
|
||||
it('should select draft media files from collection when editing a draft', () => {
|
||||
const { selectEditingDraft, selectMediaFolder } = require('../../reducers/entries');
|
||||
|
||||
selectEditingDraft.mockReturnValue(true);
|
||||
selectMediaFolder.mockReturnValue('/static/images/posts');
|
||||
|
||||
const imageField = fromJS({ name: 'image' });
|
||||
const collection = fromJS({ fields: [imageField] });
|
||||
const entry = fromJS({
|
||||
collection: 'posts',
|
||||
mediaFiles: [
|
||||
{ id: 1, path: '/static/images/posts/logos/logo.png' },
|
||||
{ id: 2, path: '/static/images/posts/general/image.png' },
|
||||
{ id: 3, path: '/static/images/posts/index.png' },
|
||||
],
|
||||
data: {},
|
||||
});
|
||||
const state = {
|
||||
config: {},
|
||||
collections: fromJS({ posts: collection }),
|
||||
entryDraft: fromJS({
|
||||
entry,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(selectMediaFiles(state, imageField)).toEqual([
|
||||
{ id: 3, key: 3, path: '/static/images/posts/index.png' },
|
||||
]);
|
||||
|
||||
expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField);
|
||||
});
|
||||
|
||||
it('should select global media files when not editing a draft', () => {
|
||||
const { selectEditingDraft } = require('../../reducers/entries');
|
||||
|
||||
selectEditingDraft.mockReturnValue(false);
|
||||
|
||||
const state = {
|
||||
mediaLibrary: Map({ files: [{ id: 1 }] }),
|
||||
};
|
||||
|
||||
expect(selectMediaFiles(state)).toEqual([{ id: 1 }]);
|
||||
});
|
||||
|
||||
it('should select global media files when not using asset store integration', () => {
|
||||
const { selectIntegration } = require('../../reducers');
|
||||
|
||||
selectIntegration.mockReturnValue({});
|
||||
|
||||
const state = {
|
||||
mediaLibrary: Map({ files: [{ id: 1 }] }),
|
||||
};
|
||||
|
||||
expect(selectMediaFiles(state)).toEqual([{ id: 1 }]);
|
||||
});
|
||||
|
||||
it('should return media file by path', () => {
|
||||
const { selectEditingDraft } = require('../../reducers/entries');
|
||||
|
||||
selectEditingDraft.mockReturnValue(false);
|
||||
|
||||
const state = {
|
||||
mediaLibrary: Map({ files: [{ id: 1, path: 'path' }] }),
|
||||
};
|
||||
|
||||
expect(selectMediaFileByPath(state, 'path')).toEqual({ id: 1, path: 'path' });
|
||||
});
|
||||
|
||||
it('should return media display URL state', () => {
|
||||
const state = {
|
||||
mediaLibrary: fromJS({ displayURLs: { id: { url: 'url' } } }),
|
||||
};
|
||||
|
||||
expect(selectMediaDisplayURL(state, 'id')).toEqual(Map({ url: 'url' }));
|
||||
});
|
||||
});
|
||||
49
node_modules/decap-cms-core/src/reducers/__tests__/medias.spec.ts
generated
vendored
Normal file
49
node_modules/decap-cms-core/src/reducers/__tests__/medias.spec.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
addAssets,
|
||||
addAsset,
|
||||
removeAsset,
|
||||
loadAssetRequest,
|
||||
loadAssetSuccess,
|
||||
loadAssetFailure,
|
||||
} from '../../actions/media';
|
||||
import reducer from '../medias';
|
||||
import { createAssetProxy } from '../../valueObjects/AssetProxy';
|
||||
|
||||
describe('medias', () => {
|
||||
const asset = createAssetProxy({ url: 'url', path: 'path' });
|
||||
|
||||
it('should add assets', () => {
|
||||
expect(reducer({}, addAssets([asset]))).toEqual({
|
||||
path: { asset, isLoading: false, error: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('should add asset', () => {
|
||||
expect(reducer({}, addAsset(asset))).toEqual({
|
||||
path: { asset, isLoading: false, error: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove asset', () => {
|
||||
expect(
|
||||
reducer({ [asset.path]: { asset, isLoading: false, error: null } }, removeAsset(asset.path)),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it('should mark asset as loading', () => {
|
||||
expect(reducer({}, loadAssetRequest(asset.path))).toEqual({ path: { isLoading: true } });
|
||||
});
|
||||
|
||||
it('should mark asset as not loading', () => {
|
||||
expect(reducer({}, loadAssetSuccess(asset.path))).toEqual({
|
||||
path: { isLoading: false, error: null },
|
||||
});
|
||||
});
|
||||
|
||||
it('should set loading error', () => {
|
||||
const error = new Error('some error');
|
||||
expect(reducer({}, loadAssetFailure(asset.path, error))).toEqual({
|
||||
path: { isLoading: false, error },
|
||||
});
|
||||
});
|
||||
});
|
||||
46
node_modules/decap-cms-core/src/reducers/auth.ts
generated
vendored
Normal file
46
node_modules/decap-cms-core/src/reducers/auth.ts
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
AUTH_REQUEST,
|
||||
AUTH_SUCCESS,
|
||||
AUTH_FAILURE,
|
||||
AUTH_REQUEST_DONE,
|
||||
LOGOUT,
|
||||
} from '../actions/auth';
|
||||
|
||||
import type { User } from 'decap-cms-lib-util';
|
||||
import type { AuthAction } from '../actions/auth';
|
||||
|
||||
export type Auth = {
|
||||
isFetching: boolean;
|
||||
user: User | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
|
||||
export const defaultState: Auth = {
|
||||
isFetching: false,
|
||||
user: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const auth = produce((state: Auth, action: AuthAction) => {
|
||||
switch (action.type) {
|
||||
case AUTH_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case AUTH_SUCCESS:
|
||||
state.user = action.payload;
|
||||
break;
|
||||
case AUTH_FAILURE:
|
||||
state.error = action.payload && action.payload.toString();
|
||||
break;
|
||||
case AUTH_REQUEST_DONE:
|
||||
state.isFetching = false;
|
||||
break;
|
||||
case LOGOUT:
|
||||
state.user = undefined;
|
||||
state.isFetching = false;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default auth;
|
||||
487
node_modules/decap-cms-core/src/reducers/collections.ts
generated
vendored
Normal file
487
node_modules/decap-cms-core/src/reducers/collections.ts
generated
vendored
Normal file
@@ -0,0 +1,487 @@
|
||||
import { List, Set, fromJS, OrderedMap } from 'immutable';
|
||||
import { get, escapeRegExp } from 'lodash';
|
||||
import { stringTemplate } from 'decap-cms-lib-widgets';
|
||||
|
||||
import consoleError from '../lib/consoleError';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
|
||||
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
|
||||
import { getFormatExtensions } from '../formats/formats';
|
||||
import { selectMediaFolder } from './entries';
|
||||
import { summaryFormatter } from '../lib/formatters';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
Collections,
|
||||
CollectionFiles,
|
||||
EntryField,
|
||||
EntryMap,
|
||||
ViewFilter,
|
||||
ViewGroup,
|
||||
CmsConfig,
|
||||
} from '../types/redux';
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Backend } from '../backend';
|
||||
|
||||
const { keyToPathArray } = stringTemplate;
|
||||
|
||||
const defaultState: Collections = fromJS({});
|
||||
|
||||
function collections(state = defaultState, action: ConfigAction) {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const collections = action.payload.collections;
|
||||
let newState = OrderedMap({});
|
||||
collections.forEach(collection => {
|
||||
newState = newState.set(collection.name, fromJS(collection));
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const selectors = {
|
||||
[FOLDER]: {
|
||||
entryExtension(collection: Collection) {
|
||||
const ext =
|
||||
collection.get('extension') ||
|
||||
get(getFormatExtensions(), collection.get('format') || 'frontmatter');
|
||||
if (!ext) {
|
||||
throw new Error(`No extension found for format ${collection.get('format')}`);
|
||||
}
|
||||
|
||||
return ext.replace(/^\./, '');
|
||||
},
|
||||
fields(collection: Collection) {
|
||||
return collection.get('fields');
|
||||
},
|
||||
entryPath(collection: Collection, slug: string) {
|
||||
const folder = (collection.get('folder') as string).replace(/\/$/, '');
|
||||
return `${folder}/${slug}.${this.entryExtension(collection)}`;
|
||||
},
|
||||
entrySlug(collection: Collection, path: string) {
|
||||
const folder = (collection.get('folder') as string).replace(/\/$/, '');
|
||||
const slug = path
|
||||
.split(folder + '/')
|
||||
.pop()
|
||||
?.replace(new RegExp(`\\.${escapeRegExp(this.entryExtension(collection))}$`), '');
|
||||
|
||||
return slug;
|
||||
},
|
||||
allowNewEntries(collection: Collection) {
|
||||
return collection.get('create');
|
||||
},
|
||||
allowDeletion(collection: Collection) {
|
||||
return collection.get('delete', true);
|
||||
},
|
||||
templateName(collection: Collection) {
|
||||
return collection.get('name');
|
||||
},
|
||||
},
|
||||
[FILES]: {
|
||||
fileForEntry(collection: Collection, slug: string) {
|
||||
const files = collection.get('files');
|
||||
return files && files.filter(f => f?.get('name') === slug).get(0);
|
||||
},
|
||||
fields(collection: Collection, slug: string) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('fields');
|
||||
},
|
||||
entryPath(collection: Collection, slug: string) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('file');
|
||||
},
|
||||
entrySlug(collection: Collection, path: string) {
|
||||
const file = (collection.get('files') as CollectionFiles)
|
||||
.filter(f => f?.get('file') === path)
|
||||
.get(0);
|
||||
return file && file.get('name');
|
||||
},
|
||||
entryLabel(collection: Collection, slug: string) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('label');
|
||||
},
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
},
|
||||
allowDeletion(collection: Collection) {
|
||||
return collection.get('delete', false);
|
||||
},
|
||||
templateName(_collection: Collection, slug: string) {
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getFieldsWithMediaFolders(fields: EntryField[]) {
|
||||
const fieldsWithMediaFolders = fields.reduce((acc, f) => {
|
||||
if (f.has('media_folder')) {
|
||||
acc = [...acc, f];
|
||||
}
|
||||
|
||||
if (f.has('fields')) {
|
||||
const fields = f.get('fields')?.toArray() as EntryField[];
|
||||
acc = [...acc, ...getFieldsWithMediaFolders(fields)];
|
||||
} else if (f.has('field')) {
|
||||
const field = f.get('field') as EntryField;
|
||||
acc = [...acc, ...getFieldsWithMediaFolders([field])];
|
||||
} else if (f.has('types')) {
|
||||
const types = f.get('types')?.toArray() as EntryField[];
|
||||
acc = [...acc, ...getFieldsWithMediaFolders(types)];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as EntryField[]);
|
||||
|
||||
return fieldsWithMediaFolders;
|
||||
}
|
||||
|
||||
export function getFileFromSlug(collection: Collection, slug: string) {
|
||||
return collection
|
||||
.get('files')
|
||||
?.toArray()
|
||||
.find(f => f.get('name') === slug);
|
||||
}
|
||||
|
||||
export function selectFieldsWithMediaFolders(collection: Collection, slug: string) {
|
||||
if (collection.has('folder')) {
|
||||
const fields = collection.get('fields').toArray();
|
||||
return getFieldsWithMediaFolders(fields);
|
||||
} else if (collection.has('files')) {
|
||||
const fields = getFileFromSlug(collection, slug)?.get('fields').toArray() || [];
|
||||
return getFieldsWithMediaFolders(fields);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function selectMediaFolders(config: CmsConfig, collection: Collection, entry: EntryMap) {
|
||||
const fields = selectFieldsWithMediaFolders(collection, entry.get('slug'));
|
||||
const folders = fields.map(f => selectMediaFolder(config, collection, entry, f));
|
||||
if (collection.has('files')) {
|
||||
const file = getFileFromSlug(collection, entry.get('slug'));
|
||||
if (file) {
|
||||
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
|
||||
}
|
||||
}
|
||||
if (collection.has('media_folder')) {
|
||||
// stop evaluating media folders at collection level
|
||||
collection = collection.delete('files');
|
||||
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
|
||||
}
|
||||
|
||||
return Set(folders).toArray();
|
||||
}
|
||||
|
||||
export function selectFields(collection: Collection, slug: string) {
|
||||
return selectors[collection.get('type')].fields(collection, slug);
|
||||
}
|
||||
|
||||
export function selectFolderEntryExtension(collection: Collection) {
|
||||
return selectors[FOLDER].entryExtension(collection);
|
||||
}
|
||||
|
||||
export function selectFileEntryLabel(collection: Collection, slug: string) {
|
||||
return selectors[FILES].entryLabel(collection, slug);
|
||||
}
|
||||
|
||||
export function selectEntryPath(collection: Collection, slug: string) {
|
||||
return selectors[collection.get('type')].entryPath(collection, slug);
|
||||
}
|
||||
|
||||
export function selectEntrySlug(collection: Collection, path: string) {
|
||||
return selectors[collection.get('type')].entrySlug(collection, path);
|
||||
}
|
||||
|
||||
export function selectAllowNewEntries(collection: Collection) {
|
||||
return selectors[collection.get('type')].allowNewEntries(collection);
|
||||
}
|
||||
|
||||
export function selectAllowDeletion(collection: Collection) {
|
||||
return selectors[collection.get('type')].allowDeletion(collection);
|
||||
}
|
||||
|
||||
export function selectTemplateName(collection: Collection, slug: string) {
|
||||
return selectors[collection.get('type')].templateName(collection, slug);
|
||||
}
|
||||
|
||||
export function getFieldsNames(fields: EntryField[], prefix = '') {
|
||||
let names = fields.map(f => `${prefix}${f.get('name')}`);
|
||||
|
||||
fields.forEach((f, index) => {
|
||||
if (f.has('fields')) {
|
||||
const fields = f.get('fields')?.toArray() as EntryField[];
|
||||
names = [...names, ...getFieldsNames(fields, `${names[index]}.`)];
|
||||
} else if (f.has('field')) {
|
||||
const field = f.get('field') as EntryField;
|
||||
names = [...names, ...getFieldsNames([field], `${names[index]}.`)];
|
||||
} else if (f.has('types')) {
|
||||
const types = f.get('types')?.toArray() as EntryField[];
|
||||
names = [...names, ...getFieldsNames(types, `${names[index]}.`)];
|
||||
}
|
||||
});
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
export function selectField(collection: Collection, key: string) {
|
||||
const array = keyToPathArray(key);
|
||||
let name: string | undefined;
|
||||
let field;
|
||||
let fields = collection.get('fields', List<EntryField>()).toArray();
|
||||
while ((name = array.shift()) && fields) {
|
||||
field = fields.find(f => f.get('name') === name);
|
||||
if (field?.has('fields')) {
|
||||
fields = field?.get('fields')?.toArray() as EntryField[];
|
||||
} else if (field?.has('field')) {
|
||||
fields = [field?.get('field') as EntryField];
|
||||
} else if (field?.has('types')) {
|
||||
fields = field?.get('types')?.toArray() as EntryField[];
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
export function traverseFields(
|
||||
fields: List<EntryField>,
|
||||
updater: (field: EntryField) => EntryField,
|
||||
done = () => false,
|
||||
) {
|
||||
if (done()) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
fields = fields
|
||||
.map(f => {
|
||||
const field = updater(f as EntryField);
|
||||
if (done()) {
|
||||
return field;
|
||||
} else if (field.has('fields')) {
|
||||
return field.set('fields', traverseFields(field.get('fields')!, updater, done));
|
||||
} else if (field.has('field')) {
|
||||
return field.set(
|
||||
'field',
|
||||
traverseFields(List([field.get('field')!]), updater, done).get(0),
|
||||
);
|
||||
} else if (field.has('types')) {
|
||||
return field.set('types', traverseFields(field.get('types')!, updater, done));
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
})
|
||||
.toList() as List<EntryField>;
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function updateFieldByKey(
|
||||
collection: Collection,
|
||||
key: string,
|
||||
updater: (field: EntryField) => EntryField,
|
||||
) {
|
||||
const selected = selectField(collection, key);
|
||||
if (!selected) {
|
||||
return collection;
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
|
||||
function updateAndBreak(f: EntryField) {
|
||||
const field = f as EntryField;
|
||||
if (field === selected) {
|
||||
updated = true;
|
||||
return updater(field);
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
collection = collection.set(
|
||||
'fields',
|
||||
traverseFields(collection.get('fields', List<EntryField>()), updateAndBreak, () => updated),
|
||||
);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
export function selectIdentifier(collection: Collection) {
|
||||
const identifier = collection.get('identifier_field');
|
||||
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : [...IDENTIFIER_FIELDS];
|
||||
const fieldNames = getFieldsNames(collection.get('fields', List()).toArray());
|
||||
return identifierFields.find(id =>
|
||||
fieldNames.find(name => name.toLowerCase().trim() === id.toLowerCase().trim()),
|
||||
);
|
||||
}
|
||||
|
||||
export function selectInferredField(collection: Collection, fieldName: string) {
|
||||
if (fieldName === 'title' && collection.get('identifier_field')) {
|
||||
return selectIdentifier(collection);
|
||||
}
|
||||
const inferableField = (
|
||||
INFERABLE_FIELDS as Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
synonyms: string[];
|
||||
secondaryTypes: string[];
|
||||
fallbackToFirstField: boolean;
|
||||
showError: boolean;
|
||||
}
|
||||
>
|
||||
)[fieldName];
|
||||
const fields = collection.get('fields');
|
||||
let field;
|
||||
|
||||
// If collection has no fields or fieldName is not defined within inferables list, return null
|
||||
if (!fields || !inferableField) return null;
|
||||
// Try to return a field of the specified type with one of the synonyms
|
||||
const mainTypeFields = fields
|
||||
.filter(f => f?.get('widget', 'string') === inferableField.type)
|
||||
.map(f => f?.get('name'));
|
||||
field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f as string) !== -1);
|
||||
if (field && field.size > 0) return field.first();
|
||||
|
||||
// Try to return a field for each of the specified secondary types
|
||||
const secondaryTypeFields = fields
|
||||
.filter(f => inferableField.secondaryTypes.indexOf(f?.get('widget', 'string') as string) !== -1)
|
||||
.map(f => f?.get('name'));
|
||||
field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f as string) !== -1);
|
||||
if (field && field.size > 0) return field.first();
|
||||
|
||||
// Try to return the first field of the specified type
|
||||
if (inferableField.fallbackToFirstField && mainTypeFields.size > 0) return mainTypeFields.first();
|
||||
|
||||
// Coundn't infer the field. Show error and return null.
|
||||
if (inferableField.showError) {
|
||||
consoleError(
|
||||
`The Field ${fieldName} is missing for the collection “${collection.get('name')}”`,
|
||||
`Decap CMS tries to infer the entry ${fieldName} automatically, but one couldn't be found for entries of the collection “${collection.get(
|
||||
'name',
|
||||
)}”. Please check your site configuration.`,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function selectEntryCollectionTitle(collection: Collection, entry: EntryMap) {
|
||||
// prefer formatted summary over everything else
|
||||
const summaryTemplate = collection.get('summary');
|
||||
if (summaryTemplate) return summaryFormatter(summaryTemplate, entry, collection);
|
||||
|
||||
// if the collection is a file collection return the label of the entry
|
||||
if (collection.get('type') == FILES) {
|
||||
const label = selectFileEntryLabel(collection, entry.get('slug'));
|
||||
if (label) return label;
|
||||
}
|
||||
|
||||
// try to infer a title field from the entry data
|
||||
const entryData = entry.get('data');
|
||||
const titleField = selectInferredField(collection, 'title');
|
||||
const result = titleField && entryData.getIn(keyToPathArray(titleField));
|
||||
|
||||
// if the custom field does not yield a result, fallback to 'title'
|
||||
if (!result && titleField !== 'title') {
|
||||
return entryData.getIn(keyToPathArray('title'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function selectDefaultSortableFields(
|
||||
collection: Collection,
|
||||
backend: Backend,
|
||||
hasIntegration: boolean,
|
||||
) {
|
||||
let defaultSortable = SORTABLE_FIELDS.map((type: string) => {
|
||||
const field = selectInferredField(collection, type);
|
||||
if (backend.isGitBackend() && type === 'author' && !field && !hasIntegration) {
|
||||
// default to commit author if not author field is found
|
||||
return COMMIT_AUTHOR;
|
||||
}
|
||||
return field;
|
||||
}).filter(Boolean);
|
||||
|
||||
if (backend.isGitBackend() && !hasIntegration) {
|
||||
// always have commit date by default
|
||||
defaultSortable = [COMMIT_DATE, ...defaultSortable];
|
||||
}
|
||||
|
||||
return defaultSortable as string[];
|
||||
}
|
||||
|
||||
export function selectSortableFields(collection: Collection, t: (key: string) => string) {
|
||||
const fields = collection
|
||||
.get('sortable_fields')
|
||||
.toArray()
|
||||
.map(key => {
|
||||
if (key === COMMIT_DATE) {
|
||||
return { key, field: { name: key, label: t('collection.defaultFields.updatedOn.label') } };
|
||||
}
|
||||
const field = selectField(collection, key);
|
||||
if (key === COMMIT_AUTHOR && !field) {
|
||||
return { key, field: { name: key, label: t('collection.defaultFields.author.label') } };
|
||||
}
|
||||
|
||||
return { key, field: field?.toJS() };
|
||||
})
|
||||
.filter(item => !!item.field)
|
||||
.map(item => ({ ...item.field, key: item.key }));
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function selectSortDataPath(collection: Collection, key: string) {
|
||||
if (key === COMMIT_DATE) {
|
||||
return 'updatedOn';
|
||||
} else if (key === COMMIT_AUTHOR && !selectField(collection, key)) {
|
||||
return 'author';
|
||||
} else {
|
||||
return `data.${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectViewFilters(collection: Collection) {
|
||||
const viewFilters = collection.get('view_filters').toJS() as ViewFilter[];
|
||||
return viewFilters;
|
||||
}
|
||||
|
||||
export function selectViewGroups(collection: Collection) {
|
||||
const viewGroups = collection.get('view_groups').toJS() as ViewGroup[];
|
||||
return viewGroups;
|
||||
}
|
||||
|
||||
export function selectFieldsComments(collection: Collection, entryMap: EntryMap) {
|
||||
let fields: EntryField[] = [];
|
||||
if (collection.has('folder')) {
|
||||
fields = collection.get('fields').toArray();
|
||||
} else if (collection.has('files')) {
|
||||
const file = collection.get('files')!.find(f => f?.get('name') === entryMap.get('slug'));
|
||||
fields = file.get('fields').toArray();
|
||||
}
|
||||
const comments: Record<string, string> = {};
|
||||
const names = getFieldsNames(fields);
|
||||
names.forEach(name => {
|
||||
const field = selectField(collection, name);
|
||||
if (field?.has('comment')) {
|
||||
comments[name] = field.get('comment')!;
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
export function selectHasMetaPath(collection: Collection) {
|
||||
return (
|
||||
collection.has('folder') &&
|
||||
collection.get('type') === FOLDER &&
|
||||
collection.has('meta') &&
|
||||
collection.get('meta')?.has('path')
|
||||
);
|
||||
}
|
||||
|
||||
export default collections;
|
||||
11
node_modules/decap-cms-core/src/reducers/combinedReducer.ts
generated
vendored
Normal file
11
node_modules/decap-cms-core/src/reducers/combinedReducer.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import reducers from './index';
|
||||
|
||||
function createRootReducer() {
|
||||
return combineReducers({
|
||||
...reducers,
|
||||
});
|
||||
}
|
||||
|
||||
export default createRootReducer;
|
||||
38
node_modules/decap-cms-core/src/reducers/config.ts
generated
vendored
Normal file
38
node_modules/decap-cms-core/src/reducers/config.ts
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { CmsConfig } from '../types/redux';
|
||||
|
||||
const defaultState = {
|
||||
isFetching: true,
|
||||
};
|
||||
|
||||
const config = produce((state: CmsConfig, action: ConfigAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case CONFIG_SUCCESS:
|
||||
return {
|
||||
...action.payload,
|
||||
isFetching: false,
|
||||
error: undefined,
|
||||
};
|
||||
case CONFIG_FAILURE:
|
||||
state.isFetching = false;
|
||||
state.error = action.payload.toString();
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectLocale(state: CmsConfig) {
|
||||
return state.locale || 'en';
|
||||
}
|
||||
|
||||
export function selectUseWorkflow(state: CmsConfig) {
|
||||
return state.publish_mode === EDITORIAL_WORKFLOW;
|
||||
}
|
||||
|
||||
export default config;
|
||||
36
node_modules/decap-cms-core/src/reducers/cursors.js
generated
vendored
Normal file
36
node_modules/decap-cms-core/src/reducers/cursors.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { Cursor } from 'decap-cms-lib-util';
|
||||
|
||||
import {
|
||||
ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
|
||||
// Since pagination can be used for a variety of views (collections
|
||||
// and searches are the most common examples), we namespace cursors by
|
||||
// their type before storing them in the state.
|
||||
export function selectCollectionEntriesCursor(state, collectionName) {
|
||||
return new Cursor(state.getIn(['cursorsByType', 'collectionEntries', collectionName]));
|
||||
}
|
||||
|
||||
function cursors(state = fromJS({ cursorsByType: { collectionEntries: {} } }), action) {
|
||||
switch (action.type) {
|
||||
case ENTRIES_SUCCESS: {
|
||||
return state.setIn(
|
||||
['cursorsByType', 'collectionEntries', action.payload.collection],
|
||||
Cursor.create(action.payload.cursor).store,
|
||||
);
|
||||
}
|
||||
case FILTER_ENTRIES_SUCCESS:
|
||||
case GROUP_ENTRIES_SUCCESS:
|
||||
case SORT_ENTRIES_SUCCESS: {
|
||||
return state.deleteIn(['cursorsByType', 'collectionEntries', action.payload.collection]);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default cursors;
|
||||
52
node_modules/decap-cms-core/src/reducers/deploys.ts
generated
vendored
Normal file
52
node_modules/decap-cms-core/src/reducers/deploys.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
DEPLOY_PREVIEW_REQUEST,
|
||||
DEPLOY_PREVIEW_SUCCESS,
|
||||
DEPLOY_PREVIEW_FAILURE,
|
||||
} from '../actions/deploys';
|
||||
|
||||
import type { DeploysAction } from '../actions/deploys';
|
||||
|
||||
export type Deploys = {
|
||||
[key: string]: {
|
||||
isFetching: boolean;
|
||||
url?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const defaultState: Deploys = {};
|
||||
|
||||
const deploys = produce((state: Deploys, action: DeploysAction) => {
|
||||
switch (action.type) {
|
||||
case DEPLOY_PREVIEW_REQUEST: {
|
||||
const { collection, slug } = action.payload;
|
||||
const key = `${collection}.${slug}`;
|
||||
state[key] = state[key] || {};
|
||||
state[key].isFetching = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case DEPLOY_PREVIEW_SUCCESS: {
|
||||
const { collection, slug, url, status } = action.payload;
|
||||
const key = `${collection}.${slug}`;
|
||||
state[key].isFetching = false;
|
||||
state[key].url = url;
|
||||
state[key].status = status;
|
||||
break;
|
||||
}
|
||||
|
||||
case DEPLOY_PREVIEW_FAILURE: {
|
||||
const { collection, slug } = action.payload;
|
||||
state[`${collection}.${slug}`].isFetching = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectDeployPreview(state: Deploys, collection: string, slug: string) {
|
||||
return state[`${collection}.${slug}`];
|
||||
}
|
||||
|
||||
export default deploys;
|
||||
163
node_modules/decap-cms-core/src/reducers/editorialWorkflow.ts
generated
vendored
Normal file
163
node_modules/decap-cms-core/src/reducers/editorialWorkflow.ts
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { startsWith } from 'lodash';
|
||||
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_REQUEST,
|
||||
UNPUBLISHED_ENTRY_REDIRECT,
|
||||
UNPUBLISHED_ENTRY_SUCCESS,
|
||||
UNPUBLISHED_ENTRIES_REQUEST,
|
||||
UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
UNPUBLISHED_ENTRY_DELETE_SUCCESS,
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { EditorialWorkflowAction, EditorialWorkflow, Entities } from '../types/redux';
|
||||
|
||||
function unpublishedEntries(state = Map(), action: EditorialWorkflowAction) {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const publishMode = action.payload && action.payload.publish_mode;
|
||||
if (publishMode === EDITORIAL_WORKFLOW) {
|
||||
// Editorial workflow state is explicitly initiated after the config.
|
||||
return Map({ entities: Map(), pages: Map() });
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case UNPUBLISHED_ENTRY_REQUEST:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isFetching'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_REDIRECT:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_SUCCESS:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.entry.slug}`],
|
||||
fromJS(action.payload!.entry),
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRIES_REQUEST:
|
||||
return state.setIn(['pages', 'isFetching'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRIES_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
action.payload!.entries.forEach(entry =>
|
||||
map.setIn(
|
||||
['entities', `${entry.collection}.${entry.slug}`],
|
||||
fromJS(entry).set('isFetching', false),
|
||||
),
|
||||
);
|
||||
map.set(
|
||||
'pages',
|
||||
Map({
|
||||
...action.payload!.pages,
|
||||
ids: List(action.payload!.entries.map(entry => entry.slug)),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPersisting'],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_SUCCESS:
|
||||
// Update Optimistically
|
||||
return state.withMutations(map => {
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.entry.get('slug')}`],
|
||||
fromJS(action.payload!.entry),
|
||||
);
|
||||
map.deleteIn([
|
||||
'entities',
|
||||
`${action.payload!.collection}.${action.payload!.entry.get('slug')}`,
|
||||
'isPersisting',
|
||||
]);
|
||||
map.updateIn(['pages', 'ids'], List(), list =>
|
||||
list.push(action.payload!.entry.get('slug')),
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_FAILURE:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPersisting'],
|
||||
false,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST:
|
||||
// Update Optimistically
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'status'],
|
||||
action.payload!.newStatus,
|
||||
);
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
false,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPublishing'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_DELETE_SUCCESS:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_FAILURE:
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntry(state: EditorialWorkflow, collection: string, slug: string) {
|
||||
return state && state.getIn(['entities', `${collection}.${slug}`]);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntriesByStatus(state: EditorialWorkflow, status: string) {
|
||||
if (!state) return null;
|
||||
const entities = state.get('entities') as Entities;
|
||||
return entities.filter(entry => entry.get('status') === status).valueSeq();
|
||||
}
|
||||
|
||||
export function selectUnpublishedSlugs(state: EditorialWorkflow, collection: string) {
|
||||
if (!state.get('entities')) return null;
|
||||
const entities = state.get('entities') as Entities;
|
||||
return entities
|
||||
.filter((_v, k) => startsWith(k as string, `${collection}.`))
|
||||
.map(entry => entry.get('slug'))
|
||||
.valueSeq();
|
||||
}
|
||||
|
||||
export default unpublishedEntries;
|
||||
806
node_modules/decap-cms-core/src/reducers/entries.ts
generated
vendored
Normal file
806
node_modules/decap-cms-core/src/reducers/entries.ts
generated
vendored
Normal file
@@ -0,0 +1,806 @@
|
||||
import { Map, List, fromJS, OrderedMap, Set } from 'immutable';
|
||||
import { dirname, join } from 'path';
|
||||
import { isAbsolutePath, basename } from 'decap-cms-lib-util';
|
||||
import { trim, once, sortBy, set, orderBy, groupBy } from 'lodash';
|
||||
import { stringTemplate } from 'decap-cms-lib-widgets';
|
||||
|
||||
import { SortDirection } from '../types/redux';
|
||||
import { folderFormatter } from '../lib/formatters';
|
||||
import { selectSortDataPath } from './collections';
|
||||
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||
import {
|
||||
ENTRY_REQUEST,
|
||||
ENTRY_SUCCESS,
|
||||
ENTRY_FAILURE,
|
||||
ENTRIES_REQUEST,
|
||||
ENTRIES_SUCCESS,
|
||||
ENTRIES_FAILURE,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
SORT_ENTRIES_REQUEST,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_FAILURE,
|
||||
FILTER_ENTRIES_REQUEST,
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
FILTER_ENTRIES_FAILURE,
|
||||
GROUP_ENTRIES_REQUEST,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
GROUP_ENTRIES_FAILURE,
|
||||
CHANGE_VIEW_STYLE,
|
||||
} from '../actions/entries';
|
||||
import { VIEW_STYLE_LIST } from '../constants/collectionViews';
|
||||
import { joinUrlPath } from '../lib/urlHelper';
|
||||
|
||||
import type {
|
||||
EntriesAction,
|
||||
EntryRequestPayload,
|
||||
EntrySuccessPayload,
|
||||
EntriesSuccessPayload,
|
||||
EntryObject,
|
||||
Entries,
|
||||
CmsConfig,
|
||||
Collection,
|
||||
EntryFailurePayload,
|
||||
EntryDeletePayload,
|
||||
EntriesRequestPayload,
|
||||
EntryDraft,
|
||||
EntryMap,
|
||||
EntryField,
|
||||
CollectionFiles,
|
||||
EntriesSortRequestPayload,
|
||||
EntriesSortFailurePayload,
|
||||
SortMap,
|
||||
SortObject,
|
||||
Sort,
|
||||
Filter,
|
||||
Group,
|
||||
FilterMap,
|
||||
GroupMap,
|
||||
EntriesFilterRequestPayload,
|
||||
EntriesFilterFailurePayload,
|
||||
ChangeViewStylePayload,
|
||||
EntriesGroupRequestPayload,
|
||||
EntriesGroupFailurePayload,
|
||||
GroupOfEntries,
|
||||
} from '../types/redux';
|
||||
|
||||
const { keyToPathArray } = stringTemplate;
|
||||
|
||||
let collection: string;
|
||||
let loadedEntries: EntryObject[];
|
||||
let append: boolean;
|
||||
let page: number;
|
||||
let slug: string;
|
||||
|
||||
const storageSortKey = 'decap-cms.entries.sort';
|
||||
const viewStyleKey = 'decap-cms.entries.viewStyle';
|
||||
type StorageSortObject = SortObject & { index: number };
|
||||
type StorageSort = { [collection: string]: { [key: string]: StorageSortObject } };
|
||||
|
||||
const loadSort = once(() => {
|
||||
const sortString = localStorage.getItem(storageSortKey);
|
||||
if (sortString) {
|
||||
try {
|
||||
const sort: StorageSort = JSON.parse(sortString);
|
||||
let map = Map() as Sort;
|
||||
Object.entries(sort).forEach(([collection, sort]) => {
|
||||
let orderedMap = OrderedMap() as SortMap;
|
||||
sortBy(Object.values(sort), ['index']).forEach(value => {
|
||||
const { key, direction } = value;
|
||||
orderedMap = orderedMap.set(key, fromJS({ key, direction }));
|
||||
});
|
||||
map = map.set(collection, orderedMap);
|
||||
});
|
||||
return map;
|
||||
} catch (e) {
|
||||
return Map() as Sort;
|
||||
}
|
||||
}
|
||||
return Map() as Sort;
|
||||
});
|
||||
|
||||
function clearSort() {
|
||||
localStorage.removeItem(storageSortKey);
|
||||
}
|
||||
|
||||
function persistSort(sort: Sort | undefined) {
|
||||
if (sort) {
|
||||
const storageSort: StorageSort = {};
|
||||
sort.keySeq().forEach(key => {
|
||||
const collection = key as string;
|
||||
const sortObjects = (sort.get(collection).valueSeq().toJS() as SortObject[]).map(
|
||||
(value, index) => ({ ...value, index }),
|
||||
);
|
||||
|
||||
sortObjects.forEach(value => {
|
||||
set(storageSort, [collection, value.key], value);
|
||||
});
|
||||
});
|
||||
localStorage.setItem(storageSortKey, JSON.stringify(storageSort));
|
||||
} else {
|
||||
clearSort();
|
||||
}
|
||||
}
|
||||
|
||||
const loadViewStyle = once(() => {
|
||||
const viewStyle = localStorage.getItem(viewStyleKey);
|
||||
if (viewStyle) {
|
||||
return viewStyle;
|
||||
}
|
||||
|
||||
localStorage.setItem(viewStyleKey, VIEW_STYLE_LIST);
|
||||
return VIEW_STYLE_LIST;
|
||||
});
|
||||
|
||||
function clearViewStyle() {
|
||||
localStorage.removeItem(viewStyleKey);
|
||||
}
|
||||
|
||||
function persistViewStyle(viewStyle: string | undefined) {
|
||||
if (viewStyle) {
|
||||
localStorage.setItem(viewStyleKey, viewStyle);
|
||||
} else {
|
||||
clearViewStyle();
|
||||
}
|
||||
}
|
||||
|
||||
function entries(
|
||||
state = Map({ entities: Map(), pages: Map(), sort: loadSort(), viewStyle: loadViewStyle() }),
|
||||
action: EntriesAction,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ENTRY_REQUEST: {
|
||||
const payload = action.payload as EntryRequestPayload;
|
||||
return state.setIn(['entities', `${payload.collection}.${payload.slug}`, 'isFetching'], true);
|
||||
}
|
||||
|
||||
case ENTRY_SUCCESS: {
|
||||
const payload = action.payload as EntrySuccessPayload;
|
||||
collection = payload.collection;
|
||||
slug = payload.entry.slug;
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['entities', `${collection}.${slug}`], fromJS(payload.entry));
|
||||
const ids = map.getIn(['pages', collection, 'ids'], List());
|
||||
if (!ids.includes(slug)) {
|
||||
map.setIn(['pages', collection, 'ids'], ids.unshift(slug));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case ENTRIES_REQUEST: {
|
||||
const payload = action.payload as EntriesRequestPayload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.setIn(['pages', payload.collection, 'isFetching'], true);
|
||||
});
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case ENTRIES_SUCCESS: {
|
||||
const payload = action.payload as EntriesSuccessPayload;
|
||||
collection = payload.collection;
|
||||
loadedEntries = payload.entries;
|
||||
append = payload.append;
|
||||
page = payload.page;
|
||||
return state.withMutations(map => {
|
||||
loadedEntries.forEach(entry =>
|
||||
map.setIn(
|
||||
['entities', `${collection}.${entry.slug}`],
|
||||
fromJS(entry).set('isFetching', false),
|
||||
),
|
||||
);
|
||||
|
||||
const ids = List(loadedEntries.map(entry => entry.slug));
|
||||
map.setIn(
|
||||
['pages', collection],
|
||||
Map({
|
||||
page,
|
||||
ids: append ? map.getIn(['pages', collection, 'ids'], List()).concat(ids) : ids,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
case ENTRIES_FAILURE:
|
||||
return state.setIn(['pages', action.meta.collection, 'isFetching'], false);
|
||||
|
||||
case ENTRY_FAILURE: {
|
||||
const payload = action.payload as EntryFailurePayload;
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['entities', `${payload.collection}.${payload.slug}`, 'isFetching'], false);
|
||||
map.setIn(
|
||||
['entities', `${payload.collection}.${payload.slug}`, 'error'],
|
||||
payload.error.message,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS: {
|
||||
const payload = action.payload as EntriesSuccessPayload;
|
||||
loadedEntries = payload.entries;
|
||||
return state.withMutations(map => {
|
||||
loadedEntries.forEach(entry =>
|
||||
map.setIn(
|
||||
['entities', `${entry.collection}.${entry.slug}`],
|
||||
fromJS(entry).set('isFetching', false),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
case ENTRY_DELETE_SUCCESS: {
|
||||
const payload = action.payload as EntryDeletePayload;
|
||||
return state.withMutations(map => {
|
||||
map.deleteIn(['entities', `${payload.collectionName}.${payload.entrySlug}`]);
|
||||
map.updateIn(['pages', payload.collectionName, 'ids'], (ids: string[]) =>
|
||||
ids.filter(id => id !== payload.entrySlug),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
case SORT_ENTRIES_REQUEST: {
|
||||
const payload = action.payload as EntriesSortRequestPayload;
|
||||
const { collection, key, direction } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
const sort = OrderedMap({ [key]: Map({ key, direction }) });
|
||||
map.setIn(['sort', collection], sort);
|
||||
map.setIn(['pages', collection, 'isFetching'], true);
|
||||
map.deleteIn(['pages', collection, 'page']);
|
||||
});
|
||||
persistSort(newState.get('sort') as Sort);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_SUCCESS:
|
||||
case FILTER_ENTRIES_SUCCESS:
|
||||
case SORT_ENTRIES_SUCCESS: {
|
||||
const payload = action.payload as { collection: string; entries: EntryObject[] };
|
||||
const { collection, entries } = payload;
|
||||
loadedEntries = entries;
|
||||
const newState = state.withMutations(map => {
|
||||
loadedEntries.forEach(entry =>
|
||||
map.setIn(
|
||||
['entities', `${entry.collection}.${entry.slug}`],
|
||||
fromJS(entry).set('isFetching', false),
|
||||
),
|
||||
);
|
||||
map.setIn(['pages', collection, 'isFetching'], false);
|
||||
const ids = List(loadedEntries.map(entry => entry.slug));
|
||||
map.setIn(
|
||||
['pages', collection],
|
||||
Map({
|
||||
page: 1,
|
||||
ids,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case SORT_ENTRIES_FAILURE: {
|
||||
const payload = action.payload as EntriesSortFailurePayload;
|
||||
const { collection, key } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.deleteIn(['sort', collection, key]);
|
||||
map.setIn(['pages', collection, 'isFetching'], false);
|
||||
});
|
||||
persistSort(newState.get('sort') as Sort);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_REQUEST: {
|
||||
const payload = action.payload as EntriesFilterRequestPayload;
|
||||
const { collection, filter } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
const current: FilterMap = map.getIn(['filter', collection, filter.id], fromJS(filter));
|
||||
map.setIn(
|
||||
['filter', collection, current.get('id')],
|
||||
current.set('active', !current.get('active')),
|
||||
);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_FAILURE: {
|
||||
const payload = action.payload as EntriesFilterFailurePayload;
|
||||
const { collection, filter } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.deleteIn(['filter', collection, filter.id]);
|
||||
map.setIn(['pages', collection, 'isFetching'], false);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_REQUEST: {
|
||||
const payload = action.payload as EntriesGroupRequestPayload;
|
||||
const { collection, group } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
const current: GroupMap = map.getIn(['group', collection, group.id], fromJS(group));
|
||||
map.deleteIn(['group', collection]);
|
||||
map.setIn(
|
||||
['group', collection, current.get('id')],
|
||||
current.set('active', !current.get('active')),
|
||||
);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_FAILURE: {
|
||||
const payload = action.payload as EntriesGroupFailurePayload;
|
||||
const { collection, group } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.deleteIn(['group', collection, group.id]);
|
||||
map.setIn(['pages', collection, 'isFetching'], false);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case CHANGE_VIEW_STYLE: {
|
||||
const payload = action.payload as unknown as ChangeViewStylePayload;
|
||||
const { style } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.setIn(['viewStyle'], style);
|
||||
});
|
||||
persistViewStyle(newState.get('viewStyle') as string);
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectEntriesSort(entries: Entries, collection: string) {
|
||||
const sort = entries.get('sort') as Sort | undefined;
|
||||
return sort?.get(collection);
|
||||
}
|
||||
|
||||
export function selectEntriesFilter(entries: Entries, collection: string) {
|
||||
const filter = entries.get('filter') as Filter | undefined;
|
||||
return filter?.get(collection) || Map();
|
||||
}
|
||||
|
||||
export function selectEntriesGroup(entries: Entries, collection: string) {
|
||||
const group = entries.get('group') as Group | undefined;
|
||||
return group?.get(collection) || Map();
|
||||
}
|
||||
|
||||
export function selectEntriesGroupField(entries: Entries, collection: string) {
|
||||
const groups = selectEntriesGroup(entries, collection);
|
||||
const value = groups?.valueSeq().find(v => v?.get('active') === true);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function selectEntriesSortFields(entries: Entries, collection: string) {
|
||||
const sort = selectEntriesSort(entries, collection);
|
||||
const values =
|
||||
sort
|
||||
?.valueSeq()
|
||||
.filter(v => v?.get('direction') !== SortDirection.None)
|
||||
.toArray() || [];
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function selectEntriesFilterFields(entries: Entries, collection: string) {
|
||||
const filter = selectEntriesFilter(entries, collection);
|
||||
const values =
|
||||
filter
|
||||
?.valueSeq()
|
||||
.filter(v => v?.get('active') === true)
|
||||
.toArray() || [];
|
||||
return values;
|
||||
}
|
||||
|
||||
export function selectViewStyle(entries: Entries) {
|
||||
return entries.get('viewStyle');
|
||||
}
|
||||
|
||||
export function selectEntry(state: Entries, collection: string, slug: string) {
|
||||
return state.getIn(['entities', `${collection}.${slug}`]);
|
||||
}
|
||||
|
||||
export function selectPublishedSlugs(state: Entries, collection: string) {
|
||||
return state.getIn(['pages', collection, 'ids'], List<string>());
|
||||
}
|
||||
|
||||
function getPublishedEntries(state: Entries, collectionName: string) {
|
||||
const slugs = selectPublishedSlugs(state, collectionName);
|
||||
const entries =
|
||||
slugs &&
|
||||
(slugs.map(slug => selectEntry(state, collectionName, slug as string)) as List<EntryMap>);
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function selectEntries(state: Entries, collection: Collection) {
|
||||
const collectionName = collection.get('name');
|
||||
let entries = getPublishedEntries(state, collectionName);
|
||||
|
||||
const sortFields = selectEntriesSortFields(state, collectionName);
|
||||
if (sortFields && sortFields.length > 0) {
|
||||
const keys = sortFields.map(v => selectSortDataPath(collection, v.get('key')));
|
||||
const orders = sortFields.map(v =>
|
||||
v.get('direction') === SortDirection.Ascending ? 'asc' : 'desc',
|
||||
);
|
||||
entries = fromJS(orderBy(entries.toJS(), keys, orders));
|
||||
}
|
||||
|
||||
const filters = selectEntriesFilterFields(state, collectionName);
|
||||
if (filters && filters.length > 0) {
|
||||
entries = entries
|
||||
.filter(e => {
|
||||
const allMatched = filters.every(f => {
|
||||
const pattern = f.get('pattern');
|
||||
const field = f.get('field');
|
||||
const data = e!.get('data') || Map();
|
||||
const toMatch = data.getIn(keyToPathArray(field));
|
||||
const matched =
|
||||
toMatch !== undefined && new RegExp(String(pattern)).test(String(toMatch));
|
||||
return matched;
|
||||
});
|
||||
return allMatched;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function getGroup(entry: EntryMap, selectedGroup: GroupMap) {
|
||||
const label = selectedGroup.get('label');
|
||||
const field = selectedGroup.get('field');
|
||||
|
||||
const fieldData = entry.getIn(['data', ...keyToPathArray(field)]);
|
||||
if (fieldData === undefined) {
|
||||
return {
|
||||
id: 'missing_value',
|
||||
label,
|
||||
value: fieldData,
|
||||
};
|
||||
}
|
||||
|
||||
const dataAsString = String(fieldData);
|
||||
if (selectedGroup.has('pattern')) {
|
||||
const pattern = selectedGroup.get('pattern');
|
||||
let value = '';
|
||||
try {
|
||||
const regex = new RegExp(pattern);
|
||||
const matched = dataAsString.match(regex);
|
||||
if (matched) {
|
||||
value = matched[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Invalid view group pattern '${pattern}' for field '${field}'`, e);
|
||||
}
|
||||
return {
|
||||
id: `${label}${value}`,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${label}${fieldData}`,
|
||||
label,
|
||||
value: typeof fieldData === 'boolean' ? fieldData : dataAsString,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectGroups(state: Entries, collection: Collection) {
|
||||
const collectionName = collection.get('name');
|
||||
const entries = getPublishedEntries(state, collectionName);
|
||||
|
||||
const selectedGroup = selectEntriesGroupField(state, collectionName);
|
||||
if (selectedGroup === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let groups: Record<string, { id: string; label: string; value: string | boolean | undefined }> =
|
||||
{};
|
||||
const groupedEntries = groupBy(entries.toArray(), entry => {
|
||||
const group = getGroup(entry, selectedGroup);
|
||||
groups = { ...groups, [group.id]: group };
|
||||
return group.id;
|
||||
});
|
||||
|
||||
const groupsArray: GroupOfEntries[] = Object.entries(groupedEntries).map(([id, entries]) => {
|
||||
return {
|
||||
...groups[id],
|
||||
paths: Set(entries.map(entry => entry.get('path'))),
|
||||
};
|
||||
});
|
||||
|
||||
return groupsArray;
|
||||
}
|
||||
|
||||
export function selectEntryByPath(state: Entries, collection: string, path: string) {
|
||||
const slugs = selectPublishedSlugs(state, collection);
|
||||
const entries =
|
||||
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as List<EntryMap>);
|
||||
|
||||
return entries && entries.find(e => e?.get('path') === path);
|
||||
}
|
||||
|
||||
export function selectEntriesLoaded(state: Entries, collection: string) {
|
||||
return !!state.getIn(['pages', collection]);
|
||||
}
|
||||
|
||||
export function selectIsFetching(state: Entries, collection: string) {
|
||||
return state.getIn(['pages', collection, 'isFetching'], false);
|
||||
}
|
||||
|
||||
const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
|
||||
|
||||
function getFileField(collectionFiles: CollectionFiles, slug: string | undefined) {
|
||||
const file = collectionFiles.find(f => f?.get('name') === slug);
|
||||
return file;
|
||||
}
|
||||
|
||||
function hasCustomFolder(
|
||||
folderKey: 'media_folder' | 'public_folder',
|
||||
collection: Collection | null,
|
||||
slug: string | undefined,
|
||||
field: EntryField | undefined,
|
||||
) {
|
||||
if (!collection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field && field.has(folderKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.has('files')) {
|
||||
const file = getFileField(collection.get('files')!, slug);
|
||||
if (file && file.has(folderKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.has(folderKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function traverseFields(
|
||||
folderKey: 'media_folder' | 'public_folder',
|
||||
config: CmsConfig,
|
||||
collection: Collection,
|
||||
entryMap: EntryMap | undefined,
|
||||
field: EntryField,
|
||||
fields: EntryField[],
|
||||
currentFolder: string,
|
||||
): string | null {
|
||||
const matchedField = fields.filter(f => f === field)[0];
|
||||
if (matchedField) {
|
||||
return folderFormatter(
|
||||
matchedField.has(folderKey) ? matchedField.get(folderKey)! : `{{${folderKey}}}`,
|
||||
entryMap,
|
||||
collection,
|
||||
currentFolder,
|
||||
folderKey,
|
||||
config.slug,
|
||||
);
|
||||
}
|
||||
|
||||
for (let f of fields) {
|
||||
if (!f.has(folderKey)) {
|
||||
// add identity template if doesn't exist
|
||||
f = f.set(folderKey, `{{${folderKey}}}`);
|
||||
}
|
||||
const folder = folderFormatter(
|
||||
f.get(folderKey)!,
|
||||
entryMap,
|
||||
collection,
|
||||
currentFolder,
|
||||
folderKey,
|
||||
config.slug,
|
||||
);
|
||||
let fieldFolder = null;
|
||||
if (f.has('fields')) {
|
||||
fieldFolder = traverseFields(
|
||||
folderKey,
|
||||
config,
|
||||
collection,
|
||||
entryMap,
|
||||
field,
|
||||
f.get('fields')!.toArray(),
|
||||
folder,
|
||||
);
|
||||
} else if (f.has('field')) {
|
||||
fieldFolder = traverseFields(
|
||||
folderKey,
|
||||
config,
|
||||
collection,
|
||||
entryMap,
|
||||
field,
|
||||
[f.get('field')!],
|
||||
folder,
|
||||
);
|
||||
} else if (f.has('types')) {
|
||||
fieldFolder = traverseFields(
|
||||
folderKey,
|
||||
config,
|
||||
collection,
|
||||
entryMap,
|
||||
field,
|
||||
f.get('types')!.toArray(),
|
||||
folder,
|
||||
);
|
||||
}
|
||||
if (fieldFolder != null) {
|
||||
return fieldFolder;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function evaluateFolder(
|
||||
folderKey: 'media_folder' | 'public_folder',
|
||||
config: CmsConfig,
|
||||
collection: Collection,
|
||||
entryMap: EntryMap | undefined,
|
||||
field: EntryField | undefined,
|
||||
) {
|
||||
let currentFolder = config[folderKey]!;
|
||||
|
||||
// add identity template if doesn't exist
|
||||
if (!collection.has(folderKey)) {
|
||||
collection = collection.set(folderKey, `{{${folderKey}}}`);
|
||||
}
|
||||
|
||||
if (collection.has('files')) {
|
||||
// files collection evaluate the collection template
|
||||
// then move on to the specific file configuration denoted by the slug
|
||||
currentFolder = folderFormatter(
|
||||
collection.get(folderKey)!,
|
||||
entryMap,
|
||||
collection,
|
||||
currentFolder,
|
||||
folderKey,
|
||||
config.slug,
|
||||
);
|
||||
|
||||
let file = getFileField(collection.get('files')!, entryMap?.get('slug'));
|
||||
if (file) {
|
||||
if (!file.has(folderKey)) {
|
||||
// add identity template if doesn't exist
|
||||
file = file.set(folderKey, `{{${folderKey}}}`);
|
||||
}
|
||||
|
||||
// evaluate the file template and keep evaluating until we match our field
|
||||
currentFolder = folderFormatter(
|
||||
file.get(folderKey)!,
|
||||
entryMap,
|
||||
collection,
|
||||
currentFolder,
|
||||
folderKey,
|
||||
config.slug,
|
||||
);
|
||||
|
||||
if (field) {
|
||||
const fieldFolder = traverseFields(
|
||||
folderKey,
|
||||
config,
|
||||
collection,
|
||||
entryMap,
|
||||
field,
|
||||
file.get('fields')!.toArray(),
|
||||
currentFolder,
|
||||
);
|
||||
|
||||
if (fieldFolder !== null) {
|
||||
currentFolder = fieldFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// folder collection, evaluate the collection template
|
||||
// and keep evaluating until we match our field
|
||||
currentFolder = folderFormatter(
|
||||
collection.get(folderKey)!,
|
||||
entryMap,
|
||||
collection,
|
||||
currentFolder,
|
||||
folderKey,
|
||||
config.slug,
|
||||
);
|
||||
|
||||
if (field) {
|
||||
const fieldFolder = traverseFields(
|
||||
folderKey,
|
||||
config,
|
||||
collection,
|
||||
entryMap,
|
||||
field,
|
||||
collection.get('fields')!.toArray(),
|
||||
currentFolder,
|
||||
);
|
||||
|
||||
if (fieldFolder !== null) {
|
||||
currentFolder = fieldFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentFolder;
|
||||
}
|
||||
|
||||
export function selectMediaFolder(
|
||||
config: CmsConfig,
|
||||
collection: Collection | null,
|
||||
entryMap: EntryMap | undefined,
|
||||
field: EntryField | undefined,
|
||||
) {
|
||||
const name = 'media_folder';
|
||||
let mediaFolder = config[name];
|
||||
|
||||
const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);
|
||||
|
||||
if (customFolder) {
|
||||
const folder = evaluateFolder(name, config, collection!, entryMap, field);
|
||||
if (folder.startsWith('/')) {
|
||||
// return absolute paths as is
|
||||
mediaFolder = join(folder);
|
||||
} else {
|
||||
const entryPath = entryMap?.get('path');
|
||||
mediaFolder = entryPath
|
||||
? join(dirname(entryPath), folder)
|
||||
: join(collection!.get('folder') as string, DRAFT_MEDIA_FILES);
|
||||
}
|
||||
}
|
||||
|
||||
return trim(mediaFolder, '/');
|
||||
}
|
||||
|
||||
export function selectMediaFilePath(
|
||||
config: CmsConfig,
|
||||
collection: Collection | null,
|
||||
entryMap: EntryMap | undefined,
|
||||
mediaPath: string,
|
||||
field: EntryField | undefined,
|
||||
) {
|
||||
if (isAbsolutePath(mediaPath)) {
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
const mediaFolder = selectMediaFolder(config, collection, entryMap, field);
|
||||
|
||||
return join(mediaFolder, basename(mediaPath));
|
||||
}
|
||||
|
||||
export function selectMediaFilePublicPath(
|
||||
config: CmsConfig,
|
||||
collection: Collection | null,
|
||||
mediaPath: string,
|
||||
entryMap: EntryMap | undefined,
|
||||
field: EntryField | undefined,
|
||||
) {
|
||||
if (isAbsolutePath(mediaPath)) {
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
const name = 'public_folder';
|
||||
let publicFolder = config[name]!;
|
||||
|
||||
const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);
|
||||
|
||||
if (customFolder) {
|
||||
publicFolder = evaluateFolder(name, config, collection!, entryMap, field);
|
||||
}
|
||||
|
||||
if (isAbsolutePath(publicFolder)) {
|
||||
return joinUrlPath(publicFolder, basename(mediaPath));
|
||||
}
|
||||
|
||||
return join(publicFolder, basename(mediaPath));
|
||||
}
|
||||
|
||||
export function selectEditingDraft(state: EntryDraft) {
|
||||
const entry = state.get('entry');
|
||||
const workflowDraft = entry && !entry.isEmpty();
|
||||
return workflowDraft;
|
||||
}
|
||||
|
||||
export default entries;
|
||||
218
node_modules/decap-cms-core/src/reducers/entryDraft.js
generated
vendored
Normal file
218
node_modules/decap-cms-core/src/reducers/entryDraft.js
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { get } from 'lodash';
|
||||
import { join } from 'path';
|
||||
|
||||
import {
|
||||
DRAFT_CREATE_FROM_ENTRY,
|
||||
DRAFT_CREATE_EMPTY,
|
||||
DRAFT_DISCARD,
|
||||
DRAFT_CHANGE_FIELD,
|
||||
DRAFT_VALIDATION_ERRORS,
|
||||
DRAFT_CLEAR_ERRORS,
|
||||
DRAFT_LOCAL_BACKUP_RETRIEVED,
|
||||
DRAFT_CREATE_FROM_LOCAL_BACKUP,
|
||||
DRAFT_CREATE_DUPLICATE_FROM_ENTRY,
|
||||
ENTRY_PERSIST_REQUEST,
|
||||
ENTRY_PERSIST_SUCCESS,
|
||||
ENTRY_PERSIST_FAILURE,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
ADD_DRAFT_ENTRY_MEDIA_FILE,
|
||||
REMOVE_DRAFT_ENTRY_MEDIA_FILE,
|
||||
} from '../actions/entries';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { selectFolderEntryExtension, selectHasMetaPath } from './collections';
|
||||
import { getDataPath, duplicateI18nFields } from '../lib/i18n';
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
|
||||
function entryDraftReducer(state = Map(), action) {
|
||||
switch (action.type) {
|
||||
case DRAFT_CREATE_FROM_ENTRY:
|
||||
// Existing Entry
|
||||
return state.withMutations(state => {
|
||||
state.set('entry', fromJS(action.payload.entry));
|
||||
state.setIn(['entry', 'newRecord'], false);
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', false);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_EMPTY:
|
||||
// New Entry
|
||||
return state.withMutations(state => {
|
||||
state.set('entry', fromJS(action.payload));
|
||||
state.setIn(['entry', 'newRecord'], true);
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', false);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_FROM_LOCAL_BACKUP:
|
||||
// Local Backup
|
||||
return state.withMutations(state => {
|
||||
const backupDraftEntry = state.get('localBackup');
|
||||
const backupEntry = backupDraftEntry.get('entry');
|
||||
state.delete('localBackup');
|
||||
state.set('entry', backupEntry);
|
||||
state.setIn(['entry', 'newRecord'], !backupEntry.get('path'));
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', true);
|
||||
state.set('key', uuid());
|
||||
});
|
||||
case DRAFT_CREATE_DUPLICATE_FROM_ENTRY:
|
||||
// Duplicate Entry
|
||||
return state.withMutations(state => {
|
||||
state.set('entry', fromJS(action.payload));
|
||||
state.setIn(['entry', 'newRecord'], true);
|
||||
state.set('mediaFiles', List());
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', true);
|
||||
});
|
||||
case DRAFT_DISCARD:
|
||||
return initialState;
|
||||
case DRAFT_LOCAL_BACKUP_RETRIEVED: {
|
||||
const { entry } = action.payload;
|
||||
const newState = new Map({
|
||||
entry: fromJS(entry),
|
||||
});
|
||||
return state.set('localBackup', newState);
|
||||
}
|
||||
case DRAFT_CHANGE_FIELD: {
|
||||
return state.withMutations(state => {
|
||||
const { field, value, metadata, entries, i18n } = action.payload;
|
||||
const name = field.get('name');
|
||||
const meta = field.get('meta');
|
||||
|
||||
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
if (meta) {
|
||||
state.setIn(['entry', 'meta', name], value);
|
||||
} else {
|
||||
state.setIn(['entry', ...dataPath, name], value);
|
||||
if (i18n) {
|
||||
state = duplicateI18nFields(state, field, i18n.locales, i18n.defaultLocale);
|
||||
}
|
||||
}
|
||||
state.mergeDeepIn(['fieldsMetaData'], fromJS(metadata));
|
||||
const newData = state.getIn(['entry', ...dataPath]);
|
||||
const newMeta = state.getIn(['entry', 'meta']);
|
||||
state.set(
|
||||
'hasChanged',
|
||||
!entries.some(e => newData.equals(e.get(...dataPath))) ||
|
||||
!entries.some(e => newMeta.equals(e.get('meta'))),
|
||||
);
|
||||
});
|
||||
}
|
||||
case DRAFT_VALIDATION_ERRORS:
|
||||
if (action.payload.errors.length === 0) {
|
||||
return state.deleteIn(['fieldsErrors', action.payload.uniquefieldId]);
|
||||
} else {
|
||||
return state.setIn(['fieldsErrors', action.payload.uniquefieldId], action.payload.errors);
|
||||
}
|
||||
|
||||
case DRAFT_CLEAR_ERRORS: {
|
||||
return state.set('fieldsErrors', Map());
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_REQUEST:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
|
||||
return state.setIn(['entry', 'isPersisting'], true);
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_FAILURE:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_FAILURE: {
|
||||
return state.deleteIn(['entry', 'isPersisting']);
|
||||
}
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST:
|
||||
return state.setIn(['entry', 'isUpdatingStatus'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE:
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
return state.deleteIn(['entry', 'isUpdatingStatus']);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
|
||||
return state.setIn(['entry', 'isPublishing'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_FAILURE:
|
||||
return state.deleteIn(['entry', 'isPublishing']);
|
||||
|
||||
case ENTRY_PERSIST_SUCCESS:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_SUCCESS:
|
||||
return state.withMutations(state => {
|
||||
state.deleteIn(['entry', 'isPersisting']);
|
||||
state.set('hasChanged', false);
|
||||
if (!state.getIn(['entry', 'slug'])) {
|
||||
state.setIn(['entry', 'slug'], action.payload.slug);
|
||||
}
|
||||
});
|
||||
|
||||
case ENTRY_DELETE_SUCCESS:
|
||||
return state.withMutations(state => {
|
||||
state.deleteIn(['entry', 'isPersisting']);
|
||||
state.set('hasChanged', false);
|
||||
});
|
||||
|
||||
case ADD_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
return state.withMutations(state => {
|
||||
const mediaFiles = state.getIn(['entry', 'mediaFiles']);
|
||||
|
||||
state.setIn(
|
||||
['entry', 'mediaFiles'],
|
||||
mediaFiles
|
||||
.filterNot(file => file.get('id') === action.payload.id)
|
||||
.insert(0, fromJS(action.payload)),
|
||||
);
|
||||
state.set('hasChanged', true);
|
||||
});
|
||||
}
|
||||
|
||||
case REMOVE_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
return state.withMutations(state => {
|
||||
const mediaFiles = state.getIn(['entry', 'mediaFiles']);
|
||||
|
||||
state.setIn(
|
||||
['entry', 'mediaFiles'],
|
||||
mediaFiles.filterNot(file => file.get('id') === action.payload.id),
|
||||
);
|
||||
state.set('hasChanged', true);
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectCustomPath(collection, entryDraft) {
|
||||
if (!selectHasMetaPath(collection)) {
|
||||
return;
|
||||
}
|
||||
const meta = entryDraft.getIn(['entry', 'meta']);
|
||||
const path = meta && meta.get('path');
|
||||
const indexFile = get(collection.toJS(), ['meta', 'path', 'index_file']);
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
const customPath = path && join(collection.get('folder'), path, `${indexFile}.${extension}`);
|
||||
return customPath;
|
||||
}
|
||||
|
||||
export default entryDraftReducer;
|
||||
45
node_modules/decap-cms-core/src/reducers/globalUI.ts
generated
vendored
Normal file
45
node_modules/decap-cms-core/src/reducers/globalUI.ts
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { USE_OPEN_AUTHORING } from '../actions/auth';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
export type GlobalUI = {
|
||||
isFetching: boolean;
|
||||
useOpenAuthoring: boolean;
|
||||
};
|
||||
|
||||
const LOADING_IGNORE_LIST = [
|
||||
'DEPLOY_PREVIEW',
|
||||
'STATUS_REQUEST',
|
||||
'STATUS_SUCCESS',
|
||||
'STATUS_FAILURE',
|
||||
];
|
||||
|
||||
function ignoreWhenLoading(action: AnyAction) {
|
||||
return LOADING_IGNORE_LIST.some(type => action.type.includes(type));
|
||||
}
|
||||
|
||||
const defaultState: GlobalUI = {
|
||||
isFetching: false,
|
||||
useOpenAuthoring: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for some global UI state that we want to share between components
|
||||
*/
|
||||
const globalUI = produce((state: GlobalUI, action: AnyAction) => {
|
||||
// Generic, global loading indicator
|
||||
if (!ignoreWhenLoading(action) && action.type.includes('REQUEST')) {
|
||||
state.isFetching = true;
|
||||
} else if (
|
||||
!ignoreWhenLoading(action) &&
|
||||
(action.type.includes('SUCCESS') || action.type.includes('FAILURE'))
|
||||
) {
|
||||
state.isFetching = false;
|
||||
} else if (action.type === USE_OPEN_AUTHORING) {
|
||||
state.useOpenAuthoring = true;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default globalUI;
|
||||
82
node_modules/decap-cms-core/src/reducers/index.ts
generated
vendored
Normal file
82
node_modules/decap-cms-core/src/reducers/index.ts
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import { List } from 'immutable';
|
||||
|
||||
import auth from './auth';
|
||||
import config from './config';
|
||||
import integrations, * as fromIntegrations from './integrations';
|
||||
import entries, * as fromEntries from './entries';
|
||||
import cursors from './cursors';
|
||||
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||
import entryDraft from './entryDraft';
|
||||
import collections from './collections';
|
||||
import search from './search';
|
||||
import medias from './medias';
|
||||
import mediaLibrary from './mediaLibrary';
|
||||
import deploys, * as fromDeploys from './deploys';
|
||||
import globalUI from './globalUI';
|
||||
import status from './status';
|
||||
import notifications from './notifications';
|
||||
|
||||
import type { Status } from '../constants/publishModes';
|
||||
import type { State, Collection } from '../types/redux';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
config,
|
||||
collections,
|
||||
search,
|
||||
integrations,
|
||||
entries,
|
||||
cursors,
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
medias,
|
||||
mediaLibrary,
|
||||
deploys,
|
||||
globalUI,
|
||||
status,
|
||||
notifications,
|
||||
};
|
||||
|
||||
export default reducers;
|
||||
|
||||
/*
|
||||
* Selectors
|
||||
*/
|
||||
export function selectEntry(state: State, collection: string, slug: string) {
|
||||
return fromEntries.selectEntry(state.entries, collection, slug);
|
||||
}
|
||||
|
||||
export function selectEntries(state: State, collection: Collection) {
|
||||
return fromEntries.selectEntries(state.entries, collection);
|
||||
}
|
||||
|
||||
export function selectPublishedSlugs(state: State, collection: string) {
|
||||
return fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||
}
|
||||
|
||||
export function selectSearchedEntries(state: State, availableCollections: string[]) {
|
||||
// only return search results for actually available collections
|
||||
return List(state.search.entryIds)
|
||||
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
|
||||
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
|
||||
}
|
||||
|
||||
export function selectDeployPreview(state: State, collection: string, slug: string) {
|
||||
return fromDeploys.selectDeployPreview(state.deploys, collection, slug);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntry(state: State, collection: string, slug: string) {
|
||||
return fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, collection, slug);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntriesByStatus(state: State, status: Status) {
|
||||
return fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
||||
}
|
||||
|
||||
export function selectUnpublishedSlugs(state: State, collection: string) {
|
||||
return fromEditorialWorkflow.selectUnpublishedSlugs(state.editorialWorkflow, collection);
|
||||
}
|
||||
|
||||
export function selectIntegration(state: State, collection: string | null, hook: string) {
|
||||
return fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||
}
|
||||
59
node_modules/decap-cms-core/src/reducers/integrations.ts
generated
vendored
Normal file
59
node_modules/decap-cms-core/src/reducers/integrations.ts
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Integrations, CmsConfig } from '../types/redux';
|
||||
|
||||
interface Acc {
|
||||
providers: Record<string, {}>;
|
||||
hooks: Record<string, string | Record<string, string>>;
|
||||
}
|
||||
|
||||
export function getIntegrations(config: CmsConfig) {
|
||||
const integrations = config.integrations || [];
|
||||
const newState = integrations.reduce(
|
||||
(acc, integration) => {
|
||||
const { hooks, collections, provider, ...providerData } = integration;
|
||||
acc.providers[provider] = { ...providerData };
|
||||
if (!collections) {
|
||||
hooks.forEach(hook => {
|
||||
acc.hooks[hook] = provider;
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
const integrationCollections =
|
||||
collections === '*' ? config.collections.map(collection => collection.name) : collections;
|
||||
integrationCollections.forEach(collection => {
|
||||
hooks.forEach(hook => {
|
||||
acc.hooks[collection]
|
||||
? ((acc.hooks[collection] as Record<string, string>)[hook] = provider)
|
||||
: (acc.hooks[collection] = { [hook]: provider });
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ providers: {}, hooks: {} } as Acc,
|
||||
);
|
||||
return fromJS(newState);
|
||||
}
|
||||
|
||||
const defaultState = fromJS({ providers: {}, hooks: {} });
|
||||
|
||||
function integrations(state = defaultState, action: ConfigAction): Integrations | null {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
return getIntegrations(action.payload);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectIntegration(state: Integrations, collection: string | null, hook: string) {
|
||||
return collection
|
||||
? state.getIn(['hooks', collection, hook], false)
|
||||
: state.getIn(['hooks', hook], false);
|
||||
}
|
||||
|
||||
export default integrations;
|
||||
296
node_modules/decap-cms-core/src/reducers/mediaLibrary.ts
generated
vendored
Normal file
296
node_modules/decap-cms-core/src/reducers/mediaLibrary.ts
generated
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
import { Map, List } from 'immutable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { dirname } from 'path';
|
||||
|
||||
import {
|
||||
MEDIA_LIBRARY_OPEN,
|
||||
MEDIA_LIBRARY_CLOSE,
|
||||
MEDIA_LIBRARY_CREATE,
|
||||
MEDIA_INSERT,
|
||||
MEDIA_REMOVE_INSERTED,
|
||||
MEDIA_LOAD_REQUEST,
|
||||
MEDIA_LOAD_SUCCESS,
|
||||
MEDIA_LOAD_FAILURE,
|
||||
MEDIA_PERSIST_REQUEST,
|
||||
MEDIA_PERSIST_SUCCESS,
|
||||
MEDIA_PERSIST_FAILURE,
|
||||
MEDIA_DELETE_REQUEST,
|
||||
MEDIA_DELETE_SUCCESS,
|
||||
MEDIA_DELETE_FAILURE,
|
||||
MEDIA_DISPLAY_URL_REQUEST,
|
||||
MEDIA_DISPLAY_URL_SUCCESS,
|
||||
MEDIA_DISPLAY_URL_FAILURE,
|
||||
} from '../actions/mediaLibrary';
|
||||
import { selectEditingDraft, selectMediaFolder } from './entries';
|
||||
import { selectIntegration } from './';
|
||||
|
||||
import type { MediaLibraryAction } from '../actions/mediaLibrary';
|
||||
import type {
|
||||
State,
|
||||
MediaLibraryInstance,
|
||||
MediaFile,
|
||||
MediaFileMap,
|
||||
DisplayURLState,
|
||||
EntryField,
|
||||
} from '../types/redux';
|
||||
|
||||
const defaultState: {
|
||||
isVisible: boolean;
|
||||
showMediaButton: boolean;
|
||||
controlMedia: Map<string, string>;
|
||||
displayURLs: Map<string, string>;
|
||||
externalLibrary?: MediaLibraryInstance;
|
||||
controlID?: string;
|
||||
page?: number;
|
||||
files?: MediaFile[];
|
||||
config: Map<string, unknown>;
|
||||
field?: EntryField;
|
||||
value?: string | string[];
|
||||
replaceIndex?: number | boolean;
|
||||
} = {
|
||||
isVisible: false,
|
||||
showMediaButton: true,
|
||||
controlMedia: Map(),
|
||||
displayURLs: Map(),
|
||||
config: Map(),
|
||||
};
|
||||
|
||||
function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
|
||||
switch (action.type) {
|
||||
case MEDIA_LIBRARY_CREATE:
|
||||
return state.withMutations(map => {
|
||||
map.set('externalLibrary', action.payload);
|
||||
map.set('showMediaButton', action.payload.enableStandalone());
|
||||
});
|
||||
|
||||
case MEDIA_LIBRARY_OPEN: {
|
||||
const { controlID, forImage, privateUpload, config, field, value, replaceIndex } =
|
||||
action.payload;
|
||||
const libConfig = config || Map();
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return Map({
|
||||
isVisible: true,
|
||||
forImage,
|
||||
controlID,
|
||||
canInsert: !!controlID,
|
||||
privateUpload,
|
||||
config: libConfig,
|
||||
controlMedia: Map(),
|
||||
displayURLs: Map(),
|
||||
field,
|
||||
value,
|
||||
replaceIndex,
|
||||
});
|
||||
}
|
||||
return state.withMutations(map => {
|
||||
map.set('isVisible', true);
|
||||
map.set('forImage', forImage ?? false);
|
||||
map.set('controlID', controlID ?? '');
|
||||
map.set('canInsert', !!controlID);
|
||||
map.set('privateUpload', privateUpload);
|
||||
map.set('config', libConfig);
|
||||
map.set('field', field ?? '');
|
||||
map.set('value', value == '' && libConfig.get('multiple') ? [] : value ?? '');
|
||||
map.set('replaceIndex', replaceIndex ?? false);
|
||||
});
|
||||
}
|
||||
|
||||
case MEDIA_LIBRARY_CLOSE:
|
||||
return state.set('isVisible', false);
|
||||
|
||||
case MEDIA_INSERT: {
|
||||
const { mediaPath } = action.payload;
|
||||
const controlID = state.get('controlID');
|
||||
const value = state.get('value');
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['controlMedia', controlID], mediaPath);
|
||||
});
|
||||
}
|
||||
|
||||
const replaceIndex = state.get('replaceIndex');
|
||||
const mediaArray = Array.isArray(mediaPath) ? mediaPath : [mediaPath];
|
||||
const valueArray = value as string[];
|
||||
if (typeof replaceIndex == 'number') {
|
||||
valueArray[replaceIndex] = mediaArray[0];
|
||||
} else {
|
||||
valueArray.push(...mediaArray);
|
||||
}
|
||||
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['controlMedia', controlID], valueArray);
|
||||
});
|
||||
}
|
||||
|
||||
case MEDIA_REMOVE_INSERTED: {
|
||||
const controlID = action.payload.controlID;
|
||||
return state.setIn(['controlMedia', controlID], '');
|
||||
}
|
||||
|
||||
case MEDIA_LOAD_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isLoading', true);
|
||||
map.set('isPaginating', action.payload.page > 1);
|
||||
});
|
||||
|
||||
case MEDIA_LOAD_SUCCESS: {
|
||||
const {
|
||||
files = [],
|
||||
page,
|
||||
canPaginate,
|
||||
dynamicSearch,
|
||||
dynamicSearchQuery,
|
||||
privateUpload,
|
||||
} = action.payload;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const filesWithKeys = files.map(file => ({ ...file, key: uuid() }));
|
||||
return state.withMutations(map => {
|
||||
map.set('isLoading', false);
|
||||
map.set('isPaginating', false);
|
||||
map.set('page', page ?? 1);
|
||||
map.set('hasNextPage', !!(canPaginate && files.length > 0));
|
||||
map.set('dynamicSearch', dynamicSearch ?? false);
|
||||
map.set('dynamicSearchQuery', dynamicSearchQuery ?? '');
|
||||
map.set('dynamicSearchActive', !!dynamicSearchQuery);
|
||||
if (page && page > 1) {
|
||||
const updatedFiles = (map.get('files') as MediaFile[]).concat(filesWithKeys);
|
||||
map.set('files', updatedFiles);
|
||||
} else {
|
||||
map.set('files', filesWithKeys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case MEDIA_LOAD_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isLoading', false);
|
||||
}
|
||||
|
||||
case MEDIA_PERSIST_REQUEST:
|
||||
return state.set('isPersisting', true);
|
||||
|
||||
case MEDIA_PERSIST_SUCCESS: {
|
||||
const { file, privateUpload } = action.payload;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.withMutations(map => {
|
||||
const fileWithKey = { ...file, key: uuid() };
|
||||
const files = map.get('files') as MediaFile[];
|
||||
const updatedFiles = [fileWithKey, ...files];
|
||||
map.set('files', updatedFiles);
|
||||
map.set('isPersisting', false);
|
||||
});
|
||||
}
|
||||
|
||||
case MEDIA_PERSIST_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isPersisting', false);
|
||||
}
|
||||
|
||||
case MEDIA_DELETE_REQUEST:
|
||||
return state.set('isDeleting', true);
|
||||
|
||||
case MEDIA_DELETE_SUCCESS: {
|
||||
const { file, privateUpload } = action.payload;
|
||||
const { key, id } = file;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.withMutations(map => {
|
||||
const files = map.get('files') as MediaFile[];
|
||||
const updatedFiles = files.filter(file => (key ? file.key !== key : file.id !== id));
|
||||
map.set('files', updatedFiles);
|
||||
map.deleteIn(['displayURLs', id]);
|
||||
map.set('isDeleting', false);
|
||||
});
|
||||
}
|
||||
|
||||
case MEDIA_DELETE_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isDeleting', false);
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_REQUEST:
|
||||
return state.setIn(['displayURLs', action.payload.key, 'isFetching'], true);
|
||||
|
||||
case MEDIA_DISPLAY_URL_SUCCESS: {
|
||||
const displayURLPath = ['displayURLs', action.payload.key];
|
||||
return state
|
||||
.setIn([...displayURLPath, 'isFetching'], false)
|
||||
.setIn([...displayURLPath, 'url'], action.payload.url);
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_FAILURE: {
|
||||
const displayURLPath = ['displayURLs', action.payload.key];
|
||||
return (
|
||||
state
|
||||
.setIn([...displayURLPath, 'isFetching'], false)
|
||||
// make sure that err is set so the CMS won't attempt to load
|
||||
// the image again
|
||||
.setIn([...displayURLPath, 'err'], action.payload.err || true)
|
||||
.deleteIn([...displayURLPath, 'url'])
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectMediaFiles(state: State, field?: EntryField) {
|
||||
const { mediaLibrary, entryDraft } = state;
|
||||
const editingDraft = selectEditingDraft(state.entryDraft);
|
||||
const integration = selectIntegration(state, null, 'assetStore');
|
||||
|
||||
let files;
|
||||
if (editingDraft && !integration) {
|
||||
const entryFiles = entryDraft
|
||||
.getIn(['entry', 'mediaFiles'], List<MediaFileMap>())
|
||||
.toJS() as MediaFile[];
|
||||
const entry = entryDraft.get('entry');
|
||||
const collection = state.collections.get(entry?.get('collection'));
|
||||
const mediaFolder = selectMediaFolder(state.config, collection, entry, field);
|
||||
files = entryFiles
|
||||
.filter(f => dirname(f.path) === mediaFolder)
|
||||
.map(file => ({ key: file.id, ...file }));
|
||||
} else {
|
||||
files = mediaLibrary.get('files') || [];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function selectMediaFileByPath(state: State, path: string) {
|
||||
const files = selectMediaFiles(state);
|
||||
const file = files.find(file => file.path === path);
|
||||
return file;
|
||||
}
|
||||
|
||||
export function selectMediaDisplayURL(state: State, id: string) {
|
||||
const displayUrlState = state.mediaLibrary.getIn(
|
||||
['displayURLs', id],
|
||||
Map() as unknown as DisplayURLState,
|
||||
);
|
||||
return displayUrlState;
|
||||
}
|
||||
|
||||
export default mediaLibrary;
|
||||
66
node_modules/decap-cms-core/src/reducers/medias.ts
generated
vendored
Normal file
66
node_modules/decap-cms-core/src/reducers/medias.ts
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
ADD_ASSETS,
|
||||
ADD_ASSET,
|
||||
REMOVE_ASSET,
|
||||
LOAD_ASSET_REQUEST,
|
||||
LOAD_ASSET_SUCCESS,
|
||||
LOAD_ASSET_FAILURE,
|
||||
} from '../actions/media';
|
||||
|
||||
import type { MediasAction } from '../actions/media';
|
||||
import type AssetProxy from '../valueObjects/AssetProxy';
|
||||
|
||||
export type Medias = {
|
||||
[path: string]: { asset: AssetProxy | undefined; isLoading: boolean; error: Error | null };
|
||||
};
|
||||
|
||||
const defaultState: Medias = {};
|
||||
|
||||
const medias = produce((state: Medias, action: MediasAction) => {
|
||||
switch (action.type) {
|
||||
case ADD_ASSETS: {
|
||||
const assets = action.payload;
|
||||
assets.forEach(asset => {
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ADD_ASSET: {
|
||||
const asset = action.payload;
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
break;
|
||||
}
|
||||
case REMOVE_ASSET: {
|
||||
const path = action.payload;
|
||||
delete state[path];
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_REQUEST: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = true;
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_SUCCESS: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = null;
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_FAILURE: {
|
||||
const { path, error } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = error;
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectIsLoadingAsset(state: Medias) {
|
||||
return Object.values(state).some(state => state.isLoading);
|
||||
}
|
||||
|
||||
export default medias;
|
||||
52
node_modules/decap-cms-core/src/reducers/notifications.ts
generated
vendored
Normal file
52
node_modules/decap-cms-core/src/reducers/notifications.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import { produce } from 'immer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
NOTIFICATION_SEND,
|
||||
NOTIFICATION_DISMISS,
|
||||
NOTIFICATIONS_CLEAR,
|
||||
} from '../actions/notifications';
|
||||
|
||||
import type {
|
||||
NotificationsAction,
|
||||
NotificationMessage,
|
||||
NotificationPayload,
|
||||
} from '../actions/notifications';
|
||||
import type { TypeOptions } from 'react-toastify';
|
||||
|
||||
export type Notification = {
|
||||
id: string;
|
||||
message: string | NotificationMessage;
|
||||
dismissAfter?: number;
|
||||
type: TypeOptions | undefined;
|
||||
};
|
||||
|
||||
export type NotificationsState = {
|
||||
notifications: Notification[];
|
||||
};
|
||||
|
||||
const defaultState: NotificationsState = {
|
||||
notifications: [],
|
||||
};
|
||||
|
||||
const notifications = produce((state: NotificationsState, action: NotificationsAction) => {
|
||||
switch (action.type) {
|
||||
case NOTIFICATIONS_CLEAR:
|
||||
state.notifications = [];
|
||||
break;
|
||||
case NOTIFICATION_DISMISS:
|
||||
state.notifications = state.notifications.filter(n => n.id !== action.id);
|
||||
break;
|
||||
case NOTIFICATION_SEND:
|
||||
state.notifications = [
|
||||
...state.notifications,
|
||||
{
|
||||
id: uuid(),
|
||||
...(action.payload as NotificationPayload),
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default notifications;
|
||||
111
node_modules/decap-cms-core/src/reducers/search.ts
generated
vendored
Normal file
111
node_modules/decap-cms-core/src/reducers/search.ts
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
QUERY_FAILURE,
|
||||
QUERY_REQUEST,
|
||||
QUERY_SUCCESS,
|
||||
SEARCH_CLEAR,
|
||||
SEARCH_ENTRIES_FAILURE,
|
||||
SEARCH_ENTRIES_REQUEST,
|
||||
SEARCH_ENTRIES_SUCCESS,
|
||||
CLEAR_REQUESTS,
|
||||
} from '../actions/search';
|
||||
|
||||
import type { SearchAction } from '../actions/search';
|
||||
import type { EntryValue } from '../valueObjects/Entry';
|
||||
|
||||
export type Search = {
|
||||
isFetching: boolean;
|
||||
term: string;
|
||||
collections: string[];
|
||||
page: number;
|
||||
entryIds: { collection: string; slug: string }[];
|
||||
queryHits: Record<string, EntryValue[]>;
|
||||
error: Error | undefined;
|
||||
requests: QueryRequest[];
|
||||
};
|
||||
|
||||
type QueryResponse = {
|
||||
hits: EntryValue[];
|
||||
query: string;
|
||||
};
|
||||
|
||||
export type QueryRequest = {
|
||||
id: string;
|
||||
expires: Date;
|
||||
queryResponse: Promise<QueryResponse>;
|
||||
};
|
||||
|
||||
const defaultState: Search = {
|
||||
isFetching: false,
|
||||
term: '',
|
||||
collections: [],
|
||||
page: 0,
|
||||
entryIds: [],
|
||||
queryHits: {},
|
||||
error: undefined,
|
||||
requests: [],
|
||||
};
|
||||
|
||||
const search = produce((state: Search, action: SearchAction) => {
|
||||
switch (action.type) {
|
||||
case SEARCH_CLEAR:
|
||||
return defaultState;
|
||||
|
||||
case SEARCH_ENTRIES_REQUEST: {
|
||||
const { page, searchTerm, searchCollections } = action.payload;
|
||||
state.isFetching = true;
|
||||
state.term = searchTerm;
|
||||
state.collections = searchCollections;
|
||||
state.page = page;
|
||||
break;
|
||||
}
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS: {
|
||||
const { entries, page } = action.payload;
|
||||
const entryIds = entries.map(entry => ({ collection: entry.collection, slug: entry.slug }));
|
||||
state.isFetching = false;
|
||||
state.page = page;
|
||||
state.entryIds =
|
||||
!page || isNaN(page) || page === 0 ? entryIds : state.entryIds.concat(entryIds);
|
||||
break;
|
||||
}
|
||||
|
||||
case SEARCH_ENTRIES_FAILURE: {
|
||||
const { error } = action.payload;
|
||||
state.isFetching = false;
|
||||
state.error = error;
|
||||
break;
|
||||
}
|
||||
|
||||
case QUERY_REQUEST: {
|
||||
const { searchTerm, request } = action.payload;
|
||||
state.isFetching = true;
|
||||
state.term = searchTerm;
|
||||
if (request) {
|
||||
state.requests.push(request);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CLEAR_REQUESTS: {
|
||||
state.requests = state.requests.filter(req => req.expires >= new Date());
|
||||
break;
|
||||
}
|
||||
|
||||
case QUERY_SUCCESS: {
|
||||
const { namespace, hits } = action.payload;
|
||||
state.isFetching = false;
|
||||
state.queryHits[namespace] = hits;
|
||||
break;
|
||||
}
|
||||
|
||||
case QUERY_FAILURE: {
|
||||
const { error } = action.payload;
|
||||
state.isFetching = false;
|
||||
state.error = error;
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default search;
|
||||
40
node_modules/decap-cms-core/src/reducers/status.ts
generated
vendored
Normal file
40
node_modules/decap-cms-core/src/reducers/status.ts
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
|
||||
|
||||
import type { StatusAction } from '../actions/status';
|
||||
|
||||
export type Status = {
|
||||
isFetching: boolean;
|
||||
status: {
|
||||
auth: { status: boolean };
|
||||
api: { status: boolean; statusPage: string };
|
||||
};
|
||||
error: Error | undefined;
|
||||
};
|
||||
|
||||
const defaultState: Status = {
|
||||
isFetching: false,
|
||||
status: {
|
||||
auth: { status: true },
|
||||
api: { status: true, statusPage: '' },
|
||||
},
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const status = produce((state: Status, action: StatusAction) => {
|
||||
switch (action.type) {
|
||||
case STATUS_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
state.isFetching = false;
|
||||
state.status = action.payload.status;
|
||||
break;
|
||||
case STATUS_FAILURE:
|
||||
state.isFetching = false;
|
||||
state.error = action.payload.error;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default status;
|
||||
Reference in New Issue
Block a user