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:
parent
66da66affd
commit
34e1f09105
@ -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',
|
||||||
|
@ -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'll use that.)
|
||||||
|
</p>
|
||||||
<ForkButtonsContainer>
|
<ForkButtonsContainer>
|
||||||
<LoginButton onClick={approveFork}>Fork the repo</LoginButton>
|
<LoginButton onClick={approveFork}>Fork the repo</LoginButton>
|
||||||
<LoginButton onClick={refuseFork}>Don't fork the repo</LoginButton>
|
<LoginButton onClick={refuseFork}>Don't fork the repo</LoginButton>
|
||||||
|
@ -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) {
|
||||||
|
@ -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) ||
|
||||||
|
@ -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));
|
||||||
})
|
})
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user