diff --git a/client/app/account/account.js b/client/app/account/account.js index defbb45..0fed4c4 100644 --- a/client/app/account/account.js +++ b/client/app/account/account.js @@ -1,22 +1,22 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider - .state('login', { + .state('manticore.login', { url: '/login', templateUrl: 'app/account/login/login.html', controller: 'LoginCtrl' }) - .state('signup', { + .state('manticore.signup', { url: '/signup', templateUrl: 'app/account/signup/signup.html', controller: 'SignupCtrl' }) - .state('settings', { + .state('manticore.settings', { url: '/settings', templateUrl: 'app/account/settings/settings.html', controller: 'SettingsCtrl', authenticate: true }); - }); \ No newline at end of file + }); diff --git a/client/app/app.js b/client/app/app.js index 0eee4ed..a46089a 100644 --- a/client/app/app.js +++ b/client/app/app.js @@ -1,58 +1,58 @@ 'use strict'; angular.module('manticoreApp', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'ui.bootstrap', 'angularFileUpload', 'fileSaver', 'smart-table', 'angularMoment', 'angularLoad' ]) - .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { + .config(function ($stateProvider, $urlRouterProvider, $urlMatcherFactoryProvider, $locationProvider, $httpProvider) { $urlRouterProvider .otherwise('/'); - + $urlMatcherFactoryProvider.strictMode(false); $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/editor/editor.js b/client/app/editor/editor.js index 2503631..c6a67b6 100644 --- a/client/app/editor/editor.js +++ b/client/app/editor/editor.js @@ -1,41 +1,41 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider - .state('editor', { + .state('manticore.editor', { abstract: true, url: '/document', reload: true, template: '' }) - .state('editor.forDocument', { + .state('manticore.editor.forDocument', { url: '/:id', resolve: { socketio: function (angularLoad) { return angularLoad.loadScript('socket.io/socket.io.js'); }, document: function ($stateParams, $http) { return $http.get('/api/documents/' + $stateParams.id) .then(function(response) { return response.data; }); } }, templateUrl: 'app/editor/editor.html', controller: function ($scope, document) { $scope.document = document; } }) - .state('editor.fromTemplate', { + .state('manticore.editor.fromTemplate', { url: '/:id/new', resolve: { document: function ($stateParams, $state, $http) { return $http.get('/api/documents/fromTemplate/' + $stateParams.id) .then(function (response) { - $state.go('editor.forDocument', { id: response.data._id }, { location: 'replace' }); + $state.go('manticore.editor.forDocument', { id: response.data._id }, { location: 'replace' }); }); } } }); }); diff --git a/client/app/main/main.js b/client/app/main/main.js index 392014e..a5f052e 100644 --- a/client/app/main/main.js +++ b/client/app/main/main.js @@ -1,11 +1,24 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider - .state('main', { + .state('manticore', { + url: '', + abstract: true, + resolve: { + config: function ($http) { + return $http.get('/config'); + } + }, + template: '', + controller: function ($rootScope, config) { + $rootScope.config = config.data; + } + }) + .state('manticore.main', { url: '/', templateUrl: 'app/main/main.html', controller: 'MainCtrl' }); - }); \ No newline at end of file + }); diff --git a/client/app/main/main.styl b/client/app/main/main.styl index dab28f8..a2e373b 100644 --- a/client/app/main/main.styl +++ b/client/app/main/main.styl @@ -1,29 +1,30 @@ .document-form margin 20px 0 #banner border-bottom none margin-top -20px #banner h1 font-size 60px letter-spacing -1px line-height 1 .hero-unit color #000 padding 30px 15px position relative text-align center text-shadow 0 1px 0 rgba(0, 0, 0, 0.1) img max-width: 100%; .footer border-top 1px solid #E5E5E5 margin-top 70px padding 30px 0 text-align center .btn - border-radius 0 !important + border-radius 1 !important + border: none !important diff --git a/client/app/templates/templates.js b/client/app/templates/templates.js index 6572cd3..341ef36 100644 --- a/client/app/templates/templates.js +++ b/client/app/templates/templates.js @@ -1,11 +1,11 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider - .state('templates', { + .state('manticore.templates', { url: '/templates', templateUrl: 'app/templates/templates.html', controller: 'TemplatesCtrl' }); - }); \ No newline at end of file + }); diff --git a/client/app/users/users.js b/client/app/users/users.js index 8564f3e..ea6e60d 100644 --- a/client/app/users/users.js +++ b/client/app/users/users.js @@ -1,11 +1,11 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider - .state('users', { + .state('manticore.users', { url: '/users', templateUrl: 'app/users/users.html', controller: 'UsersCtrl' }); }); diff --git a/client/components/createMenu/createMenu.jade b/client/components/createMenu/createMenu.jade index 461d7b9..71256f3 100644 --- a/client/components/createMenu/createMenu.jade +++ b/client/components/createMenu/createMenu.jade @@ -1,7 +1,7 @@ ul.dropdown-menu.create-menu li(ng-repeat='template in templates') - a(ui-sref='editor.fromTemplate({ id: template._id })' target='_blank') + a(ui-sref='manticore.editor.fromTemplate({ id: template._id })' target='_blank') span.text-strong {{template.title}} br div.text-muted {{template.description}} diff --git a/client/components/documentList/documentList.jade b/client/components/documentList/documentList.jade index cd5712b..bd542a5 100644 --- a/client/components/documentList/documentList.jade +++ b/client/components/documentList/documentList.jade @@ -1,16 +1,16 @@ .container(ng-switch='documents.length === 0') .no-docs(ng-switch-when='true') p.advice No documents yet. Why not add some? .document-list(ng-switch-when='false') 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.forDocument({id: document._id})' target='_blank') {{document.title}} + a(ui-sref='manticore.editor.forDocument({id: document._id})' target='_blank') {{document.title}} td {{document.creator.name}} td {{document.date | amCalendar}} diff --git a/client/components/exportButton/exportButton.controller.js b/client/components/exportButton/exportButton.controller.js index 45f6867..c55a0f5 100644 --- a/client/components/exportButton/exportButton.controller.js +++ b/client/components/exportButton/exportButton.controller.js @@ -1,25 +1,73 @@ 'use strict'; angular.module('manticoreApp') -.controller('ExportButtonCtrl', function ($scope, SaveAs) { +.controller('ExportButtonCtrl', function ($scope, $http, $timeout, SaveAs) { + $scope.label = 'Download'; + $scope.isExporting = false; + $scope.conversionHost = $scope.$root.config.conversionHost; + $scope.items = [ - { - label: 'OpenDocument Text (.odt)', - format: 'odt' - } + { format: 'odt', label: 'OpenDocument Text (.odt)' }, ]; + if ($scope.conversionHost) { + $scope.items = $scope.items.concat([ + { format: 'pdf', label: 'Portable Document Format (.pdf)' }, + { format: 'txt', label: 'Plain Text (.txt)' }, + { format: 'docx', label: 'Microsoft Word (.docx)' }, + { format: 'doc', label: 'Microsoft Word, old (.doc)' }, + { format: 'html', label: 'HTML (.html)' }, + ]); + } $scope.export = function (format) { - var fileName = $scope.editor.getMetadata('dc:title') || $scope.document.title, - addExtension = fileName.split('.').pop() !== 'odt'; + $scope.label = 'Downloading...'; + $scope.isExporting = true; + + var title = $scope.editor.getMetadata('dc:title') || $scope.document.title, + fileName = title.replace(/\.[^.$]+$/, ''); - if (format === 'odt') { - $scope.editor.getDocumentAsByteArray(function (err, data) { + $scope.editor.getDocumentAsByteArray(function (err, data) { + if (format === 'odt') { SaveAs.download( [data.buffer], - fileName + (addExtension ? '.odt' : ''), - { type: 'application/vnd.oasis.opendocument.text' }); - }); - } + fileName + '.odt', + { type: 'application/vnd.oasis.opendocument.text' } + ); + $scope.label = 'Download'; + $scope.isExporting = false; + } else { + var formData = new FormData(); + formData.append('document', new Blob([data.buffer], { type: 'application/vnd.oasis.opendocument.text' })); + $http({ + method: 'POST', + url: $scope.conversionHost + '/convert/' + format, + data: formData, + responseType: 'arraybuffer', + transformRequest: angular.identity, + transformResponse: angular.identity, + headers: { 'Content-Type': undefined } + }) + .success(function (data, status, headers) { + SaveAs.download( + [data], + fileName + '.' + format, + { type: headers('content-type') } + ); + $timeout(function () { + $scope.label = 'Download'; + $scope.isExporting = false; + }); + }) + .error(function () { + $timeout(function () { + $scope.label = 'Error while downloading'; + }); + $timeout(function () { + $scope.label = 'Download'; + $scope.isExporting = false; + }, 1000); + }); + } + }); }; }); diff --git a/client/components/exportButton/exportButton.directive.js b/client/components/exportButton/exportButton.directive.js index 0a9c7e2..d8b8dad 100644 --- a/client/components/exportButton/exportButton.directive.js +++ b/client/components/exportButton/exportButton.directive.js @@ -1,10 +1,11 @@ 'use strict'; angular.module('manticoreApp') .directive('exportButton', function () { return { templateUrl: 'components/exportButton/exportButton.html', restrict: 'E', - controller: 'ExportButtonCtrl' + controller: 'ExportButtonCtrl', + scope: true }; }); diff --git a/client/components/exportButton/exportButton.jade b/client/components/exportButton/exportButton.jade index 3854e59..7cb6499 100644 --- a/client/components/exportButton/exportButton.jade +++ b/client/components/exportButton/exportButton.jade @@ -1,7 +1,9 @@ div.export-button.btn-group(dropdown) - button.btn.btn-danger(type='button' ng-click='export(items[0].format)') Download - button.btn.btn-danger.dropdown-toggle(dropdown-toggle) + button.btn.btn-danger(type='button' ng-click='export(items[0].format)' ng-disabled='isExporting') + i.fa(ng-class='isExporting ? "fa-spin fa-circle-o-notch": "fa-cloud-download"') + | {{label}} + button.btn.btn-danger.dropdown-toggle(dropdown-toggle ng-show='conversionHost' ng-disabled='isExporting') span.caret ul.dropdown-menu(role='menu') li(ng-repeat='item in items') a(href='#' ng-click='export(item.format)') {{item.label}}... diff --git a/client/components/exportButton/exportButton.styl b/client/components/exportButton/exportButton.styl index 6d6c501..0a2e1c2 100644 --- a/client/components/exportButton/exportButton.styl +++ b/client/components/exportButton/exportButton.styl @@ -1,3 +1,2 @@ .export-button - padding: 10px 10px; - height: 50px; + margin: 10px 10px; diff --git a/client/components/navbar/navbar.jade b/client/components/navbar/navbar.jade index b2404e0..001a9ba 100644 --- a/client/components/navbar/navbar.jade +++ b/client/components/navbar/navbar.jade @@ -1,46 +1,46 @@ 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.nav-text.navbar-brand(href='/') manticore + a.nav-text.navbar-brand(ui-sref='manticore.main') manticore div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed') ul.nav.navbar-nav li(ng-show='isAdmin()', ng-class='{active: isActive("/users")}') - a.nav-text(ui-sref='users') Users + a.nav-text(ui-sref='manticore.users') Users li(ng-show='isAdmin()', ng-class='{active: isActive("/templates")}') - a.nav-text(ui-sref='templates') Templates + a.nav-text(ui-sref='manticore.templates') Templates li.dropdown(ng-if='isLoggedIn() && isActive("/")' dropdown) a.nav-text.dropdown-toggle(href='#' dropdown-toggle role='button') | New span.caret create-menu li.dropdown(ng-if='isLoggedIn() && isActive("/")' dropdown auto-close='disabled') a.nav-text.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.nav-text(href='/signup') Sign up li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}') a.nav-text(href='/login') Login li(ng-show='isLoggedIn()') p.nav-text.navbar-text Hello {{ getCurrentUser().name }} li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}') a.nav-text(href='/settings') span.glyphicon.glyphicon-cog li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}') a.nav-text(href='', ng-click='logout()') Logout diff --git a/client/components/saveButton/saveButton.controller.js b/client/components/saveButton/saveButton.controller.js index d0f6f13..8ffcbfd 100644 --- a/client/components/saveButton/saveButton.controller.js +++ b/client/components/saveButton/saveButton.controller.js @@ -1,28 +1,28 @@ 'use strict'; angular.module('manticoreApp') .controller('SaveButtonCtrl', function ($scope, $timeout) { $scope.label = 'Save'; $scope.isSaving = false; $scope.save = function () { $scope.label = 'Saving'; $scope.isSaving = true; var socket = $scope.editor.clientAdaptor.getSocket(); socket.emit('save', function (err) { $timeout(function () { if (err) { $scope.label = 'Error while saving'; } else { $scope.label = 'Saved just now'; } }); $timeout(function () { $scope.label = 'Save'; $scope.isSaving = false; - }, 5000); + }, 1000); }); }; }); diff --git a/client/components/saveButton/saveButton.directive.js b/client/components/saveButton/saveButton.directive.js index 3aa2c27..bce295b 100644 --- a/client/components/saveButton/saveButton.directive.js +++ b/client/components/saveButton/saveButton.directive.js @@ -1,10 +1,11 @@ 'use strict'; angular.module('manticoreApp') .directive('saveButton', function () { return { templateUrl: 'components/saveButton/saveButton.html', restrict: 'E', - controller: 'SaveButtonCtrl' + controller: 'SaveButtonCtrl', + scope: true }; }); diff --git a/client/components/saveButton/saveButton.jade b/client/components/saveButton/saveButton.jade index 642d4b0..d4c3aa6 100644 --- a/client/components/saveButton/saveButton.jade +++ b/client/components/saveButton/saveButton.jade @@ -1,2 +1,4 @@ div.save-button - button.btn.btn-primary(type='button' ng-click='save()' ng-disabled='isSaving') {{label}} + button.btn.btn-primary(type='button' ng-click='save()' ng-disabled='isSaving') + i.fa(ng-class='isSaving ? "fa-spin fa-circle-o-notch": "fa-cloud-upload"') + | {{label}} diff --git a/client/components/saveButton/saveButton.styl b/client/components/saveButton/saveButton.styl index a52ebc5..906c612 100644 --- a/client/components/saveButton/saveButton.styl +++ b/client/components/saveButton/saveButton.styl @@ -1,3 +1,2 @@ .save-button - padding: 10px 10px; - height: 50px; + margin: 10px 10px; diff --git a/client/components/titleEditor/titleEditor.styl b/client/components/titleEditor/titleEditor.styl index 3d04259..bde1ae7 100644 --- a/client/components/titleEditor/titleEditor.styl +++ b/client/components/titleEditor/titleEditor.styl @@ -1,20 +1,19 @@ .title-container display: block; max-width: 400px; - padding: 10px 10px; - height: 50px; + margin: 10px 10px; .title-editor float: left; font-size: 16px; line-height: 20px; padding: 5px; border: none; outline: none; text-overflow: ellipsis; resize: horizontal; width: 100%; text-decoration: underline; background-color: white; &:hover, &:focus background-color: #eeeeee; diff --git a/server/config/environment/index.js b/server/config/environment/index.js index f1b3de7..e25996a 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -1,73 +1,75 @@ 'use strict'; var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { if(!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; } // All configurations will extend these options // ============================================ var all = { env: process.env.NODE_ENV, // Root path of server root: path.normalize(__dirname + '/../../..'), // Server port port: process.env.PORT || 9000, // Should we populate the DB with sample data? seedDB: true, // Secret for session, you will want to change this and make it an environment variable secrets: { session: 'manticore-secret' }, // List of user roles userRoles: ['guest', 'user', 'admin'], // MongoDB connection options mongo: { options: { db: { safe: true } } }, + conversionHost: process.env.LOCODOC_SERVER, + auth: { type: process.env.AUTH || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, key: process.env.WEBDAV_ENCRYPTION_KEY }, 'ldap': { server: process.env.LDAP_SERVER, base: process.env.LDAP_BASE, filter: process.env.LDAP_FILTER, bindDn: process.env.LDAP_BIND_DN, bindPw: process.env.LDAP_BIND_PW } }, storage: { type: process.env.STORAGE || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, key: process.env.WEBDAV_ENCRYPTION_KEY } } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./' + process.env.NODE_ENV + '.js') || {}); diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js index ca97837..c9a4bde 100644 --- a/server/config/local.env.sample.js +++ b/server/config/local.env.sample.js @@ -1,46 +1,49 @@ 'use strict'; // Use local.env.js for environment variables that grunt will set when the server starts locally. // Use for your api keys, secrets, etc. This file should not be tracked by git. // // You will need to set these on the server you deploy to. module.exports = { DOMAIN: 'http://localhost:9000', SESSION_SECRET: 'manticore-secret', // Control debug level for modules using visionmedia/debug DEBUG: '', /* * Supported authentication strategies. * 1. 'local' for using Manticore's built-in accounts system. Allow signups. * 2. 'webdav' for authenticating against a WebDAV server. Only login, no signups. * 3. 'ldap' for authenticating against an LDAP service. Only login, no signups. */ AUTH: 'local', /* * Supported storage backends. * 1. 'local' for storing everything in Mongo/GridFS. The fastest and most reliable way. * Can be used with any AUTH strategy. * 2. 'webdav' for two-way synchronizing of documents with a WebDAV server. * Can be used if AUTH is 'ldap' or 'webdav'; those credentials are used to talk to the webdav server. */ STORAGE: 'local', /* * WebDAV server config, only if AUTH or STORAGE is 'webdav'. * Make sure you provide an encryption key to protect users' webdav credentials. */ WEBDAV_SERVER: 'https://kolabmachine', WEBDAV_PATH: '/iRony/files/Files', WEBDAV_ENCRYPTION_KEY: 'your-AES-encryption-key', // LDAP server config, required iff AUTH is 'ldap'. {{username}} will be replaced with users' logins LDAP_SERVER: 'ldaps://kolabmachine', LDAP_BASE: 'ou=People,dc=test,dc=example,dc=org', LDAP_FILTER: '(&(objectclass=person)(|(uid={{username}})(mail={{username}})))', LDAP_BIND_DN: 'uid=kolab-service,ou=Special Users,dc=test,dc=example,dc=org', - LDAP_BIND_PW: 'kolab-service-pass' + LDAP_BIND_PW: 'kolab-service-pass', + + // locodoc (document format conversion) server + LOCODOC_SERVER: 'http://localhost:3030' }; diff --git a/server/routes.js b/server/routes.js index 4290697..c4c3470 100644 --- a/server/routes.js +++ b/server/routes.js @@ -1,28 +1,35 @@ /** * Main application routes */ 'use strict'; var errors = require('./components/errors'); +var config = require('./config/environment'); 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')); + app.get('/config', function (req, res) { + res.json(200, { + conversionHost: config.conversionHost + }); + }); + // 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 (except dynamically-added genesis routes) // should redirect to the index.html app.route(/^\/(?!genesis).*/) .get(function(req, res) { res.sendfile(app.get('appPath') + '/index.html'); }); };