feat: bundle assets with content (#2958)
* fix(media_folder_relative): use collection name in unpublished entry * refactor: pass arguments as object to AssetProxy ctor * feat: support media folders per collection * feat: resolve media files path based on entry path * fix: asset public path resolving * refactor: introduce typescript for AssetProxy * refactor: code cleanup * refactor(asset-proxy): add tests,switch to typescript,extract arguments * refactor: typescript for editorialWorkflow * refactor: add typescript for media library actions * refactor: fix type error on map set * refactor: move locale selector into reducer * refactor: add typescript for entries actions * refactor: remove duplication between asset store and media lib * feat: load assets from backend using API * refactor(github): add typescript, cache media files * fix: don't load media URL if already loaded * feat: add media folder config to collection * fix: load assets from API when not in UI state * feat: load entry media files when opening media library * fix: editorial workflow draft media files bug fixes * test(unit): fix unit tests * fix: editor control losing focus * style: add eslint object-shorthand rule * test(cypress): re-record mock data * fix: fix non github backends, large media * test: uncomment only in tests * fix(backend-test): add missing displayURL property * test(e2e): add media library tests * test(e2e): enable visual testing * test(e2e): add github backend media library tests * test(e2e): add git-gateway large media tests * chore: post rebase fixes * test: fix tests * test: fix tests * test(cypress): fix tests * docs: add media_folder docs * test(e2e): add media library delete test * test(e2e): try and fix image comparison on CI * ci: reduce test machines from 9 to 8 * test: add reducers and selectors unit tests * test(e2e): disable visual regression testing for now * test: add getAsset unit tests * refactor: use Asset class component instead of hooks * build: don't inline source maps * test: add more media path tests
This commit is contained in:
committed by
Shawn Erquhart
parent
7e4d4c1cc4
commit
2b41d8a838
@ -1,6 +1,6 @@
|
||||
import { Map } from 'immutable';
|
||||
import { configLoaded, configLoading, configFailed } from 'Actions/config';
|
||||
import config from 'Reducers/config';
|
||||
import config, { selectLocale } from 'Reducers/config';
|
||||
|
||||
describe('config', () => {
|
||||
it('should handle an empty state', () => {
|
||||
@ -22,4 +22,8 @@ describe('config', () => {
|
||||
Map({ error: 'Error: Config could not be loaded' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to "en" locale', () => {
|
||||
expect(selectLocale(Map())).toEqual('en');
|
||||
});
|
||||
});
|
||||
|
@ -1,67 +1,215 @@
|
||||
import { Map, OrderedMap, fromJS } from 'immutable';
|
||||
import * as actions from 'Actions/entries';
|
||||
import reducer from '../entries';
|
||||
import reducer, {
|
||||
selectMediaFolder,
|
||||
selectMediaFilePath,
|
||||
selectMediaFilePublicPath,
|
||||
} from '../entries';
|
||||
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
|
||||
|
||||
const initialState = OrderedMap({
|
||||
posts: Map({ name: 'posts' }),
|
||||
});
|
||||
|
||||
describe('entries', () => {
|
||||
it('should mark entries as fetching', () => {
|
||||
expect(reducer(initialState, actions.entriesLoading(Map({ name: 'posts' })))).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
pages: {
|
||||
posts: { isFetching: true },
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
describe('reducer', () => {
|
||||
it('should mark entries as fetching', () => {
|
||||
expect(reducer(initialState, actions.entriesLoading(Map({ 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(Map({ 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(Map({ name: 'posts' }), entry))).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
entities: {
|
||||
'posts.a': { slug: 'a', path: '' },
|
||||
},
|
||||
pages: {
|
||||
posts: {
|
||||
ids: ['a'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle loaded entries', () => {
|
||||
const entries = [
|
||||
{ slug: 'a', path: '' },
|
||||
{ slug: 'b', title: 'B' },
|
||||
];
|
||||
expect(
|
||||
reducer(initialState, actions.entriesLoaded(Map({ 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'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
describe('selectMediaFolder', () => {
|
||||
it('should return global media folder when not in editorial workflow', () => {
|
||||
expect(selectMediaFolder(Map({ media_folder: 'static/media' }))).toEqual('static/media');
|
||||
});
|
||||
|
||||
it("should return global media folder when in editorial workflow and collection doesn't specify media_folder", () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts' }),
|
||||
),
|
||||
).toEqual('static/media');
|
||||
});
|
||||
|
||||
it('should return draft media folder when in editorial workflow, collection specifies media_folder and entry path is null', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
null,
|
||||
),
|
||||
).toEqual('posts/DRAFT_MEDIA_FILES');
|
||||
});
|
||||
|
||||
it('should return relative media folder when in editorial workflow, collection specifies media_folder and entry path is not null', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
'posts/title/index.md',
|
||||
),
|
||||
).toEqual('posts/title');
|
||||
});
|
||||
|
||||
it('should resolve relative media folder', () => {
|
||||
expect(
|
||||
selectMediaFolder(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '../' }),
|
||||
'posts/title/index.md',
|
||||
),
|
||||
).toEqual('posts/');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle loaded entry', () => {
|
||||
const entry = { slug: 'a', path: '' };
|
||||
expect(reducer(initialState, actions.entryLoaded(Map({ name: 'posts' }), entry))).toEqual(
|
||||
OrderedMap(
|
||||
fromJS({
|
||||
posts: { name: 'posts' },
|
||||
entities: {
|
||||
'posts.a': { slug: 'a', path: '' },
|
||||
},
|
||||
pages: {
|
||||
posts: {
|
||||
ids: ['a'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
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 when absolute path', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
null,
|
||||
null,
|
||||
'/media/image.png',
|
||||
),
|
||||
).toBe('static/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from global media folder when relative path for collection with no media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts' }),
|
||||
null,
|
||||
'image.png',
|
||||
),
|
||||
).toBe('static/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from collection media folder when relative path for collection with media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
null,
|
||||
'image.png',
|
||||
),
|
||||
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
|
||||
});
|
||||
|
||||
it('should handle relative media_folder', () => {
|
||||
expect(
|
||||
selectMediaFilePath(
|
||||
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
|
||||
'posts/title/index.md',
|
||||
'image.png',
|
||||
),
|
||||
).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 when not in editorial workflow', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(Map({ public_folder: '/media' }), null, '/media/image.png'),
|
||||
).toBe('/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from public folder when in editorial workflow for collection with no media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts' }),
|
||||
'image.png',
|
||||
),
|
||||
).toBe('/media/image.png');
|
||||
});
|
||||
|
||||
it('should resolve path from collection media folder when in editorial workflow for collection with media folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||
'image.png',
|
||||
),
|
||||
).toBe('image.png');
|
||||
});
|
||||
|
||||
it('should handle relative media_folder', () => {
|
||||
expect(
|
||||
selectMediaFilePublicPath(
|
||||
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
|
||||
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
|
||||
'image.png',
|
||||
),
|
||||
).toBe('../../static/media/image.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import * as actions from 'Actions/entries';
|
||||
import reducer from '../entryDraft';
|
||||
|
||||
@ -6,7 +6,6 @@ jest.mock('uuid/v4', () => jest.fn(() => '1'));
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
mediaFiles: List(),
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
@ -33,7 +32,6 @@ describe('entryDraft reducer', () => {
|
||||
...entry,
|
||||
newRecord: false,
|
||||
},
|
||||
mediaFiles: [],
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
@ -52,7 +50,6 @@ describe('entryDraft reducer', () => {
|
||||
...entry,
|
||||
newRecord: true,
|
||||
},
|
||||
mediaFiles: [],
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
@ -124,16 +121,15 @@ describe('entryDraft reducer', () => {
|
||||
describe('REMOVE_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should remove a media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.set('mediaFiles', List([{ id: '1' }, { id: '2' }])),
|
||||
initialState.setIn(['entry', 'mediaFiles'], fromJS([{ id: '1' }, { id: '2' }])),
|
||||
actions.removeDraftEntryMediaFile({ id: '1' }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
mediaFiles: [{ id: '2' }],
|
||||
entry: { mediaFiles: [{ id: '2' }] },
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
hasChanged: true,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
@ -142,34 +138,15 @@ describe('entryDraft reducer', () => {
|
||||
describe('ADD_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should overwrite an existing media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.set('mediaFiles', List([{ id: '1', name: 'old' }])),
|
||||
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' }],
|
||||
entry: { mediaFiles: [{ id: '1', name: 'new' }] },
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_DRAFT_ENTRY_MEDIA_FILES', () => {
|
||||
it('should overwrite an existing media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState,
|
||||
actions.setDraftEntryMediaFiles([{ id: '1' }, { id: '2' }]),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
mediaFiles: [{ id: '1' }, { id: '2' }],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
hasChanged: true,
|
||||
key: '',
|
||||
});
|
||||
});
|
||||
@ -177,7 +154,7 @@ describe('entryDraft reducer', () => {
|
||||
|
||||
describe('DRAFT_CREATE_FROM_LOCAL_BACKUP', () => {
|
||||
it('should create draft from local backup', () => {
|
||||
const localBackup = Map({ entry: fromJS(entry), mediaFiles: List([{ id: '1' }]) });
|
||||
const localBackup = Map({ entry: fromJS({ ...entry, mediaFiles: [{ id: '1' }] }) });
|
||||
|
||||
const actualState = reducer(initialState.set('localBackup', localBackup), {
|
||||
type: actions.DRAFT_CREATE_FROM_LOCAL_BACKUP,
|
||||
@ -185,9 +162,9 @@ describe('entryDraft reducer', () => {
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {
|
||||
...entry,
|
||||
mediaFiles: [{ id: '1' }],
|
||||
newRecord: false,
|
||||
},
|
||||
mediaFiles: [{ id: '1' }],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
@ -200,17 +177,18 @@ describe('entryDraft reducer', () => {
|
||||
it('should set local backup', () => {
|
||||
const mediaFiles = [{ id: '1' }];
|
||||
|
||||
const actualState = reducer(initialState, actions.localBackupRetrieved(entry, mediaFiles));
|
||||
const actualState = reducer(
|
||||
initialState,
|
||||
actions.localBackupRetrieved({ ...entry, mediaFiles }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
mediaFiles: [],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
localBackup: {
|
||||
entry,
|
||||
mediaFiles: [{ id: '1' }],
|
||||
entry: { ...entry, mediaFiles: [{ id: '1' }] },
|
||||
},
|
||||
key: '',
|
||||
});
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import integrations from '../integrations';
|
||||
import { CONFIG_SUCCESS } from '../../actions/config';
|
||||
|
||||
describe('integrations', () => {
|
||||
it('should return default state when no integrations', () => {
|
||||
const result = integrations(null, {
|
||||
type: CONFIG_SUCCESS,
|
||||
payload: fromJS({ integrations: [] }),
|
||||
});
|
||||
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: fromJS({
|
||||
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' }, { name: 'pages' }, { name: 'faq' }],
|
||||
}),
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -1,38 +1,16 @@
|
||||
import { Map } from 'immutable';
|
||||
import { ADD_MEDIA_FILES_TO_LIBRARY, mediaDeleted } from 'Actions/mediaLibrary';
|
||||
import mediaLibrary from '../mediaLibrary';
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import { mediaDeleted } from 'Actions/mediaLibrary';
|
||||
import mediaLibrary, {
|
||||
selectMediaFiles,
|
||||
selectMediaFileByPath,
|
||||
selectMediaDisplayURL,
|
||||
} from '../mediaLibrary';
|
||||
|
||||
jest.mock('uuid/v4');
|
||||
jest.mock('Reducers/editorialWorkflow');
|
||||
jest.mock('Reducers');
|
||||
|
||||
describe('mediaLibrary', () => {
|
||||
const uuid = require('uuid/v4');
|
||||
|
||||
it('should add media files to library', () => {
|
||||
uuid.mockReturnValue('newKey');
|
||||
|
||||
expect(
|
||||
mediaLibrary(
|
||||
Map({
|
||||
files: [
|
||||
{ sha: 'old', path: 'path', key: 'key1' },
|
||||
{ sha: 'sha', path: 'some-other-pas', key: 'key2' },
|
||||
],
|
||||
}),
|
||||
{
|
||||
type: ADD_MEDIA_FILES_TO_LIBRARY,
|
||||
payload: { mediaFiles: [{ sha: 'new', path: 'path' }] },
|
||||
},
|
||||
),
|
||||
).toEqual(
|
||||
Map({
|
||||
files: [
|
||||
{ sha: 'new', path: 'path', key: 'newKey' },
|
||||
{ sha: 'sha', path: 'some-other-pas', key: 'key2' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove media file by key', () => {
|
||||
expect(
|
||||
mediaLibrary(
|
||||
@ -64,4 +42,60 @@ describe('mediaLibrary', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should select draft media files when editing a workflow draft', () => {
|
||||
const { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
|
||||
|
||||
selectEditingWorkflowDraft.mockReturnValue(true);
|
||||
|
||||
const state = {
|
||||
entryDraft: fromJS({ entry: { mediaFiles: [{ id: 1 }] } }),
|
||||
};
|
||||
|
||||
expect(selectMediaFiles(state)).toEqual([{ key: 1, id: 1 }]);
|
||||
});
|
||||
|
||||
it('should select global media files when not editing a workflow draft', () => {
|
||||
const { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
|
||||
|
||||
selectEditingWorkflowDraft.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 { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
|
||||
|
||||
selectEditingWorkflowDraft.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' }));
|
||||
});
|
||||
});
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { Map } from 'immutable';
|
||||
import { addAssets, addAsset, removeAsset } from 'Actions/media';
|
||||
import reducer from '../medias';
|
||||
|
||||
jest.mock('ValueObjects/AssetProxy');
|
||||
|
||||
describe('medias', () => {
|
||||
it('should add assets', () => {
|
||||
expect(reducer(Map(), addAssets([{ public_path: 'public_path' }]))).toEqual(
|
||||
Map({ public_path: { public_path: 'public_path' } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add asset', () => {
|
||||
expect(reducer(Map(), addAsset({ public_path: 'public_path' }))).toEqual(
|
||||
Map({ public_path: { public_path: 'public_path' } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove asset', () => {
|
||||
expect(
|
||||
reducer(Map({ public_path: { public_path: 'public_path' } }), removeAsset('public_path')),
|
||||
).toEqual(Map());
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import { addAssets, addAsset, removeAsset } 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(fromJS({}), addAssets([asset]))).toEqual(Map({ path: asset }));
|
||||
});
|
||||
|
||||
it('should add asset', () => {
|
||||
expect(reducer(fromJS({}), addAsset(asset))).toEqual(Map({ path: asset }));
|
||||
});
|
||||
|
||||
it('should remove asset', () => {
|
||||
expect(reducer(fromJS({ path: asset }), removeAsset(asset.path))).toEqual(Map());
|
||||
});
|
||||
});
|
@ -1,172 +0,0 @@
|
||||
import { List } from 'immutable';
|
||||
import { get, escapeRegExp } from 'lodash';
|
||||
import consoleError from 'Lib/consoleError';
|
||||
import { CONFIG_SUCCESS } from 'Actions/config';
|
||||
import { FILES, FOLDER } from 'Constants/collectionTypes';
|
||||
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from 'Constants/fieldInference';
|
||||
import { formatExtensions } from 'Formats/formats';
|
||||
|
||||
const collections = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const configCollections = action.payload ? action.payload.get('collections') : List();
|
||||
return configCollections
|
||||
.toOrderedMap()
|
||||
.map(collection => {
|
||||
if (collection.has('folder')) {
|
||||
return collection.set('type', FOLDER);
|
||||
}
|
||||
if (collection.has('files')) {
|
||||
return collection.set('type', FILES);
|
||||
}
|
||||
})
|
||||
.mapKeys((key, collection) => collection.get('name'));
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const selectors = {
|
||||
[FOLDER]: {
|
||||
entryExtension(collection) {
|
||||
return (
|
||||
collection.get('extension') ||
|
||||
get(formatExtensions, collection.get('format') || 'frontmatter')
|
||||
).replace(/^\./, '');
|
||||
},
|
||||
fields(collection) {
|
||||
return collection.get('fields');
|
||||
},
|
||||
entryPath(collection, slug) {
|
||||
const folder = collection.get('folder').replace(/\/$/, '');
|
||||
return `${folder}/${slug}.${this.entryExtension(collection)}`;
|
||||
},
|
||||
entrySlug(collection, path) {
|
||||
const folder = collection.get('folder').replace(/\/$/, '');
|
||||
const slug = path
|
||||
.split(folder + '/')
|
||||
.pop()
|
||||
.replace(new RegExp(`\\.${escapeRegExp(this.entryExtension(collection))}$`), '');
|
||||
|
||||
return slug;
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFolder';
|
||||
},
|
||||
allowNewEntries(collection) {
|
||||
return collection.get('create');
|
||||
},
|
||||
allowDeletion(collection) {
|
||||
return collection.get('delete', true);
|
||||
},
|
||||
templateName(collection) {
|
||||
return collection.get('name');
|
||||
},
|
||||
},
|
||||
[FILES]: {
|
||||
fileForEntry(collection, slug) {
|
||||
const files = collection.get('files');
|
||||
return files && files.filter(f => f.get('name') === slug).get(0);
|
||||
},
|
||||
fields(collection, slug) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('fields');
|
||||
},
|
||||
entryPath(collection, slug) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('file');
|
||||
},
|
||||
entrySlug(collection, path) {
|
||||
const file = collection
|
||||
.get('files')
|
||||
.filter(f => f.get('file') === path)
|
||||
.get(0);
|
||||
return file && file.get('name');
|
||||
},
|
||||
entryLabel(collection, slug) {
|
||||
const path = this.entryPath(collection, slug);
|
||||
const files = collection.get('files');
|
||||
return files && files.find(f => f.get('file') === path).get('label');
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFiles';
|
||||
},
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
},
|
||||
allowDeletion(collection) {
|
||||
return collection.get('delete', false);
|
||||
},
|
||||
templateName(collection, slug) {
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const selectFields = (collection, slug) =>
|
||||
selectors[collection.get('type')].fields(collection, slug);
|
||||
export const selectFolderEntryExtension = collection =>
|
||||
selectors[FOLDER].entryExtension(collection);
|
||||
export const selectFileEntryLabel = (collection, slug) =>
|
||||
selectors[FILES].entryLabel(collection, slug);
|
||||
export const selectEntryPath = (collection, slug) =>
|
||||
selectors[collection.get('type')].entryPath(collection, slug);
|
||||
export const selectEntrySlug = (collection, path) =>
|
||||
selectors[collection.get('type')].entrySlug(collection, path);
|
||||
export const selectListMethod = collection => selectors[collection.get('type')].listMethod();
|
||||
export const selectAllowNewEntries = collection =>
|
||||
selectors[collection.get('type')].allowNewEntries(collection);
|
||||
export const selectAllowDeletion = collection =>
|
||||
selectors[collection.get('type')].allowDeletion(collection);
|
||||
export const selectTemplateName = (collection, slug) =>
|
||||
selectors[collection.get('type')].templateName(collection, slug);
|
||||
export const selectIdentifier = collection => {
|
||||
const identifier = collection.get('identifier_field');
|
||||
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS;
|
||||
const fieldNames = collection.get('fields', []).map(field => field.get('name'));
|
||||
return identifierFields.find(id =>
|
||||
fieldNames.find(name => name.toLowerCase().trim() === id.toLowerCase().trim()),
|
||||
);
|
||||
};
|
||||
export const selectInferedField = (collection, fieldName) => {
|
||||
if (fieldName === 'title' && collection.get('identifier_field')) {
|
||||
return selectIdentifier(collection);
|
||||
}
|
||||
const inferableField = INFERABLE_FIELDS[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) !== -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')) !== -1)
|
||||
.map(f => f.get('name'));
|
||||
field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -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')}”`,
|
||||
`Netlify 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 default collections;
|
195
packages/netlify-cms-core/src/reducers/collections.ts
Normal file
195
packages/netlify-cms-core/src/reducers/collections.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { List } from 'immutable';
|
||||
import { get, escapeRegExp } from 'lodash';
|
||||
import consoleError from '../lib/consoleError';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from '../constants/fieldInference';
|
||||
import { formatExtensions } from '../formats/formats';
|
||||
import { CollectionsAction, Collection, CollectionFiles, EntryField } from '../types/redux';
|
||||
|
||||
const collections = (state = null, action: CollectionsAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const configCollections = action.payload
|
||||
? action.payload.get('collections')
|
||||
: List<Collection>();
|
||||
|
||||
return (
|
||||
configCollections
|
||||
.toOrderedMap()
|
||||
.map(item => {
|
||||
const collection = item as Collection;
|
||||
if (collection.has('folder')) {
|
||||
return collection.set('type', FOLDER);
|
||||
}
|
||||
if (collection.has('files')) {
|
||||
return collection.set('type', FILES);
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
.mapKeys((key: string, collection: Collection) => collection.get('name'))
|
||||
);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
enum ListMethod {
|
||||
ENTRIES_BY_FOLDER = 'entriesByFolder',
|
||||
ENTRIES_BY_FILES = 'entriesByFiles',
|
||||
}
|
||||
|
||||
const selectors = {
|
||||
[FOLDER]: {
|
||||
entryExtension(collection: Collection) {
|
||||
return (
|
||||
collection.get('extension') ||
|
||||
get(formatExtensions, collection.get('format') || 'frontmatter')
|
||||
).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;
|
||||
},
|
||||
listMethod() {
|
||||
return ListMethod.ENTRIES_BY_FOLDER;
|
||||
},
|
||||
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 path = this.entryPath(collection, slug);
|
||||
const files = collection.get('files');
|
||||
return files && files.find(f => f?.get('file') === path).get('label');
|
||||
},
|
||||
listMethod() {
|
||||
return ListMethod.ENTRIES_BY_FILES;
|
||||
},
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
},
|
||||
allowDeletion(collection: Collection) {
|
||||
return collection.get('delete', false);
|
||||
},
|
||||
templateName(collection: Collection, slug: string) {
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const selectFields = (collection: Collection, slug: string) =>
|
||||
selectors[collection.get('type')].fields(collection, slug);
|
||||
export const selectFolderEntryExtension = (collection: Collection) =>
|
||||
selectors[FOLDER].entryExtension(collection);
|
||||
export const selectFileEntryLabel = (collection: Collection, slug: string) =>
|
||||
selectors[FILES].entryLabel(collection, slug);
|
||||
export const selectEntryPath = (collection: Collection, slug: string) =>
|
||||
selectors[collection.get('type')].entryPath(collection, slug);
|
||||
export const selectEntrySlug = (collection: Collection, path: string) =>
|
||||
selectors[collection.get('type')].entrySlug(collection, path);
|
||||
export const selectListMethod = (collection: Collection) =>
|
||||
selectors[collection.get('type')].listMethod();
|
||||
export const selectAllowNewEntries = (collection: Collection) =>
|
||||
selectors[collection.get('type')].allowNewEntries(collection);
|
||||
export const selectAllowDeletion = (collection: Collection) =>
|
||||
selectors[collection.get('type')].allowDeletion(collection);
|
||||
export const selectTemplateName = (collection: Collection, slug: string) =>
|
||||
selectors[collection.get('type')].templateName(collection, slug);
|
||||
export const selectIdentifier = (collection: Collection) => {
|
||||
const identifier = collection.get('identifier_field');
|
||||
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS;
|
||||
const fieldNames = collection.get('fields', List<EntryField>()).map(field => field?.get('name'));
|
||||
return identifierFields.find(id =>
|
||||
fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()),
|
||||
);
|
||||
};
|
||||
export const selectInferedField = (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')}”`,
|
||||
`Netlify 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 default collections;
|
@ -1,7 +1,8 @@
|
||||
import { Map } from 'immutable';
|
||||
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE, CONFIG_MERGE } from 'Actions/config';
|
||||
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE, CONFIG_MERGE } from '../actions/config';
|
||||
import { Config, ConfigAction } from '../types/redux';
|
||||
|
||||
const config = (state = Map({ isFetching: true }), action) => {
|
||||
const config = (state = Map({ isFetching: true }), action: ConfigAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_MERGE:
|
||||
return state.mergeDeep(action.payload);
|
||||
@ -21,4 +22,6 @@ const config = (state = Map({ isFetching: true }), action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const selectLocale = (state: Config) => state.get('locale', 'en') as string;
|
||||
|
||||
export default config;
|
@ -86,7 +86,7 @@ const unpublishedEntries = (state = Map(), action) => {
|
||||
// Update Optimistically
|
||||
return state.deleteIn([
|
||||
'entities',
|
||||
`${action.payload.collection}.${action.payload.entry.get('slug')}`,
|
||||
`${action.payload.collection}.${action.payload.slug}`,
|
||||
'isPersisting',
|
||||
]);
|
||||
|
||||
@ -150,4 +150,11 @@ export const selectUnpublishedSlugs = (state, collection) => {
|
||||
.valueSeq();
|
||||
};
|
||||
|
||||
export const selectEditingWorkflowDraft = state => {
|
||||
const entry = state.entryDraft.get('entry');
|
||||
const useWorkflow = state.config.get('publish_mode') === EDITORIAL_WORKFLOW;
|
||||
const workflowDraft = entry && !entry.isEmpty() && useWorkflow;
|
||||
return workflowDraft;
|
||||
};
|
||||
|
||||
export default unpublishedEntries;
|
||||
|
@ -1,115 +0,0 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import {
|
||||
ENTRY_REQUEST,
|
||||
ENTRY_SUCCESS,
|
||||
ENTRY_FAILURE,
|
||||
ENTRIES_REQUEST,
|
||||
ENTRIES_SUCCESS,
|
||||
ENTRIES_FAILURE,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
} from 'Actions/entries';
|
||||
|
||||
import { SEARCH_ENTRIES_SUCCESS } from 'Actions/search';
|
||||
|
||||
let collection;
|
||||
let loadedEntries;
|
||||
let append;
|
||||
let page;
|
||||
let slug;
|
||||
|
||||
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||
switch (action.type) {
|
||||
case ENTRY_REQUEST:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'],
|
||||
true,
|
||||
);
|
||||
|
||||
case ENTRY_SUCCESS:
|
||||
collection = action.payload.collection;
|
||||
slug = action.payload.entry.slug;
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['entities', `${collection}.${slug}`], fromJS(action.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:
|
||||
return state.setIn(['pages', action.payload.collection, 'isFetching'], true);
|
||||
|
||||
case ENTRIES_SUCCESS:
|
||||
collection = action.payload.collection;
|
||||
loadedEntries = action.payload.entries;
|
||||
append = action.payload.append;
|
||||
page = action.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:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(
|
||||
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'],
|
||||
false,
|
||||
);
|
||||
map.setIn(
|
||||
['entities', `${action.payload.collection}.${action.payload.slug}`, 'error'],
|
||||
action.payload.error.message,
|
||||
);
|
||||
});
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS:
|
||||
loadedEntries = action.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:
|
||||
return state.withMutations(map => {
|
||||
map.deleteIn(['entities', `${action.payload.collectionName}.${action.payload.entrySlug}`]);
|
||||
map.updateIn(['pages', action.payload.collectionName, 'ids'], ids =>
|
||||
ids.filter(id => id !== action.payload.entrySlug),
|
||||
);
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectEntry = (state, collection, slug) =>
|
||||
state.getIn(['entities', `${collection}.${slug}`]);
|
||||
|
||||
export const selectPublishedSlugs = (state, collection) =>
|
||||
state.getIn(['pages', collection, 'ids'], List());
|
||||
|
||||
export const selectEntries = (state, collection) => {
|
||||
const slugs = selectPublishedSlugs(state, collection);
|
||||
return slugs && slugs.map(slug => selectEntry(state, collection, slug));
|
||||
};
|
||||
|
||||
export default entries;
|
200
packages/netlify-cms-core/src/reducers/entries.ts
Normal file
200
packages/netlify-cms-core/src/reducers/entries.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { dirname, join } from 'path';
|
||||
import {
|
||||
ENTRY_REQUEST,
|
||||
ENTRY_SUCCESS,
|
||||
ENTRY_FAILURE,
|
||||
ENTRIES_REQUEST,
|
||||
ENTRIES_SUCCESS,
|
||||
ENTRIES_FAILURE,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
|
||||
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||
import {
|
||||
EntriesAction,
|
||||
EntryRequestPayload,
|
||||
EntrySuccessPayload,
|
||||
EntriesSuccessPayload,
|
||||
EntryObject,
|
||||
Entries,
|
||||
Config,
|
||||
Collection,
|
||||
EntryFailurePayload,
|
||||
EntryDeletePayload,
|
||||
EntriesRequestPayload,
|
||||
} from '../types/redux';
|
||||
import { isAbsolutePath, basename } from 'netlify-cms-lib-util/src';
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
|
||||
let collection: string;
|
||||
let loadedEntries: EntryObject[];
|
||||
let append: boolean;
|
||||
let page: number;
|
||||
let slug: string;
|
||||
|
||||
const entries = (state = Map({ entities: Map(), pages: Map() }), 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;
|
||||
return state.setIn(['pages', payload.collection, 'isFetching'], true);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectEntry = (state: Entries, collection: string, slug: string) =>
|
||||
state.getIn(['entities', `${collection}.${slug}`]);
|
||||
|
||||
export const selectPublishedSlugs = (state: Entries, collection: string) =>
|
||||
state.getIn(['pages', collection, 'ids'], List<string>());
|
||||
|
||||
export const selectEntries = (state: Entries, collection: string) => {
|
||||
const slugs = selectPublishedSlugs(state, collection);
|
||||
return slugs && slugs.map(slug => selectEntry(state, collection, slug as string));
|
||||
};
|
||||
|
||||
const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
|
||||
|
||||
export const selectMediaFolder = (
|
||||
config: Config,
|
||||
collection: Collection | null,
|
||||
entryPath: string | null,
|
||||
) => {
|
||||
let mediaFolder = config.get('media_folder');
|
||||
|
||||
const useWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
|
||||
if (useWorkflow && collection && collection.has('media_folder')) {
|
||||
if (entryPath) {
|
||||
const entryDir = dirname(entryPath);
|
||||
mediaFolder = join(entryDir, collection.get('media_folder') as string);
|
||||
} else {
|
||||
mediaFolder = join(collection.get('folder') as string, DRAFT_MEDIA_FILES);
|
||||
}
|
||||
}
|
||||
|
||||
return mediaFolder;
|
||||
};
|
||||
|
||||
export const selectMediaFilePath = (
|
||||
config: Config,
|
||||
collection: Collection | null,
|
||||
entryPath: string | null,
|
||||
mediaPath: string,
|
||||
) => {
|
||||
if (isAbsolutePath(mediaPath)) {
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
let mediaFolder;
|
||||
if (mediaPath.startsWith('/')) {
|
||||
// absolute media paths are not bound to a collection
|
||||
mediaFolder = selectMediaFolder(config, null, null);
|
||||
} else {
|
||||
mediaFolder = selectMediaFolder(config, collection, entryPath);
|
||||
}
|
||||
|
||||
return join(mediaFolder, basename(mediaPath));
|
||||
};
|
||||
|
||||
export const selectMediaFilePublicPath = (
|
||||
config: Config,
|
||||
collection: Collection | null,
|
||||
mediaPath: string,
|
||||
) => {
|
||||
if (isAbsolutePath(mediaPath)) {
|
||||
return mediaPath;
|
||||
}
|
||||
|
||||
let publicFolder = config.get('public_folder');
|
||||
|
||||
const useWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
|
||||
if (useWorkflow && collection && collection.has('media_folder')) {
|
||||
publicFolder = collection.get('media_folder') as string;
|
||||
}
|
||||
|
||||
return join(publicFolder, basename(mediaPath));
|
||||
};
|
||||
|
||||
export default entries;
|
@ -15,9 +15,7 @@ import {
|
||||
ENTRY_PERSIST_FAILURE,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
ADD_DRAFT_ENTRY_MEDIA_FILE,
|
||||
SET_DRAFT_ENTRY_MEDIA_FILES,
|
||||
REMOVE_DRAFT_ENTRY_MEDIA_FILE,
|
||||
CLEAR_DRAFT_ENTRY_MEDIA_FILES,
|
||||
} from 'Actions/entries';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
@ -27,7 +25,6 @@ import {
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
mediaFiles: List(),
|
||||
fieldsMetaData: Map(),
|
||||
fieldsErrors: Map(),
|
||||
hasChanged: false,
|
||||
@ -41,7 +38,6 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
return state.withMutations(state => {
|
||||
state.set('entry', action.payload.entry);
|
||||
state.setIn(['entry', 'newRecord'], false);
|
||||
state.set('mediaFiles', action.payload.mediaFiles || List());
|
||||
// An existing entry may already have metadata. If we surfed away and back to its
|
||||
// editor page, the metadata will have been fetched already, so we shouldn't
|
||||
// clear it as to not break relation lists.
|
||||
@ -55,7 +51,6 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
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', false);
|
||||
@ -69,7 +64,6 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.delete('localBackup');
|
||||
state.set('entry', backupEntry);
|
||||
state.setIn(['entry', 'newRecord'], !backupEntry.get('path'));
|
||||
state.set('mediaFiles', backupDraftEntry.get('mediaFiles'));
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', true);
|
||||
@ -88,10 +82,9 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
case DRAFT_DISCARD:
|
||||
return initialState;
|
||||
case DRAFT_LOCAL_BACKUP_RETRIEVED: {
|
||||
const { entry, mediaFiles } = action.payload;
|
||||
const { entry } = action.payload;
|
||||
const newState = new Map({
|
||||
entry: fromJS(entry),
|
||||
mediaFiles: List(mediaFiles),
|
||||
});
|
||||
return state.set('localBackup', newState);
|
||||
}
|
||||
@ -139,28 +132,31 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.set('hasChanged', false);
|
||||
});
|
||||
|
||||
case ADD_DRAFT_ENTRY_MEDIA_FILE:
|
||||
if (state.has('mediaFiles')) {
|
||||
return state.update('mediaFiles', list =>
|
||||
list.filterNot(file => file.id === action.payload.id).push({ ...action.payload }),
|
||||
);
|
||||
}
|
||||
return state;
|
||||
case ADD_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
return state.withMutations(state => {
|
||||
const mediaFiles = state.getIn(['entry', 'mediaFiles']);
|
||||
|
||||
case SET_DRAFT_ENTRY_MEDIA_FILES: {
|
||||
return state.set('mediaFiles', List(action.payload));
|
||||
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:
|
||||
if (state.has('mediaFiles')) {
|
||||
return state.update('mediaFiles', list =>
|
||||
list.filterNot(file => file.id === action.payload.id),
|
||||
);
|
||||
}
|
||||
return state;
|
||||
case REMOVE_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
return state.withMutations(state => {
|
||||
const mediaFiles = state.getIn(['entry', 'mediaFiles']);
|
||||
|
||||
case CLEAR_DRAFT_ENTRY_MEDIA_FILES:
|
||||
return state.set('mediaFiles', List());
|
||||
state.setIn(
|
||||
['entry', 'mediaFiles'],
|
||||
mediaFiles.filterNot(file => file.get('id') === action.payload.id),
|
||||
);
|
||||
state.set('hasChanged', true);
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
@ -7,10 +7,12 @@ 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 medias, * as fromMedias from './medias';
|
||||
import deploys, * as fromDeploys from './deploys';
|
||||
import globalUI from './globalUI';
|
||||
import { Status } from '../constants/publishModes';
|
||||
import { State } from '../types/redux';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
@ -22,8 +24,8 @@ const reducers = {
|
||||
cursors,
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
mediaLibrary,
|
||||
medias,
|
||||
mediaLibrary,
|
||||
deploys,
|
||||
globalUI,
|
||||
};
|
||||
@ -33,16 +35,16 @@ export default reducers;
|
||||
/*
|
||||
* Selectors
|
||||
*/
|
||||
export const selectEntry = (state, collection, slug) =>
|
||||
export const selectEntry = (state: State, collection: string, slug: string) =>
|
||||
fromEntries.selectEntry(state.entries, collection, slug);
|
||||
|
||||
export const selectEntries = (state, collection) =>
|
||||
export const selectEntries = (state: State, collection: string) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const selectPublishedSlugs = (state, collection) =>
|
||||
export const selectPublishedSlugs = (state: State, collection: string) =>
|
||||
fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||
|
||||
export const selectSearchedEntries = state => {
|
||||
export const selectSearchedEntries = (state: State) => {
|
||||
const searchItems = state.search.get('entryIds');
|
||||
return (
|
||||
searchItems &&
|
||||
@ -52,27 +54,17 @@ export const selectSearchedEntries = state => {
|
||||
);
|
||||
};
|
||||
|
||||
export const selectDeployPreview = (state, collection, slug) =>
|
||||
export const selectDeployPreview = (state: State, collection: string, slug: string) =>
|
||||
fromDeploys.selectDeployPreview(state.deploys, collection, slug);
|
||||
|
||||
export const selectUnpublishedEntry = (state, collection, slug) =>
|
||||
export const selectUnpublishedEntry = (state: State, collection: string, slug: string) =>
|
||||
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, collection, slug);
|
||||
|
||||
export const selectUnpublishedEntriesByStatus = (state, status) =>
|
||||
export const selectUnpublishedEntriesByStatus = (state: State, status: Status) =>
|
||||
fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
||||
|
||||
export const selectUnpublishedSlugs = (state, collection) =>
|
||||
export const selectUnpublishedSlugs = (state: State, collection: string) =>
|
||||
fromEditorialWorkflow.selectUnpublishedSlugs(state.editorialWorkflow, collection);
|
||||
|
||||
export const selectIntegration = (state, collection, hook) =>
|
||||
export const selectIntegration = (state: State, collection: string | null, hook: string) =>
|
||||
fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||
|
||||
export const getAsset = (state, path) => {
|
||||
/**
|
||||
* If an external media library is in use, just return the path.
|
||||
*/
|
||||
if (state.mediaLibrary.get('externalLibrary')) {
|
||||
return path;
|
||||
}
|
||||
return fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
|
||||
};
|
@ -1,10 +1,16 @@
|
||||
import { fromJS, List } from 'immutable';
|
||||
import { CONFIG_SUCCESS } from 'Actions/config';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { Integrations, IntegrationsAction, Integration } from '../types/redux';
|
||||
|
||||
const integrations = (state = null, action) => {
|
||||
interface Acc {
|
||||
providers: Record<string, {}>;
|
||||
hooks: Record<string, string | Record<string, string>>;
|
||||
}
|
||||
|
||||
const integrations = (state = null, action: IntegrationsAction): Integrations | null => {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const integrations = action.payload.get('integrations', List()).toJS() || [];
|
||||
const integrations: Integration[] = action.payload.get('integrations', List()).toJS() || [];
|
||||
const newState = integrations.reduce(
|
||||
(acc, integration) => {
|
||||
const { hooks, collections, provider, ...providerData } = integration;
|
||||
@ -17,18 +23,18 @@ const integrations = (state = null, action) => {
|
||||
}
|
||||
const integrationCollections =
|
||||
collections === '*'
|
||||
? action.payload.collections.map(collection => collection.name)
|
||||
: collections;
|
||||
? action.payload.get('collections').map(collection => collection.get('name'))
|
||||
: (collections as string[]);
|
||||
integrationCollections.forEach(collection => {
|
||||
hooks.forEach(hook => {
|
||||
acc.hooks[collection]
|
||||
? (acc.hooks[collection][hook] = provider)
|
||||
? ((acc.hooks[collection] as Record<string, string>)[hook] = provider)
|
||||
: (acc.hooks[collection] = { [hook]: provider });
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ providers: {}, hooks: {} },
|
||||
{ providers: {}, hooks: {} } as Acc,
|
||||
);
|
||||
return fromJS(newState);
|
||||
}
|
||||
@ -37,7 +43,7 @@ const integrations = (state = null, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const selectIntegration = (state, collection, hook) =>
|
||||
export const selectIntegration = (state: Integrations, collection: string | null, hook: string) =>
|
||||
collection
|
||||
? state.getIn(['hooks', collection, hook], false)
|
||||
: state.getIn(['hooks', hook], false);
|
@ -1,6 +1,5 @@
|
||||
import { Map } from 'immutable';
|
||||
import { Map, List } from 'immutable';
|
||||
import uuid from 'uuid/v4';
|
||||
import { differenceBy } from 'lodash';
|
||||
import {
|
||||
MEDIA_LIBRARY_OPEN,
|
||||
MEDIA_LIBRARY_CLOSE,
|
||||
@ -19,8 +18,9 @@ import {
|
||||
MEDIA_DISPLAY_URL_REQUEST,
|
||||
MEDIA_DISPLAY_URL_SUCCESS,
|
||||
MEDIA_DISPLAY_URL_FAILURE,
|
||||
ADD_MEDIA_FILES_TO_LIBRARY,
|
||||
} from 'Actions/mediaLibrary';
|
||||
import { selectEditingWorkflowDraft } from 'Reducers/editorialWorkflow';
|
||||
import { selectIntegration } from 'Reducers';
|
||||
|
||||
const defaultState = {
|
||||
isVisible: false,
|
||||
@ -129,12 +129,6 @@ const mediaLibrary = (state = Map(defaultState), action) => {
|
||||
map.set('isPersisting', false);
|
||||
});
|
||||
}
|
||||
case ADD_MEDIA_FILES_TO_LIBRARY: {
|
||||
const { mediaFiles } = action.payload;
|
||||
let updatedFiles = differenceBy(state.get('files'), mediaFiles, 'path');
|
||||
updatedFiles = [...mediaFiles.map(file => ({ ...file, key: uuid() })), ...updatedFiles];
|
||||
return state.set('files', updatedFiles);
|
||||
}
|
||||
case MEDIA_PERSIST_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
@ -193,4 +187,33 @@ const mediaLibrary = (state = Map(defaultState), action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export function selectMediaFiles(state) {
|
||||
const { mediaLibrary, entryDraft } = state;
|
||||
const workflowDraft = selectEditingWorkflowDraft(state);
|
||||
const integration = selectIntegration(state, null, 'assetStore');
|
||||
|
||||
let files;
|
||||
if (workflowDraft && !integration) {
|
||||
files = entryDraft
|
||||
.getIn(['entry', 'mediaFiles'], List())
|
||||
.toJS()
|
||||
.map(file => ({ key: file.id, ...file }));
|
||||
} else {
|
||||
files = mediaLibrary.get('files') || [];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function selectMediaFileByPath(state, path) {
|
||||
const files = selectMediaFiles(state);
|
||||
const file = files.find(file => file.path === path);
|
||||
return file;
|
||||
}
|
||||
|
||||
export function selectMediaDisplayURL(state, id) {
|
||||
const displayUrlState = state.mediaLibrary.getIn(['displayURLs', id], Map());
|
||||
return displayUrlState;
|
||||
}
|
||||
|
||||
export default mediaLibrary;
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { Map } from 'immutable';
|
||||
import { resolvePath } from 'netlify-cms-lib-util';
|
||||
import { ADD_ASSETS, ADD_ASSET, REMOVE_ASSET } from 'Actions/media';
|
||||
import AssetProxy from 'ValueObjects/AssetProxy';
|
||||
|
||||
const medias = (state = Map(), action) => {
|
||||
switch (action.type) {
|
||||
case ADD_ASSETS: {
|
||||
let newState = state;
|
||||
action.payload.forEach(asset => {
|
||||
newState = newState.set(asset.public_path, asset);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
case ADD_ASSET:
|
||||
return state.set(action.payload.public_path, action.payload);
|
||||
case REMOVE_ASSET:
|
||||
return state.delete(action.payload);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default medias;
|
||||
|
||||
const memoizedProxies = {};
|
||||
export const getAsset = (publicFolder, state, path) => {
|
||||
// No path provided, skip
|
||||
if (!path) return null;
|
||||
|
||||
let proxy = state.get(path) || memoizedProxies[path];
|
||||
if (proxy) {
|
||||
// There is already an AssetProxy in memory for this path. Use it.
|
||||
return proxy;
|
||||
}
|
||||
|
||||
// Create a new AssetProxy (for consistency) and return it.
|
||||
proxy = memoizedProxies[path] = new AssetProxy(resolvePath(path, publicFolder), null, true);
|
||||
return proxy;
|
||||
};
|
29
packages/netlify-cms-core/src/reducers/medias.ts
Normal file
29
packages/netlify-cms-core/src/reducers/medias.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { ADD_ASSETS, ADD_ASSET, REMOVE_ASSET } from '../actions/media';
|
||||
import AssetProxy from '../valueObjects/AssetProxy';
|
||||
import { Medias, MediasAction } from '../types/redux';
|
||||
|
||||
const medias = (state: Medias = fromJS({}), action: MediasAction) => {
|
||||
switch (action.type) {
|
||||
case ADD_ASSETS: {
|
||||
const payload = action.payload as AssetProxy[];
|
||||
let newState = state;
|
||||
payload.forEach(asset => {
|
||||
newState = newState.set(asset.path, asset);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
case ADD_ASSET: {
|
||||
const payload = action.payload as AssetProxy;
|
||||
return state.set(payload.path, payload);
|
||||
}
|
||||
case REMOVE_ASSET: {
|
||||
const payload = action.payload as string;
|
||||
return state.delete(payload);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default medias;
|
Reference in New Issue
Block a user