diff --git a/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.js b/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.js
new file mode 100644
index 0000000000..7f28a35a09
--- /dev/null
+++ b/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.js
@@ -0,0 +1,78 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { inject, observer } from 'mobx-react'
+import { Button, Box } from 'grommet'
+import { Modal } from '@zooniverse/react-components'
+import counterpart from 'counterpart'
+import en from './locales/en'
+
+import SubjectViewer from '../SubjectViewer'
+
+counterpart.registerTranslations('en', en)
+
+function storeMapper (stores) {
+ const {
+ hideFeedback,
+ hideSubjectViewer,
+ messages,
+ showModal
+ } = stores.classifierStore.feedback
+ return {
+ hideFeedback,
+ hideSubjectViewer,
+ messages,
+ showModal
+ }
+}
+
+@inject(storeMapper)
+@observer
+class FeedbackModal extends React.Component {
+ render () {
+ const label = counterpart('FeedbackModal.label')
+ const { hideFeedback, hideSubjectViewer, messages, showModal } = this.props
+
+ if (showModal) {
+ return (
+
+ <>
+
+ {!hideSubjectViewer && }
+
+ {messages.map(message =>
+ -
+ {message}
+
+ )}
+
+
+
+
+
+ >
+
+ )
+ }
+
+ return null
+ }
+}
+
+FeedbackModal.wrappedComponent.propTypes = {
+ hideFeedback: PropTypes.func,
+ messages: PropTypes.arrayOf(PropTypes.string),
+ showModal: PropTypes.bool
+}
+
+export default FeedbackModal
diff --git a/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.spec.js b/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.spec.js
new file mode 100644
index 0000000000..c010084ca3
--- /dev/null
+++ b/packages/lib-classifier/src/components/Classifier/components/Feedback/FeedbackModal.spec.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import { expect } from 'chai'
+import sinon from 'sinon'
+import { Button } from 'grommet'
+import FeedbackModal from './FeedbackModal'
+
+describe('FeedbackModal', function () {
+ it('should render without crashing', function () {
+ const wrapper = shallow()
+ expect(wrapper).to.be.ok
+ })
+
+ it('should not render if showModal false', function () {
+ const wrapper = shallow()
+ expect(wrapper.html()).to.be.null
+ })
+
+ it('should show messages', function () {
+ const wrapper = shallow()
+ const list = wrapper.find('li')
+ expect(list).to.have.lengthOf(3)
+ })
+
+ it('should call hideFeedback on close', function () {
+ const hideFeedbackStub = sinon.stub()
+ const wrapper = shallow()
+ wrapper.find(Button).simulate('click')
+ expect(hideFeedbackStub).to.have.been.called
+ })
+})
diff --git a/packages/lib-classifier/src/components/Classifier/components/Feedback/index.js b/packages/lib-classifier/src/components/Classifier/components/Feedback/index.js
new file mode 100644
index 0000000000..72ddfb2b11
--- /dev/null
+++ b/packages/lib-classifier/src/components/Classifier/components/Feedback/index.js
@@ -0,0 +1 @@
+export { default } from './FeedbackModal'
diff --git a/packages/lib-classifier/src/components/Classifier/components/Feedback/locales/en.json b/packages/lib-classifier/src/components/Classifier/components/Feedback/locales/en.json
new file mode 100644
index 0000000000..a6eb26a592
--- /dev/null
+++ b/packages/lib-classifier/src/components/Classifier/components/Feedback/locales/en.json
@@ -0,0 +1,6 @@
+{
+ "FeedbackModal": {
+ "close": "Close",
+ "label": "Feedback"
+ }
+}
\ No newline at end of file
diff --git a/packages/lib-classifier/src/components/Classifier/components/Layout/components/DefaultLayout/DefaultLayout.js b/packages/lib-classifier/src/components/Classifier/components/Layout/components/DefaultLayout/DefaultLayout.js
index c6bc0d2ae6..877c1f4d24 100644
--- a/packages/lib-classifier/src/components/Classifier/components/Layout/components/DefaultLayout/DefaultLayout.js
+++ b/packages/lib-classifier/src/components/Classifier/components/Layout/components/DefaultLayout/DefaultLayout.js
@@ -2,6 +2,7 @@ import React from 'react'
import styled from 'styled-components'
import { pxToRem } from '@zooniverse/react-components'
+import FeedbackModal from '../../../Feedback'
import ImageToolbar from '../../../ImageToolbar'
import MetaTools from '../../../MetaTools'
import SubjectViewer from '../../../SubjectViewer'
@@ -56,6 +57,7 @@ function DefaultLayout () {
+
)
}
diff --git a/packages/lib-classifier/src/components/Classifier/components/TaskArea/components/Tasks/Tasks.js b/packages/lib-classifier/src/components/Classifier/components/TaskArea/components/Tasks/Tasks.js
index 6c220fb07e..7daa818d15 100644
--- a/packages/lib-classifier/src/components/Classifier/components/TaskArea/components/Tasks/Tasks.js
+++ b/packages/lib-classifier/src/components/Classifier/components/TaskArea/components/Tasks/Tasks.js
@@ -10,8 +10,6 @@ import TaskHelp from './components/TaskHelp'
import { default as TaskNavButtons } from './components/TaskNavButtons'
function storeMapper (stores) {
- // TODO remove feedback store, added so FeedbackStore afterAttach would run during initial store development
- const { isActive } = stores.classifierStore.feedback
const { loadingState } = stores.classifierStore.workflows
const { active: step } = stores.classifierStore.workflowSteps
const tasks = stores.classifierStore.workflowSteps.activeStepTasks
diff --git a/packages/lib-classifier/src/store/FeedbackStore.js b/packages/lib-classifier/src/store/FeedbackStore.js
index 198386d347..dc4ef78e0b 100644
--- a/packages/lib-classifier/src/store/FeedbackStore.js
+++ b/packages/lib-classifier/src/store/FeedbackStore.js
@@ -1,5 +1,6 @@
import { autorun } from 'mobx'
-import { addDisposer, getRoot, onAction, types } from 'mobx-state-tree'
+import { addDisposer, addMiddleware, getRoot, onAction, types } from 'mobx-state-tree'
+import { flatten } from 'lodash'
import helpers from './feedback/helpers'
import strategies from './feedback/strategies'
@@ -7,23 +8,37 @@ import strategies from './feedback/strategies'
const FeedbackStore = types
.model('FeedbackStore', {
isActive: types.optional(types.boolean, false),
- rules: types.map(types.frozen({}))
+ rules: types.map(types.frozen({})),
+ showModal: types.optional(types.boolean, false)
})
+ .volatile(self => ({
+ onHide: () => true
+ }))
+ .views(self => ({
+ get hideSubjectViewer () {
+ return flatten(Array.from(self.rules.values()))
+ .some(rule => rule.hideSubjectViewer)
+ },
+ get messages () {
+ return flatten(Array.from(self.rules.values()))
+ .map(rule => {
+ if (rule.success && rule.successEnabled) {
+ return rule.successMessage
+ } else if (!rule.success && rule.failureEnabled) {
+ return rule.failureMessage
+ }
+ }).filter(Boolean)
+ }
+ }))
.actions(self => {
- function afterAttach () {
- createSubjectObserver()
- createClassificationObserver()
+ function setOnHide (onHide) {
+ self.onHide = onHide
}
- function createSubjectObserver () {
- const subjectDisposer = autorun(() => {
- const subject = getRoot(self).subjects.active
- if (subject) {
- self.reset()
- self.createRules(subject)
- }
- })
- addDisposer(self, subjectDisposer)
+ function afterAttach () {
+ createClassificationObserver()
+ createSubjectMiddleware()
+ createSubjectObserver()
}
function createClassificationObserver () {
@@ -31,15 +46,49 @@ const FeedbackStore = types
onAction(getRoot(self).classifications, (call) => {
if (call.name === 'completeClassification') {
const annotations = getRoot(self).classifications.currentAnnotations
- for (const value of annotations.values()) {
- self.update(value)
- }
+ annotations.forEach(annotation => self.update(annotation))
}
})
})
addDisposer(self, classificationDisposer)
}
+ function onSubjectAdvance (call, next, abort) {
+ const shouldShowFeedback = self.isActive && self.messages.length && !self.showModal
+ if (shouldShowFeedback) {
+ abort()
+ const onHide = getRoot(self).subjects.advance
+ self.setOnHide(onHide)
+ self.showFeedback()
+ } else {
+ next(call)
+ }
+ }
+
+ function createSubjectMiddleware () {
+ const subjectMiddleware = autorun(() => {
+ addMiddleware(getRoot(self).subjects, (call, next, abort) => {
+ if (call.name === 'advance') {
+ onSubjectAdvance(call, next, abort)
+ } else {
+ next(call)
+ }
+ })
+ })
+ addDisposer(self, subjectMiddleware)
+ }
+
+ function createSubjectObserver () {
+ const subjectDisposer = autorun(() => {
+ const subject = getRoot(self).subjects.active
+ if (subject) {
+ self.reset()
+ self.createRules(subject)
+ }
+ })
+ addDisposer(self, subjectDisposer)
+ }
+
function createRules (subject) {
const project = getRoot(self).projects.active
const workflow = getRoot(self).workflows.active
@@ -51,6 +100,15 @@ const FeedbackStore = types
}
}
+ function showFeedback () {
+ self.showModal = true
+ }
+
+ function hideFeedback () {
+ self.onHide()
+ self.showModal = false
+ }
+
function update (annotation) {
const { task, value } = annotation
const taskRules = self.rules.get(task) || []
@@ -64,11 +122,16 @@ const FeedbackStore = types
function reset () {
self.isActive = false
self.rules.clear()
+ self.showModal = false
}
return {
afterAttach,
createRules,
+ setOnHide,
+ showFeedback,
+ hideFeedback,
+ onSubjectAdvance,
update,
reset
}
diff --git a/packages/lib-classifier/src/store/FeedbackStore.spec.js b/packages/lib-classifier/src/store/FeedbackStore.spec.js
index ded9f6f33c..6c9c60a078 100644
--- a/packages/lib-classifier/src/store/FeedbackStore.spec.js
+++ b/packages/lib-classifier/src/store/FeedbackStore.spec.js
@@ -3,31 +3,47 @@ import sinon from 'sinon'
import FeedbackStore from './FeedbackStore'
import strategies from './feedback/strategies'
import helpers from './feedback/helpers'
-console.log(helpers)
describe('Model > FeedbackStore', function () {
let feedback
let feedbackStub
before(function () {
+ sinon.stub(helpers, 'isFeedbackActive').callsFake(() => feedbackStub.isActive)
+ sinon.stub(helpers, 'generateRules').callsFake(() => feedbackStub.rules)
+ strategies.testStrategy = {
+ reducer: sinon.stub().callsFake(rule => rule)
+ }
+ })
+
+ beforeEach(function () {
feedbackStub = {
isActive: true,
rules: {
T0: [{
id: 'testRule',
+ hideSubjectViewer: true,
answer: '0',
- strategy: "testStrategy",
+ strategy: 'testStrategy',
+ success: true,
successEnabled: true,
successMessage: 'Yay!',
failureEnabled: true,
failureMessage: 'No!'
+ }],
+ T1: [{
+ id: 'testRule',
+ hideSubjectViewer: false,
+ answer: '0',
+ strategy: 'testStrategy',
+ success: false,
+ successEnabled: true,
+ successMessage: 'Yippee!',
+ failureEnabled: true,
+ failureMessage: 'Nope!'
}]
- }
- }
- sinon.stub(helpers, 'isFeedbackActive').callsFake(() => feedbackStub.isActive)
- sinon.stub(helpers, 'generateRules').callsFake(() => feedbackStub.rules)
- strategies.testStrategy = {
- reducer: sinon.stub().callsFake(rule => rule)
+ },
+ showModal: false
}
feedback = FeedbackStore.create(feedbackStub)
})
@@ -48,7 +64,7 @@ describe('Model > FeedbackStore', function () {
id: '3'
}
- before(function () {
+ beforeEach(function () {
feedback.projects = {
active: project
}
@@ -58,6 +74,11 @@ describe('Model > FeedbackStore', function () {
feedback.createRules(subject)
})
+ afterEach(function () {
+ helpers.isFeedbackActive.resetHistory()
+ helpers.generateRules.resetHistory()
+ })
+
it('should set active state', function () {
expect(helpers.isFeedbackActive).to.have.been.calledOnceWith(project, subject, workflow)
})
@@ -68,7 +89,7 @@ describe('Model > FeedbackStore', function () {
})
describe('update', function () {
- before(function () {
+ beforeEach(function () {
const annotation = { task: 'T0', value: 0 }
feedback.update(annotation)
})
@@ -80,7 +101,7 @@ describe('Model > FeedbackStore', function () {
})
describe('reset', function () {
- before(function () {
+ beforeEach(function () {
feedback.reset()
})
@@ -89,7 +110,93 @@ describe('Model > FeedbackStore', function () {
})
it('should reset feedback rules', function () {
- expect(feedback.rules).to.be.empty
+ expect(feedback.rules.toJSON()).to.be.empty
+ })
+
+ it('should reset showModal state', function () {
+ expect(feedback.showModal).to.be.false
+ })
+ })
+
+ describe('showFeedback', function () {
+ it('should set showModal state to true', function () {
+ feedback.showFeedback()
+ expect(feedback.showModal).to.be.true
+ })
+ })
+
+ describe('hideFeedback', function () {
+ beforeEach(function () {
+ feedback.setOnHide(sinon.stub())
+ feedback.hideFeedback()
+ })
+
+ it('should set showModal state to false', function () {
+ expect(feedback.showModal).to.be.false
+ })
+
+ it('should call the onHide callback', function () {
+ expect(feedback.onHide).to.have.been.calledOnce
+ })
+ })
+
+ describe('messages', function () {
+ it('should return an array of feedback messages', function () {
+ expect(feedback.messages).to.eql(['Yay!', 'Nope!'])
+ })
+ })
+
+ describe('hideSubjectViewer', function () {
+ it('should return true if any rule hides subject viewer', function () {
+ expect(feedback.hideSubjectViewer).to.equal(true)
+ })
+
+ it('should return false if no rule hides subject viewer', function () {
+ const [ rule ] = feedback.rules.get('T0')
+ rule.hideSubjectViewer = false
+ expect(feedback.hideSubjectViewer).to.equal(false)
+ })
+ })
+
+ describe('when the subject queue advances', function () {
+ const call = {
+ name: 'advance',
+ args: []
+ }
+ const next = sinon.stub()
+ const abort = sinon.stub()
+
+ describe('when feedback is inactive', function () {
+ beforeEach(function () {
+ feedback = FeedbackStore.create({ isActive: false })
+ feedback.onSubjectAdvance(call, next, abort)
+ })
+
+ it('should continue the action', function () {
+ expect(next.withArgs(call)).to.have.been.calledOnce
+ })
+ })
+
+ describe('when feedback is active', function () {
+ beforeEach(function () {
+ feedback = FeedbackStore.create(feedbackStub)
+ feedback.subjects = {
+ advance: sinon.stub()
+ }
+ feedback.onSubjectAdvance(call, next, abort)
+ })
+
+ it('should abort the action', function () {
+ expect(abort).to.have.been.calledOnce
+ })
+
+ it('should show feedback', function () {
+ expect(feedback.showModal).to.be.true
+ })
+
+ it('should set the onHide callback', function () {
+ expect(feedback.onHide).to.equal(feedback.subjects.advance)
+ })
})
})
})