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

Upload images as part of editorial workflow #2397

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions packages/netlify-cms-backend-github/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export default class API {

persistFiles(entry, mediaFiles, options) {
const uploadPromises = [];
const files = entry ? mediaFiles.concat(entry) : mediaFiles;
const files = entry && entry.path ? mediaFiles.concat(entry) : mediaFiles;

files.forEach(file => {
if (file.uploaded) {
Expand Down Expand Up @@ -372,10 +372,13 @@ export default class API {
});
}

editorialWorkflowGit(fileTree, entry, filesList, options) {
const contentKey = entry.slug;
async editorialWorkflowGit(fileTree, entry, filesList, options) {
const contentKey = (entry && entry.slug) || options.slug;
const branchName = this.generateBranchName(contentKey);
const unpublished = options.unpublished || false;

const metadata = await this.retrieveMetadata(contentKey);
// Check if the meta is from a media only PR
const unpublished = options.unpublished || (metadata && !!metadata.isMediaOnlyPR) || false;
if (!unpublished) {
// Open new editorial review workflow for this entry - Create new metadata and commit to new branch`
let prResponse;
Expand All @@ -384,7 +387,7 @@ export default class API {
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
.then(changeTree => this.commit(options.commitMessage, changeTree))
.then(commitResponse => this.createBranch(branchName, commitResponse.sha))
.then(() => this.createPR(options.commitMessage, branchName))
.then(() => this.createPR(options.PRName || options.commitMessage, branchName))
.then(pr => {
prResponse = pr;
return this.user();
Expand All @@ -404,12 +407,13 @@ export default class API {
description: options.parsedData && options.parsedData.description,
objects: {
entry: {
path: entry.path,
sha: entry.sha,
path: (entry && entry.path) || '',
sha: (entry && entry.sha) || '',
},
files: filesList,
},
timeStamp: new Date().toISOString(),
isMediaOnlyPR: options.isMediaOnlyPR || false,
});
});
} else {
Expand All @@ -428,10 +432,14 @@ export default class API {
const files = [...metadataFiles, ...filesList];
const pr = { ...metadata.pr, head: newHead.sha };
const objects = {
entry: { path: entry.path, sha: entry.sha },
entry: {
path: (entry && entry.path) || '',
sha: (entry && entry.sha) || '',
},
files: uniq(files),
};
const updatedMetadata = { ...metadata, pr, title, description, objects };
const isMediaOnlyPR = options.isMediaOnlyPR || false;
const updatedMetadata = { ...metadata, pr, title, description, objects, isMediaOnlyPR };

/**
* If an asset store is in use, assets are always accessible, so we
Expand All @@ -448,7 +456,13 @@ export default class API {
* repo, which means pull requests opened for editorial workflow
* entries must be rebased if assets have been added or removed.
*/
return this.rebasePullRequest(pr.number, branchName, contentKey, metadata, newHead);
return this.rebasePullRequest(
pr.number,
branchName,
contentKey,
updatedMetadata,
newHead,
);
});
}
}
Expand Down
52 changes: 50 additions & 2 deletions packages/netlify-cms-core/src/actions/mediaLibrary.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Map } from 'immutable';
import { actions as notifActions } from 'redux-notifications';
import { getBlobSHA } from 'netlify-cms-lib-util';
import { currentBackend } from 'coreSrc/backend';
import { currentBackend, slugFormatter } from 'coreSrc/backend';
import { createAssetProxy } from 'ValueObjects/AssetProxy';
import { selectIntegration } from 'Reducers';
import { getIntegrationProvider } from 'Integrations';
import { addAsset } from './media';
import { sanitizeSlug } from 'Lib/urlHelper';
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';

const { notifSend } = notifActions;

Expand Down Expand Up @@ -141,6 +142,53 @@ export function persistMedia(file, opts = {}) {
const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug'));
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);

const entryDraft = state.entryDraft;
const config = state.config;
const publishMode = state.config.get('publish_mode');
const options = {
mode: publishMode,
slug: `upload_${fileName}`,
};

if (publishMode === EDITORIAL_WORKFLOW) {
const tempCollectionName = state.entryDraft.getIn(['entry', 'collection']); // May be set to draft sometimes.
const collectionName =
tempCollectionName === 'draft'
? state.entryDraft.getIn(['entry', 'metadata', 'collection'])
: tempCollectionName;
const collection = state.collections.get(collectionName) || '';
const collectionLabel = collection && collection.get('label');
let slug;
try {
slug =
entryDraft.getIn(['entry', 'slug']) ||
slugFormatter(collection, entryDraft.getIn(['entry', 'data']), config.get('slug'));
} catch (e) {
console.error(e);
dispatch({ type: MEDIA_LIBRARY_CLOSE });
dispatch(
notifSend({
message: {
key: 'ui.toast.missingRequiredFieldsForSlugForUpload',
},
kind: 'danger',
dismissAfter: 10000,
}),
);
return;
}

options.PRName = `Create ${collectionLabel} "${slug}"`;
options.slug = slug;
options.collectionName = collectionName;
options.isMediaOnlyPR = true;
options.useWorkflow = true;
if (tempCollectionName === 'draft') {
// This is a draft. Update the branch.
options.unpublished = true;
}
}

/**
* Check for existing files of the same name before persisting. If no asset
* store integration is used, files are being stored in Git, so we can
Expand All @@ -162,7 +210,7 @@ export function persistMedia(file, opts = {}) {
const assetProxy = await createAssetProxy(fileName, file, false, privateUpload);
dispatch(addAsset(assetProxy));
if (!integration) {
const asset = await backend.persistMedia(state.config, assetProxy);
const asset = await backend.persistMedia(state.config, assetProxy, options);
const displayURL = asset.displayURL || URL.createObjectURL(file);
return dispatch(mediaPersisted({ id, displayURL, ...asset }));
}
Expand Down
9 changes: 5 additions & 4 deletions packages/netlify-cms-core/src/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function getLabelForFileCollectionEntry(collection, path) {
return files && files.find(f => f.get('file') === path).get('label');
}

function slugFormatter(collection, entryData, slugConfig) {
export function slugFormatter(collection, entryData, slugConfig) {
const template = collection.get('slug') || '{{slug}}';

const identifier = entryData.get(selectIdentifier(collection));
Expand Down Expand Up @@ -686,11 +686,12 @@ class Backend {
return this.implementation.persistEntry(entryObj, MediaFiles, opts).then(() => entryObj.slug);
}

persistMedia(config, file) {
const options = {
persistMedia(config, file, options) {
const modifiedOptions = {
commitMessage: commitMessageFormatter('uploadMedia', config, { path: file.path }),
...options,
};
return this.implementation.persistMedia(file, options);
return this.implementation.persistMedia(file, modifiedOptions);
}

deleteEntry(config, collection, slug) {
Expand Down
2 changes: 2 additions & 0 deletions packages/netlify-cms-core/src/constants/defaultPhrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export function getPhrases() {
onFailToUpdateStatus: 'Failed to update status: %{details}',
missingRequiredField:
"Oops, you've missed a required field. Please complete before saving.",
missingRequiredFieldsForSlugForUpload:
'Unable to create slug for this entry, make sure you have filled out all required fields BEFORE trying to upload media.',
entrySaved: 'Entry saved',
entryPublished: 'Entry published',
onFailToPublishEntry: 'Failed to publish: %{details}',
Expand Down
2 changes: 1 addition & 1 deletion packages/netlify-cms-core/src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ export const getAsset = (state, path) => {
if (state.mediaLibrary.get('externalLibrary')) {
return path;
}
return fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
return fromMedias.getAsset(state.config.get('public_folder'), state.medias, path, state);
};
25 changes: 23 additions & 2 deletions packages/netlify-cms-core/src/reducers/medias.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Map } from 'immutable';
import { resolvePath } from 'netlify-cms-lib-util';
import { ADD_ASSET, REMOVE_ASSET } from 'Actions/media';
import AssetProxy from 'ValueObjects/AssetProxy';
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';

const medias = (state = Map(), action) => {
switch (action.type) {
Expand All @@ -18,16 +19,36 @@ const medias = (state = Map(), action) => {
export default medias;

const memoizedProxies = {};
export const getAsset = (publicFolder, state, path) => {
export const getAsset = (publicFolder, mediaState, path, state) => {
// No path provided, skip
if (!path) return null;

let proxy = state.get(path) || memoizedProxies[path];
let proxy = mediaState.get(path) || memoizedProxies[path];
if (proxy) {
// There is already an AssetProxy in memmory for this path. Use it.
return proxy;
}

// Get information we need to see if this is in a unpublished branch.
const entryDraft = state.entryDraft;
const publishMode = state.config.getIn(['publish_mode'], '');
const isDraft = entryDraft.getIn(['entry', 'collection'], false) === 'draft';
const isGithub = state.config.getIn(['backend', 'name'], false) === 'github';

// Hanldes github editorial mode by using the uploaded branch URL to show image
if (publishMode === EDITORIAL_WORKFLOW && isGithub && isDraft) {
const branch = entryDraft.getIn(['entry', 'metaData', 'branch'], false);
const repo = state.config.getIn(['backend', 'repo'], false);
const mediaFolder = state.config.getIn(['media_folder'], false);
const fileName = path.replace(publicFolder + '/', '');
proxy = memoizedProxies[path] = new AssetProxy(
`https://raw.githubusercontent.com/${repo}/${branch}/${mediaFolder}/${fileName}`,
null,
true,
);
return proxy;
}

// Create a new AssetProxy (for consistency) and return it.
proxy = memoizedProxies[path] = new AssetProxy(resolvePath(path, publicFolder), null, true);
return proxy;
Expand Down