From 5c9ad4b19b3f03111b5bf2c24b5dd484ca7ab81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Mon, 19 Jan 2015 11:28:35 +0100 Subject: [PATCH] 'Export/import project' feature --- CHANGELOG.md | 3 +- app/coffee/app.coffee | 7 +- .../modules/admin/project-profile.coffee | 88 ++++++++++++++++++- app/coffee/modules/base.coffee | 1 + app/coffee/modules/common/confirm.coffee | 29 +++++- app/coffee/modules/common/importer.coffee | 73 +++++++++++++++ app/coffee/modules/common/loading.coffee | 3 +- app/coffee/modules/nav.coffee | 5 +- app/coffee/modules/resources.coffee | 4 + app/coffee/modules/resources/projects.coffee | 79 +++++++++++++++-- app/coffee/modules/user-settings/main.coffee | 2 +- app/index.jade | 2 + app/partials/admin/admin-project-export.jade | 21 +++++ .../includes/components/loading-bar.jade | 2 +- .../admin-submenu-project-profile.jade | 4 + .../modules/lightbox-generic-loading.jade | 12 +++ .../modules/lightbox-generic-success.jade | 1 + .../project/project-navigation-base.jade | 10 ++- app/partials/project/projects.jade | 8 +- app/styles/components/loading-bar.scss | 2 +- app/styles/dependencies/helpers.scss | 12 +++ app/styles/layout/animation.scss | 14 +++ app/styles/layout/elements.scss | 4 - app/styles/layout/us-detail.scss | 26 +++--- .../modules/admin/admin-project-export.scss | 29 ++++++ app/styles/modules/common/assigned-to.scss | 14 +-- app/styles/modules/common/history.scss | 6 ++ app/styles/modules/common/lightbox.scss | 48 +++++++++- app/styles/modules/common/projects-nav.scss | 18 +++- app/styles/modules/filters/filters.scss | 14 +-- app/styles/modules/home-projects-list.scss | 21 ++++- app/styles/modules/kanban/kanban-table.scss | 1 + .../modules/user-settings/user-profile.scss | 22 ++--- app/styles/modules/wiki/wiki-nav.scss | 12 +-- app/svg/spinner-circle.svg | 1 + main-sass.js | 1 + 36 files changed, 515 insertions(+), 84 deletions(-) create mode 100644 app/coffee/modules/common/importer.coffee create mode 100644 app/partials/admin/admin-project-export.jade create mode 100644 app/partials/includes/modules/lightbox-generic-loading.jade create mode 100644 app/styles/modules/admin/admin-project-export.scss create mode 100644 app/svg/spinner-circle.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 37394547..3b87d34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog # -## 1.5.0 ??? (unreleased) +## 1.5.0 Betula Pendula - FOSDEM 2015 (unreleased) ### Features - Not showing closed milestones by default in backlog view. - In kanban view an archived user story status doesn't show his content by default. +- Now you can export and import projects between Taiga instances. ### Misc - Lots of small and not so small bugfixes. diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index d7f34ada..2349b3a7 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -81,6 +81,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven {templateUrl: "admin/admin-project-default-values.html"}) $routeProvider.when("/project/:pslug/admin/project-profile/modules", {templateUrl: "admin/admin-project-modules.html"}) + $routeProvider.when("/project/:pslug/admin/project-profile/export", + {templateUrl: "admin/admin-project-export.html"}) $routeProvider.when("/project/:pslug/admin/project-values/us-status", {templateUrl: "admin/admin-project-values-us-status.html"}) $routeProvider.when("/project/:pslug/admin/project-values/us-points", @@ -186,7 +188,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven # If there is an error in the version throw a notify error versionCheckHttpIntercept = ($q, $confirm) -> - versionErrorMsg = "Someone inside Taiga has changed this before and our Oompa Loompas cannot apply your changes. Please reload and apply your changes again (they will be lost)." #TODO: i18n + versionErrorMsg = "Someone inside Taiga has changed this before and our Oompa Loompas cannot apply your changes. + Please reload and apply your changes again (they will be lost)." #TODO: i18n httpResponseError = (response) -> if response.status == 400 && response.data.version @@ -202,7 +205,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $provide.factory("versionCheckHttpIntercept", ["$q", "$tgConfirm", versionCheckHttpIntercept]) - $httpProvider.interceptors.push('versionCheckHttpIntercept') + $httpProvider.interceptors.push('versionCheckHttpIntercept'); window.checksley.updateValidators({ linewidth: (val, width) -> diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 9b00f527..fcdca9f4 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -117,7 +117,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) -> if data._error_message $confirm.notify("error", data._error_message) - submitButton = $el.find(".submit-button"); + submitButton = $el.find(".submit-button") $el.on "submit", "form", submit $el.on "click", ".submit-button", submit @@ -210,3 +210,89 @@ ProjectModulesDirective = ($repo, $confirm, $loading) -> return {link:link} module.directive("tgProjectModules", ["$tgRepo", "$tgConfirm", "$tgLoading", ProjectModulesDirective]) + + +############################################################################# +## Project Export Directive +############################################################################# + +ProjectExportDirective = ($window, $rs, $confirm) -> + link = ($scope, $el, $attrs) -> + buttonsEl = $el.find(".admin-project-export-buttons") + showButtons = -> buttonsEl.removeClass("hidden") + hideButtons = -> buttonsEl.addClass("hidden") + + resultEl = $el.find(".admin-project-export-result") + showResult = -> resultEl.removeClass("hidden") + hideResult = -> resultEl.addClass("hidden") + + spinnerEl = $el.find(".spin") + showSpinner = -> spinnerEl.removeClass("hidden") + hideSpinner = -> spinnerEl.addClass("hidden") + + resultTitleEl = $el.find(".result-title") + setLoadingTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n + setAsyncTitle = -> resultTitleEl.html("We are generating your dump file") # TODO: i18n + setSyncTitle = -> resultTitleEl.html("Your dump file ir ready!") # TODO: i18n + + resultMessageEl = $el.find(".result-message ") + setLoadingMessage = -> resultMessageEl.html("Please don't close this page.") # TODO: i18n + setAsyncMessage = -> resultMessageEl.html("We will send you an email when ready.") # TODO: i18n + setSyncMessage = (url) -> resultMessageEl.html("If the download doesn't start automatically click + here.") # TODO: i18n + + showLoadingMode = -> + showSpinner() + setLoadingTitle() + setLoadingMessage() + hideButtons() + showResult() + + showExportResultAsyncMode = -> + hideSpinner() + setAsyncTitle() + setAsyncMessage() + + showExportResultSyncMode = (url) -> + hideSpinner() + setSyncTitle() + setSyncMessage(url) + + showErrorMode = -> + hideSpinner() + hideResult() + showButtons() + + $el.on "click", "a.button-export", debounce 2000, (event) => + event.preventDefault() + + onSuccess = (result) => + if result.status == 202 # Async mode + showExportResultAsyncMode() + else #result.status == 200 # Sync mode + dumpUrl = result.data.url + showExportResultSyncMode(dumpUrl) + $window.open(dumpUrl, "_blank") + + onError = (result) => + showErrorMode() + + errorMsg = "Our oompa loompas have some problems generasting your dump. + Please try again. " # TODO: i18n + + if result.status == 429 # TOO MANY REQUESTS + errorMsg = "Sorry, our oompa loompas are very busy right now. + Please try again in a few minutes. " # TODO: i18n + else if result.data?._error_message + errorMsg = "Our oompa loompas have some problems generasting your dump: + #{result.data._error_message}" # TODO: i18n + + $confirm.notify("error", errorMsg) + + showLoadingMode() + $rs.projects.export($scope.projectId).then(onSuccess, onError) + + return {link:link} + +module.directive("tgProjectExport", ["$window", "$tgResources", "$tgConfirm", ProjectExportDirective]) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 7e123634..5a98d548 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -83,6 +83,7 @@ urls = { "project-admin-project-profile-details": "/project/:project/admin/project-profile/details" "project-admin-project-profile-default-values": "/project/:project/admin/project-profile/default-values" "project-admin-project-profile-modules": "/project/:project/admin/project-profile/modules" + "project-admin-project-profile-export": "/project/:project/admin/project-profile/export" "project-admin-project-values-us-status": "/project/:project/admin/project-values/us-status" "project-admin-project-values-us-points": "/project/:project/admin/project-values/us-points" "project-admin-project-values-task-status": "/project/:project/admin/project-values/task-status" diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index 48d484a4..039fc89a 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -148,11 +148,12 @@ class ConfirmService extends taiga.Service return defered.promise - success: (message) -> + success: (title, message) -> el = angular.element(".lightbox-generic-success") # Render content - el.find("h2.title").html(message) + el.find("h2.title").html(title) if title + el.find("p.message").html(message) if message defered = @q.defer() # Assign event handlers @@ -170,6 +171,30 @@ class ConfirmService extends taiga.Service return defered.promise + loader: (title, message) -> + el = angular.element(".lightbox-generic-loading") + + # Render content + el.find("h2.title").html(title) if title + el.find("p.message").html(message) if message + + return { + start: => @lightboxService.open(el) + stop: => @lightboxService.close(el) + update: (status, title, message, percent) => + el.find("h2.title").html(title) if title + el.find("p.message").html(message) if message + + if percent + el.find(".spin").addClass("hidden") + el.find(".progress-bar-wrapper").removeClass("hidden") + el.find(".progress-bar-wrapper > .bar").width(percent + '%') + el.find(".progress-bar-wrapper > span").html(percent + '%').css('left', (percent - 9) + '%' ) + else + el.find(".spin").removeClass("hidden") + el.find(".progress-bar-wrapper").addClass("hidden") + } + notify: (type, message, title, time) -> # NOTE: Typesi are: error, success, light-error # See partials/components/notification-message.jade) diff --git a/app/coffee/modules/common/importer.coffee b/app/coffee/modules/common/importer.coffee new file mode 100644 index 00000000..e1bdbd96 --- /dev/null +++ b/app/coffee/modules/common/importer.coffee @@ -0,0 +1,73 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/common/importer.coffee +### + +module = angular.module("taigaCommon") + + +ImportProjectButtonDirective = ($rs, $confirm, $location, $navUrls) -> + link = ($scope, $el, $attrs) -> + $el.on "click", ".import-project-button", (event) -> + event.preventDefault() + $el.find("input.import-file").val("") + $el.find("input.import-file").trigger("click") + + $el.on "change", "input.import-file", (event) -> + event.preventDefault() + file = event.target.files[0] + return if not file + + loader = $confirm.loader("Uploading dump file") + + onSuccess = (result) -> + loader.stop() + if result.status == 202 # Async mode + title = "Our Oompa Loompas are importing your project" # TODO: i18n + message = "This process could take a few minutes
We will send you + an email when ready" # TODO: i18n + $confirm.success(title, message) + + else # result.status == 201 # Sync mode + ctx = {project: result.data.slug} + $location.path($navUrls.resolve("project-admin-project-profile-details", ctx)) + $confirm.notify("success", "Your project has been imported successfuly.") # TODO: i18n + + onError = (result) -> + loader.stop() + console.log "Error", result + errorMsg = "Our oompa loompas have some problems importing your dump data. + Please try again. " # TODO: i18n + + if result.status == 429 # TOO MANY REQUESTS + errorMsg = "Sorry, our oompa loompas are very busy right now. + Please try again in a few minutes. " # TODO: i18n + else if result.data?._error_message + errorMsg = "Our oompa loompas have some problems importing your dump data: + #{result.data._error_message}" # TODO: i18n + + $confirm.notify("error", errorMsg) + + loader.start() + $rs.projects.import(file, loader.update).then(onSuccess, onError) + + return {link: link} + +module.directive("tgImportProjectButton", ["$tgResources", "$tgConfirm", "$location", "$tgNavUrls", + ImportProjectButtonDirective]) diff --git a/app/coffee/modules/common/loading.coffee b/app/coffee/modules/common/loading.coffee index a1d169af..b8f5ad7a 100644 --- a/app/coffee/modules/common/loading.coffee +++ b/app/coffee/modules/common/loading.coffee @@ -26,7 +26,8 @@ class TgLoadingService extends taiga.Service if not target.hasClass('loading') target.data('loading-old-content', target.html()) target.addClass('loading') - target.html("") + target.html("loading...") + debugger finish: (target) -> if target.hasClass('loading') diff --git a/app/coffee/modules/nav.coffee b/app/coffee/modules/nav.coffee index 29740c50..ffe765c6 100644 --- a/app/coffee/modules/nav.coffee +++ b/app/coffee/modules/nav.coffee @@ -147,7 +147,7 @@ ProjectsNavigationDirective = ($rootscope, animationFrame, $timeout, tgLoader, $ loadingStart = new Date().getTime() - $el.on "click", ".create-project-button .button", (event) -> + $el.on "click", ".create-project-button", (event) -> event.preventDefault() $ctrl.newProject() @@ -168,7 +168,8 @@ ProjectsNavigationDirective = ($rootscope, animationFrame, $timeout, tgLoader, $ } -module.directive("tgProjectsNav", ["$rootScope", "animationFrame", "$timeout", "tgLoader", "$tgLocation", "$compile", "$tgTemplate", ProjectsNavigationDirective]) +module.directive("tgProjectsNav", ["$rootScope", "animationFrame", "$timeout", "tgLoader", "$tgLocation", "$compile", + "$tgTemplate", ProjectsNavigationDirective]) ############################################################################# diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index 46ebe973..ac877edf 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -100,6 +100,10 @@ urls = { # Feedback "feedback": "/feedback" + + # Export/Import + "exporter": "/exporter" + "importer": "/importer/load_dump" } # Initialize api urls service diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee index 24e6bda0..d6a5a0ad 100644 --- a/app/coffee/modules/resources/projects.coffee +++ b/app/coffee/modules/resources/projects.coffee @@ -21,15 +21,17 @@ taiga = @.taiga +sizeFormat = @.taiga.sizeFormat -resourceProvider = ($repo, $http, $urls) -> + +resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $rootScope) -> service = {} - service.get = (id) -> - return $repo.queryOne("projects", id) + service.get = (projectId) -> + return $repo.queryOne("projects", projectId) - service.getBySlug = (slug) -> - return $repo.queryOne("projects", "by_slug?slug=#{slug}") + service.getBySlug = (projectSlug) -> + return $repo.queryOne("projects", "by_slug?slug=#{projectSlug}") service.list = -> return $repo.queryMany("projects") @@ -55,12 +57,73 @@ resourceProvider = ($repo, $http, $urls) -> service.memberStats = (projectId) -> return $repo.queryOneRaw("projects", "#{projectId}/member_stats") - service.tagsColors = (id) -> - return $repo.queryOne("projects", "#{id}/tags_colors") + service.tagsColors = (projectId) -> + return $repo.queryOne("projects", "#{projectId}/tags_colors") + + service.export = (projectId) -> + url = "#{$urls.resolve("exporter")}/#{projectId}" + return $http.get(url) + + service.import = (file, statusUpdater) -> + defered = $q.defer() + + maxFileSize = $config.get("maxUploadFileSize", null) + if maxFileSize and file.size > maxFileSize + response = { + status: 413, + data: _error_message: "'#{file.name}' (#{sizeFormat(file.size)}) is too heavy for our oompa + loompas, try it with a smaller than (#{sizeFormat(maxFileSize)})" + } + defered.reject(response) + return defered.promise + + uploadProgress = (evt) => + percent = Math.round((evt.loaded / evt.total) * 100) + message = "Uloaded #{sizeFormat(evt.loaded)} of #{sizeFormat(evt.total)}" + statusUpdater("in-progress", null, message, percent) + + uploadComplete = (evt) => + statusUpdater("done", "Importing Project", "This process can take a while, please keep the window open.") # i18n + + uploadFailed = (evt) => + statusUpdater("error") + + complete = (evt) => + response = {} + try + response.data = JSON.parse(evt.target.responseText) + catch + response.data = {} + response.status = evt.target.status + + defered.resolve(response) if response.status in [201, 202] + defered.reject(response) + + failed = (evt) => + defered.reject("fail") + + data = new FormData() + data.append('dump', file) + + xhr = new XMLHttpRequest() + xhr.upload.addEventListener("progress", uploadProgress, false) + xhr.upload.addEventListener("load", uploadComplete, false) + xhr.upload.addEventListener("error", uploadFailed, false) + xhr.upload.addEventListener("abort", uploadFailed, false) + xhr.addEventListener("load", complete, false) + xhr.addEventListener("error", failed, false) + + xhr.open("POST", $urls.resolve("importer")) + xhr.setRequestHeader("Authorization", "Bearer #{$auth.getToken()}") + xhr.setRequestHeader('Accept', 'application/json') + xhr.send(data) + + return defered.promise return (instance) -> instance.projects = service module = angular.module("taigaResources") -module.factory("$tgProjectsResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", resourceProvider]) +module.factory("$tgProjectsResourcesProvider", ["$tgConfig", "$tgRepo", "$tgHttp", "$tgUrls", "$tgAuth", "$q", + resourceProvider]) diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee index dbfffe35..7edb8eb1 100644 --- a/app/coffee/modules/user-settings/main.coffee +++ b/app/coffee/modules/user-settings/main.coffee @@ -145,7 +145,7 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) -> $el.on "change", "#avatar-field", (event) -> if $scope.avatarAttachment - $el.find('.overlay').show() + $el.find('.overlay').css('display', 'flex') $rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError) # Use gravatar photo diff --git a/app/index.jade b/app/index.jade index 0c31b58a..c2cd9308 100644 --- a/app/index.jade +++ b/app/index.jade @@ -29,6 +29,8 @@ html(lang="en") include partials/includes/modules/lightbox-generic-success div.lightbox.lightbox-generic-error include partials/includes/modules/lightbox-generic-error + div.lightbox.lightbox-generic-loading + include partials/includes/modules/lightbox-generic-loading div.lightbox.lightbox-search(tg-search-box) include partials/includes/modules/lightbox-search div.lightbox.lightbox-feedback.lightbox-generic-form(tg-lb-feedback) diff --git a/app/partials/admin/admin-project-export.jade b/app/partials/admin/admin-project-export.jade new file mode 100644 index 00000000..ccac3ecb --- /dev/null +++ b/app/partials/admin/admin-project-export.jade @@ -0,0 +1,21 @@ +div.wrapper(ng-controller="ProjectProfileController as ctrl", + ng-init="section='admin'; sectionName='Export'") + sidebar.menu-secondary.sidebar(tg-admin-navigation="project-profile") + include ../includes/modules/admin-menu + + sidebar.menu-tertiary.sidebar(tg-admin-navigation="export") + include ../includes/modules/admin-submenu-project-profile + + section.main.admin-common(tg-project-export) + header + include ../includes/components/mainTitle + p.admin-subtitle Export your project to save a backup or to create a new one based on this. + + div.admin-project-export-buttons + a.button.button-green.button-export(href="", title="Export your project") Export + + div.admin-project-export-result.hidden + div.spin.hidden + img(src="/svg/spinner-circle.svg", alt="loading...") + h3.result-title + p.result-message diff --git a/app/partials/includes/components/loading-bar.jade b/app/partials/includes/components/loading-bar.jade index 5f1f7937..677c5310 100644 --- a/app/partials/includes/components/loading-bar.jade +++ b/app/partials/includes/components/loading-bar.jade @@ -1,4 +1,4 @@ -div.loading +div.loading-bar span.item.item-1 span.item.item-2 span.item.item-3 diff --git a/app/partials/includes/modules/admin-submenu-project-profile.jade b/app/partials/includes/modules/admin-submenu-project-profile.jade index 535a6b3f..e9522a86 100644 --- a/app/partials/includes/modules/admin-submenu-project-profile.jade +++ b/app/partials/includes/modules/admin-submenu-project-profile.jade @@ -16,3 +16,7 @@ section.admin-submenu a(href="", tg-nav="project-admin-project-profile-modules:project=project.slug") span.title Modules span.icon.icon-arrow-right + li#adminmenu-export + a(href="", tg-nav="project-admin-project-profile-export:project=project.slug") + span.title Export + span.icon.icon-arrow-right diff --git a/app/partials/includes/modules/lightbox-generic-loading.jade b/app/partials/includes/modules/lightbox-generic-loading.jade new file mode 100644 index 00000000..d4bb9f06 --- /dev/null +++ b/app/partials/includes/modules/lightbox-generic-loading.jade @@ -0,0 +1,12 @@ +//- See $confirm.loader to undestand this template + +section + div.spin.hidden + img(src="/svg/spinner-circle.svg", alt="loading...") + + div.progress-bar-wrapper.hidden + div.bar + span.progress + + h2.title + p.message diff --git a/app/partials/includes/modules/lightbox-generic-success.jade b/app/partials/includes/modules/lightbox-generic-success.jade index e8277d96..02230f5d 100644 --- a/app/partials/includes/modules/lightbox-generic-success.jade +++ b/app/partials/includes/modules/lightbox-generic-success.jade @@ -2,6 +2,7 @@ a.close(href="", title="close") span.icon.icon-delete section h2.title + p.message div.options a.button.button-green(href="", title="Accept") span Accept diff --git a/app/partials/project/project-navigation-base.jade b/app/partials/project/project-navigation-base.jade index 61da9123..793ca0a0 100644 --- a/app/partials/project/project-navigation-base.jade +++ b/app/partials/project/project-navigation-base.jade @@ -4,8 +4,14 @@ form input(type="text", placeholder="Search in...", class="search-project") a(class="icon icon-search") -div(class="create-project-button") - a(class="button button-green" href="") Create project +div(class="create-project-button-wrapper") + a(class="button button-green create-project-button" href="" title="Create new project") + Create project + + div(tg-import-project-button) + a(class="button button-blackish import-project-button" href="" title="Import project") + span(class="icon icon-upload") + input(class="import-file hidden" type="file") div(class="projects-pagination" tg-projects-pagination) a(class="v-pagination-previous icon icon-arrow-up", href="") diff --git a/app/partials/project/projects.jade b/app/partials/project/projects.jade index d9348576..ed120fba 100644 --- a/app/partials/project/projects.jade +++ b/app/partials/project/projects.jade @@ -25,4 +25,10 @@ div.home-projects-list(ng-controller="ProjectsController as ctrl") div(tg-projects-list) .create-project-button-wrapper - a.button.button-green(href="", ng-click="ctrl.newProject()") Create project + a.button.button-green.create-project-button(href="", ng-click="ctrl.newProject()", + title="Create new project") Create project + div(tg-import-project-button) + a.button.button-blackish.import-project-button(href="", title="Import project") + span.icon.icon-upload + input.import-file.hidden(type="file") + diff --git a/app/styles/components/loading-bar.scss b/app/styles/components/loading-bar.scss index 65941a21..1e68bf04 100644 --- a/app/styles/components/loading-bar.scss +++ b/app/styles/components/loading-bar.scss @@ -1,4 +1,4 @@ -.loading { +.loading-bar { align-items: stretch; display: flex; flex-direction: row; diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index c3acdd91..a17e8dff 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -58,6 +58,9 @@ padding: 12px; text-align: center; } + .loading-spinner { + @extend %loading-spinner; + } } %button { @@ -91,3 +94,12 @@ background: url('/images/invitation_bg.jpg') no-repeat center center; background-size: cover; } + +%loading-spinner { + animation-timing-function: ease-in-out; + animation: rotate 1.5s cubic-bezier(.00, .05, .87, 1.04) infinite alternate; + margin: 0 auto; + max-height: 1rem; + max-width: 1rem; + transform-origin: 32 32; +} diff --git a/app/styles/layout/animation.scss b/app/styles/layout/animation.scss index 5981239f..e47897df 100644 --- a/app/styles/layout/animation.scss +++ b/app/styles/layout/animation.scss @@ -10,6 +10,20 @@ } } +//Spin +@-webkit-keyframes rotate { + 50% { + filter: invert(1); + transform: rotate(360deg); + } +} + +@keyframes rotate { + 50% { + transform: rotate(360deg); + } +} + @include keyframes(formSlide) { 0% { filter: blur(5px); diff --git a/app/styles/layout/elements.scss b/app/styles/layout/elements.scss index 7bf3788b..44f9c1da 100644 --- a/app/styles/layout/elements.scss +++ b/app/styles/layout/elements.scss @@ -39,10 +39,6 @@ sup { vertical-align: middle; } -.icon-spinner { - animation: spin 1s linear infinite; -} - .clickable { cursor: pointer; } diff --git a/app/styles/layout/us-detail.scss b/app/styles/layout/us-detail.scss index 706e6e53..3f0e1d14 100644 --- a/app/styles/layout/us-detail.scss +++ b/app/styles/layout/us-detail.scss @@ -86,8 +86,7 @@ width: 100%; } .icon-edit, - .icon-floppy, - .icon-spinner { + .icon-floppy { @extend %large; color: $gray-light; margin-left: .5rem; @@ -125,6 +124,11 @@ margin-right: 5rem; } } + .loading-spinner { + @extend %loading-spinner; + max-height: 1.5rem; + max-width: 1.5rem; + } } .blocked-warning { @@ -187,15 +191,15 @@ position: absolute; right: 1rem; top: .2rem; - .save { - color: $blackish; - opacity: .6; - top: 0; - } &:hover { opacity: .3; transition: opacity .2s linear; } + .loading-spinner { + @extend %loading-spinner; + max-height: 1.5rem; + max-width: 1.5rem; + } } .edit { color: $grayer; @@ -344,9 +348,8 @@ .level-name { color: darken($whitish, 20%); float: right; - &.loading span { - animation: loading .5s linear; - animation: spin 1s linear infinite; + .loading-spinner { + @extend %loading-spinner; } } } @@ -413,6 +416,9 @@ } } } + .loading-spinner { + @extend %loading-spinner; + } } .us-status { diff --git a/app/styles/modules/admin/admin-project-export.scss b/app/styles/modules/admin/admin-project-export.scss new file mode 100644 index 00000000..b4f31b45 --- /dev/null +++ b/app/styles/modules/admin/admin-project-export.scss @@ -0,0 +1,29 @@ +.admin-project-export-buttons { + margin-top: 2rem; +} + +.admin-project-export-result { + margin-top: 1rem; + .spin { + margin: 0 auto; + width: 2.5rem; + img { + @extend %loading-spinner; + width: 100%; + } + } + h3 { + @extend %bold; + @extend %large; + background: $whitish; + color: $gray; + margin: .5rem; + padding: .5rem; + text-align: center; + } + p { + color: $gray-light; + margin: .5rem 0; + text-align: center; + } +} diff --git a/app/styles/modules/common/assigned-to.scss b/app/styles/modules/common/assigned-to.scss index d1185798..72b2a1de 100644 --- a/app/styles/modules/common/assigned-to.scss +++ b/app/styles/modules/common/assigned-to.scss @@ -10,16 +10,10 @@ } } } - &.loading { - width: 100%; - span { - animation: loading .5s linear; - animation: spin 1s linear infinite; - font-size: 30px; - padding: 20px 0; - text-align: center; - width: 100%; - } + .loading-spinner { + @extend %loading-spinner; + margin: 0 auto; + max-width: 2rem; } .user-avatar { flex-grow: 1; diff --git a/app/styles/modules/common/history.scss b/app/styles/modules/common/history.scss index 1cfc957d..1b37baae 100644 --- a/app/styles/modules/common/history.scss +++ b/app/styles/modules/common/history.scss @@ -108,6 +108,12 @@ .preview-icon { opacity: 0; } + .loading-spinner { + @extend %loading-spinner; + max-height: 1rem; + max-width: 1rem; + } + } .show-more-comments { @extend %small; diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index aaa93e6f..44f3ee38 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -389,15 +389,57 @@ .lightbox-generic-success, -.lightbox-generic-error { +.lightbox-generic-error, +.lightbox-generic-loading { section { - flex-basis: 420px; + flex-basis: 500px; flex-grow: 0; - width: 420px; + flex-shrink: 0; + width: 500px; } h2 { line-height: 2rem; } + p { + text-align: center; + } +} + +.lightbox-generic-loading { + .spin { + margin: 1rem auto; + width: 5rem; + img { + @extend %loading-spinner; + max-height: 100%; + max-width: 100%; + width: 100%; + } + } + .progress-bar-wrapper { + background: darken($whitish, 5%); + height: 30px; + margin-bottom: 1rem; + padding: 3px; + position: relative; + .bar { + background: $fresh-taiga; + height: 24px; + position: absolute; + transition: width .1s linear; + } + .progress { + @extend %title; + @extend %bold; + @extend %large; + background: darken($whitish, 5%); + bottom: 35px; + color: $gray; + padding: .3rem; + position: absolute; + transition: left .1s linear; + } + } } .lightbox-create-issue { diff --git a/app/styles/modules/common/projects-nav.scss b/app/styles/modules/common/projects-nav.scss index 867f2c45..4949a0d3 100644 --- a/app/styles/modules/common/projects-nav.scss +++ b/app/styles/modules/common/projects-nav.scss @@ -42,12 +42,24 @@ flex-direction: column; margin-top: 1rem; } - .create-project-button { + .create-project-button-wrapper { + display: flex; flex-shrink: 0; margin-top: 1rem; - a { + .create-project-button { + flex-grow: 8; + margin-right: .2rem; text-align: center; - width: 100%; + } + .import-project-button { + flex-grow: 1; + padding-left: .5rem; + padding-right: .5rem; + text-align: center; + .icon { + color: $grayer; + margin: 0; + } } } .v-pagination-previous, diff --git a/app/styles/modules/filters/filters.scss b/app/styles/modules/filters/filters.scss index 8a619a83..52c31475 100644 --- a/app/styles/modules/filters/filters.scss +++ b/app/styles/modules/filters/filters.scss @@ -35,20 +35,14 @@ opacity: 0; transition: all .1s ease-in; .loading { - background: $grayer; - border: 1px solid #b8b8b8; - display: inline-block; margin: 0; padding: 8px; text-align: center; width: 100%; - .icon-spinner { - color: $whitish; - float: none; - } - span { - animation: loading .5s linear; - animation: spin 1s linear infinite; + .loading-spinner { + @extend %loading-spinner; + max-height: 1rem; + max-width: 1rem; } } } diff --git a/app/styles/modules/home-projects-list.scss b/app/styles/modules/home-projects-list.scss index 2ad96b3e..186006de 100644 --- a/app/styles/modules/home-projects-list.scss +++ b/app/styles/modules/home-projects-list.scss @@ -76,7 +76,6 @@ height: 130px; margin-bottom: 1rem; margin-right: 1rem; - overflow: hidden; position: relative; transition: background-color .3s linear; width: 23.5%; @@ -158,8 +157,24 @@ width: 100%; } .create-project-button-wrapper { - position: relative; - width: 100%; + display: flex; + flex-shrink: 0; + margin-top: 1rem; + .create-project-button { + flex-grow: 8; + margin-right: .2rem; + text-align: center; + } + .import-project-button { + flex-grow: 1; + padding-left: .5rem; + padding-right: .5rem; + text-align: center; + .icon { + color: $grayer; + margin: 0; + } + } } .button-green { color: $whitish; diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss index 73e6287f..d542a418 100644 --- a/app/styles/modules/kanban/kanban-table.scss +++ b/app/styles/modules/kanban/kanban-table.scss @@ -101,6 +101,7 @@ $column-margin: 0 10px 0 0; margin: $column-margin; max-width: $column-width; overflow-y: auto; + widows: $column-width; &:last-child { margin-right: 0; } diff --git a/app/styles/modules/user-settings/user-profile.scss b/app/styles/modules/user-settings/user-profile.scss index 603624d7..68d11ca2 100644 --- a/app/styles/modules/user-settings/user-profile.scss +++ b/app/styles/modules/user-settings/user-profile.scss @@ -12,27 +12,29 @@ .image-container { position: relative; } - img { - border: 2px solid $white; + .avatar { border-radius: 8%; width: 100%; } .overlay { - background: rgba($white, .9); + align-content: center; + align-items: center; + background: rgba($blackish, .9); bottom: 0; display: none; + justify-content: center; left: 0; position: absolute; right: 0; top: 0; + width: 100%; } - .icon-spinner { - @extend %xlarge; - animation: spin infinite .8s linear; - color: $gray-light; - left: 40%; - position: absolute; - top: 40%; + .loading-spinner { + @extend %loading-spinner; + border: 0; + flex-grow: 0; + transform-origin: 32 32; + width: 30%; } p { @extend %xsmall; diff --git a/app/styles/modules/wiki/wiki-nav.scss b/app/styles/modules/wiki/wiki-nav.scss index aa8421a4..6b63e87f 100644 --- a/app/styles/modules/wiki/wiki-nav.scss +++ b/app/styles/modules/wiki/wiki-nav.scss @@ -31,18 +31,14 @@ } } &.loading { - background: $grayer; - border: 1px solid #b8b8b8; margin: 0; padding: 8px; text-align: center; width: 100%; - .icon-spinner { - color: $whitish; - float: none; - } - span { - animation: loading .5s linear, spin 1s linear infinite; + .loading-spinner { + @extend %loading-spinner; + max-height: 1rem; + max-width: 1rem; } } } diff --git a/app/svg/spinner-circle.svg b/app/svg/spinner-circle.svg new file mode 100644 index 00000000..27cdb3b2 --- /dev/null +++ b/app/svg/spinner-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/main-sass.js b/main-sass.js index 68df15c7..40f1cbce 100644 --- a/main-sass.js +++ b/main-sass.js @@ -119,6 +119,7 @@ exports.files = function () { 'modules/admin/admin-submenu-roles', 'modules/admin/admin-roles', 'modules/admin/admin-functionalities', + 'modules/admin/admin-project-export', 'modules/admin/admin-membership-table', 'modules/admin/admin-project-profile', 'modules/admin/default-values',