Skip to content

Commit

Permalink
feat: add approve PR button to workflow UI
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Dec 15, 2021
1 parent a9e5488 commit 8ffaf3f
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 0 deletions.
22 changes: 22 additions & 0 deletions packages/netlify-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
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
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',
);
}
}
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
7 changes: 7 additions & 0 deletions packages/netlify-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,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
5 changes: 5 additions & 0 deletions packages/netlify-cms-core/src/components/Workflow/Workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
loadUnpublishedEntries,
updateUnpublishedEntryStatus,
publishUnpublishedEntry,
approveEntry,
deleteUnpublishedEntry,
} from '../../actions/editorialWorkflow';
import { selectUnpublishedEntriesByStatus } from '../../reducers';
Expand Down Expand Up @@ -62,6 +63,7 @@ class Workflow extends Component {
loadUnpublishedEntries: PropTypes.func.isRequired,
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
publishUnpublishedEntry: PropTypes.func.isRequired,
approveEntry: PropTypes.func.isRequired,
deleteUnpublishedEntry: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
Expand All @@ -81,6 +83,7 @@ class Workflow extends Component {
unpublishedEntries,
updateUnpublishedEntryStatus,
publishUnpublishedEntry,
approveEntry,
deleteUnpublishedEntry,
collections,
t,
Expand Down Expand Up @@ -127,6 +130,7 @@ class Workflow extends Component {
entries={unpublishedEntries}
handleChangeStatus={updateUnpublishedEntryStatus}
handlePublish={publishUnpublishedEntry}
handleApprove={approveEntry}
handleDelete={deleteUnpublishedEntry}
isOpenAuthoring={isOpenAuthoring}
collections={collections}
Expand Down Expand Up @@ -162,5 +166,6 @@ export default connect(mapStateToProps, {
loadUnpublishedEntries,
updateUnpublishedEntryStatus,
publishUnpublishedEntry,
approveEntry,
deleteUnpublishedEntry,
})(translate()(Workflow));
13 changes: 13 additions & 0 deletions packages/netlify-cms-core/src/components/Workflow/WorkflowCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ const PublishButton = styled.button`
}
`;

const ApproveButton = styled.button`
${styles.button};
background-color: ${colorsRaw.green};
margin-left: 6px;
color: ${colors.textLight};
&[disabled] {
${buttons.disabled};
}
`;

const WorkflowCardContainer = styled.div`
${components.card};
margin-bottom: 24px;
Expand Down Expand Up @@ -128,6 +139,7 @@ function WorkflowCard({
allowPublish,
canPublish,
onPublish,
onApprove,
postAuthor,
t,
}) {
Expand All @@ -153,6 +165,7 @@ function WorkflowCard({
: t('workflow.workflowCard.publishNewEntry')}
</PublishButton>
)}
<ApproveButton onClick={onApprove}>{t('workflow.workflowCard.approveEntry')}</ApproveButton>
</CardButtonContainer>
</WorkflowCardContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ class WorkflowList extends React.Component {
this.props.handlePublish(collection, slug);
};

requestApprove = (collection, slug) => {
this.props.handleApprove(collection, slug);
};

// eslint-disable-next-line react/display-name
renderColumns = (entries, column) => {
const { isOpenAuthoring, collections, t } = this.props;
Expand Down Expand Up @@ -245,6 +249,7 @@ class WorkflowList extends React.Component {
allowPublish={allowPublish}
canPublish={canPublish}
onPublish={this.requestPublish.bind(this, collectionName, slug, ownStatus)}
onApprove={this.requestApprove.bind(this, collectionName, slug)}
postAuthor={postAuthor}
/>
</div>,
Expand Down
1 change: 1 addition & 0 deletions packages/netlify-cms-lib-util/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export interface Implementation {
newStatus: string,
) => Promise<void>;
publishUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
approveEntry?: (collection: string, slug: string) => Promise<void>;
deleteUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
getDeployPreview: (
collectionName: string,
Expand Down
3 changes: 3 additions & 0 deletions packages/netlify-cms-locales/src/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,10 @@ const en = {
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
entrySaved: 'Entry saved',
entryPublished: 'Entry published',
entryApproved: 'Entry approved',
entryUnpublished: 'Entry unpublished',
onFailToPublishEntry: 'Failed to publish: %{details}',
onFailToApproveEntry: 'Failed to approve: %{details}',
onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}',
entryUpdated: 'Entry status updated',
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
Expand All @@ -293,6 +295,7 @@ const en = {
deleteNewEntry: 'Delete new entry',
publishChanges: 'Publish changes',
publishNewEntry: 'Publish new entry',
approveEntry: 'Approve entry',
},
workflowList: {
onDeleteEntry: 'Are you sure you want to delete this entry?',
Expand Down

0 comments on commit 8ffaf3f

Please sign in to comment.