Open Authoring bugfixes and pagination improvements (#2523)

* Fix handling of displayURLs which are strings

* Add fromFetchArguments to unsentRequest

* Add parseLinkHeader to backendUtil

* Handle paginated endpoints in GitHub API

* Rename fork workflow to Open Authoring across the whole repo

* Fixes for bugs in GitHub API introduced by Open Authoring changes

* Fix getDeployPreview

* Fix incorrect auth header formatting GitHub implementation

cf. https://github.com/netlify/netlify-cms/pull/2456#discussion_r309633387

* Remove unused and broken method from GitHub API

cf. https://github.com/netlify/netlify-cms/pull/2456#discussion_r308687145

* Fix editorialWorkflowGit method in GitHub API

* Request published entry content from origin repo

* Better error when deleting a published post in Open Authoring

* Rename to Open Authoring in fork request message

Also adds a note to the fork request message that an existing fork of
the same repo will be used automatically.

* fix linting
This commit is contained in:
Benaiah Mischenko 2019-08-24 10:54:59 -07:00 committed by Shawn Erquhart
parent 66da66affd
commit 34e1f09105
16 changed files with 223 additions and 151 deletions

View File

@ -3,6 +3,7 @@ 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, uniq } from 'lodash';
import { map } from 'lodash/fp'; import { map } from 'lodash/fp';
import { import {
getPaginatedRequestIterator,
APIError, APIError,
EditorialWorkflowError, EditorialWorkflowError,
filterPromisesWith, filterPromisesWith,
@ -11,7 +12,7 @@ import {
resolvePromiseProperties, resolvePromiseProperties,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
const CMS_BRANCH_PREFIX = 'cms/'; const CMS_BRANCH_PREFIX = 'cms';
const replace404WithEmptyArray = err => (err && err.status === 404 ? [] : Promise.reject(err)); const replace404WithEmptyArray = err => (err && err.status === 404 ? [] : Promise.reject(err));
@ -21,7 +22,7 @@ export default class API {
this.token = config.token || false; this.token = config.token || false;
this.branch = config.branch || 'master'; this.branch = config.branch || 'master';
this.originRepo = config.originRepo; this.originRepo = config.originRepo;
this.useForkWorkflow = config.useForkWorkflow; this.useOpenAuthoring = config.useOpenAuthoring;
this.repo = config.repo || ''; this.repo = config.repo || '';
this.repoURL = `/repos/${this.repo}`; this.repoURL = `/repos/${this.repo}`;
this.originRepoURL = this.originRepo && `/repos/${this.originRepo}`; this.originRepoURL = this.originRepo && `/repos/${this.originRepo}`;
@ -83,6 +84,20 @@ export default class API {
return this.api_root + path; return this.api_root + path;
} }
parseResponse(response) {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
}
const textPromise = response.text().then(text => {
if (!response.ok) {
return Promise.reject(text);
}
return text;
});
return textPromise;
}
request(path, options = {}) { request(path, options = {}) {
const headers = this.requestHeaders(options.headers || {}); const headers = this.requestHeaders(options.headers || {});
const url = this.urlFor(path, options); const url = this.urlFor(path, options);
@ -90,23 +105,26 @@ export default class API {
return fetch(url, { ...options, headers }) return fetch(url, { ...options, headers })
.then(response => { .then(response => {
responseStatus = response.status; responseStatus = response.status;
const contentType = response.headers.get('Content-Type'); return this.parseResponse(response);
if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
}
const text = response.text();
if (!response.ok) {
return Promise.reject(text);
}
return text;
}) })
.catch(error => { .catch(error => {
throw new APIError(error.message, responseStatus, 'GitHub'); throw new APIError(error.message, responseStatus, 'GitHub');
}); });
} }
async requestAllPages(url, options = {}) {
const processedURL = this.urlFor(url, options);
const pagesIterator = getPaginatedRequestIterator(processedURL, options);
const pagesToParse = [];
for await (const page of pagesIterator) {
pagesToParse.push(this.parseResponse(page));
}
const pages = await Promise.all(pagesToParse);
return [].concat(...pages);
}
generateContentKey(collectionName, slug) { generateContentKey(collectionName, slug) {
if (!this.useForkWorkflow) { if (!this.useOpenAuthoring) {
// this doesn't use the collection, but we need to leave it that way for backwards // this doesn't use the collection, but we need to leave it that way for backwards
// compatibility // compatibility
return slug; return slug;
@ -115,22 +133,16 @@ export default class API {
return `${this.repo}/${collectionName}/${slug}`; return `${this.repo}/${collectionName}/${slug}`;
} }
generateBranchNameFromCollectionAndSlug(collectionName, slug) {
return this.generateContentKey(collectionName, slug).then(contentKey =>
this.generateBranchName(contentKey),
);
}
generateBranchName(contentKey) { generateBranchName(contentKey) {
return `${CMS_BRANCH_PREFIX}${contentKey}`; return `${CMS_BRANCH_PREFIX}/${contentKey}`;
} }
branchNameFromRef(ref) { branchNameFromRef(ref) {
return ref.substring('refs/heads/'.length - 1); return ref.substring('refs/heads/'.length);
} }
contentKeyFromRef(ref) { contentKeyFromRef(ref) {
return ref.substring(`refs/heads/${CMS_BRANCH_PREFIX}/`.length - 1); return ref.substring(`refs/heads/${CMS_BRANCH_PREFIX}/`.length);
} }
checkMetadataRef() { checkMetadataRef() {
@ -212,28 +224,34 @@ export default class API {
cache: 'no-store', cache: 'no-store',
}; };
if (!this.useForkWorkflow) { if (!this.useOpenAuthoring) {
return this.request(`${this.repoURL}/contents/${key}.json`, metadataRequestOptions) return this.request(`${this.repoURL}/contents/${key}.json`, metadataRequestOptions)
.then(response => JSON.parse(response)) .then(response => JSON.parse(response))
.catch(() => .catch(err => {
console.log( if (err.message === 'Not Found') {
'%c %s does not have metadata', console.log(
'line-height: 30px;text-align: center;font-weight: bold', '%c %s does not have metadata',
key, 'line-height: 30px;text-align: center;font-weight: bold',
), key,
); );
}
throw err;
});
} }
const [user, repo] = key.split('/'); const [user, repo] = key.split('/');
return this.request(`/repos/${user}/${repo}/contents/${key}.json`, metadataRequestOptions) return this.request(`/repos/${user}/${repo}/contents/${key}.json`, metadataRequestOptions)
.then(response => JSON.parse(response)) .then(response => JSON.parse(response))
.catch(() => .catch(err => {
console.log( if (err.message === 'Not Found') {
'%c %s does not have metadata', console.log(
'line-height: 30px;text-align: center;font-weight: bold', '%c %s does not have metadata',
key, 'line-height: 30px;text-align: center;font-weight: bold',
), key,
); );
}
throw err;
});
}); });
} }
@ -251,22 +269,22 @@ export default class API {
.split('/') .split('/')
.slice(0, -1) .slice(0, -1)
.join('/'); .join('/');
return this.listFiles(dir) return this.listFiles(dir, { repoURL, branch })
.then(files => files.find(file => file.path === path)) .then(files => files.find(file => file.path === path))
.then(file => this.getBlob(file.sha)); .then(file => this.getBlob(file.sha, { repoURL }));
} }
throw error; throw error;
}); });
} }
} }
getBlob(sha) { getBlob(sha, { repoURL = this.repoURL } = {}) {
return localForage.getItem(`gh.${sha}`).then(cached => { return localForage.getItem(`gh.${sha}`).then(cached => {
if (cached) { if (cached) {
return cached; return cached;
} }
return this.request(`${this.repoURL}/git/blobs/${sha}`, { return this.request(`${repoURL}/git/blobs/${sha}`, {
headers: { Accept: 'application/vnd.github.VERSION.raw' }, headers: { Accept: 'application/vnd.github.VERSION.raw' },
}).then(result => { }).then(result => {
localForage.setItem(`gh.${sha}`, result); localForage.setItem(`gh.${sha}`, result);
@ -275,9 +293,9 @@ export default class API {
}); });
} }
listFiles(path) { listFiles(path, { repoURL = this.repoURL, branch = this.branch } = {}) {
return this.request(`${this.repoURL}/contents/${path.replace(/\/$/, '')}`, { return this.request(`${repoURL}/contents/${path.replace(/\/$/, '')}`, {
params: { ref: this.branch }, params: { ref: branch },
}) })
.then(files => { .then(files => {
if (!Array.isArray(files)) { if (!Array.isArray(files)) {
@ -292,7 +310,7 @@ export default class API {
const metaDataPromise = this.retrieveMetadata(contentKey).then(data => const metaDataPromise = this.retrieveMetadata(contentKey).then(data =>
data.objects.entry.path ? data : Promise.reject(null), data.objects.entry.path ? data : Promise.reject(null),
); );
const repoURL = this.useForkWorkflow const repoURL = this.useOpenAuthoring
? `/repos/${contentKey ? `/repos/${contentKey
.split('/') .split('/')
.slice(0, 2) .slice(0, 2)
@ -317,7 +335,7 @@ export default class API {
isUnpublishedEntryModification(path, branch) { isUnpublishedEntryModification(path, branch) {
return this.readFile(path, null, { return this.readFile(path, null, {
branch, branch,
repoURL: this.useForkWorkflow ? this.originRepoURL : this.repoURL, repoURL: this.useOpenAuthoring ? this.originRepoURL : this.repoURL,
}) })
.then(() => true) .then(() => true)
.catch(err => { .catch(err => {
@ -338,8 +356,7 @@ export default class API {
// Get PRs with a `head` of `branchName`. Note that this is a // Get PRs with a `head` of `branchName`. Note that this is a
// substring match, so we need to check that the `head.ref` of // substring match, so we need to check that the `head.ref` of
// at least one of the returned objects matches `branchName`. // at least one of the returned objects matches `branchName`.
// TODO: this is a paginated endpoint return this.requestAllPages(`${repoURL}/pulls`, {
return this.request(`${repoURL}/pulls`, {
params: { params: {
head: usernameOfFork ? `${usernameOfFork}:${branchName}` : branchName, head: usernameOfFork ? `${usernameOfFork}:${branchName}` : branchName,
...(state ? { state } : {}), ...(state ? { state } : {}),
@ -350,10 +367,10 @@ export default class API {
branchHasPR = async ({ branchName, ...rest }) => { branchHasPR = async ({ branchName, ...rest }) => {
const prs = await this.getPRsForBranchName({ branchName, ...rest }); const prs = await this.getPRsForBranchName({ branchName, ...rest });
return prs.some(pr => this.branchNameFromRef(pr.head.ref) === branchName); return prs.some(pr => pr.head.ref === branchName);
}; };
getUpdatedForkWorkflowMetadata = async (contentKey, { metadata: metadataArg } = {}) => { getUpdatedOpenAuthoringMetadata = async (contentKey, { metadata: metadataArg } = {}) => {
const metadata = metadataArg || (await this.retrieveMetadata(contentKey)) || {}; const metadata = metadataArg || (await this.retrieveMetadata(contentKey)) || {};
const { pr: prMetadata, status } = metadata; const { pr: prMetadata, status } = metadata;
@ -402,10 +419,10 @@ export default class API {
const onlyBranchesWithOpenPRs = filterPromisesWith(({ ref }) => const onlyBranchesWithOpenPRs = filterPromisesWith(({ ref }) =>
this.branchHasPR({ branchName: this.branchNameFromRef(ref), state: 'open' }), this.branchHasPR({ branchName: this.branchNameFromRef(ref), state: 'open' }),
); );
const getUpdatedForkWorkflowBranches = flow([ const getUpdatedOpenAuthoringBranches = flow([
map(async branch => { map(async branch => {
const contentKey = this.contentKeyFromRef(branch.ref); const contentKey = this.contentKeyFromRef(branch.ref);
const metadata = await this.getUpdatedForkWorkflowMetadata(contentKey); const metadata = await this.getUpdatedOpenAuthoringMetadata(contentKey);
// filter out removed entries // filter out removed entries
if (!metadata) { if (!metadata) {
return Promise.reject('Unpublished entry was removed'); return Promise.reject('Unpublished entry was removed');
@ -418,8 +435,8 @@ export default class API {
const branches = await this.request(`${this.repoURL}/git/refs/heads/cms`).catch( const branches = await this.request(`${this.repoURL}/git/refs/heads/cms`).catch(
replace404WithEmptyArray, replace404WithEmptyArray,
); );
const filterFunction = this.useForkWorkflow const filterFunction = this.useOpenAuthoring
? getUpdatedForkWorkflowBranches ? getUpdatedOpenAuthoringBranches
: onlyBranchesWithOpenPRs; : onlyBranchesWithOpenPRs;
return await filterFunction(branches); return await filterFunction(branches);
} catch (err) { } catch (err) {
@ -436,7 +453,7 @@ export default class API {
* concept of entry "status". Useful for things like deploy preview links. * concept of entry "status". Useful for things like deploy preview links.
*/ */
async getStatuses(sha) { async getStatuses(sha) {
const repoURL = this.useForkWorkflow ? this.originRepoURL : this.repoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
try { try {
const resp = await this.request(`${repoURL}/commits/${sha}/status`); const resp = await this.request(`${repoURL}/commits/${sha}/status`);
return resp.statuses; return resp.statuses;
@ -501,6 +518,9 @@ export default class API {
} }
deleteFile(path, message, options = {}) { deleteFile(path, message, options = {}) {
if (this.useOpenAuthoring) {
return Promise.reject('Cannot delete published entries as an Open Authoring user!');
}
const branch = options.branch || this.branch; const branch = options.branch || this.branch;
const pathArray = path.split('/'); const pathArray = path.split('/');
const filename = last(pathArray); const filename = last(pathArray);
@ -531,14 +551,14 @@ export default class API {
const contentKey = this.generateContentKey(options.collectionName, entry.slug); const contentKey = this.generateContentKey(options.collectionName, entry.slug);
const branchName = this.generateBranchName(contentKey); const branchName = this.generateBranchName(contentKey);
const unpublished = options.unpublished || false; const unpublished = options.unpublished || false;
const branchData = await this.getBranch();
if (!unpublished) { if (!unpublished) {
// Open new editorial review workflow for this entry - Create new metadata and commit to new branch // Open new editorial review workflow for this entry - Create new metadata and commit to new branch
const userPromise = this.user(); const userPromise = this.user();
const branchData = await this.getBranch();
const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree); const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree);
const commitResponse = await this.commit(options.commitMessage, changeTree); const commitResponse = await this.commit(options.commitMessage, changeTree);
await this.createBranch(branchName, commitResponse.sha); await this.createBranch(branchName, commitResponse.sha);
const pr = this.useForkWorkflow const pr = this.useOpenAuthoring
? undefined ? undefined
: await this.createPR(options.commitMessage, branchName); : await this.createPR(options.commitMessage, branchName);
const user = await userPromise; const user = await userPromise;
@ -568,6 +588,7 @@ export default class API {
}); });
} else { } else {
// Entry is already on editorial review workflow - just update metadata and commit to existing branch // Entry is already on editorial review workflow - just update metadata and commit to existing branch
const branchData = await this.getBranch(branchName);
const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree); const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree);
const commitPromise = this.commit(options.commitMessage, changeTree); const commitPromise = this.commit(options.commitMessage, changeTree);
const metadataPromise = this.retrieveMetadata(contentKey); const metadataPromise = this.retrieveMetadata(contentKey);
@ -710,7 +731,7 @@ export default class API {
* Get a pull request by PR number. * Get a pull request by PR number.
*/ */
getPullRequest(prNumber) { getPullRequest(prNumber) {
const repoURL = this.useForkWorkflow ? this.repoURL : this.originRepoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
return this.request(`${repoURL}/pulls/${prNumber} }`); return this.request(`${repoURL}/pulls/${prNumber} }`);
} }
@ -718,7 +739,7 @@ export default class API {
* Get the list of commits for a given pull request. * Get the list of commits for a given pull request.
*/ */
getPullRequestCommits(prNumber) { getPullRequestCommits(prNumber) {
const repoURL = this.useForkWorkflow ? this.repoURL : this.originRepoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
return this.request(`${repoURL}/pulls/${prNumber}/commits`); return this.request(`${repoURL}/pulls/${prNumber}/commits`);
} }
@ -744,7 +765,7 @@ export default class API {
const contentKey = this.generateContentKey(collectionName, slug); const contentKey = this.generateContentKey(collectionName, slug);
const metadata = await this.retrieveMetadata(contentKey); const metadata = await this.retrieveMetadata(contentKey);
if (!this.useForkWorkflow) { if (!this.useOpenAuthoring) {
return this.storeMetadata(contentKey, { return this.storeMetadata(contentKey, {
...metadata, ...metadata,
status, status,
@ -752,7 +773,7 @@ export default class API {
} }
if (status === 'pending_publish') { if (status === 'pending_publish') {
throw new Error('Fork workflow entries may not be set to the status "pending_publish".'); throw new Error('Open Authoring entries may not be set to the status "pending_publish".');
} }
const { pr: prMetadata } = metadata; const { pr: prMetadata } = metadata;
@ -809,8 +830,8 @@ export default class API {
); );
} }
publishUnpublishedEntry(collection, slug) { publishUnpublishedEntry(collectionName, slug) {
const contentKey = this.generateContentKey(collection.get('name'), slug); const contentKey = this.generateContentKey(collectionName, slug);
const branchName = this.generateBranchName(contentKey); const branchName = this.generateBranchName(contentKey);
return this.retrieveMetadata(contentKey) return this.retrieveMetadata(contentKey)
.then(metadata => this.mergePR(metadata.pr, metadata.objects)) .then(metadata => this.mergePR(metadata.pr, metadata.objects))
@ -847,7 +868,7 @@ export default class API {
} }
assertCmsBranch(branchName) { assertCmsBranch(branchName) {
return branchName.startsWith(CMS_BRANCH_PREFIX); return branchName.startsWith(`${CMS_BRANCH_PREFIX}/`);
} }
patchBranch(branchName, sha, opts = {}) { patchBranch(branchName, sha, opts = {}) {
@ -864,8 +885,8 @@ export default class API {
async createPR(title, head, base = this.branch) { async createPR(title, head, base = this.branch) {
const body = 'Automatically generated by Netlify CMS'; const body = 'Automatically generated by Netlify CMS';
const repoURL = this.useForkWorkflow ? this.originRepoURL : this.repoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
const headReference = this.useForkWorkflow ? `${(await this.user()).login}:${head}` : head; const headReference = this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head;
return this.request(`${repoURL}/pulls`, { return this.request(`${repoURL}/pulls`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ title, body, head: headReference, base }), body: JSON.stringify({ title, body, head: headReference, base }),
@ -874,7 +895,7 @@ export default class API {
async openPR(pullRequest) { async openPR(pullRequest) {
const { number } = pullRequest; const { number } = pullRequest;
const repoURL = this.useForkWorkflow ? this.originRepoURL : this.repoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
console.log('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold'); console.log('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold');
return this.request(`${repoURL}/pulls/${number}`, { return this.request(`${repoURL}/pulls/${number}`, {
method: 'PATCH', method: 'PATCH',
@ -886,7 +907,7 @@ export default class API {
closePR(pullrequest) { closePR(pullrequest) {
const prNumber = pullrequest.number; const prNumber = pullrequest.number;
const repoURL = this.useForkWorkflow ? this.originRepoURL : this.repoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold'); console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold');
return this.request(`${repoURL}/pulls/${prNumber}`, { return this.request(`${repoURL}/pulls/${prNumber}`, {
method: 'PATCH', method: 'PATCH',
@ -899,7 +920,7 @@ export default class API {
mergePR(pullrequest, objects) { mergePR(pullrequest, objects) {
const headSha = pullrequest.head; const headSha = pullrequest.head;
const prNumber = pullrequest.number; const prNumber = pullrequest.number;
const repoURL = this.useForkWorkflow ? this.originRepoURL : this.repoURL; const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold'); console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold');
return this.request(`${repoURL}/pulls/${prNumber}/merge`, { return this.request(`${repoURL}/pulls/${prNumber}/merge`, {
method: 'PUT', method: 'PUT',

View File

@ -50,7 +50,7 @@ export default class GitHubAuthenticationPage extends React.Component {
}); });
}; };
loginWithForkWorkflow(data) { loginWithOpenAuthoring(data) {
const { backend } = this.props; const { backend } = this.props;
this.setState({ findingFork: true }); this.setState({ findingFork: true });
@ -80,8 +80,8 @@ export default class GitHubAuthenticationPage extends React.Component {
this.setState({ loginError: err.toString() }); this.setState({ loginError: err.toString() });
return; return;
} }
if (this.props.config.getIn(['backend', 'fork_workflow'])) { if (this.props.config.getIn(['backend', 'open_authoring'])) {
return this.loginWithForkWorkflow(data).then(() => this.props.onLogin(data)); return this.loginWithOpenAuthoring(data).then(() => this.props.onLogin(data));
} }
this.props.onLogin(data); this.props.onLogin(data);
}); });
@ -105,7 +105,10 @@ export default class GitHubAuthenticationPage extends React.Component {
return { return {
renderPageContent: ({ LoginButton }) => ( renderPageContent: ({ LoginButton }) => (
<ForkApprovalContainer> <ForkApprovalContainer>
<p>Forking workflow is enabled: we need to use a fork on your github account.</p> <p>
Open Authoring is enabled: we need to use a fork on your github account. (If a fork
already exists, we&#39;ll use that.)
</p>
<ForkButtonsContainer> <ForkButtonsContainer>
<LoginButton onClick={approveFork}>Fork the repo</LoginButton> <LoginButton onClick={approveFork}>Fork the repo</LoginButton>
<LoginButton onClick={refuseFork}>Don&#39;t fork the repo</LoginButton> <LoginButton onClick={refuseFork}>Don&#39;t fork the repo</LoginButton>

View File

@ -50,11 +50,11 @@ export default class GitHub {
this.api = this.options.API || null; this.api = this.options.API || null;
this.forkWorkflowEnabled = config.getIn(['backend', 'fork_workflow'], false); this.openAuthoringEnabled = config.getIn(['backend', 'open_authoring'], false);
if (this.forkWorkflowEnabled) { if (this.openAuthoringEnabled) {
if (!this.options.useWorkflow) { if (!this.options.useWorkflow) {
throw new Error( throw new Error(
'backend.fork_workflow is true but publish_mode is not set to editorial_workflow.', 'backend.open_authoring is true but publish_mode is not set to editorial_workflow.',
); );
} }
this.originRepo = config.getIn(['backend', 'repo'], ''); this.originRepo = config.getIn(['backend', 'repo'], '');
@ -74,7 +74,7 @@ export default class GitHub {
} }
restoreUser(user) { restoreUser(user) {
return this.forkWorkflowEnabled return this.openAuthoringEnabled
? this.authenticateWithFork({ userData: user, getPermissionToFork: () => true }).then(() => ? this.authenticateWithFork({ userData: user, getPermissionToFork: () => true }).then(() =>
this.authenticate(user), this.authenticate(user),
) )
@ -86,7 +86,7 @@ export default class GitHub {
var repoExists = false; var repoExists = false;
while (!repoExists) { while (!repoExists) {
repoExists = await fetch(`${this.api_root}/repos/${repo}`, { repoExists = await fetch(`${this.api_root}/repos/${repo}`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `token ${token}` },
}) })
.then(() => true) .then(() => true)
.catch(err => (err && err.status === 404 ? false : Promise.reject(err))); .catch(err => (err && err.status === 404 ? false : Promise.reject(err)));
@ -102,7 +102,7 @@ export default class GitHub {
if (!this._currentUserPromise) { if (!this._currentUserPromise) {
this._currentUserPromise = fetch(`${this.api_root}/user`, { this._currentUserPromise = fetch(`${this.api_root}/user`, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `token ${token}`,
}, },
}).then(res => res.json()); }).then(res => res.json());
} }
@ -117,7 +117,7 @@ export default class GitHub {
`${this.api_root}/repos/${this.originRepo}/collaborators/${username}/permission`, `${this.api_root}/repos/${this.originRepo}/collaborators/${username}/permission`,
{ {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `token ${token}`,
}, },
}, },
) )
@ -128,15 +128,15 @@ export default class GitHub {
} }
async authenticateWithFork({ userData, getPermissionToFork }) { async authenticateWithFork({ userData, getPermissionToFork }) {
if (!this.forkWorkflowEnabled) { if (!this.openAuthoringEnabled) {
throw new Error('Cannot authenticate with fork; forking workflow is turned off.'); throw new Error('Cannot authenticate with fork; Open Authoring is turned off.');
} }
const { token } = userData; const { token } = userData;
// Origin maintainers should be able to use the CMS normally // Origin maintainers should be able to use the CMS normally
if (await this.userIsOriginMaintainer({ token })) { if (await this.userIsOriginMaintainer({ token })) {
this.repo = this.originRepo; this.repo = this.originRepo;
this.useForkWorkflow = false; this.useOpenAuthoring = false;
return Promise.resolve(); return Promise.resolve();
} }
@ -145,10 +145,10 @@ export default class GitHub {
const fork = await fetch(`${this.api_root}/repos/${this.originRepo}/forks`, { const fork = await fetch(`${this.api_root}/repos/${this.originRepo}/forks`, {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `token ${token}`,
}, },
}).then(res => res.json()); }).then(res => res.json());
this.useForkWorkflow = true; this.useOpenAuthoring = true;
this.repo = fork.full_name; this.repo = fork.full_name;
return this.pollUntilForkExists({ repo: fork.full_name, token }); return this.pollUntilForkExists({ repo: fork.full_name, token });
} }
@ -159,10 +159,10 @@ export default class GitHub {
token: this.token, token: this.token,
branch: this.branch, branch: this.branch,
repo: this.repo, repo: this.repo,
originRepo: this.useForkWorkflow ? this.originRepo : undefined, originRepo: this.useOpenAuthoring ? this.originRepo : undefined,
api_root: this.api_root, api_root: this.api_root,
squash_merges: this.squash_merges, squash_merges: this.squash_merges,
useForkWorkflow: this.useForkWorkflow, useOpenAuthoring: this.useOpenAuthoring,
initialWorkflowStatus: this.options.initialWorkflowStatus, initialWorkflowStatus: this.options.initialWorkflowStatus,
}); });
const user = await this.api.user(); const user = await this.api.user();
@ -186,7 +186,7 @@ export default class GitHub {
} }
// Authorized user // Authorized user
return { ...user, token: state.token, useForkWorkflow: this.useForkWorkflow }; return { ...user, token: state.token, useOpenAuthoring: this.useOpenAuthoring };
} }
logout() { logout() {
@ -199,14 +199,14 @@ export default class GitHub {
} }
async entriesByFolder(collection, extension) { async entriesByFolder(collection, extension) {
const repoURL = `/repos/${this.useForkWorkflow ? this.originRepo : this.repo}`; const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
const files = await this.api.listFiles(collection.get('folder')); const files = await this.api.listFiles(collection.get('folder'), { repoURL });
const filteredFiles = files.filter(file => file.name.endsWith('.' + extension)); const filteredFiles = files.filter(file => file.name.endsWith('.' + extension));
return this.fetchFiles(filteredFiles, { repoURL }); return this.fetchFiles(filteredFiles, { repoURL });
} }
entriesByFiles(collection) { entriesByFiles(collection) {
const repoURL = `/repos/${this.useForkWorkflow ? this.originRepo : this.repo}`; const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
const files = collection.get('files').map(collectionFile => ({ const files = collection.get('files').map(collectionFile => ({
path: collectionFile.get('file'), path: collectionFile.get('file'),
label: collectionFile.get('label'), label: collectionFile.get('label'),
@ -243,7 +243,7 @@ export default class GitHub {
// Fetches a single entry. // Fetches a single entry.
getEntry(collection, slug, path) { getEntry(collection, slug, path) {
const repoURL = `/repos/${this.useForkWorkflow ? this.originRepo : this.repo}`; const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
return this.api.readFile(path, null, { repoURL }).then(data => ({ return this.api.readFile(path, null, { repoURL }).then(data => ({
file: { path }, file: { path },
data, data,
@ -365,7 +365,8 @@ export default class GitHub {
return null; return null;
} }
const statuses = await this.api.getStatuses(data.pr.head); const headSHA = typeof data.pr.head === 'string' ? data.pr.head : data.pr.head.sha;
const statuses = await this.api.getStatuses(headSHA);
const deployStatus = getPreviewStatus(statuses, this.config); const deployStatus = getPreviewStatus(statuses, this.config);
if (deployStatus) { if (deployStatus) {

View File

@ -1,6 +1,13 @@
import { localForage, unsentRequest, then, APIError, Cursor } from 'netlify-cms-lib-util'; import {
localForage,
parseLinkHeader,
unsentRequest,
then,
APIError,
Cursor,
} from 'netlify-cms-lib-util';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { fromJS, List, Map } from 'immutable'; import { fromJS, Map } from 'immutable';
import { flow, partial, result } from 'lodash'; import { flow, partial, result } from 'lodash';
export default class API { export default class API {
@ -114,20 +121,8 @@ export default class API {
const pageCount = parseInt(headers.get('X-Total-Pages'), 10) - 1; const pageCount = parseInt(headers.get('X-Total-Pages'), 10) - 1;
const pageSize = parseInt(headers.get('X-Per-Page'), 10); const pageSize = parseInt(headers.get('X-Per-Page'), 10);
const count = parseInt(headers.get('X-Total'), 10); const count = parseInt(headers.get('X-Total'), 10);
const linksRaw = headers.get('Link'); const links = parseLinkHeader(headers.get('Link'));
const links = List(linksRaw.split(',')) const actions = Map(links)
.map(str => str.trim().split(';'))
.map(([linkStr, keyStr]) => [
keyStr.match(/rel="(.*?)"/)[1],
unsentRequest.fromURL(
linkStr
.trim()
.match(/<(.*?)>/)[1]
.replace(/\+/g, '%20'),
),
])
.update(list => Map(list));
const actions = links
.keySeq() .keySeq()
.flatMap(key => .flatMap(key =>
(key === 'prev' && index > 0) || (key === 'prev' && index > 0) ||

View File

@ -7,7 +7,7 @@ export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS'; export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILURE = 'AUTH_FAILURE'; export const AUTH_FAILURE = 'AUTH_FAILURE';
export const AUTH_REQUEST_DONE = 'AUTH_REQUEST_DONE'; export const AUTH_REQUEST_DONE = 'AUTH_REQUEST_DONE';
export const USE_FORK_WORKFLOW = 'USE_FORK_WORKFLOW'; export const USE_OPEN_AUTHORING = 'USE_OPEN_AUTHORING';
export const LOGOUT = 'LOGOUT'; export const LOGOUT = 'LOGOUT';
export function authenticating() { export function authenticating() {
@ -37,9 +37,9 @@ export function doneAuthenticating() {
}; };
} }
export function useForkWorkflow() { export function useOpenAuthoring() {
return { return {
type: USE_FORK_WORKFLOW, type: USE_OPEN_AUTHORING,
}; };
} }
@ -59,8 +59,8 @@ export function authenticateUser() {
.currentUser() .currentUser()
.then(user => { .then(user => {
if (user) { if (user) {
if (user.useForkWorkflow) { if (user.useOpenAuthoring) {
dispatch(useForkWorkflow()); dispatch(useOpenAuthoring());
} }
dispatch(authenticate(user)); dispatch(authenticate(user));
} else { } else {
@ -83,8 +83,8 @@ export function loginUser(credentials) {
return backend return backend
.authenticate(credentials) .authenticate(credentials)
.then(user => { .then(user => {
if (user.useForkWorkflow) { if (user.useOpenAuthoring) {
dispatch(useForkWorkflow()); dispatch(useOpenAuthoring());
} }
dispatch(authenticate(user)); dispatch(authenticate(user));
}) })

View File

@ -250,9 +250,10 @@ export function loadMediaDisplayURL(file) {
) { ) {
return Promise.resolve(); return Promise.resolve();
} }
if (typeof url === 'string' || typeof displayURL === 'string') { if (typeof displayURL === 'string') {
dispatch(mediaDisplayURLRequest(id)); dispatch(mediaDisplayURLRequest(id));
dispatch(mediaDisplayURLSuccess(id, displayURL)); dispatch(mediaDisplayURLSuccess(id, displayURL));
return;
} }
try { try {
const backend = currentBackend(state.config); const backend = currentBackend(state.config);

View File

@ -61,7 +61,7 @@ class Editor extends React.Component {
newEntry: PropTypes.bool.isRequired, newEntry: PropTypes.bool.isRequired,
displayUrl: PropTypes.string, displayUrl: PropTypes.string,
hasWorkflow: PropTypes.bool, hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool, useOpenAuthoring: PropTypes.bool,
unpublishedEntry: PropTypes.bool, unpublishedEntry: PropTypes.bool,
isModification: PropTypes.bool, isModification: PropTypes.bool,
collectionEntriesLoaded: PropTypes.bool, collectionEntriesLoaded: PropTypes.bool,
@ -351,7 +351,7 @@ class Editor extends React.Component {
hasChanged, hasChanged,
displayUrl, displayUrl,
hasWorkflow, hasWorkflow,
useForkWorkflow, useOpenAuthoring,
unpublishedEntry, unpublishedEntry,
newEntry, newEntry,
isModification, isModification,
@ -399,7 +399,7 @@ class Editor extends React.Component {
hasChanged={hasChanged} hasChanged={hasChanged}
displayUrl={displayUrl} displayUrl={displayUrl}
hasWorkflow={hasWorkflow} hasWorkflow={hasWorkflow}
useForkWorkflow={useForkWorkflow} useOpenAuthoring={useOpenAuthoring}
hasUnpublishedChanges={unpublishedEntry} hasUnpublishedChanges={unpublishedEntry}
isNewEntry={newEntry} isNewEntry={newEntry}
isModification={isModification} isModification={isModification}
@ -425,7 +425,7 @@ function mapStateToProps(state, ownProps) {
const hasChanged = entryDraft.get('hasChanged'); const hasChanged = entryDraft.get('hasChanged');
const displayUrl = config.get('display_url'); const displayUrl = config.get('display_url');
const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW; const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
const useForkWorkflow = globalUI.get('useForkWorkflow', false); const useOpenAuthoring = globalUI.get('useOpenAuthoring', false);
const isModification = entryDraft.getIn(['entry', 'isModification']); const isModification = entryDraft.getIn(['entry', 'isModification']);
const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]); const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]);
const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug); const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug);
@ -445,7 +445,7 @@ function mapStateToProps(state, ownProps) {
hasChanged, hasChanged,
displayUrl, displayUrl,
hasWorkflow, hasWorkflow,
useForkWorkflow, useOpenAuthoring,
isModification, isModification,
collectionEntriesLoaded, collectionEntriesLoaded,
currentStatus, currentStatus,

View File

@ -166,7 +166,7 @@ class EditorInterface extends Component {
hasChanged, hasChanged,
displayUrl, displayUrl,
hasWorkflow, hasWorkflow,
useForkWorkflow, useOpenAuthoring,
hasUnpublishedChanges, hasUnpublishedChanges,
isNewEntry, isNewEntry,
isModification, isModification,
@ -241,7 +241,7 @@ class EditorInterface extends Component {
displayUrl={displayUrl} displayUrl={displayUrl}
collection={collection} collection={collection}
hasWorkflow={hasWorkflow} hasWorkflow={hasWorkflow}
useForkWorkflow={useForkWorkflow} useOpenAuthoring={useOpenAuthoring}
hasUnpublishedChanges={hasUnpublishedChanges} hasUnpublishedChanges={hasUnpublishedChanges}
isNewEntry={isNewEntry} isNewEntry={isNewEntry}
isModification={isModification} isModification={isModification}
@ -295,7 +295,7 @@ EditorInterface.propTypes = {
hasChanged: PropTypes.bool, hasChanged: PropTypes.bool,
displayUrl: PropTypes.string, displayUrl: PropTypes.string,
hasWorkflow: PropTypes.bool, hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool, useOpenAuthoring: PropTypes.bool,
hasUnpublishedChanges: PropTypes.bool, hasUnpublishedChanges: PropTypes.bool,
isNewEntry: PropTypes.bool, isNewEntry: PropTypes.bool,
isModification: PropTypes.bool, isModification: PropTypes.bool,

View File

@ -218,7 +218,7 @@ class EditorToolbar extends React.Component {
displayUrl: PropTypes.string, displayUrl: PropTypes.string,
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
hasWorkflow: PropTypes.bool, hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool, useOpenAuthoring: PropTypes.bool,
hasUnpublishedChanges: PropTypes.bool, hasUnpublishedChanges: PropTypes.bool,
isNewEntry: PropTypes.bool, isNewEntry: PropTypes.bool,
isModification: PropTypes.bool, isModification: PropTypes.bool,
@ -380,7 +380,7 @@ class EditorToolbar extends React.Component {
onPublishAndNew, onPublishAndNew,
currentStatus, currentStatus,
isNewEntry, isNewEntry,
useForkWorkflow, useOpenAuthoring,
t, t,
} = this.props; } = this.props;
if (currentStatus) { if (currentStatus) {
@ -408,7 +408,7 @@ class EditorToolbar extends React.Component {
onClick={() => onChangeStatus('PENDING_REVIEW')} onClick={() => onChangeStatus('PENDING_REVIEW')}
icon={currentStatus === status.get('PENDING_REVIEW') && 'check'} icon={currentStatus === status.get('PENDING_REVIEW') && 'check'}
/> />
{useForkWorkflow ? ( {useOpenAuthoring ? (
'' ''
) : ( ) : (
<StatusDropdownItem <StatusDropdownItem
@ -418,7 +418,7 @@ class EditorToolbar extends React.Component {
/> />
)} )}
</ToolbarDropdown> </ToolbarDropdown>
{useForkWorkflow ? ( {useOpenAuthoring ? (
'' ''
) : ( ) : (
<ToolbarDropdown <ToolbarDropdown

View File

@ -55,7 +55,7 @@ class Workflow extends Component {
static propTypes = { static propTypes = {
collections: ImmutablePropTypes.orderedMap, collections: ImmutablePropTypes.orderedMap,
isEditorialWorkflow: PropTypes.bool.isRequired, isEditorialWorkflow: PropTypes.bool.isRequired,
isForkWorkflow: PropTypes.bool, isOpenAuthoring: PropTypes.bool,
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
unpublishedEntries: ImmutablePropTypes.map, unpublishedEntries: ImmutablePropTypes.map,
loadUnpublishedEntries: PropTypes.func.isRequired, loadUnpublishedEntries: PropTypes.func.isRequired,
@ -75,7 +75,7 @@ class Workflow extends Component {
render() { render() {
const { const {
isEditorialWorkflow, isEditorialWorkflow,
isForkWorkflow, isOpenAuthoring,
isFetching, isFetching,
unpublishedEntries, unpublishedEntries,
updateUnpublishedEntryStatus, updateUnpublishedEntryStatus,
@ -127,7 +127,7 @@ class Workflow extends Component {
handleChangeStatus={updateUnpublishedEntryStatus} handleChangeStatus={updateUnpublishedEntryStatus}
handlePublish={publishUnpublishedEntry} handlePublish={publishUnpublishedEntry}
handleDelete={deleteUnpublishedEntry} handleDelete={deleteUnpublishedEntry}
isForkWorkflow={isForkWorkflow} isOpenAuthoring={isOpenAuthoring}
/> />
</WorkflowContainer> </WorkflowContainer>
); );
@ -137,8 +137,8 @@ class Workflow extends Component {
function mapStateToProps(state) { function mapStateToProps(state) {
const { collections, config, globalUI } = state; const { collections, config, globalUI } = state;
const isEditorialWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW; const isEditorialWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
const isForkWorkflow = globalUI.get('useForkWorkflow', false); const isOpenAuthoring = globalUI.get('useOpenAuthoring', false);
const returnObj = { collections, isEditorialWorkflow, isForkWorkflow }; const returnObj = { collections, isEditorialWorkflow, isOpenAuthoring };
if (isEditorialWorkflow) { if (isEditorialWorkflow) {
returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false); returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false);

View File

@ -17,7 +17,7 @@ const WorkflowListContainer = styled.div`
grid-template-columns: 33.3% 33.3% 33.3%; grid-template-columns: 33.3% 33.3% 33.3%;
`; `;
const WorkflowListContainerForkWorkflow = styled.div` const WorkflowListContainerOpenAuthoring = styled.div`
min-height: 60%; min-height: 60%;
display: grid; display: grid;
grid-template-columns: 50% 50% 0%; grid-template-columns: 50% 50% 0%;
@ -134,7 +134,7 @@ class WorkflowList extends React.Component {
handlePublish: PropTypes.func.isRequired, handlePublish: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired, handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
isForkWorkflow: PropTypes.bool, isOpenAuthoring: PropTypes.bool,
}; };
handleChangeStatus = (newStatus, dragProps) => { handleChangeStatus = (newStatus, dragProps) => {
@ -162,7 +162,7 @@ class WorkflowList extends React.Component {
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderColumns = (entries, column) => { renderColumns = (entries, column) => {
const { isForkWorkflow } = this.props; const { isOpenAuthoring } = this.props;
if (!entries) return null; if (!entries) return null;
if (!column) { if (!column) {
@ -180,8 +180,8 @@ class WorkflowList extends React.Component {
styles.column, styles.column,
styles.columnPosition(idx), styles.columnPosition(idx),
isHovered && styles.columnHovered, isHovered && styles.columnHovered,
isForkWorkflow && currColumn === 'pending_publish' && styles.hiddenColumn, isOpenAuthoring && currColumn === 'pending_publish' && styles.hiddenColumn,
isForkWorkflow && currColumn === 'pending_review' && styles.hiddenRightBorder, isOpenAuthoring && currColumn === 'pending_review' && styles.hiddenRightBorder,
]} ]}
> >
<ColumnHeader name={currColumn}> <ColumnHeader name={currColumn}>
@ -248,8 +248,8 @@ class WorkflowList extends React.Component {
render() { render() {
const columns = this.renderColumns(this.props.entries); const columns = this.renderColumns(this.props.entries);
const ListContainer = this.props.isForkWorkflow const ListContainer = this.props.isOpenAuthoring
? WorkflowListContainerForkWorkflow ? WorkflowListContainerOpenAuthoring
: WorkflowListContainer; : WorkflowListContainer;
return <ListContainer>{columns}</ListContainer>; return <ListContainer>{columns}</ListContainer>;
} }

View File

@ -1,16 +1,16 @@
import { Map } from 'immutable'; import { Map } from 'immutable';
import { USE_FORK_WORKFLOW } from 'Actions/auth'; import { USE_OPEN_AUTHORING } from 'Actions/auth';
/* /*
* Reducer for some global UI state that we want to share between components * Reducer for some global UI state that we want to share between components
* */ * */
const globalUI = (state = Map({ isFetching: false, useForkWorkflow: false }), action) => { const globalUI = (state = Map({ isFetching: false, useOpenAuthoring: false }), action) => {
// Generic, global loading indicator // Generic, global loading indicator
if (action.type.indexOf('REQUEST') > -1) { if (action.type.indexOf('REQUEST') > -1) {
return state.set('isFetching', true); return state.set('isFetching', true);
} else if (action.type.indexOf('SUCCESS') > -1 || action.type.indexOf('FAILURE') > -1) { } else if (action.type.indexOf('SUCCESS') > -1 || action.type.indexOf('FAILURE') > -1) {
return state.set('isFetching', false); return state.set('isFetching', false);
} else if (action.type === USE_FORK_WORKFLOW) { } else if (action.type === USE_OPEN_AUTHORING) {
return state.set('useForkWorkflow', true); return state.set('useOpenAuthoring', true);
} }
return state; return state;
}; };

View File

@ -1,6 +1,8 @@
import { get } from 'lodash'; import { flow, fromPairs, get } from 'lodash';
import { map } from 'lodash/fp';
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { fileExtension } from './path'; import { fileExtension } from './path';
import unsentRequest from './unsentRequest';
export const filterByPropExtension = (extension, propName) => arr => export const filterByPropExtension = (extension, propName) => arr =>
arr.filter(el => fileExtension(get(el, propName)) === extension); arr.filter(el => fileExtension(get(el, propName)) === extension);
@ -40,3 +42,36 @@ export const parseResponse = async (res, { expectingOk = true, format = 'text' }
}; };
export const responseParser = options => res => parseResponse(res, options); export const responseParser = options => res => parseResponse(res, options);
export const parseLinkHeader = flow([
linksString => linksString.split(','),
map(str => str.trim().split(';')),
map(([linkStr, keyStr]) => [
keyStr.match(/rel="(.*?)"/)[1],
linkStr
.trim()
.match(/<(.*?)>/)[1]
.replace(/\+/g, '%20'),
]),
fromPairs,
]);
export const getPaginatedRequestIterator = (url, options = {}, linkHeaderRelName = 'next') => {
let req = unsentRequest.fromFetchArguments(url, options);
const next = async () => {
if (!req) {
return { done: true };
}
const pageResponse = await unsentRequest.performRequest(req);
const linkHeader = pageResponse.headers.get('Link');
const nextURL = linkHeader && parseLinkHeader(linkHeader)[linkHeaderRelName];
req = nextURL && unsentRequest.fromURL(nextURL);
return { value: pageResponse };
};
return {
[Symbol.asyncIterator]: () => ({
next,
}),
};
};

View File

@ -11,7 +11,13 @@ import {
then, then,
} from './promise'; } from './promise';
import unsentRequest from './unsentRequest'; import unsentRequest from './unsentRequest';
import { filterByPropExtension, parseResponse, responseParser } from './backendUtil'; import {
filterByPropExtension,
getPaginatedRequestIterator,
parseLinkHeader,
parseResponse,
responseParser,
} from './backendUtil';
import loadScript from './loadScript'; import loadScript from './loadScript';
import getBlobSHA from './getBlobSHA'; import getBlobSHA from './getBlobSHA';
@ -33,6 +39,7 @@ export const NetlifyCmsLibUtil = {
then, then,
unsentRequest, unsentRequest,
filterByPropExtension, filterByPropExtension,
parseLinkHeader,
parseResponse, parseResponse,
responseParser, responseParser,
loadScript, loadScript,
@ -56,6 +63,8 @@ export {
then, then,
unsentRequest, unsentRequest,
filterByPropExtension, filterByPropExtension,
parseLinkHeader,
getPaginatedRequestIterator,
parseResponse, parseResponse,
responseParser, responseParser,
loadScript, loadScript,

View File

@ -13,6 +13,12 @@ const fromURL = wholeURL => {
return Map({ url, ...(allParamsString ? { params: decodeParams(allParamsString) } : {}) }); return Map({ url, ...(allParamsString ? { params: decodeParams(allParamsString) } : {}) });
}; };
const fromFetchArguments = (wholeURL, options) => {
return fromURL(wholeURL).merge(
(options ? fromJS(options) : Map()).remove('url').remove('params'),
);
};
const encodeParams = params => const encodeParams = params =>
params params
.entrySeq() .entrySeq()
@ -25,8 +31,8 @@ const toURL = req =>
const toFetchArguments = req => [ const toFetchArguments = req => [
toURL(req), toURL(req),
req req
.delete('url') .remove('url')
.delete('params') .remove('params')
.toJS(), .toJS(),
]; ];
@ -85,6 +91,7 @@ const withTimestamp = ensureRequestArg(req => withParams({ ts: new Date().getTim
export default { export default {
toURL, toURL,
fromURL, fromURL,
fromFetchArguments,
performRequest, performRequest,
withMethod, withMethod,
withDefaultMethod, withDefaultMethod,

View File

@ -17,17 +17,17 @@ At the same time, any contributors who _do_ have write access to the repository
- Your repo on GitHub must be public. - Your repo on GitHub must be public.
## Enabling the Open Authoring ## Enabling Open Authoring
1. [Enable the editorial workflow](/docs/configuration-options/#publish-mode) by setting `publish_mode` to `editorial_workflow` in your `config.yml`. 1. [Enable the editorial workflow](/docs/configuration-options/#publish-mode) by setting `publish_mode` to `editorial_workflow` in your `config.yml`.
2. Set `fork_workflow` to `true` in the `backend` section of your `config.yml`, as follows: 2. Set `open_authoring` to `true` in the `backend` section of your `config.yml`, as follows:
```yaml ```yaml
backend: backend:
name: github name: github
repo: owner-name/repo-name # Path to your GitHub repository repo: owner-name/repo-name # Path to your GitHub repository
fork_workflow: true open_authoring: true
``` ```
## Usage ## Usage