Commit 50ac3778 authored by Alexandros Georgantas's avatar Alexandros Georgantas

refactor(template manager): wip structuring the template manager feature

parent cd51950e
......@@ -6,6 +6,7 @@ const customTag = require('./customTag')
const division = require('./division')
const team = require('./team')
const user = require('./user')
const template = require('./template')
// const bookCollectionTranslation = require('./bookCollectionTranslation')
// const bookComponentState = require('./bookComponentState')
// const bookComponentTranslation = require('./bookComponentTranslation')
......@@ -24,6 +25,7 @@ module.exports = {
file.typeDefs,
team.typeDefs,
user.typeDefs,
template.typeDefs,
].join(' '),
resolvers: merge(
{},
......@@ -35,6 +37,7 @@ module.exports = {
division.resolvers,
file.resolvers,
team.resolvers,
template.resolvers,
user.resolvers,
),
// context: {
......
const TEMPLATE_CREATED = 'TEMPLATE_CREATED'
const TEMPLATE_UPDATED = 'TEMPLATE_UPDATED'
const TEMPLATE_DELETED = 'TEMPLATE_DELETED'
module.exports = { TEMPLATE_CREATED, TEMPLATE_UPDATED, TEMPLATE_DELETED }
......@@ -3,32 +3,35 @@ type Template {
templateName: String!
thumbnailSrc: String
author: String
target: String
files: [File]!
}
input CreateTemplateInput{
input CreateTemplateInput {
templateName: String!
thumbnailSrc: String
files: [Upload]
# thumbnailSrc: String
target: String
author: String
files: [File]!
# filePaths: [String]!
}
input UpdateTemplateInput{
input UpdateTemplateInput {
id: ID!
templateName: String
thumbnailSrc: String
target: String
author: String
files: [File]
filePaths: [String]
}
extend type Query {
getTemplates(ascending: Boolean, sortKey: String): [Template]!
getTemplate(id:ID!): Template!
getTemplate(id: ID!): Template!
}
extend type Mutation {
createTemplate(input: CreateTemplateInput): Template!
updateTemplate(input: UpdateTemplateInput): Template!
deleteTemplate(id:ID!): ID!
}
\ No newline at end of file
deleteTemplate(id: ID!): ID!
}
const indexOf = require('lodash/indexOf')
const orderBy = require('lodash/orderBy')
const map = require('lodash/map')
const find = require('lodash/find')
const utils = require('../helpers/utils')
const path = require('path')
const fs = require('fs-extra')
const config = require('config')
const uploadsPath = config.get('pubsweet-server').uploads
const {
Template,
File,
......@@ -11,10 +15,88 @@ const {
// const pubsweetServer = require('pubsweet-server')
// const { pubsubManager } = pubsweetServer
const getTemplates = async (_, {}, ctx) => {}
const getTemplate = async (_, {}, ctx) => {}
const getTemplates = async (_, { ascending, sortKey }, ctx) => {
const templates = await Template.query().where('deleted', false)
const order = ascending ? 'asc' : 'desc'
const sorted = orderBy(templates, sortKey, [order])
const result = map(sorted, item => find(templates, { id: item.id }))
return result
}
const getTemplate = async (_, { id }, ctx) => Template.query().findById(id)
const createTemplate = async (_, { input }, ctx) => {
const { templateName, author, files, target } = input
const fileIds = []
let thumbnailId
const allowedFonts = ['.otf', '.woff', '.woff2']
const allowedFiles = ['.css', '.otf', '.woff', '.woff2']
const regex = new RegExp(
'([a-zA-Z0-9s_\\.-:])+(' + allowedFiles.join('|') + ')$',
)
try {
const newTemplate = await new Template({
templateName,
author,
target,
}).save()
await Promise.all(
map(files, async file => {
const { createReadStream, filename, mimetype, encoding } = await file
if (!regex.test(filename))
throw new Error('File extension is not allowed')
const outPath = path.join(
uploadsPath,
'templates',
newTemplate.id,
filename,
)
await fs.ensureDir(uploadsPath)
await fs.ensureDir(`${uploadsPath}/templates`)
await fs.ensureDir(`${uploadsPath}/templates/${newTemplate.id}`)
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 {
console.log('filename', typeof filename)
const newFile = await new File({
filename: 'hahahaha',
mimetype,
src: outPath,
templateId: newTemplate.id,
}).save()
fileIds.push(newFile.id)
resolve()
} catch (e) {
throw new Error(e)
}
})
stream.on('error', reject)
})
}),
)
return Template.query().patchAndFetchById(newTemplate.id, {
files: fileIds,
})
} catch (e) {
throw new Error(e)
}
}
const createTemplate = async (_, {}, ctx) => {}
const updateTemplate = async (_, {}, ctx) => {}
const deleteTemplate = async (_, {}, ctx) => {}
......@@ -29,8 +111,9 @@ module.exports = {
deleteTemplate,
},
Template: {
async files(divisionId, _, ctx) {
async files(template, _, ctx) {
// TODO:
console.log('template', template)
},
},
Subscription: {},
......
......@@ -121,6 +121,7 @@ class Book extends Base {
async addDivision(label) {
return new Division({
bookId: this.id,
bookComponents: [],
label,
}).save()
}
......
......@@ -13,7 +13,7 @@ create table book (
collection_id uuid not null references book_collection,
/*
to do
we cannot enforce the integrity of division id's, as an array of foreign
we ceannot enforc the integrity of division id's, as an array of foreign
keys is not yet supported in postgres. there seems to be some work on this,
so we should update when the feature is in postgres.
*/
......
......@@ -41,7 +41,7 @@ class Division extends Base {
static get relationMappings() {
const { model: Book } = require('../book')
const { model: BookComponent } = require('../bookComponent')
// const { model: BookComponent } = require('../bookComponent')
return {
book: {
......@@ -52,14 +52,14 @@ class Division extends Base {
to: 'Book.id',
},
},
bookComponents: {
relation: Model.HasManyRelation,
modelClass: BookComponent,
join: {
from: 'BookComponent.divisionId',
to: 'Division.id',
},
},
// bookComponents: {
// relation: Model.HasManyRelation,
// modelClass: BookComponent,
// join: {
// from: 'Division.id',
// to: 'BookComponent.divisionId',
// },
// },
}
}
......
......@@ -10,7 +10,7 @@ create table file (
--foreign
book_id uuid references book,
book_component_id uuid references bookComponent
book_component_id uuid references book_component,
template_id uuid references template,
reference_id uuid not null,
size int,
......
......@@ -10,8 +10,7 @@ create table template (
reference_id uuid,
author text,
thumbnail_id uuid references file,
templateName text not null
target string,
files text[],
template_name text not null,
target text,
files text[]
);
\ No newline at end of file
exports.up = async knex =>
knex.schema.table('template', table => {
table.uuid('thumbnailId').references('file')
})
const { Model } = require('./node_modules/objection')
const { Model } = require('objection')
const Base = require('../editoriaBase')
const { arrayOfIds, id, stringNotEmpty, string, targetType } = require('../helpers').schema
const {
arrayOfIds,
id,
stringNotEmpty,
string,
targetType,
} = require('../helpers').schema
class Template extends Base {
constructor(properties) {
......
......@@ -52,7 +52,10 @@ class Navigation extends React.Component {
render() {
const { logoutUser, currentUser, client } = this.props
const links = [<Action to="/books">Books</Action>]
const links = [
<Action to="/books">Books</Action>,
<Action to="/templates">Templates</Action>,
]
if (currentUser === null) return null
......
{
"name": "pubsweet-component-editoria-templates",
"version": "0.1.0",
"description": "The application dashboard for the Editoria project.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"pubsweet-component",
"pubsweet-client"
],
"repository": {
"type": "git",
"url": "https://gitlab.coko.foundation/editoria/editoria-templates"
},
"author": "Alexandros Georgantas",
"license": "ISC",
"dependencies": {
"@pubsweet/ui": "^10.3.0",
"editoria-common": "^0.1.6",
"formik": "^1.5.1",
"lodash": "4.17.4",
"pubsweet-client": "^9.2.3",
"react": "^16.2.0",
"react-adopt": "^0.6.0",
"react-modal": "^3.6.1",
"react-powerplug": "^1.0.0",
"react-router-dom": "^5.0.0",
"styled-components": "^4.1.3"
},
"devDependencies": {
"enzyme": "^2.9.1",
"enzyme-to-json": "^1.5.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^20.0.4",
"prop-types": "^15.5.10",
"react-addons-test-utils": "^15.6.0",
"react-test-renderer": "^15.6.1",
"sinon": "^2.3.8",
"sinon-as-promised": "^4.0.3"
}
}
import React from 'react'
import { get } from 'lodash'
import { adopt } from 'react-adopt'
import withModal from 'editoria-common/src/withModal'
import Templates from './Templates'
import { getTemplatesQuery, createTemplateMutation } from './queries'
const mapper = {
withModal,
getTemplatesQuery,
createTemplateMutation,
}
const mapProps = args => {
return {
templates: get(args.getTemplatesQuery, 'data.getTemplates'),
createTemplate: args.createTemplateMutation.createTemplate,
showModal: args.withModal.showModal,
hideModal: args.withModal.hideModal,
loading: args.getTemplatesQuery.networkStatus === 1,
onChangeSort: args.getTemplatesQuery.refetch,
refetching:
args.getTemplatesQuery.networkStatus === 4 ||
args.getTemplatesQuery.networkStatus === 2, // possible apollo bug
}
}
const Composed = adopt(mapper, mapProps)
const Connected = () => (
<Composed>
{({ templates, createTemplate, onChangeSort, refetching, loading }) => {
return (
<Templates
templates={templates}
createTemplate={createTemplate}
onChangeSort={onChangeSort}
refetching={refetching}
loading={loading}
/>
)
}}
</Composed>
)
export default Connected
import React, { Component } from 'react'
import styled from 'styled-components'
import { UploadFilesButton } from './ui'
const Container = styled.div`
display: block;
clear: both;
float: none;
margin: 0 auto;
max-width: 100%;
`
const InnerWrapper = styled.div`
display: block;
clear: both;
float: none;
margin: 0 auto;
max-width: 76%;
`
export class Template extends Component {
render() {
const {
templates,
createTemplate,
onChangeSort,
refetching,
loading,
} = this.props
if (loading) return 'Loading...'
return (
<Container>
<h1>Hello templates</h1>
<UploadFilesButton createTemplate={createTemplate} />
</Container>
)
}
}
export default Template
import React from 'react'
import { Mutation } from 'react-apollo'
import gql from 'graphql-tag'
const CREATE_TEMPLATE = gql`
mutation CreateTemplate($input: CreateTemplateInput!) {
createTemplate(input: $input) {
id
}
}
`
const createTemplateMutation = props => {
const { render } = props
return (
<Mutation mutation={CREATE_TEMPLATE}>
{(createTemplate, createTemplateResult) =>
render({ createTemplate, createTemplateResult })
}
</Mutation>
)
}
export default createTemplateMutation
import React from 'react'
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
const GET_TEMPLATES = gql`
query GetTemplates(
$ascending: Boolean = true
$sortKey: String = "templateName"
) {
getTemplates(ascending: $ascending, sortKey: $sortKey) {
id
templateName
}
}
`
const getTemplatesQuery = props => {
const { render } = props
return (
<Query
fetchPolicy="cache-and-network"
notifyOnNetworkStatusChange
query={GET_TEMPLATES}
>
{render}
</Query>
)
}
export { GET_TEMPLATES }
export default getTemplatesQuery
export { default as createTemplateMutation } from './createTemplate'
export { default as getTemplatesQuery } from './getTemplates'
export { default as UploadButton } from './src/UploadButton'
export { default as UploadFilesButton } from './src/UploadFilesButton'
export { ButtonWithIcon, DefaultButton, ButtonWithoutLabel } from './src/Button'
import React from 'react'
import styled from 'styled-components'
import { th } from '@pubsweet/ui-toolkit'
const Button = styled.button`
align-items: center;
background: none;
border: none;
display: flex;
color: #828282;
padding: 0;
font-family: 'Fira Sans Condensed' !important;
/* padding: calc(${th('gridUnit')} / 2); */
svg {
svg {
path{
fill: #828282;
}
}
width:28px;
height:28px;
}
&:disabled {
color: ${th('colorFurniture')};
svg {
path{
fill: ${th('colorFurniture')};
}
}
cursor: not-allowed !important;
font-size: ${th('fontSizeBase')} !important;
line-height: ${th('lineHeightBase')} !important;
font-style: normal !important;
font-weight: 200 !important;
}
&:not(:disabled):hover {
/* background-color: ${th('colorBackgroundHue')}; */
color: ${th('colorPrimary')};
svg {
path{
fill: ${th('colorPrimary')};
}
}
}
&:not(:disabled):active {
/* background-color: ${th('colorFurniture')}; */
border: none;
color: ${th('colorPrimary')};
outline: none;
svg {
path{
fill: ${th('colorPrimary')};
}
}
}
&:focus {
outline: 0;
}
`
const Icon = styled.span`
height: calc(3.5 * ${th('gridUnit')});
/* margin: 0 ${th('gridUnit')} 0 0; */
padding: 0;
width: calc(3.5 * ${th('gridUnit')});
`
const OnlyIcon = styled.span`
height: calc(3.5 * ${th('gridUnit')});
padding: 0;
width: calc(3.5 * ${th('gridUnit')});
`
const Label = styled.div`
font-size: ${th('fontSizeBase')};
line-height: ${th('lineHeightBase')};
padding-right: 4px;
`
const ButtonWithIcon = ({
onClick,
icon,
label,
disabled,
title,
className,
}) => {
return (
<Button
title={title}
className={className}
onClick={onClick}
disabled={disabled}
>
<Icon>{icon}</Icon>
<Label>{label.toUpperCase()}</Label>
</Button>
)
}
const DefaultButton = ({ onClick, label, disabled, className, title }) => {
return (
<Button
title={title}
className={className}
onClick={onClick}
disabled={disabled}
>
<Label>{label.toUpperCase()}</Label>
</Button>
)
}
const ButtonWithoutLabel = ({ onClick, icon, disabled, className, title }) => {
return (
<Button
title={title}
className={className}
onClick={onClick}
disabled={disabled}
>
<OnlyIcon>{icon}</OnlyIcon>
</Button>
)
}
export { ButtonWithIcon, DefaultButton, ButtonWithoutLabel }
import React from 'react'
import styled from 'styled-components'
import { ButtonWithIcon } from './Button'
import { th } from '@pubsweet/ui-toolkit'
const Input = styled.input`
display: none !important;
`
const UploadButton = ({
onChange,
multiple,
accept,
label,
disabled,
id,
className,
}) => {
const onClick = event => {
event.preventDefault()
document.getElementById(`file-uploader-${id}`).click()
}
const icon = (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Common/Icon/Upload">
<rect width="28" height="28" fill="white" />
<g id="Common/Icon-background">
<rect width="28" height="28" fill="white" />
</g>
<g id="Vector">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.5658 13.9109C14.2578 13.5892 13.7594 13.5867 13.4442 13.9001L11.0442 16.3142C10.7266 16.6342 10.7178 17.1609 11.025 17.4925C11.3322 17.8234 11.8386 17.8342 12.1562 17.5134L13.2002 16.4634V21.1667C13.2002 21.6275 13.5586 22 14.0002 22C14.4418 22 14.8002 21.6275 14.8002 21.1667V16.5117L15.8346 17.5892C15.9906 17.7517 16.1954 17.8334 16.4002 17.8334C16.605 17.8334 16.8098 17.7517 16.9658 17.5892C17.2786 17.2634 17.2786 16.7367 16.9658 16.4109L14.5658 13.9109Z"
fill="#828282"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.54 10.3708C17.884 8.38416 16.0648 7 14 7C11.936 7 10.1168 8.38416 9.46 10.3708C7.5088 10.6466 6 12.3933 6 14.4999C6 15.5174 6.3552 16.4966 7.0008 17.2574C7.2928 17.6016 7.7984 17.6358 8.1296 17.3308C8.4608 17.0249 8.492 16.4991 8.2 16.1533C7.8128 15.6983 7.6 15.1099 7.6 14.4999C7.6 13.1216 8.6768 12 10 12H10.08C10.4608 12 10.7896 11.72 10.8648 11.3308C11.1632 9.78748 12.4824 8.66665 14 8.66665C15.5184 8.66665 16.8368 9.78748 17.136 11.3308C17.2112 11.72 17.5392 12 17.92 12H18C19.3232 12 20.4 13.1216 20.4 14.4999C20.4 15.1099 20.1872 15.6983 19.8008 16.1533C19.508 16.4991 19.54 17.0249 19.8704 17.3308C20.0232 17.4708 20.212 17.5391 20.4 17.5391C20.6216 17.5391 20.8416 17.4433 21 17.2574C21.6448 16.4966 22 15.5174 22 14.4999C22 12.3933 20.4912 10.6466 18.54 10.3708Z"
fill="#828282"
/>
</g>
</g>
</svg>
)
return (
<React.Fragment>
<ButtonWithIcon
className={className}
icon={icon}
label={label}
onClick={onClick}
disabled={disabled}
/>
<Input
accept={accept}
id={`file-uploader-${id}`}
multiple={multiple}
type="file"
onChange={onChange}
/>
</React.Fragment>
)