Skip to content

Commit

Permalink
Review workflow UI improvements. (#319)
Browse files Browse the repository at this point in the history
  • Loading branch information
n1k0 committed Nov 17, 2016
1 parent e8cd440 commit fe2a770
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 26 deletions.
81 changes: 62 additions & 19 deletions src/plugins/signoff/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ import AdminLink from "../../components/AdminLink";
import { ProgressBar, ProgressStep } from "./ProgressBar.js";


function isMember(groupKey, source, sessionState, bucketState) {
const {serverInfo: {user={}, capabilities}} = sessionState;
if (!user.id) {
return false;
}
const {signer={}} = capabilities;
const {[groupKey]: defaultGroupName} = signer;
const {[groupKey]: groupName=defaultGroupName} = source;
const {id: userId} = user;
const {groups} = bucketState;
const group = groups.find(g => g.id === groupName);
if (group == null) {
// XXX for now if we can't access the group it's probably because the user
// doesn't have the permission to read it, so we mark the user has a member
// of the group.
// Later when https://github.com/Kinto/kinto/pull/891/files lands, we'll
// have access to the principals attached to a given authentication, so we'll
// be able to properly check for membership.
return true;
}
return group.members.includes(userId);
}

function isEditor(source, sessionState, bucketState) {
return isMember("editors_group", source, sessionState, bucketState);
}

function isReviewer(source, sessionState, bucketState) {
return isMember("reviewers_group", source, sessionState, bucketState);
}

function isLastEditor(source, sessionState) {
const {serverInfo: {user={}}} = sessionState;
const {lastEditor} = source;
return user.id === lastEditor;
}

export default class SignoffToolBar extends React.Component {
props: {
sessionState: SessionState,
Expand Down Expand Up @@ -59,32 +96,38 @@ export default class SignoffToolBar extends React.Component {
}

const {source, preview, destination} = resource;
const canRequestReview = canEdit && isEditor(source, sessionState, bucketState);
const canReview = canEdit &&
isReviewer(source, sessionState, bucketState) &&
!isLastEditor(source, sessionState);
const canSign = canEdit && isReviewer(source, sessionState, bucketState);

// Default status is request review
const step = status == "to-review" ? 1 : status == "signed" ? 2 : 0;
return (
<ProgressBar>
<WorkInProgress label="Work in progress"
step={0}
currentStep={step}
canEdit={canEdit}
requestReview={requestReview}
source={source} />
<WorkInProgress
label="Work in progress"
step={0}
currentStep={step}
canEdit={canRequestReview}
requestReview={requestReview}
source={source} />
<Review label="Waiting review"
step={1}
currentStep={step}
canEdit={canEdit}
approveChanges={approveChanges}
declineChanges={declineChanges}
source={source}
preview={preview} />
step={1}
currentStep={step}
canEdit={canReview}
approveChanges={approveChanges}
declineChanges={declineChanges}
source={source}
preview={preview} />
<Signed label="Signed"
step={2}
currentStep={step}
canEdit={canEdit}
reSign={approveChanges}
source={source}
destination={destination} />
step={2}
currentStep={step}
canEdit={canSign}
reSign={approveChanges}
source={source}
destination={destination} />
</ProgressBar>
);
}
Expand Down
11 changes: 7 additions & 4 deletions src/plugins/signoff/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function* fetchWorkflowInfo(source, preview, destination) {
export function* handleRequestReview(getState, action) {
const {bucket} = getState();
try {
const collection = yield call([this, _updateCollectionAttributes], getState, {status: "to-review"});
const collection = yield call(_updateCollectionAttributes, getState, {status: "to-review"});
yield put(routeLoadSuccess({bucket, collection}));
yield put(notifySuccess("Review requested."));
} catch(e) {
Expand All @@ -118,7 +118,7 @@ export function* handleRequestReview(getState, action) {
export function* handleDeclineChanges(getState, action) {
const {bucket} = getState();
try {
const collection = yield call([this, _updateCollectionAttributes], getState, {status: "work-in-progress"});
const collection = yield call(_updateCollectionAttributes, getState, {status: "work-in-progress"});
yield put(routeLoadSuccess({bucket, collection}));
yield put(notifySuccess("Changes declined."));
} catch(e) {
Expand All @@ -129,7 +129,7 @@ export function* handleDeclineChanges(getState, action) {
export function* handleApproveChanges(getState, action) {
const {bucket} = getState();
try {
const collection = yield call([this, _updateCollectionAttributes], getState, {status: "to-sign"});
const collection = yield call(_updateCollectionAttributes, getState, {status: "to-sign"});
yield put(routeLoadSuccess({bucket, collection}));
yield put(notifySuccess("Signature requested."));
} catch(e) {
Expand All @@ -144,5 +144,8 @@ function _updateCollectionAttributes(getState, data) {
collection: {data: {id: cid, last_modified}}
} = getState();
const coll = client.bucket(bid).collection(cid);
return coll.setData(data, {safe: true, patch: true, last_modified});
return coll.setData(data, {safe: true, patch: true, last_modified})
.then(() => coll.getData())
// FIXME: https://github.com/Kinto/kinto-http.js/issues/150
.then(attributes => ({data: attributes}));
}
2 changes: 2 additions & 0 deletions src/plugins/signoff/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type SourceInfo = {
lastEditor: string,
lastReviewer: string,
lastStatusChanged: number,
editors_group?: string,
reviewers_group?: string,
};

export type PreviewInfo = {
Expand Down
1 change: 0 additions & 1 deletion src/reducers/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const DEFAULT: SessionState = {
buckets: [],
serverInfo: {
capabilities: {},
user: {},
},
redirectURL: null,
};
Expand Down
3 changes: 2 additions & 1 deletion src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type Capabilities = {
history?: Object,
permissions_endpoint?: Object,
schema?: Object,
signer?: Object,
};

export type ClientError = {
Expand Down Expand Up @@ -272,7 +273,7 @@ export type SessionState = {
export type ServerInfo = {
capabilities: Capabilities,
user?: {
id?: string,
id: string,
bucket?: string,
}
};
Expand Down
113 changes: 113 additions & 0 deletions test/plugins/signoff/sagas_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect } from "chai";
import { put } from "redux-saga/effects";

import { notifySuccess } from "../../../src/actions/notifications";
import { routeLoadSuccess } from "../../../src/actions/route";
import { setClient } from "../../../src/client";
import * as actions from "../../../src/plugins/signoff/actions";
import * as saga from "../../../src/plugins/signoff/sagas";


describe("signoff plugin sagas", () => {
let bucket, collection, getState;

before(() => {
collection = {
getData() {},
setData() {},
};
bucket = {collection() {return collection;}};
setClient({bucket() {return bucket;}});
getState = () => ({
bucket: {data: {id: "buck"}},
collection: {data: {id: "coll", last_modified: 42}}
});
});

describe("handleRequestReview()", () => {
let handleRequestReview;

before(() => {
const action = actions.requestReview();
handleRequestReview = saga.handleRequestReview(getState, action);
});

it("should update the collection status as 'to-review'", () => {
expect(handleRequestReview.next({id: "coll"}).value)
.to.have.property("CALL")
.to.have.property("args")
.to.include({status: "to-review"});
});

it("should dispatch the routeLoadSuccess action", () => {
expect(handleRequestReview.next({data: {id: "coll", status: "to-review"}}).value)
.eql(put(routeLoadSuccess({
bucket: {data: {id: "buck"}},
collection: {data: {id: "coll", status: "to-review"}},
})));
});

it("should dispatch a success notification", () => {
expect(handleRequestReview.next().value)
.eql(put(notifySuccess("Review requested.")));
});
});

describe("handleDeclineChanges()", () => {
let handleDeclineChanges;

before(() => {
const action = actions.declineChanges();
handleDeclineChanges = saga.handleDeclineChanges(getState, action);
});

it("should update the collection status as 'work-in-progress'", () => {
expect(handleDeclineChanges.next({id: "coll"}).value)
.to.have.property("CALL")
.to.have.property("args")
.to.include({status: "work-in-progress"});
});

it("should dispatch the routeLoadSuccess action", () => {
expect(handleDeclineChanges.next({data: {id: "coll", status: "work-in-progress"}}).value)
.eql(put(routeLoadSuccess({
bucket: {data: {id: "buck"}},
collection: {data: {id: "coll", status: "work-in-progress"}},
})));
});

it("should dispatch a success notification", () => {
expect(handleDeclineChanges.next().value)
.eql(put(notifySuccess("Changes declined.")));
});
});

describe("handleApproveChanges()", () => {
let handleApproveChanges;

before(() => {
const action = actions.approveChanges();
handleApproveChanges = saga.handleApproveChanges(getState, action);
});

it("should update the collection status as 'to-sign'", () => {
expect(handleApproveChanges.next({id: "coll"}).value)
.to.have.property("CALL")
.to.have.property("args")
.to.include({status: "to-sign"});
});

it("should dispatch the routeLoadSuccess action", () => {
expect(handleApproveChanges.next({data: {id: "coll", status: "to-sign"}}).value)
.eql(put(routeLoadSuccess({
bucket: {data: {id: "buck"}},
collection: {data: {id: "coll", status: "to-sign"}},
})));
});

it("should dispatch a success notification", () => {
expect(handleApproveChanges.next().value)
.eql(put(notifySuccess("Signature requested.")));
});
});
});
1 change: 0 additions & 1 deletion test/reducers/sessions_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ describe("session reducer", () => {
redirectURL: null,
serverInfo: {
capabilities: {},
user: {},
},
});
});
Expand Down

0 comments on commit fe2a770

Please sign in to comment.