Commit 3ae1a5ff authored by Alexandros Georgantas's avatar Alexandros Georgantas

refactor(template manager): create, delete, read, subscriptions, ui

parent a16e38c7
type Template {
id: ID!
templateName: String!
thumbnailSrc: String
name: String!
thumbnail: File
author: String
trimSize: String
target: String
files: [File]!
}
input CreateTemplateInput {
templateName: String!
files: [Upload]
# thumbnailSrc: String
target: String
name: String!
thumbnail: Upload
author: String
# filePaths: [String]!
trimSize: String
target: String
files: [Upload]!
}
input UpdateTemplateInput {
id: ID!
templateName: String
thumbnailSrc: String
target: String
name: String!
thumbnail: Upload
author: String
filePaths: [String]
trimSize: String
target: String
files: [Upload]!
}
extend type Query {
......@@ -35,3 +37,10 @@ extend type Mutation {
updateTemplate(input: UpdateTemplateInput): Template!
deleteTemplate(id: ID!): ID!
}
extend type Subscription {
templateCreated: Template!
templateDeleted: Template!
templateUpdated: Template!
}
const orderBy = require('lodash/orderBy')
const map = require('lodash/map')
const find = require('lodash/find')
const clone = require('lodash/clone')
const path = require('path')
const fs = require('fs-extra')
const config = require('config')
const logger = require('@pubsweet/logger')
const uploadsPath = config.get('pubsweet-server').uploads
const {
Template,
File,
FileTranslation,
} = require('editoria-data-model/src').models
const { Template, File } = require('editoria-data-model/src').models
const pubsweetServer = require('pubsweet-server')
// const pubsweetServer = require('pubsweet-server')
const { pubsubManager } = pubsweetServer
const {
TEMPLATE_CREATED,
TEMPLATE_DELETED,
TEMPLATE_UPDATED,
} = require('./consts')
// const { pubsubManager } = pubsweetServer
const getTemplates = async (_, { ascending, sortKey }, ctx) => {
const templates = await Template.query().where('deleted', false)
const sortable = map(templates, template => {
const { id, name, author, target } = template
return { id, name: name.toLowerCase().trim(), author, target }
})
const order = ascending ? 'asc' : 'desc'
const sorted = orderBy(templates, sortKey, [order])
const sorted = orderBy(sortable, sortKey, [order])
const result = map(sorted, item => find(templates, { id: item.id }))
return result
}
const getTemplate = async (_, { id }, ctx) => Template.query().findById(id)
const getTemplate = async (_, { id }, ctx) => Template.findById(id)
const createTemplate = async (_, { input }, ctx) => {
const { templateName, author, files, target } = input
const { name, author, files, target, trimSize, thumbnail } = input
const allowedFonts = ['.otf', '.woff', '.woff2']
// const allowedFonts = ['.otf', '.woff', '.woff2']
const allowedThumbnails = ['.png', '.jpg', '.jpeg']
const allowedFiles = ['.css', '.otf', '.woff', '.woff2']
const regex = new RegExp(
const regexFiles = new RegExp(
'([a-zA-Z0-9s_\\.-:])+(' + allowedFiles.join('|') + ')$',
)
const regexThumbnails = new RegExp(
'([a-zA-Z0-9s_\\.-:])+(' + allowedThumbnails.join('|') + ')$',
)
try {
const pubsub = await pubsubManager.getPubsub()
logger.info('About to create a new template')
const newTemplate = await new Template({
templateName,
name,
author,
target,
files: [],
trimSize,
}).save()
logger.info(`New template created with id ${newTemplate.id}`)
if (files.length > 0) {
logger.info(
`There is/are ${files.length} file/s to be uploaded for the template`,
)
await Promise.all(
map(files, async file => {
const { createReadStream, filename, mimetype, encoding } = await file
if (!regexFiles.test(filename))
throw new Error('File extension is not allowed')
const outPath = path.join(
uploadsPath,
'templates',
newTemplate.id,
filename,
)
await Promise.all(
map(files, async file => {
const { createReadStream, filename, mimetype, encoding } = await file
if (!regex.test(filename))
await fs.ensureDir(uploadsPath)
await fs.ensureDir(`${uploadsPath}/templates`)
await fs.ensureDir(`${uploadsPath}/templates/${newTemplate.id}`)
logger.info(`The path the the files will be stored is ${outPath}`)
const outStream = fs.createWriteStream(outPath)
const stream = createReadStream()
stream.pipe(
outStream,
{ encoding },
)
outStream.on('error', () => {
throw new Error('Unable to write file')
})
return new Promise((resolve, reject) => {
stream.on('end', async () => {
try {
logger.info('File uploaded to server')
const newFile = await new File({
filename,
mimetype,
source: outPath,
templateId: newTemplate.id,
}).save()
logger.info(
`File representation created on the db with file id ${
newFile.id
}`,
)
resolve()
} catch (e) {
throw new Error(e)
}
})
stream.on('error', reject)
})
}),
)
}
if (thumbnail) {
logger.info('There is a thumbnail file to be uploaded for the template')
await new Promise(async (resolve, reject) => {
const {
createReadStream,
filename,
mimetype,
encoding,
} = await thumbnail
if (!regexThumbnails.test(filename))
throw new Error('File extension is not allowed')
const outPath = path.join(
uploadsPath,
......@@ -69,32 +145,89 @@ const createTemplate = async (_, { input }, ctx) => {
outStream.on('error', () => {
throw new Error('Unable to write file')
})
return new Promise((resolve, reject) => {
stream.on('end', async () => {
try {
await new File({
filename,
mimetype,
source: outPath,
templateId: newTemplate.id,
}).save()
resolve()
} catch (e) {
throw new Error(e)
}
})
stream.on('error', reject)
stream.on('end', async () => {
try {
logger.info('Thumbnail uploaded to the server')
const newThumbnail = await new File({
filename,
mimetype,
source: outPath,
templateId: newTemplate.id,
}).save()
logger.info(
`Thumbnail representation created on the db with file id ${
newThumbnail.id
}`,
)
await Template.query()
.patch({ thumbnailId: newThumbnail.id })
.findById(newTemplate.id)
logger.info('Template thumbnailId property updated')
resolve()
} catch (e) {
throw new Error(e)
}
})
}),
)
stream.on('error', reject)
})
}
pubsub.publish(TEMPLATE_CREATED, {
templateCreated: newTemplate,
})
logger.info('New template created msg broadcasted')
return newTemplate
} catch (e) {
throw new Error(e)
}
}
// TODO:
const updateTemplate = async (_, {}, ctx) => {}
const deleteTemplate = async (_, {}, ctx) => {}
const deleteTemplate = async (_, { id }, ctx) => {
try {
const pubsub = await pubsubManager.getPubsub()
const toBeDeleted = await Template.query().patchAndFetchById(id, {
deleted: true,
})
logger.info(
`Template with id ${toBeDeleted.id} patched with deleted set to true`,
)
const files = await toBeDeleted.getFiles()
const templatePath = path.join(uploadsPath, 'templates', toBeDeleted.id)
logger.info(
`${
files.length
} associated files should be patched with deleted set to true`,
)
await Promise.all(
map(files, async file => {
try {
const deletedFile = await File.query().patchAndFetchById(file.id, {
deleted: true,
})
logger.info(
`File with id ${deletedFile.id} patched with deleted set to true`,
)
return deletedFile
} catch (e) {
throw new Error(e)
}
}),
)
await fs.remove(templatePath)
logger.info(`Files deleted from the server on patch ${templatePath}`)
pubsub.publish(TEMPLATE_DELETED, {
templateDeleted: toBeDeleted,
})
logger.info('Template deleted msg broadcasted')
return id
} catch (e) {
throw new Error(e)
}
}
module.exports = {
Query: {
......@@ -108,9 +241,30 @@ module.exports = {
},
Template: {
async files(template, _, ctx) {
const temp = await Template.findById(template.id)
return temp.getFiles()
return template.getFiles()
},
async thumbnail(template, _, ctx) {
return template.getThumbnail()
},
},
Subscription: {
templateCreated: {
subscribe: async () => {
const pubsub = await pubsubManager.getPubsub()
return pubsub.asyncIterator(TEMPLATE_CREATED)
},
},
templateDeleted: {
subscribe: async () => {
const pubsub = await pubsubManager.getPubsub()
return pubsub.asyncIterator(TEMPLATE_DELETED)
},
},
templateUpdated: {
subscribe: async () => {
const pubsub = await pubsubManager.getPubsub()
return pubsub.asyncIterator(TEMPLATE_UPDATED)
},
},
},
Subscription: {},
}
......@@ -33,10 +33,10 @@ const large = css`
`
const largeNarrow = css`
bottom: 40px;
bottom: 10%;
left: 20%;
right: 20%;
top: 40px;
top: 10%;
`
const medium = css`
......
......@@ -27,19 +27,12 @@ const Base = require('../editoriaBase')
const { model: BookCollection } = require('../bookCollection')
const { model: Division } = require('../division')
const {
booleanDefaultFalse,
date,
id,
string,
year,
} = require('../helpers').schema
const { booleanDefaultFalse, id, string, year } = require('../helpers').schema
class Book extends Base {
constructor(properties) {
super(properties)
this.type = 'book'
console.log('in book c')
}
static get tableName() {
......
......@@ -5,7 +5,6 @@ const Base = require('../editoriaBase')
const {
arrayOfStringsNotEmpty,
foreignType,
string,
id,
integerPositive,
mimetype,
......@@ -17,7 +16,6 @@ class File extends Base {
constructor(properties) {
super(properties)
this.type = 'file'
console.log('in model c', this)
}
static get tableName() {
......@@ -65,7 +63,7 @@ class File extends Base {
},
}
}
static get schema() {
return {
type: 'object',
......
......@@ -11,5 +11,6 @@ create table template (
reference_id uuid,
author text,
name text not null,
target text
target text,
trim_size text
);
\ No newline at end of file
const { Model } = require('objection')
const remove = require('lodash/remove')
const Base = require('../editoriaBase')
const {
arrayOfIds,
id,
stringNotEmpty,
string,
targetType,
} = require('../helpers').schema
const { id, stringNotEmpty, string, targetType } = require('../helpers').schema
class Template extends Base {
constructor(properties) {
......@@ -26,9 +20,10 @@ class Template extends Base {
properties: {
name: stringNotEmpty,
referenceId: id,
author: string,
thumbnailId: id,
author: string,
target: targetType,
trimSize: string,
},
}
}
......@@ -56,11 +51,19 @@ class Template extends Base {
}
}
getFiles() {
return this.$relatedQuery('files')
async getFiles() {
const { thumbnailId } = this
const associatedFiles = await this.$relatedQuery('files')
if (thumbnailId) {
remove(associatedFiles, file => file.id === thumbnailId)
}
remove(associatedFiles, file => file.deleted === true)
return associatedFiles
}
getThumbnail() {
return this.$relatedQuery('thumbnail')
async getThumbnail() {
const associatedThumbnails = await this.$relatedQuery('thumbnail')
remove(associatedThumbnails, file => file.deleted === true)
return associatedThumbnails[0]
}
}
......
......@@ -27,7 +27,8 @@
"react-modal": "^3.6.1",
"react-powerplug": "^1.0.0",
"react-router-dom": "^5.0.0",
"styled-components": "^4.1.3"
"styled-components": "^4.1.3",
"react-select":"^3.0.4"
},
"devDependencies": {
"enzyme": "^2.9.1",
......
......@@ -4,18 +4,30 @@ import { adopt } from 'react-adopt'
import withModal from 'editoria-common/src/withModal'
import Templates from './Templates'
import { getTemplatesQuery, createTemplateMutation } from './queries'
import {
getTemplatesQuery,
createTemplateMutation,
deleteTemplateMutation,
templateCreatedSubscription,
templateUpdatedSubscription,
templateDeletedSubscription,
} from './queries'
const mapper = {
withModal,
getTemplatesQuery,
createTemplateMutation,
deleteTemplateMutation,
templateCreatedSubscription,
templateUpdatedSubscription,
templateDeletedSubscription,
}
const mapProps = args => {
return {
templates: get(args.getTemplatesQuery, 'data.getTemplates'),
createTemplate: args.createTemplateMutation.createTemplate,
deleteTemplateMutation: args.deleteTemplateMutation.deleteTemplate,
showModal: args.withModal.showModal,
hideModal: args.withModal.hideModal,
loading: args.getTemplatesQuery.networkStatus === 1,
......@@ -24,7 +36,14 @@ const mapProps = args => {
const { createTemplateMutation, withModal } = args
const { createTemplate } = createTemplateMutation
const { showModal, hideModal } = withModal
const onConfirm = (files, thumbnail, name, author, target, trimSize) => {
const onConfirm = ({
files,
thumbnail,
name,
author,
target,
trimSize,
}) =>
createTemplate({
variables: {
input: {
......@@ -37,13 +56,29 @@ const mapProps = args => {
},
},
})
hideModal()
}
showModal('createTemplateModal', {
onConfirm,
hideModal,
})
},
onDeleteTemplate: (templateId, templateName) => {
const { deleteTemplateMutation, withModal } = args
const { deleteTemplate } = deleteTemplateMutation
const { showModal, hideModal } = withModal
const onConfirm = () => {
deleteTemplate({
variables: {
id: templateId,
},
})
hideModal()
}
showModal('deleteTemplateModal', {
onConfirm,
templateName,
})
},
refetching:
args.getTemplatesQuery.networkStatus === 4 ||
args.getTemplatesQuery.networkStatus === 2, // possible apollo bug
......@@ -54,13 +89,23 @@ const Composed = adopt(mapper, mapProps)
const Connected = () => (
<Composed>
{({ templates, onCreateTemplate, onChangeSort, refetching, loading }) => {
{({
templates,
onCreateTemplate,
onDeleteTemplate,
onChangeSort,
refetching,
loading,
createTemplate,
}) => {
return (
<Templates
templates={templates}
onCreateTemplate={onCreateTemplate}
onDeleteTemplate={onDeleteTemplate}
onChangeSort={onChangeSort}
refetching={refetching}
createTemplate={createTemplate}
loading={loading}
/>
)
......
......@@ -22,7 +22,9 @@ export class Template extends Component {
const {
templates,
onCreateTemplate,
onDeleteTemplate,
onChangeSort,
createTemplate,
refetching,
loading,
} = this.props
......@@ -39,7 +41,7 @@ export class Template extends Component {
title="Templates"
/>
<InnerWrapper>
<TemplatesGrid templates={templates} />
<TemplatesGrid templates={templates} onDeleteTemplate={onDeleteTemplate} />
{/* <TemplateList
templates={templates}
// bookRules={rules.bookRules}
......
import React from 'react'
import { Mutation } from 'react-apollo'
import gql from 'graphql-tag'
const DELETE_TEMPLATE = gql`
mutation DeleteTemplate($id: ID!) {
deleteTemplate(id: $id)
}
`
const deleteTemplateMutation = props => {
const { render } = props
return (
<Mutation mutation={DELETE_TEMPLATE}>
{(deleteTemplate, deleteTemplateResult) =>
render({ deleteTemplate, deleteTemplateResult })
}
</Mutation>
)
}
export default deleteTemplateMutation
......@@ -5,11 +5,11 @@ import gql from 'graphql-tag'
const GET_TEMPLATES = gql`
query GetTemplates(
$ascending: Boolean = true
$sortKey: String = "templateName"
$sortKey: String = "name"
) {
getTemplates(ascending: $ascending, sortKey: $sortKey) {
id
templateName
name
}
}
`
......
export { default as createTemplateMutation } from './createTemplate'
export { default as deleteTemplateMutation } from './deleteTemplate'
export { default as getTemplatesQuery } from './getTemplates'
export {
templateCreatedSubscription,
templateUpdatedSubscription,
templateDeletedSubscription,
} from './templateSubscriptions'
import React from 'react'
import { Subscription } from 'react-apollo'
import gql from 'graphql-tag'
const TEMPLATE_CREATED_SUBSCRIPTION = gql`
subscription TemplateCreated {
templateCreated {
id
}
}
`
const TEMPLATE_UPDATED_SUBSCRIPTION = gql`
subscription TemplateUpdated {
templateUpdated {
id
}
}
`
const TEMPLATE_DELETED_SUBSCRIPTION = gql`
subscription TemplateDeleted {
templateDeleted {
id
}
}
`
const templateCreatedSubscription = props => {
const { render, getTemplatesQuery } = props
const { refetch } = getTemplatesQuery
const triggerRefetch = () => {
refetch()
}
return (
<Subscription
onSubscriptionData={triggerRefetch}
subscription={TEMPLATE_CREATED_SUBSCRIPTION}
>
{render}
</Subscription>
)
}
const templateUpdatedSubscription = props => {
const { render, getTemplatesQuery } = props
const { refetch } = getTemplatesQuery
const triggerRefetch = () => {
refetch()
}
return (
<Subscription
onSubscriptionData={triggerRefetch}
subscription={TEMPLATE_UPDATED_SUBSCRIPTION}
>
{render}
</Subscription>
)
}
const templateDeletedSubscription = props => {
const { render, getTemplatesQuery } = props
const { refetch } = getTemplatesQuery
const triggerRefetch = () => {
refetch()
}