Commit cd96da98 authored by Jure's avatar Jure

Initial commit

parents
Pipeline #13646 failed with stages
in 35 seconds
**/dist
**/node_modules
**/coverage
{
"parser": "babel-eslint",
"env": {
"es6": true,
"browser": true
},
"extends": ["pubsweet"],
"rules": {
"import/no-dynamic-require": 0,
"global-require": 0,
"import/no-extraneous-dependencies": 0,
"sort-keys": 0
},
"overrides": [
{
"files": ["test/**/*.test.js"],
"globals": {
"fixture": true,
"text": true
}
}
]
}
# Created by https://www.gitignore.io/api/osx,node
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# IDEs
.idea
# pubsweet-specific
myapp
testapp
api/db/*
!api/db/.gitkeep
.vscode
.wallaby.js
logs
.env.*
client/dist
server/dist
coverage/
yarn-error.log
package-lock.json
config/local*.*
_build
variables:
IMAGE_ORG: pubsweet
IMAGE_NAME: starter
stages:
- build
- test
build:
image: docker:19.03.1
services:
- docker:19.03.1-dind
stage: build
script:
- docker version
- docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
- echo "Ignore warning! Cannot perform an interactive login from a non TTY device"
- docker build -t $IMAGE_ORG/$IMAGE_NAME:$CI_COMMIT_SHA .
- docker push $IMAGE_ORG/$IMAGE_NAME:$CI_COMMIT_SHA
test:chrome:
image: $IMAGE_ORG/$IMAGE_NAME:$CI_COMMIT_SHA
stage: test
variables:
GIT_STRATEGY: none
# setup data for postgres image
POSTGRES_USER: test
POSTGRES_PASSWORD: pw
# connection details for tests
PGUSER: test
PGPASSWORD: pw
NODE_ENV: test
services:
- postgres
script:
- cd ${HOME}
# specify host here else it confuses the linked postgres image
- PGHOST=postgres npx testcafe 'chrome:headless --no-sandbox' test/**/*.test.js
test:firefox:
image: $IMAGE_ORG/$IMAGE_NAME:$CI_COMMIT_SHA
stage: test
variables:
GIT_STRATEGY: none
# setup data for postgres image
POSTGRES_USER: test
POSTGRES_PASSWORD: pw
# connection details for tests
PGUSER: test
PGPASSWORD: pw
NODE_ENV: test
services:
- postgres
script:
- cd ${HOME}
# specify host here else it confuses the linked postgres image
- PGHOST=postgres npx testcafe firefox:headless test/**/*.test.js
lint:
image: $IMAGE_ORG/$IMAGE_NAME:$CI_COMMIT_SHA
stage: test
variables:
GIT_STRATEGY: none
script:
- cd ${HOME}
- npm run lint
{
"*.{js,jsx}": ["prettier --write", "eslint --fix", "git add"],
"*.{json,md,css,scss}": ["prettier --write", "git add"]
}
{
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions"
]
}
import 'regenerator-runtime/runtime'
import React from 'react'
import ReactDOM from 'react-dom'
import { hot } from 'react-hot-loader'
import { Root } from 'pubsweet-client'
import { createBrowserHistory } from 'history'
import theme from './theme'
import routes from './routes'
const history = createBrowserHistory()
const rootEl = document.getElementById('root')
ReactDOM.render(
<Root history={history} routes={routes} theme={theme} />,
rootEl,
)
export default hot(module)(Root)
import React from 'react'
import PropTypes from 'prop-types'
const App = ({ children, ...props }) => <div>{children}</div>
App.propTypes = {
children: PropTypes.node.isRequired,
}
export default App
import { createGlobalStyle } from 'styled-components'
const GlobalStyle = createGlobalStyle`
/* http://meyerweb.com/eric/tools/css/reset/
v4.0 | 20180602
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
// ol, ul {
// list-style: none;
// }
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html {
display: flex;
min-height: 100%;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
body {
box-sizing: border-box;
height: 100%;
width: 100%;
overscroll-behavior-y: none;
}
#root {
height: 100%
width: 100%
}
* {
border: 0;
box-sizing: inherit;
-webkit-font-smoothing: auto;
font-weight: inherit;
margin: 0;
outline: 0;
padding: 0;
text-decoration: none;
text-rendering: optimizeLegibility;
-webkit-appearance: none;
-moz-appearance: none;
}
`
export default GlobalStyle
\ No newline at end of file
{
"name": "pubsweet-component-login",
"version": "3.0.24",
"description": "Basic login component for PubSweet",
"main": "src/index.js",
"author": "Collaborative Knowledge Foundation",
"license": "MIT",
"dependencies": {
"@pubsweet/ui": "^12.3.3",
"@pubsweet/ui-toolkit": "^2.2.21",
"formik": "^1.3.0",
"prop-types": "^15.5.10",
"react-router-dom": "^5.0.0",
"recompose": "^0.30.0",
"styled-components": "^4.1.3"
},
"peerDependencies": {
"@apollo/react-hoc": ">=3.0.1",
"config": ">=3.0.1",
"graphql-tag": ">=2.10.0",
"lodash": ">=4.17.11",
"pubsweet-client": ">=1.0.0",
"react": ">=16.9",
"styled-components": ">=4.1.3"
},
"repository": {
"type": "git",
"url": "https://gitlab.coko.foundation/pubsweet/pubsweet",
"path": "components/client/component-login"
},
"gitHead": "6b100b76f21785e5e50fca082a2743d3d0b1c88a"
}
import React, { useState } from 'react'
import { Redirect } from 'react-router-dom'
import PropTypes from 'prop-types'
import { withFormik, Field } from 'formik'
import { isEmpty } from 'lodash'
import config from 'config'
import { override } from '@pubsweet/ui-toolkit'
import { useMutation } from '@apollo/react-hooks'
import {
CenteredColumn,
ErrorText,
H1,
Link,
Button,
TextField,
} from '@pubsweet/ui'
import styled from 'styled-components'
import { LOGIN_USER } from './graphql/mutations'
const getNextUrl = () => {
const url = new URL(window.location.href)
// Where should we redirect after successful login?
const redirectLink =
(config['pubsweet-component-login'] &&
config['pubsweet-component-login'].redirect) ||
config['pubsweet-client']['login-redirect']
return `${url.searchParams.get('next') || redirectLink}`
}
const localStorage = window.localStorage || undefined
const handleSubmit = (values, { props, setSubmitting, setErrors }) => {
console.log(values, props)
return props
.loginUser({ variables: { input: values } })
.then(({ data, errors }) => {
if (!errors) {
localStorage.setItem('token', data.loginUser.token)
setTimeout(() => {
console.log(props)
props.onLoggedIn(getNextUrl())
}, 100)
}
})
.catch(e => {
if (e.graphQLErrors && e.graphQLErrors.length > 0) {
setSubmitting(false)
setErrors(e.graphQLErrors[0].message)
}
})
}
const Logo = styled.div`
${override('Login.Logo')};
`
Logo.displayName = 'Logo'
const FormContainer = styled.div`
${override('Login.FormContainer')};
`
const UsernameInput = props => (
<TextField label="Username" placeholder="Username" {...props.field} />
)
const PasswordInput = props => (
<TextField
label="Password"
placeholder="Password"
{...props.field}
type="password"
/>
)
const Login = ({
errors,
logo = null,
signup = true,
passwordReset = true,
redirectLink,
handleSubmit,
}) => {
// Is ORCID authentication enabled?
const orcid =
config['pubsweet-component-login'] &&
config['pubsweet-component-login'].orcid
return redirectLink ? (
<Redirect to={redirectLink} />
) : (
<CenteredColumn small>
{logo && (
<Logo>
<img alt="pubsweet-logo" src={`${logo}`} />
</Logo>
)}
<FormContainer>
<H1>Login</H1>
{!isEmpty(errors) && <ErrorText>{errors}</ErrorText>}
<form onSubmit={handleSubmit}>
<Field component={UsernameInput} name="username" />
<Field component={PasswordInput} name="password" />
<Button primary type="submit">
Login
</Button>
</form>
{orcid && <p>Log in with orcid </p>}
{signup && (
<p>
Don&apos;t have an account? <Link to="/signup">Sign up</Link>
</p>
)}
{passwordReset && (
<p>
Forgot your password?{' '}
<Link to="/password-reset">Reset password</Link>
</p>
)}
</FormContainer>
</CenteredColumn>
)
}
Login.propTypes = {
error: PropTypes.string,
actions: PropTypes.object,
location: PropTypes.object,
signup: PropTypes.bool,
passwordReset: PropTypes.bool,
logo: PropTypes.string,
}
const EnhancedFormik = withFormik({
initialValues: {
username: '',
password: '',
},
mapPropsToValues: props => ({
username: props.username,
password: props.password,
}),
displayName: 'login',
handleSubmit,
})(Login)
export default props => {
const [loginUser, { data }] = useMutation(LOGIN_USER)
const [redirectLink, setRedirectLink] = useState(null)
const onLoggedIn = () => setRedirectLink(getNextUrl())
return (
<EnhancedFormik
loginUser={loginUser}
onLoggedIn={onLoggedIn}
redirectLink={redirectLink}
{...props}
/>
)
}
A login form
```js
const { withFormik } = require('formik')
const LoginForm = withFormik({
initialValues: {
username: '',
password: '',
},
mapPropsToValues: props => ({
username: props.username,
password: props.password,
}),
displayName: 'login',
handleSubmit: val => console.log(val),
})(Login)
;<LoginForm />
```
Which can have an error message:
```js
const { withFormik } = require('formik')
const LoginForm = withFormik({
initialValues: {
username: '',
password: '',
},
mapPropsToValues: props => ({
username: props.username,
password: props.password,
}),
displayName: 'login',
handleSubmit: (values, { setErrors }) =>
setErrors('Wrong username or password.'),
})(Login)
;<LoginForm />
```
import gql from 'graphql-tag'
export const LOGIN_USER = gql`
mutation($input: LoginUserInput) {
loginUser(input: $input) {
token
}
}
`
import Login from './Login'
export default Login
import { shallow } from 'enzyme'
import React from 'react'
import { Field } from 'formik'
import { Button, Link, ErrorText } from '@pubsweet/ui'
import Login from '../src/Login'
describe('<Login/>', () => {
const makeWrapper = (props = {}) => shallow(<Login {...props} />)
it('renders the login form', () => {
const wrapper = makeWrapper()
expect(wrapper.debug()).toMatchSnapshot()
expect(wrapper.find(Field)).toHaveLength(2)
expect(wrapper.find(Button)).toHaveLength(1)
expect(wrapper.find(Link)).toHaveLength(2)
})
it('shows error', () => {
const wrapper = makeWrapper({ errors: 'Yikes!' })
expect(wrapper.find(ErrorText)).toHaveLength(1)
})
it('can hide logo', () => {
const logo = 'data:image/gif;base64,R0lGODlhDwAPAKECAAAAzMzM/////'
const wrapper1 = makeWrapper({ logo })
const wrapper2 = makeWrapper({ logo: null })
expect(wrapper1.find('Logo')).toHaveLength(1)
expect(wrapper2.find('Logo')).toHaveLength(0)
})
it('can hide sign up link', () => {
const wrapper1 = makeWrapper()
const wrapper2 = makeWrapper({ signup: false })
expect(wrapper1.find({ to: '/signup' })).toHaveLength(1)
expect(wrapper2.find({ to: '/signup' })).toHaveLength(0)
})
it('can hide password reset link', () => {
const wrapper1 = makeWrapper()
const wrapper2 = makeWrapper({ passwordReset: false })
expect(wrapper1.find({ to: '/password-reset' })).toHaveLength(1)
expect(wrapper2.find({ to: '/password-reset' })).toHaveLength(0)
})
it('triggers submit handler', () => {
const handleSubmit = jest.fn()
const wrapper = makeWrapper({ handleSubmit })
wrapper.find('form').simulate('submit')
expect(handleSubmit).toHaveBeenCalled()
})
})
import React from 'react'
import { mount } from 'enzyme'
import { MockedProvider } from '@apollo/react-testing'
import { ThemeProvider } from 'styled-components'
import { MemoryRouter, Route, Switch } from 'react-router-dom'
import wait from 'waait'
import { LOGIN_USER } from '../src/graphql/mutations'
import LoginContainer from '../src/LoginContainer'
const user1 = {
id: 'user1',
username: 'admin',
password: 'adminadmin',
admin: true,
teams: [],
}