diff --git a/packages/ui/package.json b/packages/ui/package.json index 324f60c944b1a84d078b1e4e6fd627ce3a4aa67b..6e579c93ce972ef4b9197193ea377ed80886ab55 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@pubsweet/ui", - "version": "0.1.3", + "version": "0.1.2", "files": ["docs", "dist", "src"], "main": "src", "jsnext:main": "src", diff --git a/packages/ui/src/atoms/StateItem.js b/packages/ui/src/atoms/StateItem.js new file mode 100644 index 0000000000000000000000000000000000000000..3d579363229cba8dbf5beee73f74fee850727b31 --- /dev/null +++ b/packages/ui/src/atoms/StateItem.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types' +import React from 'react' +import classNames from 'classnames' +import classes from './StateItem.local.scss' + +const StateItem = ({ disabled, name, update, values, index }) => { + const handleInteraction = () => { + if (disabled) return + + const nextIndex = arrayShift(values, index) + update(name, nextIndex) + } + + const arrayShift = (array, i) => (i === array.length - 1 ? 0 : i + 1) + + return ( + <span + className={classNames(classes.root, { + [classes.disabled]: disabled, + })} + disabled={disabled} + onClick={handleInteraction} + onKeyPress={handleInteraction} + role="button" + tabIndex="0" + > + {values[index]} + </span> + ) +} + +StateItem.propTypes = { + disabled: PropTypes.bool, + index: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + update: PropTypes.func.isRequired, + values: PropTypes.arrayOf(PropTypes.string).isRequired, +} + +StateItem.defaultProps = { + disabled: false, +} + +export default StateItem diff --git a/packages/ui/src/atoms/StateItem.local.scss b/packages/ui/src/atoms/StateItem.local.scss new file mode 100644 index 0000000000000000000000000000000000000000..e96cb249e4500b83e4611a6afee114db3a9b31c7 --- /dev/null +++ b/packages/ui/src/atoms/StateItem.local.scss @@ -0,0 +1,28 @@ +$dark-grey: #404040; +$light-grey: #b3b3b3; + +.root { + cursor: pointer; + font-family: var(--font-interface); + font-size: 16px; + font-style: italic; + padding: 0; + + &:focus { + outline: none; + } + + &:hover { + color: $dark-grey; + transition: 0.25s ease-in-out 0s; + } +} + +.disabled { + color: $light-grey; + cursor: default; + + &:hover { + color: $light-grey; + } +} diff --git a/packages/ui/src/atoms/StateItem.md b/packages/ui/src/atoms/StateItem.md new file mode 100644 index 0000000000000000000000000000000000000000..73b351ddbe57810bce9b6e3c251ae111a1224408 --- /dev/null +++ b/packages/ui/src/atoms/StateItem.md @@ -0,0 +1,22 @@ +An interactive element which upon click changes its text content. The available text values should be provided as an array of strings. The actual operation which takes place is a right shift on the array of provided values + +```js +const data = { + values: ['To Clean', 'Cleaning', 'Cleaned'], + index: 0, + disabled: false, + name: 'clean', +} + +const update = value => { + console.log('value', value) +} + +;<StateItem + values={data.values} + disabled={data.disabled} + update={update} + index={data.index} + name={data.name} +/> +``` diff --git a/packages/ui/src/molecules/StateList.js b/packages/ui/src/molecules/StateList.js new file mode 100644 index 0000000000000000000000000000000000000000..a9c964c866b32683af97cb2c864df6986a89c960 --- /dev/null +++ b/packages/ui/src/molecules/StateList.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { map, uniqueId, keys, last } from 'lodash' +import { ChevronRight } from 'react-feather' + +import classes from './StateList.local.scss' +import StateItem from '../atoms/StateItem' + +const StateList = ({ currentValues, update, values }) => { + const progressIds = keys(values) + const lastItem = last(progressIds) + + // TODO: Placeholder -- to be implemented with authsome + const canAct = key => true + + const handleUpdate = (name, index) => { + update(name, index) + } + + const items = map(values, (valueList, name) => { + let delimiter + const currentValueIndex = currentValues[name] + + if (name !== lastItem) { + delimiter = <ChevronRight className={classes.delimiter} size={16} /> + } + + return ( + <div className={classes.itemContainer} key={uniqueId()}> + <StateItem + disabled={!canAct(name)} + index={currentValueIndex} + name={name} + update={handleUpdate} + values={valueList} + /> + {delimiter} + </div> + ) + }) + + return <div className={classes.stateListContainer}>{items}</div> +} + +StateList.propTypes = { + currentValues: PropTypes.objectOf(PropTypes.number).isRequired, + update: PropTypes.func.isRequired, + values: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)).isRequired, +} + +export default StateList diff --git a/packages/ui/src/molecules/StateList.local.scss b/packages/ui/src/molecules/StateList.local.scss new file mode 100644 index 0000000000000000000000000000000000000000..47f3c0ac047c21992f5d7caba80324360ccc6336 --- /dev/null +++ b/packages/ui/src/molecules/StateList.local.scss @@ -0,0 +1,16 @@ +.stateListContainer { + align-items: center; + display: flex; + flex-direction: row; +} + +.itemContainer { + align-items: center; + display: flex; + flex-direction: row; +} + +.delimiter { + margin-left: 5px; + margin-right: 5px; +} diff --git a/packages/ui/src/molecules/StateList.md b/packages/ui/src/molecules/StateList.md new file mode 100644 index 0000000000000000000000000000000000000000..ffe22bb92667224ca9630d02bed00d9ecaa8b89d --- /dev/null +++ b/packages/ui/src/molecules/StateList.md @@ -0,0 +1,20 @@ +A list of State Items separated by a delimiter. + +```js +const current = { + style: 0, + edit: 0, + clean: 0, + review: 0, +} +const stateValues = { + clean: ['To Clean', 'Cleaning', 'Cleaned'], + edit: ['To Edit', 'Editing', 'Edited'], + review: ['To Review', 'Reviewing', 'Reviewed'], + style: ['To Style', 'Styling', 'Styled'], +} +const update = data => { + console.log('data', data) +} +;<StateList currentValues={current} values={stateValues} update={update} /> +``` diff --git a/packages/ui/test/StateItem.test.js b/packages/ui/test/StateItem.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a109adf701bc150ad111804eb565f235885068f6 --- /dev/null +++ b/packages/ui/test/StateItem.test.js @@ -0,0 +1,46 @@ +import React from 'react' +import { clone } from 'lodash' +import { shallow, render } from 'enzyme' +import renderer from 'react-test-renderer' + +import StateItem from '../src/atoms/StateItem' + +const myMock = jest.fn() +const props = { + values: ['To Clean', 'Cleaning', 'Cleaned'], + disabled: false, + update: myMock, + index: 1, + name: 'clean', +} + +const wrapper = shallow(<StateItem {...props} />) +const wrapperRendered = render(<StateItem {...props} />) + +describe('StateItem', () => { + test('is rendered correctly', () => { + const tree = renderer.create(<StateItem {...props} />).toJSON() + expect(tree).toMatchSnapshot() + }) + + test('with default props class disabled should not exist', () => { + expect(wrapper.is('.disabled')).toBe(false) + }) + + test('with given props should be disabled', () => { + const newProps = clone(props) + newProps.disabled = true + const newWrapper = shallow(<StateItem {...newProps} />) + + expect(newWrapper.is('.disabled')).toBe(true) + }) + + test('should render the value Cleaning', () => { + expect(wrapperRendered.text()).toEqual(props.values[props.index]) + }) + + test('update method should be triggered upon click', () => { + wrapper.simulate('click') + expect(wrapper.instance().props.update).toHaveBeenCalled() + }) +}) diff --git a/packages/ui/test/StateList.test.js b/packages/ui/test/StateList.test.js new file mode 100644 index 0000000000000000000000000000000000000000..aa56cfe13900a300be8ebcf4ad2c2b17066cf865 --- /dev/null +++ b/packages/ui/test/StateList.test.js @@ -0,0 +1,59 @@ +import React from 'react' +import { forIn } from 'lodash' +import { shallow } from 'enzyme' +import renderer from 'react-test-renderer' + +import StateItem from '../src/atoms/StateItem' +import StateList from '../src/molecules/StateList' + +const currentValues = { + style: 0, + edit: 0, + clean: 0, + review: 0, +} +const stateValues = { + clean: ['To Clean', 'Cleaning', 'Cleaned'], + edit: ['To Edit', 'Editing', 'Edited'], + review: ['To Review', 'Reviewing', 'Reviewed'], + style: ['To Style', 'Styling', 'Styled'], +} + +const props = { + currentValues, + values: stateValues, + update: () => null, +} +const wrapper = shallow(<StateList {...props} />) +const stateItems = wrapper.find(StateItem) + +describe('StateList', () => { + test('is rendered correctly', () => { + const tree = renderer.create(<StateList {...props} />).toJSON() + expect(tree).toMatchSnapshot() + }) + + test('should contain four State Item children', () => { + const itemsNumber = Object.keys(stateValues).length + + expect(stateItems.exists()).toEqual(true) + expect(stateItems).toHaveLength(itemsNumber) + }) + + test('gets the correct props', () => { + let i = 0 + const stateItemComp = stateItems.getElements() + + forIn(stateValues, (value, key) => { + const stateItem = stateItemComp[i] + const stateItemProps = stateItem.props + + expect(stateItemProps.disabled).toEqual(false) + expect(stateItemProps.index).toEqual(props.currentValues[key]) + expect(stateItemProps.name).toEqual(key) + expect(stateItemProps.values).toEqual(props.values[key]) + + i += 1 + }) + }) +}) diff --git a/packages/ui/test/__snapshots__/StateItem.test.js.snap b/packages/ui/test/__snapshots__/StateItem.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..0ed454eec8d2107af1c490e8aa56f512654e9ef7 --- /dev/null +++ b/packages/ui/test/__snapshots__/StateItem.test.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StateItem is rendered correctly 1`] = ` +<span + className="root" + disabled={false} + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex="0" +> + Cleaning +</span> +`; diff --git a/packages/ui/test/__snapshots__/StateList.test.js.snap b/packages/ui/test/__snapshots__/StateList.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..4a07e16e4c6c3c216473c8758081e6d56b465443 --- /dev/null +++ b/packages/ui/test/__snapshots__/StateList.test.js.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StateList is rendered correctly 1`] = ` +<div + className="stateListContainer" +> + <div + className="itemContainer" + > + <span + className="root" + disabled={false} + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex="0" + > + To Clean + </span> + <svg + className="delimiter" + fill="none" + height={16} + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + viewBox="0 0 24 24" + width={16} + xmlns="http://www.w3.org/2000/svg" + > + <polyline + points="9 18 15 12 9 6" + /> + </svg> + </div> + <div + className="itemContainer" + > + <span + className="root" + disabled={false} + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex="0" + > + To Edit + </span> + <svg + className="delimiter" + fill="none" + height={16} + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + viewBox="0 0 24 24" + width={16} + xmlns="http://www.w3.org/2000/svg" + > + <polyline + points="9 18 15 12 9 6" + /> + </svg> + </div> + <div + className="itemContainer" + > + <span + className="root" + disabled={false} + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex="0" + > + To Review + </span> + <svg + className="delimiter" + fill="none" + height={16} + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + viewBox="0 0 24 24" + width={16} + xmlns="http://www.w3.org/2000/svg" + > + <polyline + points="9 18 15 12 9 6" + /> + </svg> + </div> + <div + className="itemContainer" + > + <span + className="root" + disabled={false} + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex="0" + > + To Style + </span> + </div> +</div> +`; diff --git a/yarn.lock b/yarn.lock index 1ee8cc4cada25f9c05213059eef1aec9a23eda28..4135b452110ab577de8ee559e9e3bff10661d5ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,35 +55,6 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@pubsweet/db-manager@^0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@pubsweet/db-manager/-/db-manager-0.0.10.tgz#19d2613aa05f135fbe313b2842345d4cdac73c0e" - dependencies: - "@pubsweet/logger" "^0.1.0" - fs-extra "^4.0.2" - isomorphic-fetch "^2.2.1" - joi "^11.0.2" - pouchdb "^6.3.4" - pubsweet-server "^1.0.2" - -"@pubsweet/db-manager@^0.0.8": - version "0.0.8" - resolved "https://registry.yarnpkg.com/@pubsweet/db-manager/-/db-manager-0.0.8.tgz#f0d2d48ab393aa7958d6bcd8c97f0500686653bf" - dependencies: - "@pubsweet/logger" "0.0.1" - fs-extra "^4.0.2" - isomorphic-fetch "^2.2.1" - joi "^11.0.2" - pouchdb "^6.3.4" - pubsweet-server "^1.0.0-beta.2" - -"@pubsweet/logger@0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@pubsweet/logger/-/logger-0.0.1.tgz#ec0c15f04e0c64232c29173848ffe6da8190c9c2" - dependencies: - config "^1.26.2" - joi "^10.6.0" - "@pubsweet/starter@git+https://gitlab.coko.foundation/pubsweet/pubsweet-starter.git": version "1.0.0-alpha.1" resolved "git+https://gitlab.coko.foundation/pubsweet/pubsweet-starter.git#2a2a5c197a90befc2c60a6cf226b699353a6daae" @@ -5534,10 +5505,6 @@ isemail@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" -isemail@2.x.x: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6" - isemail@3.x.x: version "3.0.0" resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.0.0.tgz#c89a46bb7a3361e1759f8028f9082488ecce3dff" @@ -5638,10 +5605,6 @@ istanbul-reports@^1.1.3: dependencies: handlebars "^4.0.3" -items@2.x.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" - javascript-stringify@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" @@ -5875,23 +5838,6 @@ joi-browser@^13.0.1: version "13.0.1" resolved "https://registry.yarnpkg.com/joi-browser/-/joi-browser-13.0.1.tgz#06a7b782d94bca6fa0f107138846ea16588b2e7b" -joi@^10.0.6, joi@^10.4.1, joi@^10.6.0: - version "10.6.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-10.6.0.tgz#52587f02d52b8b75cdb0c74f0b164a191a0e1fc2" - dependencies: - hoek "4.x.x" - isemail "2.x.x" - items "2.x.x" - topo "2.x.x" - -joi@^11.0.2: - version "11.4.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-11.4.0.tgz#f674897537b625e9ac3d0b7e1604c828ad913ccb" - dependencies: - hoek "4.x.x" - isemail "3.x.x" - topo "2.x.x" - joi@^13.1.0: version "13.1.0" resolved "https://registry.yarnpkg.com/joi/-/joi-13.1.0.tgz#59e7b8714b932a1e342c3583d5841d7169ff1822" @@ -8600,15 +8546,6 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -pubsweet-component-form-group@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/pubsweet-component-form-group/-/pubsweet-component-form-group-0.1.5.tgz#392d76f8a9506731c82a7027ba95ab562f6c90ca" - dependencies: - joi-browser "^13.0.1" - prop-types "^15.5.10" - pubsweet-server "^1.0.3" - react-bootstrap "^0.31.3" - pubsweet-component-posts-manager@0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/pubsweet-component-posts-manager/-/pubsweet-component-posts-manager-0.6.5.tgz#d35ac7f74bf198d652ff81e9a790ea006ec3719c" @@ -8622,67 +8559,6 @@ pubsweet-component-posts-manager@0.6.5: react-router-dom "^4.2.2" redux "^3.7.2" -pubsweet-server@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pubsweet-server/-/pubsweet-server-1.0.2.tgz#3c48267c6e010ddea6c380599c341aadd465f541" - dependencies: - "@pubsweet/logger" "^0.1.0" - authsome "0.0.9" - bcrypt "^1.0.2" - bluebird "^3.5.1" - body-parser "^1.15.2" - colors "^1.1.2" - config "^1.26.2" - cookie-parser "^1.4.3" - dotenv "^4.0.0" - express "^4.16.1" - helmet "^3.8.1" - http-status-codes "^1.0.6" - joi "^10.0.6" - jsonwebtoken "^7.1.7" - lodash "^4.0.0" - minimist "^1.2.0" - morgan "^1.8.2" - multer "^1.1.0" - passport "^0.3.2" - passport-anonymous "^1.0.1" - passport-http-bearer "^1.0.1" - passport-local "^1.0.0" - pouchdb-adapter-http "^6.2.0" - pouchdb-adapter-leveldb "^6.1.1" - pouchdb-adapter-memory "^6.1.1" - pouchdb-core "^6.1.1" - pouchdb-find "^0.10.3" - pouchdb-upsert "^2.0.0" - promise-queue "^2.2.3" - prompt "^1.0.0" - pubsweet-sse "^0.1.4" - relational-pouch "^1.4.5" - uuid "^3.0.1" - winston "^2.2.0" - -pubsweet@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pubsweet/-/pubsweet-1.1.2.tgz#398e8570fae5dd925599f720094dad76c05c2353" - dependencies: - "@pubsweet/db-manager" "^0.0.8" - "@pubsweet/logger" "^0.1.0" - bluebird "^3.5.0" - colors "^1.1.2" - commander "^2.9.0" - express "^4.15.3" - forever-monitor "^1.7.0" - fs-extra "^4.0.2" - inflection "^1.12.0" - joi "^10.4.1" - prompt flatiron/prompt#1c95d1d8d333b5fbc13fa5f0619f3dcf0d514f87 - pubsweet-server "^1.0.2" - require-relative "^0.8.7" - uuid "^3.0.1" - webpack "^3.8.1" - webpack-dev-middleware "^1.12.0" - webpack-hot-middleware "^2.20.0" - pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" @@ -10850,12 +10726,6 @@ topo@1.x.x: dependencies: hoek "2.x.x" -topo@2.x.x: - version "2.0.2" - resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" - dependencies: - hoek "4.x.x" - topo@3.x.x: version "3.0.0" resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a"