Commit 284bebb5 authored by Jure's avatar Jure

Merge branch 'migrate_to_graphql' into 'master'

Migrate to GraphQL, upgrade PubSweet and improve starter

See merge request !54
parents 37cd940e 59d40cfe
Pipeline #12331 passed with stages
in 7 minutes and 48 seconds
{
"presets": [
"env",
"react",
"stage-2"
"@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"
]
}
......@@ -4,20 +4,19 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { hot } from 'react-hot-loader'
import { configureStore, Root } from 'pubsweet-client'
import { Root } from 'pubsweet-client'
import theme from '@pubsweet/coko-theme'
import createHistory from 'history/createBrowserHistory'
import { createBrowserHistory } from 'history'
import routes from './routes'
const history = createHistory()
const store = configureStore(history, {})
const history = createBrowserHistory()
const rootEl = document.getElementById('root')
ReactDOM.render(
<Root history={history} routes={routes} store={store} theme={theme} />,
<Root history={history} routes={routes} theme={theme} />,
rootEl,
)
......
import React from 'react'
import PropTypes from 'prop-types'
import Navigation from './Navigation/Navigation'
const App = ({ children, ...props }) => (
<div>
<Navigation />
{children}
</div>
)
const App = ({ children, ...props }) => <div>{children}</div>
App.propTypes = {
children: PropTypes.node.isRequired,
......
import React from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'
import AuthenticatedComponent from 'pubsweet-client/src/components/AuthenticatedComponent'
import ConnectedNavigation from './Navigation/ConnectedNavigation'
import HelloWorld from './HelloWorld'
import KitchenSink from './KitchenSink'
const Dashboard = () => (
<>
{/* Everything in the dashboard is for authenticated users only */}
<AuthenticatedComponent>
<ConnectedNavigation />
<Switch>
<Redirect exact path="/dashboard" to="/dashboard/hello-world" />
<Route component={HelloWorld} path="/dashboard/hello-world" />
<Route component={KitchenSink} path="/dashboard/kitchen-sink" />
</Switch>
</AuthenticatedComponent>
</>
)
export default Dashboard
import React from 'react'
const HelloWorld = () => <div>Hello World!</div>
export default HelloWorld
import React, { useState } from 'react'
import {
H1,
Link,
Icon,
StateItem,
TextArea,
TextField,
Steps,
CenteredColumn,
Section,
} from '@pubsweet/ui'
import { NoteEditor } from 'xpub-edit'
const HelloWorld = () => {
const stateItemValues = ['To Clean', 'Cleaning', 'Cleaned']
const [stateItemIndex, setIndex] = useState(0)
const [textAreaValue, setTextAreaValue] = useState(
'A text area you can write in...',
)
const [textFieldValue, setTextFieldValue] = useState(
'And a text field to write in too!',
)
const [currentStep, setCurrentStep] = useState(0)
return (
<CenteredColumn>
<H1>A little kitchen sink garden</H1>
<Section>
This is a small number of components available in PubSweet! For more go
to <Link to="http://pubsweet.coko.foundation">our docs.</Link>
</Section>
<Section>
<StateItem
index={stateItemIndex}
update={(_, nextIndex) => setIndex(nextIndex)}
values={stateItemValues}
/>
</Section>
<Section>
<TextArea
label="Foo"
onChange={event => setTextAreaValue(event.target.value)}
placeholder="so you can write some in here"
value={textAreaValue}
/>
</Section>
<Section>
<TextField
label="Foo"
onChange={event => setTextFieldValue(event.target.value)}
placeholder="so you can write some in here"
value={textFieldValue}
/>
</Section>
<Section>
<p>There are things like a step/wizard component:</p>
<Steps currentStep={currentStep} margin="40px 50px">
<Steps.Step title="First step" />
<Steps.Step title="Second step" />
<Steps.Step title="Third step" />
</Steps>
<button
onClick={() => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}}
>
Prev
</button>
<button
onClick={() => {
if (currentStep < 3) {
setCurrentStep(currentStep + 1)
}
}}
>
Next
</button>
</Section>
<Section>
<p>Or icons... </p>
<Icon size={6}>check_circle</Icon>
</Section>
<Section>
<p>Or more complete editors, like this one</p>
<NoteEditor
onBlur={value => value}
onChange={value => value}
placeholder="Enter a message…"
title="Note"
value="I'm a more complete editor!"
/>
</Section>
</CenteredColumn>
)
}
export default HelloWorld
import React from 'react'
import { Action, ActionGroup } from '@pubsweet/ui'
const LandingPage = () => (
<>
<div>Hello World! Your PubSweet application is running just fine!</div>
<ActionGroup>
<Action to="/login">Login</Action>
<Action to="/signup">Signup</Action>
<Action to="/dashboard">Dashboard</Action>
</ActionGroup>
</>
)
export default LandingPage
import React from 'react'
import { Query, ApolloConsumer } from 'react-apollo'
import gql from 'graphql-tag'
import Navigation from './Navigation'
const CURRENT_USER = gql`
query CurrentUser {
currentUser {
id
username
admin
}
}
`
const ConnectedNavigation = props => (
<Query query={CURRENT_USER}>
{({ loading, error, data }) => {
if (loading) return 'Loading...'
return (
<ApolloConsumer>
{client => (
<Navigation
client={client}
currentUser={data.currentUser}
loading={loading}
{...props}
/>
)}
</ApolloConsumer>
)
}}
</Query>
)
export default ConnectedNavigation
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose, withProps } from 'recompose'
import { AppBar, Action } from '@pubsweet/ui'
import actions from 'pubsweet-client/src/actions'
import { Action, AppBar } from '@pubsweet/ui'
const Navigation = ({ logoutUser, currentUser, client }) => {
const links = [
<Action to="/dashboard/hello-world">Hello World</Action>,
<Action to="/dashboard/kitchen-sink">Kitchen Sink</Action>,
]
const Navigation = ({ logoutUser, currentUser }) => (
return (
<div>
<AppBar
brand={<img alt="pubsweet" src="/assets/pubsweet.jpg" />}
navLinkComponents={[<Action to="/helloworld">Hello World</Action>]}
onLogoutClick={logoutUser}
navLinkComponents={links}
onLogoutClick={() => logoutUser(client)}
user={currentUser}
/>
)
</div>
)
}
Navigation.propTypes = {
client: PropTypes.any, // eslint-disable-line
currentUser: PropTypes.any, // eslint-disable-line
history: PropTypes.any.isRequired, // eslint-disable-line
logoutUser: PropTypes.func.isRequired,
// eslint-disable-next-line react/require-default-props
currentUser: PropTypes.shape({
username: PropTypes.string,
admin: PropTypes.bool,
}),
}
export default connect(
state => ({
currentUser: state.currentUser.user,
}),
{ logoutUser: actions.logoutUser },
export default compose(
withRouter,
withProps(props => ({
logoutUser: client => {
client.cache.reset()
localStorage.removeItem('token')
props.history.push('/login')
},
})),
)(Navigation)
import React from 'react'
import PropTypes from 'prop-types'
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
const CURRENT_USER = gql`
query CurrentUser {
currentUser {
id
username
admin
}
}
`
const CurrentUserQuery = props => {
const { render } = props
return (
<Query fetchPolicy="network-only" query={CURRENT_USER}>
{render}
</Query>
)
}
CurrentUserQuery.propTypes = {
render: PropTypes.any, // eslint-disable-line
}
export default CurrentUserQuery
......@@ -2,23 +2,23 @@ import React from 'react'
import { Route, Switch } from 'react-router-dom'
// Authentication
import Login from 'pubsweet-component-login/LoginContainer'
import Signup from 'pubsweet-component-signup/SignupContainer'
import PasswordReset from 'pubsweet-component-password-reset-frontend/PasswordReset'
import AuthenticatedComponent from 'pubsweet-client/src/components/AuthenticatedComponent'
import Login from 'pubsweet-component-login'
import Signup from 'pubsweet-component-signup'
import PasswordReset from '@pubsweet/component-password-reset-client'
import App from './components/App'
const HelloWorld = () => (
<AuthenticatedComponent>
<div>Hello World!</div>
</AuthenticatedComponent>
)
// This is your presentation side of things
import LandingPage from './components/LandingPage'
// And this is where your admin stuff goes
import Dashboard from './components/Dashboard'
export default (
<App>
<Switch>
<Route component={HelloWorld} path="/dashboard" />
<Route component={LandingPage} exact path="/" />
<Route component={Dashboard} path="/dashboard" />
<Route component={Login} path="/login" />
<Route component={Signup} path="/signup" />
<Route component={PasswordReset} path="/password-reset" />
......
const { pickBy } = require('lodash')
class AuthsomeMode {
/**
* Creates a new instance of AuthsomeMode
*
* @param {string} userId A user's UUID
* @param {string} operation The operation you're authorizing for
* @param {any} object The object of authorization
* @param {any} context Context for authorization, e.g. database access
* @returns {string}
*/
constructor(userId, operation, object, context) {
this.userId = userId
this.operation = AuthsomeMode.mapOperation(operation)
this.object = object
this.context = context
}
/**
* Maps operations from HTTP verbs to semantic verbs
*
* @param {any} operation
* @returns {string}
*/
static mapOperation(operation) {
const operationMap = {
GET: 'read',
POST: 'create',
PATCH: 'update',
DELETE: 'delete',
}
return operationMap[operation] ? operationMap[operation] : operation
}
async isTeamMember(teamType, object) {
let membershipCondition
if (object) {
membershipCondition = team =>
team.teamType === teamType &&
team.object &&
team.object.id === object.id
} else {
membershipCondition = team => team.teamType === teamType
}
const memberships = await Promise.all(
this.user.teams.map(async teamId => {
const teamFound = await this.context.models.Team.find(teamId)
if (teamFound) {
return membershipCondition(teamFound)
}
return false
}),
)
return memberships.includes(true)
}
isAuthor(object) {
return this.isTeamMember('author', object)
}
async findCollectionByObject(object) {
const { type, id, book, object: collection } = object
let collectionId
switch (type) {
case 'fragment':
collectionId = book
break
case 'team':
collectionId = collection.id
break
default:
collectionId = id
break
}
if (id) {
return this.context.models.Collection.find(collectionId)
}
return undefined
}
async canRead() {
this.user = await this.context.models.User.find(this.userId)
// const collection = await this.findCollectionByObject(this.object)
// const permission = await this.isAuthor(collection)
return true
}
async canListCollections() {
this.user = await this.context.models.User.find(this.userId)
return {
filter: async collections => {
const filteredCollections = await Promise.all(
collections.map(async collection => {
const condition = await this.isAuthor(collection)
return condition ? collection : undefined
}, this),
)
return filteredCollections.filter(collection => collection)
},
}
}
async canReadUser() {
this.user = await this.context.models.User.find(this.userId)
if (this.user.id === this.object.id) {
return true
}
return {
filter: user =>
pickBy(user, (_, key) => ['id', 'username', 'type'].includes(key)),
}
}
async canListTeams() {
this.user = await this.context.models.User.find(this.userId)
return {
filter: async teams => {
const filteredTeams = await Promise.all(
teams.map(async team => {
const condition = this.belongsToTeam(team.id)
return condition ? team : undefined
}, this),
)
return filteredTeams.filter(team => team)
},
}
}
belongsToTeam(teamId) {
return this.user.teams.includes(teamId)
}
async canReadTeam() {
this.user = await this.context.models.User.find(this.userId)
return true
}
async canCreateTeam() {
this.user = await this.context.models.User.find(this.userId)
return true
}
async canUpdateTeam() {
this.user = await this.context.models.User.find(this.userId)
return true
}
async canCreateCollection() {
this.user = await this.context.models.User.find(this.userId)
return true
}
}
module.exports = {
before: async (userId, operation, object, context) => {
const user = userId && (await context.models.User.find(userId))
return user && user.admin
},
GET: (userId, operation, object, context) => {
// const mode = new AuthsomeMode(userId, operation, object, context)
// GET /api/collections
if (object && object.path === '/api/collections') {
return true
}
// GET /api/collection
if (object && object.type === 'collection') {
return true
}
// GET /api/collections/:collectionId/fragments
if (object && object.path === '/api/fragments') {
return true
}
// GET /api/collections/:collectionId/fragments/:fragmentId
if (object && object.type === 'fragment') {
return true
}
// GET /api/users
if (object && object.path === '/api/users') {
return true
}
// // GET /api/teams
if (object && object.path === '/api/teams') {
return true
}
// // GET /api/team
if (object && object.type === 'team') {
return true
}
// // GET /api/user
if (object && object.type === 'user') {
return true
}
return false
},
POST: (userId, operation, object, context) => {
const mode = new AuthsomeMode(userId, operation, object, context)
// POST /api/collections
if (object && object.path === '/api/collections') {
return mode.canCreateCollection()
}
// POST /api/users
if (object && object.path === '/api/users') {
return true
}
// POST /api/users/authenticate
if (object && object.token) {
return true
}
// POST /api/fragments
if (object && object.path === '/api/collections/:collectionId/fragments') {
return true
}
// POST /api/teams
if (object && object.path === '/api/teams') {
return true
}
return false
},
PATCH: (userId, operation, object, context) => {
// const mode = new AuthsomeMode(userId, operation, object, context)
// PATCH /api/collections/:id
let data
if (object) {
if (object.current) {
data = object.current
} else {
data = object
}
} else {
return false
}
if (data.type === 'collection') {
return true
}
// PATCH /api/fragments/:id
if (data.type === 'fragment') {
return true
}
// PATCH /api/teams/:id
if (data.current.type === 'team') {
return true
}
return false
},
DELETE: (userId, operation, object, context) => {
// const mode = new AuthsomeMode(userId, operation, object, context)
// DELETE /api/collections/:id
if (object && object.type === 'collection') {
return true
}
// DELETE /api/fragments/:id
if (object && object.type === 'fragment') {
return true
}
// DELETE /api/teams/:id
if (object && object.type === 'team') {
return true
}
return false
},
before: async (userId, operation, object, context) => true,
// const user = userId && (await context.models.User.find(userId))
// return user && user.admin
}
[
"pubsweet-component-login",
"pubsweet-component-password-reset-backend",
"pubsweet-component-signup",
"@pubsweet/model-user",
"@pubsweet/model-team",
"@pubsweet/model-blog",
"@pubsweet/model-blogpost"
"@pubsweet/model-blogpost",
"@pubsweet/component-password-reset-server"
]