From 2b30c5b5780869bc20559690220d9673d26b51ca Mon Sep 17 00:00:00 2001 From: Daneryl Date: Thu, 21 Jun 2018 11:05:43 +0200 Subject: [PATCH 01/20] fixed lints --- app/api/search/specs/analyzers.spec.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/api/search/specs/analyzers.spec.js b/app/api/search/specs/analyzers.spec.js index b72bc5c2c7..a34c56eddf 100644 --- a/app/api/search/specs/analyzers.spec.js +++ b/app/api/search/specs/analyzers.spec.js @@ -1,7 +1,8 @@ -import elastic from '../elastic'; +import { catchErrors } from 'api/utils/jasmineHelpers'; +import { index as elasticIndex } from 'api/config/elasticIndexes'; import instanceElasticTesting from 'api/utils/elastic_testing'; -import {index as elasticIndex} from 'api/config/elasticIndexes'; -import {catchErrors} from 'api/utils/jasmineHelpers'; + +import elastic from '../elastic'; describe('custom language analyzers', () => { const elasticTesting = instanceElasticTesting('analyzers_index_test'); @@ -12,13 +13,13 @@ describe('custom language analyzers', () => { describe('persian', () => { it('should index persian without errors', (done) => { - let persianText = `براي مشارکت فعال در کالس آماده باشيد. برای رسیدن به این هدف، بايد پیش از کالس به فايلهاي صوتي گوش کنيد، + const persianText = `براي مشارکت فعال در کالس آماده باشيد. برای رسیدن به این هدف، بايد پیش از کالس به فايلهاي صوتي گوش کنيد، ويديوها را ببينيد و تمرينهاي آنها را انجام دهيد. به صداهاي جديد گوش کنيد و حروف جديد را بنويسيد. هر چهقدر الزم است اين کار را تکرار کنید، تا وقتي که شناختن و توليد اين صداها و حروف برايتان راحت و آسان شود. تمرينهاي در خانه را پيش از کالس انجام دهيد تا براي خواندن و نوشتن در کالس آماده باشيد. وقت کالس بايد صرف تمرين شود، نه شنيدن سخنراني استادتان.`; - elastic.index({index: elasticIndex, type: 'fullText', body: {fullText_persian: persianText}, id: '123_whatever', parent: 123}) + elastic.index({ index: elasticIndex, type: 'fullText', body: { fullText_persian: persianText }, id: '123_whatever', parent: 123 }) .then(() => done()) .catch(done.fail); }); From 9de90f09425b23fec28a281c8bea148be3a25c90 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Fri, 22 Jun 2018 09:32:55 +0200 Subject: [PATCH 02/20] new custom uploads on settings, API --- app/api/upload/routes.js | 8 +++ app/api/upload/specs/uploads.spec.js | 45 ++++++++++++++++ app/api/upload/uploads.js | 5 ++ app/api/upload/uploadsModel.js | 17 +++++++ app/react/Routes.js | 4 +- .../Settings/components/CustomUploads.js | 51 +++++++++++++++++++ app/react/Settings/index.js | 2 + app/react/Uploads/actions/uploadsActions.js | 15 ++++++ 8 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 app/api/upload/specs/uploads.spec.js create mode 100644 app/api/upload/uploads.js create mode 100644 app/api/upload/uploadsModel.js create mode 100644 app/react/Settings/components/CustomUploads.js diff --git a/app/api/upload/routes.js b/app/api/upload/routes.js index 20ad4d127f..8a7968b1a4 100644 --- a/app/api/upload/routes.js +++ b/app/api/upload/routes.js @@ -7,6 +7,7 @@ import relationships from 'api/relationships'; import PDF from './PDF'; import needsAuthorization from '../auth/authMiddleware'; import { uploadDocumentsPath } from '../config/paths'; +import uploads from './uploads'; import fs from 'fs'; import logger from 'shared/logger'; @@ -89,6 +90,13 @@ export default (app) => { app.post('/api/upload', needsAuthorization(['admin', 'editor']), upload.any(), (req, res) => uploadProcess(req, res)); + app.post('/api/customisation/upload', needsAuthorization(['admin', 'editor']), upload.any(), (req, res) => { + uploads.save(req.files[0]) + .then((saved) => { + res.json(saved); + }); + }); + app.post('/api/reupload', needsAuthorization(['admin', 'editor']), upload.any(), (req, res) => entities.getById(req.body.document) .then(doc => Promise.all([doc, relationships.deleteTextReferences(doc.sharedId, doc.language)])) .then(([doc]) => entities.saveMultiple([{ _id: doc._id, toc: [] }])) diff --git a/app/api/upload/specs/uploads.spec.js b/app/api/upload/specs/uploads.spec.js new file mode 100644 index 0000000000..0b132e7194 --- /dev/null +++ b/app/api/upload/specs/uploads.spec.js @@ -0,0 +1,45 @@ +/* eslint-disable max-nested-callbacks */ +import { catchErrors } from 'api/utils/jasmineHelpers'; +import db from 'api/utils/testing_db'; +import uploads from '../uploads'; +import uploadsModel from '../uploadsModel'; + +describe('uploads', () => { + let file; + + beforeEach((done) => { + file = { + fieldname: 'file', + originalname: 'gadgets-01.pdf', + encoding: '7bit', + mimetype: 'application/octet-stream', + destination: `${__dirname}/uploads/`, + filename: 'f2082bf51b6ef839690485d7153e847a.pdf', + path: `${__dirname}/uploads/f2082bf51b6ef839690485d7153e847a.pdf`, + size: 171411271 + }; + + db.clearAllAndLoad({ uploads: [{}] }).then(done).catch(catchErrors(done)); + }); + + afterAll((done) => { + db.disconnect().then(done); + }); + + describe('save', () => { + it('should save file passed', async () => { + let saved = await uploads.save(file); + saved = await uploadsModel.getById(saved._id); + + expect(saved.creationDate).toBeDefined(); + + expect(saved).toMatchObject({ + originalname: 'gadgets-01.pdf', + mimetype: 'application/octet-stream', + filename: 'f2082bf51b6ef839690485d7153e847a.pdf', + size: 171411271 + }); + }); + }); +}); + diff --git a/app/api/upload/uploads.js b/app/api/upload/uploads.js new file mode 100644 index 0000000000..ef4fca43e5 --- /dev/null +++ b/app/api/upload/uploads.js @@ -0,0 +1,5 @@ +import model from './uploadsModel'; + +export default { + save: model.save.bind(model) +}; diff --git a/app/api/upload/uploadsModel.js b/app/api/upload/uploadsModel.js new file mode 100644 index 0000000000..0859b959da --- /dev/null +++ b/app/api/upload/uploadsModel.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; +import date from 'api/utils/date.js'; + +import instanceModel from 'api/odm'; + +const uploadSchema = new mongoose.Schema({ + originalname: String, + filename: String, + mimetype: String, + size: Number, + creationDate: { type: Number, default: date.currentUTC }, +}, { emitIndexErrors: true }); + +const schema = mongoose.model('uploads', uploadSchema); +const Model = instanceModel(schema); + +export default Model; diff --git a/app/react/Routes.js b/app/react/Routes.js index 55d67bc6a2..93a3bd38d8 100644 --- a/app/react/Routes.js +++ b/app/react/Routes.js @@ -18,7 +18,8 @@ import { ThesaurisList, TranslationsList, FiltersForm, - Customisation + Customisation, + CustomUploads } from 'app/Settings'; import Pages from 'app/Pages/Pages'; @@ -109,6 +110,7 @@ const routes = ( + diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js new file mode 100644 index 0000000000..177819badb --- /dev/null +++ b/app/react/Settings/components/CustomUploads.js @@ -0,0 +1,51 @@ +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import Dropzone from 'react-dropzone'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +import { t } from 'app/I18N'; + +import { upload } from '../../Uploads/actions/uploadsActions'; + +export class CustomUploads extends Component { + onDrop(files) { + files.forEach((file) => { + this.props.upload(file); + }); + } + render() { + return ( +
+
{t('System', 'Custom Uploads')}
+
+ +
+ + Browse your PDFs to upload + or drop your files here. +
+
+ + ProTip! + For better performance, upload your documents in batches of 50 or less. +
+
+
+
+ ); + } +} + +CustomUploads.propTypes = { + upload: PropTypes.func.isRequired +}; + +const mapStateToProps = () => ({}); +const mapDispatchToProps = dispatch => bindActionCreators({ upload }, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(CustomUploads); diff --git a/app/react/Settings/index.js b/app/react/Settings/index.js index f00fb8d2eb..b396e874d6 100644 --- a/app/react/Settings/index.js +++ b/app/react/Settings/index.js @@ -12,6 +12,8 @@ import TranslationsList from './components/TranslationsList'; import FiltersForm from './components/FiltersForm'; import Customisation from './components/Customisation'; +export { default as CustomUploads } from './components/CustomUploads'; + export { Settings, SettingsAPI, diff --git a/app/react/Uploads/actions/uploadsActions.js b/app/react/Uploads/actions/uploadsActions.js index 071cbc7cdb..3bc227700e 100644 --- a/app/react/Uploads/actions/uploadsActions.js +++ b/app/react/Uploads/actions/uploadsActions.js @@ -50,6 +50,21 @@ export function uploadDocument(docId, file) { }; } +export function upload(docId, file) { + return function (dispatch) { + superagent.post(`${APIURL}customisation/upload`) + .set('Accept', 'application/json') + .attach('file', file, file.name) + .on('progress', (data) => { + dispatch({ type: types.UPLOAD_PROGRESS, doc: docId, progress: Math.floor(data.percent) }); + }) + .on('response', () => { + dispatch({ type: types.UPLOAD_COMPLETE, doc: docId }); + }) + .end(); + }; +} + export function documentProcessed(sharedId) { return { type: types.DOCUMENT_PROCESSED, sharedId }; } From c70710cc61b2e77e7041a93f83107e558910b34c Mon Sep 17 00:00:00 2001 From: Daneryl Date: Fri, 22 Jun 2018 11:28:31 +0200 Subject: [PATCH 03/20] testing upload progress component --- .babelrc | 1 + .../Settings/components/CustomUploads.js | 13 +++- .../actions/specs/uploadsActions.spec.js | 59 +++++++-------- app/react/Uploads/actions/uploadsActions.js | 72 +++++++------------ package.json | 1 + yarn.lock | 9 ++- 6 files changed, 78 insertions(+), 77 deletions(-) diff --git a/.babelrc b/.babelrc index ddb6926857..c7c4690971 100644 --- a/.babelrc +++ b/.babelrc @@ -11,6 +11,7 @@ } }, "plugins": [ + "transform-object-rest-spread", "transform-es2015-typeof-symbol", "transform-class-properties", "add-module-exports", diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index 177819badb..b86a63a3be 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -8,10 +8,20 @@ import { t } from 'app/I18N'; import { upload } from '../../Uploads/actions/uploadsActions'; +const UploadProgressCompoent = ({ progress }) => { + return {progress} %; +}; + +const UploadProgress = connect(({ progress }, props) => { + console.log(progress.toJS()); + + return {progress: 50}; +})(UploadProgressCompoent); + export class CustomUploads extends Component { onDrop(files) { files.forEach((file) => { - this.props.upload(file); + this.props.upload(file.preview, file); }); } render() { @@ -35,6 +45,7 @@ export class CustomUploads extends Component { For better performance, upload your documents in batches of 50 or less. + ); diff --git a/app/react/Uploads/actions/specs/uploadsActions.spec.js b/app/react/Uploads/actions/specs/uploadsActions.spec.js index 20337f4764..4f70634661 100644 --- a/app/react/Uploads/actions/specs/uploadsActions.spec.js +++ b/app/react/Uploads/actions/specs/uploadsActions.spec.js @@ -1,11 +1,12 @@ -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import backend from 'fetch-mock'; +import { File } from 'babel-core'; import superagent from 'superagent'; +import thunk from 'redux-thunk'; -import {APIURL} from 'app/config.js'; -import {mockID} from 'shared/uniqueID.js'; +import { APIURL } from 'app/config.js'; +import { mockID } from 'shared/uniqueID.js'; import * as actions from 'app/Uploads/actions/uploadsActions'; +import backend from 'fetch-mock'; +import configureMockStore from 'redux-mock-store'; import * as notificationsTypes from 'app/Notifications/actions/actionTypes'; import * as types from 'app/Uploads/actions/actionTypes'; @@ -17,23 +18,23 @@ describe('uploadsActions', () => { mockID(); backend.restore(); backend - .post(APIURL + 'documents', {body: JSON.stringify({testBackendResult: 'ok'})}) - .delete(APIURL + 'documents?name=doc&_id=abc1', {body: JSON.stringify({testBackendResult: 'ok'})}); + .post(`${APIURL}documents`, { body: JSON.stringify({ testBackendResult: 'ok' }) }) + .delete(`${APIURL}documents?name=doc&_id=abc1`, { body: JSON.stringify({ testBackendResult: 'ok' }) }); }); afterEach(() => backend.restore()); describe('enterUploads()', () => { it('should return a ENTER_UPLOADS_SECTION', () => { - let action = actions.enterUploads(); - expect(action).toEqual({type: types.ENTER_UPLOADS_SECTION}); + const action = actions.enterUploads(); + expect(action).toEqual({ type: types.ENTER_UPLOADS_SECTION }); }); }); describe('conversionComplete()', () => { it('should return a CONVERSION_COMPLETE with the document id', () => { - let action = actions.conversionComplete('document_id'); - expect(action).toEqual({type: types.CONVERSION_COMPLETE, doc: 'document_id'}); + const action = actions.conversionComplete('document_id'); + expect(action).toEqual({ type: types.CONVERSION_COMPLETE, doc: 'document_id' }); }); }); @@ -42,20 +43,20 @@ describe('uploadsActions', () => { it('should create a document', (done) => { backend.restore(); backend - .post(APIURL + 'documents', {body: JSON.stringify({_id: 'test', sharedId: 'sharedId'})}); + .post(`${APIURL}documents`, { body: JSON.stringify({ _id: 'test', sharedId: 'sharedId' }) }); - let newDoc = {name: 'doc'}; + const newDoc = { name: 'doc' }; const store = mockStore({}); const expectedActions = [ - {type: types.NEW_UPLOAD_DOCUMENT, doc: 'sharedId'}, - {type: types.ELEMENT_CREATED, doc: {_id: 'test', sharedId: 'sharedId'}} + { type: types.NEW_UPLOAD_DOCUMENT, doc: 'sharedId' }, + { type: types.ELEMENT_CREATED, doc: { _id: 'test', sharedId: 'sharedId' } } ]; store.dispatch(actions.createDocument(newDoc)) .then((createdDoc) => { - expect(createdDoc).toEqual({_id: 'test', sharedId: 'sharedId'}); - expect(backend.lastOptions().body).toEqual(JSON.stringify({name: 'doc'})); + expect(createdDoc).toEqual({ _id: 'test', sharedId: 'sharedId' }); + expect(backend.lastOptions().body).toEqual(JSON.stringify({ name: 'doc' })); expect(store.getActions()).toEqual(expectedActions); done(); }) @@ -65,21 +66,21 @@ describe('uploadsActions', () => { describe('uploadDocument', () => { it('should create a document and upload file while dispatching the upload progress', () => { - let mockUpload = superagent.post(APIURL + 'upload'); + const mockUpload = superagent.post(`${APIURL}upload`); spyOn(mockUpload, 'field').and.returnValue(mockUpload); spyOn(mockUpload, 'attach').and.returnValue(mockUpload); spyOn(superagent, 'post').and.returnValue(mockUpload); const expectedActions = [ - {type: types.UPLOAD_PROGRESS, doc: 'abc1', progress: 55}, - {type: types.UPLOAD_PROGRESS, doc: 'abc1', progress: 65}, - {type: types.UPLOAD_COMPLETE, doc: 'abc1'} + { type: types.UPLOAD_PROGRESS, doc: 'abc1', progress: 55 }, + { type: types.UPLOAD_PROGRESS, doc: 'abc1', progress: 65 }, + { type: types.UPLOAD_COMPLETE, doc: 'abc1' } ]; const store = mockStore({}); // needed to work with firefox/chrome and phantomjs - let file = {name: 'filename'}; - let isChrome = typeof File === 'function'; + let file = { name: 'filename' }; + const isChrome = typeof File === 'function'; if (isChrome) { file = new File([], 'filename'); } @@ -89,8 +90,8 @@ describe('uploadsActions', () => { expect(mockUpload.field).toHaveBeenCalledWith('document', 'abc1'); expect(mockUpload.attach).toHaveBeenCalledWith('file', file, file.name); - mockUpload.emit('progress', {percent: 55.1}); - mockUpload.emit('progress', {percent: 65}); + mockUpload.emit('progress', { percent: 55.1 }); + mockUpload.emit('progress', { percent: 65 }); mockUpload.emit('response'); expect(store.getActions()).toEqual(expectedActions); }); @@ -98,17 +99,17 @@ describe('uploadsActions', () => { describe('publishDocument', () => { it('should save the document with published:true and dispatch notification on success', (done) => { - let document = {name: 'doc', _id: 'abc1'}; + const document = { name: 'doc', _id: 'abc1' }; const expectedActions = [ - {type: notificationsTypes.NOTIFY, notification: {message: 'Document published', type: 'success', id: 'unique_id'}}, - {type: types.REMOVE_DOCUMENT, doc: document} + { type: notificationsTypes.NOTIFY, notification: { message: 'Document published', type: 'success', id: 'unique_id' } }, + { type: types.REMOVE_DOCUMENT, doc: document } ]; const store = mockStore({}); store.dispatch(actions.publishDocument(document)) .then(() => { - expect(backend.lastOptions().body).toEqual(JSON.stringify({name: 'doc', _id: 'abc1', published: true})); + expect(backend.lastOptions().body).toEqual(JSON.stringify({ name: 'doc', _id: 'abc1', published: true })); expect(store.getActions()).toEqual(expectedActions); }) .then(done) diff --git a/app/react/Uploads/actions/uploadsActions.js b/app/react/Uploads/actions/uploadsActions.js index 3bc227700e..27d418674f 100644 --- a/app/react/Uploads/actions/uploadsActions.js +++ b/app/react/Uploads/actions/uploadsActions.js @@ -15,7 +15,7 @@ export function enterUploads() { } export function newEntity() { - return function (dispatch, getState) { + return (dispatch, getState) => { const newEntityMetadata = { title: '', type: 'entity' }; dispatch(metadata.actions.loadInReduxForm('uploads.sidepanel.metadata', newEntityMetadata, getState().templates.toJS())); dispatch(selectSingleDocument(newEntityMetadata)); @@ -23,20 +23,18 @@ export function newEntity() { } export function createDocument(newDoc) { - return function (dispatch) { - return api.post('documents', newDoc) - .then((response) => { - const doc = response.json; - dispatch({ type: types.NEW_UPLOAD_DOCUMENT, doc: doc.sharedId }); - dispatch({ type: types.ELEMENT_CREATED, doc }); - return doc; - }); - }; + return dispatch => api.post('documents', newDoc) + .then((response) => { + const doc = response.json; + dispatch({ type: types.NEW_UPLOAD_DOCUMENT, doc: doc.sharedId }); + dispatch({ type: types.ELEMENT_CREATED, doc }); + return doc; + }); } -export function uploadDocument(docId, file) { - return function (dispatch) { - superagent.post(`${APIURL}upload`) +export function upload(docId, file, endpoint = 'upload') { + return dispatch => new Promise((resolve) => { + superagent.post(APIURL + endpoint) .set('Accept', 'application/json') .field('document', docId) .attach('file', file, file.name) @@ -45,24 +43,14 @@ export function uploadDocument(docId, file) { }) .on('response', () => { dispatch({ type: types.UPLOAD_COMPLETE, doc: docId }); + resolve(); }) .end(); - }; + }); } -export function upload(docId, file) { - return function (dispatch) { - superagent.post(`${APIURL}customisation/upload`) - .set('Accept', 'application/json') - .attach('file', file, file.name) - .on('progress', (data) => { - dispatch({ type: types.UPLOAD_PROGRESS, doc: docId, progress: Math.floor(data.percent) }); - }) - .on('response', () => { - dispatch({ type: types.UPLOAD_COMPLETE, doc: docId }); - }) - .end(); - }; +export function uploadDocument(docId, file) { + return dispatch => upload(docId, file)(dispatch); } export function documentProcessed(sharedId) { @@ -74,31 +62,23 @@ export function documentProcessError(sharedId) { } export function publishEntity(entity) { - return function (dispatch) { - entity.published = true; - return api.post('entities', entity) - .then(() => { - dispatch(notify('Entity published', 'success')); - dispatch({ type: types.REMOVE_DOCUMENT, doc: entity }); - }); - }; + return dispatch => api.post('entities', { ...entity, published: true }) + .then(() => { + dispatch(notify('Entity published', 'success')); + dispatch({ type: types.REMOVE_DOCUMENT, doc: entity }); + }); } export function publishDocument(doc) { - return function (dispatch) { - doc.published = true; - return api.post('documents', doc) - .then(() => { - dispatch(notify('Document published', 'success')); - dispatch({ type: types.REMOVE_DOCUMENT, doc }); - }); - }; + return dispatch => api.post('documents', { ...doc, published: true }) + .then(() => { + dispatch(notify('Document published', 'success')); + dispatch({ type: types.REMOVE_DOCUMENT, doc }); + }); } export function publish(entity) { - return function (dispatch) { - return entity.type === 'entity' ? dispatch(publishEntity(entity)) : dispatch(publishDocument(entity)); - }; + return dispatch => entity.type === 'entity' ? dispatch(publishEntity(entity)) : dispatch(publishDocument(entity)); } export function conversionComplete(docId) { diff --git a/package.json b/package.json index f9e63be00d..9bce99c566 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "assets-webpack-plugin": "Kronuz/assets-webpack-plugin", "babel-jest": "^22.4.0", "babel-plugin-react-transform": "3.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-polyfill": "^6.23.0", "compression-webpack-plugin": "^1.0.0", "copy-webpack-plugin": "^4.2.3", diff --git a/yarn.lock b/yarn.lock index 282e5f44bf..999c39e434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -753,7 +753,7 @@ babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" -babel-plugin-syntax-object-rest-spread@^6.13.0: +babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -961,6 +961,13 @@ babel-plugin-transform-flow-strip-types@^6.22.0: babel-plugin-syntax-flow "^6.18.0" babel-runtime "^6.22.0" +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + babel-plugin-transform-react-constant-elements@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-constant-elements/-/babel-plugin-transform-react-constant-elements-6.23.0.tgz#2f119bf4d2cdd45eb9baaae574053c604f6147dd" From 761776ba34b44d96aa9b41cecdd3d48e5161578b Mon Sep 17 00:00:00 2001 From: Daneryl Date: Fri, 22 Jun 2018 18:11:01 +0200 Subject: [PATCH 04/20] CustomUploads, WIP --- .../Settings/components/CustomUploads.js | 39 ++--- .../components/specs/CustomUploads.spec.js | 59 +++++++ .../__snapshots__/CustomUploads.spec.js.snap | 154 ++++++++++++++++++ .../actions/specs/uploadsActions.spec.js | 39 ++++- app/react/Uploads/actions/uploadsActions.js | 16 +- app/react/reducer.js | 1 + 6 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 app/react/Settings/components/specs/CustomUploads.spec.js create mode 100644 app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index b86a63a3be..ae76ce71f7 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -6,22 +6,12 @@ import React, { Component } from 'react'; import { t } from 'app/I18N'; -import { upload } from '../../Uploads/actions/uploadsActions'; - -const UploadProgressCompoent = ({ progress }) => { - return {progress} %; -}; - -const UploadProgress = connect(({ progress }, props) => { - console.log(progress.toJS()); - - return {progress: 50}; -})(UploadProgressCompoent); +import { uploadCustom } from '../../Uploads/actions/uploadsActions'; export class CustomUploads extends Component { onDrop(files) { files.forEach((file) => { - this.props.upload(file.preview, file); + this.props.upload(file); }); } render() { @@ -32,31 +22,42 @@ export class CustomUploads extends Component {
- Browse your PDFs to upload + Browse files to upload or drop your files here.
- ProTip! - For better performance, upload your documents in batches of 50 or less.
- + {this.props.progress &&

Uploading ...

} +
    + {this.props.customUploads.map((upload) => { + return
  • {`/uploaded_documents/${upload.get('filename')}`}
  • ; + })} +
); } } +CustomUploads.defaultProps = { + progress: false +}; + CustomUploads.propTypes = { + progress: PropTypes.bool, upload: PropTypes.func.isRequired }; -const mapStateToProps = () => ({}); -const mapDispatchToProps = dispatch => bindActionCreators({ upload }, dispatch); +export const mapStateToProps = ({ customUploads, progress }) => ({ + customUploads, + progress: !!progress.filter((v, key) => key.match(/customUpload/)).size +}); + +const mapDispatchToProps = dispatch => bindActionCreators({ upload: uploadCustom }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(CustomUploads); diff --git a/app/react/Settings/components/specs/CustomUploads.spec.js b/app/react/Settings/components/specs/CustomUploads.spec.js new file mode 100644 index 0000000000..c43729d2ea --- /dev/null +++ b/app/react/Settings/components/specs/CustomUploads.spec.js @@ -0,0 +1,59 @@ +import Immutable from 'immutable'; +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CustomUploads, mapStateToProps } from '../CustomUploads'; + +describe('CustomUploads', () => { + let component; + let props; + + beforeEach(() => { + props = { + upload: jasmine.createSpy('upload') + }; + }); + + const render = () => { + component = shallow(); + }; + + it('should render CustomUploads component with uploaded files', () => { + props.customUploads = Immutable.fromJS([ + { filename: 'file1' }, + { filename: 'file2' }, + ]); + render(); + expect(component).toMatchSnapshot(); + }); + + describe('when upload on progress', () => { + it('should render on progress feedback', () => { + props.progress = true; + render(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('mapStateToProps', () => { + it('should map current progress and files to props', () => { + const state = { + customUploads: 'customUploads', + progress: Immutable.fromJS({}) + }; + + let props = mapStateToProps(state); + expect(props.customUploads).toBe('customUploads'); + expect(props.progress).toBe(false); + + state.progress = Immutable.fromJS({ customUpload_unique_id: 1, customUpload_unique_id2: 100 }); + props = mapStateToProps(state); + expect(props.progress).toBe(true); + + state.progress = Immutable.fromJS({ not_custom_upload: 9 }); + props = mapStateToProps(state); + expect(props.progress).toBe(false); + }); + }); +}); diff --git a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap new file mode 100644 index 0000000000..e95b68871a --- /dev/null +++ b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap @@ -0,0 +1,154 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomUploads should render CustomUploads component 1`] = ` +
+
+ Custom Uploads +
+
+ +
+ + + Browse files to upload + + + or drop your files here. + +
+
+ +
+
+
+
+`; + +exports[`CustomUploads should render CustomUploads component with uploaded files 1`] = ` +
+
+ Custom Uploads +
+
+ +
+ + + Browse files to upload + + + or drop your files here. + +
+
+ +
+
+
+
+`; + +exports[`CustomUploads when upload on progress should render on progress feedback 1`] = ` +
+
+ Custom Uploads +
+
+ +
+ + + Browse files to upload + + + or drop your files here. + +
+
+ +
+
+
+

+ Uploading ... +

+
+`; diff --git a/app/react/Uploads/actions/specs/uploadsActions.spec.js b/app/react/Uploads/actions/specs/uploadsActions.spec.js index 4f70634661..c255b75259 100644 --- a/app/react/Uploads/actions/specs/uploadsActions.spec.js +++ b/app/react/Uploads/actions/specs/uploadsActions.spec.js @@ -9,6 +9,7 @@ import backend from 'fetch-mock'; import configureMockStore from 'redux-mock-store'; import * as notificationsTypes from 'app/Notifications/actions/actionTypes'; import * as types from 'app/Uploads/actions/actionTypes'; +import { actions as basicActions } from 'app/BasicReducer'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -92,11 +93,47 @@ describe('uploadsActions', () => { mockUpload.emit('progress', { percent: 55.1 }); mockUpload.emit('progress', { percent: 65 }); - mockUpload.emit('response'); + mockUpload.emit('response', { text: JSON.stringify({ test: 'test' }) }); expect(store.getActions()).toEqual(expectedActions); }); }); + describe('uploadCustom', () => { + it('should upload a file and then add it to the customUploads', (done) => { + const mockUpload = superagent.post(`${APIURL}customisation/upload`); + spyOn(mockUpload, 'field').and.returnValue(mockUpload); + spyOn(mockUpload, 'attach').and.returnValue(mockUpload); + spyOn(superagent, 'post').and.returnValue(mockUpload); + + const expectedActions = [ + { type: types.UPLOAD_PROGRESS, doc: 'customUpload_unique_id', progress: 65 }, + { type: types.UPLOAD_PROGRESS, doc: 'customUpload_unique_id', progress: 75 }, + { type: types.UPLOAD_COMPLETE, doc: 'customUpload_unique_id' }, + basicActions.push('customUploads', { test: 'test' }) + ]; + const store = mockStore({}); + + // needed to work with firefox/chrome and phantomjs + let file = { name: 'filename' }; + const isChrome = typeof File === 'function'; + if (isChrome) { + file = new File([], 'filename'); + } + // + + store.dispatch(actions.uploadCustom(file)) + .then(() => { + expect(mockUpload.attach).toHaveBeenCalledWith('file', file, file.name); + expect(store.getActions()).toEqual(expectedActions); + done(); + }); + + mockUpload.emit('progress', { percent: 65.1 }); + mockUpload.emit('progress', { percent: 75 }); + mockUpload.emit('response', { text: JSON.stringify({ test: 'test' }) }); + }); + }); + describe('publishDocument', () => { it('should save the document with published:true and dispatch notification on success', (done) => { const document = { name: 'doc', _id: 'abc1' }; diff --git a/app/react/Uploads/actions/uploadsActions.js b/app/react/Uploads/actions/uploadsActions.js index 27d418674f..7a376c03e9 100644 --- a/app/react/Uploads/actions/uploadsActions.js +++ b/app/react/Uploads/actions/uploadsActions.js @@ -7,6 +7,8 @@ import * as types from 'app/Uploads/actions/actionTypes'; import { APIURL } from '../../config.js'; import api from '../../utils/api'; +import uniqueID from 'shared/uniqueID'; +import { actions as basicActions } from 'app/BasicReducer'; export function enterUploads() { return { @@ -41,14 +43,24 @@ export function upload(docId, file, endpoint = 'upload') { .on('progress', (data) => { dispatch({ type: types.UPLOAD_PROGRESS, doc: docId, progress: Math.floor(data.percent) }); }) - .on('response', () => { + .on('response', (response) => { dispatch({ type: types.UPLOAD_COMPLETE, doc: docId }); - resolve(); + resolve(JSON.parse(response.text)); }) .end(); }); } +export function uploadCustom(file) { + return (dispatch) => { + const id = `customUpload_${uniqueID()}`; + return upload(id, file, 'customisation/upload')(dispatch) + .then((response) => { + dispatch(basicActions.push('customUploads', response)); + }); + }; +} + export function uploadDocument(docId, file) { return dispatch => upload(docId, file)(dispatch); } diff --git a/app/react/reducer.js b/app/react/reducer.js index 790a1b5f13..3c7c6ac92e 100644 --- a/app/react/reducer.js +++ b/app/react/reducer.js @@ -40,6 +40,7 @@ export default combineReducers({ thesauri, entityView, thesauris: createReducer('thesauris', []), + customUploads: createReducer('customUploads', []), dictionaries: createReducer('dictionaries', []), relationTypes: createReducer('relationTypes', []), relationType: modelReducer('relationType', { name: '' }), From 673b57b4d2f8d82ef6fe48ffb8f83e82dbc0d511 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Mon, 25 Jun 2018 12:27:49 +0200 Subject: [PATCH 05/20] created Thumbnail component --- app/react/Layout/Thumbnail.js | 34 +++++++++ app/react/Layout/index.js | 19 ++--- app/react/Layout/specs/Thumbnail.spec.js | 36 ++++++++++ .../__snapshots__/Thumbnail.spec.js.snap | 31 ++++++++ .../Settings/components/CustomUploads.js | 16 +++-- .../components/specs/CustomUploads.spec.js | 5 +- .../__snapshots__/CustomUploads.spec.js.snap | 71 +++++-------------- 7 files changed, 138 insertions(+), 74 deletions(-) create mode 100644 app/react/Layout/Thumbnail.js create mode 100644 app/react/Layout/specs/Thumbnail.spec.js create mode 100644 app/react/Layout/specs/__snapshots__/Thumbnail.spec.js.snap diff --git a/app/react/Layout/Thumbnail.js b/app/react/Layout/Thumbnail.js new file mode 100644 index 0000000000..502ae17b06 --- /dev/null +++ b/app/react/Layout/Thumbnail.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +const getExtension = filename => filename.substr(filename.lastIndexOf('.') + 1); + +const acceptedThumbnailExtensions = ['png', 'gif', 'jpg']; + +export class Thumbnail extends Component { + render() { + const extension = getExtension(this.props.file); + let thumbnail; + + if (acceptedThumbnailExtensions.includes(extension)) { + thumbnail = {this.props.alt}; + } + + if (extension === 'pdf') { + thumbnail = pdf; + } + + return
{thumbnail}
; + } +} + +Thumbnail.defaultProps = { + alt: '' +}; + +Thumbnail.propTypes = { + file: PropTypes.string.isRequired, + alt: PropTypes.string +}; + +export default Thumbnail; diff --git a/app/react/Layout/index.js b/app/react/Layout/index.js index ece7d5a1f2..55a840c0ac 100644 --- a/app/react/Layout/index.js +++ b/app/react/Layout/index.js @@ -1,13 +1,6 @@ -import Item from './Item'; -import TemplateLabel from './TemplateLabel'; -import Icon from './Icon'; -import SidePanel from './SidePanel'; -import DocumentLanguage from './DocumentLanguage'; - -export { - Item, - TemplateLabel, - Icon, - SidePanel, - DocumentLanguage -}; +export { default as Item } from './Item'; +export { default as TemplateLabel } from './TemplateLabel'; +export { default as Icon } from './Icon'; +export { default as SidePanel } from './SidePanel'; +export { default as DocumentLanguage } from './DocumentLanguage'; +export { default as Thumbnail } from './Thumbnail'; diff --git a/app/react/Layout/specs/Thumbnail.spec.js b/app/react/Layout/specs/Thumbnail.spec.js new file mode 100644 index 0000000000..5a090ca9bf --- /dev/null +++ b/app/react/Layout/specs/Thumbnail.spec.js @@ -0,0 +1,36 @@ +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { Thumbnail } from '../Thumbnail'; + +describe('Thumbnail', () => { + let component; + let props; + + beforeEach(() => { + props = {}; + }); + + const render = () => { + component = shallow(); + }; + + it('should render an image when file has image extension', () => { + props.file = 'image.jpg'; + render(); + expect(component).toMatchSnapshot(); + }); + + it('should render pdf icon when .pdf extension', () => { + props.file = 'pdf.pdf'; + render(); + expect(component).toMatchSnapshot(); + }); + + it('should render generic file as default', () => { + props.file = 'document.doc'; + render(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/app/react/Layout/specs/__snapshots__/Thumbnail.spec.js.snap b/app/react/Layout/specs/__snapshots__/Thumbnail.spec.js.snap new file mode 100644 index 0000000000..97c30611bc --- /dev/null +++ b/app/react/Layout/specs/__snapshots__/Thumbnail.spec.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Thumbnail should render an image when file has image extension 1`] = ` +
+ +
+`; + +exports[`Thumbnail should render generic file as default 1`] = ` +
+`; + +exports[`Thumbnail should render pdf icon when .pdf extension 1`] = ` +
+ + + pdf + +
+`; diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index ae76ce71f7..edff95a381 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -1,6 +1,7 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import Dropzone from 'react-dropzone'; +import Immutable from 'immutable'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; @@ -9,11 +10,17 @@ import { t } from 'app/I18N'; import { uploadCustom } from '../../Uploads/actions/uploadsActions'; export class CustomUploads extends Component { + constructor(props) { + super(props); + this.onDrop = this.onDrop.bind(this); + } + onDrop(files) { files.forEach((file) => { this.props.upload(file); }); } + render() { return (
@@ -21,11 +28,11 @@ export class CustomUploads extends Component {
- Browse files to upload + or drop your files here.
@@ -35,9 +42,7 @@ export class CustomUploads extends Component {
{this.props.progress &&

Uploading ...

}
    - {this.props.customUploads.map((upload) => { - return
  • {`/uploaded_documents/${upload.get('filename')}`}
  • ; - })} + {this.props.customUploads.map(upload =>
  • {`/uploaded_documents/${upload.get('filename')}`}
  • )}
); @@ -50,6 +55,7 @@ CustomUploads.defaultProps = { CustomUploads.propTypes = { progress: PropTypes.bool, + customUploads: PropTypes.instanceOf(Immutable.List).isRequired, upload: PropTypes.func.isRequired }; diff --git a/app/react/Settings/components/specs/CustomUploads.spec.js b/app/react/Settings/components/specs/CustomUploads.spec.js index c43729d2ea..50568514db 100644 --- a/app/react/Settings/components/specs/CustomUploads.spec.js +++ b/app/react/Settings/components/specs/CustomUploads.spec.js @@ -11,7 +11,8 @@ describe('CustomUploads', () => { beforeEach(() => { props = { - upload: jasmine.createSpy('upload') + upload: jasmine.createSpy('upload'), + customUploads: Immutable.fromJS([]) }; }); @@ -43,7 +44,7 @@ describe('CustomUploads', () => { progress: Immutable.fromJS({}) }; - let props = mapStateToProps(state); + props = mapStateToProps(state); expect(props.customUploads).toBe('customUploads'); expect(props.progress).toBe(false); diff --git a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap index e95b68871a..f0520ed885 100644 --- a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap +++ b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap @@ -1,55 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CustomUploads should render CustomUploads component 1`] = ` -
-
- Custom Uploads -
-
- -
- - - Browse files to upload - - - or drop your files here. - -
-
- -
-
-
-
-`; - exports[`CustomUploads should render CustomUploads component with uploaded files 1`] = `
- Browse files to upload - + or drop your files here. @@ -97,6 +47,18 @@ exports[`CustomUploads should render CustomUploads component with uploaded files
+
    +
  • + /uploaded_documents/file1 +
  • +
  • + /uploaded_documents/file2 +
  • +
`; @@ -129,11 +91,11 @@ exports[`CustomUploads when upload on progress should render on progress feedbac - Browse files to upload - + or drop your files here. @@ -150,5 +112,6 @@ exports[`CustomUploads when upload on progress should render on progress feedbac

Uploading ...

+
    `; From d96e56ab502e15a76cbb6fb2362396014c76a42e Mon Sep 17 00:00:00 2001 From: Daneryl Date: Mon, 25 Jun 2018 14:35:53 +0200 Subject: [PATCH 06/20] uploads get api endpoint --- app/api/upload/routes.js | 21 +++++++++++++------ .../specs/__snapshots__/routes.spec.js.snap | 18 ++++++++++++++++ app/api/upload/specs/fixtures.js | 8 +++++-- app/api/upload/specs/routes.spec.js | 16 ++++++++++++++ app/api/upload/uploads.js | 3 ++- .../Settings/components/CustomUploads.js | 11 +++++++--- .../__snapshots__/CustomUploads.spec.js.snap | 8 +++++++ 7 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 app/api/upload/specs/__snapshots__/routes.spec.js.snap diff --git a/app/api/upload/routes.js b/app/api/upload/routes.js index 8a7968b1a4..1d57f10c72 100644 --- a/app/api/upload/routes.js +++ b/app/api/upload/routes.js @@ -1,23 +1,25 @@ import path from 'path'; import multer from 'multer'; + import ID from 'shared/uniqueID'; -import languages from 'shared/languages'; import entities from 'api/entities'; +import fs from 'fs'; +import languages from 'shared/languages'; +import logger from 'shared/logger'; +import path from 'path'; import relationships from 'api/relationships'; + +import { uploadDocumentsPath } from '../config/paths'; import PDF from './PDF'; import needsAuthorization from '../auth/authMiddleware'; -import { uploadDocumentsPath } from '../config/paths'; import uploads from './uploads'; -import fs from 'fs'; - -import logger from 'shared/logger'; const storage = multer.diskStorage({ destination(req, file, cb) { cb(null, path.normalize(`${uploadDocumentsPath}/`)); }, filename(req, file, cb) { - cb(null, `${Date.now() + ID()}.pdf`); + cb(null, Date.now() + ID() + path.extname(file.originalname)); } }); @@ -97,6 +99,13 @@ export default (app) => { }); }); + app.get('/api/customisation/upload', needsAuthorization(['admin', 'editor']), (req, res) => { + uploads.get() + .then((result) => { + res.json(result); + }); + }); + app.post('/api/reupload', needsAuthorization(['admin', 'editor']), upload.any(), (req, res) => entities.getById(req.body.document) .then(doc => Promise.all([doc, relationships.deleteTextReferences(doc.sharedId, doc.language)])) .then(([doc]) => entities.saveMultiple([{ _id: doc._id, toc: [] }])) diff --git a/app/api/upload/specs/__snapshots__/routes.spec.js.snap b/app/api/upload/specs/__snapshots__/routes.spec.js.snap new file mode 100644 index 0000000000..cee8c494cf --- /dev/null +++ b/app/api/upload/specs/__snapshots__/routes.spec.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`upload routes GET/customisation/upload should return all uploads 1`] = ` +Array [ + "upload1", + "upload2", +] +`; + +exports[`upload routes POST/customisation/upload should save the upload and return it 1`] = ` +Object { + "__v": 0, + "filename": "f2082bf51b6ef839690485d7153e847a.pdf", + "mimetype": "application/octet-stream", + "originalname": "gadgets-01.pdf", + "size": 171411271, +} +`; diff --git a/app/api/upload/specs/fixtures.js b/app/api/upload/specs/fixtures.js index 0bdfe3e70c..3618ffb48b 100644 --- a/app/api/upload/specs/fixtures.js +++ b/app/api/upload/specs/fixtures.js @@ -4,8 +4,12 @@ const entityId = db.id(); export default { entities: [ - {_id: entityId, sharedId: 'id', language: 'es', title: 'Gadgets 01 ES', toc: [{_id: db.id(), label: 'existingToc'}]}, - {_id: db.id(), sharedId: 'id', language: 'en', title: 'Gadgets 01 EN'} + { _id: entityId, sharedId: 'id', language: 'es', title: 'Gadgets 01 ES', toc: [{ _id: db.id(), label: 'existingToc' }] }, + { _id: db.id(), sharedId: 'id', language: 'en', title: 'Gadgets 01 EN' } + ], + uploads: [ + { _id: db.id(), originalname: 'upload1' }, + { _id: db.id(), originalname: 'upload2' }, ] }; diff --git a/app/api/upload/specs/routes.spec.js b/app/api/upload/specs/routes.spec.js index 281dd02d8c..c160405a3d 100644 --- a/app/api/upload/specs/routes.spec.js +++ b/app/api/upload/specs/routes.spec.js @@ -177,4 +177,20 @@ describe('upload routes', () => { .catch(done.fail); }); }); + + describe('POST/customisation/upload', () => { + it('should save the upload and return it', async () => { + const result = await routes.post('/api/customisation/upload', req); + delete result._id; + delete result.creationDate; + expect(result).toMatchSnapshot(); + }); + }); + + describe('GET/customisation/upload', () => { + it('should return all uploads', async () => { + const result = await routes.get('/api/customisation/upload', {}); + expect(result.map(r => r.originalname)).toMatchSnapshot(); + }); + }); }); diff --git a/app/api/upload/uploads.js b/app/api/upload/uploads.js index ef4fca43e5..bfa5902d75 100644 --- a/app/api/upload/uploads.js +++ b/app/api/upload/uploads.js @@ -1,5 +1,6 @@ import model from './uploadsModel'; export default { - save: model.save.bind(model) + save: model.save.bind(model), + get: model.get.bind(model), }; diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index edff95a381..8c67a4e6ab 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -4,6 +4,7 @@ import Dropzone from 'react-dropzone'; import Immutable from 'immutable'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { Thumbnail } from 'app/Layout'; import { t } from 'app/I18N'; @@ -42,7 +43,11 @@ export class CustomUploads extends Component { {this.props.progress &&

    Uploading ...

    }
      - {this.props.customUploads.map(upload =>
    • {`/uploaded_documents/${upload.get('filename')}`}
    • )} + {this.props.customUploads.map(upload => ( +
    • + {`/uploaded_documents/${upload.get('filename')}`} +
    • + ))}
    ); @@ -60,8 +65,8 @@ CustomUploads.propTypes = { }; export const mapStateToProps = ({ customUploads, progress }) => ({ - customUploads, - progress: !!progress.filter((v, key) => key.match(/customUpload/)).size + customUploads, + progress: !!progress.filter((v, key) => key.match(/customUpload/)).size }); const mapDispatchToProps = dispatch => bindActionCreators({ upload: uploadCustom }, dispatch); diff --git a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap index f0520ed885..3953b2d691 100644 --- a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap +++ b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap @@ -51,11 +51,19 @@ exports[`CustomUploads should render CustomUploads component with uploaded files
  • + /uploaded_documents/file1
  • + /uploaded_documents/file2
From f3beb6cb33b3eb04bff6b1cf20c4d97a6c150f12 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Mon, 25 Jun 2018 16:52:29 +0200 Subject: [PATCH 07/20] CustomUploads as a Route --- .../Settings/components/CustomUploads.js | 26 ++++++++++++++---- .../components/specs/CustomUploads.spec.js | 27 ++++++++++++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index 8c67a4e6ab..1361c0cc26 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -3,16 +3,24 @@ import { connect } from 'react-redux'; import Dropzone from 'react-dropzone'; import Immutable from 'immutable'; import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Thumbnail } from 'app/Layout'; +import React from 'react'; +import { Thumbnail } from 'app/Layout'; +import { actions } from 'app/BasicReducer'; import { t } from 'app/I18N'; +import RouteHandler from 'app/App/RouteHandler'; +import api from 'app/utils/api'; import { uploadCustom } from '../../Uploads/actions/uploadsActions'; -export class CustomUploads extends Component { - constructor(props) { - super(props); +export class CustomUploads extends RouteHandler { + static requestState() { + return api.get('customisation/upload') + .then(customUploads => ({ customUploads: customUploads.json })); + } + + constructor(props, context) { + super(props, context); this.onDrop = this.onDrop.bind(this); } @@ -22,6 +30,10 @@ export class CustomUploads extends Component { }); } + setReduxState(state) { + this.context.store.dispatch(actions.set('customUpload', state.customUploads)); + } + render() { return (
@@ -54,6 +66,10 @@ export class CustomUploads extends Component { } } +CustomUploads.contextTypes = { + store: PropTypes.object +}; + CustomUploads.defaultProps = { progress: false }; diff --git a/app/react/Settings/components/specs/CustomUploads.spec.js b/app/react/Settings/components/specs/CustomUploads.spec.js index 50568514db..77a7115c86 100644 --- a/app/react/Settings/components/specs/CustomUploads.spec.js +++ b/app/react/Settings/components/specs/CustomUploads.spec.js @@ -2,14 +2,17 @@ import Immutable from 'immutable'; import React from 'react'; import { shallow } from 'enzyme'; +import api from 'app/utils/api'; import { CustomUploads, mapStateToProps } from '../CustomUploads'; describe('CustomUploads', () => { let component; let props; + let context; beforeEach(() => { + spyOn(api, 'get').and.returnValue(Promise.resolve('uploads')); props = { upload: jasmine.createSpy('upload'), customUploads: Immutable.fromJS([]) @@ -17,7 +20,8 @@ describe('CustomUploads', () => { }); const render = () => { - component = shallow(); + context = { store: { dispatch: jasmine.createSpy('dispatch') } }; + component = shallow(, context); }; it('should render CustomUploads component with uploaded files', () => { @@ -57,4 +61,25 @@ describe('CustomUploads', () => { expect(props.progress).toBe(false); }); }); + + describe('requestState', () => { + it('should get the uploads', (done) => { + render(); + CustomUploads.requestState() + .then((state) => { + expect(api.get).toHaveBeenCalledWith('/customisation/upload'); + expect(state.customUploads).toEqual('uploads'); + done(); + }); + }); + }); + + describe('setReduxState', () => { + it('should set customUploads in state', () => { + render(); + const instance = component.instance(); + instance.setReduxState({ customUploads: 'CustomUploads' }); + expect(context.store.dispatch).toHaveBeenCalledWith({ type: 'customUploads/SET', value: 'customUploads' }); + }); + }); }); From 4ef68dc0508eb98648e58d1ed2c914197bf00945 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Mon, 25 Jun 2018 17:56:08 +0200 Subject: [PATCH 08/20] fixed tests for CustomUploads --- app/react/Settings/components/CustomUploads.js | 6 +----- .../Settings/components/specs/CustomUploads.spec.js | 13 +++++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index 1361c0cc26..64bac98676 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -31,7 +31,7 @@ export class CustomUploads extends RouteHandler { } setReduxState(state) { - this.context.store.dispatch(actions.set('customUpload', state.customUploads)); + this.context.store.dispatch(actions.set('customUploads', state.customUploads)); } render() { @@ -66,10 +66,6 @@ export class CustomUploads extends RouteHandler { } } -CustomUploads.contextTypes = { - store: PropTypes.object -}; - CustomUploads.defaultProps = { progress: false }; diff --git a/app/react/Settings/components/specs/CustomUploads.spec.js b/app/react/Settings/components/specs/CustomUploads.spec.js index 77a7115c86..b6815d072f 100644 --- a/app/react/Settings/components/specs/CustomUploads.spec.js +++ b/app/react/Settings/components/specs/CustomUploads.spec.js @@ -6,13 +6,14 @@ import api from 'app/utils/api'; import { CustomUploads, mapStateToProps } from '../CustomUploads'; -describe('CustomUploads', () => { +fdescribe('CustomUploads', () => { let component; let props; let context; + let instance; beforeEach(() => { - spyOn(api, 'get').and.returnValue(Promise.resolve('uploads')); + spyOn(api, 'get').and.returnValue(Promise.resolve({ json: 'uploads' })); props = { upload: jasmine.createSpy('upload'), customUploads: Immutable.fromJS([]) @@ -21,7 +22,8 @@ describe('CustomUploads', () => { const render = () => { context = { store: { dispatch: jasmine.createSpy('dispatch') } }; - component = shallow(, context); + component = shallow(, { context }); + instance = component.instance(); }; it('should render CustomUploads component with uploaded files', () => { @@ -67,7 +69,7 @@ describe('CustomUploads', () => { render(); CustomUploads.requestState() .then((state) => { - expect(api.get).toHaveBeenCalledWith('/customisation/upload'); + expect(api.get).toHaveBeenCalledWith('customisation/upload'); expect(state.customUploads).toEqual('uploads'); done(); }); @@ -77,8 +79,7 @@ describe('CustomUploads', () => { describe('setReduxState', () => { it('should set customUploads in state', () => { render(); - const instance = component.instance(); - instance.setReduxState({ customUploads: 'CustomUploads' }); + instance.setReduxState({ customUploads: 'customUploads' }); expect(context.store.dispatch).toHaveBeenCalledWith({ type: 'customUploads/SET', value: 'customUploads' }); }); }); From 9474ba1f9a9baa6ec730a9123b8f2f9ca82ae6cd Mon Sep 17 00:00:00 2001 From: RafaPolit Date: Mon, 25 Jun 2018 18:28:33 -0500 Subject: [PATCH 09/20] Added some basic styling to the custom-uploads. --- app/react/App/scss/modules/_settings.scss | 51 ++++++++++++++----- .../Settings/components/CustomUploads.js | 22 ++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/app/react/App/scss/modules/_settings.scss b/app/react/App/scss/modules/_settings.scss index 0ec7fff598..5629be046d 100644 --- a/app/react/App/scss/modules/_settings.scss +++ b/app/react/App/scss/modules/_settings.scss @@ -4,7 +4,7 @@ -webkit-flex-wrap: wrap; flex-wrap: wrap; overflow: auto; - @media(min-width: 1024px) { + @media(min-width: 1024px) { -webkit-flex-wrap: nowrap; flex-wrap: nowrap; } @@ -32,7 +32,7 @@ .settings { background: $c-white; - + .panel { border: 0; margin: 0; @@ -42,11 +42,11 @@ .settings-navigation { background: $c-background; - + .panel { background-color: transparent; } - + .panel-heading { background-color: transparent; text-transform: uppercase; @@ -59,7 +59,7 @@ .panel-heading:first-of-type .panel-heading { border-top: 0; } - + .list-group-item { border: 0; padding-left: 20px; @@ -101,7 +101,7 @@ } } } - + .fa { margin-right: 5px; } @@ -109,24 +109,24 @@ .settings-content { padding: 15px; - + .panel-heading { background-color: transparent; font-size: $f-size-xl; font-weight: 300; - + input { font-size: $f-size-xl; } } - + h2 { padding: 16px 0; font-size: $f-size-lg; font-weight: 300; border-bottom: 1px solid $c-grey-light; } - + .list-group-item:last-of-type { border-bottom: 0; } @@ -144,7 +144,7 @@ @media(min-width: 1024px) { right: calc((100% - 400px)/2); } - + .btn { position: relative; border-radius: 50%; @@ -216,7 +216,7 @@ .settings-content > .panel > .list-group { // height: calc(100% - 51px); padding-bottom: 85px; - // overflow-y: scroll; + // overflow-y: scroll; } .page-creator, @@ -247,3 +247,30 @@ } } } + +.custom-uploads { + .uploading { + font-size: 22px; + margin-bottom: 15px; + } + ul { + padding-left: 0px; + li { + display: flex; + + div.info { + margin-left: 15px; + + span { + font-style: italic; + font-weight: bold; + } + } + } + } + + .thumbnail { + max-width: 80px; + max-height: 80px; + } +} diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index 64bac98676..0a4b33c9a5 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -38,7 +38,7 @@ export class CustomUploads extends RouteHandler { return (
{t('System', 'Custom Uploads')}
-
+
+ {this.props.progress &&

 Uploading ...

} +
    + {this.props.customUploads.map(upload => ( +
  • + +
    + URL:
    + {`/uploaded_documents/${upload.get('filename')}`} +
    +
  • + ))} +
- {this.props.progress &&

Uploading ...

} -
    - {this.props.customUploads.map(upload => ( -
  • - {`/uploaded_documents/${upload.get('filename')}`} -
  • - ))} -
); } From 4601a31807ad48971bbd302be95769120aa1c52c Mon Sep 17 00:00:00 2001 From: Daneryl Date: Tue, 26 Jun 2018 13:00:01 +0200 Subject: [PATCH 10/20] fixed css --- app/react/App/scss/modules/_settings.scss | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/react/App/scss/modules/_settings.scss b/app/react/App/scss/modules/_settings.scss index 5629be046d..1e5658a2de 100644 --- a/app/react/App/scss/modules/_settings.scss +++ b/app/react/App/scss/modules/_settings.scss @@ -270,7 +270,16 @@ } .thumbnail { - max-width: 80px; - max-height: 80px; + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } } } From 332a090403e64f1a55c25ca4f300b59d5abbba0b Mon Sep 17 00:00:00 2001 From: Daneryl Date: Tue, 26 Jun 2018 13:49:20 +0200 Subject: [PATCH 11/20] delete upload method --- app/api/upload/specs/uploads.spec.js | 19 ++++- app/api/upload/uploads.js | 23 ++++++ .../__snapshots__/CustomUploads.spec.js.snap | 77 ++++++++++++------- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/app/api/upload/specs/uploads.spec.js b/app/api/upload/specs/uploads.spec.js index 0b132e7194..ce3154b071 100644 --- a/app/api/upload/specs/uploads.spec.js +++ b/app/api/upload/specs/uploads.spec.js @@ -1,11 +1,16 @@ /* eslint-disable max-nested-callbacks */ import { catchErrors } from 'api/utils/jasmineHelpers'; import db from 'api/utils/testing_db'; +import fs from 'fs'; +import path from 'path'; + import uploads from '../uploads'; import uploadsModel from '../uploadsModel'; +import { uploadDocumentsPath } from '../../config/paths'; describe('uploads', () => { let file; + const uploadId = db.id(); beforeEach((done) => { file = { @@ -19,7 +24,7 @@ describe('uploads', () => { size: 171411271 }; - db.clearAllAndLoad({ uploads: [{}] }).then(done).catch(catchErrors(done)); + db.clearAllAndLoad({ uploads: [{ _id: uploadId, filename: 'upload.filename' }] }).then(done).catch(catchErrors(done)); }); afterAll((done) => { @@ -41,5 +46,17 @@ describe('uploads', () => { }); }); }); + + describe('delete', () => { + fit('should delete the file', async () => { + fs.writeFileSync(path.join(uploadDocumentsPath, 'upload.filename')); + + expect(fs.existsSync(path.join(uploadDocumentsPath, 'upload.filename'))).toBe(true); + + await uploads.delete(uploadId); + + expect(fs.existsSync(path.join(uploadDocumentsPath, 'upload.filename'))).toBe(false); + }); + }); }); diff --git a/app/api/upload/uploads.js b/app/api/upload/uploads.js index bfa5902d75..75759bf684 100644 --- a/app/api/upload/uploads.js +++ b/app/api/upload/uploads.js @@ -1,6 +1,29 @@ +import fs from 'fs'; +import path from 'path'; + +import { uploadDocumentsPath } from '../config/paths'; import model from './uploadsModel'; +const deleteFile = filename => new Promise((resolve, reject) => { + fs.unlink(path.join(uploadDocumentsPath, filename), (err) => { + if (err) { + reject(err); + } + resolve(); + }); +}); + export default { save: model.save.bind(model), + get: model.get.bind(model), + + async delete(_id) { + const upload = await model.getById(_id); + + await model.delete(_id); + await deleteFile(upload.filename); + + return upload; + } }; diff --git a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap index 3953b2d691..e02c3f7503 100644 --- a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap +++ b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap @@ -10,7 +10,7 @@ exports[`CustomUploads should render CustomUploads component with uploaded files Custom Uploads
+
    +
  • + +
    + URL: +
    + + /uploaded_documents/file1 + +
    +
  • +
  • + +
    + URL: +
    + + /uploaded_documents/file2 + +
    +
  • +
-
    -
  • - - /uploaded_documents/file1 -
  • -
  • - - /uploaded_documents/file2 -
  • -
`; @@ -80,7 +100,7 @@ exports[`CustomUploads when upload on progress should render on progress feedbac Custom Uploads
+

+ +  Uploading ... +

+
    -

    - Uploading ... -

    -
      `; From ade9bc6d0dd3d4acb33b73ea5b1b2fca6e6629cd Mon Sep 17 00:00:00 2001 From: Daneryl Date: Tue, 26 Jun 2018 18:01:27 +0200 Subject: [PATCH 12/20] Dumb Confirm Modal Component --- .babelrc | 6 +- app/api/templates/routes.js | 4 +- app/react/Layout/ConfirmButton.js | 40 +++++ app/react/Layout/ConfirmModal.js | 41 +++++ app/react/Layout/index.js | 1 + app/react/Layout/specs/ConfirmButton.spec.js | 31 ++++ app/react/Layout/specs/ConfirmModal.spec.js | 34 ++++ .../__snapshots__/ConfirmButton.spec.js.snap | 24 +++ .../__snapshots__/ConfirmModal.spec.js.snap | 161 ++++++++++++++++++ .../Settings/components/CustomUploads.js | 8 +- 10 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 app/react/Layout/ConfirmButton.js create mode 100644 app/react/Layout/ConfirmModal.js create mode 100644 app/react/Layout/specs/ConfirmButton.spec.js create mode 100644 app/react/Layout/specs/ConfirmModal.spec.js create mode 100644 app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap create mode 100644 app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap diff --git a/.babelrc b/.babelrc index c7c4690971..56d2e77134 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,9 @@ { - "presets": ["env", "react"], + "presets": [["env", { + "targets": { + "node": "current" + } + }], "react"], "retainLines": "true", "env": { "production": { diff --git a/app/api/templates/routes.js b/app/api/templates/routes.js index 5942ec0b77..8f1f116879 100644 --- a/app/api/templates/routes.js +++ b/app/api/templates/routes.js @@ -3,13 +3,13 @@ import settings from 'api/settings'; import needsAuthorization from '../auth/authMiddleware'; export default app => { - app.post('/api/templates', needsAuthorization(), (req, res) => { + app.post('/api/templates', needsAuthorization(), (req, res, next) => { templates.save(req.body, req.language) .then((response) => { res.json(response); req.io.sockets.emit('templateChange', response); }) - .catch(res.error); + .catch(next); }); app.get('/api/templates', (req, res) => { diff --git a/app/react/Layout/ConfirmButton.js b/app/react/Layout/ConfirmButton.js new file mode 100644 index 0000000000..1083000879 --- /dev/null +++ b/app/react/Layout/ConfirmButton.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Confirm from '../App/Confirm'; + +class ConfirmButton extends Component { + constructor(props) { + super(props); + this.state = { + showModal: false + }; + this.openModal = this.openModal.bind(this); + } + + openModal() { + this.setState({ showModal: true }); + } + + render() { + return ( + + + {this.state.showModal && this.setState({ showModal: false })}/>} + + ); + } +} + +ConfirmButton.defaultProps = { + children: '' +}; + +ConfirmButton.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.string, + ]) +}; + +export default ConfirmButton; diff --git a/app/react/Layout/ConfirmModal.js b/app/react/Layout/ConfirmModal.js new file mode 100644 index 0000000000..2513be5201 --- /dev/null +++ b/app/react/Layout/ConfirmModal.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from './Modal'; + +class ConfirmModal extends Component { + render() { + return ( + + + +

      {this.props.title}

      +

      {this.props.message}

      +
      + + + + + + +
      + ); + } +} + +ConfirmModal.defaultProps = { + isOpen: true, + message: 'Are you sure you want to continue?', + title: 'Confirm action', + type: 'danger' +}; + +ConfirmModal.propTypes = { + isOpen: PropTypes.bool, + type: PropTypes.string, + message: PropTypes.string, + title: PropTypes.string, + onAccept: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + +export default ConfirmModal; diff --git a/app/react/Layout/index.js b/app/react/Layout/index.js index 55a840c0ac..ee9e1ed211 100644 --- a/app/react/Layout/index.js +++ b/app/react/Layout/index.js @@ -4,3 +4,4 @@ export { default as Icon } from './Icon'; export { default as SidePanel } from './SidePanel'; export { default as DocumentLanguage } from './DocumentLanguage'; export { default as Thumbnail } from './Thumbnail'; +export { default as ConfirmButton } from './ConfirmButton'; diff --git a/app/react/Layout/specs/ConfirmButton.spec.js b/app/react/Layout/specs/ConfirmButton.spec.js new file mode 100644 index 0000000000..d4dc421b2d --- /dev/null +++ b/app/react/Layout/specs/ConfirmButton.spec.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import { shallow } from 'enzyme'; + +import ConfirmButton from '../ConfirmButton'; + +describe('ConfirmButton', () => { + let component; + let props; + + beforeEach(() => { + props = {}; + }); + + const render = () => { + component = shallow(text); + }; + + it('should render a button', () => { + render(); + expect(component).toMatchSnapshot(); + }); + + describe('on click', () => { + it('should render a Confirm modal', () => { + component.find('button').simulate('click'); + component.update(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/react/Layout/specs/ConfirmModal.spec.js b/app/react/Layout/specs/ConfirmModal.spec.js new file mode 100644 index 0000000000..3c478d2e41 --- /dev/null +++ b/app/react/Layout/specs/ConfirmModal.spec.js @@ -0,0 +1,34 @@ +import React from 'react'; + +import { shallow } from 'enzyme'; + +import ConfirmModal from '../ConfirmModal'; + +describe('ConfirmModal', () => { + let component; + let props; + + beforeEach(() => { + props = { + onAccept: jasmine.createSpy('onAccept'), + onCancel: jasmine.createSpy('onCancel') + }; + }); + + const render = () => { + component = shallow(); + }; + + it('should render a confirm modal', () => { + render(); + expect(component).toMatchSnapshot(); + }); + + describe('when clicking on cancel/accept buttond', () => { + it('should call onCancel and onAccept', () => { + render(); + component.find('.cancel-button').simulate('click'); + expect(props.onCancel).toHaveBeenCalled(); + }); + }); +}); diff --git a/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap b/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap new file mode 100644 index 0000000000..46b7799999 --- /dev/null +++ b/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmButton on click should render a Confirm modal 1`] = ` + + + + +`; + +exports[`ConfirmButton should render a button 1`] = ` + + + +`; diff --git a/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap new file mode 100644 index 0000000000..fa095ec8d1 --- /dev/null +++ b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmModal should 1`] = ` + + +

      + Confirm action +

      +

      + Are you sure you want to continue? +

      + +
      + + +
      +
      +`; + +exports[`ConfirmModal should call onCancel and onAccept 1`] = ` + + +

      + Confirm action +

      +

      + Are you sure you want to continue? +

      + +
      + + +
      +
      +`; + +exports[`ConfirmModal should render a confirm modal 1`] = ` + + +

      + Confirm action +

      +

      + Are you sure you want to continue? +

      + +
      + + +
      +
      +`; + +exports[`ConfirmModal should render a confirm modal 2`] = ` + + +

      + Confirm action +

      +

      + Are you sure you want to continue? +

      + +
      + + +
      +
      +`; + +exports[`ConfirmModal when clicking on cancel/accept buttond should call onCancel and onAccept 1`] = ` + + +

      + Confirm action +

      +

      + Are you sure you want to continue? +

      + +
      + + +
      +
      +`; diff --git a/app/react/Settings/components/CustomUploads.js b/app/react/Settings/components/CustomUploads.js index 0a4b33c9a5..2eb4e96030 100644 --- a/app/react/Settings/components/CustomUploads.js +++ b/app/react/Settings/components/CustomUploads.js @@ -5,7 +5,7 @@ import Immutable from 'immutable'; import PropTypes from 'prop-types'; import React from 'react'; -import { Thumbnail } from 'app/Layout'; +import { Thumbnail, ConfirmButton } from 'app/Layout'; import { actions } from 'app/BasicReducer'; import { t } from 'app/I18N'; import RouteHandler from 'app/App/RouteHandler'; @@ -60,6 +60,7 @@ export class CustomUploads extends RouteHandler {
      URL:
      {`/uploaded_documents/${upload.get('filename')}`} + delete
      ))} @@ -74,6 +75,11 @@ CustomUploads.defaultProps = { progress: false }; +CustomUploads.contextTypes = { + confirm: PropTypes.func, + store: PropTypes.object +}; + CustomUploads.propTypes = { progress: PropTypes.bool, customUploads: PropTypes.instanceOf(Immutable.List).isRequired, From c843cfa816c6c1b4cdaa31a76f7da51a2022748c Mon Sep 17 00:00:00 2001 From: Daneryl Date: Wed, 27 Jun 2018 10:18:31 +0200 Subject: [PATCH 13/20] ConfirmButton now uses ConfirmModal component --- app/react/Layout/ConfirmButton.js | 32 ++++- app/react/Layout/ConfirmModal.js | 2 +- app/react/Layout/specs/ConfirmButton.spec.js | 32 ++++- app/react/Layout/specs/ConfirmModal.spec.js | 3 + .../__snapshots__/ConfirmButton.spec.js.snap | 27 +++- .../__snapshots__/ConfirmModal.spec.js.snap | 128 ------------------ 6 files changed, 90 insertions(+), 134 deletions(-) diff --git a/app/react/Layout/ConfirmButton.js b/app/react/Layout/ConfirmButton.js index 1083000879..22959123f0 100644 --- a/app/react/Layout/ConfirmButton.js +++ b/app/react/Layout/ConfirmButton.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Confirm from '../App/Confirm'; + +import ConfirmModal from './ConfirmModal'; class ConfirmButton extends Component { constructor(props) { @@ -9,6 +10,17 @@ class ConfirmButton extends Component { showModal: false }; this.openModal = this.openModal.bind(this); + this.onAccept = this.onAccept.bind(this); + this.closeModal = this.closeModal.bind(this); + } + + onAccept() { + this.closeModal(); + this.props.action(); + } + + closeModal() { + this.setState({ showModal: false }); } openModal() { @@ -19,17 +31,31 @@ class ConfirmButton extends Component { return ( - {this.state.showModal && this.setState({ showModal: false })}/>} + { + this.state.showModal && + + } ); } } ConfirmButton.defaultProps = { - children: '' + children: '', + message: 'Are you sure you want to continue?', + title: 'Confirm action', + action: () => false }; ConfirmButton.propTypes = { + action: PropTypes.func, + message: PropTypes.string, + title: PropTypes.string, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node, diff --git a/app/react/Layout/ConfirmModal.js b/app/react/Layout/ConfirmModal.js index 2513be5201..c4a73cd7c7 100644 --- a/app/react/Layout/ConfirmModal.js +++ b/app/react/Layout/ConfirmModal.js @@ -13,7 +13,7 @@ class ConfirmModal extends Component { - + diff --git a/app/react/Layout/specs/ConfirmButton.spec.js b/app/react/Layout/specs/ConfirmButton.spec.js index d4dc421b2d..f6448db3f3 100644 --- a/app/react/Layout/specs/ConfirmButton.spec.js +++ b/app/react/Layout/specs/ConfirmButton.spec.js @@ -4,12 +4,16 @@ import { shallow } from 'enzyme'; import ConfirmButton from '../ConfirmButton'; +import ConfirmModal from '../ConfirmModal'; + describe('ConfirmButton', () => { let component; let props; beforeEach(() => { - props = {}; + props = { + action: jasmine.createSpy('action') + }; }); const render = () => { @@ -23,9 +27,35 @@ describe('ConfirmButton', () => { describe('on click', () => { it('should render a Confirm modal', () => { + render(); component.find('button').simulate('click'); component.update(); expect(component).toMatchSnapshot(); }); + + describe('onAccept', () => { + it('should execute action and close modal', () => { + render(); + component.find('button').simulate('click'); + component.update(); + component.find(ConfirmModal).props().onAccept(); + component.update(); + + expect(component).toMatchSnapshot(); + expect(props.action).toHaveBeenCalled(); + }); + }); + + describe('onCancel', () => { + it('should should close modal', () => { + render(); + component.find('button').simulate('click'); + component.update(); + component.find(ConfirmModal).props().onCancel(); + component.update(); + + expect(component).toMatchSnapshot(); + }); + }); }); }); diff --git a/app/react/Layout/specs/ConfirmModal.spec.js b/app/react/Layout/specs/ConfirmModal.spec.js index 3c478d2e41..e4843ca1e0 100644 --- a/app/react/Layout/specs/ConfirmModal.spec.js +++ b/app/react/Layout/specs/ConfirmModal.spec.js @@ -29,6 +29,9 @@ describe('ConfirmModal', () => { render(); component.find('.cancel-button').simulate('click'); expect(props.onCancel).toHaveBeenCalled(); + + component.find('.confirm-button').simulate('click'); + expect(props.onAccept).toHaveBeenCalled(); }); }); }); diff --git a/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap b/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap index 46b7799999..4a3975ce19 100644 --- a/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap +++ b/app/react/Layout/specs/__snapshots__/ConfirmButton.spec.js.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`ConfirmButton on click onAccept should execute action and close modal 1`] = ` + + + +`; + +exports[`ConfirmButton on click onCancel should should close modal 1`] = ` + + + +`; + exports[`ConfirmButton on click should render a Confirm modal 1`] = ` - `; diff --git a/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap index fa095ec8d1..f6b06b83f0 100644 --- a/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap +++ b/app/react/Layout/specs/__snapshots__/ConfirmModal.spec.js.snap @@ -1,69 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConfirmModal should 1`] = ` - - -

      - Confirm action -

      -

      - Are you sure you want to continue? -

      - -
      - - -
      -
      -`; - -exports[`ConfirmModal should call onCancel and onAccept 1`] = ` - - -

      - Confirm action -

      -

      - Are you sure you want to continue? -

      - -
      - - -
      -
      -`; - exports[`ConfirmModal should render a confirm modal 1`] = ` `; - -exports[`ConfirmModal should render a confirm modal 2`] = ` - - -

      - Confirm action -

      -

      - Are you sure you want to continue? -

      - -
      - - -
      -
      -`; - -exports[`ConfirmModal when clicking on cancel/accept buttond should call onCancel and onAccept 1`] = ` - - -

      - Confirm action -

      -

      - Are you sure you want to continue? -

      - -
      - - -
      -
      -`; From 7603a518e99946b2f4b47baac6819b3562821eb6 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Wed, 27 Jun 2018 10:48:25 +0200 Subject: [PATCH 14/20] delete customUpload action --- .../__snapshots__/CustomUploads.spec.js.snap | 14 ++++++++++++ .../actions/specs/uploadsActions.spec.js | 22 ++++++++++++++++++- app/react/Uploads/actions/uploadsActions.js | 11 ++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap index e02c3f7503..a3779a0fd1 100644 --- a/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap +++ b/app/react/Settings/components/specs/__snapshots__/CustomUploads.spec.js.snap @@ -64,6 +64,13 @@ exports[`CustomUploads should render CustomUploads component with uploaded files > /uploaded_documents/file1 + + delete +
    • /uploaded_documents/file2 + + delete +
    diff --git a/app/react/Uploads/actions/specs/uploadsActions.spec.js b/app/react/Uploads/actions/specs/uploadsActions.spec.js index c255b75259..3f4fe80913 100644 --- a/app/react/Uploads/actions/specs/uploadsActions.spec.js +++ b/app/react/Uploads/actions/specs/uploadsActions.spec.js @@ -3,13 +3,15 @@ import superagent from 'superagent'; import thunk from 'redux-thunk'; import { APIURL } from 'app/config.js'; +import { actions as basicActions } from 'app/BasicReducer'; import { mockID } from 'shared/uniqueID.js'; import * as actions from 'app/Uploads/actions/uploadsActions'; import backend from 'fetch-mock'; import configureMockStore from 'redux-mock-store'; import * as notificationsTypes from 'app/Notifications/actions/actionTypes'; import * as types from 'app/Uploads/actions/actionTypes'; -import { actions as basicActions } from 'app/BasicReducer'; + +import api from '../../../utils/api'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -134,6 +136,24 @@ describe('uploadsActions', () => { }); }); + describe('deleteCustomUpload', () => { + it('should delete the upload and remove it locally on success', (done) => { + spyOn(api, 'delete').and.returnValue(Promise.resolve({ json: { _id: 'deleted' } })); + + const expectedActions = [ + basicActions.remove('customUploads', { _id: 'deleted' }) + ]; + + const store = mockStore({}); + + store.dispatch(actions.deleteCustomUpload('id')) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + done(); + }); + }); + }); + describe('publishDocument', () => { it('should save the document with published:true and dispatch notification on success', (done) => { const document = { name: 'doc', _id: 'abc1' }; diff --git a/app/react/Uploads/actions/uploadsActions.js b/app/react/Uploads/actions/uploadsActions.js index 7a376c03e9..8c25b7b86b 100644 --- a/app/react/Uploads/actions/uploadsActions.js +++ b/app/react/Uploads/actions/uploadsActions.js @@ -1,14 +1,14 @@ import superagent from 'superagent'; +import { actions as basicActions } from 'app/BasicReducer'; import { notify } from 'app/Notifications'; import { selectSingleDocument } from 'app/Library/actions/libraryActions'; import * as metadata from 'app/Metadata'; import * as types from 'app/Uploads/actions/actionTypes'; +import uniqueID from 'shared/uniqueID'; import { APIURL } from '../../config.js'; import api from '../../utils/api'; -import uniqueID from 'shared/uniqueID'; -import { actions as basicActions } from 'app/BasicReducer'; export function enterUploads() { return { @@ -61,6 +61,13 @@ export function uploadCustom(file) { }; } +export function deleteCustomUpload(_id) { + return dispatch => api.delete('/customisation/upload', { _id }) + .then((response) => { + dispatch(basicActions.remove('customUploads', response.json)); + }); +} + export function uploadDocument(docId, file) { return dispatch => upload(docId, file)(dispatch); } From 06d262d5da42c192edf6809ef4bb9dcc93db04a8 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Wed, 27 Jun 2018 11:00:35 +0200 Subject: [PATCH 15/20] fixed server tests --- app/api/templates/routes.js | 4 ++-- app/api/upload/routes.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/api/templates/routes.js b/app/api/templates/routes.js index 8f1f116879..5942ec0b77 100644 --- a/app/api/templates/routes.js +++ b/app/api/templates/routes.js @@ -3,13 +3,13 @@ import settings from 'api/settings'; import needsAuthorization from '../auth/authMiddleware'; export default app => { - app.post('/api/templates', needsAuthorization(), (req, res, next) => { + app.post('/api/templates', needsAuthorization(), (req, res) => { templates.save(req.body, req.language) .then((response) => { res.json(response); req.io.sockets.emit('templateChange', response); }) - .catch(next); + .catch(res.error); }); app.get('/api/templates', (req, res) => { diff --git a/app/api/upload/routes.js b/app/api/upload/routes.js index 1d57f10c72..5aa060968c 100644 --- a/app/api/upload/routes.js +++ b/app/api/upload/routes.js @@ -6,7 +6,6 @@ import entities from 'api/entities'; import fs from 'fs'; import languages from 'shared/languages'; import logger from 'shared/logger'; -import path from 'path'; import relationships from 'api/relationships'; import { uploadDocumentsPath } from '../config/paths'; From c13fe7fb6db4f34d30586a283ce47a56b9b8d1b4 Mon Sep 17 00:00:00 2001 From: Daneryl Date: Wed, 27 Jun 2018 11:07:03 +0200 Subject: [PATCH 16/20] fixed GoogleAnalytics components tests --- app/react/App/GoogleAnalytics.js | 14 +++++++------- app/react/App/specs/GoogleAnalytics.spec.js | 9 +++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/react/App/GoogleAnalytics.js b/app/react/App/GoogleAnalytics.js index 37ee5826e1..362c4b8dfb 100644 --- a/app/react/App/GoogleAnalytics.js +++ b/app/react/App/GoogleAnalytics.js @@ -1,7 +1,8 @@ +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import {isClient} from 'app/utils'; +import React, { Component } from 'react'; + +import { isClient } from 'app/utils'; export function trackPage() { if (isClient && window.ga) { @@ -10,7 +11,6 @@ export function trackPage() { } export class GoogleAnalytics extends Component { - constructor(props) { super(props); if (!props.analyticsTrackingId || !isClient) { @@ -27,15 +27,15 @@ export class GoogleAnalytics extends Component { if (!this.props.analyticsTrackingId) { return false; } - return ; + return