Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: commit media with post #2851

Merged
merged 26 commits into from
Nov 17, 2019
Merged

feat: commit media with post #2851

merged 26 commits into from
Nov 17, 2019

Conversation

erezrokah
Copy link
Contributor

@erezrokah erezrokah commented Nov 7, 2019

Fixes #1344

Opening a new one since I can't re-open #2823 due to a forced commit (rebased branch).

This is a new (breaking) behaviour for the editorial workflow (not sure if we should make it the default).

When creating a new draft entry while in editorial workflow all media files will be stored on the client side until the first save (instead of being committed directly to the default branch). On first save the media files and entry will be pushed together as a single commit.

Any further changes will be synched between the browser and the corresponding branch.

Main changes:

  • Update GitHub backend to support multiple file persistence in a single commit. This includes adding/modifying/deleting multiple files in a single commit and required changing the way we create and update git trees.
  • Sync all the different locations media files are stored - redux store, asset store, local backup and GitHub repo.
  • Update test backend to support it since it is the only other backend that supports editorial workflow.
  • Re-record the fixtures data for the cypress tests due to the API changes.
  • UI changes to mark the media files as draft when in edit mode.
  • Added unit tests for almost every part of the code I touched.

Items left:

  • Regression tests for all other backends and non editorial flow mode for GitHub and test backend
  • Bug fixes
  • Add some tests to the media library UI components.
  • Decide if this should be the default behaviour
  • Code cleanup
  • Make sure I didn't break any media library integrations

Known issue:

  • Currently the editor components are not synced with the media library. For example, one can add an image component in the editor, choose an image from the media library, then delete that image from the media library and the image component will still show up in the editor. I think this is unrelated to this pull request so I won't fix it at the moment (ideally we would like to warn before deleting an image that is linked to an entry).

Stuff I found and fixed during my tests:

parentSha: sha,
}));
});
async updateTree(sha, files) {
Copy link
Contributor Author

@erezrokah erezrokah Nov 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used nested file paths to update the tree instead of recursion (@erquhart figured it out).
This is also the only way we got file deletion to work.

Should have some performance boost.

@@ -322,7 +323,9 @@ export default class GitHub {

async persistMedia(mediaFile, options = {}) {
try {
await this.api.persistFiles(null, [mediaFile], options);
if (!options.draft) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip the update and just return the expected object structure

@@ -656,7 +630,7 @@ export default class API {
}

if (pr) {
return this.rebasePullRequest(pr.number, branchName, contentKey, metadata, commit);
return this.rebasePullRequest(pr.number, branchName, contentKey, updatedMetadata, commit);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to fix this to make sure metadata.object.files are synced

@@ -681,7 +655,7 @@ export default class API {
*/
const [baseBranch, commits] = await Promise.all([
this.getBranch(),
this.getPullRequestCommits(prNumber, head),
this.getPullRequestCommits(prNumber),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPullRequestCommits accepts only one argument

@@ -343,6 +347,29 @@ export default class GitHub {
return this.api.deleteFile(path, commitMessage, options);
}

async getMediaFiles(data) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loads the draft entry media files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

resolve({
slug,
file: { path },
file: { path: data.metaData.objects.entry.path },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be consistent with unpublishedEntry

const assetProxies = await Promise.all(
mediaFiles.map(({ file }) => createAssetProxy(file.name, file)),
);
dispatch(addAssets(assetProxies));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync assets, draft media files and media library on entry load

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this all in one place is really good for visibility, love it

const assetProxies = await Promise.all(
assets.map(asset => createAssetProxy(asset.value, asset.fileObj)),
);
dispatch(addAssets(assetProxies));
Copy link
Contributor Author

@erezrokah erezrokah Nov 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync assets - createAssetProxy creates an AssetProxy class (can't used the serialized object directly since we need the class methods).


const entry = state.entryDraft.get('entry');
const useWorkflow = state.config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW;
const draft = entry && !entry.isEmpty() && useWorkflow;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the best way to check that we are editing an entry

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the entry in state.entryDraft to always be an open entry, but maybe there are gaps - did you experience this not being the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm note sure what "open entry" means.
What I have experienced is that when you exit the editor the draft is discarded here: https://github.com/netlify/netlify-cms/blob/7554134f999970e8f8198f38200c47ff5880c0a4/packages/netlify-cms-core/src/reducers/entryDraft.js#L72

if (!integration) {
const asset = await backend.persistMedia(state.config, assetProxy);
const asset = await backend.persistMedia(state.config, assetProxy, draft);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need the backend to generate the asset object so we call persistMedia

dispatch(removeDraftEntryMediaFile({ id: file.id }));

if (!file.draft) {
await backend.deleteMedia(state.config, file.path);
Copy link
Contributor Author

@erezrokah erezrokah Nov 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the result so we can skip the backend operation completely

const updatedFiles = map.get('files').filter(file => file.key !== key);
const updatedFiles = map
.get('files')
.filter(file => (key ? file.key !== key : file.id !== id));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support remove by id

payload: { mediaFiles },
};
// add media files to library only after the library finished loading
if (state.mediaLibrary.get('isLoading') === false) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't believe I had to do this, but when loading a backup for a new unsaved entry directly from the new entry url (e.g. /collections/posts/new) the backup is loaded before the media library is mounted (hence loaded). Once the library did finish loading the entry media files were overwritten in the media library reducer.
Another solution was not to overwrite at all on media library load success, but it does makes sense to wait for the library to finish initial load before using it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is fine for now at least. It's a code smell, but we have stuff like isLoading and isFetching for a reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you start syncing actions order inside componentDidUpdate based on props you know something is not right.

A better way to wait for actions is to use redux-saga https://redux-saga.js.org/docs/advanced/FutureActions.html as it uses a pull approach, but it might be an overkill for now.

let updatedFiles = differenceBy(state.get('files'), mediaFiles, 'path');
mediaFiles.forEach(file => {
const fileWithKey = { ...file, key: uuid() };
updatedFiles = [fileWithKey, ...updatedFiles];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't something like so:

[...mediaFiles.map(file => ({ ...file, key: uuid() })), ...updatedFiles]

be sufficient here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll fix it.

});
jest.mock('ValueObjects/AssetProxy');
jest.mock('netlify-cms-lib-util');
jest.mock('ValueObjects/AssetProxy');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll remove it

@erezrokah erezrokah force-pushed the feat/commit_media_with_post branch from 04f231e to 62dc650 Compare November 10, 2019 09:04
@erezrokah erezrokah changed the title [WIP] feat: commit media with post feat: commit media with post Nov 11, 2019
@erquhart
Copy link
Contributor

This does break expectations, so from a semver standpoint we should wait for 3.0 to make it default behavior. That said, the current default is just not great at all, and I doubt anyone will have an issue with this change. We could just introduce it as default and have a backup plan to move it under a config flag if folks cry foul.

Sent with GitHawk

@erezrokah erezrokah force-pushed the feat/commit_media_with_post branch from 19a63dc to af2cecd Compare November 11, 2019 18:21
@erezrokah erezrokah requested a review from erquhart November 11, 2019 19:20
Copy link
Contributor

@erquhart erquhart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blown away by all of these tests, fantastic work Erez!! This is a big PR but I'd be comfortable merging and releasing it into the beta channel. Regarding default behavior, I'm inclined to just do it. If people are thrown off we can put it behind a flag, but I don't think that will happen.

@@ -1,6 +1,7 @@
import { Base64 } from 'js-base64';
import semaphore from 'semaphore';
import { find, flow, get, hasIn, initial, last, partial, result, uniq } from 'lodash';
import { find, flow, get, hasIn, initial, last, partial, result, differenceBy } from 'lodash';
import trimStart from 'lodash/trimStart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include this in the line above and let treeshaking resolve the rest? Might need the lodash webpack plugin for that to actually happen.

Copy link
Contributor Author

@erezrokah erezrokah Nov 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I probably copy pasted it from somewhere and didn't notice the existing import. Going forward we can use https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/import-scope.md to enforce a way to import lodash methods (probably Destructured Members) and use a Webpack/babel plugin to get tree shaking like you said.

Update: there is lodash-es, but we'll also need to check what happens if one of our dependencies imports lodash the "wrong way".

sha,
}));
return this.editorialWorkflowGit(files, entry, mediaFilesList, options);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love all of this code removal 🔥🔥🔥

@@ -343,6 +347,29 @@ export default class GitHub {
return this.api.deleteFile(path, commitMessage, options);
}

async getMediaFiles(data) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

const assetProxies = await Promise.all(
mediaFiles.map(({ file }) => createAssetProxy(file.name, file)),
);
dispatch(addAssets(assetProxies));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this all in one place is really good for visibility, love it


const entry = state.entryDraft.get('entry');
const useWorkflow = state.config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW;
const draft = entry && !entry.isEmpty() && useWorkflow;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the entry in state.entryDraft to always be an open entry, but maybe there are gaps - did you experience this not being the case?

payload: { mediaFiles },
};
// add media files to library only after the library finished loading
if (state.mediaLibrary.get('isLoading') === false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is fine for now at least. It's a code smell, but we have stuff like isLoading and isFetching for a reason.

@erezrokah erezrokah force-pushed the feat/commit_media_with_post branch from 7554134 to 4dfd87f Compare November 17, 2019 08:56
@erezrokah erezrokah merged commit 6515dee into master Nov 17, 2019
MilesRomney pushed a commit to MilesRomney/netlify-cms that referenced this pull request Nov 18, 2019
* 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
@erezrokah erezrokah deleted the feat/commit_media_with_post branch November 26, 2019 13:34
@erezrokah erezrokah mentioned this pull request Dec 15, 2019
5 tasks
nathankitchen pushed a commit to nathankitchen/netlify-cms-backend-azure that referenced this pull request Feb 24, 2020
* 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
vladdu pushed a commit to vladdu/netlify-cms that referenced this pull request Jan 26, 2021
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature Request: Media committed with Post / Collection Item
3 participants