diff --git a/client/app/admin/admin.js b/client/app/admin/admin.js deleted file mode 100644 index d82e625..0000000 --- a/client/app/admin/admin.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -angular.module('manticoreApp') - .config(function ($stateProvider) { - $stateProvider - .state('admin', { - url: '/admin', - templateUrl: 'app/admin/admin.html', - controller: 'AdminCtrl' - }); - }); \ No newline at end of file diff --git a/client/app/admin/admin.styl b/client/app/admin/admin.styl deleted file mode 100644 index d57e50d..0000000 --- a/client/app/admin/admin.styl +++ /dev/null @@ -1,2 +0,0 @@ -.trash - color rgb(209, 91, 71) \ No newline at end of file diff --git a/client/app/app.styl b/client/app/app.styl index e170c9c..1293ce7 100644 --- a/client/app/app.styl +++ b/client/app/app.styl @@ -1,56 +1,61 @@ @import "font-awesome/css/font-awesome.css" @import "bootstrap/dist/css/bootstrap.css" // // Bootstrap Fonts // @font-face font-family: 'Glyphicons Halflings' src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot') src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); // // Font Awesome Fonts // @font-face font-family: 'FontAwesome' src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0') src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); font-weight: normal font-style: normal // // App-wide Styles // .browsehappy background #ccc color #000 margin 0.2em 0 padding 0.2em 0 body background-color whitesmoke +.text-strong + font-weight bold + // Component styles are injected through grunt // injector @import 'account/login/login.styl'; -@import 'admin/admin.styl'; @import 'editor/editor.styl'; @import 'main/main.styl'; +@import 'templates/templates.styl'; +@import 'users/users.styl'; @import 'documentList/documentList.styl'; @import 'exportButton/exportButton.styl'; @import 'import/import.styl'; +@import 'labelEditor/labelEditor.styl'; @import 'modal/modal.styl'; @import 'navbar/navbar.styl'; @import 'titleEditor/titleEditor.styl'; @import 'wodo/editor.styl'; // endinjector diff --git a/client/app/templates/templates.controller.js b/client/app/templates/templates.controller.js new file mode 100644 index 0000000..8ba03af --- /dev/null +++ b/client/app/templates/templates.controller.js @@ -0,0 +1,43 @@ +'use strict'; + +angular.module('manticoreApp') + .controller('TemplatesCtrl', function ($scope, $http, Auth, FileUploader) { + var uploader = new FileUploader({ + url: '/api/templates/upload', + headers: { + 'Authorization': 'Bearer ' + Auth.getToken() + }, + removeAfterUpload: true, + autoUpload: true, + onCompleteAll: function () { + // Wait a little before firing this event, as the upload may not + // be accessible from MongoDB immediately + window.setTimeout(function () { + uploader.clearQueue(); + refresh(); + }, 1000); + } + }); + + $scope.uploader = uploader; + + $scope.update = function (template) { + $http.put('/api/templates/' + template._id, template); + }; + $scope.delete = function(template) { + $http.delete('/api/templates/' + template._id); + angular.forEach($scope.templates, function(t, i) { + if (t === template) { + $scope.templates.splice(i, 1); + } + }); + }; + + $scope.$watch('templates') + function refresh() { + $http.get('/api/templates').success(function (templates) { + $scope.templates = templates; + }); + } + refresh(); + }); diff --git a/client/app/templates/templates.jade b/client/app/templates/templates.jade new file mode 100644 index 0000000..7d96fdc --- /dev/null +++ b/client/app/templates/templates.jade @@ -0,0 +1,31 @@ +div(ng-include='"components/navbar/navbar.html"') +.container + div.btn.btn-danger.upload-button( + type='button' + uploader='uploader' + ng-disabled='uploader.isUploading' + ) + input( + type='file' + accept='.odt' + nv-file-select='' + uploader='uploader' + ) + span.glyphicon.glyphicon-upload + | {{uploader.isUploading ? 'Uploading...' : 'Upload'}} + + ul.list-group + li.list-group-item(ng-repeat='template in templates') + label-editor.text-strong( + ng-model='template.title' + ng-change='update(template)' + placeholder='Untitled template' + ) + br + label-editor.text-muted( + ng-model='template.description' + ng-change='update(template)' + placeholder='No description' + ) + a.trash(ng-click='delete(template)') + span.glyphicon.glyphicon-trash.pull-right diff --git a/client/app/templates/templates.js b/client/app/templates/templates.js new file mode 100644 index 0000000..6572cd3 --- /dev/null +++ b/client/app/templates/templates.js @@ -0,0 +1,11 @@ +'use strict'; + +angular.module('manticoreApp') + .config(function ($stateProvider) { + $stateProvider + .state('templates', { + url: '/templates', + templateUrl: 'app/templates/templates.html', + controller: 'TemplatesCtrl' + }); + }); \ No newline at end of file diff --git a/client/app/templates/templates.styl b/client/app/templates/templates.styl new file mode 100644 index 0000000..fea8563 --- /dev/null +++ b/client/app/templates/templates.styl @@ -0,0 +1,18 @@ +.upload-button + position relative + overflow hidden + display block + width 100px + margin-left auto + margin-right auto + margin-bottom 15px + input[type=file] + opacity 0; + position absolute + top 0 + bottom 0 + outline none + cursor inherit + display block + width 100% + height: 100% diff --git a/client/app/admin/admin.controller.js b/client/app/users/users.controller.js similarity index 84% rename from client/app/admin/admin.controller.js rename to client/app/users/users.controller.js index b2d24af..31fec48 100644 --- a/client/app/admin/admin.controller.js +++ b/client/app/users/users.controller.js @@ -1,17 +1,17 @@ 'use strict'; angular.module('manticoreApp') - .controller('AdminCtrl', function ($scope, $http, Auth, User) { + .controller('UsersCtrl', function ($scope, $http, Auth, User) { // Use the User $resource to fetch all users $scope.users = User.query(); $scope.delete = function(user) { User.remove({ id: user._id }); angular.forEach($scope.users, function(u, i) { if (u === user) { $scope.users.splice(i, 1); } }); }; }); diff --git a/client/app/admin/admin.jade b/client/app/users/users.jade similarity index 61% rename from client/app/admin/admin.jade rename to client/app/users/users.jade index fd80a0b..2b18af8 100644 --- a/client/app/admin/admin.jade +++ b/client/app/users/users.jade @@ -1,11 +1,9 @@ div(ng-include='"components/navbar/navbar.html"') .container - p - | The delete user and user index api routes are restricted to users with the 'admin' role. ul.list-group li.list-group-item(ng-repeat='user in users') strong {{user.name}} br span.text-muted {{user.email}} a.trash(ng-click='delete(user)') - span.glyphicon.glyphicon-trash.pull-right \ No newline at end of file + span.glyphicon.glyphicon-trash.pull-right diff --git a/client/app/users/users.js b/client/app/users/users.js new file mode 100644 index 0000000..8564f3e --- /dev/null +++ b/client/app/users/users.js @@ -0,0 +1,11 @@ +'use strict'; + +angular.module('manticoreApp') + .config(function ($stateProvider) { + $stateProvider + .state('users', { + url: '/users', + templateUrl: 'app/users/users.html', + controller: 'UsersCtrl' + }); + }); diff --git a/client/app/users/users.styl b/client/app/users/users.styl new file mode 100644 index 0000000..cdc0c97 --- /dev/null +++ b/client/app/users/users.styl @@ -0,0 +1,3 @@ +.trash + color rgb(209, 91, 71) + cursor pointer diff --git a/client/components/import/import.jade b/client/components/import/import.jade index bb34755..0a65d64 100644 --- a/client/components/import/import.jade +++ b/client/components/import/import.jade @@ -1,80 +1,80 @@ div.import-box(ng-controller='ImportCtrl') .row.upload-widget - div.upload-button( + div.upload-area( type='button' nv-file-over='' uploader='uploader' ) input( type='file' accept='.odt' nv-file-select='' uploader='uploader' multiple ) | Click to select (.odt) files to upload, or drag them here .row(ng-hide='!uploader.queue.length') table.table thead tr th(width='50%') Name th(ng-show='uploader.isHTML5') Size th(ng-show='uploader.isHTML5') Progress th.status Status th.actions Actions tbody tr(ng-repeat='item in uploader.queue') td.name-cell span(tooltip='{{item.file.name}}' data-container='body') | {{item.file.name}} td(ng-show='uploader.isHTML5' nowrap) | {{item.file.size/1024/1024|number:2}} MB td(ng-show='uploader.isHTML5') progressbar(value='item.progress') {{item.progress}}% td.status.text-center span(ng-show='item.isSuccess') i.glyphicon.glyphicon-ok span(ng-show='item.isCancel') i.glyphicon.glyphicon-ban-circle span(ng-show='item.isError') i.glyphicon.glyphicon-remove td.actions(nowrap) button.btn.btn-success.btn-xs( ng-click='item.upload()' ng-disabled='item.isReady || item.isUploading || item.isSuccess' ) i.glyphicon.glyphicon-upload button.btn.btn-warning.btn-xs( ng-click='item.cancel()' ng-disabled='!item.isUploading' ) i.glyphicon.glyphicon-ban-circle button.btn.btn-danger.btn-xs( ng-click='item.remove()' ) i.glyphicon.glyphicon-trash //- div.progress-overall progressbar(value='uploader.progress') {{uploader.progress}}% div.btn-group.btn-group-justified div.btn.btn-success.btn-s( type='button' ng-click='uploader.uploadAll()' ng-disabled='!uploader.getNotUploadedItems().length' ) span.glyphicon.glyphicon-upload | Upload all div.btn.btn-warning.btn-s( type='button' ng-click='uploader.cancelAll()' ng-disabled='!uploader.isUploading' ) span.glyphicon.glyphicon-ban-circle | Cancel all div.btn.btn-danger.btn-s( type='button' ng-click='uploader.removeAll()' ng-disabled='!uploader.queue.length' ) span.glyphicon.glyphicon-trash | Remove all diff --git a/client/components/import/import.styl b/client/components/import/import.styl index 993713e..dc310b2 100644 --- a/client/components/import/import.styl +++ b/client/components/import/import.styl @@ -1,55 +1,55 @@ .import-box width 400px .row margin: 0 .upload-widget margin-left 5px !important margin-right 5px !important -.upload-button +.upload-area width 100% position relative overflow hidden padding 20px border 2px dashed lightgray border-radius 5px cursor pointer text-align center background-color #eee input[type=file] opacity 0; position absolute top 0 bottom 0 outline none cursor inherit display block width 100% height: 100% &:hover border 2px dashed red background-color #ddd td .progress margin-bottom 0 .progress-overall progress padding-left 8px padding-right 8px th, td border-bottom-width 1px !important .table .actions display none .status display none .name-cell max-width 0 word-wrap break-word text-overflow ellipsis overflow hidden white-space nowrap diff --git a/client/components/labelEditor/labelEditor.directive.js b/client/components/labelEditor/labelEditor.directive.js new file mode 100644 index 0000000..463a63b --- /dev/null +++ b/client/components/labelEditor/labelEditor.directive.js @@ -0,0 +1,25 @@ +'use strict'; + +angular.module('manticoreApp') +.directive('labelEditor', function ($timeout) { + return { + templateUrl: 'components/labelEditor/labelEditor.html', + restrict: 'E', + scope: { + model: '=ngModel' + }, + link: function (scope, element, attrs) { + scope.placeholder = attrs.placeholder; + scope.handleEnterKey = function ($event) { + if ($event.keyCode === 13) { + $event.target.blur(); + } + }; + scope.change = function () { + $timeout(function () { + scope.$parent.$eval(attrs.ngChange); + }); + }; + } + }; +}); diff --git a/client/components/labelEditor/labelEditor.jade b/client/components/labelEditor/labelEditor.jade new file mode 100644 index 0000000..f8927d8 --- /dev/null +++ b/client/components/labelEditor/labelEditor.jade @@ -0,0 +1,12 @@ +input.label-editor( + ng-model='model' + ng-model-options='{updateOn: "blur"}' + ng-change='change(model)' + ng-keypress='handleEnterKey($event)' + ng-mouseenter='hover = true' + ng-mouseleave='hover = false' + ng-focus='focus = true' + ng-blur='focus = false' + placeholder='{{placeholder}}' +) +span.glyphicon.glyphicon-pencil.label-editor-pencil(ng-show='hover && !focus') diff --git a/client/components/labelEditor/labelEditor.styl b/client/components/labelEditor/labelEditor.styl new file mode 100644 index 0000000..4ba93ed --- /dev/null +++ b/client/components/labelEditor/labelEditor.styl @@ -0,0 +1,17 @@ +.label-editor + border none + outline none + text-overflow ellipsis + padding-left 5px + margin-left -5px + width 100% + max-width calc(100% - 50px) + &:hover + background-color #eeeeee + &:focus + background-color #eeeeee + width 100% + +.label-editor-pencil + margin-left 5px + color gray diff --git a/client/components/navbar/navbar.jade b/client/components/navbar/navbar.jade index 3fe98cb..9db98be 100644 --- a/client/components/navbar/navbar.jade +++ b/client/components/navbar/navbar.jade @@ -1,37 +1,40 @@ div.navbar.navbar-default.navbar-static-top(ng-controller='NavbarCtrl') div.container div.navbar-header button.navbar-toggle(type='button', ng-click='isCollapsed = !isCollapsed') span.sr-only Toggle navigation span.icon-bar span.icon-bar span.icon-bar a.navbar-brand(href='/') manticore div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed') ul.nav.navbar-nav - li(ng-show='isAdmin()', ng-class='{active: isActive("/admin")}') - a(href='/admin') Admin + li(ng-show='isAdmin()', ng-class='{active: isActive("/users")}') + a(ui-sref='users') Users + + li(ng-show='isAdmin()', ng-class='{active: isActive("/templates")}') + a(ui-sref='templates') Templates li.dropdown(ng-show='isLoggedIn() && isActive("/")' dropdown auto-close='disabled') a.dropdown-toggle(href='#' dropdown-toggle role='button') | Import span.caret div.dropdown-menu(ng-include='"components/import/import.html"') ul.nav.navbar-nav.navbar-right li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/signup")}') a(href='/signup') Sign up li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}') a(href='/login') Login li(ng-show='isLoggedIn()') p.navbar-text Hello {{ getCurrentUser().name }} li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}') a(href='/settings') span.glyphicon.glyphicon-cog li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}') a(href='', ng-click='logout()') Logout diff --git a/client/index.html b/client/index.html index 5c637a0..2808f8e 100644 --- a/client/index.html +++ b/client/index.html @@ -1,87 +1,90 @@
- - + + + + + diff --git a/server/api/template/index.js b/server/api/template/index.js new file mode 100644 index 0000000..ffdd890 --- /dev/null +++ b/server/api/template/index.js @@ -0,0 +1,18 @@ +'use strict'; + +var express = require('express'); +var controller = require('./template.controller'); +var auth = require('../../auth/auth.service'); + +var router = express.Router(); + +router.get('/', auth.isAuthenticated(), controller.index); +router.get('/:id', auth.isAuthenticated(), controller.show); +router.post('/', auth.hasRole('admin'), controller.create); +router.post('/upload', auth.hasRole('admin'), controller.upload, + controller.acknowledgeUpload); +router.put('/:id', auth.hasRole('admin'), controller.update); +router.patch('/:id', auth.hasRole('admin'), controller.update); +router.delete('/:id', auth.hasRole('admin'), controller.destroy); + +module.exports = router; diff --git a/server/api/template/template.controller.js b/server/api/template/template.controller.js new file mode 100644 index 0000000..acf336e --- /dev/null +++ b/server/api/template/template.controller.js @@ -0,0 +1,103 @@ +'use strict'; + +var _ = require('lodash'); +var mongoose = require('mongoose'); +var Grid = require('gridfs-stream'); +var multer = require('multer'); + +var Template = require('./template.model'); + +var gfs = Grid(mongoose.connection.db, mongoose.mongo); + +// Get list of templates +exports.index = function(req, res) { + Template.find(function (err, templates) { + if(err) { return handleError(res, err); } + return res.json(200, templates); + }); +}; + +// Get a single template +exports.show = function(req, res) { + Template.findById(req.params.id, function (err, template) { + if(err) { return handleError(res, err); } + if(!template) { return res.send(404); } + return res.json(template); + }); +}; + +exports.upload = function (req, res, next) { + multer({ + upload: null, + limits: { + fileSize: 1024 * 1024 * 20, // 20 Megabytes + }, + onFileUploadStart: function (file) { + var templateId = new mongoose.Types.ObjectId(); + + var newTemplate = new Template({ + _id: templateId, + title: file.originalname + }); + this.upload = gfs.createWriteStream({ + _id: templateId, + filename: file.originalname, + mode: 'w', + chunkSize: 1024 * 4, + content_type: file.mimetype, + root: 'fs' + }); + this.upload.on('finish', function () { + newTemplate.save(); + }); + }, + onFileUploadData: function (file, data) { + this.upload.write(data); + }, + onFileUploadComplete: function (file) { + this.upload.end(); + } + })(req, res, next); +}; + +exports.acknowledgeUpload = function (req, res) { + return res.send(200); +}; + +// Creates a new template in the DB. +exports.create = function(req, res) { + Template.create(req.body, function(err, template) { + if(err) { return handleError(res, err); } + return res.json(201, template); + }); +}; + +// Updates an existing template in the DB. +exports.update = function(req, res) { + if(req.body._id) { delete req.body._id; } + Template.findById(req.params.id, function (err, template) { + if (err) { return handleError(res, err); } + if(!template) { return res.send(404); } + var updated = _.merge(template, req.body); + updated.save(function (err) { + if (err) { return handleError(res, err); } + return res.json(200, template); + }); + }); +}; + +// Deletes a template from the DB. +exports.destroy = function(req, res) { + Template.findById(req.params.id, function (err, template) { + if(err) { return handleError(res, err); } + if(!template) { return res.send(404); } + template.remove(function(err) { + if(err) { return handleError(res, err); } + return res.send(204); + }); + }); +}; + +function handleError(res, err) { + return res.send(500, err); +} diff --git a/server/api/template/template.model.js b/server/api/template/template.model.js new file mode 100644 index 0000000..47ef90d --- /dev/null +++ b/server/api/template/template.model.js @@ -0,0 +1,11 @@ +'use strict'; + +var mongoose = require('mongoose'), + Schema = mongoose.Schema; + +var TemplateSchema = new Schema({ + title: String, + description: String + }); + +module.exports = mongoose.model('Template', TemplateSchema); diff --git a/server/api/template/template.spec.js b/server/api/template/template.spec.js new file mode 100644 index 0000000..417fd69 --- /dev/null +++ b/server/api/template/template.spec.js @@ -0,0 +1,20 @@ +'use strict'; + +var should = require('should'); +var app = require('../../app'); +var request = require('supertest'); + +describe('GET /api/templates', function() { + + it('should respond with JSON array', function(done) { + request(app) + .get('/api/templates') + .expect(200) + .expect('Content-Type', /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.be.instanceof(Array); + done(); + }); + }); +}); \ No newline at end of file diff --git a/server/routes.js b/server/routes.js index fd38254..f5cb747 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,26 +1,27 @@ /** * Main application routes */ 'use strict'; var errors = require('./components/errors'); module.exports = function(app) { // Insert routes below + app.use('/api/templates', require('./api/template')); app.use('/api/documents', require('./api/document')); app.use('/api/users', require('./api/user')); app.use('/auth', require('./auth')); // All undefined asset or api routes should return a 404 app.route('/:url(api|auth|components|app|bower_components|assets)/*') .get(errors[404]); // All other routes should redirect to the index.html app.route('/*') .get(function(req, res) { res.sendfile(app.get('appPath') + '/index.html'); }); };