diff --git a/src/plugins/signoff/components.js b/src/plugins/signoff/components.js index a137ed283..7763d385a 100644 --- a/src/plugins/signoff/components.js +++ b/src/plugins/signoff/components.js @@ -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, @@ -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 ( - + + step={1} + currentStep={step} + canEdit={canReview} + approveChanges={approveChanges} + declineChanges={declineChanges} + source={source} + preview={preview} /> + step={2} + currentStep={step} + canEdit={canSign} + reSign={approveChanges} + source={source} + destination={destination} /> ); } diff --git a/src/plugins/signoff/sagas.js b/src/plugins/signoff/sagas.js index 13a7f0e6d..1ca8b1da5 100644 --- a/src/plugins/signoff/sagas.js +++ b/src/plugins/signoff/sagas.js @@ -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) { @@ -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) { @@ -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) { @@ -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})); } diff --git a/src/plugins/signoff/types.js b/src/plugins/signoff/types.js index 53e4ba0f7..9d182d955 100644 --- a/src/plugins/signoff/types.js +++ b/src/plugins/signoff/types.js @@ -17,6 +17,8 @@ export type SourceInfo = { lastEditor: string, lastReviewer: string, lastStatusChanged: number, + editors_group?: string, + reviewers_group?: string, }; export type PreviewInfo = { diff --git a/src/reducers/session.js b/src/reducers/session.js index 6376ce8bd..22669b37a 100644 --- a/src/reducers/session.js +++ b/src/reducers/session.js @@ -22,7 +22,6 @@ const DEFAULT: SessionState = { buckets: [], serverInfo: { capabilities: {}, - user: {}, }, redirectURL: null, }; diff --git a/src/types.js b/src/types.js index b126631b6..06ce352a6 100644 --- a/src/types.js +++ b/src/types.js @@ -59,6 +59,7 @@ export type Capabilities = { history?: Object, permissions_endpoint?: Object, schema?: Object, + signer?: Object, }; export type ClientError = { @@ -253,7 +254,7 @@ export type SessionState = { export type ServerInfo = { capabilities: Capabilities, user?: { - id?: string, + id: string, bucket?: string, } }; diff --git a/test/plugins/signoff/sagas_test.js b/test/plugins/signoff/sagas_test.js new file mode 100644 index 000000000..0bf69d99c --- /dev/null +++ b/test/plugins/signoff/sagas_test.js @@ -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."))); + }); + }); +}); diff --git a/test/reducers/sessions_test.js b/test/reducers/sessions_test.js index f6d24e8c9..3200b3547 100644 --- a/test/reducers/sessions_test.js +++ b/test/reducers/sessions_test.js @@ -44,7 +44,6 @@ describe("session reducer", () => { redirectURL: null, serverInfo: { capabilities: {}, - user: {}, }, }); });