Commit c32cb16b authored by Alexandros Georgantas's avatar Alexandros Georgantas

Merge branch 'add-dynamic-component-types' into 'master'

Add dynamic component types

See merge request editoria/editoria!145
parents 3378924b fd52e458
scalar JSON
type applicationParameter {
id: ID
context: String
area: String
config: JSON
}
input updateParametersInput {
context: String
area: String
config: String
}
extend type Query {
getApplicationParameters(context: String, area: String): [applicationParameter!]!
}
extend type Mutation {
updateApplicationParameters(input: updateParametersInput!): applicationParameter!
}
extend type Subscription {
updateApplicationParameters: [applicationParameter!]!
}
const logger = require('@pubsweet/logger')
const { ApplicationParameter } = require('editoria-data-model/src').models
const pubsweetServer = require('pubsweet-server')
const { UPDATE_APPLICATION_PARAMETERS } = require('./consts')
const { pubsubManager } = pubsweetServer
const getApplicationParameters = async (_, args, ctx) => {
const { context, area } = args
const parameters = await ApplicationParameter.query()
.skipUndefined()
.where({ context, area })
// console.log(parameters)
// console.log(
// parameters.map(parameter => {
// console.log(parameter.config,11111)
// parameter.config = JSON.parse(parameter.config)
// console.log(parameter.config, 222)
// return parameter
// }),
// )
return parameters
}
const updateApplicationParameters = async (_, { input }, ctx) => {
const { context, area, config } = input
try {
const pubsub = await pubsubManager.getPubsub()
const parameter = await ApplicationParameter.query().findOne({
context,
area,
})
const updatedParameter = await parameter.$query().updateAndFetch({ config })
const applicationParameters = await ApplicationParameter.query()
pubsub.publish(UPDATE_APPLICATION_PARAMETERS, {
updateApplicationParameters: applicationParameters,
})
return updatedParameter
} catch (e) {
logger.error(e)
throw new Error(e)
}
}
module.exports = {
Query: {
getApplicationParameters,
},
Mutation: {
updateApplicationParameters,
},
Subscription: {
updateApplicationParameters: {
subscribe: async () => {
const pubsub = await pubsubManager.getPubsub()
return pubsub.asyncIterator(UPDATE_APPLICATION_PARAMETERS)
},
},
},
}
const UPDATE_APPLICATION_PARAMETERS = 'UPDATE_APPLICATION_PARAMETERS'
module.exports = {
UPDATE_APPLICATION_PARAMETERS,
}
module.exports = {
resolvers: require('./applicationParameter.resolvers'),
typeDefs: require('../graphqlLoaderUtil')(
'applicationParameter/applicationParameter.graphql',
),
}
......@@ -4,6 +4,7 @@ const {
Book,
BookComponent,
BookComponentState,
ApplicationParameter,
} = require('editoria-data-model/src').models
const {
......@@ -64,6 +65,11 @@ const getDashBoardRules = async (_, args, ctx) => {
}
const getBookBuilderRules = async (_, args, ctx) => {
const bookBuilderAppConfig = await ApplicationParameter.query().where({
context: 'bookBuilder',
area: 'stages',
})
await ctx.connectors.UserLoader.model.userTeams.clear()
const book = await Book.find(args.id)
const bookComponents = await BookComponent.query().where({
......@@ -110,7 +116,7 @@ const getBookBuilderRules = async (_, args, ctx) => {
result.bookComponentStateRules = await Promise.all(
map(bookComponentState, async value => {
const data = await Promise.all(
map(config.bookBuilder.stages, async v => {
map(bookBuilderAppConfig[0].config, async v => {
const data = await executeMultipleAuthorizeRules(
ctx,
{
......
......@@ -9,14 +9,13 @@ const keys = require('lodash/keys')
const map = require('lodash/map')
const forEach = require('lodash/forEach')
const clone = require('lodash/clone')
const get = require('lodash/get')
const assign = require('lodash/assign')
const config = require('config')
const logger = require('@pubsweet/logger')
const pubsweetServer = require('pubsweet-server')
const { withFilter } = require('graphql-subscriptions')
const {
ApplicationParameter,
BookComponentState,
BookComponent,
BookComponentTranslation,
......@@ -70,8 +69,14 @@ const ingestWordFile = async (_, { files }, ctx) => {
const addBookComponent = async (_, args, ctx, info) => {
const { divisionId, bookId, componentType, title, uploading } = args.input
const bookBuilder = get(config, 'bookBuilder')
const workflowStages = get(bookBuilder, 'stages')
const applicationParameters = await ApplicationParameter.query().where({
context: 'bookBuilder',
area: 'stages',
})
const { config: workflowStages } = applicationParameters[0]
let bookComponentWorkflowStages
try {
......@@ -153,8 +158,13 @@ const addBookComponent = async (_, args, ctx, info) => {
}
const addBookComponents = async (_, { input }, ctx, info) => {
const bookBuilder = get(config, 'bookBuilder')
const workflowStages = get(bookBuilder, 'stages')
const applicationParameters = await ApplicationParameter.query().where({
context: 'bookBuilder',
area: 'stages',
})
const { config: workflowStages } = applicationParameters[0]
let bookComponentWorkflowStages
try {
......@@ -314,8 +324,12 @@ const updateWorkflowState = async (_, { input }, ctx) => {
try {
const { id, workflowStages } = input
const pubsub = await pubsubManager.getPubsub()
const bookBuilder = get(config, 'bookBuilder')
const lockTrackChanges = get(bookBuilder, 'lockTrackChangesWhenReviewing')
const applicationParameters = await ApplicationParameter.query().where({
context: 'bookBuilder',
area: 'lockTrackChangesWhenReviewing',
})
const { config: lockTrackChanges } = applicationParameters[0]
logger.info(
`Searching of book component state for the book component with id ${id}`,
......
const indexOf = require('lodash/indexOf')
const find = require('lodash/find')
const utils = require('../helpers/utils')
const config = require('config')
const { transaction } = require('objection')
const {
ApplicationParameter,
BookComponent,
Division,
Book,
......@@ -25,6 +25,11 @@ const updateBookComponentOrder = async (
Division,
Book,
async (BookComponent, Division, Book) => {
const applicationParameters = await ApplicationParameter.query().where({
context: 'bookBuilder',
area: 'divisions',
})
const { config: divisions } = applicationParameters[0]
const bookComponent = await BookComponent.findById(bookComponentId)
const sourceDivision = await Division.findById(bookComponent.divisionId)
const found = indexOf(sourceDivision.bookComponents, bookComponentId)
......@@ -62,7 +67,7 @@ const updateBookComponentOrder = async (
bookComponents: updatedTargetDivisionBookComponents,
},
)
const divisionConfig = find(config.bookBuilder.divisions, {
const divisionConfig = find(divisions, {
name: updatedDivision.label,
})
await BookComponent.query().patchAndFetchById(bookComponentId, {
......
const authorize = require('./authorize')
const applicationParameter = require('./applicationParameter')
const book = require('./book')
const bookComponent = require('./bookComponent')
const bookCollection = require('./bookCollection')
......@@ -17,6 +18,7 @@ const merge = require('lodash/merge')
module.exports = {
typeDefs: [
authorize.typeDefs,
applicationParameter.typeDefs,
book.typeDefs,
bookComponent.typeDefs,
bookCollection.typeDefs,
......@@ -30,6 +32,7 @@ module.exports = {
resolvers: merge(
{},
authorize.resolvers,
applicationParameter.resolvers,
book.resolvers,
bookComponent.resolvers,
bookCollection.resolvers,
......
......@@ -71,13 +71,14 @@ export class BookBuilder extends React.Component {
render() {
const {
book,
state,
applicationParameter,
history,
addBookComponent,
onMetadataAdd,
addBookComponents,
currentUser,
deleteBookComponent,
updateApplicationParameters,
updateBookComponentPagination,
updateBookComponentOrder,
updateBookComponentWorkflowState,
......@@ -94,17 +95,15 @@ export class BookBuilder extends React.Component {
loading,
loadingRules,
setState,
refetchingBookBuilderRules,
onWorkflowUpdate,
} = this.props
if (loading || loadingRules) return 'Loading...'
if (!book) return null
const { canViewTeamManager, canViewMultipleFilesUpload } = rules
const { divisions, productionEditors } = book
const productionEditorActions = []
const headerActions = [
<MetadataButton book={book} onMetadataAdd={() => onMetadataAdd(book)} />,
......@@ -151,6 +150,7 @@ export class BookBuilder extends React.Component {
<Header bookTitle={book.title} actions={headerActions} />
<DivisionsArea
addBookComponent={addBookComponent}
applicationParameter={applicationParameter}
onWorkflowUpdate={onWorkflowUpdate}
addBookComponents={addBookComponents}
setState={setState}
......@@ -163,6 +163,7 @@ export class BookBuilder extends React.Component {
onDeleteBookComponent={onDeleteBookComponent}
divisions={divisions}
rules={rules}
updateApplicationParameters={updateApplicationParameters}
updateBookComponentContent={updateBookComponentContent}
updateBookComponentOrder={updateBookComponentOrder}
updateBookComponentPagination={updateBookComponentPagination}
......
......@@ -4,8 +4,8 @@ import React from 'react'
import { get, findIndex, map } from 'lodash'
import { adopt } from 'react-adopt'
import { withRouter } from 'react-router-dom'
import BookBuilder from './BookBuilder'
import withModal from 'editoria-common/src/withModal'
import BookBuilder from './BookBuilder'
import statefull from './Statefull'
import {
getBookQuery,
......@@ -21,6 +21,7 @@ import {
updateBookComponentUploadingMutation,
unlockBookComponentMutation,
updateBookComponentTypeMutation,
updateApplicationParametersMutation,
exportBookMutation,
orderChangeSubscription,
bookComponentAddedSubscription,
......@@ -52,6 +53,7 @@ const mapper = {
updateBookComponentContentMutation,
updateBookComponentUploadingMutation,
ingestWordFilesMutation,
updateApplicationParametersMutation,
updateBookComponentTypeMutation,
exportBookMutation,
lockChangeSubscription,
......@@ -88,6 +90,8 @@ const mapProps = args => ({
updateBookComponentUploading:
args.updateBookComponentUploadingMutation.updateUploading,
updateComponentType: args.updateBookComponentTypeMutation.updateComponentType,
updateApplicationParameters:
args.updateApplicationParametersMutation.updateApplicationParameter,
updateBookMetadata: args.updateBookMetadataMutation.updateMetadata,
unlockBookComponent: args.unlockBookComponentMutation.unlockBookComponent,
ingestWordFiles: args.ingestWordFilesMutation.ingestWordFiles,
......@@ -257,7 +261,7 @@ const mapProps = args => ({
const Composed = adopt(mapper, mapProps)
const Connected = props => {
const { match, history, currentUser } = props
const { match, history, currentUser, applicationParameter } = props
const { id: bookId } = match.params
return (
......@@ -273,6 +277,7 @@ const Connected = props => {
updateBookComponentPagination,
updateBookComponentOrder,
updateComponentType,
updateApplicationParameters,
updateBookComponentWorkflowState,
onError,
onWarning,
......@@ -293,9 +298,10 @@ const Connected = props => {
return (
<BookBuilder
addBookComponent={addBookComponent}
addBookComponents={addBookComponents}
applicationParameter={applicationParameter}
state={state}
setState={setState}
addBookComponents={addBookComponents}
onTeamManager={onTeamManager}
onError={onError}
onWarning={onWarning}
......@@ -316,6 +322,7 @@ const Connected = props => {
rules={rules}
updateBookComponentContent={updateBookComponentContent}
updateComponentType={updateComponentType}
updateApplicationParameters={updateApplicationParameters}
updateBookComponentOrder={updateBookComponentOrder}
updateBookComponentPagination={updateBookComponentPagination}
updateBookComponentUploading={updateBookComponentUploading}
......
......@@ -34,6 +34,11 @@ export { default as ingestWordFilesMutation } from './ingestWordFile'
export {
default as updateBookComponentTypeMutation,
} from './updateComponentType'
export {
default as updateApplicationParametersMutation,
} from './updateApplicationParameters'
export {
default as updateBookComponentUploadingMutation,
} from './updateUploading'
......
import React from 'react'
import { Mutation } from 'react-apollo'
import gql from 'graphql-tag'
const UPDATE_APPLICATION_PARAMETERS = gql`
mutation UpdateApplicationParameters($input: updateParametersInput!) {
updateApplicationParameters(input: $input) {
id
}
}
`
const updateApplicationParametersMutation = props => {
const { render } = props
return (
<Mutation mutation={UPDATE_APPLICATION_PARAMETERS}>
{(updateApplicationParameter, updateApplicationParameterResult) =>
render({
updateApplicationParameter,
updateApplicationParameterResult,
})
}
</Mutation>
)
}
export default updateApplicationParametersMutation
......@@ -38,6 +38,6 @@ const AddComponentButton = ({ add, label, type }) => {
</g>
</svg>
)
return <StyledButton onClick={addComponent} label={label} icon={icon} />
return <StyledButton icon={icon} label={label} onClick={addComponent} />
}
export default AddComponentButton
import React, { Component } from 'react'
import config from 'config'
import styled, { keyframes, css } from 'styled-components'
import React from 'react'
import styled, { keyframes } from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
// import { flow } from 'lodash'
// import { DragSource, DropTarget } from 'react-dnd'
import BookComponentTitle from './BookComponentTitle'
import BookComponentActions from './BookComponentActions'
import ComponentTypeMenu from './ComponetTypeMenu'
import { ButtonWithoutLabel } from './Button'
import SecondRow from './SecondRow'
import FirstRow from './FirstRow'
// import {
......@@ -24,7 +22,7 @@ const BookComponentContainer = styled.div`
padding-left: ${({ shouldIndent }) => (shouldIndent ? '5%' : '0')};
margin-bottom: calc(3 * ${th('gridUnit')});
background-color: white;
width:100%;
width: 100%;
`
// const ActionsRight = styled.div`
// display: flex;
......@@ -164,6 +162,7 @@ const BookComponent = ({
onWarning,
connectDragSource,
connectDropTarget,
applicationParameter,
currentUser,
history,
componentType,
......@@ -192,6 +191,7 @@ const BookComponent = ({
updateWorkflowState,
updateBookComponentContent,
updateComponentType,
updateApplicationParameters,
}) => {
const onUpdateComponentType = value => {
updateComponentType({
......@@ -204,6 +204,18 @@ const BookComponent = ({
})
}
const onAddComponentType = value => {
updateApplicationParameters({
variables: {
input: {
context: 'bookBuilder',
area: 'divisions',
config: value,
},
},
})
}
const icon = (
<svg
xmlns="http://www.w3.org/2000/svg"
......@@ -263,8 +275,10 @@ const BookComponent = ({
<ActionsLeft lock={lock}>
<GrabIcon {...provided.dragHandleProps}>{icon}</GrabIcon>
<ComponentTypeMenu
divisionType={divisionType}
addComponentType={onAddComponentType}
applicationParameter={applicationParameter}
componentType={componentType}
divisionType={divisionType}
onChange={onUpdateComponentType}
/>
{/* {lock && (
......@@ -272,9 +286,10 @@ const BookComponent = ({
)} */}
</ActionsLeft>
<BookComponentTitle
title={title}
applicationParameter={applicationParameter}
bookComponentId={id}
bookId={bookId}
title={title}
divisionType={divisionType}
componentType={componentType}
uploading={uploading}
......@@ -303,6 +318,7 @@ const BookComponent = ({
</FirstRow>
<SecondRow
applicationParameter={applicationParameter}
bookComponentId={id}
onWorkflowUpdate={onWorkflowUpdate}
onWarning={onWarning}
......
import React from 'react'
import { find, indexOf } from 'lodash'
import config from 'config'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
import withLink from 'editoria-common/src/withLink'
......@@ -59,6 +58,7 @@ const Title = styled.span`
const BookComponentTitle = ({
bookComponentId,
bookId,
applicationParameter,
uploading,
lock,
history,
......@@ -72,7 +72,11 @@ const BookComponentTitle = ({
// history.push(`/books/${bookId}/bookComponents/${bookComponentId}`)
// }
const { divisions } = config.bookBuilder
const { config: divisions } = find(applicationParameter, {
context: 'bookBuilder',
area: 'divisions',
})
// const { componentType } = bookComponent
const { showNumberBeforeComponents } = find(divisions, ['name', divisionType])
......
import React from 'react'
import React, { useState } from 'react'
import styled, { css, keyframes } from 'styled-components'
import { State } from 'react-powerplug'
import { find, map } from 'lodash'
import config from 'config'
import { find, map, groupBy, forEach } from 'lodash'
import { th } from '@pubsweet/ui-toolkit'
import { Action } from '@pubsweet/ui'
import { DefaultButton } from './Button'
import { Menu as UIMenu } from './Menu'
const AddTypeButton = styled(DefaultButton)`
padding: 10px;
transition: visibility 0.1s ease-in-out 0.1s;
`
const Input = styled.input`
border: 0;
margin: 10px;
/* line-height: 30px; */
font-family: 'Vollkorn';
color: #3f3f3f;
width: 100%;
font-size: ${th('fontSizeHeading4')};
border-bottom: 1px solid #3f3f3f;
line-height: ${th('lineHeightHeading4')};
outline: 0;
&:focus {
border-bottom: 1px solid #0964cc;
}
`
const triangle = css`
background: #3f3f3f;
content: ' ';
......@@ -17,6 +37,7 @@ const triangle = css`
width: 15px;
z-index: 200;
`
const rotate = keyframes`
from { transform: rotate(0deg);}
to { transform: rotate(360deg); }
......@@ -131,14 +152,13 @@ const triangleOption = css`
const Menu = styled(UIMenu)`
display: inline-flex;
div[role='listbox'] {
background: white;
> div:nth-child(2) {
left: 50%;
transform: translate(-50%, 0);
width: 120px;
width: 200px;
z-index: 100;
}
......@@ -150,7 +170,7 @@ const Menu = styled(UIMenu)`
overflow-y: unset;
position: relative;
text-transform: uppercase;
width: 120px;
width: 200px;
&::before {
${triangleUp}
......@@ -213,31 +233,109 @@ const OpenerWrapper = styled.div`
}
`
const AddMenu = styled.div`
display: flex;
flex-direction: row;
`
const Opener = props => {
const { toggleMenu } = props
return (
<OpenerWrapper>
<SettingsIcon onClick={toggleMenu}>{settingsIcon}</SettingsIcon>
<SettingsIcon data-test-id="component-types" onClick={toggleMenu}>
{settingsIcon}
</SettingsIcon>
</OpenerWrapper>
)
}
const ComponentTypeMenu = ({ onChange, divisionType, componentType }) => {
const { bookBuilder } = config
const { divisions } = bookBuilder
const Footer = (handleSave, divisions, divisionType) => () => {
const [text, setText] = useState(null)
const createJsonConfig = text => {
divisions.map(division => {
if (division.name === divisionType) {
division.allowedComponentTypes.push({
value: text.replace(/\s+/g, '_').toLowerCase(),
title: text,
predefined: false,
})
}
return division
})
return JSON.stringify(divisions)
}
const disabled = text ? text.replace(/\s/g, '') : false
return (
<>
<hr />
<AddMenu>
{text === null ? (
<AddTypeButton label="Add a new type" onClick={() => setText('')} />
) : (
<>
<Input
autoFocus
id="addComponentType"
defaultValue={text}
name="addComponentType"
onChange={event => setText(event.target.value)}
/>
<Action
disabled={disabled === ''}
onClick={() => handleSave(createJsonConfig(text))}
>
Save
</Action>
</>
)}
</AddMenu>
</>
)
}
const ComponentTypeMenu = ({
onChange,
addComponentType,
divisionType,
componentType,
applicationParameter,
}) => {
const { config: divisions } = find(applicationParameter, {
context: 'bookBuilder',
area: 'divisions',
})
const division = find(divisions, { name: divisionType })
const options = map(division.allowedComponentTypes, componentType => ({
label: componentType,
value: componentType,
}))
const groupedOptions = groupBy(division.allowedComponentTypes, value =>
value.predefined ? 'predefin