'Export/import project' feature

stable
Xavier Julián 2015-01-19 11:28:35 +01:00 committed by Jesús Espino
parent 1981389445
commit 5c9ad4b19b
36 changed files with 515 additions and 84 deletions

View File

@ -1,10 +1,11 @@
# Changelog # # Changelog #
## 1.5.0 ??? (unreleased) ## 1.5.0 Betula Pendula - FOSDEM 2015 (unreleased)
### Features ### Features
- Not showing closed milestones by default in backlog view. - Not showing closed milestones by default in backlog view.
- In kanban view an archived user story status doesn't show his content by default. - 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 ### Misc
- Lots of small and not so small bugfixes. - Lots of small and not so small bugfixes.

View File

@ -81,6 +81,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{templateUrl: "admin/admin-project-default-values.html"}) {templateUrl: "admin/admin-project-default-values.html"})
$routeProvider.when("/project/:pslug/admin/project-profile/modules", $routeProvider.when("/project/:pslug/admin/project-profile/modules",
{templateUrl: "admin/admin-project-modules.html"}) {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", $routeProvider.when("/project/:pslug/admin/project-values/us-status",
{templateUrl: "admin/admin-project-values-us-status.html"}) {templateUrl: "admin/admin-project-values-us-status.html"})
$routeProvider.when("/project/:pslug/admin/project-values/us-points", $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 # If there is an error in the version throw a notify error
versionCheckHttpIntercept = ($q, $confirm) -> 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) -> httpResponseError = (response) ->
if response.status == 400 && response.data.version if response.status == 400 && response.data.version
@ -202,7 +205,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
$provide.factory("versionCheckHttpIntercept", ["$q", "$tgConfirm", versionCheckHttpIntercept]) $provide.factory("versionCheckHttpIntercept", ["$q", "$tgConfirm", versionCheckHttpIntercept])
$httpProvider.interceptors.push('versionCheckHttpIntercept') $httpProvider.interceptors.push('versionCheckHttpIntercept');
window.checksley.updateValidators({ window.checksley.updateValidators({
linewidth: (val, width) -> linewidth: (val, width) ->

View File

@ -117,7 +117,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) ->
if data._error_message if data._error_message
$confirm.notify("error", 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 "submit", "form", submit
$el.on "click", ".submit-button", submit $el.on "click", ".submit-button", submit
@ -210,3 +210,89 @@ ProjectModulesDirective = ($repo, $confirm, $loading) ->
return {link:link} return {link:link}
module.directive("tgProjectModules", ["$tgRepo", "$tgConfirm", "$tgLoading", ProjectModulesDirective]) 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
<a href='#{url}' download title='Download
the dump file'>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])

View File

@ -83,6 +83,7 @@ urls = {
"project-admin-project-profile-details": "/project/:project/admin/project-profile/details" "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-default-values": "/project/:project/admin/project-profile/default-values"
"project-admin-project-profile-modules": "/project/:project/admin/project-profile/modules" "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-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-us-points": "/project/:project/admin/project-values/us-points"
"project-admin-project-values-task-status": "/project/:project/admin/project-values/task-status" "project-admin-project-values-task-status": "/project/:project/admin/project-values/task-status"

View File

@ -148,11 +148,12 @@ class ConfirmService extends taiga.Service
return defered.promise return defered.promise
success: (message) -> success: (title, message) ->
el = angular.element(".lightbox-generic-success") el = angular.element(".lightbox-generic-success")
# Render content # 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() defered = @q.defer()
# Assign event handlers # Assign event handlers
@ -170,6 +171,30 @@ class ConfirmService extends taiga.Service
return defered.promise 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) -> notify: (type, message, title, time) ->
# NOTE: Typesi are: error, success, light-error # NOTE: Typesi are: error, success, light-error
# See partials/components/notification-message.jade) # See partials/components/notification-message.jade)

View File

@ -0,0 +1,73 @@
###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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 <br/> 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])

View File

@ -26,7 +26,8 @@ class TgLoadingService extends taiga.Service
if not target.hasClass('loading') if not target.hasClass('loading')
target.data('loading-old-content', target.html()) target.data('loading-old-content', target.html())
target.addClass('loading') target.addClass('loading')
target.html("<span class='icon icon-spinner'></span>") target.html("<img class='loading-spinner' src='/svg/spinner-circle.svg' alt='loading...' />")
debugger
finish: (target) -> finish: (target) ->
if target.hasClass('loading') if target.hasClass('loading')

View File

@ -147,7 +147,7 @@ ProjectsNavigationDirective = ($rootscope, animationFrame, $timeout, tgLoader, $
loadingStart = new Date().getTime() loadingStart = new Date().getTime()
$el.on "click", ".create-project-button .button", (event) -> $el.on "click", ".create-project-button", (event) ->
event.preventDefault() event.preventDefault()
$ctrl.newProject() $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])
############################################################################# #############################################################################

View File

@ -100,6 +100,10 @@ urls = {
# Feedback # Feedback
"feedback": "/feedback" "feedback": "/feedback"
# Export/Import
"exporter": "/exporter"
"importer": "/importer/load_dump"
} }
# Initialize api urls service # Initialize api urls service

View File

@ -21,15 +21,17 @@
taiga = @.taiga taiga = @.taiga
sizeFormat = @.taiga.sizeFormat
resourceProvider = ($repo, $http, $urls) ->
resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $rootScope) ->
service = {} service = {}
service.get = (id) -> service.get = (projectId) ->
return $repo.queryOne("projects", id) return $repo.queryOne("projects", projectId)
service.getBySlug = (slug) -> service.getBySlug = (projectSlug) ->
return $repo.queryOne("projects", "by_slug?slug=#{slug}") return $repo.queryOne("projects", "by_slug?slug=#{projectSlug}")
service.list = -> service.list = ->
return $repo.queryMany("projects") return $repo.queryMany("projects")
@ -55,12 +57,73 @@ resourceProvider = ($repo, $http, $urls) ->
service.memberStats = (projectId) -> service.memberStats = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/member_stats") return $repo.queryOneRaw("projects", "#{projectId}/member_stats")
service.tagsColors = (id) -> service.tagsColors = (projectId) ->
return $repo.queryOne("projects", "#{id}/tags_colors") 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) -> return (instance) ->
instance.projects = service instance.projects = service
module = angular.module("taigaResources") module = angular.module("taigaResources")
module.factory("$tgProjectsResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", resourceProvider]) module.factory("$tgProjectsResourcesProvider", ["$tgConfig", "$tgRepo", "$tgHttp", "$tgUrls", "$tgAuth", "$q",
resourceProvider])

View File

@ -145,7 +145,7 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
$el.on "change", "#avatar-field", (event) -> $el.on "change", "#avatar-field", (event) ->
if $scope.avatarAttachment if $scope.avatarAttachment
$el.find('.overlay').show() $el.find('.overlay').css('display', 'flex')
$rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError) $rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError)
# Use gravatar photo # Use gravatar photo

View File

@ -29,6 +29,8 @@ html(lang="en")
include partials/includes/modules/lightbox-generic-success include partials/includes/modules/lightbox-generic-success
div.lightbox.lightbox-generic-error div.lightbox.lightbox-generic-error
include partials/includes/modules/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) div.lightbox.lightbox-search(tg-search-box)
include partials/includes/modules/lightbox-search include partials/includes/modules/lightbox-search
div.lightbox.lightbox-feedback.lightbox-generic-form(tg-lb-feedback) div.lightbox.lightbox-feedback.lightbox-generic-form(tg-lb-feedback)

View File

@ -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

View File

@ -1,4 +1,4 @@
div.loading div.loading-bar
span.item.item-1 span.item.item-1
span.item.item-2 span.item.item-2
span.item.item-3 span.item.item-3

View File

@ -16,3 +16,7 @@ section.admin-submenu
a(href="", tg-nav="project-admin-project-profile-modules:project=project.slug") a(href="", tg-nav="project-admin-project-profile-modules:project=project.slug")
span.title Modules span.title Modules
span.icon.icon-arrow-right 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

View File

@ -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

View File

@ -2,6 +2,7 @@ a.close(href="", title="close")
span.icon.icon-delete span.icon.icon-delete
section section
h2.title h2.title
p.message
div.options div.options
a.button.button-green(href="", title="Accept") a.button.button-green(href="", title="Accept")
span Accept span Accept

View File

@ -4,8 +4,14 @@ form
input(type="text", placeholder="Search in...", class="search-project") input(type="text", placeholder="Search in...", class="search-project")
a(class="icon icon-search") a(class="icon icon-search")
div(class="create-project-button") div(class="create-project-button-wrapper")
a(class="button button-green" href="") Create project 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) div(class="projects-pagination" tg-projects-pagination)
a(class="v-pagination-previous icon icon-arrow-up", href="") a(class="v-pagination-previous icon icon-arrow-up", href="")

View File

@ -25,4 +25,10 @@ div.home-projects-list(ng-controller="ProjectsController as ctrl")
div(tg-projects-list) div(tg-projects-list)
.create-project-button-wrapper .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")

View File

@ -1,4 +1,4 @@
.loading { .loading-bar {
align-items: stretch; align-items: stretch;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -58,6 +58,9 @@
padding: 12px; padding: 12px;
text-align: center; text-align: center;
} }
.loading-spinner {
@extend %loading-spinner;
}
} }
%button { %button {
@ -91,3 +94,12 @@
background: url('/images/invitation_bg.jpg') no-repeat center center; background: url('/images/invitation_bg.jpg') no-repeat center center;
background-size: cover; 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;
}

View File

@ -10,6 +10,20 @@
} }
} }
//Spin
@-webkit-keyframes rotate {
50% {
filter: invert(1);
transform: rotate(360deg);
}
}
@keyframes rotate {
50% {
transform: rotate(360deg);
}
}
@include keyframes(formSlide) { @include keyframes(formSlide) {
0% { 0% {
filter: blur(5px); filter: blur(5px);

View File

@ -39,10 +39,6 @@ sup {
vertical-align: middle; vertical-align: middle;
} }
.icon-spinner {
animation: spin 1s linear infinite;
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }

View File

@ -86,8 +86,7 @@
width: 100%; width: 100%;
} }
.icon-edit, .icon-edit,
.icon-floppy, .icon-floppy {
.icon-spinner {
@extend %large; @extend %large;
color: $gray-light; color: $gray-light;
margin-left: .5rem; margin-left: .5rem;
@ -125,6 +124,11 @@
margin-right: 5rem; margin-right: 5rem;
} }
} }
.loading-spinner {
@extend %loading-spinner;
max-height: 1.5rem;
max-width: 1.5rem;
}
} }
.blocked-warning { .blocked-warning {
@ -187,15 +191,15 @@
position: absolute; position: absolute;
right: 1rem; right: 1rem;
top: .2rem; top: .2rem;
.save {
color: $blackish;
opacity: .6;
top: 0;
}
&:hover { &:hover {
opacity: .3; opacity: .3;
transition: opacity .2s linear; transition: opacity .2s linear;
} }
.loading-spinner {
@extend %loading-spinner;
max-height: 1.5rem;
max-width: 1.5rem;
}
} }
.edit { .edit {
color: $grayer; color: $grayer;
@ -344,9 +348,8 @@
.level-name { .level-name {
color: darken($whitish, 20%); color: darken($whitish, 20%);
float: right; float: right;
&.loading span { .loading-spinner {
animation: loading .5s linear; @extend %loading-spinner;
animation: spin 1s linear infinite;
} }
} }
} }
@ -413,6 +416,9 @@
} }
} }
} }
.loading-spinner {
@extend %loading-spinner;
}
} }
.us-status { .us-status {

View File

@ -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;
}
}

View File

@ -10,16 +10,10 @@
} }
} }
} }
&.loading { .loading-spinner {
width: 100%; @extend %loading-spinner;
span { margin: 0 auto;
animation: loading .5s linear; max-width: 2rem;
animation: spin 1s linear infinite;
font-size: 30px;
padding: 20px 0;
text-align: center;
width: 100%;
}
} }
.user-avatar { .user-avatar {
flex-grow: 1; flex-grow: 1;

View File

@ -108,6 +108,12 @@
.preview-icon { .preview-icon {
opacity: 0; opacity: 0;
} }
.loading-spinner {
@extend %loading-spinner;
max-height: 1rem;
max-width: 1rem;
}
} }
.show-more-comments { .show-more-comments {
@extend %small; @extend %small;

View File

@ -389,15 +389,57 @@
.lightbox-generic-success, .lightbox-generic-success,
.lightbox-generic-error { .lightbox-generic-error,
.lightbox-generic-loading {
section { section {
flex-basis: 420px; flex-basis: 500px;
flex-grow: 0; flex-grow: 0;
width: 420px; flex-shrink: 0;
width: 500px;
} }
h2 { h2 {
line-height: 2rem; 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 { .lightbox-create-issue {

View File

@ -42,12 +42,24 @@
flex-direction: column; flex-direction: column;
margin-top: 1rem; margin-top: 1rem;
} }
.create-project-button { .create-project-button-wrapper {
display: flex;
flex-shrink: 0; flex-shrink: 0;
margin-top: 1rem; margin-top: 1rem;
a { .create-project-button {
flex-grow: 8;
margin-right: .2rem;
text-align: center; 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, .v-pagination-previous,

View File

@ -35,20 +35,14 @@
opacity: 0; opacity: 0;
transition: all .1s ease-in; transition: all .1s ease-in;
.loading { .loading {
background: $grayer;
border: 1px solid #b8b8b8;
display: inline-block;
margin: 0; margin: 0;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
width: 100%; width: 100%;
.icon-spinner { .loading-spinner {
color: $whitish; @extend %loading-spinner;
float: none; max-height: 1rem;
} max-width: 1rem;
span {
animation: loading .5s linear;
animation: spin 1s linear infinite;
} }
} }
} }

View File

@ -76,7 +76,6 @@
height: 130px; height: 130px;
margin-bottom: 1rem; margin-bottom: 1rem;
margin-right: 1rem; margin-right: 1rem;
overflow: hidden;
position: relative; position: relative;
transition: background-color .3s linear; transition: background-color .3s linear;
width: 23.5%; width: 23.5%;
@ -158,8 +157,24 @@
width: 100%; width: 100%;
} }
.create-project-button-wrapper { .create-project-button-wrapper {
position: relative; display: flex;
width: 100%; 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 { .button-green {
color: $whitish; color: $whitish;

View File

@ -101,6 +101,7 @@ $column-margin: 0 10px 0 0;
margin: $column-margin; margin: $column-margin;
max-width: $column-width; max-width: $column-width;
overflow-y: auto; overflow-y: auto;
widows: $column-width;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }

View File

@ -12,27 +12,29 @@
.image-container { .image-container {
position: relative; position: relative;
} }
img { .avatar {
border: 2px solid $white;
border-radius: 8%; border-radius: 8%;
width: 100%; width: 100%;
} }
.overlay { .overlay {
background: rgba($white, .9); align-content: center;
align-items: center;
background: rgba($blackish, .9);
bottom: 0; bottom: 0;
display: none; display: none;
justify-content: center;
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
width: 100%;
} }
.icon-spinner { .loading-spinner {
@extend %xlarge; @extend %loading-spinner;
animation: spin infinite .8s linear; border: 0;
color: $gray-light; flex-grow: 0;
left: 40%; transform-origin: 32 32;
position: absolute; width: 30%;
top: 40%;
} }
p { p {
@extend %xsmall; @extend %xsmall;

View File

@ -31,18 +31,14 @@
} }
} }
&.loading { &.loading {
background: $grayer;
border: 1px solid #b8b8b8;
margin: 0; margin: 0;
padding: 8px; padding: 8px;
text-align: center; text-align: center;
width: 100%; width: 100%;
.icon-spinner { .loading-spinner {
color: $whitish; @extend %loading-spinner;
float: none; max-height: 1rem;
} max-width: 1rem;
span {
animation: loading .5s linear, spin 1s linear infinite;
} }
} }
} }

View File

@ -0,0 +1 @@
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" version="1.1"><style>.s0{fill:#729fcf;}.s1{fill:#3465a4;}.s2{fill:#204a87;}.s3{fill:#fce94f;}.s4{fill:#edd400;}.s5{fill:#c4a000;}.s6{fill:#8ae234;}.s7{fill:#73d216;}.s8{fill:#4e9a06;}.s9{fill:#ef2929;}.s10{fill:#c00;}.s11{fill:#ce5c00;}</style><path d="M13 13 9.4 9.4c0 0-2 2.1-3 3.4-1 1.2-2.1 3.3-2.1 3.3l4.4 2.5M13 13 9.4 9.4c0 0-2 2.1-3 3.4-1 1.2-2.1 3.3-2.1 3.3l4.4 2.5M13 13 9.4 9.4c0 0-2 2.1-3 3.4-1 1.2-2.1 3.3-2.1 3.3l4.4 2.5" fill="#729fcf"/><path d="m18.6 8.7-2.5-4.4c0 0-2.5 1.5-3.7 2.5-1.3 1-2.9 2.6-2.9 2.6l3.6 3.6M25.1 6 23.7 1.1c0 0-2.8 0.8-4.2 1.4-1.5 0.6-3.5 1.8-3.5 1.8l2.5 4.4" fill="#3465a4"/><path d="m32 5.1 0-5.1c0 0-2.9 0.1-4.5 0.3-1.6 0.2-3.8 0.8-3.8 0.8l1.3 4.9M39 6l1.3-4.9M39 6l1.3-4.9M39 6l1.3-4.9c0 0-2.8-0.7-4.4-0.9C34.4 0 32 0 32 0l0 5.1" fill="#204a87"/><path d="m45.6 8.7 2.6-4.4c0 0-2.5-1.4-4-2C42.7 1.7 40.4 1.1 40.4 1.1l-1.3 4.9M51 13l3.6-3.6M51 13l3.6-3.6M51 13l3.6-3.6c0 0-2.1-2-3.4-3-1.2-1-3.3-2.1-3.3-2.1l-2.5 4.4" fill="#fce94f"/><path d="m55.3 18.6 4.4-2.5c0 0-1.5-2.5-2.5-3.7-1-1.3-2.6-2.9-2.6-2.9l-3.6 3.6M58 25.1l4.9-1.3M58 25.1l4.9-1.3M58 25.1l4.9-1.3c0 0-0.8-2.8-1.4-4.2-0.6-1.5-1.8-3.5-1.8-3.5l-4.4 2.5" fill="#edd400"/><path d="m58.9 32 5.1 0c0 0-0.1-2.9-0.3-4.5-0.2-1.6-0.8-3.8-0.8-3.8l-4.9 1.3M58 39l4.9 1.3M58 39l4.9 1.3M58 39l4.9 1.3c0 0 0.7-2.8 0.9-4.4 0.2-1.6 0.2-3.9 0.2-3.9l-5.1 0" fill="#c4a000"/><path d="m55.3 45.5 4.4 2.6c0 0 1.4-2.5 2-4 0.6-1.5 1.2-3.7 1.2-3.7l-4.9-1.3M51 51l3.6 3.6M51 51l3.6 3.6M51 51l3.6 3.6c0 0 2-2.1 3-3.4 1-1.2 2.1-3.3 2.1-3.3l-4.4-2.5" fill="#8ae234"/><path d="m45.4 55.3 2.5 4.4c0 0 2.5-1.5 3.7-2.5 1.3-1 2.9-2.6 2.9-2.6l-3.6-3.6M38.9 58l1.3 4.9M38.9 58l1.3 4.9M38.9 58l1.3 4.9c0 0 2.8-0.8 4.2-1.4 1.5-0.6 3.5-1.8 3.5-1.8l-2.5-4.4" fill="#73d216"/><path d="m32 58.9 0 5.1c0 0 2.9-0.1 4.5-0.3 1.6-0.2 3.8-0.8 3.8-0.8l-1.3-4.9M25 58l-1.3 4.9M25 58l-1.3 4.9M25 58l-1.3 4.9c0 0 2.8 0.7 4.4 0.9 1.6 0.2 3.9 0.2 3.9 0.2l0-5.1" fill="#4e9a06"/><path d="m18.5 55.3-2.6 4.4c0 0 2.5 1.4 4 2 1.5 0.6 3.7 1.2 3.7 1.2l1.3-4.9M13 51l-3.6 3.6M13 51l-3.6 3.6M13 51l-3.6 3.6c0 0 2.1 2 3.4 3 1.2 1 3.3 2.1 3.3 2.1l2.5-4.4" fill="#ef2929"/><path d="m8.7 45.4-4.4 2.5c0 0 1.5 2.5 2.5 3.7 1 1.3 2.6 2.9 2.6 2.9l3.6-3.6M6 38.9l-4.9 1.3M6 38.9l-4.9 1.3M6 38.9l-4.9 1.3c0 0 0.8 2.8 1.4 4.2 0.6 1.5 1.8 3.5 1.8 3.5l4.4-2.5" fill="#c00"/><path d="m5.1 32-5.1 0c0 0 0.1 2.9 0.3 4.5 0.2 1.6 0.8 3.8 0.8 3.8l4.9-1.3M6 25 1.1 23.7c0 0-0.7 2.8-0.9 4.4-0.2 1.6-0.2 3.9-0.2 3.9l5.1 0" fill="#ce5c00"/><path d="m8.7 18.5-4.4-2.6c0 0-1.4 2.5-2 4-0.6 1.5-1.2 3.7-1.2 3.7l4.9 1.3" fill="#729fcf"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -119,6 +119,7 @@ exports.files = function () {
'modules/admin/admin-submenu-roles', 'modules/admin/admin-submenu-roles',
'modules/admin/admin-roles', 'modules/admin/admin-roles',
'modules/admin/admin-functionalities', 'modules/admin/admin-functionalities',
'modules/admin/admin-project-export',
'modules/admin/admin-membership-table', 'modules/admin/admin-membership-table',
'modules/admin/admin-project-profile', 'modules/admin/admin-project-profile',
'modules/admin/default-values', 'modules/admin/default-values',