diff --git a/bower.json b/bower.json index 369998a..dba0329 100644 --- a/bower.json +++ b/bower.json @@ -1,28 +1,30 @@ { "name": "manticore", "version": "0.0.0", "dependencies": { "angular": "1.4.1", "json3": "3.3.1", "es5-shim": "4.1.7", "jquery": "1.11.0", "bootstrap": "~3.1.1", "angular-resource": "1.4.1", "angular-cookies": "1.4.1", "angular-sanitize": "1.4.1", "angular-bootstrap": "0.13.0", "font-awesome": ">=4.1.0", "lodash": "3.9.3", "angular-ui-router": "0.2.15", "angular-file-upload": "1.1.5", "wodo": "http://webodf.org/download/wodocollabtexteditor-0.5.8.preview1.zip", - "angular-file-saver": "~0.0.4" + "angular-file-saver": "~0.0.4", + "angular-smart-table": "~2.1.0", + "angular-moment": "~0.10.1" }, "devDependencies": { "angular-mocks": "1.4.1", "angular-scenario": "1.4.1" }, "resolutions": { "angular": "1.4.1" } } diff --git a/client/app/app.js b/client/app/app.js index 5149bbb..8424b71 100644 --- a/client/app/app.js +++ b/client/app/app.js @@ -1,55 +1,57 @@ 'use strict'; angular.module('manticoreApp', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'ui.bootstrap', 'angularFileUpload', - 'fileSaver' + 'fileSaver', + 'smart-table', + 'angularMoment' ]) .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { $urlRouterProvider .otherwise('/'); $locationProvider.html5Mode(true); $httpProvider.interceptors.push('authInterceptor'); }) .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) { return { // Add authorization token to headers request: function (config) { config.headers = config.headers || {}; if ($cookieStore.get('token')) { config.headers.Authorization = 'Bearer ' + $cookieStore.get('token'); } return config; }, // Intercept 401s and redirect you to login responseError: function(response) { if(response.status === 401) { $location.path('/login'); // remove any stale tokens $cookieStore.remove('token'); return $q.reject(response); } else { return $q.reject(response); } } }; }) .run(function ($rootScope, $location, Auth) { // Redirect to login if route requires auth and you're not logged in $rootScope.$on('$stateChangeStart', function (event, next) { Auth.isLoggedInAsync(function(loggedIn) { if (next.authenticate && !loggedIn) { $location.path('/login'); } }); }); }); diff --git a/client/app/app.styl b/client/app/app.styl index 3337e56..e170c9c 100644 --- a/client/app/app.styl +++ b/client/app/app.styl @@ -1,51 +1,56 @@ @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 + // 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 'documentList/documentList.styl'; @import 'exportButton/exportButton.styl'; @import 'import/import.styl'; @import 'modal/modal.styl'; +@import 'navbar/navbar.styl'; @import 'titleEditor/titleEditor.styl'; @import 'wodo/editor.styl'; // endinjector diff --git a/client/app/editor/editor.styl b/client/app/editor/editor.styl index e6c6f8b..f85ee8e 100644 --- a/client/app/editor/editor.styl +++ b/client/app/editor/editor.styl @@ -1,13 +1,17 @@ .toolbar border none + background-color white + height 50px + .toolbar-item - display: inline-block; - height: 50px; - float: left; + display inline-block + height 50px + float left #wodoContainer width 100% height calc(100% - 50px) top 50px padding 0 position absolute + background-color white diff --git a/client/app/main/main.controller.js b/client/app/main/main.controller.js index f38ffcd..726afbc 100644 --- a/client/app/main/main.controller.js +++ b/client/app/main/main.controller.js @@ -1,26 +1,6 @@ 'use strict'; angular.module('manticoreApp') .controller('MainCtrl', function ($scope, $http, Auth) { - $scope.documents = []; $scope.isLoggedIn = Auth.isLoggedIn; - - if ($scope.isLoggedIn()) { - $http.get('/api/documents').success(function(documents) { - $scope.documents = documents; - }); - } - - $scope.openDocument = function (document) { - $http.get('/api/documents/snapshot/' + _.last(document.chunks)) - .success(function (data) { - - }).error(function (data) { - console.log(data); - }) - }; - - $scope.deleteDocument = function(document) { - $http.delete('/api/documents/' + document._id); - }; }); diff --git a/client/app/main/main.jade b/client/app/main/main.jade index 485cf88..b328ec9 100644 --- a/client/app/main/main.jade +++ b/client/app/main/main.jade @@ -1,23 +1,16 @@ div(ng-include='"components/navbar/navbar.html"') -header#banner.hero-unit(ng-hide='isLoggedIn() ') +header#banner.hero-unit(ng-if='!isLoggedIn()') .container h1 Manticore p.lead Realtime collaboration for rich office documents. img(src='assets/images/manticore.jpg', alt='Manticore') -.container(ng-hide='!isLoggedIn()') - .row - .col-lg-12 - h1.page-header Documents - ul.col-md-4.col-lg-4.col-sm-6(ng-repeat='document in documents') - li - a(ui-sref='editor({id: document._id})') - | {{document.title}} +document-list(ng-if='isLoggedIn()') //- footer.footer .container p | Manticore = ' | ' a(href='https://github.com/adityab/Manticure/issues?state=open') Issues diff --git a/client/components/documentList/documentList.controller.js b/client/components/documentList/documentList.controller.js new file mode 100644 index 0000000..c429359 --- /dev/null +++ b/client/components/documentList/documentList.controller.js @@ -0,0 +1,9 @@ +'use strict'; + +angular.module('manticoreApp') +.controller('DocumentListCtrl', function ($scope, $http) { + $scope.displayedDocuments = []; + $http.get('/api/documents').success(function (documents) { + $scope.documents = documents; + }); +}); diff --git a/client/components/documentList/documentList.controller.spec.js b/client/components/documentList/documentList.controller.spec.js new file mode 100644 index 0000000..e41d6e3 --- /dev/null +++ b/client/components/documentList/documentList.controller.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('Controller: DocumentListCtrl', function () { + + // load the controller's module + beforeEach(module('manticoreApp')); + + var DocumentListCtrl, scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + DocumentListCtrl = $controller('DocumentListCtrl', { + $scope: scope + }); + })); + + it('should ...', function () { + expect(1).toEqual(1); + }); +}); diff --git a/client/components/documentList/documentList.directive.js b/client/components/documentList/documentList.directive.js new file mode 100644 index 0000000..045c196 --- /dev/null +++ b/client/components/documentList/documentList.directive.js @@ -0,0 +1,10 @@ +'use strict'; + +angular.module('manticoreApp') + .directive('documentList', function () { + return { + templateUrl: 'components/documentList/documentList.html', + restrict: 'E', + controller: 'DocumentListCtrl' + }; + }); diff --git a/client/components/documentList/documentList.jade b/client/components/documentList/documentList.jade new file mode 100644 index 0000000..342b4bf --- /dev/null +++ b/client/components/documentList/documentList.jade @@ -0,0 +1,16 @@ +.container + .no-docs(ng-if='!documents.length') + p.advice No documents yet. Why not add some? + .document-list(ng-if='documents.length') + table.table.table-striped(st-table='displayedDocuments' st-safe-src='documents') + thead + tr + th Title + th Creator + th(st-sort='date' st-sort-default='reverse') Updated + tbody + tr(ng-repeat='document in displayedDocuments') + td.title + a(ui-sref='editor({id: document._id})' target='_blank') {{document.title}} + td {{document.creator.name}} + td {{document.date | amCalendar}} diff --git a/client/components/documentList/documentList.styl b/client/components/documentList/documentList.styl new file mode 100644 index 0000000..0beedd4 --- /dev/null +++ b/client/components/documentList/documentList.styl @@ -0,0 +1,22 @@ +.advice + text-align center +.document-list + height 100% + padding 0 + table + margin 0 + thead + background-color none + tr + th + padding 12px + tbody + border 1px solid lightgray + tr + td + color gray + background-color white !important + padding 12px + &.title a + font-weight bold + color black !important diff --git a/client/components/navbar/navbar.styl b/client/components/navbar/navbar.styl new file mode 100644 index 0000000..d6bd3cc --- /dev/null +++ b/client/components/navbar/navbar.styl @@ -0,0 +1,8 @@ +.navbar-default + background-color maroon +.navbar + & p, & a + color white !important + & li.active, & li.open + a + background-color #4C1313 !important diff --git a/client/index.html b/client/index.html index 8782027..1a25fcc 100644 --- a/client/index.html +++ b/client/index.html @@ -1,82 +1,87 @@
+ + + + + diff --git a/server/api/document/document.controller.js b/server/api/document/document.controller.js index 58c6eca..2ac0c58 100644 --- a/server/api/document/document.controller.js +++ b/server/api/document/document.controller.js @@ -1,141 +1,141 @@ /** * Using Rails-like standard naming convention for endpoints. * GET /documents -> index * POST /documents -> create * GET /documents/:id -> show * PUT /documents/:id -> update * DELETE /documents/:id -> destroy */ 'use strict'; var _ = require('lodash'); var mongoose = require('mongoose'); var Grid = require('gridfs-stream'); var multer = require('multer'); var DocumentChunk = require('./document.model').DocumentChunk; var Document = require('./document.model').Document; var gfs = Grid(mongoose.connection.db, mongoose.mongo); // Get list of documents exports.index = function(req, res) { - Document.find(function (err, documents) { + Document.find().populate('creator').exec(function (err, documents) { if(err) { return handleError(res, err); } return res.json(200, documents); }); }; // Get a single document exports.show = function(req, res) { Document.findById(req.params.id, function (err, document) { if(err) { return handleError(res, err); } if(!document) { return res.send(404); } return res.json(document); }); }; exports.upload = function (req, res, next) { multer({ upload: null, limits: { fileSize: 1024 * 1024 * 20, // 20 Megabytes files: 5 }, onFileUploadStart: function (file) { var chunkId = new mongoose.Types.ObjectId(); var firstChunk = new DocumentChunk({ _id: chunkId }); var newDocument = new Document({ title: file.originalname, creator: req.user._id, chunks: [chunkId] }); this.upload = gfs.createWriteStream({ _id: chunkId, filename: file.originalname, mode: 'w', chunkSize: 1024 * 4, content_type: file.mimetype, root: 'fs' }); this.upload.on('finish', function () { firstChunk.save(function (err) { if (!err) { newDocument.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); }; exports.showSnapshot = function(req, res) { var snapshotId = req.params.id; gfs.findOne({_id: snapshotId}, function (err, file) { if (err) { return handleError(res, err); } if (!file) { return res.send(404); } var download = gfs.createReadStream({ _id: snapshotId }); download.on('error', function (err) { return handleError(res, err); }); res.set('Content-Type', file.contentType); res.attachment(file.filename) download.pipe(res); }); }; // Creates a new document in the DB. exports.create = function(req, res) { Document.create(req.body, function(err, document) { if(err) { return handleError(res, err); } return res.json(201, document); }); }; // Updates an existing document in the DB. exports.update = function(req, res) { if(req.body._id) { delete req.body._id; } Document.findById(req.params.id, function (err, document) { if (err) { return handleError(res, err); } if(!document) { return res.send(404); } var updated = _.merge(document, req.body); updated.save(function (err) { if (err) { return handleError(res, err); } return res.json(200, document); }); }); }; // Deletes a document from the DB. exports.destroy = function(req, res) { Document.findById(req.params.id, function (err, document) { if(err) { return handleError(res, err); } if(!document) { return res.send(404); } document.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/document/document.model.js b/server/api/document/document.model.js index 1415c9c..3b7c7a3 100644 --- a/server/api/document/document.model.js +++ b/server/api/document/document.model.js @@ -1,25 +1,26 @@ 'use strict'; var mongoose = require('mongoose'), Schema = mongoose.Schema; /* * Each Document Chunk has an associated ODF snapshot file within * GridFS of the same ID. */ var DocumentChunk = new Schema({ operations: { type: Array, default: [] } }); var DocumentSchema = new Schema({ title: String, created: { type: Date, default: Date.now, required: true }, + date: { type: Date, default: Date.now }, creator: { type: Schema.Types.ObjectId, ref: 'User' }, editors: { type: [{type: Schema.Types.ObjectId, ref: 'User'}], default: [] }, chunks: { type: [{type: Schema.Types.ObjectId, ref: 'DocumentChunk'}], required: true } }); module.exports = { DocumentChunk: mongoose.model('DocumentChunk', DocumentChunk), Document: mongoose.model('Document', DocumentSchema) };