Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/netlify-cms-backend-azure/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,10 @@ export default class API {
const status = labelToStatus(labelName, this.cmsLabelPrefix);
// Uses creationDate, as we do not have direct access to the updated date
const updatedAt = pullRequest.closedDate ? pullRequest.closedDate : pullRequest.creationDate;
const pullRequestAuthor =
pullRequest.createdBy?.displayName || pullRequest.createdBy?.uniqueName;
const pullRequestAuthor = {
name: pullRequest.createdBy?.displayName,
login: pullRequest.createdBy?.uniqueName,
};
return {
collection,
slug,
Expand Down Expand Up @@ -717,6 +719,22 @@ export default class API {
await this.completePullRequest(pullRequest);
}

async approveEntry(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);

const pullRequest = await this.getBranchPullRequest(branch);
const pullRequestCompleted = {
status: AzurePullRequestStatus.COMPLETED,
};

await this.requestJSON({
method: 'PATCH',
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
body: JSON.stringify(pullRequestCompleted),
});
}

async updatePullRequestLabels(pullRequest: AzurePullRequest, labels: string[]) {
const cmsLabels = pullRequest.labels.filter(l => isCMSLabel(l.name, this.cmsLabelPrefix));
await Promise.all(
Expand Down
9 changes: 9 additions & 0 deletions packages/netlify-cms-backend-azure/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,15 @@ export default class Azure implements Implementation {
);
}

approveEntry(collection: string, slug: string) {
// approveEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.approveEntry(collection, slug),
'Failed to acquire approve entry lock',
);
}

async getDeployPreview(collection: string, slug: string) {
try {
const statuses = await this.api!.getStatuses(collection, slug);
Expand Down
16 changes: 15 additions & 1 deletion packages/netlify-cms-backend-bitbucket/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,10 @@ export default class API {
const label = await this.getPullRequestLabel(pullRequest.id);
const status = labelToStatus(label, this.cmsLabelPrefix);
const updatedAt = pullRequest.updated_on;
const pullRequestAuthor = pullRequest.author.display_name;
const pullRequestAuthor = {
name: pullRequest.author.display_name,
login: pullRequest.author.username,
};
return {
collection,
slug,
Expand Down Expand Up @@ -743,6 +746,17 @@ export default class API {
await this.mergePullRequest(pullRequest);
}

async approveEntry(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);

const pullRequest = await this.getBranchPullRequest(branch);
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/pullrequests/${pullRequest.id}/approve`,
});
}

async declinePullRequest(pullRequest: BitBucketPullRequest) {
await this.requestJSON({
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,15 @@ export default class BitbucketBackend implements Implementation {
);
}

approveEntry(collection: string, slug: string) {
// approveEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.approveEntry(collection, slug),
'Failed to acquire approve entry lock',
);
}

async getDeployPreview(collection: string, slug: string) {
try {
const statuses = await this.api!.getStatuses(collection, slug);
Expand Down
26 changes: 24 additions & 2 deletions packages/netlify-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,8 @@ export default class API {
}

try {
const user: GitHubUser = await this.request(`/users/${pullRequest.user.login}`);
return user.name || user.login;
const { name, login }: GitHubUser = await this.request(`/users/${pullRequest.user.login}`);
return { name, login };
} catch {
return;
}
Expand Down Expand Up @@ -1165,6 +1165,14 @@ export default class API {
await this.deleteBranch(branch);
}

async approveEntry(collectionName: string, slug: string) {
const contentKey = this.generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);

const pullRequest = await this.getBranchPullRequest(branch);
await this.approvePR(pullRequest);
}

async createRef(type: string, name: string, sha: string) {
const result: Octokit.GitCreateRefResponse = await this.request(`${this.repoURL}/git/refs`, {
method: 'POST',
Expand Down Expand Up @@ -1341,6 +1349,20 @@ export default class API {
}
}

async approvePR(pullrequest: GitHubPull) {
console.log('%c Approving PR', 'line-height: 30px;text-align: center;font-weight: bold');
const result: Octokit.PullsCreateReviewResponse = await this.request(
`${this.originRepoURL}/pulls/${pullrequest.number}/reviews`,
{
method: 'POST',
body: JSON.stringify({
event: 'APPROVE',
}),
},
);
return result;
}

async forceMergePR(pullRequest: GitHubPull) {
const result = await this.getDifferences(pullRequest.base.sha, pullRequest.head.sha);
const files = getTreeFiles(result.files as GitHubCompareFiles);
Expand Down
7 changes: 5 additions & 2 deletions packages/netlify-cms-backend-github/src/GraphQLAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,11 @@ export default class GraphQLAPI extends API {
}

async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) {
const user = pullRequest.user as unknown as GraphQLPullsListResponseItemUser;
return user?.name || user?.login;
if (!pullRequest.user) {
return { name: '', login: '' };
}
const { name, login } = pullRequest.user as unknown as GraphQLPullsListResponseItemUser;
return { name, login };
}

async getPullRequests(
Expand Down
9 changes: 9 additions & 0 deletions packages/netlify-cms-backend-github/src/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -670,4 +670,13 @@ export default class GitHub implements Implementation {
'Failed to acquire publish entry lock',
);
}

approveEntry(collection: string, slug: string) {
// approveEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.approveEntry(collection, slug),
'Failed to acquire approve entry lock',
);
}
}
16 changes: 15 additions & 1 deletion packages/netlify-cms-backend-gitlab/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,10 @@ export default class API {
const label = mergeRequest.labels.find(l => isCMSLabel(l, this.cmsLabelPrefix)) as string;
const status = labelToStatus(label, this.cmsLabelPrefix);
const updatedAt = mergeRequest.updated_at;
const pullRequestAuthor = mergeRequest.author.name;
const pullRequestAuthor = {
name: mergeRequest.author.name,
login: mergeRequest.author.username,
};
return {
collection,
slug,
Expand Down Expand Up @@ -947,6 +950,17 @@ export default class API {
await this.mergeMergeRequest(mergeRequest);
}

async approveEntry(collectionName: string, slug: string) {
const contentKey = generateContentKey(collectionName, slug);
const branch = branchFromContentKey(contentKey);

const mergeRequest = await this.getBranchMergeRequest(branch);
await this.requestJSON({
method: 'POST',
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}/approve`,
});
}

async closeMergeRequest(mergeRequest: GitLabMergeRequest) {
await this.requestJSON({
method: 'PUT',
Expand Down
9 changes: 9 additions & 0 deletions packages/netlify-cms-backend-gitlab/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ export default class GitLab implements Implementation {
);
}

approveEntry(collection: string, slug: string) {
// approveEntry is a transactional operation
return runWithLock(
this.lock,
() => this.api!.approveEntry(collection, slug),
'Failed to acquire approve entry lock',
);
}

async getDeployPreview(collection: string, slug: string) {
try {
const statuses = await this.api!.getStatuses(collection, slug);
Expand Down
6 changes: 6 additions & 0 deletions packages/netlify-cms-core/src/__tests__/backend.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ describe('Backend', () => {
isModification: null,
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
},
});
expect(localForage.getItem).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -243,6 +245,8 @@ describe('Backend', () => {
isModification: null,
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
},
});
expect(localForage.getItem).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -392,6 +396,8 @@ describe('Backend', () => {
mediaFiles: [{ id: '1', draft: true }],
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe('entries', () => {
slug: '',
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
},
type: 'DRAFT_CREATE_EMPTY',
});
Expand Down Expand Up @@ -93,6 +95,8 @@ describe('entries', () => {
slug: '',
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
},
type: 'DRAFT_CREATE_EMPTY',
});
Expand Down Expand Up @@ -128,6 +132,8 @@ describe('entries', () => {
slug: '',
status: '',
updatedOn: '',
canApprove: false,
supportsApprove: false,
},
type: 'DRAFT_CREATE_EMPTY',
});
Expand Down
53 changes: 53 additions & 0 deletions packages/netlify-cms-core/src/actions/editorialWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE';

export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
export const APPROVE_ENTRY_REQUEST = 'APPROVE_ENTRY_REQUEST';
export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
export const APPROVE_ENTRY_SUCCESS = 'APPROVE_ENTRY_SUCCESS';
export const APPROVE_ENTRY_FAILURE = 'APPROVE_ENTRY_FAILURE';
export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE';

export const UNPUBLISHED_ENTRY_DELETE_REQUEST = 'UNPUBLISHED_ENTRY_DELETE_REQUEST';
Expand Down Expand Up @@ -200,20 +203,41 @@ function unpublishedEntryPublishRequest(collection: string, slug: string) {
};
}

function approveEntryRequest(collection: string, slug: string) {
return {
type: APPROVE_ENTRY_REQUEST,
payload: { collection, slug },
};
}

function unpublishedEntryPublished(collection: string, slug: string) {
return {
type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
payload: { collection, slug },
};
}

function entryApproved(collection: string, slug: string) {
return {
type: APPROVE_ENTRY_SUCCESS,
payload: { collection, slug },
};
}

function unpublishedEntryPublishError(collection: string, slug: string) {
return {
type: UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
payload: { collection, slug },
};
}

function entryApproveError(collection: string, slug: string) {
return {
type: APPROVE_ENTRY_FAILURE,
payload: { collection, slug },
};
}

function unpublishedEntryDeleteRequest(collection: string, slug: string) {
return {
type: UNPUBLISHED_ENTRY_DELETE_REQUEST,
Expand Down Expand Up @@ -524,6 +548,35 @@ export function publishUnpublishedEntry(collectionName: string, slug: string) {
};
}

export function approveEntry(collectionName: string, slug: string) {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
const entry = selectUnpublishedEntry(state, collectionName, slug);
dispatch(approveEntryRequest(collectionName, slug));
try {
await backend.approveEntry(entry);
dispatch(
notifSend({
message: { key: 'ui.toast.entryApproved' },
kind: 'success',
dismissAfter: 4000,
}),
);
dispatch(entryApproved(collectionName, slug));
} catch (error) {
dispatch(
notifSend({
message: { key: 'ui.toast.onFailToApproveEntry', details: error },
kind: 'danger',
dismissAfter: 8000,
}),
);
dispatch(entryApproveError(collectionName, slug));
}
};
}

export function unpublishPublishedEntry(collection: Collection, slug: string) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
Expand Down
13 changes: 12 additions & 1 deletion packages/netlify-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,9 +882,13 @@ export class Backend {
label: collection && selectFileEntryLabel(collection, slug),
mediaFiles,
updatedOn: entryData.updatedAt,
author: entryData.pullRequestAuthor,
author: entryData.pullRequestAuthor?.name || entryData.pullRequestAuthor?.login,
status: entryData.status,
meta: { path: prepareMetaPath(path, collection) },
supportsApprove: this.isGitBackend(),
canApprove: this.user?.login
? this.user?.login !== entryData.pullRequestAuthor?.login
: false,
});

const entryWithFormat = this.entryWithFormat(collection)(entry);
Expand Down Expand Up @@ -1268,6 +1272,13 @@ export class Backend {
await this.invokePostPublishEvent(entry);
}

async approveEntry(entry: EntryMap) {
const collection = entry.get('collection');
const slug = entry.get('slug');

await this.implementation.approveEntry!(collection, slug);
}

deleteUnpublishedEntry(collection: string, slug: string) {
return this.implementation.deleteUnpublishedEntry!(collection, slug);
}
Expand Down
Loading