feat: commit media with post (#2851)
* feat: commit media with post - initial commit * feat: add draft media indication * feat: sync UI media files with GitHub on entry load * feat: bug fixes * feat: delete media files from github when removed from library * test: add GitHub backend tests * test: add unit tests * fix: meta data object files are not updated * feat: used nested paths when update a tree instead of recursion * feat(test-backend): update test backend to persist media file with entry * test(e2e): re-record fixtures data * chore: code cleanup * chore: code cleanup * fix: wait for library to load before adding entry media files * chore: code cleanup * fix: don't add media files on entry when not a draft * fix: sync media library after draft entry was published * feat: update media library card draft style, add tests * test: add Editor unit tests * chore: test code cleanup * fix: publishing an entry from workflow tab throws an error * fix: duplicate media files when using test backend * refactor: fix lodash import * chore: update translations and yarn file after rebase * test(cypress): update recorded data * fix(test-backend): fix mapping of media files on publish
This commit is contained in:
@ -2,7 +2,7 @@ import { Map, List, fromJS } from 'immutable';
|
||||
import * as actions from 'Actions/entries';
|
||||
import reducer from '../entryDraft';
|
||||
|
||||
let initialState = Map({
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
mediaFiles: List(),
|
||||
fieldsMetaData: Map(),
|
||||
@ -62,6 +62,8 @@ describe('entryDraft reducer', () => {
|
||||
});
|
||||
|
||||
describe('persisting', () => {
|
||||
let initialState;
|
||||
|
||||
beforeEach(() => {
|
||||
initialState = fromJS({
|
||||
entities: {
|
||||
@ -111,4 +113,95 @@ describe('entryDraft reducer', () => {
|
||||
expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('REMOVE_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should remove a media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.set('mediaFiles', List([{ id: '1' }, { id: '2' }])),
|
||||
actions.removeDraftEntryMediaFile({ id: '1' }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
mediaFiles: [{ id: '2' }],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADD_DRAFT_ENTRY_MEDIA_FILE', () => {
|
||||
it('should overwrite an existing media file', () => {
|
||||
const actualState = reducer(
|
||||
initialState.set('mediaFiles', List([{ id: '1', name: 'old' }])),
|
||||
actions.addDraftEntryMediaFile({ id: '1', name: 'new' }),
|
||||
);
|
||||
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {},
|
||||
mediaFiles: [{ id: '1', name: 'new' }],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_CREATE_FROM_LOCAL_BACKUP', () => {
|
||||
it('should create draft from local backup', () => {
|
||||
const localBackup = Map({ entry: fromJS(entry), mediaFiles: List([{ id: '1' }]) });
|
||||
|
||||
const actualState = reducer(initialState.set('localBackup', localBackup), {
|
||||
type: actions.DRAFT_CREATE_FROM_LOCAL_BACKUP,
|
||||
});
|
||||
expect(actualState.toJS()).toEqual({
|
||||
entry: {
|
||||
...entry,
|
||||
newRecord: false,
|
||||
},
|
||||
mediaFiles: [{ id: '1' }],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: {},
|
||||
mediaFiles: [],
|
||||
fieldsMetaData: {},
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
localBackup: {
|
||||
entry,
|
||||
mediaFiles: [{ id: '1' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { Map } from 'immutable';
|
||||
import { ADD_MEDIA_FILES_TO_LIBRARY, mediaDeleted } from 'Actions/mediaLibrary';
|
||||
import mediaLibrary from '../mediaLibrary';
|
||||
|
||||
jest.mock('uuid/v4');
|
||||
|
||||
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(
|
||||
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' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
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());
|
||||
});
|
||||
});
|
@ -12,13 +12,16 @@ import {
|
||||
ENTRY_PERSIST_SUCCESS,
|
||||
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,
|
||||
UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
} from 'Actions/editorialWorkflow';
|
||||
import { ADD_ASSET, REMOVE_ASSET } from 'Actions/media';
|
||||
|
||||
const initialState = Map({
|
||||
entry: Map(),
|
||||
@ -35,7 +38,7 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
return state.withMutations(state => {
|
||||
state.set('entry', action.payload.entry);
|
||||
state.setIn(['entry', 'newRecord'], false);
|
||||
state.set('mediaFiles', List());
|
||||
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.
|
||||
@ -56,19 +59,26 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
case DRAFT_CREATE_FROM_LOCAL_BACKUP:
|
||||
// Local Backup
|
||||
return state.withMutations(state => {
|
||||
const backupEntry = state.get('localBackup');
|
||||
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('mediaFiles', List());
|
||||
state.set('mediaFiles', backupDraftEntry.get('mediaFiles'));
|
||||
state.set('fieldsMetaData', Map());
|
||||
state.set('fieldsErrors', Map());
|
||||
state.set('hasChanged', true);
|
||||
});
|
||||
case DRAFT_DISCARD:
|
||||
return initialState;
|
||||
case DRAFT_LOCAL_BACKUP_RETRIEVED:
|
||||
return state.set('localBackup', fromJS(action.payload.entry));
|
||||
case DRAFT_LOCAL_BACKUP_RETRIEVED: {
|
||||
const { entry, mediaFiles } = action.payload;
|
||||
const newState = new Map({
|
||||
entry: fromJS(entry),
|
||||
mediaFiles: List(mediaFiles),
|
||||
});
|
||||
return state.set('localBackup', newState);
|
||||
}
|
||||
case DRAFT_CHANGE_FIELD:
|
||||
return state.withMutations(state => {
|
||||
state.setIn(['entry', 'data', action.payload.field], action.payload.value);
|
||||
@ -113,14 +123,28 @@ const entryDraftReducer = (state = Map(), action) => {
|
||||
state.set('hasChanged', false);
|
||||
});
|
||||
|
||||
case ADD_ASSET:
|
||||
case ADD_DRAFT_ENTRY_MEDIA_FILE:
|
||||
if (state.has('mediaFiles')) {
|
||||
return state.update('mediaFiles', list => list.push(action.payload.public_path));
|
||||
return state.update('mediaFiles', list =>
|
||||
list.filterNot(file => file.id === action.payload.id).push({ ...action.payload }),
|
||||
);
|
||||
}
|
||||
return state;
|
||||
|
||||
case REMOVE_ASSET:
|
||||
return state.update('mediaFiles', list => list.filterNot(path => path === action.payload));
|
||||
case SET_DRAFT_ENTRY_MEDIA_FILES: {
|
||||
return state.set('mediaFiles', List(action.payload));
|
||||
}
|
||||
|
||||
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 CLEAR_DRAFT_ENTRY_MEDIA_FILES:
|
||||
return state.set('mediaFiles', List());
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Map } from 'immutable';
|
||||
import uuid from 'uuid/v4';
|
||||
import { differenceBy } from 'lodash';
|
||||
import {
|
||||
MEDIA_LIBRARY_OPEN,
|
||||
MEDIA_LIBRARY_CLOSE,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
MEDIA_DISPLAY_URL_REQUEST,
|
||||
MEDIA_DISPLAY_URL_SUCCESS,
|
||||
MEDIA_DISPLAY_URL_FAILURE,
|
||||
ADD_MEDIA_FILES_TO_LIBRARY,
|
||||
} from 'Actions/mediaLibrary';
|
||||
|
||||
const defaultState = {
|
||||
@ -127,6 +129,12 @@ 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) {
|
||||
@ -143,7 +151,9 @@ const mediaLibrary = (state = Map(defaultState), action) => {
|
||||
return state;
|
||||
}
|
||||
return state.withMutations(map => {
|
||||
const updatedFiles = map.get('files').filter(file => file.key !== key);
|
||||
const updatedFiles = map
|
||||
.get('files')
|
||||
.filter(file => (key ? file.key !== key : file.id !== id));
|
||||
map.set('files', updatedFiles);
|
||||
map.deleteIn(['displayURLs', id]);
|
||||
map.set('isDeleting', false);
|
||||
|
@ -1,10 +1,17 @@
|
||||
import { Map } from 'immutable';
|
||||
import { resolvePath } from 'netlify-cms-lib-util';
|
||||
import { ADD_ASSET, REMOVE_ASSET } from 'Actions/media';
|
||||
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:
|
||||
|
Reference in New Issue
Block a user