diff --git a/.gitignore b/.gitignore index ec0af232..b5e83639 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,10 @@ app/coffee/modules/locales/locale*.coffee *.swp *.swo .#* -tags +/tags tmp/ app/config/main.coffee scss-lint.log e2e/screenshots/ +e2e/reports/ app/modules/compile-modules/ diff --git a/AUTHORS.rst b/AUTHORS.rst index 6b5963bf..3f669728 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,8 +26,10 @@ answer newbie questions, and generally made Taiga that much better: - Guilhem Got - Jordan Rinke - Miguel de la Cruz +- Mika Andrianarijaona - Pilar Esteban - Ramiro Sánchez - Ryan Swanstrom - Vlad Topala - Wil Wade +- Iago Last diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f05dcd6..b4d17be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,42 @@ # Changelog # -## 2.2.0 ???? (Unreleased) +## 3.0.0 Stellaria Borealis (2016-10-02) ### Features -- Show a confirmation notice when you exit edit mode by pressing ESC in the markdown inputs. +- Add Epics. - Add the tribe button to link stories from tree.taiga.io with gigs in tribe.taiga.io. +- Show a confirmation notice when you exit edit mode by pressing ESC in the markdown inputs. +- Errors (not found, server error, permissions and blocked project) don't change the current url. +- Neew Attachments image slider in preview mode. +- New admin area to edit the tag colors used in your project. +- Set color when add a new tags to epics, stories, tasks or issues. +- Display the current user (me) at first in assignment lightbox (thanks to [@mikaoelitiana](https://github.com/mikaoelitiana)) +- Divide the user dashboard in two columns in large screens. +- Upvote and downvote issues from the issues list. +- Show points per role in statsection of the taskboard panel. (thanks to [@fmartingr](https://github.com/fmartingr)) +- Show a funny randon animals/color for users with no avatar (like project logos). +- Show Open Sprints in the left navigation menu (backlog submenu). +- Filters: + - Refactor the filter module. + - Add filters in the kanban panel. + - Add filter in the sprint taskboard panel. +- Cards UI improvements: + - Add zoom levels. + - Show information according the zoom level. + - Show voters, watchers, taks and attachments. + - Improve performance. +- Comments: + - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. + - Ability to edit comments, view edition history and redesign comments module UI. +- Wiki: + - Drag & Drop ordering for wiki links. + - Add a list of all wiki pages + - Add Wiki history +- Third party integrations: + - Included gogs as builtin integration. +- i18n: + - Add norwegian Bokmal (nb) translation. ### Misc - Lots of small and not so small bugfixes. diff --git a/app-loader/app-loader.coffee b/app-loader/app-loader.coffee index cee8fe74..2951fb09 100644 --- a/app-loader/app-loader.coffee +++ b/app-loader/app-loader.coffee @@ -33,7 +33,7 @@ loadStylesheet = (path) -> loadPlugin = (pluginPath) -> return new Promise (resolve, reject) -> - $.getJSON(pluginPath).then (plugin) -> + success = (plugin) -> window.taigaContribPlugins.push(plugin) if plugin.css @@ -45,6 +45,11 @@ loadPlugin = (pluginPath) -> else resolve() + fail = () -> + console.error("error loading", pluginPath); + + $.getJSON(pluginPath).then(success, fail) + loadPlugins = (plugins) -> promises = [] _.map plugins, (pluginPath) -> diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index ac0d17ed..6fd780a3 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -46,7 +46,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $animateProvider.classNameFilter(/^(?:(?!ng-animate-disabled).)*$/) - # wait until the trasnlation is ready to resolve the page + # wait until the translation is ready to resolve the page originalWhen = $routeProvider.when $routeProvider.when = (path, route) -> @@ -57,12 +57,26 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $translate().then () -> deferred.resolve() + return deferred.promise + ], + projectLoaded: ["$q", "tgProjectService", "$route", ($q, projectService, $route) -> + deferred = $q.defer() + + projectService.setSection($route.current.$$route?.section) + + if $route.current.params.pslug + projectService.setProjectBySlug($route.current.params.pslug).then(deferred.resolve) + else + projectService.cleanProject() + deferred.resolve() + return deferred.promise ] }) return originalWhen.call($routeProvider, path, route) + # Home $routeProvider.when("/", { templateUrl: "home/home.html", @@ -76,6 +90,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven } ) + # Discover $routeProvider.when("/discover", { templateUrl: "discover/discover-home/discover-home.html", @@ -97,6 +112,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven } ) + # My Projects $routeProvider.when("/projects/", { templateUrl: "projects/listing/projects-listing.html", @@ -110,17 +126,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven controllerAs: "vm" } ) - - - $routeProvider.when("/blocked-project/:pslug/", - { - templateUrl: "projects/project/blocked-project.html", - loader: true, - controller: "Project", - controllerAs: "vm" - } - ) - + # Project $routeProvider.when("/project/:pslug/", { templateUrl: "projects/project/project.html", @@ -140,6 +146,25 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven } ) + # Epics + $routeProvider.when("/project/:pslug/epics", + { + section: "epics", + templateUrl: "epics/dashboard/epics-dashboard.html", + loader: true, + controller: "EpicsDashboardCtrl", + controllerAs: "vm" + } + ) + + $routeProvider.when("/project/:pslug/epic/:epicref", + { + templateUrl: "epic/epic-detail.html", + loader: true, + section: "epics" + } + ) + $routeProvider.when("/project/:pslug/backlog", { templateUrl: "backlog/backlog.html", @@ -188,6 +213,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven # Wiki $routeProvider.when("/project/:pslug/wiki", {redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, ) + $routeProvider.when("/project/:pslug/wiki-list", + { + templateUrl: "wiki/wiki-list.html", + loader: true, + section: "wiki" + } + ) $routeProvider.when("/project/:pslug/wiki/:slug", { templateUrl: "wiki/wiki.html", @@ -289,7 +321,12 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven section: "admin" } ) - + $routeProvider.when("/project/:pslug/admin/project-values/tags", + { + templateUrl: "admin/admin-project-values-tags.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/memberships", { templateUrl: "admin/admin-memberships.html", @@ -329,6 +366,12 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven section: "admin" } ) + $routeProvider.when("/project/:pslug/admin/third-parties/gogs", + { + templateUrl: "admin/admin-third-parties-gogs.html", + section: "admin" + } + ) # Admin - Contrib Plugins $routeProvider.when("/project/:pslug/admin/contrib/:plugin", {templateUrl: "contrib/main.html"}) @@ -436,6 +479,12 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven ) # Errors/Exceptions + $routeProvider.when("/blocked-project/:pslug/", + { + templateUrl: "projects/project/blocked-project.html", + loader: true, + } + ) $routeProvider.when("/error", {templateUrl: "error/error.html"}) $routeProvider.when("/not-found", @@ -443,7 +492,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $routeProvider.when("/permission-denied", {templateUrl: "error/permission-denied.html"}) - $routeProvider.otherwise({redirectTo: "/not-found"}) + $routeProvider.otherwise({templateUrl: "error/not-found.html"}) $locationProvider.html5Mode({enabled: true, requireBase: false}) defaultHeaders = { @@ -465,15 +514,22 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $tgEventsProvider.setSessionId(taiga.sessionId) # Add next param when user try to access to a secction need auth permissions. - authHttpIntercept = ($q, $location, $navUrls, $lightboxService) -> + authHttpIntercept = ($q, $location, $navUrls, $lightboxService, errorHandlingService) -> httpResponseError = (response) -> if response.status == 0 || (response.status == -1 && !response.config.cancelable) $lightboxService.closeAll() - $location.path($navUrls.resolve("error")) - $location.replace() + + errorHandlingService.error() else if response.status == 401 and $location.url().indexOf('/login') == -1 - nextUrl = encodeURIComponent($location.url()) - $location.url($navUrls.resolve("login")).search("next=#{nextUrl}") + nextUrl = $location.url() + search = $location.search() + + if search.force_next + $location.url($navUrls.resolve("login")) + .search("force_next", search.force_next) + else + $location.url($navUrls.resolve("login")) + .search("next", nextUrl) return $q.reject(response) @@ -482,7 +538,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven } $provide.factory("authHttpIntercept", ["$q", "$location", "$tgNavUrls", "lightboxService", - authHttpIntercept]) + "tgErrorHandlingService", authHttpIntercept]) $httpProvider.interceptors.push("authHttpIntercept") @@ -536,18 +592,14 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $httpProvider.interceptors.push("versionCheckHttpIntercept") - blockingIntercept = ($q, $routeParams, $location, $navUrls) -> + blockingIntercept = ($q, errorHandlingService) -> # API calls can return blocked elements and in that situation the user will be redirected # to the blocked project page # This can happens in two scenarios # - An ok response containing a blocked_code in the data # - An error reponse when updating/creating/deleting including a 451 error code redirectToBlockedPage = -> - pslug = $routeParams.pslug - blockedUrl = $navUrls.resolve("blocked-project", {project: pslug}) - currentUrl = $location.url() - if currentUrl.indexOf(blockedUrl) == -1 - $location.replace().path(blockedUrl) + errorHandlingService.block() responseOk = (response) -> if response.data.blocked_code @@ -566,7 +618,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven responseError: responseError } - $provide.factory("blockingIntercept", ["$q", "$routeParams", "$location", "$tgNavUrls", blockingIntercept]) + $provide.factory("blockingIntercept", ["$q", "tgErrorHandlingService", blockingIntercept]) $httpProvider.interceptors.push("blockingIntercept") @@ -637,7 +689,8 @@ i18nInit = (lang, $translate) -> checksley.updateMessages('default', messages) -init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService, navigationBarService) -> +init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, + loaderService, navigationBarService, errorHandlingService) -> $log.debug("Initialize application") $rootscope.$on '$translatePartialLoaderStructureChanged', () -> @@ -680,6 +733,10 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na # Analytics $analytics.initialize() + # Initialize error handling service when location change start + $rootscope.$on '$locationChangeStart', (event) -> + errorHandlingService.init() + # On the first page load the loader is painted in `$routeChangeSuccess` # because we need to hide the tg-navigation-bar. # In the other cases the loader is in `$routeChangeSuccess` @@ -690,7 +747,7 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na un() - $rootscope.$on '$routeChangeSuccess', (event, next) -> + $rootscope.$on '$routeChangeSuccess', (event, next) -> if next.loader loaderService.start(true) @@ -698,13 +755,6 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na if !$auth.isAuthenticated() $location.path($navUrls.resolve("login")) - projectService.setSection(next.section) - - if next.params.pslug - projectService.setProjectBySlug(next.params.pslug) - else - projectService.cleanProject() - if next.title or next.description title = $translate.instant(next.title or "") description = $translate.instant(next.description or "") @@ -712,7 +762,7 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na if next.mobileViewport appMetaService.addMobileViewport() - else + else appMetaService.removeMobileViewport() if next.disableHeader @@ -720,10 +770,13 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na else navigationBarService.enableHeader() -pluginsWithModule = _.filter(@.taigaContribPlugins, (plugin) -> plugin.module) - +# Config for infinite scroll angular.module('infinite-scroll').value('THROTTLE_MILLISECONDS', 500) +# Load modules +pluginsWithModule = _.filter(@.taigaContribPlugins, (plugin) -> plugin.module) +pluginsModules = _.map(pluginsWithModule, (plugin) -> plugin.module) + modules = [ # Main Global Modules "taigaBase", @@ -754,12 +807,17 @@ modules = [ "taigaPlugins", "taigaIntegrations", "taigaComponents", + # new modules "taigaProfile", "taigaHome", "taigaUserTimeline", "taigaExternalApps", "taigaDiscover", + "taigaHistory", + "taigaWikiHistory", + "taigaEpics", + "taigaUtils" # template cache "templates", @@ -772,7 +830,7 @@ modules = [ "pascalprecht.translate", "infinite-scroll", "tgRepeat" -].concat(_.map(pluginsWithModule, (plugin) -> plugin.module)) +].concat(pluginsModules) # Main module definition module = angular.module("taiga", modules) @@ -800,9 +858,8 @@ module.run([ "$tgLocation", "$tgNavUrls", "tgAppMetaService", - "tgProjectService", "tgLoader", "tgNavigationBarService", - "$route", + "tgErrorHandlingService", init ]) diff --git a/app/coffee/classes.coffee b/app/coffee/classes.coffee index a3a74a31..89a12998 100644 --- a/app/coffee/classes.coffee +++ b/app/coffee/classes.coffee @@ -28,11 +28,9 @@ class TaigaController extends TaigaBase onInitialDataError: (xhr) => if xhr if xhr.status == 404 - @location.path(@navUrls.resolve("not-found")) - @location.replace() + @errorHandlingService.notfound() else if xhr.status == 403 - @location.path(@navUrls.resolve("permission-denied")) - @location.replace() + @errorHandlingService.permissionDenied() return @q.reject(xhr) diff --git a/app/coffee/modules/admin/lightboxes.coffee b/app/coffee/modules/admin/lightboxes.coffee index 4ff79aec..9802d72a 100644 --- a/app/coffee/modules/admin/lightboxes.coffee +++ b/app/coffee/modules/admin/lightboxes.coffee @@ -99,7 +99,14 @@ class LightboxAddMembersController _onErrorInvite: (response) -> @.submitInvites = false - @.form.setErrors(response.data) + errors = {} + _.each response.data.bulk_memberships, (value, index) => + if value.email + errors["email-#{index}"] = value.email[0] + if value.role + errors["role-#{index}"] = value.role[0] + + @.form.setErrors(errors) if response.data._error_message @confirm.notify("error", response.data._error_message) diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee index e0404170..1ab6c54f 100644 --- a/app/coffee/modules/admin/memberships.coffee +++ b/app/coffee/modules/admin/memberships.coffee @@ -48,12 +48,13 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai "$tgAnalytics", "tgAppMetaService", "$translate", - "$tgAuth" - "tgLightboxFactory" + "$tgAuth", + "tgLightboxFactory", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @analytics, - @appMetaService, @translate, @auth, @lightboxFactory) -> + @appMetaService, @translate, @auth, @lightboxFactory, @errorHandlingService) -> bindMethods(@) @scope.project = {} @@ -75,7 +76,7 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_admin - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -240,16 +241,19 @@ module.directive("tgMemberships", ["$tgTemplate", "$compile", MembershipsDirecti ## Member Avatar Directive ############################################################################# -MembershipsRowAvatarDirective = ($log, $template, $translate, $compile) -> +MembershipsRowAvatarDirective = ($log, $template, $translate, $compile, avatarService) -> template = $template.get("admin/memberships-row-avatar.html", true) link = ($scope, $el, $attrs) -> pending = $translate.instant("ADMIN.MEMBERSHIP.STATUS_PENDING") render = (member) -> + avatar = avatarService.getAvatar(member) + ctx = { full_name: if member.full_name then member.full_name else "" email: if member.user_email then member.user_email else member.email - imgurl: if member.photo then member.photo else "/" + window._version + "/images/unnamed.png" + imgurl: avatar.url + bg: avatar.bg pending: if !member.is_user_active then pending else "" isOwner: member.is_owner } @@ -271,7 +275,7 @@ MembershipsRowAvatarDirective = ($log, $template, $translate, $compile) -> return {link: link} -module.directive("tgMembershipsRowAvatar", ["$log", "$tgTemplate", '$translate', "$compile", MembershipsRowAvatarDirective]) +module.directive("tgMembershipsRowAvatar", ["$log", "$tgTemplate", '$translate', "$compile", "tgAvatarService", MembershipsRowAvatarDirective]) ############################################################################# diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 7bc7efbf..65eb759c 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -53,14 +53,16 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) "tgAppMetaService", "$translate", "$tgAuth", - "tgCurrentUserService" + "tgCurrentUserService", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, - @appMetaService, @translate, @tgAuth, @currentUserService) -> + @appMetaService, @translate, @tgAuth, @currentUserService, @errorHandlingService) -> @scope.project = {} promise = @.loadInitialData() + @scope.projectTags = [] promise.then => sectionName = @translate.instant( @scope.sectionName) @@ -83,18 +85,23 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_admin - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project - @scope.pointsList = _.sortBy(project.points, "order") + @scope.epicStatusList = _.sortBy(project.epic_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order") + @scope.pointsList = _.sortBy(project.points, "order") @scope.taskStatusList = _.sortBy(project.task_statuses, "order") - @scope.prioritiesList = _.sortBy(project.priorities, "order") - @scope.severitiesList = _.sortBy(project.severities, "order") @scope.issueTypesList = _.sortBy(project.issue_types, "order") @scope.issueStatusList = _.sortBy(project.issue_statuses, "order") + @scope.prioritiesList = _.sortBy(project.priorities, "order") + @scope.severitiesList = _.sortBy(project.severities, "order") @scope.$emit('project:loaded', project) + + @scope.projectTags = _.map @scope.project.tags, (it) => + return [it, @scope.project.tags_colors[it]] + return project loadInitialData: -> @@ -106,6 +113,21 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) openDeleteLightbox: -> @rootscope.$broadcast("deletelightbox:new", @scope.project) + addTag: (name, color) -> + tags = _.clone(@scope.project.tags) + + tags.push(name) + + @scope.projectTags.push([name, null]) + @scope.project.tags = tags + + deleteTag: (tag) -> + tags = _.clone(@scope.project.tags) + _.pull(tags, tag[0]) + _.remove @scope.projectTags, (it) => it[0] == tag[0] + + @scope.project.tags = tags + module.controller("ProjectProfileController", ProjectProfileController) @@ -222,10 +244,12 @@ ProjectModulesDirective = ($repo, $confirm, $loading, projectService) -> $el.on "change", ".module-activation.module-direct-active input", (event) -> event.preventDefault() - submit() + + $scope.$applyAsync(submit) $el.on "submit", "form", (event) -> event.preventDefault() + submit() $el.on "click", ".save", (event) -> @@ -401,6 +425,10 @@ class CsvExporterController extends taiga.Controller @._generateUuid() +class CsvExporterEpicsController extends CsvExporterController + type: "epics" + + class CsvExporterUserstoriesController extends CsvExporterController type: "userstories" @@ -413,6 +441,7 @@ class CsvExporterIssuesController extends CsvExporterController type: "issues" +module.controller("CsvExporterEpicsController", CsvExporterEpicsController) module.controller("CsvExporterUserstoriesController", CsvExporterUserstoriesController) module.controller("CsvExporterTasksController", CsvExporterTasksController) module.controller("CsvExporterIssuesController", CsvExporterIssuesController) @@ -422,6 +451,21 @@ module.controller("CsvExporterIssuesController", CsvExporterIssuesController) ## CSV Directive ############################################################################# +CsvEpicDirective = ($translate) -> + link = ($scope) -> + $scope.sectionTitle = "ADMIN.CSV.SECTION_TITLE_EPIC" + + return { + controller: "CsvExporterEpicsController", + controllerAs: "ctrl", + templateUrl: "admin/project-csv.html", + link: link, + scope: true + } + +module.directive("tgCsvEpic", ["$translate", CsvEpicDirective]) + + CsvUsDirective = ($translate) -> link = ($scope) -> $scope.sectionTitle = "ADMIN.CSV.SECTION_TITLE_US" diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee index 1d9229ee..a00d4710 100644 --- a/app/coffee/modules/admin/project-values.coffee +++ b/app/coffee/modules/admin/project-values.coffee @@ -31,6 +31,8 @@ joinStr = @.taiga.joinStr groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce debounce = @.taiga.debounce +getDefaulColorList = @.taiga.getDefaulColorList + module = angular.module("taigaAdmin") @@ -50,11 +52,12 @@ class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageM "$tgLocation", "$tgNavUrls", "tgAppMetaService", - "$translate" + "$translate", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, - @appMetaService, @translate) -> + @appMetaService, @translate, @errorHandlingService) -> @scope.project = {} promise = @.loadInitialData() @@ -74,7 +77,7 @@ class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageM loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_admin - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -156,7 +159,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) $scope.$on "$destroy", -> @@ -178,7 +181,9 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra } initializeTextTranslations = -> - $scope.addNewElementText = $translate.instant("ADMIN.PROJECT_VALUES_#{objName.toUpperCase()}.ACTION_ADD") + $scope.addNewElementText = $translate.instant( + "ADMIN.PROJECT_VALUES_#{objName.toUpperCase()}.ACTION_ADD" + ) initializeNewValue() initializeTextTranslations() @@ -265,14 +270,6 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra editionRow.removeClass('hidden') editionRow.find('input:visible').first().focus().select() - $el.on "keyup", ".edition input", (event) -> - if event.keyCode == 13 - target = angular.element(event.currentTarget) - saveValue(target) - else if event.keyCode == 27 - target = angular.element(event.currentTarget) - cancel(target) - $el.on "keyup", ".new-value input", (event) -> if event.keyCode == 13 target = $el.find(".new-value") @@ -327,7 +324,8 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra return {link:link} -module.directive("tgProjectValues", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame", "$translate", "$rootScope", ProjectValuesDirective]) +module.directive("tgProjectValues", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame", + "$translate", "$rootScope", ProjectValuesDirective]) ############################################################################# @@ -338,6 +336,12 @@ ColorSelectionDirective = () -> ## Color selection Link link = ($scope, $el, $attrs, $model) -> + $scope.colorList = getDefaulColorList() + + $scope.allowEmpty = false + if $attrs.tgAllowEmpty + $scope.allowEmpty = true + $ctrl = $el.controller() $scope.$watch $attrs.ngModel, (element) -> @@ -348,7 +352,7 @@ ColorSelectionDirective = () -> event.preventDefault() event.stopPropagation() target = angular.element(event.currentTarget) - $el.find(".select-color").hide() + $(".select-color").hide() target.siblings(".select-color").show() # Hide when click outside body = angular.element("body") @@ -371,6 +375,16 @@ ColorSelectionDirective = () -> $model.$modelValue.color = $scope.color $el.find(".select-color").hide() + $el.on "keyup", "input", (event) -> + event.stopPropagation() + if event.keyCode == 13 + $scope.$apply -> + $model.$modelValue.color = $scope.color + $el.find(".select-color").hide() + + else if event.keyCode == 27 + $el.find(".select-color").hide() + $scope.$on "$destroy", -> $el.off() @@ -688,3 +702,289 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame, $translate) module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", "$translate", ProjectCustomAttributesDirective]) + + +############################################################################# +## Tags Controller +############################################################################# + +class ProjectTagsController extends taiga.Controller + @.$inject = [ + "$scope", + "$rootScope", + "$tgRepo", + "$tgConfirm", + "$tgResources", + "$tgModel", + ] + + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @model) -> + @.loading = true + @rootscope.$on("project:loaded", @.loadTags) + + loadTags: => + return @rs.projects.tagsColors(@scope.projectId).then (tags) => + @scope.projectTagsAll = _.map tags.getAttrs(), (color, name) => + @model.make_model('tag', {name: name, color: color}) + @.filterAndSortTags() + @.loading = false + + filterAndSortTags: => + @scope.projectTags = _.filter( + _.sortBy(@scope.projectTagsAll, "name"), + (tag) => tag.name.indexOf(@scope.tagsFilter.name) != -1 + ) + + createTag: (tag, color) => + return @rs.projects.createTag(@scope.projectId, tag, color) + + editTag: (from_tag, to_tag, color) => + if from_tag == to_tag + to_tag = null + + return @rs.projects.editTag(@scope.projectId, from_tag, to_tag, color) + + deleteTag: (tag) => + @scope.loadingDelete = true + return @rs.projects.deleteTag(@scope.projectId, tag).finally => + @scope.loadingDelete = false + + startMixingTags: (tag) => + @scope.mixingTags.toTag = tag.name + + toggleMixingFromTags: (tag) => + if tag.name != @scope.mixingTags.toTag + index = @scope.mixingTags.fromTags.indexOf(tag.name) + if index == -1 + @scope.mixingTags.fromTags.push(tag.name) + else + @scope.mixingTags.fromTags.splice(index, 1) + + confirmMixingTags: () => + toTag = @scope.mixingTags.toTag + fromTags = @scope.mixingTags.fromTags + @scope.loadingMixing = true + @rs.projects.mixTags(@scope.projectId, toTag, fromTags) + .then => + @.cancelMixingTags() + @.loadTags() + .finally => + @scope.loadingMixing = false + + cancelMixingTags: () => + @scope.mixingTags.toTag = null + @scope.mixingTags.fromTags = [] + + mixingClass: (tag) => + if @scope.mixingTags.toTag != null + if tag.name == @scope.mixingTags.toTag + return "mixing-tags-to" + else if @scope.mixingTags.fromTags.indexOf(tag.name) != -1 + return "mixing-tags-from" + +module.controller("ProjectTagsController", ProjectTagsController) + + +############################################################################# +## Tags directive +############################################################################# + +ProjectTagsDirective = ($log, $repo, $confirm, $location, animationFrame, $translate, $rootscope) -> + link = ($scope, $el, $attrs) -> + $window = $(window) + $ctrl = $el.controller() + valueType = $attrs.type + objName = $attrs.objname + + initializeNewValue = -> + $scope.newValue = { + "tag": "" + "color": "" + } + + initializeTagsFilter = -> + $scope.tagsFilter = { + "name": "" + } + + initializeMixingTags = -> + $scope.mixingTags = { + "toTag": null, + "fromTags": [] + } + + initializeTextTranslations = -> + $scope.addNewElementText = $translate.instant("ADMIN.PROJECT_VALUES_TAGS.ACTION_ADD") + + initializeNewValue() + initializeTagsFilter() + initializeMixingTags() + initializeTextTranslations() + + $rootscope.$on "$translateChangeEnd", -> + $scope.$evalAsync(initializeTextTranslations) + + goToBottomList = (focus = false) => + table = $el.find(".table-main") + + $(document.body).scrollTop(table.offset().top + table.height()) + + if focus + $el.find(".new-value input:visible").first().focus() + + saveValue = (target) => + formEl = target.parents("form") + form = formEl.checksley() + return if not form.validate() + + tag = formEl.scope().tag + originalTag = tag.clone() + originalTag.revert() + + $scope.loadingEdit = true + promise = $ctrl.editTag(originalTag.name, tag.name, tag.color) + promise.then => + $ctrl.loadTags().then => + row = target.parents(".row.table-main") + row.addClass("hidden") + $scope.loadingEdit = false + row.siblings(".visualization").removeClass('hidden') + + promise.then null, (response) -> + $scope.loadingEdit = false + form.setErrors(response.data) + + saveNewValue = (target) => + formEl = target.parents("form") + formEl = target + form = formEl.checksley() + return if not form.validate() + + $scope.loadingCreate = true + promise = $ctrl.createTag($scope.newValue.tag, $scope.newValue.color) + promise.then (data) => + $ctrl.loadTags().then => + $scope.loadingCreate = false + target.addClass("hidden") + initializeNewValue() + + promise.then null, (response) -> + $scope.loadingCreate = false + form.setErrors(response.data) + + cancel = (target) -> + row = target.parents(".row.table-main") + formEl = target.parents("form") + tag = formEl.scope().tag + + $scope.$apply -> + row.addClass("hidden") + tag.revert() + row.siblings(".visualization").removeClass('hidden') + + $scope.$watch "tagsFilter.name", (tagsFilter) -> + $ctrl.filterAndSortTags() + + $window.on "keyup", (event) -> + if event.keyCode == 27 + $scope.$apply -> + initializeMixingTags() + + $el.on "click", ".show-add-new", (event) -> + event.preventDefault() + $el.find(".new-value").removeClass('hidden') + + $el.on "click", ".add-new", debounce 2000, (event) -> + event.preventDefault() + target = $el.find(".new-value") + saveNewValue(target) + + $el.on "click", ".delete-new", (event) -> + event.preventDefault() + $el.find(".new-value").addClass("hidden") + initializeNewValue() + + $el.on "click", ".mix-tags", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + $scope.$apply -> + $ctrl.startMixingTags(target.parents('form').scope().tag) + + $el.on "click", ".mixing-row", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + $scope.$apply -> + $ctrl.toggleMixingFromTags(target.parents('form').scope().tag) + + $el.on "click", ".mixing-confirm", (event) -> + event.preventDefault() + event.stopPropagation() + $scope.$apply -> + $ctrl.confirmMixingTags() + + $el.on "click", ".mixing-cancel", (event) -> + event.preventDefault() + event.stopPropagation() + $scope.$apply -> + $ctrl.cancelMixingTags() + + $el.on "click", ".edit-value", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + + row = target.parents(".row.table-main") + row.addClass("hidden") + + editionRow = row.siblings(".edition") + editionRow.removeClass('hidden') + editionRow.find('input:visible').first().focus().select() + + $el.on "keyup", ".new-value input", (event) -> + if event.keyCode == 13 + target = $el.find(".new-value") + saveNewValue(target) + else if event.keyCode == 27 + $el.find(".new-value").addClass("hidden") + initializeNewValue() + + $el.on "keyup", ".status-name input", (event) -> + target = angular.element(event.currentTarget) + if event.keyCode == 13 + saveValue(target) + else if event.keyCode == 27 + cancel(target) + + $el.on "click", ".save", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + saveValue(target) + + $el.on "click", ".cancel", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + cancel(target) + + $el.on "click", ".delete-tag", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + formEl = target.parents("form") + tag = formEl.scope().tag + + title = $translate.instant("ADMIN.COMMON.TITLE_ACTION_DELETE_TAG") + + $confirm.askOnDelete(title, tag.name).then (response) -> + onSucces = -> + $ctrl.loadTags().finally -> + response.finish() + onError = -> + $confirm.notify("error") + $ctrl.deleteTag(tag.name).then(onSucces, onError) + + $scope.$on "$destroy", -> + $el.off() + $window.off() + + return {link:link} + +module.directive("tgProjectTags", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame", + "$translate", "$rootScope", ProjectTagsDirective]) diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee index 9d4173a2..11fb5f4c 100644 --- a/app/coffee/modules/admin/roles.coffee +++ b/app/coffee/modules/admin/roles.coffee @@ -48,11 +48,12 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil "$tgLocation", "$tgNavUrls", "tgAppMetaService", - "$translate" + "$translate", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, - @appMetaService, @translate) -> + @appMetaService, @translate, @errorHandlingService) -> bindMethods(@) @scope.sectionName = "ADMIN.MENU.PERMISSIONS" @@ -71,7 +72,7 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_admin - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -98,6 +99,7 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil @scope.roles = roles @scope.role = @scope.roles[0] + return roles loadInitialData: -> @@ -351,6 +353,18 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> categories = [] + epicPermissions = [ + { key: "view_epics", name: "COMMON.PERMISIONS_CATEGORIES.EPICS.VIEW_EPICS"} + { key: "add_epic", name: "COMMON.PERMISIONS_CATEGORIES.EPICS.ADD_EPICS"} + { key: "modify_epic", name: "COMMON.PERMISIONS_CATEGORIES.EPICS.MODIFY_EPICS"} + { key: "comment_epic", name: "COMMON.PERMISIONS_CATEGORIES.EPICS.COMMENT_EPICS"} + { key: "delete_epic", name: "COMMON.PERMISIONS_CATEGORIES.EPICS.DELETE_EPICS"} + ] + categories.push({ + name: "COMMON.PERMISIONS_CATEGORIES.EPICS.NAME" , + permissions: setActivePermissions(epicPermissions) + }) + milestonePermissions = [ { key: "view_milestones", name: "COMMON.PERMISIONS_CATEGORIES.SPRINTS.VIEW_SPRINTS"} { key: "add_milestone", name: "COMMON.PERMISIONS_CATEGORIES.SPRINTS.ADD_SPRINTS"} @@ -366,6 +380,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.VIEW_USER_STORIES"} { key: "add_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.ADD_USER_STORIES"} { key: "modify_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.MODIFY_USER_STORIES"} + { key: "comment_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_USER_STORIES"} { key: "delete_us", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.DELETE_USER_STORIES"} ] categories.push({ @@ -377,6 +392,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_tasks", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.VIEW_TASKS"} { key: "add_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.ADD_TASKS"} { key: "modify_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.MODIFY_TASKS"} + { key: "comment_task", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_TASKS"} { key: "delete_task", name: "COMMON.PERMISIONS_CATEGORIES.TASKS.DELETE_TASKS"} ] categories.push({ @@ -388,6 +404,7 @@ RolePermissionsDirective = ($rootscope, $repo, $confirm, $compile) -> { key: "view_issues", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.VIEW_ISSUES"} { key: "add_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.ADD_ISSUES"} { key: "modify_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.MODIFY_ISSUES"} + { key: "comment_issue", name: "COMMON.PERMISIONS_CATEGORIES.USER_STORIES.COMMENT_ISSUES"} { key: "delete_issue", name: "COMMON.PERMISIONS_CATEGORIES.ISSUES.DELETE_ISSUES"} ] categories.push({ diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index 9cc4eaf6..74ea9ed7 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -45,10 +45,11 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga. "$tgLocation", "$tgNavUrls", "tgAppMetaService", - "$translate" + "$translate", + "tgErrorHandlingService" ] - constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appMetaService, @translate) -> + constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appMetaService, @translate, @errorHandlingService) -> bindMethods(@) @scope.sectionName = "ADMIN.WEBHOOKS.SECTION_NAME" @@ -72,7 +73,7 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga. loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_admin - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -193,15 +194,22 @@ WebhookDirective = ($rs, $repo, $confirm, $loading, $translate) -> $el.on "click", ".toggle-history", (event) -> target = angular.element(event.currentTarget) + if not webhook.logs? or webhook.logs.length == 0 updateLogs().then -> #Waiting for ng-repeat to finish timeout 0, -> - $el.find(".webhooks-history").toggleClass("open") + $el.find(".webhooks-history") + .toggleClass("open") + .slideToggle() + updateShowHideHistoryText() else - $el.find(".webhooks-history").toggleClass("open") + $el.find(".webhooks-history") + .toggleClass("open") + .slideToggle() + $scope.$apply () -> updateShowHideHistoryText() @@ -578,3 +586,50 @@ ValidOriginIpsDirective = -> } module.directive("tgValidOriginIps", ValidOriginIpsDirective) + +############################################################################# +## Gogs Controller +############################################################################# + +class GogsController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) + @.$inject = [ + "$scope", + "$tgRepo", + "$tgResources", + "$routeParams", + "tgAppMetaService", + "$translate" + ] + + constructor: (@scope, @repo, @rs, @params, @appMetaService, @translate) -> + bindMethods(@) + + @scope.sectionName = @translate.instant("ADMIN.GOGS.SECTION_NAME") + @scope.project = {} + + promise = @.loadInitialData() + + promise.then () => + title = @translate.instant("ADMIN.GOGS.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) + + promise.then null, @.onInitialDataError.bind(@) + + loadModules: -> + return @rs.modules.list(@scope.projectId, "gogs").then (gogs) => + @scope.gogs = gogs + + loadProject: -> + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id + @scope.project = project + @scope.$emit('project:loaded', project) + return project + + loadInitialData: -> + promise = @.loadProject() + promise.then(=> @.loadModules()) + return promise + +module.controller("GogsController", GogsController) diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index a2e66632..def65a3c 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -37,12 +37,13 @@ class LoginPage constructor: (currentUserService, $location, $navUrls, $routeParams) -> if currentUserService.isAuthenticated() - url = $navUrls.resolve("home") - if $routeParams['next'] - url = $routeParams['next'] - $location.search('next', null) + if not $routeParams['force_login'] + url = $navUrls.resolve("home") + if $routeParams['next'] + url = decodeURIComponent($routeParams['next']) + $location.search('next', null) - $location.path(url) + $location.url(url) module.controller('LoginPage', LoginPage) @@ -243,8 +244,9 @@ PublicRegisterMessageDirective = ($config, $navUrls, $routeParams, templates) -> return "" url = $navUrls.resolve("register") - if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("register") - nextUrl = encodeURIComponent($routeParams['next']) + + if $routeParams['force_next'] + nextUrl = encodeURIComponent($routeParams['force_next']) url += "?next=#{nextUrl}" return template({url:url}) @@ -259,7 +261,7 @@ module.directive("tgPublicRegisterMessage", ["$tgConfig", "$tgNavUrls", "$routeP "$tgTemplate", PublicRegisterMessageDirective]) -LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $events, $translate) -> +LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $events, $translate, $window) -> link = ($scope, $el, $attrs) -> form = new checksley.Form($el.find("form.login-form")) @@ -268,9 +270,16 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $ else $scope.nextUrl = $navUrls.resolve("home") + if $routeParams['force_next'] + $scope.nextUrl = decodeURIComponent($routeParams['force_next']) + onSuccess = (response) -> $events.setupConnection() - $location.url($scope.nextUrl) + + if $scope.nextUrl.indexOf('http') == 0 + $window.location.href = $scope.nextUrl + else + $location.url($scope.nextUrl) onError = (response) -> $confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT")) @@ -308,14 +317,14 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $ return {link:link} module.directive("tgLogin", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgConfig", "$routeParams", - "$tgNavUrls", "$tgEvents", "$translate", LoginDirective]) + "$tgNavUrls", "$tgEvents", "$translate", "$window", LoginDirective]) ############################################################################# ## Register Directive ############################################################################# -RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $routeParams, $analytics, $translate) -> +RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $routeParams, $analytics, $translate, $window) -> link = ($scope, $el, $attrs) -> if not $config.get("publicRegisterEnabled") $location.path($navUrls.resolve("not-found")) @@ -324,12 +333,18 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $routeParams $scope.data = {} form = $el.find("form").checksley({onlyOneErrorElement: true}) - $scope.nextUrl = $navUrls.resolve("home") + if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login") + $scope.nextUrl = decodeURIComponent($routeParams['next']) + else + $scope.nextUrl = $navUrls.resolve("home") onSuccessSubmit = (response) -> $analytics.trackEvent("auth", "register", "user registration", 1) - $location.url($scope.nextUrl) + if $scope.nextUrl.indexOf('http') == 0 + $window.location.href = $scope.nextUrl + else + $location.url($scope.nextUrl) onErrorSubmit = (response) -> if response.data._error_message @@ -357,7 +372,7 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $routeParams return {link:link} module.directive("tgRegister", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgNavUrls", "$tgConfig", - "$routeParams", "$tgAnalytics", "$translate", RegisterDirective]) + "$routeParams", "$tgAnalytics", "$translate", "$window", RegisterDirective]) ############################################################################# @@ -458,13 +473,14 @@ module.directive("tgChangePasswordFromRecovery", ["$tgAuth", "$tgConfirm", "$tgL ## Invitation ############################################################################# -InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics, $translate) -> +InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics, $translate, config) -> link = ($scope, $el, $attrs) -> token = $params.token promise = $auth.getInvitation(token) promise.then (invitation) -> $scope.invitation = invitation + $scope.publicRegisterEnabled = config.get("publicRegisterEnabled") promise.then null, (response) -> $location.path($navUrls.resolve("login")) @@ -535,7 +551,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics return {link:link} module.directive("tgInvitation", ["$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", - "$tgNavUrls", "$tgAnalytics", "$translate", InvitationDirective]) + "$tgNavUrls", "$tgAnalytics", "$translate", "$tgConfig", InvitationDirective]) ############################################################################# diff --git a/app/coffee/modules/backlog/filters.coffee b/app/coffee/modules/backlog/filters.coffee deleted file mode 100644 index 39a0dfa9..00000000 --- a/app/coffee/modules/backlog/filters.coffee +++ /dev/null @@ -1,185 +0,0 @@ -### -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino Garcia -# Copyright (C) 2014-2016 David Barragán Merino -# Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Juan Francisco Alcántara -# Copyright (C) 2014-2016 Xavi Julian -# -# 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/backlog/main.coffee -### - -taiga = @.taiga - -mixOf = @.taiga.mixOf -toggleText = @.taiga.toggleText -scopeDefer = @.taiga.scopeDefer -bindOnce = @.taiga.bindOnce -groupBy = @.taiga.groupBy -debounceLeading = @.taiga.debounceLeading - - -module = angular.module("taigaBacklog") - -############################################################################# -## Issues Filters Directive -############################################################################# - -BacklogFiltersDirective = ($q, $log, $location, $template, $compile) -> - template = $template.get("backlog/filters.html", true) - templateSelected = $template.get("backlog/filter-selected.html", true) - - link = ($scope, $el, $attrs) -> - currentFiltersType = '' - - $ctrl = $el.closest(".wrapper").controller() - selectedFilters = [] - - showFilters = (title, type) -> - $el.find(".filters-cats").hide() - $el.find(".filter-list").removeClass("hidden") - $el.find("h2.breadcrumb").removeClass("hidden") - $el.find("h2 a.subfilter span.title").html(title) - $el.find("h2 a.subfilter span.title").prop("data-type", type) - - currentFiltersType = getFiltersType() - - showCategories = -> - $el.find(".filters-cats").show() - $el.find(".filter-list").addClass("hidden") - $el.find("h2.breadcrumb").addClass("hidden") - - initializeSelectedFilters = () -> - showCategories() - selectedFilters = [] - - for name, values of $scope.filters - for val in values - selectedFilters.push(val) if val.selected - - renderSelectedFilters() - - renderSelectedFilters = -> - _.map selectedFilters, (f) => - if f.color - f.style = "border-left: 3px solid #{f.color}" - - html = templateSelected({filters: selectedFilters}) - html = $compile(html)($scope) - - $el.find(".filters-applied").html(html) - - renderFilters = (filters) -> - _.map filters, (f) => - if f.color - f.style = "border-left: 3px solid #{f.color}" - - html = template({filters:filters}) - html = $compile(html)($scope) - $el.find(".filter-list").html(html) - - getFiltersType = () -> - return $el.find("h2 a.subfilter span.title").prop('data-type') - - reloadUserstories = () -> - currentFiltersType = getFiltersType() - - $q.all([$ctrl.loadUserstories(true), $ctrl.generateFilters()]).then () -> - currentFilters = $scope.filters[currentFiltersType] - renderFilters(_.reject(currentFilters, "selected")) - - toggleFilterSelection = (type, id) -> - currentFiltersType = getFiltersType() - - filters = $scope.filters[type] - filter = _.find(filters, {id: id}) - filter.selected = (not filter.selected) - - if filter.selected - selectedFilters.push(filter) - $scope.$apply -> - $ctrl.selectFilter(type, id) - else - selectedFilters = _.reject selectedFilters, (selected) -> - return filter.type == selected.type && filter.id == selected.id - - $ctrl.unselectFilter(type, id) - - renderSelectedFilters(selectedFilters) - - if type == currentFiltersType - renderFilters(_.reject(filters, "selected")) - - reloadUserstories() - - selectQFilter = debounceLeading 100, (value) -> - return if value is undefined - - if value.length == 0 - $ctrl.replaceFilter("q", null) - else - $ctrl.replaceFilter("q", value) - - reloadUserstories() - - $scope.$watch("filtersQ", selectQFilter) - - ## Angular Watchers - $scope.$on "backlog:loaded", (ctx) -> - initializeSelectedFilters() - - $scope.$on "filters:update", (ctx) -> - $ctrl.generateFilters().then () -> - filters = $scope.filters[currentFiltersType] - - if currentFiltersType - renderFilters(_.reject(filters, "selected")) - - ## Dom Event Handlers - $el.on "click", ".filters-cats > ul > li > a", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - tags = $scope.filters[target.data("type")] - - renderFilters(_.reject(tags, "selected")) - showFilters(target.attr("title"), target.data('type')) - - $el.on "click", ".filters-inner > .filters-step-cat > .breadcrumb > .back", (event) -> - event.preventDefault() - showCategories() - - $el.on "click", ".remove-filter", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget).parent() - id = target.data("id") - type = target.data("type") - toggleFilterSelection(type, id) - - $el.on "click", ".filter-list .single-filter", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - if target.hasClass("active") - target.removeClass("active") - else - target.addClass("active") - - id = target.data("id") - type = target.data("type") - toggleFilterSelection(type, id) - - return {link:link} - -module.directive("tgBacklogFilters", ["$q", "$log", "$tgLocation", "$tgTemplate", "$compile", BacklogFiltersDirective]) diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index de1f7807..c742d74f 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -39,7 +39,7 @@ module = angular.module("taigaBacklog") ## Backlog Controller ############################################################################# -class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) +class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin, taiga.UsFiltersMixin) @.$inject = [ "$scope", "$rootScope", @@ -56,18 +56,32 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F "$translate", "$tgLoading", "tgResources", - "$tgQueueModelTransformation" + "$tgQueueModelTransformation", + "tgErrorHandlingService", + "$tgStorage", + "tgFilterRemoteStorageService" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, - @location, @appMetaService, @navUrls, @events, @analytics, @translate, @loading, @rs2, @modelTransform) -> + storeCustomFiltersName: 'backlog-custom-filters' + storeFiltersName: 'backlog-filters' + backlogOrder: {} + milestonesOrder: {} + + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appMetaService, @navUrls, + @events, @analytics, @translate, @loading, @rs2, @modelTransform, @errorHandlingService, + @storage, @filterRemoteStorageService) -> bindMethods(@) + @.backlogOrder = {} + @.milestonesOrder = {} + @.page = 1 @.disablePagination = false @.firstLoadComplete = false @scope.userstories = [] + return if @.applyStoredFilters(@params.pslug, "backlog-filters") + @scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME") @showTags = false @activeFilters = false @@ -96,15 +110,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # On Error promise.then null, @.onInitialDataError.bind(@) + filtersReloadContent: () -> + @.loadUserstories(true) + initializeEventHandlers: -> @scope.$on "usform:bulk:success", => @.loadUserstories(true) @.loadProjectStats() + @confirm.notify("success") @analytics.trackEvent("userstory", "create", "bulk create userstory on backlog", 1) @scope.$on "sprintform:create:success", => @.loadSprints() @.loadProjectStats() + @confirm.notify("success") @analytics.trackEvent("sprint", "create", "create sprint on backlog", 1) @scope.$on "usform:new:success", => @@ -112,6 +131,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @.loadProjectStats() @rootscope.$broadcast("filters:update") + @confirm.notify("success") @analytics.trackEvent("userstory", "create", "create userstory on backlog", 1) @scope.$on "sprintform:edit:success", => @@ -174,6 +194,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?) return stats + setMilestonesOrder: (sprints) -> + for sprint in sprints + @.milestonesOrder[sprint.id] = {} + for it in sprint.user_stories + @.milestonesOrder[sprint.id][it.id] = it.sprint_order + unloadClosedSprints: -> @scope.$apply => @scope.closedSprints = [] @@ -184,6 +210,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return @rs.sprints.list(@scope.projectId, params).then (result) => sprints = result.milestones + @.setMilestonesOrder(sprints) + @scope.totalClosedMilestones = result.closed # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files @@ -199,6 +227,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return @rs.sprints.list(@scope.projectId, params).then (result) => sprints = result.milestones + @.setMilestonesOrder(sprints) + @scope.totalMilestones = sprints @scope.totalClosedMilestones = result.closed @scope.totalOpenMilestones = result.open @@ -220,47 +250,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return sprints - restoreFilters: -> - selectedTags = @scope.oldSelectedTags - selectedStatuses = @scope.oldSelectedStatuses - - return if !selectedStatuses and !selectedStatuses - - @scope.filtersQ = @scope.filtersQOld - - @.replaceFilter("q", @scope.filtersQ) - - _.each [selectedTags, selectedStatuses], (filterGrp) => - _.each filterGrp, (item) => - filters = @scope.filters[item.type] - filter = _.find(filters, {id: item.id}) - filter.selected = true - - @.selectFilter(item.type, item.id) - - @.loadUserstories() - - resetFilters: -> - selectedTags = _.filter(@scope.filters.tags, "selected") - selectedStatuses = _.filter(@scope.filters.status, "selected") - - @scope.oldSelectedTags = selectedTags - @scope.oldSelectedStatuses = selectedStatuses - - @scope.filtersQOld = @scope.filtersQ - @scope.filtersQ = undefined - @.replaceFilter("q", @scope.filtersQ) - - _.each [selectedTags, selectedStatuses], (filterGrp) => - _.each filterGrp, (item) => - filters = @scope.filters[item.type] - filter = _.find(filters, {id: item.id}) - filter.selected = false - - @.unselectFilter(item.type, item.id) - - @.loadUserstories() - loadAllPaginatedUserstories: () -> page = @.page @@ -272,15 +261,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @.loadingUserstories = true @.disablePagination = true - @scope.httpParams = @.getUrlFilters() - @rs.userstories.storeQueryParams(@scope.projectId, @scope.httpParams) + params = _.clone(@location.search()) + @rs.userstories.storeQueryParams(@scope.projectId, params) if resetPagination @.page = 1 - @scope.httpParams.page = @.page + params.page = @.page - promise = @rs.userstories.listUnassigned(@scope.projectId, @scope.httpParams, pageSize) + promise = @rs.userstories.listUnassigned(@scope.projectId, params, pageSize) return promise.then (result) => userstories = result[0] @@ -292,7 +281,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files @scope.userstories = @scope.userstories.concat(_.sortBy(userstories, "backlog_order")) - @.setSearchDataFilters() + for it in @scope.userstories + @.backlogOrder[it.id] = it.backlog_order @.loadingUserstories = false @@ -317,7 +307,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.is_backlog_activated - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -343,252 +333,152 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F prepareBulkUpdateData: (uses, field="backlog_order") -> return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) - resortUserStories: (uses, field="backlog_order") -> - items = [] - - for item, index in uses - item[field] = index - if item.isModified() - items.push(item) - - return items - + # --move us api behavior-- + # If your are moving multiples USs you must use the bulk api + # If there is only one US you must use patch (repo.save) + # + # The new US position is the position of the previous US + 1. + # If the previous US has a position value that it is equal to + # other USs, you must send all the USs with that position value + # only if they are before of the target position with this USs + # if it's a patch you must add them to the header, if is a bulk + # you must send them with the other USs moveUs: (ctx, usList, newUsIndex, newSprintId) -> oldSprintId = usList[0].milestone project = usList[0].project - movedFromClosedSprint = false - movedToClosedSprint = false + if oldSprintId + sprint = @scope.sprintsById[oldSprintId] || @scope.closedSprintsById[oldSprintId] - sprint = @scope.sprintsById[oldSprintId] + if newSprintId + newSprint = @scope.sprintsById[newSprintId] || @scope.closedSprintsById[newSprintId] - # Move from closed sprint - if !sprint && @scope.closedSprintsById - sprint = @scope.closedSprintsById[oldSprintId] - movedFromClosedSprint = true if sprint + currentSprintId = if newSprintId != oldSprintId then newSprintId else oldSprintId - newSprint = @scope.sprintsById[newSprintId] + orderList = null + orderField = "" - # Move to closed sprint - if !newSprint && newSprintId - newSprint = @scope.closedSprintsById[newSprintId] - movedToClosedSprint = true if newSprint + if newSprintId != oldSprintId + if newSprintId == null # From sprint to backlog + for us, key in usList # delete from sprint userstories + _.remove sprint.user_stories, (it) -> it.id == us.id - # In the same sprint or in the backlog - if newSprintId == oldSprintId - items = null - userstories = null + orderField = "backlog_order" + orderList = @.backlogOrder - if newSprintId == null - userstories = @scope.userstories - else - userstories = newSprint.user_stories + beforeDestination = _.slice(@scope.userstories, 0, newUsIndex) + afterDestination = _.slice(@scope.userstories, newUsIndex) - @scope.$apply -> - for us, key in usList - r = userstories.indexOf(us) - userstories.splice(r, 1) + @scope.userstories = @scope.userstories.concat(usList) + else # From backlog to sprint + for us in usList # delete from sprint userstories + _.remove @scope.userstories, (it) -> it.id == us.id - args = [newUsIndex, 0].concat(usList) - Array.prototype.splice.apply(userstories, args) + orderField = "sprint_order" + orderList = @.milestonesOrder[newSprint.id] - # If in backlog - if newSprintId == null - # Rehash userstories order field + beforeDestination = _.slice(newSprint.user_stories, 0, newUsIndex) + afterDestination = _.slice(newSprint.user_stories, newUsIndex) - items = @.resortUserStories(userstories, "backlog_order") - data = @.prepareBulkUpdateData(items, "backlog_order") - - # Persist in bulk all affected - # userstories with order change - @rs.userstories.bulkUpdateBacklogOrder(project, data).then => - @rootscope.$broadcast("sprint:us:moved") - - # For sprint - else - # Rehash userstories order field - items = @.resortUserStories(userstories, "sprint_order") - data = @.prepareBulkUpdateData(items, "sprint_order") - - # Persist in bulk all affected - # userstories with order change - @rs.userstories.bulkUpdateSprintOrder(project, data).then => - @rootscope.$broadcast("sprint:us:moved") - - return promise - - # From sprint to backlog - if newSprintId == null - us.milestone = null for us in usList - - @scope.$apply => - # Add new us to backlog userstories list - # @scope.userstories.splice(newUsIndex, 0, us) - args = [newUsIndex, 0].concat(usList) - Array.prototype.splice.apply(@scope.userstories, args) - - for us, key in usList - r = sprint.user_stories.indexOf(us) - sprint.user_stories.splice(r, 1) - - # Persist the milestone change of userstory - promise = @repo.save(us) - - # Rehash userstories order field - # and persist in bulk all changes. - promise = promise.then => - items = @.resortUserStories(@scope.userstories, "backlog_order") - data = @.prepareBulkUpdateData(items, "backlog_order") - return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then => - @rootscope.$broadcast("sprint:us:moved") - - if movedFromClosedSprint - @rootscope.$broadcast("backlog:load-closed-sprints") - - promise.then null, -> - console.log "FAIL" # TODO - - return promise - - # From backlog to sprint - if oldSprintId == null - us.milestone = newSprintId for us in usList - args = [newUsIndex, 0].concat(usList) - - # Add moving us to sprint user stories list - Array.prototype.splice.apply(newSprint.user_stories, args) - - # Remove moving us from backlog userstories lists. - for us, key in usList - r = @scope.userstories.indexOf(us) - @scope.userstories.splice(r, 1) - - # From sprint to sprint + newSprint.user_stories = newSprint.user_stories.concat(usList) else - us.milestone = newSprintId for us in usList + if oldSprintId == null # backlog + orderField = "backlog_order" + orderList = @.backlogOrder - @scope.$apply => - args = [newUsIndex, 0].concat(usList) + list = _.filter @scope.userstories, (listIt) -> # Remove moved US from list + return !_.find usList, (moveIt) -> return listIt.id == moveIt.id - # Add new us to backlog userstories list - Array.prototype.splice.apply(newSprint.user_stories, args) + beforeDestination = _.slice(list, 0, newUsIndex) + afterDestination = _.slice(list, newUsIndex) + else # sprint + orderField = "sprint_order" + orderList = @.milestonesOrder[sprint.id] - # Remove the us from the sprint list. - for us in usList - r = sprint.user_stories.indexOf(us) - sprint.user_stories.splice(r, 1) + list = _.filter newSprint.user_stories, (listIt) -> # Remove moved US from list + return !_.find usList, (moveIt) -> return listIt.id == moveIt.id - #Persist the milestone change of userstory - promises = _.map usList, (us) => @repo.save(us) + beforeDestination = _.slice(list, 0, newUsIndex) + afterDestination = _.slice(list, newUsIndex) - #Rehash userstories order field - #and persist in bulk all changes. - promise = @q.all(promises).then => - items = @.resortUserStories(newSprint.user_stories, "sprint_order") - data = @.prepareBulkUpdateData(items, "sprint_order") + # previous us + previous = beforeDestination[beforeDestination.length - 1] - @rs.userstories.bulkUpdateSprintOrder(project, data).then (result) => - @rootscope.$broadcast("sprint:us:moved") + # this will store the previous us with the same position + setPreviousOrders = [] - @rs.userstories.bulkUpdateBacklogOrder(project, data).then => - @rootscope.$broadcast("sprint:us:moved") + if !previous + startIndex = 0 + else if previous + startIndex = orderList[previous.id] + 1 - if movedToClosedSprint || movedFromClosedSprint - @scope.$broadcast("backlog:load-closed-sprints") + previousWithTheSameOrder = _.filter(beforeDestination, (it) -> + it[orderField] == orderList[previous.id] + ) - promise.then null, -> - console.log "FAIL" # TODO + # we must send the USs previous to the dropped USs to tell the backend + # which USs are before the dropped USs, if they have the same value to + # order, the backend doens't know after which one do you want to drop + # the USs + if previousWithTheSameOrder.length > 1 + setPreviousOrders = _.map(previousWithTheSameOrder, (it) -> + {us_id: it.id, order: orderList[it.id]} + ) + + modifiedUs = [] + + for us, key in usList # update sprint and new position + us.milestone = currentSprintId + us[orderField] = startIndex + key + orderList[us.id] = us[orderField] + + modifiedUs.push({us_id: us.id, order: us[orderField]}) + + startIndex = orderList[usList[usList.length - 1].id] + + for it, key in afterDestination # increase position of the us after the dragged us's + orderList[it.id] = startIndex + key + 1 + + # refresh order + @scope.userstories = _.sortBy @scope.userstories, (it) => @.backlogOrder[it.id] + + for sprint in @scope.sprints + sprint.user_stories = _.sortBy sprint.user_stories, (it) => @.milestonesOrder[sprint.id][it.id] + + for sprint in @scope.closedSprints + sprint.user_stories = _.sortBy sprint.user_stories, (it) => @.milestonesOrder[sprint.id][it.id] + + # saving + if usList.length > 1 && (newSprintId != oldSprintId) # drag multiple to sprint + data = modifiedUs.concat(setPreviousOrders) + promise = @rs.userstories.bulkUpdateMilestone(project, newSprintId, data) + else if usList.length > 1 # drag multiple in backlog + data = modifiedUs.concat(setPreviousOrders) + promise = @rs.userstories.bulkUpdateBacklogOrder(project, data) + else # drag single + setOrders = {} + for it in setPreviousOrders + setOrders[it.us_id] = it.order + + options = { + headers: { + "set-orders": JSON.stringify(setOrders) + } + } + + promise = @repo.save(usList[0], true, {}, options, true) + + promise.then () => + @rootscope.$broadcast("sprint:us:moved") + + if @scope.closedSprintsById && @scope.closedSprintsById[oldSprintId] + @rootscope.$broadcast("backlog:load-closed-sprints") return promise - isFilterSelected: (type, id) -> - if @searchdata[type]? and @searchdata[type][id] - return true - return false - - setSearchDataFilters: () -> - urlfilters = @.getUrlFilters() - - if urlfilters.q - @scope.filtersQ = @scope.filtersQ or urlfilters.q - - @searchdata = {} - for name, value of urlfilters - if not @searchdata[name]? - @searchdata[name] = {} - - for val in taiga.toString(value).split(",") - @searchdata[name][val] = true - - getUrlFilters: -> - return _.pick(@location.search(), "status", "tags", "q") - - generateFilters: -> - urlfilters = @.getUrlFilters() - @scope.filters = {} - - loadFilters = {} - loadFilters.project = @scope.projectId - loadFilters.tags = urlfilters.tags - loadFilters.status = urlfilters.status - loadFilters.q = urlfilters.q - loadFilters.milestone = 'null' - - return @rs.userstories.filtersData(loadFilters).then (data) => - choicesFiltersFormat = (choices, type, byIdObject) => - _.map choices, (t) -> - t.type = type - return t - - tagsFilterFormat = (tags) => - return _.map tags, (t) -> - t.id = t.name - t.type = 'tags' - return t - - # Build filters data structure - @scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.usStatusById) - @scope.filters.tags = tagsFilterFormat(data.tags) - - selectedTags = _.filter(@scope.filters.tags, "selected") - selectedTags = _.map(selectedTags, "id") - - selectedStatuses = _.filter(@scope.filters.status, "selected") - selectedStatuses = _.map(selectedStatuses, "id") - - @.markSelectedFilters(@scope.filters, urlfilters) - - #store query params - @rs.userstories.storeQueryParams(@scope.projectId, { - "status": selectedStatuses, - "tags": selectedTags, - "project": @scope.projectId - "milestone": null - }) - - markSelectedFilters: (filters, urlfilters) -> - # Build selected filters (from url) fast lookup data structure - searchdata = {} - for name, value of _.omit(urlfilters, "page", "orderBy") - if not searchdata[name]? - searchdata[name] = {} - - for val in "#{value}".split(",") - searchdata[name][val] = true - - isSelected = (type, id) -> - if searchdata[type]? and searchdata[type][id] - return true - return false - - for key, value of filters - for obj in value - obj.selected = if isSelected(obj.type, obj.id) then true else undefined - ## Template actions updateUserStoryStatus: () -> - @.setSearchDataFilters() @.generateFilters().then () => @rootscope.$broadcast("filters:update") @.loadProjectStats() @@ -641,10 +531,10 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F currentDate = new Date().getTime() return _.find @scope.sprints, (sprint) -> - start = moment(sprint.estimated_start, 'YYYY-MM-DD').format('x') - end = moment(sprint.estimated_finish, 'YYYY-MM-DD').format('x') + start = moment(sprint.estimated_start, 'YYYY-MM-DD').format('x') + end = moment(sprint.estimated_finish, 'YYYY-MM-DD').format('x') - return currentDate >= start && currentDate <= end + return currentDate >= start && currentDate <= end module.controller("BacklogController", BacklogController) @@ -806,8 +696,15 @@ BacklogDirective = ($repo, $rootscope, $translate) -> text = $translate.instant("BACKLOG.TAGS.SHOW") elm.text(text) + openFilterInit = ($scope, $el, $ctrl) -> + sidebar = $el.find("sidebar.backlog-filter") + + sidebar.addClass("active") + + $ctrl.activeFilters = true + showHideFilter = ($scope, $el, $ctrl) -> - sidebar = $el.find("sidebar.filters-bar") + sidebar = $el.find("sidebar.backlog-filter") sidebar.one "transitionend", () -> timeout 150, -> $rootscope.$broadcast("resize") @@ -823,11 +720,6 @@ BacklogDirective = ($repo, $rootscope, $translate) -> toggleText(target, [hideText, showText]) - if !sidebar.hasClass("active") - $ctrl.resetFilters() - else - $ctrl.restoreFilters() - $ctrl.toggleActiveFilters() ## Filters Link @@ -846,11 +738,13 @@ BacklogDirective = ($repo, $rootscope, $translate) -> linkFilters($scope, $el, $attrs, $ctrl) linkDoomLine($scope, $el, $attrs, $ctrl) - filters = $ctrl.getUrlFilters() + filters = $ctrl.location.search() if filters.status || filters.tags || - filters.q - showHideFilter($scope, $el, $ctrl) + filters.q || + filters.assigned_to || + filters.owner + openFilterInit($scope, $el, $ctrl) $scope.$on "showTags", () -> showHideTags($ctrl) diff --git a/app/coffee/modules/backlog/sortable.coffee b/app/coffee/modules/backlog/sortable.coffee index eeb2948a..555d75c4 100644 --- a/app/coffee/modules/backlog/sortable.coffee +++ b/app/coffee/modules/backlog/sortable.coffee @@ -23,16 +23,10 @@ ### taiga = @.taiga - -mixOf = @.taiga.mixOf -toggleText = @.taiga.toggleText -scopeDefer = @.taiga.scopeDefer bindOnce = @.taiga.bindOnce -groupBy = @.taiga.groupBy module = angular.module("taigaBacklog") - ############################################################################# ## Sortable Directive ############################################################################# @@ -42,7 +36,7 @@ deleteElement = (el) -> $(el).off() $(el).remove() -BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> +BacklogSortableDirective = () -> link = ($scope, $el, $attrs) -> bindOnce $scope, "project", (project) -> # If the user has not enough permissions we don't enable the sortable @@ -51,10 +45,6 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> initIsBacklog = false - filterError = -> - text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR") - $tgConfirm.notify("error", text) - drake = dragula([$el[0], $('.empty-backlog')[0]], { copySortSource: false, copy: false, @@ -63,18 +53,11 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> if !$(item).hasClass('row') return false - # it doesn't move is the filter is open - parent = $(item).parent() - initIsBacklog = parent.hasClass('backlog-table-body') - - if initIsBacklog && $el.hasClass("active-filters") - filterError() - return false - return true }) drake.on 'drag', (item, container) -> + # it doesn't move is the filter is open parent = $(item).parent() initIsBacklog = parent.hasClass('backlog-table-body') @@ -88,6 +71,8 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> $(item).addClass('backlog-us-mirror') drake.on 'dragend', (item) -> + parent = $(item).parent() + $('.doom-line').remove() parent = $(item).parent() @@ -102,8 +87,6 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> $(document.body).removeClass("drag-active") - items = $(item).parent().find('.row') - sprint = null firstElement = if dragMultipleItems.length then dragMultipleItems[0] else item @@ -131,11 +114,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> usList = _.map dragMultipleItems, (item) -> return item = $(item).scope().us else - usList = _.map items, (item) -> - item = $(item) - itemUs = item.scope().us - - return itemUs + usList = [$(item).scope().us] $scope.$emit("sprint:us:move", usList, index, sprint) @@ -144,7 +123,7 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) $scope.$on "$destroy", -> @@ -153,11 +132,4 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> return {link: link} -module.directive("tgBacklogSortable", [ - "$tgRepo", - "$tgResources", - "$rootScope", - "$tgConfirm", - "$translate", - BacklogSortableDirective -]) +module.directive("tgBacklogSortable", BacklogSortableDirective) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 2806d182..5cfa7dd4 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -73,13 +73,16 @@ urls = { "project-taskboard": "/project/:project/taskboard/:sprint" "project-kanban": "/project/:project/kanban" "project-issues": "/project/:project/issues" + "project-epics": "/project/:project/epics" "project-search": "/project/:project/search" + "project-epics-detail": "/project/:project/epic/:ref" "project-userstories-detail": "/project/:project/us/:ref" "project-tasks-detail": "/project/:project/task/:ref" "project-issues-detail": "/project/:project/issue/:ref" "project-wiki": "/project/:project/wiki" + "project-wiki-list": "/project/:project/wiki-list" "project-wiki-page": "/project/:project/wiki/:slug" # Team @@ -99,6 +102,7 @@ urls = { "project-admin-project-values-severities": "/project/:project/admin/project-values/severities" "project-admin-project-values-types": "/project/:project/admin/project-values/types" "project-admin-project-values-custom-fields": "/project/:project/admin/project-values/custom-fields" + "project-admin-project-values-tags": "/project/:project/admin/project-values/tags" "project-admin-memberships": "/project/:project/admin/memberships" "project-admin-roles": "/project/:project/admin/roles" @@ -106,6 +110,7 @@ urls = { "project-admin-third-parties-github": "/project/:project/admin/third-parties/github" "project-admin-third-parties-gitlab": "/project/:project/admin/third-parties/gitlab" "project-admin-third-parties-bitbucket": "/project/:project/admin/third-parties/bitbucket" + "project-admin-third-parties-gogs": "/project/:project/admin/third-parties/gogs" "project-admin-contrib": "/project/:project/admin/contrib/:plugin" # User settings diff --git a/app/coffee/modules/base/http.coffee b/app/coffee/modules/base/http.coffee index 0c425720..57fa8e82 100644 --- a/app/coffee/modules/base/http.coffee +++ b/app/coffee/modules/base/http.coffee @@ -30,7 +30,7 @@ class HttpService extends taiga.Service constructor: (@http, @q, @storage, @rootScope, @cacheFactory, @translate) -> super() - @.cache = @cacheFactory("httpget"); + @.cache = @cacheFactory("httpget") headers: -> headers = {} diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 268a86a6..290bed50 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -41,7 +41,7 @@ class RepositoryService extends taiga.Service defered = @q.defer() url = @urls.resolve(name) - promise = @http.post(url, JSON.stringify(data)) + promise = @http.post(url, JSON.stringify(data), extraParams) promise.success (_data, _status) => defered.resolve(@model.make_model(name, _data, null, dataTypes)) @@ -67,7 +67,7 @@ class RepositoryService extends taiga.Service promises = _.map(models, (x) => @.save(x, true)) return @q.all(promises) - save: (model, patch=true) -> + save: (model, patch=true, params = {}, options, returnHeaders = false) -> defered = @q.defer() if not model.isModified() and patch @@ -75,20 +75,25 @@ class RepositoryService extends taiga.Service return defered.promise url = @.resolveUrlForModel(model) + data = JSON.stringify(model.getAttrs(patch)) if patch - promise = @http.patch(url, data) + promise = @http.patch(url, data, params, options) else - promise = @http.put(url, data) + promise = @http.put(url, data, params, options) - promise.success (data, status) => + promise.success (data, status, headers, response) => model._isModified = false model._attrs = _.extend(model.getAttrs(), data) model._modifiedAttrs = {} model.applyCasts() - defered.resolve(model) + + if returnHeaders + defered.resolve([model, headers()]) + else + defered.resolve(model) promise.error (data, status) -> defered.reject(data) diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index f8d1ab76..5ab2fa50 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -398,3 +398,50 @@ Autofocus = ($timeout) -> } module.directive('tgAutofocus', ['$timeout', Autofocus]) + +module.directive 'tgPreloadImage', () -> + spinner = "loading..." + + template = """ +
+ +
+ """ + + preload = (src, onLoad) -> + image = new Image() + image.onload = onLoad + image.src = src + + return image + + return { + template: template, + transclude: true, + replace: true, + link: (scope, el, attrs) -> + image = el.find('img:last') + timeout = null + + onLoad = () -> + el.find('.loading-spinner').remove() + image.show() + + if timeout + clearTimeout(timeout) + timeout = null + + attrs.$observe 'preloadSrc', (src) -> + if timeout + clearTimeout(timeout) + + el.find('.loading-spinner').remove() + + timeout = setTimeout () -> + el.prepend(spinner) + , 200 + + image.hide() + + preload(src, onLoad) + } diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index cde71cb0..5da45d4a 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -126,7 +126,7 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective) ## Created-by display directive ############################################################################# -CreatedByDisplayDirective = ($template, $compile, $translate, $navUrls)-> +CreatedByDisplayDirective = ($template, $compile, $translate, $navUrls, avatarService)-> # Display the owner information (full name and photo) and the date of # creation of an object (like USs, tasks and issues). # @@ -141,11 +141,15 @@ CreatedByDisplayDirective = ($template, $compile, $translate, $navUrls)-> link = ($scope, $el, $attrs) -> bindOnce $scope, $attrs.ngModel, (model) -> if model? + + avatar = avatarService.getAvatar(model.owner_extra_info) $scope.owner = model.owner_extra_info or { full_name_display: $translate.instant("COMMON.EXTERNAL_USER") - photo: "/" + window._version + "/images/user-noimage.png" } + $scope.owner.avatar = avatar.url + $scope.owner.bg = avatar.bg + $scope.url = if $scope.owner?.is_active then $navUrls.resolve("user-profile", {username: $scope.owner.username}) else "" @@ -162,9 +166,45 @@ CreatedByDisplayDirective = ($template, $compile, $translate, $navUrls)-> templateUrl: "common/components/created-by.html" } -module.directive("tgCreatedByDisplay", ["$tgTemplate", "$compile", "$translate", "$tgNavUrls", +module.directive("tgCreatedByDisplay", ["$tgTemplate", "$compile", "$translate", "$tgNavUrls", "tgAvatarService", CreatedByDisplayDirective]) + +UserDisplayDirective = ($template, $compile, $translate, $navUrls, avatarService)-> + # Display the user information (full name and photo). + # + # Example: + # div.creator(tg-user-display, tg-user-id="{{ user.id }}") + # + # Requirements: + # - scope.usersById object is required. + + link = ($scope, $el, $attrs) -> + id = $attrs.tgUserId + $scope.user = $scope.usersById[id] or { + full_name_display: $translate.instant("COMMON.EXTERNAL_USER") + } + + avatar = avatarService.getAvatar($scope.usersById[id] or null) + + $scope.user.avatar = avatar.url + $scope.user.bg = avatar.bg + + $scope.url = if $scope.user.is_active then $navUrls.resolve("user-profile", {username: $scope.user.username}) else "" + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + scope: true, + templateUrl: "common/components/user-display.html" + } + +module.directive("tgUserDisplay", ["$tgTemplate", "$compile", "$translate", "$tgNavUrls", "tgAvatarService", + UserDisplayDirective]) + ############################################################################# ## Watchers directive ############################################################################# @@ -172,7 +212,6 @@ module.directive("tgCreatedByDisplay", ["$tgTemplate", "$compile", "$translate", WatchersDirective = ($rootscope, $confirm, $repo, $modelTransform, $template, $compile, $translate) -> # You have to include a div with the tg-lb-watchers directive in the page # where use this directive - template = $template.get("common/components/watchers.html", true) link = ($scope, $el, $attrs, $model) -> isEditable = -> @@ -209,13 +248,8 @@ WatchersDirective = ($rootscope, $confirm, $repo, $modelTransform, $template, $c $confirm.notify("error") renderWatchers = (watchers) -> - ctx = { - watchers: watchers - isEditable: isEditable() - } - - html = $compile(template(ctx))($scope) - $el.html(html) + $scope.watchers = watchers + $scope.isEditable = isEditable() $el.on "click", ".js-delete-watcher", (event) -> event.preventDefault() @@ -244,12 +278,19 @@ WatchersDirective = ($rootscope, $confirm, $repo, $modelTransform, $template, $c $scope.$watch $attrs.ngModel, (item) -> return if not item? watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId]) + watchers = _.filter watchers, (it) -> return !!it + renderWatchers(watchers) $scope.$on "$destroy", -> $el.off() - return {link:link, require:"ngModel"} + return { + scope: true, + templateUrl: "common/components/watchers.html", + link:link, + require:"ngModel" + } module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgQueueModelTransformation", "$tgTemplate", "$compile", "$translate", WatchersDirective]) @@ -259,7 +300,7 @@ module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgQueue ## Assigned to directive ############################################################################# -AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $template, $translate, $compile, $currentUserService) -> +AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $template, $translate, $compile, $currentUserService, avatarService) -> # You have to include a div with the tg-lb-assignedto directive in the page # where use this directive template = $template.get("common/components/assigned-to.html", true) @@ -293,20 +334,23 @@ AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $ return transform renderAssignedTo = (assignedObject) -> + avatar = avatarService.getAvatar(assignedObject?.assigned_to_extra_info) + bg = null + if assignedObject?.assigned_to? fullName = assignedObject.assigned_to_extra_info.full_name_display - photo = assignedObject.assigned_to_extra_info.photo isUnassigned = false + bg = avatar.bg else fullName = $translate.instant("COMMON.ASSIGNED_TO.ASSIGN") - photo = "/#{window._version}/images/unnamed.png" isUnassigned = true isIocaine = assignedObject?.is_iocaine ctx = { fullName: fullName - photo: photo + avatar: avatar.url + bg: bg isUnassigned: isUnassigned isEditable: isEditable() isIocaine: isIocaine @@ -353,7 +397,7 @@ AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $ require:"ngModel" } -module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading", "$tgQueueModelTransformation", "$tgTemplate", "$translate", "$compile","tgCurrentUserService", +module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading", "$tgQueueModelTransformation", "$tgTemplate", "$translate", "$compile","tgCurrentUserService", "tgAvatarService", AssignedToDirective]) @@ -448,93 +492,6 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) -> module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "$tgTemplate", DeleteButtonDirective]) - -############################################################################# -## Editable subject directive -############################################################################# - -EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $modelTransform, $template) -> - template = $template.get("common/components/editable-subject.html") - - link = ($scope, $el, $attrs, $model) -> - - $scope.$on "object:updated", () -> - $el.find('.edit-subject').hide() - $el.find('.view-subject').show() - - isEditable = -> - return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 - - save = (subject) -> - currentLoading = $loading() - .target($el.find('.save-container')) - .start() - - transform = $modelTransform.save (item) -> - item.subject = subject - - return item - - transform.then => - $confirm.notify("success") - $rootscope.$broadcast("object:updated") - $el.find('.edit-subject').hide() - $el.find('.view-subject').show() - - transform.then null, -> - $confirm.notify("error") - - transform.finally -> - currentLoading.finish() - - return transform - - $el.click -> - return if not isEditable() - $el.find('.edit-subject').show() - $el.find('.view-subject').hide() - $el.find('input').focus() - - $el.on "click", ".save", (e) -> - e.preventDefault() - - subject = $scope.item.subject - save(subject) - - $el.on "keyup", "input", (event) -> - if event.keyCode == 13 - subject = $scope.item.subject - save(subject) - else if event.keyCode == 27 - $scope.$apply () => $model.$modelValue.revert() - - $el.find('.edit-subject').hide() - $el.find('.view-subject').show() - - $el.find('.edit-subject').hide() - - $scope.$watch $attrs.ngModel, (value) -> - return if not value - $scope.item = value - - if not isEditable() - $el.find('.view-subject .edit').remove() - - $scope.$on "$destroy", -> - $el.off() - - - return { - link: link - restrict: "EA" - require: "ngModel" - template: template - } - -module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation", - "$tgTemplate", EditableSubjectDirective]) - - ############################################################################# ## Editable description directive ############################################################################# @@ -656,7 +613,7 @@ EditableWysiwyg = (attachmentsService, attachmentsFullService) -> link = ($scope, $el, $attrs, $model) -> isInEditMode = -> - return $el.find('textarea').is(':visible') + return $el.find('textarea').is(':visible') and $model.$modelValue.id uploadFile = (file, type) -> @@ -721,6 +678,16 @@ module.directive("tgEditableWysiwyg", ["tgAttachmentsService", "tgAttachmentsFul ## completely bindonce, they only serves for visualization of data. ############################################################################# +ListItemEpicStatusDirective = -> + link = ($scope, $el, $attrs) -> + epic = $scope.$eval($attrs.tgListitemEpicStatus) + bindOnce $scope, "epicStatusById", (epicStatusById) -> + $el.html(epicStatusById[epic.status].name) + + return {link:link} + +module.directive("tgListitemEpicStatus", ListItemEpicStatusDirective) + ListItemUsStatusDirective = -> link = ($scope, $el, $attrs) -> us = $scope.$eval($attrs.tgListitemUsStatus) @@ -743,7 +710,7 @@ ListItemTaskStatusDirective = -> module.directive("tgListitemTaskStatus", ListItemTaskStatusDirective) -ListItemAssignedtoDirective = ($template, $translate) -> +ListItemAssignedtoDirective = ($template, $translate, avatarService) -> template = $template.get("common/components/list-item-assigned-to-avatar.html", true) link = ($scope, $el, $attrs) -> @@ -751,19 +718,22 @@ ListItemAssignedtoDirective = ($template, $translate) -> item = $scope.$eval($attrs.tgListitemAssignedto) ctx = { name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"), - imgurl: "/#{window._version}/images/unnamed.png" } member = usersById[item.assigned_to] + avatar = avatarService.getAvatar(member) + + ctx.imgurl = avatar.url + ctx.bg = avatar.bg + if member - ctx.imgurl = member.photo ctx.name = member.full_name_display $el.html(template(ctx)) return {link:link} -module.directive("tgListitemAssignedto", ["$tgTemplate", "$translate", ListItemAssignedtoDirective]) +module.directive("tgListitemAssignedto", ["$tgTemplate", "$translate", "tgAvatarService", ListItemAssignedtoDirective]) ListItemIssueStatusDirective = -> diff --git a/app/coffee/modules/common/custom-field-values.coffee b/app/coffee/modules/common/custom-field-values.coffee index ad3e8049..b9808cbb 100644 --- a/app/coffee/modules/common/custom-field-values.coffee +++ b/app/coffee/modules/common/custom-field-values.coffee @@ -112,31 +112,23 @@ CustomAttributesValuesDirective = ($templates, $storage) -> link = ($scope, $el, $attrs, $ctrls) -> $ctrl = $ctrls[0] $model = $ctrls[1] + hash = collapsedHash($attrs.type) + $scope.collapsed = $storage.get(hash) or false bindOnce $scope, $attrs.ngModel, (value) -> $ctrl.initialize($attrs.type, value.id) $ctrl.loadCustomAttributesValues() - $el.on "click", ".custom-fields-header .collapse", -> - hash = collapsedHash($attrs.type) - collapsed = not($storage.get(hash) or false) - $storage.set(hash, collapsed) - if collapsed - $el.find(".custom-fields-header .icon").removeClass("open") - $el.find(".custom-fields-body").removeClass("open") - else - $el.find(".custom-fields-header .icon").addClass("open") - $el.find(".custom-fields-body").addClass("open") + $scope.toggleCollapse = () -> + $scope.collapsed = !$scope.collapsed + $storage.set(hash, $scope.collapsed) $scope.$on "$destroy", -> $el.off() templateFn = ($el, $attrs) -> - collapsed = $storage.get(collapsedHash($attrs.type)) or false - return template({ requiredEditionPerm: $attrs.requiredEditionPerm - collapsed: collapsed }) return { diff --git a/app/coffee/modules/common/estimation.coffee b/app/coffee/modules/common/estimation.coffee index 6cef0ef6..ae108527 100644 --- a/app/coffee/modules/common/estimation.coffee +++ b/app/coffee/modules/common/estimation.coffee @@ -57,6 +57,7 @@ LbUsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $template, totalPoints: @calculateTotalPoints() roles: @calculateRoles() editable: @isEditable + loading: false } mainTemplate = "common/estimation/us-estimation-points-per-role.html" template = $template.get(mainTemplate, true) @@ -111,14 +112,19 @@ UsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $template, $c if us estimationProcess = $tgEstimationsService.create($el, us, $scope.project) estimationProcess.onSelectedPointForRole = (roleId, pointId, points) -> + estimationProcess.loading = roleId + estimationProcess.render() save(points).then () -> + estimationProcess.loading = false $rootScope.$broadcast("object:updated") + estimationProcess.render() estimationProcess.render = () -> ctx = { totalPoints: @calculateTotalPoints() roles: @calculateRoles() editable: @isEditable + loading: estimationProcess.loading } mainTemplate = "common/estimation/us-estimation-points-per-role.html" template = $template.get(mainTemplate, true) @@ -154,6 +160,7 @@ EstimationsService = ($template, $repo, $confirm, $q, $qqueue) -> @isEditable = @project.my_permissions.indexOf("modify_us") != -1 @roles = @project.roles @points = @project.points + @loading = false @pointsById = groupBy(@points, (x) -> x.id) @onSelectedPointForRole = (roleId, pointId) -> @render = () -> @@ -163,6 +170,7 @@ EstimationsService = ($template, $repo, $confirm, $q, $qqueue) -> $qqueue.add () => onSuccess = => deferred.resolve() + @render() onError = => $confirm.notify("error") @@ -215,7 +223,6 @@ EstimationsService = ($template, $repo, $confirm, $q, $qqueue) -> pointId = target.data("point-id") @$el.find(".popover").popover().close() - points = _.clone(@us.points, true) points[roleId] = pointId diff --git a/app/coffee/modules/common/filters.coffee b/app/coffee/modules/common/filters.coffee index 7590b45c..c232d85d 100644 --- a/app/coffee/modules/common/filters.coffee +++ b/app/coffee/modules/common/filters.coffee @@ -74,3 +74,56 @@ sizeFormat = => return @.taiga.sizeFormat module.filter("sizeFormat", sizeFormat) + + +toMutableFilter = -> + toMutable = (js) -> + return js.toJS() + + memoizedMutable = _.memoize(toMutable) + + return (input) -> + if input instanceof Immutable.List + return memoizedMutable(input) + + return input + +module.filter("toMutable", toMutableFilter) + + +byRefFilter = ($filterFilter)-> + return (userstories, filter) -> + if filter?.startsWith("#") + cleanRef= filter.substr(1) + return _.filter(userstories, (us) => String(us.ref).startsWith(cleanRef)) + + return $filterFilter(userstories, filter) + +module.filter("byRef", ["filterFilter", byRefFilter]) + + +darkerFilter = -> + return (color, luminosity) -> + # validate hex string + color = new String(color).replace(/[^0-9a-f]/gi, '') + if color.length < 6 + color = color[0]+ color[0]+ color[1]+ color[1]+ color[2]+ color[2]; + + luminosity = luminosity || 0 + + # convert to decimal and change luminosity + newColor = "#" + c = 0 + i = 0 + black = 0 + white = 255 + # for (i = 0; i < 3; i++) + for i in [0, 1, 2] + c = parseInt(color.substr(i*2,2), 16) + c = Math.round(Math.min(Math.max(black, c + (luminosity * white)), white)).toString(16) + newColor += ("00"+c).substr(c.length) + + return newColor + + +module.filter("darker", darkerFilter) diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee deleted file mode 100644 index c8017ed2..00000000 --- a/app/coffee/modules/common/history.coffee +++ /dev/null @@ -1,507 +0,0 @@ -### -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino Garcia -# Copyright (C) 2014-2016 David Barragán Merino -# Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Juan Francisco Alcántara -# Copyright (C) 2014-2016 Xavi Julian -# -# 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/history.coffee -### - -taiga = @.taiga -trim = @.taiga.trim -bindOnce = @.taiga.bindOnce -debounce = @.taiga.debounce - -module = angular.module("taigaCommon") - -IGNORED_FIELDS = { - "userstories.userstory": [ - "watchers", "kanban_order", "backlog_order", "sprint_order", "finish_date", "tribe_gig" - ], - "tasks.task": [ - "watchers", "us_order", "taskboard_order" - ], - "issues.issue": [ - "watchers" - ] -} - -############################################################################# -## History Directive (Main) -############################################################################# - - -class HistoryController extends taiga.Controller - @.$inject = ["$scope", "$tgRepo", "$tgResources"] - - constructor: (@scope, @repo, @rs) -> - - initialize: (type, objectId) -> - @.type = type - @.objectId = objectId - - loadHistory: (type, objectId) -> - return @rs.history.get(type, objectId).then (history) => - for historyResult in history - # If description was modified take only the description_html field - if historyResult.values_diff.description_diff? - historyResult.values_diff.description = historyResult.values_diff.description_diff - - delete historyResult.values_diff.description_html - delete historyResult.values_diff.description_diff - - # If block note was modified take only the blocked_note_html field - if historyResult.values_diff.blocked_note_diff? - historyResult.values_diff.blocked_note = historyResult.values_diff.blocked_note_diff - - delete historyResult.values_diff.blocked_note_html - delete historyResult.values_diff.blocked_note_diff - - for historyEntry in history - changeModel = historyEntry.key.split(":")[0] - if IGNORED_FIELDS[changeModel]? - historyEntry.values_diff = _.removeKeys(historyEntry.values_diff, IGNORED_FIELDS[changeModel]) - - @scope.history = _.filter(history, (item) -> Object.keys(item.values_diff).length > 0) - - @scope.comments = _.filter(history, (item) -> item.comment != "") - - deleteComment: (type, objectId, activityId) -> - return @rs.history.deleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) - - undeleteComment: (type, objectId, activityId) -> - return @rs.history.undeleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) - - -HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $compile, $navUrls, $rootScope, checkPermissionsService) -> - templateChangeDiff = $template.get("common/history/history-change-diff.html", true) - templateChangePoints = $template.get("common/history/history-change-points.html", true) - templateChangeGeneric = $template.get("common/history/history-change-generic.html", true) - templateChangeAttachment = $template.get("common/history/history-change-attachment.html", true) - templateChangeList = $template.get("common/history/history-change-list.html", true) - templateDeletedComment = $template.get("common/history/history-deleted-comment.html", true) - templateActivity = $template.get("common/history/history-activity.html", true) - templateBaseEntries = $template.get("common/history/history-base-entries.html", true) - templateBase = $template.get("common/history/history-base.html", true) - - link = ($scope, $el, $attrs, $ctrl) -> - # Bootstraping - type = $attrs.type - objectId = null - - showAllComments = false - showAllActivity = false - - getPrettyDateFormat = -> - return $translate.instant("ACTIVITY.DATETIME") - - bindOnce $scope, $attrs.ngModel, (model) -> - type = $attrs.type - objectId = model.id - - $ctrl.initialize(type, objectId) - $ctrl.loadHistory(type, objectId) - - # Helpers - getHumanizedFieldName = (field) -> - humanizedFieldNames = { - subject : $translate.instant("ACTIVITY.FIELDS.SUBJECT") - name: $translate.instant("ACTIVITY.FIELDS.NAME") - description : $translate.instant("ACTIVITY.FIELDS.DESCRIPTION") - content: $translate.instant("ACTIVITY.FIELDS.CONTENT") - status: $translate.instant("ACTIVITY.FIELDS.STATUS") - is_closed : $translate.instant("ACTIVITY.FIELDS.IS_CLOSED") - finish_date : $translate.instant("ACTIVITY.FIELDS.FINISH_DATE") - type: $translate.instant("ACTIVITY.FIELDS.TYPE") - priority: $translate.instant("ACTIVITY.FIELDS.PRIORITY") - severity: $translate.instant("ACTIVITY.FIELDS.SEVERITY") - assigned_to : $translate.instant("ACTIVITY.FIELDS.ASSIGNED_TO") - watchers : $translate.instant("ACTIVITY.FIELDS.WATCHERS") - milestone : $translate.instant("ACTIVITY.FIELDS.MILESTONE") - user_story: $translate.instant("ACTIVITY.FIELDS.USER_STORY") - project: $translate.instant("ACTIVITY.FIELDS.PROJECT") - is_blocked: $translate.instant("ACTIVITY.FIELDS.IS_BLOCKED") - blocked_note: $translate.instant("ACTIVITY.FIELDS.BLOCKED_NOTE") - points: $translate.instant("ACTIVITY.FIELDS.POINTS") - client_requirement : $translate.instant("ACTIVITY.FIELDS.CLIENT_REQUIREMENT") - team_requirement : $translate.instant("ACTIVITY.FIELDS.TEAM_REQUIREMENT") - is_iocaine: $translate.instant("ACTIVITY.FIELDS.IS_IOCAINE") - tags: $translate.instant("ACTIVITY.FIELDS.TAGS") - attachments : $translate.instant("ACTIVITY.FIELDS.ATTACHMENTS") - is_deprecated: $translate.instant("ACTIVITY.FIELDS.IS_DEPRECATED") - blocked_note: $translate.instant("ACTIVITY.FIELDS.BLOCKED_NOTE") - is_blocked: $translate.instant("ACTIVITY.FIELDS.IS_BLOCKED") - order: $translate.instant("ACTIVITY.FIELDS.ORDER") - backlog_order: $translate.instant("ACTIVITY.FIELDS.BACKLOG_ORDER") - sprint_order: $translate.instant("ACTIVITY.FIELDS.SPRINT_ORDER") - kanban_order: $translate.instant("ACTIVITY.FIELDS.KANBAN_ORDER") - taskboard_order: $translate.instant("ACTIVITY.FIELDS.TASKBOARD_ORDER") - us_order: $translate.instant("ACTIVITY.FIELDS.US_ORDER") - } - - return humanizedFieldNames[field] or field - - countChanges = (comment) -> - return _.keys(comment.values_diff).length - - formatChange = (change) -> - if _.isArray(change) - if change.length == 0 - return $translate.instant("ACTIVITY.VALUES.EMPTY") - return change.join(", ") - - if change == "" - return $translate.instant("ACTIVITY.VALUES.EMPTY") - - if not change? or change == false - return $translate.instant("ACTIVITY.VALUES.NO") - - if change == true - return $translate.instant("ACTIVITY.VALUES.YES") - - return change - - # Render into string (operations without mutability) - - renderAttachmentEntry = (value) -> - attachments = _.map value, (changes, type) -> - if type == "new" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.NEW_ATTACHMENT"), - diff: change.filename - }) - else if type == "deleted" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.DELETED_ATTACHMENT"), - diff: change.filename - }) - else - return _.map changes, (change) -> - name = $translate.instant("ACTIVITY.UPDATED_ATTACHMENT", {filename: change.filename}) - - diff = _.map change.changes, (values, name) -> - return { - name: getHumanizedFieldName(name) - from: formatChange(values[0]) - to: formatChange(values[1]) - } - - return templateChangeAttachment({name: name, diff: diff}) - - return _.flatten(attachments).join("\n") - - renderCustomAttributesEntry = (value) -> - customAttributes = _.map value, (changes, type) -> - if type == "new" - return _.map changes, (change) -> - html = templateChangeGeneric({ - name: change.name, - from: formatChange(""), - to: formatChange(change.value) - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if type == "deleted" - return _.map changes, (change) -> - return templateChangeDiff({ - name: $translate.instant("ACTIVITY.DELETED_CUSTOM_ATTRIBUTE") - diff: change.name - }) - else - return _.map changes, (change) -> - customAttrsChanges = _.map change.changes, (values) -> - return templateChangeGeneric({ - name: change.name - from: formatChange(values[0]) - to: formatChange(values[1]) - }) - return _.flatten(customAttrsChanges).join("\n") - - return _.flatten(customAttributes).join("\n") - - renderChangeEntry = (field, value) -> - if field == "description" - return templateChangeDiff({name: getHumanizedFieldName("description"), diff: value[1]}) - else if field == "blocked_note" - return templateChangeDiff({name: getHumanizedFieldName("blocked_note"), diff: value[1]}) - else if field == "points" - html = templateChangePoints({points: value}) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if field == "attachments" - return renderAttachmentEntry(value) - else if field == "custom_attributes" - return renderCustomAttributesEntry(value) - else if field in ["tags", "watchers"] - name = getHumanizedFieldName(field) - removed = _.difference(value[0], value[1]) - added = _.difference(value[1], value[0]) - html = templateChangeList({name:name, removed:removed, added: added}) - - html = $compile(html)($scope) - - return html[0].outerHTML - else if field == "assigned_to" - name = getHumanizedFieldName(field) - from = formatChange(value[0] or $translate.instant("ACTIVITY.VALUES.UNASSIGNED")) - to = formatChange(value[1] or $translate.instant("ACTIVITY.VALUES.UNASSIGNED")) - return templateChangeGeneric({name:name, from:from, to: to}) - else - name = getHumanizedFieldName(field) - from = formatChange(value[0]) - to = formatChange(value[1]) - return templateChangeGeneric({name:name, from:from, to: to}) - - renderChangeEntries = (change) -> - return _.map(change.values_diff, (value, field) -> renderChangeEntry(field, value)) - - renderChangesHelperText = (change) -> - size = countChanges(change) - return $translate.instant("ACTIVITY.SIZE_CHANGE", {size: size}, 'messageformat') - - renderComment = (comment) -> - if (comment.delete_comment_date or comment.delete_comment_user?.name) - html = templateDeletedComment({ - deleteCommentDate: moment(comment.delete_comment_date).format(getPrettyDateFormat()) if comment.delete_comment_date - deleteCommentUser: comment.delete_comment_user.name - deleteComment: comment.comment_html - activityId: comment.id - canRestoreComment: ($scope.user and - (comment.delete_comment_user.pk == $scope.user.id or - $scope.project.my_permissions.indexOf("modify_project") > -1)) - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - - html = templateActivity({ - avatar: comment.user.photo - userFullName: comment.user.name - userProfileUrl: if comment.user.is_active then $navUrls.resolve("user-profile", {username: comment.user.username}) else "" - creationDate: moment(comment.created_at).format(getPrettyDateFormat()) - comment: comment.comment_html - changesText: renderChangesHelperText(comment) - changes: renderChangeEntries(comment) - mode: "comment" - deleteCommentActionTitle: $translate.instant("COMMENTS.DELETE") - deleteCommentDate: moment(comment.delete_comment_date).format(getPrettyDateFormat()) if comment.delete_comment_date - deleteCommentUser: comment.delete_comment_user.name if comment.delete_comment_user?.name - activityId: comment.id - canDeleteComment: comment.user.pk == $scope.user?.id or $scope.project.my_permissions.indexOf("modify_project") > -1 - }) - - html = $compile(html)($scope) - - return html[0].outerHTML - - renderChange = (change) -> - return templateActivity({ - avatar: change.user.photo - userFullName: change.user.name - userProfileUrl: if change.user.is_active then $navUrls.resolve("user-profile", {username: change.user.username}) else "" - creationDate: moment(change.created_at).format(getPrettyDateFormat()) - comment: change.comment_html - changes: renderChangeEntries(change) - changesText: "" - mode: "activity" - deleteCommentDate: moment(change.delete_comment_date).format(getPrettyDateFormat()) if change.delete_comment_date - deleteCommentUser: change.delete_comment_user.name if change.delete_comment_user?.name - activityId: change.id - }) - - renderHistory = (entries, totalEntries) -> - if entries.length == totalEntries - showMore = 0 - else - showMore = totalEntries - entries.length - - html = templateBaseEntries({entries: entries, showMore:showMore}) - html = $compile(html)($scope) - return html - - # Render into DOM (operations with dom mutability) - - renderBase = -> - comments = $scope.comments or [] - changes = $scope.history or [] - - historyVisible = !!changes.length - commentsVisible = (!!comments.length) || checkPermissionsService.check('modify_' + $attrs.type) - - html = templateBase({ - ngmodel: $attrs.ngModel, - type: $attrs.type, - mode: $attrs.mode, - historyVisible: historyVisible, - commentsVisible: commentsVisible - }) - - html = $compile(html)($scope) - - $el.html(html) - - rerender = -> - renderBase() - renderComments() - renderActivity() - - renderComments = -> - comments = $scope.comments or [] - totalComments = comments.length - - if not showAllComments - comments = _.takeRight(comments, 4) - - comments = _.map comments, (x) -> renderComment(x) - - html = renderHistory(comments, totalComments) - $el.find(".comments-list").html(html) - - renderActivity = -> - changes = $scope.history or [] - totalChanges = changes.length - if not showAllActivity - changes = _.takeRight(changes, 4) - - changes = _.map(changes, (x) -> renderChange(x)) - html = renderHistory(changes, totalChanges) - $el.find(".changes-list").html(html) - - save = $qqueue.bindAdd (target) => - $scope.$broadcast("markdown-editor:submit") - - $el.find(".comment-list").addClass("activeanimation") - - currentLoading = $loading() - .target(target) - .start() - - onSuccess = -> - $rootScope.$broadcast("comment:new") - - $ctrl.loadHistory(type, objectId).finally -> - currentLoading.finish() - - onError = -> - currentLoading.finish() - $confirm.notify("error") - - model = $scope.$eval($attrs.ngModel) - - $ctrl.repo.save(model).then(onSuccess, onError) - - # Watchers - - $scope.$watch("comments", rerender) - $scope.$watch("history", rerender) - - $scope.$on("object:updated", -> $ctrl.loadHistory(type, objectId)) - - # Events - - $el.on "click", ".add-comment .button-green", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - save(target) - - $el.on "click", "a", (event) -> - target = angular.element(event.target) - href = target.attr('href') - if href && href.indexOf("#") == 0 - event.preventDefault() - $('body').scrollTop($(href).offset().top) - - $el.on "click", ".show-more", (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - if target.parent().is(".changes-list") - showAllActivity = not showAllActivity - renderActivity() - else - showAllComments = not showAllComments - renderComments() - - $el.on "click", ".show-deleted-comment", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parents('.activity-single').find('.hide-deleted-comment').show() - target.parents('.activity-single').find('.show-deleted-comment').hide() - target.parents('.activity-single').find('.comment-body').show() - - $el.on "click", ".hide-deleted-comment", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parents('.activity-single').find('.hide-deleted-comment').hide() - target.parents('.activity-single').find('.show-deleted-comment').show() - target.parents('.activity-single').find('.comment-body').hide() - - $el.on "click", ".changes-title", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.parent().find(".change-entry").toggleClass("active") - - $el.on "focus", ".add-comment textarea", (event) -> - $(this).addClass('active') - - $el.on "click", ".history-tabs a", (event) -> - target = angular.element(event.currentTarget) - - $el.find(".history-tabs li").removeClass("active") - target.parent().addClass("active") - - $el.find(".history section").addClass("hidden") - $el.find(".history section.#{target.data('section-class')}").removeClass("hidden") - - $el.on "click", ".comment-delete", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - activityId = target.data('activity-id') - $ctrl.deleteComment(type, objectId, activityId) - - $el.on "click", ".comment-restore", debounce 2000, (event) -> - event.preventDefault() - - target = angular.element(event.currentTarget) - activityId = target.data('activity-id') - $ctrl.undeleteComment(type, objectId, activityId) - - $scope.$on "$destroy", -> - $el.off() - - renderBase() - - return { - controller: HistoryController - restrict: "AE" - link: link - # require: ["ngModel", "tgHistory"] - } - - -module.directive("tgHistory", ["$log", "$tgLoading", "$tgQqueue", "$tgTemplate", "$tgConfirm", "$translate", - "$compile", "$tgNavUrls", "$rootScope", "tgCheckPermissionsService", HistoryDirective]) diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index c658a710..a012d064 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -28,6 +28,7 @@ bindOnce = @.taiga.bindOnce timeout = @.taiga.timeout debounce = @.taiga.debounce sizeFormat = @.taiga.sizeFormat +trim = @.taiga.trim ############################################################################# ## Common Lightbox Services @@ -35,9 +36,11 @@ sizeFormat = @.taiga.sizeFormat # the lightboxContent hide/show doesn't have sense because is an IE hack class LightboxService extends taiga.Service - constructor: (@animationFrame, @q) -> + constructor: (@animationFrame, @q, @rootScope) -> + + open: ($el, onClose) -> + @.onClose = onClose - open: ($el) -> if _.isString($el) $el = $($el) defered = @q.defer() @@ -70,25 +73,29 @@ class LightboxService extends taiga.Service return defered.promise close: ($el) -> - if _.isString($el) - $el = $($el) - docEl = angular.element(document) - docEl.off(".lightbox") - docEl.off(".keyboard-navigation") # Hack: to fix problems in the WYSIWYG textareas when press ENTER + return new Promise (resolve) => + if _.isString($el) + $el = $($el) + docEl = angular.element(document) + docEl.off(".lightbox") + docEl.off(".keyboard-navigation") # Hack: to fix problems in the WYSIWYG textareas when press ENTER - @animationFrame.add -> - $el.addClass('close') + @animationFrame.add => + $el.addClass('close') - $el.one "transitionend", => - $el.removeAttr('style') - $el.removeClass("open").removeClass('close') + $el.one "transitionend", => + $el.removeAttr('style') + $el.removeClass("open").removeClass('close') + if @.onClose + @rootScope.$apply(@.onClose) + resolve() - if $el.hasClass("remove-on-close") - scope = $el.data("scope") - scope.$destroy() if scope - $el.remove() + if $el.hasClass("remove-on-close") + scope = $el.data("scope") + scope.$destroy() if scope + $el.remove() closeAll: -> docEl = angular.element(document) @@ -96,7 +103,7 @@ class LightboxService extends taiga.Service @.close($(lightboxEl)) -module.service("lightboxService", ["animationFrame", "$q", LightboxService]) +module.service("lightboxService", ["animationFrame", "$q", "$rootScope", LightboxService]) class LightboxKeyboardNavigationService extends taiga.Service @@ -292,7 +299,44 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, attachmentsToAdd = attachmentsToAdd.push(attachment) $scope.deleteAttachment = (attachment) -> - attachmentsToDelete = attachmentsToDelete.push(attachment) + if attachment.get("id") + attachmentsToDelete = attachmentsToDelete.push(attachment) + + $scope.addTag = (tag, color) -> + value = trim(tag.toLowerCase()) + + tags = $scope.project.tags + projectTags = $scope.project.tags_colors + + tags = [] if not tags? + projectTags = {} if not projectTags? + + if value not in tags + tags.push(value) + + projectTags[tag] = color || null + + $scope.project.tags = tags + + itemtags = _.clone($scope.us.tags) + + inserted = _.find itemtags, (it) -> it[0] == value + + if !inserted + itemtags.push([tag , color]) + $scope.us.tags = itemtags + + $scope.deleteTag = (tag) -> + value = trim(tag[0].toLowerCase()) + + tags = $scope.project.tags + itemtags = _.clone($scope.us.tags) + + _.remove itemtags, (tag) -> tag[0] == value + + $scope.us.tags = itemtags + + _.pull($scope.us.tags, value) $scope.$on "usform:new", (ctx, projectId, status, statusList) -> form.reset() if form @@ -320,7 +364,10 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $el.find("label.team-requirement").removeClass("selected") $el.find("label.client-requirement").removeClass("selected") - lightboxService.open($el) + $scope.createEditUsOpen = true + + lightboxService.open $el, () -> + $scope.createEditUsOpen = false $scope.$on "usform:edit", (ctx, us, attachments) -> form.reset() if form @@ -353,7 +400,10 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, else $el.find("label.client-requirement").removeClass("selected") - lightboxService.open($el) + $scope.createEditUsOpen = true + + lightboxService.open $el, () -> + $scope.createEditUsOpen = false createAttachments = (obj) -> promises = _.map attachmentsToAdd.toJS(), (attachment) -> @@ -378,22 +428,28 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, .target(submitButton) .start() + params = { + include_attachments: true, + include_tasks: true + } + if $scope.isNew promise = $repo.create("userstories", $scope.us) broadcastEvent = "usform:new:success" else - promise = $repo.save($scope.us) + promise = $repo.save($scope.us, true) broadcastEvent = "usform:edit:success" promise.then (data) -> - deleteAttachments(data).then () => createAttachments(data) + deleteAttachments(data) + .then () => createAttachments(data) + .then () => + currentLoading.finish() + lightboxService.close($el) - return data + $rs.userstories.getByRef(data.project, data.ref, params).then (us) -> + $rootScope.$broadcast(broadcastEvent, us) - promise.then (data) -> - currentLoading.finish() - lightboxService.close($el) - $rootScope.$broadcast(broadcastEvent, data) promise.then null, (data) -> currentLoading.finish() @@ -407,8 +463,10 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $el.on "click", ".close", (event) -> event.preventDefault() + $scope.$apply -> $scope.us.revert() + lightboxService.close($el) $el.keydown (event) -> @@ -433,7 +491,7 @@ module.directive("tgLbCreateEditUserstory", [ "$translate", "$tgConfirm", "$q", - "tgAttachmentsService", + "tgAttachmentsService" CreateEditUserstoryDirective ]) @@ -442,7 +500,7 @@ module.directive("tgLbCreateEditUserstory", [ ## Creare Bulk Userstories Lightbox Directive ############################################################################# -CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading) -> +CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $loading, $model) -> link = ($scope, $el, attrs) -> form = null @@ -469,6 +527,7 @@ CreateBulkUserstoriesDirective = ($repo, $rs, $rootscope, lightboxService, $load promise = $rs.userstories.bulkCreate($scope.new.projectId, $scope.new.statusId, $scope.new.bulk) promise.then (result) -> + result = _.map(result.data, (x) => $model.make_model('userstories', x)) currentLoading.finish() $rootscope.$broadcast("usform:bulk:success", result) lightboxService.close($el) @@ -494,6 +553,7 @@ module.directive("tgLbCreateBulkUserstories", [ "$rootScope", "lightboxService", "$tgLoading", + "$tgModel", CreateBulkUserstoriesDirective ]) @@ -502,7 +562,7 @@ module.directive("tgLbCreateBulkUserstories", [ ## AssignedTo Lightbox Directive ############################################################################# -AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationService, $template, $compile) -> +AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationService, $template, $compile, avatarService) -> link = ($scope, $el, $attrs) -> selectedUser = null selectedItem = null @@ -527,12 +587,21 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic render = (selected, text) -> users = _.clone($scope.activeUsers, true) users = _.reject(users, {"id": selected.id}) if selected? + users = _.sortBy(users, (o) -> if o.id is $scope.user.id then 0 else o.id) users = _.filter(users, _.partial(filterUsers, text)) if text? + visibleUsers = _.slice(users, 0, 5) + + visibleUsers = _.map visibleUsers, (user) -> + user.avatar = avatarService.getAvatar(user) + + if selected + selected.avatar = avatarService.getAvatar(selected) if selected + ctx = { selected: selected users: _.slice(users, 0, 5) - showMore: users.length > 5 + showMore: visibleUsers } html = usersTemplate(ctx) @@ -596,14 +665,14 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic } -module.directive("tgLbAssignedto", ["lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", AssignedToLightboxDirective]) +module.directive("tgLbAssignedto", ["lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", "tgAvatarService", AssignedToLightboxDirective]) ############################################################################# ## Watchers Lightbox directive ############################################################################# -WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile) -> +WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile, avatarService) -> link = ($scope, $el, $attrs) -> selectedItem = null usersTemplate = $template.get("common/lightbox/lightbox-assigned-to-users.html", true) @@ -625,9 +694,16 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS # Render the specific list of users. render = (users) -> + visibleUsers = _.slice(users, 0, 5) + + visibleUsers = _.map visibleUsers, (user) -> + user.avatar = avatarService.getAvatar(user) + + return user + ctx = { selected: false - users: _.slice(users, 0, 5) + users: visibleUsers showMore: users.length > 5 } @@ -683,25 +759,9 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS link:link } -module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", WatchersLightboxDirective]) +module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", "tgAvatarService", WatchersLightboxDirective]) -############################################################################# -## Attachment Preview Lighbox -############################################################################# - -AttachmentPreviewLightboxDirective = (lightboxService, $template, $compile) -> - link = ($scope, $el, attrs) -> - lightboxService.open($el) - - return { - templateUrl: 'common/lightbox/lightbox-attachment-preview.html', - link: link, - scope: true - } - -module.directive("tgLbAttachmentPreview", ["lightboxService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective]) - LightboxLeaveProjectWarningDirective = (lightboxService, $template, $compile) -> link = ($scope, $el, attrs) -> lightboxService.open($el) diff --git a/app/coffee/modules/common/loading.coffee b/app/coffee/modules/common/loading.coffee index aec1069e..5ee375e2 100644 --- a/app/coffee/modules/common/loading.coffee +++ b/app/coffee/modules/common/loading.coffee @@ -59,6 +59,7 @@ TgLoadingService = ($compile) -> start: -> target = service.settings.target + service.settings.classes.map (className) -> target.removeClass(className) if not target.hasClass('loading') && !service.settings.template @@ -109,6 +110,7 @@ LoadingDirective = ($loading) -> template = $el.html() $scope.$watch attr.tgLoading, (showLoading) => + if showLoading currentLoading = $loading() .target($el) @@ -120,6 +122,7 @@ LoadingDirective = ($loading) -> currentLoading.finish() return { + priority: 99999, link:link } diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee index 0bd42dfa..cb38eee9 100644 --- a/app/coffee/modules/common/tags.coffee +++ b/app/coffee/modules/common/tags.coffee @@ -26,6 +26,7 @@ taiga = @.taiga trim = @.taiga.trim bindOnce = @.taiga.bindOnce + module = angular.module("taigaCommon") # Directive that parses/format tags inputfield. @@ -61,28 +62,38 @@ ColorizeTagsDirective = -> templates = { backlog: _.template(""" <% _.each(tags, function(tag) { %> - <%- tag.name %> + + style="border-left: 5px solid <%- tag[1] %>" + <% } %> + title="<%- tag[0] %>"><%- tag[0] %> <% }) %> """) kanban: _.template(""" <% _.each(tags, function(tag) { %> - + + style="border-color: <%- tag[1] %>" + <% } %> + title="<%- tag[0] %>" /> <% }) %> """) taskboard: _.template(""" <% _.each(tags, function(tag) { %> - + + style="border-color: <%- tag[1] %>" + <% } %> + title="<%- tag[0] %>" /> <% }) %> """) } link = ($scope, $el, $attrs, $ctrl) -> - render = (srcTags) -> + render = (tags) -> template = templates[$attrs.tgColorizeTagsType] - srcTags.sort() - tags = _.map srcTags, (tag) -> - color = $scope.project.tags_colors[tag] - return {name: tag, color: color} html = template({tags: tags}) $el.html(html) @@ -111,15 +122,18 @@ LbTagLineDirective = ($rs, $template, $compile) -> autocomplete = null link = ($scope, $el, $attrs, $model) -> - ## Render - renderTags = (tags, tagsColors) -> - ctx = { - tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) - } + withoutColors = _.has($attrs, "withoutColors") - _.map ctx.tags, (tag) => - if tag.color - tag.style = "border-left: 5px solid #{tag.color}" + ## Render + renderTags = (tags, tagsColors = []) -> + color = if not withoutColors then tagsColors[t] else null + + ctx = { + tags: _.map(tags, (t) -> { + name: t, + style: if color then "border-left: 5px solid #{color}" else "" + }) + } html = $compile(templateTags(ctx))($scope) $el.find(".tags-container").html(html) @@ -196,7 +210,7 @@ LbTagLineDirective = ($rs, $template, $compile) -> autocomplete = new Awesomplete(input[0], { list: _.keys(project.tags_colors) - }); + }) input.on "awesomplete-selectcomplete", () -> addValue(input.val()) @@ -216,189 +230,3 @@ LbTagLineDirective = ($rs, $template, $compile) -> } module.directive("tgLbTagLine", ["$tgResources", "$tgTemplate", "$compile", LbTagLineDirective]) - - -############################################################################# -## TagLine Directive (for detail pages) -############################################################################# - -TagLineDirective = ($rootScope, $repo, $rs, $confirm, $modelTransform, $template, $compile) -> - ENTER_KEY = 13 - ESC_KEY = 27 - COMMA_KEY = 188 - - templateTags = $template.get("common/tag/tags-line-tags.html", true) - - link = ($scope, $el, $attrs, $model) -> - autocomplete = null - - isEditable = -> - if $attrs.requiredPerm? - return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 - - return true - - ## Render - renderTags = (tags, tagsColors) -> - ctx = { - tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) - isEditable: isEditable() - } - html = $compile(templateTags(ctx))($scope) - $el.find("div.tags-container").html(html) - - renderInReadModeOnly = -> - $el.find(".add-tag").remove() - $el.find("input").remove() - $el.find(".save").remove() - - showAddTagButton = -> $el.find(".add-tag").removeClass("hidden") - hideAddTagButton = -> $el.find(".add-tag").addClass("hidden") - - showAddTagButtonText = -> $el.find(".add-tag-text").removeClass("hidden") - hideAddTagButtonText = -> $el.find(".add-tag-text").addClass("hidden") - - showSaveButton = -> $el.find(".save").removeClass("hidden") - hideSaveButton = -> $el.find(".save").addClass("hidden") - - showInput = -> $el.find("input").removeClass("hidden").focus() - hideInput = -> $el.find("input").addClass("hidden").blur() - resetInput = -> - $el.find("input").val("") - - autocomplete.close() - - ## Aux methods - addValue = (value) -> - value = trim(value.toLowerCase()) - return if value.length == 0 - - transform = $modelTransform.save (item) -> - if not item.tags - item.tags = [] - - tags = _.clone(item.tags) - - tags.push(value) if value not in tags - - item.tags = tags - - return item - - onSuccess = -> - $rootScope.$broadcast("object:updated") - - onError = -> - $confirm.notify("error") - - hideSaveButton() - - return transform.then(onSuccess, onError) - - deleteValue = (value) -> - value = trim(value.toLowerCase()) - return if value.length == 0 - - transform = $modelTransform.save (item) -> - tags = _.clone(item.tags, false) - item.tags = _.pull(tags, value) - - return item - - onSuccess = -> - $rootScope.$broadcast("object:updated") - - onError = -> - $confirm.notify("error") - - return transform.then(onSuccess, onError) - - saveInputTag = () -> - value = $el.find("input").val() - - addValue(value) - resetInput() - - ## Events - $el.on "keypress", "input", (event) -> - target = angular.element(event.currentTarget) - - if event.keyCode == ENTER_KEY - saveInputTag() - else if String.fromCharCode(event.keyCode) == ',' - event.preventDefault() - saveInputTag() - else - if target.val().length - showSaveButton() - else - hideSaveButton() - - $el.on "keyup", "input", (event) -> - if event.keyCode == ESC_KEY - resetInput() - hideInput() - hideSaveButton() - showAddTagButton() - - $el.on "click", ".save", (event) -> - event.preventDefault() - saveInputTag() - - $el.on "click", ".add-tag", (event) -> - event.preventDefault() - hideAddTagButton() - showInput() - - $el.on "click", ".remove-tag", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - - value = target.siblings(".tag-name").text() - - deleteValue(value) - - bindOnce $scope, "project.tags_colors", (tags_colors) -> - if not isEditable() - renderInReadModeOnly() - return - - showAddTagButton() - - input = $el.find("input") - - autocomplete = new Awesomplete(input[0], { - list: _.keys(tags_colors) - }); - - input.on "awesomplete-selectcomplete", () -> - addValue(input.val()) - input.val("") - - - $scope.$watchCollection () -> - return $model.$modelValue?.tags - , () -> - model = $model.$modelValue - - return if not model - - if model.tags?.length - hideAddTagButtonText() - else - showAddTagButtonText() - - tagsColors = $scope.project?.tags_colors or [] - renderTags(model.tags, tagsColors) - - $scope.$on "$destroy", -> - $el.off() - - return { - link:link, - require:"ngModel" - templateUrl: "common/tag/tag-line.html" - } - -module.directive("tgTagLine", ["$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$tgQueueModelTransformation", - "$tgTemplate", "$compile", TagLineDirective]) diff --git a/app/coffee/modules/common/wisiwyg.coffee b/app/coffee/modules/common/wisiwyg.coffee index e6fe3b0c..ee143b59 100644 --- a/app/coffee/modules/common/wisiwyg.coffee +++ b/app/coffee/modules/common/wisiwyg.coffee @@ -83,7 +83,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans markdownDomNode = element.parents(".markdown") markItUpDomNode = element.parents(".markItUp") - $rs.mdrender.render($scope.projectId, $model.$modelValue).then (data) -> + $rs.mdrender.render($scope.projectId || $scope.vm.projectId, $model.$modelValue).then (data) -> html = previewTemplate({data: data.data}) html = $compile(html)($scope) @@ -374,7 +374,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans search: (term, callback) -> term = taiga.slugify(term) - searchTypes = ['issues', 'tasks', 'userstories'] + searchTypes = ['issues', 'tasks', 'userstories', 'epics'] searchProps = ['ref', 'subject'] filter = (item) => @@ -384,8 +384,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans return false cancelablePromise.abort() if cancelablePromise - - cancelablePromise = $rs.search.do($scope.projectId, term) + cancelablePromise = $rs.search.do($scope.projectId || $scope.vm.projectId, term) cancelablePromise.then (res) => # ignore wikipages if they're the only results. can't exclude them in search @@ -441,7 +440,7 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans search: (term, callback) -> term = taiga.slugify(term) - $rs.search.do($scope.projectId, term).then (res) => + $rs.search.do($scope.projectId || $scope.vm.projectId, term).then (res) => if res.count < 1 callback([]) diff --git a/app/coffee/modules/controllerMixins.coffee b/app/coffee/modules/controllerMixins.coffee index 18724b4b..a6c501cd 100644 --- a/app/coffee/modules/controllerMixins.coffee +++ b/app/coffee/modules/controllerMixins.coffee @@ -110,4 +110,201 @@ class FiltersMixin location = if load then @location else @location.noreload(@scope) location.search(name, value) + applyStoredFilters: (projectSlug, key) -> + if _.isEmpty(@location.search()) + filters = @.getFilters(projectSlug, key) + if Object.keys(filters).length + @location.search(filters) + @location.replace() + + return true + + return false + + storeFilters: (projectSlug, params, filtersHashSuffix) -> + ns = "#{projectSlug}:#{filtersHashSuffix}" + hash = taiga.generateHash([projectSlug, ns]) + @storage.set(hash, params) + + getFilters: (projectSlug, filtersHashSuffix) -> + ns = "#{projectSlug}:#{filtersHashSuffix}" + hash = taiga.generateHash([projectSlug, ns]) + + return @storage.get(hash) or {} + + formatSelectedFilters: (type, list, urlIds) -> + selectedIds = urlIds.split(',') + selectedFilters = _.filter list, (it) -> + selectedIds.indexOf(_.toString(it.id)) != -1 + + return _.map selectedFilters, (it) -> + return { + id: it.id + key: type + ":" + it.id + dataType: type, + name: it.name + color: it.color + } + taiga.FiltersMixin = FiltersMixin + +############################################################################# +## Us Filters Mixin +############################################################################# + +class UsFiltersMixin + changeQ: (q) -> + @.replaceFilter("q", q) + @.filtersReloadContent() + @.generateFilters() + + removeFilter: (filter) -> + @.unselectFilter(filter.dataType, filter.id) + @.filtersReloadContent() + @.generateFilters() + + addFilter: (newFilter) -> + @.selectFilter(newFilter.category.dataType, newFilter.filter.id) + @.filtersReloadContent() + @.generateFilters() + + selectCustomFilter: (customFilter) -> + @.replaceAllFilters(customFilter.filter) + @.filtersReloadContent() + @.generateFilters() + + saveCustomFilter: (name) -> + filters = {} + urlfilters = @location.search() + filters.tags = urlfilters.tags + filters.status = urlfilters.status + filters.assigned_to = urlfilters.assigned_to + filters.owner = urlfilters.owner + + @filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) => + userFilters[name] = filters + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters) + + removeCustomFilter: (customFilter) -> + @filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) => + delete userFilters[customFilter.id] + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters) + @.generateFilters() + + generateFilters: -> + @.storeFilters(@params.pslug, @location.search(), @.storeFiltersName) + + urlfilters = @location.search() + + loadFilters = {} + loadFilters.project = @scope.projectId + loadFilters.tags = urlfilters.tags + loadFilters.status = urlfilters.status + loadFilters.assigned_to = urlfilters.assigned_to + loadFilters.owner = urlfilters.owner + loadFilters.epic = urlfilters.epic + loadFilters.q = urlfilters.q + + return @q.all([ + @rs.userstories.filtersData(loadFilters), + @filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName) + ]).then (result) => + data = result[0] + customFiltersRaw = result[1] + + statuses = _.map data.statuses, (it) -> + it.id = it.id.toString() + + return it + tags = _.map data.tags, (it) -> + it.id = it.name + + return it + tagsWithAtLeastOneElement = _.filter tags, (tag) -> + return tag.count > 0 + assignedTo = _.map data.assigned_to, (it) -> + if it.id + it.id = it.id.toString() + else + it.id = "null" + + it.name = it.full_name || "Unassigned" + + return it + owner = _.map data.owners, (it) -> + it.id = it.id.toString() + it.name = it.full_name + + return it + epic = _.map data.epics, (it) -> + if it.id + it.id = it.id.toString() + it.name = "##{it.ref} #{it.subject}" + else + it.id = "null" + it.name = "Not in an epic" + + return it + + @.selectedFilters = [] + + if loadFilters.status + selected = @.formatSelectedFilters("status", statuses, loadFilters.status) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.tags + selected = @.formatSelectedFilters("tags", tags, loadFilters.tags) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.assigned_to + selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.owner + selected = @.formatSelectedFilters("owner", owner, loadFilters.owner) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.epic + selected = @.formatSelectedFilters("epic", epic, loadFilters.epic) + @.selectedFilters = @.selectedFilters.concat(selected) + + @.filterQ = loadFilters.q + + @.filters = [ + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"), + dataType: "status", + content: statuses + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"), + dataType: "tags", + content: tags, + hideEmpty: true, + totalTaggedElements: tagsWithAtLeastOneElement.length + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"), + dataType: "assigned_to", + content: assignedTo + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), + dataType: "owner", + content: owner + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.EPIC"), + dataType: "epic", + content: epic + } + ] + + @.customFilters = [] + _.forOwn customFiltersRaw, (value, key) => + @.customFilters.push({id: key, name: key, filter: value}) + + +taiga.UsFiltersMixin = UsFiltersMixin diff --git a/app/coffee/modules/epics.coffee b/app/coffee/modules/epics.coffee new file mode 100644 index 00000000..743e70d4 --- /dev/null +++ b/app/coffee/modules/epics.coffee @@ -0,0 +1,25 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/epics.coffee +### + +module = angular.module("taigaEpics", []) diff --git a/app/coffee/modules/epics/detail.coffee b/app/coffee/modules/epics/detail.coffee new file mode 100644 index 00000000..f5a35849 --- /dev/null +++ b/app/coffee/modules/epics/detail.coffee @@ -0,0 +1,337 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/epics/detail.coffee +### + +taiga = @.taiga + +mixOf = @.taiga.mixOf +toString = @.taiga.toString +joinStr = @.taiga.joinStr +groupBy = @.taiga.groupBy +bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods + +module = angular.module("taigaEpics") + +############################################################################# +## Epic Detail Controller +############################################################################# + +class EpicDetailController extends mixOf(taiga.Controller, taiga.PageMixin) + @.$inject = [ + "$scope", + "$rootScope", + "$tgRepo", + "$tgConfirm", + "$tgResources", + "tgResources" + "$routeParams", + "$q", + "$tgLocation", + "$log", + "tgAppMetaService", + "$tgAnalytics", + "$tgNavUrls", + "$translate", + "$tgQueueModelTransformation", + "tgErrorHandlingService" + ] + + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location, + @log, @appMetaService, @analytics, @navUrls, @translate, @modelTransform, @errorHandlingService) -> + bindMethods(@) + + @scope.epicRef = @params.epicref + @scope.sectionName = @translate.instant("EPIC.SECTION_NAME") + @.initializeEventHandlers() + + promise = @.loadInitialData() + + # On Success + promise.then => + @._setMeta() + @.initializeOnDeleteGoToUrl() + + # On Error + promise.then null, @.onInitialDataError.bind(@) + + _setMeta: -> + title = @translate.instant("EPIC.PAGE_TITLE", { + epicRef: "##{@scope.epic.ref}" + epicSubject: @scope.epic.subject + projectName: @scope.project.name + }) + description = @translate.instant("EPIC.PAGE_DESCRIPTION", { + epicStatus: @scope.statusById[@scope.epic.status]?.name or "--" + epicDescription: angular.element(@scope.epic.description_html or "").text() + }) + @appMetaService.setAll(title, description) + + initializeEventHandlers: -> + @scope.$on "attachment:create", => + @analytics.trackEvent("attachment", "create", "create attachment on epic", 1) + + @scope.$on "comment:new", => + @.loadEpic() + + @scope.$on "custom-attributes-values:edit", => + @rootscope.$broadcast("object:updated") + + initializeOnDeleteGoToUrl: -> + ctx = {project: @scope.project.slug} + @scope.onDeleteGoToUrl = @navUrls.resolve("project-epics", ctx) + + loadProject: -> + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id + @scope.project = project + @scope.immutableProject = Immutable.fromJS(project._attrs) + @scope.$emit('project:loaded', project) + @scope.statusList = project.epic_statuses + @scope.statusById = groupBy(project.epic_statuses, (x) -> x.id) + return project + + loadEpic: -> + return @rs.epics.getByRef(@scope.projectId, @params.epicref).then (epic) => + @scope.epic = epic + @scope.immutableEpic = Immutable.fromJS(epic._attrs) + @scope.epicId = epic.id + @scope.commentModel = epic + + @modelTransform.setObject(@scope, 'epic') + + if @scope.epic.neighbors.previous?.ref? + ctx = { + project: @scope.project.slug + ref: @scope.epic.neighbors.previous.ref + } + @scope.previousUrl = @navUrls.resolve("project-epics-detail", ctx) + + if @scope.epic.neighbors.next?.ref? + ctx = { + project: @scope.project.slug + ref: @scope.epic.neighbors.next.ref + } + @scope.nextUrl = @navUrls.resolve("project-epics-detail", ctx) + + loadUserstories: -> + return @rs2.userstories.listInEpic(@scope.epicId).then (data) => + @scope.userstories = data + + loadInitialData: -> + promise = @.loadProject() + return promise.then (project) => + @.fillUsersAndRoles(project.members, project.roles) + @.loadEpic().then(=> @.loadUserstories()) + + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadEpic() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.epics.upvote(@scope.epicId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadEpic() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.epics.downvote(@scope.epicId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadEpic() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.epics.watch(@scope.epicId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadEpic() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.epics.unwatch(@scope.epicId).then(onSuccess, onError) + + onSelectColor: (color) -> + onSelectColorSuccess = () => + @rootscope.$broadcast("object:updated") + @confirm.notify('success') + + onSelectColorError = () => + @confirm.notify('error') + + transform = @modelTransform.save (epic) -> + epic.color = color + return epic + + return transform.then(onSelectColorSuccess, onSelectColorError) + +module.controller("EpicDetailController", EpicDetailController) + + +############################################################################# +## Epic status display directive +############################################################################# + +EpicStatusDisplayDirective = ($template, $compile) -> + # Display if an epic is open or closed and its status. + # + # Example: + # tg-epic-status-display(ng-model="epic") + # + # Requirements: + # - Epic object (ng-model) + # - scope.statusById object + + template = $template.get("common/components/status-display.html", true) + + link = ($scope, $el, $attrs) -> + render = (epic) -> + status = $scope.statusById[epic.status] + + html = template({ + is_closed: status.is_closed + status: status + }) + + html = $compile(html)($scope) + $el.html(html) + + $scope.$watch $attrs.ngModel, (epic) -> + render(epic) if epic? + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgEpicStatusDisplay", ["$tgTemplate", "$compile", EpicStatusDisplayDirective]) + + +############################################################################# +## Epic status button directive +############################################################################# + +EpicStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTransform, $compile, $translate, $template) -> + # Display the status of epic and you can edit it. + # + # Example: + # tg-epic-status-button(ng-model="epic") + # + # Requirements: + # - Epic object (ng-model) + # - scope.statusById object + # - $scope.project.my_permissions + + template = $template.get("common/components/status-button.html", true) + + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project.my_permissions.indexOf("modify_epic") != -1 + + render = (epic) => + status = $scope.statusById[epic.status] + + html = $compile(template({ + status: status + statuses: $scope.statusList + editable: isEditable() + }))($scope) + + $el.html(html) + + save = (status) -> + currentLoading = $loading() + .target($el) + .start() + + transform = $modelTransform.save (epic) -> + epic.status = status + + return epic + + onSuccess = -> + $rootScope.$broadcast("object:updated") + currentLoading.finish() + + onError = -> + $confirm.notify("error") + currentLoading.finish() + + transform.then(onSuccess, onError) + + $el.on "click", ".js-edit-status", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + $el.find(".pop-status").popover().open() + + $el.on "click", ".status", (event) -> + event.preventDefault() + event.stopPropagation() + return if not isEditable() + + target = angular.element(event.currentTarget) + + $.fn.popover().closeAll() + + save(target.data("status-id")) + + $scope.$watch () -> + return $model.$modelValue?.status + , () -> + epic = $model.$modelValue + render(epic) if epic + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + require: "ngModel" + } + +module.directive("tgEpicStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation", + "$compile", "$translate", "$tgTemplate", EpicStatusButtonDirective]) diff --git a/app/coffee/modules/events.coffee b/app/coffee/modules/events.coffee index ef0bb491..d43eb12f 100644 --- a/app/coffee/modules/events.coffee +++ b/app/coffee/modules/events.coffee @@ -96,6 +96,7 @@ class EventsService maxMissedHeartbeats = @config.get("eventsMaxMissedHeartbeats", 5) heartbeatIntervalTime = @config.get("eventsHeartbeatIntervalTime", 60000) + reconnectTryInterval = @config.get("eventsReconnectTryInterval", 10000) @.missedHeartbeats = 0 @.heartbeatInterval = setInterval(() => @@ -108,7 +109,7 @@ class EventsService @log.debug("HeartBeat send PING") catch e @log.error("HeartBeat error: " + e.message) - @.stopHeartBeatMessages() + @.setupConnection() , heartbeatIntervalTime) @log.debug("HeartBeat enabled") @@ -228,11 +229,13 @@ class EventsService onError: (error) -> @log.error("WebSocket error: #{error}") @.error = true + setTimeout(@.setupConnection, @.reconnectTryInterval) onClose: -> @log.debug("WebSocket closed.") @.connected = false @.stopHeartBeatMessages() + setTimeout(@.setupConnection, @.reconnectTryInterval) class EventsProvider diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index 1448c28a..bb65f413 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -52,11 +52,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgAnalytics", "$tgNavUrls", "$translate", - "$tgQueueModelTransformation" + "$tgQueueModelTransformation", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appMetaService, @analytics, @navUrls, @translate, @modelTransform) -> + @log, @appMetaService, @analytics, @navUrls, @translate, @modelTransform, @errorHandlingService) -> bindMethods(@) @scope.issueRef = @params.issueref diff --git a/app/coffee/modules/issues/lightboxes.coffee b/app/coffee/modules/issues/lightboxes.coffee index 3be9bb6b..e94f485e 100644 --- a/app/coffee/modules/issues/lightboxes.coffee +++ b/app/coffee/modules/issues/lightboxes.coffee @@ -25,6 +25,7 @@ taiga = @.taiga bindOnce = @.taiga.bindOnce debounce = @.taiga.debounce +trim = @.taiga.trim module = angular.module("taigaIssues") @@ -44,8 +45,8 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, resetAttachments() $el.find(".tag-input").val("") - - lightboxService.open($el) + lightboxService.open $el, () -> + $scope.createIssueOpen = false $scope.issue = { project: project.id @@ -57,10 +58,11 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, tags: [] } + $scope.createIssueOpen = true + $scope.$on "$destroy", -> $el.off() - createAttachments = (obj) -> promises = _.map attachmentsToAdd.toJS(), (attachment) -> return attachmentsService.upload(attachment.file, obj.id, $scope.issue.project, 'issue') @@ -76,6 +78,42 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, $scope.addAttachment = (attachment) -> attachmentsToAdd = attachmentsToAdd.push(attachment) + $scope.addTag = (tag, color) -> + value = trim(tag.toLowerCase()) + + tags = $scope.project.tags + projectTags = $scope.project.tags_colors + + tags = [] if not tags? + projectTags = {} if not projectTags? + + if value not in tags + tags.push(value) + + projectTags[tag] = color || null + + $scope.project.tags = tags + + itemtags = _.clone($scope.issue.tags) + + inserted = _.find itemtags, (it) -> it[0] == value + + if !inserted + itemtags.push([tag , color]) + $scope.issue.tags = itemtags + + $scope.deleteTag = (tag) -> + value = trim(tag[0].toLowerCase()) + + tags = $scope.project.tags + itemtags = _.clone($scope.us.tags) + + _.remove itemtags, (tag) -> tag[0] == value + + $scope.us.tags = itemtags + + _.pull($scope.issue.tags, value) + submit = debounce 2000, (event) => event.preventDefault() @@ -101,7 +139,6 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, currentLoading.finish() $confirm.notify("error") - submitButton = $el.find(".submit-button") $el.on "submit", "form", submit @@ -109,8 +146,8 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, return {link:link} -module.directive("tgLbCreateIssue", ["$tgRepo", "$tgConfirm", "$rootScope", "lightboxService", "$tgLoading", "$q", "tgAttachmentsService", - CreateIssueDirective]) +module.directive("tgLbCreateIssue", ["$tgRepo", "$tgConfirm", "$rootScope", "lightboxService", "$tgLoading", + "$q", "tgAttachmentsService", CreateIssueDirective]) ############################################################################# diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee index 21ab8c3a..81b577a5 100644 --- a/app/coffee/modules/issues/list.coffee +++ b/app/coffee/modules/issues/list.coffee @@ -32,6 +32,7 @@ groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce debounceLeading = @.taiga.debounceLeading startswith = @.taiga.startswith +bindMethods = @.taiga.bindMethods module = angular.module("taigaIssues") @@ -54,20 +55,24 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$tgNavUrls", "$tgEvents", "$tgAnalytics", - "$translate" + "$translate", + "tgErrorHandlingService", + "$tgStorage", + "tgFilterRemoteStorageService" ] + filtersHashSuffix: "issues-filters" + myFiltersHashSuffix: "issues-my-filters" + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService, - @navUrls, @events, @analytics, @translate) -> + @navUrls, @events, @analytics, @translate, @errorHandlingService, @storage, @filterRemoteStorageService) -> + bindMethods(@) + @scope.sectionName = "Issues" @scope.filters = {} + @.voting = false - if _.isEmpty(@location.search()) - filters = @rs.issues.getFilters(@params.pslug) - filters.page = 1 - @location.search(filters) - @location.replace() - return + return if @.applyStoredFilters(@params.pslug, @.filtersHashSuffix) promise = @.loadInitialData() @@ -87,18 +92,207 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @analytics.trackEvent("issue", "create", "create issue on issues list", 1) @.loadIssues() + changeQ: (q) -> + @.unselectFilter("page") + @.replaceFilter("q", q) + @.loadIssues() + @.generateFilters() + + removeFilter: (filter) -> + @.unselectFilter("page") + @.unselectFilter(filter.dataType, filter.id) + @.loadIssues() + @.generateFilters() + + addFilter: (newFilter) -> + @.unselectFilter("page") + @.selectFilter(newFilter.category.dataType, newFilter.filter.id) + @.loadIssues() + @.generateFilters() + + selectCustomFilter: (customFilter) -> + orderBy = @location.search().order_by + + if orderBy + customFilter.filter.order_by = orderBy + + @.unselectFilter("page") + @.replaceAllFilters(customFilter.filter) + @.loadIssues() + @.generateFilters() + + removeCustomFilter: (customFilter) -> + console.log "oooo" + @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) => + console.log userFilters[customFilter.id] + delete userFilters[customFilter.id] + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters) + + saveCustomFilter: (name) -> + filters = {} + urlfilters = @location.search() + filters.tags = urlfilters.tags + filters.status = urlfilters.status + filters.type = urlfilters.type + filters.severity = urlfilters.severity + filters.priority = urlfilters.priority + filters.assigned_to = urlfilters.assigned_to + filters.owner = urlfilters.owner + + @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) => + userFilters[name] = filters + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters) + + generateFilters: -> + @.storeFilters(@params.pslug, @location.search(), @.filtersHashSuffix) + + urlfilters = @location.search() + + loadFilters = {} + loadFilters.project = @scope.projectId + loadFilters.tags = urlfilters.tags + loadFilters.status = urlfilters.status + loadFilters.type = urlfilters.type + loadFilters.severity = urlfilters.severity + loadFilters.priority = urlfilters.priority + loadFilters.assigned_to = urlfilters.assigned_to + loadFilters.owner = urlfilters.owner + loadFilters.q = urlfilters.q + + return @q.all([ + @rs.issues.filtersData(loadFilters), + @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix) + ]).then (result) => + data = result[0] + customFiltersRaw = result[1] + + statuses = _.map data.statuses, (it) -> + it.id = it.id.toString() + + return it + type = _.map data.types, (it) -> + it.id = it.id.toString() + + return it + severity = _.map data.severities, (it) -> + it.id = it.id.toString() + + return it + priority = _.map data.priorities, (it) -> + it.id = it.id.toString() + + return it + tags = _.map data.tags, (it) -> + it.id = it.name + + return it + + tagsWithAtLeastOneElement = _.filter tags, (tag) -> + return tag.count > 0 + + assignedTo = _.map data.assigned_to, (it) -> + if it.id + it.id = it.id.toString() + else + it.id = "null" + + it.name = it.full_name || "Unassigned" + + return it + owner = _.map data.owners, (it) -> + it.id = it.id.toString() + it.name = it.full_name + + return it + + @.selectedFilters = [] + + if loadFilters.status + selected = @.formatSelectedFilters("status", statuses, loadFilters.status) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.tags + selected = @.formatSelectedFilters("tags", tags, loadFilters.tags) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.assigned_to + selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.owner + selected = @.formatSelectedFilters("owner", owner, loadFilters.owner) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.type + selected = @.formatSelectedFilters("type", type, loadFilters.type) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.severity + selected = @.formatSelectedFilters("severity", severity, loadFilters.severity) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.priority + selected = @.formatSelectedFilters("priority", priority, loadFilters.priority) + @.selectedFilters = @.selectedFilters.concat(selected) + + @.filterQ = loadFilters.q + + @.filters = [ + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.TYPE"), + dataType: "type", + content: type + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.SEVERITY"), + dataType: "severity", + content: severity + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.PRIORITIES"), + dataType: "priority", + content: priority + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"), + dataType: "status", + content: statuses + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"), + dataType: "tags", + content: tags, + hideEmpty: true, + totalTaggedElements: tagsWithAtLeastOneElement.length + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"), + dataType: "assigned_to", + content: assignedTo + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), + dataType: "owner", + content: owner + } + ] + + @.customFilters = [] + _.forOwn customFiltersRaw, (value, key) => + @.customFilters.push({id: key, name: key, filter: value}) + initializeSubscription: -> routingKey = "changes.project.#{@scope.projectId}.issues" @events.subscribe @scope, routingKey, (message) => @.loadIssues() - storeFilters: -> - @rs.issues.storeFilters(@params.pslug, @location.search()) loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.is_issues_activated - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -115,160 +309,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi return project - getUrlFilters: -> - filters = _.pick(@location.search(), "page", "tags", "status", "types", - "q", "severities", "priorities", - "assignedTo", "createdBy", "orderBy") - - filters.page = 1 if not filters.page - return filters - - getUrlFilter: (name) -> - filters = _.pick(@location.search(), name) - return filters[name] - - loadMyFilters: -> - return @rs.issues.getMyFilters(@scope.projectId).then (filters) => - return _.map filters, (value, key) => - return {id: key, name: key, type: "myFilters", selected: false} - - removeNotExistingFiltersFromUrl: -> - currentSearch = @location.search() - urlfilters = @.getUrlFilters() - - for filterName, filterValue of urlfilters - if filterName == "page" or filterName == "orderBy" or filterName == "q" - continue - - if filterName == "tags" - splittedValues = _.map("#{filterValue}".split(",")) - else - splittedValues = _.map("#{filterValue}".split(","), (x) -> if x == "null" then null else parseInt(x)) - - existingValues = _.intersection(splittedValues, _.map(@scope.filters[filterName], "id")) - if splittedValues.length != existingValues.length - @location.search(filterName, existingValues.join()) - - if currentSearch != @location.search() - @location.replace() - - markSelectedFilters: (filters, urlfilters) -> - # Build selected filters (from url) fast lookup data structure - searchdata = {} - for name, value of _.omit(urlfilters, "page", "orderBy") - if not searchdata[name]? - searchdata[name] = {} - - for val in "#{value}".split(",") - searchdata[name][val] = true - - isSelected = (type, id) -> - if searchdata[type]? and searchdata[type][id] - return true - return false - - for key, value of filters - for obj in value - obj.selected = if isSelected(obj.type, obj.id) then true else undefined - - loadFilters: () -> - urlfilters = @.getUrlFilters() - - if urlfilters.q - @scope.filtersQ = urlfilters.q - - # Load My Filters - promise = @.loadMyFilters().then (myFilters) => - @scope.filters.myFilters = myFilters - return myFilters - - loadFilters = {} - loadFilters.project = @scope.projectId - loadFilters.tags = urlfilters.tags - loadFilters.status = urlfilters.status - loadFilters.q = urlfilters.q - loadFilters.types = urlfilters.types - loadFilters.severities = urlfilters.severities - loadFilters.priorities = urlfilters.priorities - loadFilters.assigned_to = urlfilters.assignedTo - loadFilters.owner = urlfilters.createdBy - - # Load default filters data - promise = promise.then => - return @rs.issues.filtersData(loadFilters) - - # Format filters and set them on scope - return promise.then (data) => - usersFiltersFormat = (users, type, unknownOption) => - reformatedUsers = _.map users, (t) => - t.type = type - t.name = if t.full_name then t.full_name else unknownOption - - return t - - unknownItem = _.remove(reformatedUsers, (u) -> not u.id) - reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase()) - if unknownItem.length > 0 - reformatedUsers.unshift(unknownItem[0]) - return reformatedUsers - - choicesFiltersFormat = (choices, type, byIdObject) => - _.map choices, (t) -> - t.type = type - return t - - tagsFilterFormat = (tags) => - return _.map tags, (t) -> - t.id = t.name - t.type = 'tags' - return t - - # Build filters data structure - @scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.issueStatusById) - @scope.filters.severities = choicesFiltersFormat(data.severities, "severities", @scope.severityById) - @scope.filters.priorities = choicesFiltersFormat(data.priorities, "priorities", @scope.priorityById) - @scope.filters.assignedTo = usersFiltersFormat(data.assigned_to, "assignedTo", "Unassigned") - @scope.filters.createdBy = usersFiltersFormat(data.owners, "createdBy", "Unknown") - @scope.filters.types = choicesFiltersFormat(data.types, "types", @scope.issueTypeById) - @scope.filters.tags = tagsFilterFormat(data.tags) - - @.removeNotExistingFiltersFromUrl() - @.markSelectedFilters(@scope.filters, urlfilters) - - @rootscope.$broadcast("filters:loaded", @scope.filters) - # We need to guarantee that the last petition done here is the finally used # When searching by text loadIssues can be called fastly with different parameters and # can be resolved in a different order than generated # We count the requests made and only if the callback is for the last one data is updated loadIssuesRequests: 0 loadIssues: => - @scope.urlFilters = @.getUrlFilters() + params = @location.search() - # Convert stored filters to http parameters - # ready filters (the name difference exists - # because of some automatic lookups and is - # the simplest way todo it without adding - # additional complexity to code. - @scope.httpParams = {} - for name, values of @scope.urlFilters - if name == "severities" - name = "severity" - else if name == "orderBy" - name = "order_by" - else if name == "priorities" - name = "priority" - else if name == "assignedTo" - name = "assigned_to" - else if name == "createdBy" - name = "owner" - else if name == "status" - name = "status" - else if name == "types" - name = "type" - @scope.httpParams[name] = values - - promise = @rs.issues.list(@scope.projectId, @scope.httpParams) + promise = @rs.issues.list(@scope.projectId, params) @.loadIssuesRequests += 1 promise.index = @.loadIssuesRequests promise.then (data) => @@ -287,26 +336,10 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi return promise.then (project) => @.fillUsersAndRoles(project.members, project.roles) @.initializeSubscription() - @.loadFilters() + @.generateFilters() return @.loadIssues() - saveCurrentFiltersTo: (newFilter) -> - deferred = @q.defer() - @rs.issues.getMyFilters(@scope.projectId).then (filters) => - filters[newFilter] = @location.search() - @rs.issues.storeMyFilters(@scope.projectId, filters).then => - deferred.resolve() - return deferred.promise - - deleteMyFilter: (filter) -> - deferred = @q.defer() - @rs.issues.getMyFilters(@scope.projectId).then (filters) => - delete filters[filter] - @rs.issues.storeMyFilters(@scope.projectId, filters).then => - deferred.resolve() - return deferred.promise - # Functions used from templates addNewIssue: -> @rootscope.$broadcast("issueform:new", @scope.project) @@ -314,6 +347,33 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi addIssuesInBulk: -> @rootscope.$broadcast("issueform:bulk", @scope.projectId) + upVoteIssue: (issueId) -> + @.voting = issueId + onSuccess = => + @.loadIssues() + @.voting = null + onError = => + @confirm.notify("error") + @.voting = null + + return @rs.issues.upvote(issueId).then(onSuccess, onError) + + downVoteIssue: (issueId) -> + @.voting = issueId + onSuccess = => + @.loadIssues() + @.voting = null + onError = => + @confirm.notify("error") + @.voting = null + + return @rs.issues.downvote(issueId).then(onSuccess, onError) + + getOrderBy: -> + if _.isString(@location.search().order_by) + return @location.search().order_by + else + return "created_date" module.controller("IssuesController", IssuesController) @@ -408,28 +468,40 @@ IssuesDirective = ($log, $location, $template, $compile) -> ## Issues Filters linkOrdering = ($scope, $el, $attrs, $ctrl) -> # Draw the arrow the first time - currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date" + + currentOrder = $ctrl.getOrderBy() + if currentOrder - icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom" + icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-down" colHeadElement = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']") - colHeadElement.html("#{colHeadElement.html()}") + + svg = $("").attr("svg-icon", icon) + + colHeadElement.append(svg) + $compile(colHeadElement.contents())($scope) $el.on "click", ".row.title > div", (event) -> target = angular.element(event.currentTarget) - currentOrder = $ctrl.getUrlFilter("orderBy") + currentOrder = $ctrl.getOrderBy() newOrder = target.data("fieldname") finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder $scope.$apply -> - $ctrl.replaceFilter("orderBy", finalOrder) - $ctrl.storeFilters() + $ctrl.replaceFilter("order_by", finalOrder) + + $ctrl.storeFilters($ctrl.params.pslug, $location.search(), $ctrl.filtersHashSuffix) $ctrl.loadIssues().then -> # Update the arrow - $el.find(".row.title > div > span.icon").remove() - icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom" - target.html("#{target.html()}") + $el.find(".row.title > div > tg-svg").remove() + icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-down" + + svg = $("") + .attr("svg-icon", icon) + + target.append(svg) + $compile(target.contents())($scope) ## Issues Link link = ($scope, $el, $attrs) -> @@ -445,253 +517,6 @@ IssuesDirective = ($log, $location, $template, $compile) -> module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", IssuesDirective]) -############################################################################# -## Issues Filters Directive -############################################################################# - -IssuesFiltersDirective = ($q, $log, $location, $rs, $confirm, $loading, $template, $translate, $compile, $auth) -> - template = $template.get("issue/issues-filters.html", true) - templateSelected = $template.get("issue/issues-filters-selected.html", true) - - link = ($scope, $el, $attrs) -> - $ctrl = $el.closest(".wrapper").controller() - - selectedFilters = [] - - showFilters = (title, type) -> - $el.find(".filters-cats").hide() - $el.find(".filter-list").removeClass("hidden") - $el.find(".breadcrumb").removeClass("hidden") - $el.find("h2 .subfilter .title").html(title) - $el.find("h2 .subfilter .title").prop("data-type", type) - - showCategories = -> - $el.find(".filters-cats").show() - $el.find(".filter-list").addClass("hidden") - $el.find(".breadcrumb").addClass("hidden") - - initializeSelectedFilters = (filters) -> - selectedFilters = [] - for name, values of filters - for val in values - selectedFilters.push(val) if val.selected - - renderSelectedFilters(selectedFilters) - - renderSelectedFilters = (selectedFilters) -> - _.filter selectedFilters, (f) => - if f.color - f.style = "border-left: 3px solid #{f.color}" - - html = templateSelected({filters:selectedFilters}) - html = $compile(html)($scope) - $el.find(".filters-applied").html(html) - - if $auth.isAuthenticated() && selectedFilters.length > 0 - $el.find(".save-filters").show() - else - $el.find(".save-filters").hide() - - renderFilters = (filters) -> - _.filter filters, (f) => - if f.color - f.style = "border-left: 3px solid #{f.color}" - - html = template({filters:filters}) - html = $compile(html)($scope) - $el.find(".filter-list").html(html) - - getFiltersType = () -> - return $el.find(".subfilter .title").prop('data-type') - - reloadIssues = () -> - currentFiltersType = getFiltersType() - - $q.all([$ctrl.loadIssues(), $ctrl.loadFilters()]).then () -> - filters = $scope.filters[currentFiltersType] - renderFilters(_.reject(filters, "selected")) - - toggleFilterSelection = (type, id) -> - if type == "myFilters" - $rs.issues.getMyFilters($scope.projectId).then (data) -> - myFilters = data - filters = myFilters[id] - filters.page = 1 - $ctrl.replaceAllFilters(filters) - $ctrl.storeFilters() - $ctrl.loadIssues() - $ctrl.markSelectedFilters($scope.filters, filters) - initializeSelectedFilters($scope.filters) - return null - - filters = $scope.filters[type] - filterId = if type == 'tags' then taiga.toString(id) else id - filter = _.find(filters, {id: filterId}) - filter.selected = (not filter.selected) - - # Convert id to null as string for properly - # put null value on url parameters - id = "null" if id is null - - if filter.selected - selectedFilters.push(filter) - $ctrl.selectFilter(type, id) - $ctrl.selectFilter("page", 1) - $ctrl.storeFilters() - else - selectedFilters = _.reject selectedFilters, (f) -> - return f.id == filter.id && f.type == filter.type - - $ctrl.unselectFilter(type, id) - $ctrl.selectFilter("page", 1) - $ctrl.storeFilters() - - reloadIssues() - - renderSelectedFilters(selectedFilters) - - currentFiltersType = getFiltersType() - - if type == currentFiltersType - renderFilters(_.reject(filters, "selected")) - - # Angular Watchers - $scope.$on "filters:loaded", (ctx, filters) -> - initializeSelectedFilters(filters) - - $scope.$on "filters:issueupdate", (ctx, filters) -> - html = template({filters:filters.status}) - html = $compile(html)($scope) - $el.find(".filter-list").html(html) - - selectQFilter = debounceLeading 100, (value, oldValue) -> - return if value is undefined or value == oldValue - - $ctrl.replaceFilter("page", null, true) - - if value.length == 0 - $ctrl.replaceFilter("q", null) - $ctrl.storeFilters() - else - $ctrl.replaceFilter("q", value) - $ctrl.storeFilters() - - reloadIssues() - - unwatchIssues = $scope.$watch "issues", (newValue) -> - if !_.isUndefined(newValue) - $scope.$watch("filtersQ", selectQFilter) - unwatchIssues() - - # Dom Event Handlers - $el.on "click", ".filters-cat-single", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - tags = $scope.filters[target.data("type")] - renderFilters(_.reject(tags, "selected")) - showFilters(target.attr("title"), target.data("type")) - - $el.on "click", ".back", (event) -> - event.preventDefault() - showCategories($el) - - $el.on "click", ".filters-applied .remove-filter", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget).parent() - - id = target.data("id") or null - type = target.data("type") - toggleFilterSelection(type, id) - - $el.on "click", ".filter-list .single-filter", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - target.toggleClass("active") - - id = target.data("id") or null - type = target.data("type") - - # A saved filter can't be active - if type == "myFilters" - target.removeClass("active") - - toggleFilterSelection(type, id) - - $el.on "click", ".filter-list .remove-filter", (event) -> - event.preventDefault() - event.stopPropagation() - - target = angular.element(event.currentTarget) - customFilterName = target.parent().data('id') - title = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.TITLE") - message = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.MESSAGE", {customFilterName: customFilterName}) - - $confirm.askOnDelete(title, message).then (askResponse) -> - promise = $ctrl.deleteMyFilter(customFilterName) - promise.then -> - promise = $ctrl.loadMyFilters() - promise.then (filters) -> - askResponse.finish() - $scope.filters.myFilters = filters - renderFilters($scope.filters.myFilters) - promise.then null, -> - askResponse.finish() - promise.then null, -> - askResponse.finish(false) - $confirm.notify("error") - - - $el.on "click", ".save-filters", (event) -> - event.preventDefault() - renderFilters($scope.filters["myFilters"]) - showFilters("My filters", "myFilters") - $el.find('.save-filters').hide() - $el.find('.my-filter-name').removeClass("hidden") - $el.find('.my-filter-name').focus() - $scope.$apply() - - $el.on "keyup", ".my-filter-name", (event) -> - event.preventDefault() - if event.keyCode == 13 - target = angular.element(event.currentTarget) - newFilter = target.val() - currentLoading = $loading() - .target($el.find(".new")) - .start() - promise = $ctrl.saveCurrentFiltersTo(newFilter) - promise.then -> - loadPromise = $ctrl.loadMyFilters() - loadPromise.then (filters) -> - currentLoading.finish() - $scope.filters.myFilters = filters - - currentfilterstype = $el.find("h2 .subfilter .title").prop('data-type') - if currentfilterstype == "myFilters" - renderFilters($scope.filters.myFilters) - - $el.find('.my-filter-name').addClass("hidden") - $el.find('.save-filters').show() - - loadPromise.then null, -> - currentLoading.finish() - $confirm.notify("error", "Error loading custom filters") - - promise.then null, -> - currentLoading.finish() - $el.find(".my-filter-name").val(newFilter).focus().select() - $confirm.notify("error", "Filter not saved") - - else if event.keyCode == 27 - $el.find('.my-filter-name').val('') - $el.find('.my-filter-name').addClass("hidden") - $el.find('.save-filters').show() - - return {link:link} - -module.directive("tgIssuesFilters", ["$q", "$log", "$tgLocation", "$tgResources", "$tgConfirm", "$tgLoading", - "$tgTemplate", "$translate", "$compile", "$tgAuth", IssuesFiltersDirective]) - - ############################################################################# ## Issue status Directive (popover for change status) ############################################################################# @@ -778,9 +603,9 @@ module.directive("tgIssueStatusInlineEdition", ["$tgRepo", "$tgTemplate", "$root ## Issue assigned to Directive ############################################################################# -IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> +IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate, avatarService) -> template = _.template(""" - <%- name %> + <%- name %>
<%- name %>
""") @@ -792,9 +617,14 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> } member = $scope.usersById[issue.assigned_to] + + avatar = avatarService.getAvatar(member) + ctx.imgurl = avatar.url + ctx.bg = null + if member ctx.name = member.full_name_display - ctx.imgurl = member.photo + ctx.bg = avatar.bg $el.find(".avatar").html(template(ctx)) $el.find(".issue-assignedto").attr('title', ctx.name) @@ -826,5 +656,5 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> return {link: link} -module.directive("tgIssueAssignedToInlineEdition", ["$tgRepo", "$rootScope", "$translate" +module.directive("tgIssueAssignedToInlineEdition", ["$tgRepo", "$rootScope", "$translate", "tgAvatarService", IssueAssignedToInlineEditionDirective]) diff --git a/app/coffee/modules/kanban/kanban-usertories.coffee b/app/coffee/modules/kanban/kanban-usertories.coffee new file mode 100644 index 00000000..dc1faf82 --- /dev/null +++ b/app/coffee/modules/kanban/kanban-usertories.coffee @@ -0,0 +1,188 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: kanban-userstories.service.coffee +### + +groupBy = @.taiga.groupBy + +class KanbanUserstoriesService extends taiga.Service + @.$inject = [] + + constructor: () -> + @.reset() + + reset: () -> + @.userstoriesRaw = [] + @.archivedStatus = [] + @.statusHide = [] + @.foldStatusChanged = {} + @.usByStatus = Immutable.Map() + + init: (project, usersById) -> + @.project = project + @.usersById = usersById + + resetFolds: () -> + @.foldStatusChanged = {} + @.refresh() + + toggleFold: (usId) -> + @.foldStatusChanged[usId] = !@.foldStatusChanged[usId] + @.refresh() + + set: (userstories) -> + @.userstoriesRaw = userstories + @.refreshRawOrder() + @.refresh() + + add: (us) -> + @.userstoriesRaw = @.userstoriesRaw.concat(us) + @.refreshRawOrder() + @.refresh() + + addArchivedStatus: (statusId) -> + @.archivedStatus.push(statusId) + + isUsInArchivedHiddenStatus: (usId) -> + us = @.getUsModel(usId) + + return @.archivedStatus.indexOf(us.status) != -1 && + @.statusHide.indexOf(us.status) != -1 + + hideStatus: (statusId) -> + @.deleteStatus(statusId) + @.statusHide.push(statusId) + + showStatus: (statusId) -> + _.remove @.statusHide, (it) -> return it == statusId + + getStatus: (statusId) -> + return _.filter @.userstoriesRaw, (us) -> return us.status == statusId + + deleteStatus: (statusId) -> + toDelete = _.filter @.userstoriesRaw, (us) -> return us.status == statusId + toDelete = _.map (it) -> return it.id + + @.archived = _.difference(@.archived, toDelete) + + @.userstoriesRaw = _.filter @.userstoriesRaw, (us) -> return us.status != statusId + + @.refresh() + + refreshRawOrder: () -> + @.order = {} + + @.order[it.id] = it.kanban_order for it in @.userstoriesRaw + + assignOrders: (order) -> + order = _.invert(order) + @.order = _.assign(@.order, order) + + @.refresh() + + move: (id, statusId, index) -> + us = @.getUsModel(id) + + usByStatus = _.filter @.userstoriesRaw, (it) => + return it.status == statusId + + usByStatus = _.sortBy usByStatus, (it) => @.order[it.id] + + usByStatusWithoutMoved = _.filter usByStatus, (it) => it.id != id + beforeDestination = _.slice(usByStatusWithoutMoved, 0, index) + afterDestination = _.slice(usByStatusWithoutMoved, index) + + setOrders = {} + + previous = beforeDestination[beforeDestination.length - 1] + + previousWithTheSameOrder = _.filter beforeDestination, (it) => + @.order[it.id] == @.order[previous.id] + + if previousWithTheSameOrder.length > 1 + for it in previousWithTheSameOrder + setOrders[it.id] = @.order[it.id] + + if !previous + @.order[us.id] = 0 + else if previous + @.order[us.id] = @.order[previous.id] + 1 + + for it, key in afterDestination + @.order[it.id] = @.order[us.id] + key + 1 + + us.status = statusId + us.kanban_order = @.order[us.id] + + @.refresh() + + return {"us_id": us.id, "order": @.order[us.id], "set_orders": setOrders} + + replace: (us) -> + @.usByStatus = @.usByStatus.map (status) -> + findedIndex = status.findIndex (usItem) -> + return usItem.get('id') == us.get('id') + + if findedIndex != -1 + status = status.set(findedIndex, us) + + return status + + replaceModel: (us) -> + @.userstoriesRaw = _.map @.userstoriesRaw, (usItem) -> + if us.id == usItem.id + return us + else + return usItem + + @.refresh() + + getUs: (id) -> + findedUs = null + + @.usByStatus.forEach (status) -> + findedUs = status.find (us) -> return us.get('id') == id + + return false if findedUs + + return findedUs + + getUsModel: (id) -> + return _.find @.userstoriesRaw, (us) -> return us.id == id + + refresh: -> + @.userstoriesRaw = _.sortBy @.userstoriesRaw, (it) => @.order[it.id] + + userstories = @.userstoriesRaw + userstories = _.map userstories, (usModel) => + us = {} + us.foldStatusChanged = @.foldStatusChanged[usModel.id] + us.model = usModel.getAttrs() + us.images = _.filter usModel.attachments, (it) -> return !!it.thumbnail_card_url + us.id = usModel.id + us.assigned_to = @.usersById[usModel.assigned_to] + us.colorized_tags = _.map us.model.tags, (tag) => + return {name: tag[0], color: tag[1]} + + return us + + usByStatus = _.groupBy userstories, (us) -> + return us.model.status + + @.usByStatus = Immutable.fromJS(usByStatus) + +angular.module("taigaKanban").service("tgKanbanUserstories", KanbanUserstoriesService) diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index c23f4f28..643472f5 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -34,26 +34,18 @@ bindMethods = @.taiga.bindMethods module = angular.module("taigaKanban") -# Vars - -defaultViewMode = "maximized" -viewModes = [ - "maximized", - "minimized" -] - - ############################################################################# ## Kanban Controller ############################################################################# -class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) +class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin, taiga.UsFiltersMixin) @.$inject = [ "$scope", "$rootScope", "$tgRepo", "$tgConfirm", "$tgResources", + "tgResources", "$routeParams", "$q", "$tgLocation", @@ -61,16 +53,27 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$tgNavUrls", "$tgEvents", "$tgAnalytics", - "$translate" + "$translate", + "tgErrorHandlingService", + "$tgModel", + "tgKanbanUserstories", + "$tgStorage", + "tgFilterRemoteStorageService" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @appMetaService, @navUrls, @events, @analytics, @translate) -> + storeCustomFiltersName: 'kanban-custom-filters' + storeFiltersName: 'kanban-filters' + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location, + @appMetaService, @navUrls, @events, @analytics, @translate, @errorHandlingService, + @model, @kanbanUserstoriesService, @storage, @filterRemoteStorageService) -> bindMethods(@) + @kanbanUserstoriesService.reset() + @.openFilter = false + + return if @.applyStoredFilters(@params.pslug, "kanban-filters") @scope.sectionName = @translate.instant("KANBAN.SECTION_NAME") - @scope.statusViewModes = {} @.initializeEventHandlers() promise = @.loadInitialData() @@ -87,80 +90,109 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi # On Error promise.then null, @.onInitialDataError.bind(@) + taiga.defineImmutableProperty @.scope, "usByStatus", () => + return @kanbanUserstoriesService.usByStatus + + setZoom: (zoomLevel, zoom) -> + if @.zoomLevel != zoomLevel + @kanbanUserstoriesService.resetFolds() + + @.zoomLevel = zoomLevel + @.zoom = zoom + + filtersReloadContent: () -> + @.loadUserstories().then () => + openArchived = _.difference(@kanbanUserstoriesService.archivedStatus, + @kanbanUserstoriesService.statusHide) + if openArchived.length + for statusId in openArchived + @.loadUserStoriesForStatus({}, statusId) + initializeEventHandlers: -> - @scope.$on "usform:new:success", => - @.loadUserstories() - @.refreshTagsColors() + @scope.$on "usform:new:success", (event, us) => + @.refreshTagsColors().then () => + @kanbanUserstoriesService.add(us) + @analytics.trackEvent("userstory", "create", "create userstory on kanban", 1) - @scope.$on "usform:bulk:success", => - @.loadUserstories() + @scope.$on "usform:bulk:success", (event, uss) => + @.refreshTagsColors().then () => + @kanbanUserstoriesService.add(uss) + @analytics.trackEvent("userstory", "create", "bulk create userstory on kanban", 1) - @scope.$on "usform:edit:success", => - @.loadUserstories() - @.refreshTagsColors() + @scope.$on "usform:edit:success", (event, us) => + @.refreshTagsColors().then () => + @kanbanUserstoriesService.replaceModel(us) @scope.$on("assigned-to:added", @.onAssignedToChanged) @scope.$on("kanban:us:move", @.moveUs) @scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus) @scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus) - # Template actions - addNewUs: (type, statusId) -> switch type - when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, statusId, @scope.usStatusList) - when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, statusId) + when "standard" then @rootscope.$broadcast("usform:new", + @scope.projectId, statusId, @scope.usStatusList) + when "bulk" then @rootscope.$broadcast("usform:bulk", + @scope.projectId, statusId) + + editUs: (id) -> + us = @kanbanUserstoriesService.getUs(id) + us = us.set('loading', true) + @kanbanUserstoriesService.replace(us) + + @rs.userstories.getByRef(us.getIn(['model', 'project']), us.getIn(['model', 'ref'])) + .then (editingUserStory) => + @rs2.attachments.list("us", us.get('id'), us.getIn(['model', 'project'])).then (attachments) => + @rootscope.$broadcast("usform:edit", editingUserStory, attachments.toJS()) + + us = us.set('loading', false) + @kanbanUserstoriesService.replace(us) + + showPlaceHolder: (statusId) -> + if @scope.usStatusList[0].id == statusId && + !@kanbanUserstoriesService.userstoriesRaw.length + return true + + return false + + toggleFold: (id) -> + @kanbanUserstoriesService.toggleFold(id) + + isUsInArchivedHiddenStatus: (usId) -> + return @kanbanUserstoriesService.isUsInArchivedHiddenStatus(usId) + + changeUsAssignedTo: (id) -> + us = @kanbanUserstoriesService.getUsModel(id) - changeUsAssignedTo: (us) -> @rootscope.$broadcast("assigned-to:add", us) - # Scope Events Handlers + onAssignedToChanged: (ctx, userid, usModel) -> + usModel.assigned_to = userid - onAssignedToChanged: (ctx, userid, us) -> - us.assigned_to = userid + @kanbanUserstoriesService.replaceModel(usModel) - promise = @repo.save(us) + promise = @repo.save(usModel) promise.then null, -> console.log "FAIL" # TODO - # Load data methods refreshTagsColors: -> return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => - @scope.project.tags_colors = tags_colors + @scope.project.tags_colors = tags_colors._attrs loadUserstories: -> params = { - status__is_archived: false + status__is_archived: false, + include_attachments: true, + include_tasks: true } + params = _.merge params, @location.search() + promise = @rs.userstories.listAll(@scope.projectId, params).then (userstories) => - @scope.userstories = userstories - - usByStatus = _.groupBy(userstories, "status") - us_archived = [] - for status in @scope.usStatusList - if not usByStatus[status.id]? - usByStatus[status.id] = [] - if @scope.usByStatus? - for us in @scope.usByStatus[status.id] - if us.status != status.id - us_archived.push(us) - - # Must preserve the archived columns if loaded - if status.is_archived and @scope.usByStatus? and @scope.usByStatus[status.id].length != 0 - for us in @scope.usByStatus[status.id].concat(us_archived) - if us.status == status.id - usByStatus[status.id].push(us) - - usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order") - - if userstories.length == 0 - status = @scope.usStatusList[0] - usByStatus[status.id].push({isPlaceholder: true}) - - @scope.usByStatus = usByStatus + @kanbanUserstoriesService.init(@scope.project, @scope.usersById) + @kanbanUserstoriesService.set(userstories) # The broadcast must be executed when the DOM has been fully reloaded. # We can't assure when this exactly happens so we need a defer @@ -174,14 +206,28 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi return promise loadUserStoriesForStatus: (ctx, statusId) -> - params = { status: statusId } + filteredStatus = @location.search().status + + # if there are filters applied the action doesn't end if the statusId is not in the url + if filteredStatus + filteredStatus = filteredStatus.split(",").map (it) -> parseInt(it, 10) + + return if filteredStatus.indexOf(statusId) == -1 + + params = { + status: statusId + include_attachments: true, + include_tasks: true + } + + params = _.merge params, @location.search() + return @rs.userstories.listAll(@scope.projectId, params).then (userstories) => - @scope.usByStatus[statusId] = _.sortBy(userstories, "kanban_order") @scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories) + return userstories hideUserStoriesForStatus: (ctx, statusId) -> - @scope.usByStatus[statusId] = [] @scope.$broadcast("kanban:hidden-userstories-for-status", statusId) loadKanban: -> @@ -193,7 +239,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.is_kanban_activated - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -203,8 +249,6 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id) @scope.usStatusList = _.sortBy(project.us_statuses, "order") - @.generateStatusViewModes() - @scope.$emit("project:loaded", project) return project @@ -219,82 +263,40 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @.fillUsersAndRoles(project.members, project.roles) @.initializeSubscription() @.loadKanban() - - - ## View Mode methods - - generateStatusViewModes: -> - storedStatusViewModes = @rs.kanban.getStatusViewModes(@scope.projectId) - - @scope.statusViewModes = {} - for status in @scope.usStatusList - mode = storedStatusViewModes[status.id] || defaultViewMode - - @scope.statusViewModes[status.id] = mode - - @.storeStatusViewModes() - - storeStatusViewModes: -> - @rs.kanban.storeStatusViewModes(@scope.projectId, @scope.statusViewModes) - - updateStatusViewMode: (statusId, newViewMode) -> - @scope.statusViewModes[statusId] = newViewMode - @.storeStatusViewModes() - - isMaximized: (statusId) -> - mode = @scope.statusViewModes[statusId] or defaultViewMode - return mode == 'maximized' - - isMinimized: (statusId) -> - mode = @scope.statusViewModes[statusId] or defaultViewMode - return mode == 'minimized' + @.generateFilters() # Utils methods prepareBulkUpdateData: (uses, field="kanban_order") -> return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) - resortUserStories: (uses) -> - items = [] - for item, index in uses - item.kanban_order = index - if item.isModified() - items.push(item) - - return items - moveUs: (ctx, us, oldStatusId, newStatusId, index) -> - if oldStatusId != newStatusId - # Remove us from old status column - r = @scope.usByStatus[oldStatusId].indexOf(us) - @scope.usByStatus[oldStatusId].splice(r, 1) + us = @kanbanUserstoriesService.getUsModel(us.get('id')) - # Add us to new status column. - @scope.usByStatus[newStatusId].splice(index, 0, us) - us.status = newStatusId - else - r = @scope.usByStatus[newStatusId].indexOf(us) - @scope.usByStatus[newStatusId].splice(r, 1) - @scope.usByStatus[newStatusId].splice(index, 0, us) + moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index) - itemsToSave = @.resortUserStories(@scope.usByStatus[newStatusId]) - @scope.usByStatus[newStatusId] = _.sortBy(@scope.usByStatus[newStatusId], "kanban_order") + params = { + include_attachments: true, + include_tasks: true + } - # Persist the userstory - promise = @repo.save(us) + options = { + headers: { + "set-orders": JSON.stringify(moveUpdateData.set_orders) + } + } - # Rehash userstories order field - # and persist in bulk all changes. - promise = promise.then => - itemsToSave = _.reject(itemsToSave, {"id": us.id}) - data = @.prepareBulkUpdateData(itemsToSave) + promise = @repo.save(us, true, params, options, true) - return @rs.userstories.bulkUpdateKanbanOrder(us.project, data).then => - return itemsToSave + promise = promise.then (result) => + headers = result[1] + + if headers && headers['taiga-info-order-updated'] + order = JSON.parse(headers['taiga-info-order-updated']) + @kanbanUserstoriesService.assignOrders(order) return promise - module.controller("KanbanController", KanbanController) ############################################################################# @@ -321,7 +323,7 @@ module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective]) ## Kanban Archived Status Column Header Control ############################################################################# -KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) -> +KanbanArchivedStatusHeaderDirective = ($rootscope, $translate, kanbanUserstoriesService) -> showArchivedText = $translate.instant("KANBAN.ACTION_SHOW_ARCHIVED") hideArchivedText = $translate.instant("KANBAN.ACTION_HIDE_ARCHIVED") @@ -329,6 +331,9 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) -> status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader) hidden = true + kanbanUserstoriesService.addArchivedStatus(status.id) + kanbanUserstoriesService.hideStatus(status.id) + $scope.class = "icon-watch" $scope.title = showArchivedText @@ -341,24 +346,27 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) -> $scope.title = showArchivedText $rootscope.$broadcast("kanban:hide-userstories-for-status", status.id) + kanbanUserstoriesService.hideStatus(status.id) else $scope.class = "icon-unwatch" $scope.title = hideArchivedText $rootscope.$broadcast("kanban:show-userstories-for-status", status.id) + kanbanUserstoriesService.showStatus(status.id) + $scope.$on "$destroy", -> $el.off() return {link:link} -module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", KanbanArchivedStatusHeaderDirective]) +module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", "$translate", "tgKanbanUserstories", KanbanArchivedStatusHeaderDirective]) ############################################################################# ## Kanban Archived Status Column Intro Directive ############################################################################# -KanbanArchivedStatusIntroDirective = ($translate) -> +KanbanArchivedStatusIntroDirective = ($translate, kanbanUserstoriesService) -> userStories = [] link = ($scope, $el, $attrs) -> @@ -366,105 +374,40 @@ KanbanArchivedStatusIntroDirective = ($translate) -> status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro) $el.text(hiddenUserStoriexText) - updateIntroText = -> - if userStories.length > 0 + updateIntroText = (hasArchived) -> + if hasArchived $el.text("") else $el.text(hiddenUserStoriexText) $scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) -> - # The destination columnd is this one - if status.id == newStatusId - # Reorder - if status.id == oldStatusId - r = userStories.indexOf(itemUs) - userStories.splice(r, 1) - userStories.splice(itemIndex, 0, itemUs) - - # Archiving user story - else - itemUs.isArchived = true - userStories.splice(itemIndex, 0, itemUs) - - # Unarchiving user story - else if status.id == oldStatusId - itemUs.isArchived = false - r = userStories.indexOf(itemUs) - userStories.splice(r, 1) - - updateIntroText() + hasArchived = !!kanbanUserstoriesService.getStatus(newStatusId).length + updateIntroText(hasArchived) $scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) -> if statusId == status.id - userStories = _.filter(userStoriesLoaded, (us) -> us.status == status.id) - updateIntroText() + kanbanUserstoriesService.deleteStatus(statusId) + kanbanUserstoriesService.add(userStoriesLoaded) + + hasArchived = !!kanbanUserstoriesService.getStatus(statusId).length + updateIntroText(hasArchived) $scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) -> if statusId == status.id - userStories = [] - updateIntroText() + updateIntroText(false) $scope.$on "$destroy", -> $el.off() return {link:link} -module.directive("tgKanbanArchivedStatusIntro", ["$translate", KanbanArchivedStatusIntroDirective]) - - -############################################################################# -## Kanban User Story Directive -############################################################################# - -KanbanUserstoryDirective = ($rootscope, $loading, $rs, $rs2) -> - link = ($scope, $el, $attrs, $model) -> - $scope.$watch "us", (us) -> - if us.is_blocked and not $el.hasClass("blocked") - $el.addClass("blocked") - else if not us.is_blocked and $el.hasClass("blocked") - $el.removeClass("blocked") - - $el.on 'click', '.edit-us', (event) -> - if $el.find(".icon-edit").hasClass("noclick") - return - - target = $(event.target) - - currentLoading = $loading() - .target(target) - .timeout(200) - .removeClasses("icon-edit") - .start() - - us = $model.$modelValue - $rs.userstories.getByRef(us.project, us.ref).then (editingUserStory) => - $rs2.attachments.list("us", us.id, us.project).then (attachments) => - $rootscope.$broadcast("usform:edit", editingUserStory, attachments.toJS()) - currentLoading.finish() - - $scope.getTemplateUrl = () -> - if $scope.us.isPlaceholder - return "common/components/kanban-placeholder.html" - else - return "kanban/kanban-task.html" - - $scope.$on "$destroy", -> - $el.off() - - return { - template: '', - link: link - require: "ngModel" - } - -module.directive("tgKanbanUserstory", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", KanbanUserstoryDirective]) +module.directive("tgKanbanArchivedStatusIntro", ["$translate", "tgKanbanUserstories", KanbanArchivedStatusIntroDirective]) ############################################################################# ## Kanban Squish Column Directive ############################################################################# KanbanSquishColumnDirective = (rs) -> - link = ($scope, $el, $attrs) -> $scope.$on "project:loaded", (event, project) -> $scope.folds = rs.kanban.getStatusColumnModes(project.id) @@ -484,6 +427,7 @@ KanbanSquishColumnDirective = (rs) -> return 310 totalWidth = _.reduce columnWidths, (total, width) -> return total + width + $el.find('.kanban-table-inner').css("width", totalWidth) return {link: link} @@ -501,7 +445,7 @@ KanbanWipLimitDirective = -> redrawWipLimit = => $el.find(".kanban-wip-limit").remove() timeout 200, => - element = $el.find(".kanban-task")[status.wip_limit] + element = $el.find("tg-card")[status.wip_limit] if element angular.element(element).before("
") @@ -517,79 +461,3 @@ KanbanWipLimitDirective = -> return {link: link} module.directive("tgKanbanWipLimit", KanbanWipLimitDirective) - - -############################################################################# -## Kanban User Directive -############################################################################# - -KanbanUserDirective = ($log, $compile, $translate) -> - template = _.template(""" -
- class="not-clickable"<% } %>> - <%- name %> - -
- """) - - clickable = false - - link = ($scope, $el, $attrs, $model) -> - username_label = $el.parent().find("a.task-assigned") - username_label.addClass("not-clickable") - - if not $attrs.tgKanbanUserAvatar - return $log.error "KanbanUserDirective: no attr is defined" - - wtid = $scope.$watch $attrs.tgKanbanUserAvatar, (v) -> - if not $scope.usersById? - $log.error "KanbanUserDirective requires userById set in scope." - wtid() - else - user = $scope.usersById[v] - render(user) - - render = (user) -> - if user is undefined - ctx = { - name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"), - imgurl: "/#{window._version}/images/unnamed.png", - clickable: clickable - } - else - ctx = { - name: user.full_name_display, - imgurl: user.photo, - clickable: clickable - } - - html = $compile(template(ctx))($scope) - $el.html(html) - username_label.text(ctx.name) - - bindOnce $scope, "project", (project) -> - if project.my_permissions.indexOf("modify_us") > -1 - clickable = true - $el.on "click", (event) => - if $el.find("a").hasClass("noclick") - return - - us = $model.$modelValue - $ctrl = $el.controller() - $ctrl.changeUsAssignedTo(us) - - username_label.removeClass("not-clickable") - username_label.on "click", (event) -> - if $el.find("a").hasClass("noclick") - return - - us = $model.$modelValue - $ctrl = $el.controller() - $ctrl.changeUsAssignedTo(us) - - $scope.$on "$destroy", -> - $el.off() - - return {link: link, require:"ngModel"} - -module.directive("tgKanbanUserAvatar", ["$log", "$compile", "$translate", KanbanUserDirective]) diff --git a/app/coffee/modules/kanban/sortable.coffee b/app/coffee/modules/kanban/sortable.coffee index 41cbb23d..fda8e063 100644 --- a/app/coffee/modules/kanban/sortable.coffee +++ b/app/coffee/modules/kanban/sortable.coffee @@ -40,8 +40,12 @@ module = angular.module("taigaKanban") KanbanSortableDirective = ($repo, $rs, $rootscope) -> link = ($scope, $el, $attrs) -> - bindOnce $scope, "project", (project) -> - if not (project.my_permissions.indexOf("modify_us") > -1) + unwatch = $scope.$watch "usByStatus", (usByStatus) -> + return if !usByStatus || !usByStatus.size + + unwatch() + + if not ($scope.project.my_permissions.indexOf("modify_us") > -1) return oldParentScope = null @@ -63,7 +67,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) -> copy: false, mirrorContainer: tdom[0], moves: (item) -> - return $(item).hasClass('kanban-task') + return $(item).is('tg-card') }) drake.on 'drag', (item) -> @@ -83,14 +87,14 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) -> deleteElement(itemEl) $scope.$apply -> - $rootscope.$broadcast("kanban:us:move", itemUs, itemUs.status, newStatusId, itemIndex) + $rootscope.$broadcast("kanban:us:move", itemUs, itemUs.getIn(['model', 'status']), newStatusId, itemIndex) scroll = autoScroll(containers, { - margin: 20, + margin: 100, pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) $scope.$on "$destroy", -> diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee index 49e4d103..d8eede4e 100644 --- a/app/coffee/modules/related-tasks.coffee +++ b/app/coffee/modules/related-tasks.coffee @@ -168,6 +168,8 @@ RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $scope.newTask = $tgmodel.make_model("tasks", newTask) render = -> + return if $scope.openNewRelatedTask + $scope.openNewRelatedTask = true $el.on "keyup", "input", (event)-> @@ -229,7 +231,7 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) -> link = ($scope, $el, $attrs) -> loadTasks = -> return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) => - $scope.tasks = _.sortBy(tasks, 'ref') + $scope.tasks = _.sortBy(tasks, (x) => [x.us_order, x.ref]) return tasks _isVisible = -> @@ -267,9 +269,9 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) -> module.directive("tgRelatedTasks", ["$tgRepo", "$tgResources", "$rootScope", RelatedTasksDirective]) -RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> +RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, $translate, avatarService) -> template = _.template(""" - <%- name %> + <%- name %>
<%- name %>
""") @@ -277,11 +279,15 @@ RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> updateRelatedTask = (task) -> ctx = { name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"), - imgurl: "/" + window._version + "/images/unnamed.png" } + member = $scope.usersById[task.assigned_to] + + avatar = avatarService.getAvatar(member) + ctx.imgurl = avatar.url + ctx.bg = avatar.bg + if member - ctx.imgurl = member.photo ctx.name = member.full_name_display $el.find(".avatar").html(template(ctx)) @@ -320,5 +326,5 @@ RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, $translate) -> return {link: link} -module.directive("tgRelatedTaskAssignedToInlineEdition", ["$tgRepo", "$rootScope", "$translate", +module.directive("tgRelatedTaskAssignedToInlineEdition", ["$tgRepo", "$rootScope", "$translate", "tgAvatarService", RelatedTaskAssignedToInlineEditionDirective]) diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index c32a9f56..20b9fa9a 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -81,6 +81,7 @@ urls = { "project-transfer-start": "/projects/%s/transfer_start" # Project Values - Choises + "epic-statuses": "/epic-statuses" "userstory-statuses": "/userstory-statuses" "points": "/points" "task-statuses": "/task-statuses" @@ -92,11 +93,21 @@ urls = { # Milestones/Sprints "milestones": "/milestones" + # Epics + "epics": "/epics" + "epic-upvote": "/epics/%s/upvote" + "epic-downvote": "/epics/%s/downvote" + "epic-watch": "/epics/%s/watch" + "epic-unwatch": "/epics/%s/unwatch" + "epic-related-userstories": "/epics/%s/related_userstories" + "epic-related-userstories-bulk-create": "/epics/%s/related_userstories/bulk_create" + # User stories "userstories": "/userstories" "bulk-create-us": "/userstories/bulk_create" "bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order" - "bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order" + "bulk-update-us-milestone": "/userstories/bulk_update_milestone" + "bulk-update-us-miles-order": "/userstories/bulk_update_sprint_order" "bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order" "userstories-filters": "/userstories/filters_data" "userstory-upvote": "/userstories/%s/upvote" @@ -112,6 +123,7 @@ urls = { "task-downvote": "/tasks/%s/downvote" "task-watch": "/tasks/%s/watch" "task-unwatch": "/tasks/%s/unwatch" + "task-filters": "/tasks/filters_data" # Issues "issues": "/issues" @@ -128,26 +140,30 @@ urls = { "wiki-links": "/wiki-links" # History + "history/epic": "/history/epic" "history/us": "/history/userstory" "history/issue": "/history/issue" "history/task": "/history/task" - "history/wiki": "/history/wiki" + "history/wiki": "/history/wiki/%s" # Attachments + "attachments/epic": "/epics/attachments" "attachments/us": "/userstories/attachments" "attachments/issue": "/issues/attachments" "attachments/task": "/tasks/attachments" "attachments/wiki_page": "/wiki/attachments" # Custom Attributess + "custom-attributes/epic": "/epic-custom-attributes" "custom-attributes/userstory": "/userstory-custom-attributes" - "custom-attributes/issue": "/issue-custom-attributes" "custom-attributes/task": "/task-custom-attributes" + "custom-attributes/issue": "/issue-custom-attributes" # Custom Attributess - Values + "custom-attributes-values/epic": "/epics/custom-attributes-values" "custom-attributes-values/userstory": "/userstories/custom-attributes-values" - "custom-attributes-values/issue": "/issues/custom-attributes-values" "custom-attributes-values/task": "/tasks/custom-attributes-values" + "custom-attributes-values/issue": "/issues/custom-attributes-values" # Webhooks "webhooks": "/webhooks" @@ -156,6 +172,7 @@ urls = { "webhooklogs-resend": "/webhooklogs/%s/resend" # Reports - CSV + "epics-csv": "/epics/csv?uuid=%s" "userstories-csv": "/userstories/csv?uuid=%s" "tasks-csv": "/tasks/csv?uuid=%s" "issues-csv": "/issues/csv?uuid=%s" @@ -217,6 +234,7 @@ module.run([ "$tgRolesResourcesProvider", "$tgUserSettingsResourcesProvider", "$tgSprintsResourcesProvider", + "$tgEpicsResourcesProvider", "$tgUserstoriesResourcesProvider", "$tgTasksResourcesProvider", "$tgIssuesResourcesProvider", diff --git a/app/coffee/modules/resources/custom-attributes-values.coffee b/app/coffee/modules/resources/custom-attributes-values.coffee index f5a38b2c..904d506e 100644 --- a/app/coffee/modules/resources/custom-attributes-values.coffee +++ b/app/coffee/modules/resources/custom-attributes-values.coffee @@ -29,6 +29,9 @@ resourceProvider = ($repo) -> return $repo.queryOne(resource, objectId) service = { + epic: { + get: (objectId) -> _get(objectId, "custom-attributes-values/epic") + } userstory: { get: (objectId) -> _get(objectId, "custom-attributes-values/userstory") } diff --git a/app/coffee/modules/resources/custom-attributes.coffee b/app/coffee/modules/resources/custom-attributes.coffee index 520ec2d2..88ae4872 100644 --- a/app/coffee/modules/resources/custom-attributes.coffee +++ b/app/coffee/modules/resources/custom-attributes.coffee @@ -32,6 +32,9 @@ resourceProvider = ($repo) -> return $repo.queryMany(resource, {project: projectId}) service = { + epic:{ + list: (projectId) -> _list(projectId, "custom-attributes/epic") + } userstory:{ list: (projectId) -> _list(projectId, "custom-attributes/userstory") } diff --git a/app/coffee/modules/resources/epics.coffee b/app/coffee/modules/resources/epics.coffee new file mode 100644 index 00000000..480395ce --- /dev/null +++ b/app/coffee/modules/resources/epics.coffee @@ -0,0 +1,77 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/resources/epics.coffee +### + + +taiga = @.taiga + +generateHash = taiga.generateHash + + +resourceProvider = ($repo, $http, $urls, $storage) -> + service = {} + hashSuffix = "epics-queryparams" + + service.getByRef = (projectId, ref) -> + params = service.getQueryParams(projectId) + params.project = projectId + params.ref = ref + return $repo.queryOne("epics", "by_ref", params) + + service.listValues = (projectId, type) -> + params = {"project": projectId} + service.storeQueryParams(projectId, params) + return $repo.queryMany(type, params) + + service.storeQueryParams = (projectId, params) -> + ns = "#{projectId}:#{hashSuffix}" + hash = generateHash([projectId, ns]) + $storage.set(hash, params) + + service.getQueryParams = (projectId) -> + ns = "#{projectId}:#{hashSuffix}" + hash = generateHash([projectId, ns]) + return $storage.get(hash) or {} + + service.upvote = (epicId) -> + url = $urls.resolve("epic-upvote", epicId) + return $http.post(url) + + service.downvote = (epicId) -> + url = $urls.resolve("epic-downvote", epicId) + return $http.post(url) + + service.watch = (epicId) -> + url = $urls.resolve("epic-watch", epicId) + return $http.post(url) + + service.unwatch = (epicId) -> + url = $urls.resolve("epic-unwatch", epicId) + return $http.post(url) + + return (instance) -> + instance.epics = service + + +module = angular.module("taigaResources") +module.factory("$tgEpicsResourcesProvider", ["$tgRepo","$tgHttp", "$tgUrls", "$tgStorage", resourceProvider]) diff --git a/app/coffee/modules/resources/history.coffee b/app/coffee/modules/resources/history.coffee index 5ea6886b..6e0942b1 100644 --- a/app/coffee/modules/resources/history.coffee +++ b/app/coffee/modules/resources/history.coffee @@ -31,6 +31,25 @@ resourceProvider = ($repo, $http, $urls) -> service.get = (type, objectId) -> return $repo.queryOneRaw("history/#{type}", objectId) + service.editComment = (type, objectId, activityId, comment) -> + url = $urls.resolve("history/#{type}") + url = "#{url}/#{objectId}/edit_comment" + params = { + id: activityId + } + commentData = { + comment: comment + } + return $http.post(url, commentData, params).then (data) => + return data.data + + service.getCommentHistory = (type, objectId, activityId) -> + url = $urls.resolve("history/#{type}") + url = "#{url}/#{objectId}/comment_versions" + params = {id: activityId} + return $http.get(url, params).then (data) => + return data.data + service.deleteComment = (type, objectId, activityId) -> url = $urls.resolve("history/#{type}") url = "#{url}/#{objectId}/delete_comment" diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee index 1568b035..60ea24b6 100644 --- a/app/coffee/modules/resources/issues.coffee +++ b/app/coffee/modules/resources/issues.coffee @@ -30,8 +30,6 @@ generateHash = taiga.generateHash resourceProvider = ($repo, $http, $urls, $storage, $q) -> service = {} hashSuffix = "issues-queryparams" - filtersHashSuffix = "issues-filters" - myFiltersHashSuffix = "issues-my-filters" service.get = (projectId, issueId) -> params = service.getQueryParams(projectId) @@ -95,53 +93,6 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) -> hash = generateHash([projectId, ns]) return $storage.get(hash) or {} - service.storeFilters = (projectSlug, params) -> - ns = "#{projectSlug}:#{filtersHashSuffix}" - hash = generateHash([projectSlug, ns]) - $storage.set(hash, params) - - service.getFilters = (projectSlug) -> - ns = "#{projectSlug}:#{filtersHashSuffix}" - hash = generateHash([projectSlug, ns]) - return $storage.get(hash) or {} - - service.storeMyFilters = (projectId, myFilters) -> - deferred = $q.defer() - url = $urls.resolve("user-storage") - ns = "#{projectId}:#{myFiltersHashSuffix}" - hash = generateHash([projectId, ns]) - if _.isEmpty(myFilters) - promise = $http.delete("#{url}/#{hash}", {key: hash, value:myFilters}) - promise.then -> - deferred.resolve() - promise.then null, -> - deferred.reject() - else - promise = $http.put("#{url}/#{hash}", {key: hash, value:myFilters}) - promise.then (data) -> - deferred.resolve() - promise.then null, (data) -> - innerPromise = $http.post("#{url}", {key: hash, value:myFilters}) - innerPromise.then -> - deferred.resolve() - innerPromise.then null, -> - deferred.reject() - return deferred.promise - - service.getMyFilters = (projectId) -> - deferred = $q.defer() - url = $urls.resolve("user-storage") - ns = "#{projectId}:#{myFiltersHashSuffix}" - hash = generateHash([projectId, ns]) - - promise = $http.get("#{url}/#{hash}") - promise.then (data) -> - deferred.resolve(data.data.value) - promise.then null, (data) -> - deferred.resolve({}) - - return deferred.promise - return (instance) -> instance.issues = service diff --git a/app/coffee/modules/resources/kanban.coffee b/app/coffee/modules/resources/kanban.coffee index a79bee06..48bd2074 100644 --- a/app/coffee/modules/resources/kanban.coffee +++ b/app/coffee/modules/resources/kanban.coffee @@ -32,16 +32,6 @@ resourceProvider = ($storage) -> hashSuffixStatusViewModes = "kanban-statusviewmodels" hashSuffixStatusColumnModes = "kanban-statuscolumnmodels" - service.storeStatusViewModes = (projectId, params) -> - ns = "#{projectId}:#{hashSuffixStatusViewModes}" - hash = generateHash([projectId, ns]) - $storage.set(hash, params) - - service.getStatusViewModes = (projectId) -> - ns = "#{projectId}:#{hashSuffixStatusViewModes}" - hash = generateHash([projectId, ns]) - return $storage.get(hash) or {} - service.storeStatusColumnModes = (projectId, params) -> ns = "#{projectId}:#{hashSuffixStatusColumnModes}" hash = generateHash([projectId, ns]) diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee index b4822b05..dcb48a72 100644 --- a/app/coffee/modules/resources/projects.coffee +++ b/app/coffee/modules/resources/projects.coffee @@ -40,7 +40,7 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) -> return $repo.queryMany("projects") service.listByMember = (memberId) -> - params = {"member": memberId, "order_by": "memberships__user_order"} + params = {"member": memberId, "order_by": "user_order"} return $repo.queryMany("projects", params) service.templates = -> @@ -61,18 +61,22 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) -> url = $urls.resolve("bulk-update-projects-order") return $http.post(url, bulkData) + service.regenerate_epics_csv_uuid = (projectId) -> + url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_epics_csv_uuid" + return $http.post(url) + service.regenerate_userstories_csv_uuid = (projectId) -> url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_userstories_csv_uuid" return $http.post(url) - service.regenerate_issues_csv_uuid = (projectId) -> - url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_issues_csv_uuid" - return $http.post(url) - service.regenerate_tasks_csv_uuid = (projectId) -> url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_tasks_csv_uuid" return $http.post(url) + service.regenerate_issues_csv_uuid = (projectId) -> + url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_issues_csv_uuid" + return $http.post(url) + service.leave = (projectId) -> url = "#{$urls.resolve("projects")}/#{projectId}/leave" return $http.post(url) @@ -83,6 +87,34 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) -> service.tagsColors = (projectId) -> return $repo.queryOne("projects", "#{projectId}/tags_colors") + service.deleteTag = (projectId, tag) -> + url = "#{$urls.resolve("projects")}/#{projectId}/delete_tag" + return $http.post(url, {tag: tag}) + + service.createTag = (projectId, tag, color) -> + url = "#{$urls.resolve("projects")}/#{projectId}/create_tag" + data = {} + data.tag = tag + data.color = null + if color + data.color = color + return $http.post(url, data) + + service.editTag = (projectId, from_tag, to_tag, color) -> + url = "#{$urls.resolve("projects")}/#{projectId}/edit_tag" + data = {} + data.from_tag = from_tag + if to_tag + data.to_tag = to_tag + data.color = null + if color + data.color = color + return $http.post(url, data) + + service.mixTags = (projectId, to_tag, from_tags) -> + url = "#{$urls.resolve("projects")}/#{projectId}/mix_tags" + return $http.post(url, {to_tag: to_tag, from_tags: from_tags}) + service.export = (projectId) -> url = "#{$urls.resolve("exporter")}/#{projectId}" return $http.get(url) diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 6a838a5d..4c47ad6e 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -38,17 +38,23 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params.project = projectId return $repo.queryOne("tasks", taskId, params) - service.getByRef = (projectId, ref) -> + service.getByRef = (projectId, ref, extraParams) -> params = service.getQueryParams(projectId) params.project = projectId params.ref = ref + + params = _.extend({}, params, extraParams) + return $repo.queryOne("tasks", "by_ref", params) service.listInAllProjects = (filters) -> return $repo.queryMany("tasks", filters) - service.list = (projectId, sprintId=null, userStoryId=null) -> - params = {project: projectId} + service.filtersData = (params) -> + return $repo.queryOneRaw("task-filters", null, params) + + service.list = (projectId, sprintId=null, userStoryId=null, params) -> + params = _.merge(params, {project: projectId}) params.milestone = sprintId if sprintId params.user_story = userStoryId if userStoryId service.storeQueryParams(projectId, params) @@ -56,7 +62,7 @@ resourceProvider = ($repo, $http, $urls, $storage) -> service.bulkCreate = (projectId, sprintId, usId, data) -> url = $urls.resolve("bulk-create-tasks") - params = {project_id: projectId, sprint_id: sprintId, us_id: usId, bulk_tasks: data} + params = {project_id: projectId, milestone_id: sprintId, us_id: usId, bulk_tasks: data} return $http.post(url, params).then (result) -> return result.data diff --git a/app/coffee/modules/resources/userstories.coffee b/app/coffee/modules/resources/userstories.coffee index 935f6d3a..f4c5cca4 100644 --- a/app/coffee/modules/resources/userstories.coffee +++ b/app/coffee/modules/resources/userstories.coffee @@ -26,7 +26,7 @@ taiga = @.taiga generateHash = taiga.generateHash -resourceProvider = ($repo, $http, $urls, $storage) -> +resourceProvider = ($repo, $http, $urls, $storage, $q) -> service = {} hashSuffix = "userstories-queryparams" @@ -35,10 +35,12 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params.project = projectId return $repo.queryOne("userstories", usId, params) - service.getByRef = (projectId, ref) -> + service.getByRef = (projectId, ref, extraParams = {}) -> params = service.getQueryParams(projectId) params.project = projectId params.ref = ref + params = _.extend({}, params, extraParams) + return $repo.queryOne("userstories", "by_ref", params) service.listInAllProjects = (filters) -> @@ -96,9 +98,9 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params = {project_id: projectId, bulk_stories: data} return $http.post(url, params) - service.bulkUpdateSprintOrder = (projectId, data) -> - url = $urls.resolve("bulk-update-us-sprint-order") - params = {project_id: projectId, bulk_stories: data} + service.bulkUpdateMilestone = (projectId, milestoneId, data) -> + url = $urls.resolve("bulk-update-us-milestone") + params = {project_id: projectId, milestone_id: milestoneId, bulk_stories: data} return $http.post(url, params) service.bulkUpdateKanbanOrder = (projectId, data) -> @@ -133,4 +135,4 @@ resourceProvider = ($repo, $http, $urls, $storage) -> instance.userstories = service module = angular.module("taigaResources") -module.factory("$tgUserstoriesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", resourceProvider]) +module.factory("$tgUserstoriesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", "$q", resourceProvider]) diff --git a/app/coffee/modules/resources/wiki.coffee b/app/coffee/modules/resources/wiki.coffee index c333d9cb..8c7beee4 100644 --- a/app/coffee/modules/resources/wiki.coffee +++ b/app/coffee/modules/resources/wiki.coffee @@ -34,6 +34,9 @@ resourceProvider = ($repo, $http, $urls) -> service.getBySlug = (projectId, slug) -> return $repo.queryOne("wiki", "by_slug?project=#{projectId}&slug=#{slug}") + service.list = (projectId) -> + return $repo.queryMany("wiki", {project: projectId}) + service.listLinks = (projectId) -> return $repo.queryMany("wiki-links", {project: projectId}) diff --git a/app/coffee/modules/search.coffee b/app/coffee/modules/search.coffee index 1c154887..37cf51bc 100644 --- a/app/coffee/modules/search.coffee +++ b/app/coffee/modules/search.coffee @@ -48,10 +48,11 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgLocation", "tgAppMetaService", "$tgNavUrls", - "$translate" + "$translate", + "tgErrorHandlingService" ] - constructor: (@scope, @repo, @rs, @params, @q, @location, @appMetaService, @navUrls, @translate) -> + constructor: (@scope, @repo, @rs, @params, @q, @location, @appMetaService, @navUrls, @translate, @errorHandlingService) -> @scope.sectionName = "Search" promise = @.loadInitialData() @@ -87,6 +88,8 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) return @rs.projects.getBySlug(@params.pslug).then (project) => @scope.project = project @scope.$emit('project:loaded', project) + + @scope.epicStatusById = groupBy(project.epic_statuses, (x) -> x.id) @scope.issueStatusById = groupBy(project.issue_statuses, (x) -> x.id) @scope.taskStatusById = groupBy(project.task_statuses, (x) -> x.id) @scope.severityById = groupBy(project.severities, (x) -> x.id) @@ -193,7 +196,7 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> return selectedSection if data - for name in ["userstories", "issues", "tasks", "wikipages"] + for name in ["userstories", "epics", "issues", "tasks", "wikipages"] value = data[name] if value.length > maxVal @@ -221,6 +224,7 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> activeSectionName = section.name templates = { + epics: $templatecache.get("search-epics") issues: $templatecache.get("search-issues") tasks: $templatecache.get("search-tasks") userstories: $templatecache.get("search-userstories") diff --git a/app/coffee/modules/taskboard/lightboxes.coffee b/app/coffee/modules/taskboard/lightboxes.coffee index 756d84e8..276cadc9 100644 --- a/app/coffee/modules/taskboard/lightboxes.coffee +++ b/app/coffee/modules/taskboard/lightboxes.coffee @@ -25,6 +25,7 @@ taiga = @.taiga bindOnce = @.taiga.bindOnce debounce = @.taiga.debounce +trim = @.taiga.trim CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, attachmentsService) -> link = ($scope, $el, attrs) -> @@ -41,7 +42,8 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer attachmentsToAdd = attachmentsToAdd.push(attachment) $scope.deleteAttachment = (attachment) -> - attachmentsToDelete = attachmentsToDelete.push(attachment) + if attachment.get("id") + attachmentsToDelete = attachmentsToDelete.push(attachment) createAttachments = (obj) -> promises = _.map attachmentsToAdd.toJS(), (attachment) -> @@ -55,6 +57,45 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer return $q.all(promises) + tagsToAdd = [] + + $scope.addTag = (tag, color) -> + value = trim(tag.toLowerCase()) + + tags = $scope.project.tags + projectTags = $scope.project.tags_colors + + tags = [] if not tags? + projectTags = {} if not projectTags? + + if value not in tags + tags.push(value) + + projectTags[tag] = color || null + + $scope.project.tags = tags + + itemtags = _.clone($scope.task.tags) + + inserted = _.find itemtags, (it) -> it[0] == value + + if !inserted + itemtags.push([tag , color]) + $scope.task.tags = itemtags + + + $scope.deleteTag = (tag) -> + value = trim(tag[0].toLowerCase()) + + tags = $scope.project.tags + itemtags = _.clone($scope.task.tags) + + _.remove itemtags, (tag) -> tag[0] == value + + $scope.task.tags = itemtags + + _.pull($scope.task.tags, value) + $scope.$on "taskform:new", (ctx, sprintId, usId) -> $scope.task = { project: $scope.projectId @@ -78,7 +119,10 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer $el.find(".title").html(newTask + " ") $el.find(".tag-input").val("") - lightboxService.open($el) + lightboxService.open $el, () -> + $scope.createEditTaskOpen = false + + $scope.createEditTaskOpen = true $scope.$on "taskform:edit", (ctx, task, attachments) -> $scope.task = task @@ -96,7 +140,10 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer $el.find(".title").html(edit + " ") $el.find(".tag-input").val("") - lightboxService.open($el) + lightboxService.open $el, () -> + $scope.createEditTaskOpen = false + + $scope.createEditTaskOpen = true submitButton = $el.find(".submit-button") @@ -108,6 +155,11 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer if not form.validate() return + params = { + include_attachments: true, + include_tasks: true + } + if $scope.isNew promise = $repo.create("tasks", $scope.task) broadcastEvent = "taskform:new:success" @@ -116,20 +168,22 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer broadcastEvent = "taskform:edit:success" promise.then (data) -> - createAttachments(data) deleteAttachments(data) + .then () => createAttachments(data) + .then () => + currentLoading.finish() + lightboxService.close($el) - return data + $rs.tasks.getByRef(data.project, data.ref, params).then (task) -> + $rootscope.$broadcast(broadcastEvent, task) currentLoading = $loading() .target(submitButton) .start() - # FIXME: error handling? promise.then (data) -> currentLoading.finish() lightboxService.close($el) - $rootscope.$broadcast(broadcastEvent, data) $el.on "submit", "form", submit @@ -139,7 +193,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer return {link: link} -CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) -> +CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $model) -> link = ($scope, $el, attrs) -> $scope.form = {data: "", usId: null} @@ -161,6 +215,7 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService) - promise = $rs.tasks.bulkCreate(projectId, sprintId, usId, data) promise.then (result) -> + result = _.map(result, (x) => $model.make_model('userstories', x)) currentLoading.finish() $rootscope.$broadcast("taskform:bulk:success", result) lightboxService.close($el) @@ -205,5 +260,6 @@ module.directive("tgLbCreateBulkTasks", [ "$rootScope", "$tgLoading", "lightboxService", + "$tgModel", CreateBulkTasksDirective ]) diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index d32b159f..74f95d84 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -38,13 +38,14 @@ module = angular.module("taigaTaskboard") ## Taskboard Controller ############################################################################# -class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) +class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) @.$inject = [ "$scope", "$rootScope", "$tgRepo", "$tgConfirm", "$tgResources", + "tgResources" "$routeParams", "$q", "tgAppMetaService", @@ -52,12 +53,21 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgNavUrls" "$tgEvents" "$tgAnalytics", - "$translate" + "$translate", + "tgErrorHandlingService", + "tgTaskboardTasks", + "$tgStorage", + "tgFilterRemoteStorageService" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appMetaService, @location, @navUrls, - @events, @analytics, @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @appMetaService, @location, @navUrls, + @events, @analytics, @translate, @errorHandlingService, @taskboardTasksService, @storage, @filterRemoteStorageService) -> bindMethods(@) + @taskboardTasksService.reset() + @scope.userstories = [] + @.openFilter = false + + return if @.applyStoredFilters(@params.pslug, "tasks-filters") @scope.sectionName = @translate.instant("TASKBOARD.SECTION_NAME") @.initializeEventHandlers() @@ -69,6 +79,155 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) # On Error promise.then null, @.onInitialDataError.bind(@) + taiga.defineImmutableProperty @.scope, "usTasks", () => + return @taskboardTasksService.usTasks + + setZoom: (zoomLevel, zoom) -> + if @.zoomLevel != zoomLevel + @taskboardTasksService.resetFolds() + + @.zoomLevel = zoomLevel + @.zoom = zoom + + if @.zoomLevel == '0' + @rootscope.$broadcast("sprint:zoom0") + + changeQ: (q) -> + @.replaceFilter("q", q) + @.loadTasks() + @.generateFilters() + + removeFilter: (filter) -> + @.unselectFilter(filter.dataType, filter.id) + @.loadTasks() + @.generateFilters() + + addFilter: (newFilter) -> + @.selectFilter(newFilter.category.dataType, newFilter.filter.id) + @.loadTasks() + @.generateFilters() + + selectCustomFilter: (customFilter) -> + @.replaceAllFilters(customFilter.filter) + @.loadTasks() + @.generateFilters() + + removeCustomFilter: (customFilter) -> + @filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) => + delete userFilters[customFilter.id] + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters) + + saveCustomFilter: (name) -> + filters = {} + urlfilters = @location.search() + filters.tags = urlfilters.tags + filters.status = urlfilters.status + filters.assigned_to = urlfilters.assigned_to + filters.owner = urlfilters.owner + + @filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) => + userFilters[name] = filters + + @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters) + + generateFilters: -> + @.storeFilters(@params.pslug, @location.search(), "tasks-filters") + + urlfilters = @location.search() + + loadFilters = {} + loadFilters.project = @scope.projectId + loadFilters.milestone = @scope.sprintId + loadFilters.tags = urlfilters.tags + loadFilters.status = urlfilters.status + loadFilters.assigned_to = urlfilters.assigned_to + loadFilters.owner = urlfilters.owner + loadFilters.q = urlfilters.q + + return @q.all([ + @rs.tasks.filtersData(loadFilters), + @filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters') + ]).then (result) => + data = result[0] + customFiltersRaw = result[1] + + statuses = _.map data.statuses, (it) -> + it.id = it.id.toString() + + return it + tags = _.map data.tags, (it) -> + it.id = it.name + + return it + + tagsWithAtLeastOneElement = _.filter tags, (tag) -> + return tag.count > 0 + + assignedTo = _.map data.assigned_to, (it) -> + if it.id + it.id = it.id.toString() + else + it.id = "null" + + it.name = it.full_name || "Unassigned" + + return it + owner = _.map data.owners, (it) -> + it.id = it.id.toString() + it.name = it.full_name + + return it + + @.selectedFilters = [] + + if loadFilters.status + selected = @.formatSelectedFilters("status", statuses, loadFilters.status) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.tags + selected = @.formatSelectedFilters("tags", tags, loadFilters.tags) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.assigned_to + selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to) + @.selectedFilters = @.selectedFilters.concat(selected) + + if loadFilters.owner + selected = @.formatSelectedFilters("owner", owner, loadFilters.owner) + @.selectedFilters = @.selectedFilters.concat(selected) + + @.filterQ = loadFilters.q + + @.filters = [ + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"), + dataType: "status", + content: statuses + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"), + dataType: "tags", + content: tags, + hideEmpty: true, + totalTaggedElements: tagsWithAtLeastOneElement.length + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_TO"), + dataType: "assigned_to", + content: assignedTo + }, + { + title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), + dataType: "owner", + content: owner + } + ] + + @.customFilters = [] + _.forOwn customFiltersRaw, (value, key) => + @.customFilters.push({id: key, name: key, filter: value}) + _setMeta: -> prettyDate = @translate.instant("BACKLOG.SPRINTS.DATE") @@ -91,24 +250,33 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) @appMetaService.setAll(title, description) initializeEventHandlers: -> - # TODO: Reload entire taskboard after create/edit tasks seems - # a big overhead. It should be optimized in near future. - @scope.$on "taskform:bulk:success", => - @.loadTaskboard() + @scope.$on "taskform:bulk:success", (event, tasks) => + @.refreshTagsColors().then () => + @taskboardTasksService.add(tasks) + @analytics.trackEvent("task", "create", "bulk create task on taskboard", 1) - @scope.$on "taskform:new:success", => - @.loadTaskboard() + @scope.$on "taskform:new:success", (event, task) => + @.refreshTagsColors().then () => + @taskboardTasksService.add(task) + @analytics.trackEvent("task", "create", "create task on taskboard", 1) - @scope.$on("taskform:edit:success", => @.loadTaskboard()) - @scope.$on("taskboard:task:move", @.taskMove) + @scope.$on "taskform:edit:success", (event, task) => + @.refreshTagsColors().then () => + @taskboardTasksService.replaceModel(task) - @scope.$on "assigned-to:added", (ctx, userId, task) => - task.assigned_to = userId - promise = @repo.save(task) - promise.then null, -> - console.log "FAIL" # TODO + @scope.$on("taskboard:task:move", @.taskMove) + @scope.$on("assigned-to:added", @.onAssignedToChanged) + + onAssignedToChanged: (ctx, userid, taskModel) -> + taskModel.assigned_to = userid + + @taskboardTasksService.replaceModel(taskModel) + + promise = @repo.save(taskModel) + promise.then null, -> + console.log "FAIL" # TODO initializeSubscription: -> routingKey = "changes.project.#{@scope.projectId}.tasks" @@ -124,12 +292,11 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => if not project.is_backlog_activated - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.project = project # Not used at this momment @scope.pointsList = _.sortBy(project.points, "order") - # @scope.roleList = _.sortBy(project.roles, "order") @scope.pointsById = groupBy(project.points, (e) -> e.id) @scope.roleById = groupBy(project.roles, (e) -> e.id) @scope.taskStatusList = _.sortBy(project.task_statuses, "order") @@ -163,40 +330,27 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) refreshTagsColors: -> return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => - @scope.project.tags_colors = tags_colors + @scope.project.tags_colors = tags_colors._attrs loadSprint: -> return @rs.sprints.get(@scope.projectId, @scope.sprintId).then (sprint) => @scope.sprint = sprint @scope.userstories = _.sortBy(sprint.user_stories, "sprint_order") + + @taskboardTasksService.setUserstories(@scope.userstories) + return sprint loadTasks: -> - return @rs.tasks.list(@scope.projectId, @scope.sprintId).then (tasks) => - @scope.tasks = _.sortBy(tasks, 'taskboard_order') - @scope.usTasks = {} + params = { + include_attachments: true, + } - # Iterate over all userstories and - # null userstory for unassigned tasks - for us in _.union(@scope.userstories, [{id:null}]) - @scope.usTasks[us.id] = {} - for status in @scope.taskStatusList - @scope.usTasks[us.id][status.id] = [] + params = _.merge params, @location.search() - for task in @scope.tasks - if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]? - @scope.usTasks[task.user_story][task.status].push(task) - - if tasks.length == 0 - - if @scope.userstories.length > 0 - usId = @scope.userstories[0].id - else - usId = null - - @scope.usTasks[usId][@scope.taskStatusList[0].id].push({isPlaceholder: true}) - - return tasks + return @rs.tasks.list(@scope.projectId, @scope.sprintId, null, params).then (tasks) => + @taskboardTasksService.init(@scope.project, @scope.usersById) + @taskboardTasksService.set(tasks) loadTaskboard: -> return @q.all([ @@ -218,60 +372,97 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) return data return promise.then(=> @.loadProject()) - .then(=> @.loadTaskboard()) + .then => + @.generateFilters() - refreshTasksOrder: (tasks) -> - items = @.resortTasks(tasks) - data = @.prepareBulkUpdateData(items) + return @.loadTaskboard().then(=> @.setRolePoints()) - return @rs.tasks.bulkUpdateTaskTaskboardOrder(@scope.project.id, data) + showPlaceHolder: (statusId, usId) -> + if !@taskboardTasksService.tasksRaw.length + if @scope.taskStatusList[0].id == statusId && + (!@scope.userstories.length || @scope.userstories[0].id == usId) + return true - resortTasks: (tasks) -> - items = [] + return false - for item, index in tasks - item["taskboard_order"] = index - if item.isModified() - items.push(item) + editTask: (id) -> + task = @.taskboardTasksService.getTask(id) - return items + task = task.set('loading', true) + @taskboardTasksService.replace(task) - prepareBulkUpdateData: (uses) -> - return _.map(uses, (x) -> {"task_id": x.id, "order": x["taskboard_order"]}) + @rs.tasks.getByRef(task.getIn(['model', 'project']), task.getIn(['model', 'ref'])).then (editingTask) => + @rs2.attachments.list("task", task.get('id'), task.getIn(['model', 'project'])).then (attachments) => + @rootscope.$broadcast("taskform:edit", editingTask, attachments.toJS()) + task = task.set('loading', false) + @taskboardTasksService.replace(task) - taskMove: (ctx, task, usId, statusId, order) -> - # Remove task from old position - r = @scope.usTasks[task.user_story][task.status].indexOf(task) - @scope.usTasks[task.user_story][task.status].splice(r, 1) + taskMove: (ctx, task, oldStatusId, usId, statusId, order) -> + task = @taskboardTasksService.getTaskModel(task.get('id')) - # Add task to new position - tasks = @scope.usTasks[usId][statusId] - tasks.splice(order, 0, task) + moveUpdateData = @taskboardTasksService.move(task.id, usId, statusId, order) - task.user_story = usId - task.status = statusId - task.taskboard_order = order + params = { + status__is_archived: false, + include_attachments: true, + } - promise = @repo.save(task) + options = { + headers: { + "set-orders": JSON.stringify(moveUpdateData.set_orders) + } + } - @rootscope.$broadcast("sprint:task:moved", task) + promise = @repo.save(task, true, params, options, true).then (result) => + headers = result[1] + + if headers && headers['taiga-info-order-updated'] + order = JSON.parse(headers['taiga-info-order-updated']) + @taskboardTasksService.assignOrders(order) - promise.then => - @.refreshTasksOrder(tasks) @.loadSprintStats() - promise.then null, => - console.log "FAIL TASK SAVE" - ## Template actions addNewTask: (type, us) -> switch type when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id) when "bulk" then @rootscope.$broadcast("taskform:bulk", @scope.sprintId, us?.id) - editTaskAssignedTo: (task) -> + toggleFold: (id) -> + @taskboardTasksService.toggleFold(id) + + changeTaskAssignedTo: (id) -> + task = @taskboardTasksService.getTaskModel(id) + @rootscope.$broadcast("assigned-to:add", task) + setRolePoints: () -> + computableRoles = _.filter(@scope.project.roles, "computable") + + getRole = (roleId) => + roleId = parseInt(roleId, 10) + return _.find computableRoles, (role) -> role.id == roleId + + getPoint = (pointId) => + poitnId = parseInt(pointId, 10) + return _.find @scope.project.points, (point) -> point.id == pointId + + pointsByRole = _.reduce @scope.userstories, (result, us, key) => + _.forOwn us.points, (pointId, roleId) -> + role = getRole(roleId) + point = getPoint(pointId) + + if !result[role.id] + result[role.id] = role + result[role.id].points = 0 + + result[role.id].points += point.value + + return result + , {} + + @scope.pointsByRole = Object.keys(pointsByRole).map (key) -> return pointsByRole[key] + module.controller("TaskboardController", TaskboardController) @@ -302,43 +493,6 @@ TaskboardDirective = ($rootscope) -> module.directive("tgTaskboard", ["$rootScope", TaskboardDirective]) - -############################################################################# -## Taskboard Task Directive -############################################################################# - -TaskboardTaskDirective = ($rootscope, $loading, $rs, $rs2) -> - link = ($scope, $el, $attrs, $model) -> - $scope.$watch "task", (task) -> - if task.is_blocked and not $el.hasClass("blocked") - $el.addClass("blocked") - else if not task.is_blocked and $el.hasClass("blocked") - $el.removeClass("blocked") - - $el.find(".edit-task").on "click", (event) -> - if $el.find('.edit-task').hasClass('noclick') - return - - $scope.$apply -> - target = $(event.target) - - currentLoading = $loading() - .target(target) - .timeout(200) - .start() - - task = $scope.task - - $rs.tasks.getByRef(task.project, task.ref).then (editingTask) => - $rs2.attachments.list("task", editingTask.id, editingTask.project).then (attachments) => - $rootscope.$broadcast("taskform:edit", editingTask, attachments.toJS()) - currentLoading.finish() - - return {link:link} - - -module.directive("tgTaskboardTask", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", TaskboardTaskDirective]) - ############################################################################# ## Taskboard Squish Column Directive ############################################################################# @@ -348,14 +502,18 @@ TaskboardSquishColumnDirective = (rs) -> maxColumnWidth = 300 link = ($scope, $el, $attrs) -> + $scope.$on "sprint:zoom0", () => + recalculateTaskboardWidth() + $scope.$on "sprint:task:moved", () => recalculateTaskboardWidth() - bindOnce $scope, "usTasks", (project) -> - $scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id) - $scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId) + $scope.$watch "usTasks", () -> + if $scope.project + $scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id) + $scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId) - recalculateTaskboardWidth() + recalculateTaskboardWidth() $scope.foldStatus = (status) -> $scope.statusesFolded[status.id] = !!!$scope.statusesFolded[status.id] @@ -374,7 +532,10 @@ TaskboardSquishColumnDirective = (rs) -> recalculateTaskboardWidth() getCeilWidth = (usId, statusId) => - tasks = $scope.usTasks[usId][statusId].length + if usId + tasks = $scope.usTasks.getIn([usId.toString(), statusId.toString()]).size + else + tasks = $scope.usTasks.getIn(['null', statusId.toString()]).size if $scope.statusesFolded[statusId] if tasks and $scope.usFolded[usId] @@ -393,7 +554,10 @@ TaskboardSquishColumnDirective = (rs) -> if width column.css('max-width', width) else - column.css("max-width", maxColumnWidth) + if $scope.ctrl.zoomLevel == '0' + column.css("max-width", 148) + else + column.css("max-width", maxColumnWidth) refreshTaskboardTableWidth = () => columnWidths = [] @@ -429,65 +593,3 @@ TaskboardSquishColumnDirective = (rs) -> return {link: link} module.directive("tgTaskboardSquishColumn", ["$tgResources", TaskboardSquishColumnDirective]) - -############################################################################# -## Taskboard User Directive -############################################################################# - -TaskboardUserDirective = ($log, $translate) -> - clickable = false - - link = ($scope, $el, $attrs) -> - username_label = $el.parent().find("a.task-assigned") - username_label.addClass("not-clickable") - - $scope.$watch 'task.assigned_to', (assigned_to) -> - user = $scope.usersById[assigned_to] - - if user is undefined - _.assign($scope, { - name: $translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED"), - imgurl: "/#{window._version}/images/unnamed.png", - clickable: clickable - }) - else - _.assign($scope, { - name: user.full_name_display, - imgurl: user.photo, - clickable: clickable - }) - - username_label.text($scope.name) - - - bindOnce $scope, "project", (project) -> - if project.my_permissions.indexOf("modify_task") > -1 - clickable = true - $el.find(".avatar-assigned-to").on "click", (event) => - if $el.find('a').hasClass('noclick') - return - - $ctrl = $el.controller() - $ctrl.editTaskAssignedTo($scope.task) - - username_label.removeClass("not-clickable") - username_label.on "click", (event) -> - if $el.find('a').hasClass('noclick') - return - - $ctrl = $el.controller() - $ctrl.editTaskAssignedTo($scope.task) - - - return { - link: link, - templateUrl: "taskboard/taskboard-user.html", - scope: { - "usersById": "=users", - "project": "=", - "task": "=", - } - } - - -module.directive("tgTaskboardUserAvatar", ["$log", "$translate", TaskboardUserDirective]) diff --git a/app/coffee/modules/taskboard/sortable.coffee b/app/coffee/modules/taskboard/sortable.coffee index c4237b56..9b92eb51 100644 --- a/app/coffee/modules/taskboard/sortable.coffee +++ b/app/coffee/modules/taskboard/sortable.coffee @@ -37,11 +37,14 @@ module = angular.module("taigaBacklog") ## Sortable Directive ############################################################################# -TaskboardSortableDirective = ($repo, $rs, $rootscope) -> +TaskboardSortableDirective = ($repo, $rs, $rootscope, $translate) -> link = ($scope, $el, $attrs) -> - bindOnce $scope, "tasks", (xx) -> - # If the user has not enough permissions we don't enable the sortable - if not ($scope.project.my_permissions.indexOf("modify_us") > -1) + unwatch = $scope.$watch "usTasks", (usTasks) -> + return if !usTasks || !usTasks.size + + unwatch() + + if not ($scope.project.my_permissions.indexOf("modify_task") > -1) return oldParentScope = null @@ -49,6 +52,10 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) -> itemEl = null tdom = $el + filterError = -> + text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR") + $tgConfirm.notify("error", text) + deleteElement = (itemEl) -> # Completelly remove item and its scope from dom itemEl.scope().$destroy() @@ -62,12 +69,23 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) -> copySortSource: false, copy: false, mirrorContainer: $el[0], - moves: (item) -> return $(item).hasClass('taskboard-task') + accepts: (el, target) -> return !$(target).hasClass('taskboard-userstory-box') + moves: (item) -> + return $(item).is('tg-card') }) drake.on 'drag', (item) -> oldParentScope = $(item).parent().scope() + if $el.hasClass("active-filters") + filterError() + + setTimeout (() -> + drake.cancel(true) + ), 0 + + return false + drake.on 'dragend', (item) -> parentEl = $(item).parent() itemEl = $(item) @@ -84,14 +102,15 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) -> deleteElement(itemEl) $scope.$apply -> - $rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex) + $rootscope.$broadcast("taskboard:task:move", itemTask, itemTask.getIn(['model', 'status']), newUsId, newStatusId, itemIndex) + scroll = autoScroll([$('.taskboard-table-body')[0]], { - margin: 20, + margin: 100, pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) $scope.$on "$destroy", -> @@ -105,5 +124,6 @@ module.directive("tgTaskboardSortable", [ "$tgRepo", "$tgResources", "$rootScope", + "$translate", TaskboardSortableDirective ]) diff --git a/app/coffee/modules/taskboard/taskboard-tasks.coffee b/app/coffee/modules/taskboard/taskboard-tasks.coffee new file mode 100644 index 00000000..013238fb --- /dev/null +++ b/app/coffee/modules/taskboard/taskboard-tasks.coffee @@ -0,0 +1,172 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: home.service.coffee +### + +groupBy = @.taiga.groupBy + +class TaskboardTasksService extends taiga.Service + @.$inject = [] + constructor: () -> + @.reset() + + reset: () -> + @.tasksRaw = [] + @.foldStatusChanged = {} + @.usTasks = Immutable.Map() + + init: (project, usersById) -> + @.project = project + @.usersById = usersById + + resetFolds: () -> + @.foldStatusChanged = {} + @.refresh() + + toggleFold: (taskId) -> + @.foldStatusChanged[taskId] = !@.foldStatusChanged[taskId] + @.refresh() + + add: (task) -> + @.tasksRaw = @.tasksRaw.concat(task) + @.refresh() + + set: (tasks) -> + @.tasksRaw = tasks + @.refreshRawOrder() + @.refresh() + + setUserstories: (userstories) -> + @.userstories = userstories + + refreshRawOrder: () -> + @.order = {} + + @.order[task.id] = task.taskboard_order for task in @.tasksRaw + + assignOrders: (order) -> + order = _.invert(order) + @.order = _.assign(@.order, order) + + @.refresh() + + getTask: (id) -> + findedTask = null + + @.usTasks.forEach (us) -> + us.forEach (status) -> + findedTask = status.find (task) -> return task.get('id') == id + + return false if findedTask + + return false if findedTask + + return findedTask + + replace: (task) -> + @.usTasks = @.usTasks.map (us) -> + return us.map (status) -> + findedIndex = status.findIndex (usItem) -> + return usItem.get('id') == us.get('id') + + if findedIndex != -1 + status = status.set(findedIndex, task) + + return status + + getTaskModel: (id) -> + return _.find @.tasksRaw, (task) -> return task.id == id + + replaceModel: (task) -> + @.tasksRaw = _.map @.tasksRaw, (it) -> + if task.id == it.id + return task + else + return it + + @.refresh() + + move: (id, usId, statusId, index) -> + task = @.getTaskModel(id) + + taskByUsStatus = _.filter @.tasksRaw, (task) => + return task.status == statusId && task.user_story == usId + + taskByUsStatus = _.sortBy taskByUsStatus, (it) => @.order[it.id] + + taksWithoutMoved = _.filter taskByUsStatus, (it) => it.id != id + beforeDestination = _.slice(taksWithoutMoved, 0, index) + afterDestination = _.slice(taksWithoutMoved, index) + + setOrders = {} + + previous = beforeDestination[beforeDestination.length - 1] + + previousWithTheSameOrder = _.filter beforeDestination, (it) => + @.order[it.id] == @.order[previous.id] + + if previousWithTheSameOrder.length > 1 + for it in previousWithTheSameOrder + setOrders[it.id] = @.order[it.id] + + if !previous + @.order[task.id] = 0 + else if previous + @.order[task.id] = @.order[previous.id] + 1 + + for it, key in afterDestination + @.order[it.id] = @.order[task.id] + key + 1 + + task.status = statusId + task.user_story = usId + task.taskboard_order = @.order[task.id] + + @.refresh() + + return {"task_id": task.id, "order": @.order[task.id], "set_orders": setOrders} + + refresh: -> + @.tasksRaw = _.sortBy @.tasksRaw, (it) => @.order[it.id] + + tasks = @.tasksRaw + taskStatusList = _.sortBy(@.project.task_statuses, "order") + + usTasks = {} + + # Iterate over all userstories and + # null userstory for unassigned tasks + for us in _.union(@.userstories, [{id:null}]) + usTasks[us.id] = {} + for status in taskStatusList + usTasks[us.id][status.id] = [] + + for taskModel in tasks + if usTasks[taskModel.user_story]? and usTasks[taskModel.user_story][taskModel.status]? + task = {} + task.foldStatusChanged = @.foldStatusChanged[taskModel.id] + task.model = taskModel.getAttrs() + task.images = _.filter taskModel.attachments, (it) -> return !!it.thumbnail_card_url + task.id = taskModel.id + task.assigned_to = @.usersById[taskModel.assigned_to] + task.colorized_tags = _.map task.model.tags, (tag) => + return {name: tag[0], color: tag[1]} + + usTasks[taskModel.user_story][taskModel.status].push(task) + + @.usTasks = Immutable.fromJS(usTasks) + +angular.module("taigaKanban").service("tgTaskboardTasks", TaskboardTasksService) diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index 05e6aff4..2e9ae523 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -50,11 +50,12 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgNavUrls", "$tgAnalytics", "$translate", - "$tgQueueModelTransformation" + "$tgQueueModelTransformation", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appMetaService, @navUrls, @analytics, @translate, @modelTransform) -> + @log, @appMetaService, @navUrls, @analytics, @translate, @modelTransform, @errorHandlingService) -> bindMethods(@) @scope.taskRef = @params.taskref diff --git a/app/coffee/modules/team/main.coffee b/app/coffee/modules/team/main.coffee index a90b6181..283e69e9 100644 --- a/app/coffee/modules/team/main.coffee +++ b/app/coffee/modules/team/main.coffee @@ -45,11 +45,12 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) "tgAppMetaService", "$tgAuth", "$translate", - "tgProjectService" + "tgProjectService", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService, @auth, - @translate, @projectService) -> + @translate, @projectService, @errorHandlingService) -> @scope.sectionName = "TEAM.SECTION_NAME" promise = @.loadInitialData() @@ -80,6 +81,8 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) for member in @scope.activeUsers @scope.totals[member.id] = 0 + console.log @scope.activeUsers + # Get current user @scope.currentUser = _.find(@scope.activeUsers, {id: user?.id}) diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee index 98348150..e1ac9139 100644 --- a/app/coffee/modules/user-settings/main.coffee +++ b/app/coffee/modules/user-settings/main.coffee @@ -45,19 +45,19 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgLocation", "$tgNavUrls", "$tgAuth", - "$translate" + "$translate", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @config, @repo, @confirm, @rs, @params, @q, @location, @navUrls, - @auth, @translate) -> + @auth, @translate, @errorHandlingService) -> @scope.sectionName = "USER_SETTINGS.MENU.SECTION_TITLE" @scope.project = {} @scope.user = @auth.getUser() if !@scope.user - @location.path(@navUrls.resolve("permission-denied")) - @location.replace() + @errorHandlingService.permissionDenied() @scope.lang = @getLan() @scope.theme = @getTheme() diff --git a/app/coffee/modules/user-settings/notifications.coffee b/app/coffee/modules/user-settings/notifications.coffee index c089c542..0cb25f4c 100644 --- a/app/coffee/modules/user-settings/notifications.coffee +++ b/app/coffee/modules/user-settings/notifications.coffee @@ -44,10 +44,11 @@ class UserNotificationsController extends mixOf(taiga.Controller, taiga.PageMixi "$q", "$tgLocation", "$tgNavUrls", - "$tgAuth" + "$tgAuth", + "tgErrorHandlingService" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth, @errorHandlingService) -> @scope.sectionName = "USER_SETTINGS.NOTIFICATIONS.SECTION_NAME" @scope.user = @auth.getUser() promise = @.loadInitialData() diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index db09821d..950185df 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -50,12 +50,13 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgNavUrls", "$tgAnalytics", "$translate", - "$tgConfig", - "$tgQueueModelTransformation" + "$tgQueueModelTransformation", + "tgErrorHandlingService", + "$tgConfig" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, - @navUrls, @analytics, @translate, @configService, @modelTransform) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, + @log, @appMetaService, @navUrls, @analytics, @translate, @modelTransform, @errorHandlingService, @configService) -> bindMethods(@) @scope.usRef = @params.usref @@ -165,20 +166,6 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @modelTransform.setObject(@scope, 'us') - if @scope.us.neighbors.previous?.ref? - ctx = { - project: @scope.project.slug - ref: @scope.us.neighbors.previous.ref - } - @scope.previousUrl = @navUrls.resolve("project-userstories-detail", ctx) - - if @scope.us.neighbors.next?.ref? - ctx = { - project: @scope.project.slug - ref: @scope.us.neighbors.next.ref - } - @scope.nextUrl = @navUrls.resolve("project-userstories-detail", ctx) - return us loadSprint: -> @@ -330,7 +317,7 @@ UsStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTransfor $el.html(html) - $compile($el.contents())($scope); + $compile($el.contents())($scope) save = (status) => $el.find(".pop-status").popover().close() diff --git a/app/coffee/modules/wiki/main.coffee b/app/coffee/modules/wiki/main.coffee index de86ead3..b192b09a 100644 --- a/app/coffee/modules/wiki/main.coffee +++ b/app/coffee/modules/wiki/main.coffee @@ -51,11 +51,13 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "tgAppMetaService", "$tgNavUrls", "$tgAnalytics", - "$translate" + "$translate", + "tgErrorHandlingService" ] constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, @location, - @filter, @log, @appMetaService, @navUrls, @analytics, @translate) -> + @filter, @log, @appMetaService, @navUrls, @analytics, @translate, @errorHandlingService) -> + @scope.$on("wiki:links:move", @.moveLink) @scope.projectSlug = @params.pslug @scope.wikiSlug = @params.slug @scope.wikiTitle = @scope.wikiSlug @@ -86,7 +88,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.is_wiki_activated - @location.path(@navUrls.resolve("permission-denied")) + @errorHandlingService.permissionDenied() @scope.projectId = project.id @scope.project = project @@ -155,6 +157,16 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @repo.remove(@scope.wiki).then onSuccess, onError + moveLink: (ctx, item, itemIndex) => + values = @scope.wikiLinks + r = values.indexOf(item) + values.splice(r, 1) + values.splice(itemIndex, 0, item) + _.each values, (value, index) -> + value.order = index + + @repo.saveAll(values) + module.controller("WikiDetailController", WikiDetailController) @@ -162,7 +174,7 @@ module.controller("WikiDetailController", WikiDetailController) ## Wiki Summary Directive ############################################################################# -WikiSummaryDirective = ($log, $template, $compile, $translate) -> +WikiSummaryDirective = ($log, $template, $compile, $translate, avatarService) -> template = $template.get("wiki/wiki-summary.html", true) link = ($scope, $el, $attrs, $model) -> @@ -172,10 +184,12 @@ WikiSummaryDirective = ($log, $template, $compile, $translate) -> else user = $scope.usersById[wiki.last_modifier] + avatar = avatarService.getAvatar(user) + if user is undefined - user = {name: "unknown", imgUrl: "/" + window._version + "/images/user-noimage.png"} + user = {name: "unknown", avatar: avatar} else - user = {name: user.full_name_display, imgUrl: user.photo} + user = {name: user.full_name_display, avatar: avatar} ctx = { totalEditions: wiki.editions @@ -199,14 +213,15 @@ WikiSummaryDirective = ($log, $template, $compile, $translate) -> require: "ngModel" } -module.directive("tgWikiSummary", ["$log", "$tgTemplate", "$compile", "$translate", WikiSummaryDirective]) +module.directive("tgWikiSummary", ["$log", "$tgTemplate", "$compile", "$translate", "tgAvatarService", WikiSummaryDirective]) ############################################################################# ## Editable Wiki Content Directive ############################################################################# -EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $analytics, $qqueue, $translate) -> +EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $analytics, $qqueue, $translate, + $wikiHistoryService) -> link = ($scope, $el, $attrs, $model) -> isEditable = -> return $scope.project.my_permissions.indexOf("modify_wiki_page") != -1 @@ -228,7 +243,6 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ return if not $model.$modelValue.id $model.$modelValue.revert() - switchToReadMode() getSelectedText = -> @@ -245,6 +259,7 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ $model.$setViewValue wikiPage.clone() + $wikiHistoryService.loadHistoryEntries() $confirm.notify("success") switchToReadMode() @@ -252,8 +267,7 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ $confirm.notify("error") currentLoading = $loading() - .removeClasses("icon-floppy") - .target($el.find('.icon-floppy')) + .target($el.find('.save')) .start() if wiki.id? @@ -322,4 +336,5 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ } module.directive("tgEditableWikiContent", ["$window", "$document", "$tgRepo", "$tgConfirm", "$tgLoading", - "$tgAnalytics", "$tgQqueue", "$translate", EditableWikiContentDirective]) + "$tgAnalytics", "$tgQqueue", "$translate", "tgWikiHistoryService", + EditableWikiContentDirective]) diff --git a/app/coffee/modules/wiki/nav.coffee b/app/coffee/modules/wiki/nav.coffee index 31ecf4ab..5b6bb01b 100644 --- a/app/coffee/modules/wiki/nav.coffee +++ b/app/coffee/modules/wiki/nav.coffee @@ -38,12 +38,16 @@ module = angular.module("taigaWiki") WikiNavDirective = ($tgrepo, $log, $location, $confirm, $analytics, $loading, $template, $compile, $translate) -> template = $template.get("wiki/wiki-nav.html", true) - link = ($scope, $el, $attrs) -> + + linkWikiLinks = ($scope, $el, $attrs) -> $ctrl = $el.controller() if not $attrs.ngModel? return $log.error "WikiNavDirective: no ng-model attr is defined" + addWikiLinkPermission = $scope.project.my_permissions.indexOf("add_wiki_link") > -1 + drake = null + render = (wikiLinks) -> addWikiLinkPermission = $scope.project.my_permissions.indexOf("add_wiki_link") > -1 deleteWikiLinkPermission = $scope.project.my_permissions.indexOf("delete_wiki_link") > -1 @@ -58,8 +62,37 @@ WikiNavDirective = ($tgrepo, $log, $location, $confirm, $analytics, $loading, $t html = $compile(html)($scope) $el.off() + if addWikiLinkPermission and drake + drake.destroy() + $el.html(html) + if addWikiLinkPermission + itemEl = null + tdom = $el.find(".sortable") + + drake = dragula([tdom[0]], { + direction: 'vertical', + copySortSource: false, + copy: false, + mirrorContainer: tdom[0], + moves: (item) -> return $(item).is('li') + }) + + drake.on 'dragend', (item) -> + itemEl = $(item) + item = itemEl.scope().link + itemIndex = itemEl.index() + $scope.$emit("wiki:links:move", item, itemIndex) + + scroll = autoScroll(window, { + margin: 20, + pixels: 30, + scrollWhenOutside: true, + autoScroll: () -> + return this.down && drake.dragging + }) + $el.on "click", ".add-button", (event) -> event.preventDefault() $el.find(".new").removeClass("hidden") @@ -130,9 +163,14 @@ WikiNavDirective = ($tgrepo, $log, $location, $confirm, $analytics, $loading, $t $el.find(".new input").val('') $el.find(".add-button").show() - bindOnce($scope, $attrs.ngModel, render) + link = ($scope, $el, $attrs) -> + linkWikiLinks($scope, $el, $attrs) + + $scope.$on "$destroy", -> + $el.off() + return {link:link} module.directive("tgWikiNav", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgAnalytics", diff --git a/app/coffee/modules/wiki/pages-list.coffee b/app/coffee/modules/wiki/pages-list.coffee new file mode 100644 index 00000000..5aaddb8c --- /dev/null +++ b/app/coffee/modules/wiki/pages-list.coffee @@ -0,0 +1,100 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/wiki/pages-list.coffee +### + +taiga = @.taiga + +mixOf = @.taiga.mixOf + +module = angular.module("taigaWiki") + +############################################################################# +## Wiki Pages List Controller +############################################################################# + +class WikiPagesListController extends mixOf(taiga.Controller, taiga.PageMixin) + @.$inject = [ + "$scope", + "$rootScope", + "$tgRepo", + "$tgModel", + "$tgConfirm", + "$tgResources", + "$routeParams", + "$q", + "$tgNavUrls", + "tgErrorHandlingService" + ] + + constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, + @navUrls, @errorHandlingService) -> + @scope.projectSlug = @params.pslug + @scope.wikiSlug = @params.slug + @scope.wikiTitle = @scope.wikiSlug + @scope.sectionName = "Wiki" + @scope.linksVisible = false + + promise = @.loadInitialData() + + # On Error + promise.then null, @.onInitialDataError.bind(@) + + loadProject: -> + return @rs.projects.getBySlug(@params.pslug).then (project) => + if not project.is_wiki_activated + @errorHandlingService.permissionDenied() + + @scope.projectId = project.id + @scope.project = project + @scope.$emit('project:loaded', project) + return project + + loadWikiPages: -> + promise = @rs.wiki.list(@scope.projectId).then (wikipages) => + @scope.wikipages = wikipages + + loadWikiLinks: -> + return @rs.wiki.listLinks(@scope.projectId).then (wikiLinks) => + @scope.wikiLinks = wikiLinks + + for link in @scope.wikiLinks + link.url = @navUrls.resolve("project-wiki-page", { + project: @scope.projectSlug + slug: link.href + }) + + selectedWikiLink = _.find(wikiLinks, {href: @scope.wikiSlug}) + @scope.wikiTitle = selectedWikiLink.title if selectedWikiLink? + + loadInitialData: -> + promise = @.loadProject() + return promise.then (project) => + @.fillUsersAndRoles(project.members, project.roles) + @q.all([@.loadWikiLinks(), @.loadWikiPages()]).then @.checkLinksPerms.bind(this) + + checkLinksPerms: -> + if @scope.project.my_permissions.indexOf("add_wiki_link") != -1 || + (@scope.project.my_permissions.indexOf("view_wiki_links") != -1 && @scope.wikiLinks.length) + @scope.linksVisible = true + +module.controller("WikiPagesListController", WikiPagesListController) diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee index a8ec6979..a2d02c99 100644 --- a/app/coffee/utils.coffee +++ b/app/coffee/utils.coffee @@ -28,21 +28,24 @@ addClass = (el, className) -> else el.className += ' ' + className + nl2br = (str) => breakTag = '
' return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + breakTag + '$2') + bindMethods = (object) => dependencies = _.keys(object) methods = [] _.forIn object, (value, key) => - if key not in dependencies + if key not in dependencies && _.isFunction(value) methods.push(key) _.bindAll(object, methods) + bindOnce = (scope, attr, continuation) => val = scope.$eval(attr) if val != undefined @@ -75,6 +78,7 @@ slugify = (data) -> .replace(/[^\w\-]+/g, '') .replace(/\-\-+/g, '-') + unslugify = (data) -> if data return _.capitalize(data.replace(/-/g, ' ')) @@ -165,6 +169,7 @@ sizeFormat = (input, precision=1) -> size = (input / Math.pow(1024, number)).toFixed(precision) return "#{size} #{units[number]}" + stripTags = (str, exception) -> if exception pattern = new RegExp('<(?!' + exception + '\s*\/?)[^>]+>', 'gi') @@ -172,6 +177,7 @@ stripTags = (str, exception) -> else return String(str).replace(/<\/?[^>]+>/g, '') + replaceTags = (str, tags, replace) -> # open tag pattern = new RegExp('<(' + tags + ')>', 'gi') @@ -183,6 +189,7 @@ replaceTags = (str, tags, replace) -> return str + defineImmutableProperty = (obj, name, fn) => Object.defineProperty obj, name, { get: () => @@ -197,6 +204,7 @@ defineImmutableProperty = (obj, name, fn) => return fn_result } + _.mixin removeKeys: (obj, keys) -> _.chain([keys]).flatten().reduce( @@ -211,10 +219,14 @@ _.mixin , [ [] ]) - isImage = (name) -> return name.match(/\.(jpe?g|png|gif|gifv|webm)/i) != null + +isPdf = (name) -> + return name.match(/\.(pdf)/i) != null + + patch = (oldImmutable, newImmutable) -> pathObj = {} @@ -227,6 +239,18 @@ patch = (oldImmutable, newImmutable) -> return pathObj +DEFAULT_COLOR_LIST = [ + '#fce94f', '#edd400', '#c4a000', '#8ae234', '#73d216', '#4e9a06', '#d3d7cf', + '#fcaf3e', '#f57900', '#ce5c00', '#729fcf', '#3465a4', '#204a87', '#888a85', + '#ad7fa8', '#75507b', '#5c3566', '#ef2929', '#cc0000', '#a40000', '#222222' +] + +getRandomDefaultColor = () -> + return _.sample(DEFAULT_COLOR_LIST) + +getDefaulColorList = () -> + return _.clone(DEFAULT_COLOR_LIST) + taiga = @.taiga taiga.addClass = addClass taiga.nl2br = nl2br @@ -252,4 +276,7 @@ taiga.stripTags = stripTags taiga.replaceTags = replaceTags taiga.defineImmutableProperty = defineImmutableProperty taiga.isImage = isImage +taiga.isPdf = isPdf taiga.patch = patch +taiga.getRandomDefaultColor = getRandomDefaultColor +taiga.getDefaulColorList = getDefaulColorList diff --git a/app/images/backlog-empty.png b/app/images/backlog-empty.png deleted file mode 100644 index a7179c86..00000000 Binary files a/app/images/backlog-empty.png and /dev/null differ diff --git a/app/images/empty/empty_contact.png b/app/images/empty/empty_contact.png new file mode 100644 index 00000000..2868e3bb Binary files /dev/null and b/app/images/empty/empty_contact.png differ diff --git a/app/images/empty/empty_des.png b/app/images/empty/empty_des.png new file mode 100644 index 00000000..7600162b Binary files /dev/null and b/app/images/empty/empty_des.png differ diff --git a/app/images/empty/empty_field.png b/app/images/empty/empty_field.png new file mode 100644 index 00000000..ba627ce7 Binary files /dev/null and b/app/images/empty/empty_field.png differ diff --git a/app/images/empty/empty_like.png b/app/images/empty/empty_like.png new file mode 100644 index 00000000..4d0e7967 Binary files /dev/null and b/app/images/empty/empty_like.png differ diff --git a/app/images/empty/empty_mex.png b/app/images/empty/empty_mex.png new file mode 100644 index 00000000..1800ee7c Binary files /dev/null and b/app/images/empty/empty_mex.png differ diff --git a/app/images/empty/empty_moon.png b/app/images/empty/empty_moon.png new file mode 100644 index 00000000..376f7510 Binary files /dev/null and b/app/images/empty/empty_moon.png differ diff --git a/app/images/empty/empty_sprint.png b/app/images/empty/empty_sprint.png new file mode 100644 index 00000000..808e0663 Binary files /dev/null and b/app/images/empty/empty_sprint.png differ diff --git a/app/images/empty/empty_tex.png b/app/images/empty/empty_tex.png new file mode 100644 index 00000000..02330ecb Binary files /dev/null and b/app/images/empty/empty_tex.png differ diff --git a/app/images/empty/empty_upvote.png b/app/images/empty/empty_upvote.png new file mode 100644 index 00000000..fbd66f24 Binary files /dev/null and b/app/images/empty/empty_upvote.png differ diff --git a/app/images/empty/empty_watch.png b/app/images/empty/empty_watch.png new file mode 100644 index 00000000..e69c543a Binary files /dev/null and b/app/images/empty/empty_watch.png differ diff --git a/app/images/epics-empty.png b/app/images/epics-empty.png new file mode 100644 index 00000000..28363733 Binary files /dev/null and b/app/images/epics-empty.png differ diff --git a/app/images/issues-empty.png b/app/images/issues-empty.png deleted file mode 100644 index 4668e027..00000000 Binary files a/app/images/issues-empty.png and /dev/null differ diff --git a/app/images/search-empty.png b/app/images/search-empty.png deleted file mode 100644 index d95477dc..00000000 Binary files a/app/images/search-empty.png and /dev/null differ diff --git a/app/images/sprint-empty.png b/app/images/sprint-empty.png deleted file mode 100644 index e51329a4..00000000 Binary files a/app/images/sprint-empty.png and /dev/null differ diff --git a/app/images/user-avatars/user-avatar-01.png b/app/images/user-avatars/user-avatar-01.png new file mode 100644 index 00000000..6695e8f6 Binary files /dev/null and b/app/images/user-avatars/user-avatar-01.png differ diff --git a/app/images/user-avatars/user-avatar-02.png b/app/images/user-avatars/user-avatar-02.png new file mode 100644 index 00000000..16034e35 Binary files /dev/null and b/app/images/user-avatars/user-avatar-02.png differ diff --git a/app/images/user-avatars/user-avatar-03.png b/app/images/user-avatars/user-avatar-03.png new file mode 100644 index 00000000..3fc9a4a4 Binary files /dev/null and b/app/images/user-avatars/user-avatar-03.png differ diff --git a/app/images/user-avatars/user-avatar-04.png b/app/images/user-avatars/user-avatar-04.png new file mode 100644 index 00000000..4c456402 Binary files /dev/null and b/app/images/user-avatars/user-avatar-04.png differ diff --git a/app/images/user-avatars/user-avatar-05.png b/app/images/user-avatars/user-avatar-05.png new file mode 100644 index 00000000..265f94f0 Binary files /dev/null and b/app/images/user-avatars/user-avatar-05.png differ diff --git a/app/index.jade b/app/index.jade index 66d7d234..7ba7bb63 100644 --- a/app/index.jade +++ b/app/index.jade @@ -20,9 +20,14 @@ html(lang="en") window.prerenderReady = false; body(tg-main) - div(tg-navigation-bar) + div(tg-navigation-bar, ng-if="!errorHandling.showingError") + div(ng-if="!errorHandling.showingError") + div.master(ng-view) - div.master(ng-view) + div(ng-if="errorHandling.notfound", ng-include="'error/not-found.html'") + div(ng-if="errorHandling.error", ng-include="'error/error.html'") + div(ng-if="errorHandling.permissionDenied", ng-include="'error/permission-denied.html'") + div(ng-if="errorHandling.blocked", ng-include="'projects/project/blocked-project.html'") div.lightbox.lightbox-generic-ask include partials/includes/modules/lightbox-generic-ask diff --git a/app/js/dom-autoscroller.js b/app/js/dom-autoscroller.js index 08831141..535eb2a6 100644 --- a/app/js/dom-autoscroller.js +++ b/app/js/dom-autoscroller.js @@ -464,13 +464,70 @@ }; + var createPointCB = function createPointCB(object){ + // A persistent object (as opposed to returned object) is used to save memory + // This is good to prevent layout thrashing, or for games, and such + + // NOTE + // This uses IE fixes which should be OK to remove some day. :) + // Some speed will be gained by removal of these. + + // pointCB should be saved in a variable on return + // This allows the usage of element.removeEventListener + + return function pointCB(event){ + + event = event || window.event; // IE-ism + object.target = event.target || event.srcElement || event.originalTarget; + object.element = this; + object.type = event.type; + + // Support touch + // http://www.creativebloq.com/javascript/make-your-site-work-touch-devices-51411644 + + if(event.targetTouches){ + object.x = event.targetTouches[0].clientX; + object.y = event.targetTouches[0].clientY; + object.pageX = event.pageX; + object.pageY = event.pageY; + }else{ + + // If pageX/Y aren't available and clientX/Y are, + // calculate pageX/Y - logic taken from jQuery. + // (This is to support old IE) + // NOTE Hopefully this can be removed soon. + + if (event.pageX === null && event.clientX !== null) { + var eventDoc = (event.target && event.target.ownerDocument) || document; + var doc = eventDoc.documentElement; + var body = eventDoc.body; + + object.pageX = event.clientX + + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - + (doc && doc.clientLeft || body && body.clientLeft || 0); + object.pageY = event.clientY + + (doc && doc.scrollTop || body && body.scrollTop || 0) - + (doc && doc.clientTop || body && body.clientTop || 0 ); + }else{ + object.pageX = event.pageX; + object.pageY = event.pageY; + } + + // pageX, and pageY change with page scroll + // so we're not going to use those for x, and y. + // NOTE Most browsers also alias clientX/Y with x/y + // so that's something to consider down the road. + + object.x = event.clientX; + object.y = event.clientY; + } + + }; + + //NOTE Remember accessibility, Aria roles, and labels. + }; // Autscroller - - function AutoScrollerFactory(element, options){ - return new AutoScroller(element, options); - } - function AutoScroller(elements, options){ var self = this, pixels = 2; options = options || {}; @@ -479,7 +536,10 @@ this.scrolling = false; this.scrollWhenOutside = options.scrollWhenOutside || false; - this.point = pointer(elements); + var point = {}, pointCB = createPointCB(point), down = false; + + window.addEventListener('mousemove', pointCB, false); + window.addEventListener('touchmove', pointCB, false); if(!isNaN(options.pixels)){ pixels = options.pixels; @@ -494,12 +554,30 @@ } this.destroy = function() { - this.point.destroy(); + window.removeEventListener('mousemove', pointCB, false); + window.removeEventListener('touchmove', pointCB, false); + window.removeEventListener('mousedown', onDown, false); + window.removeEventListener('touchstart', onDown, false); + window.removeEventListener('mouseup', onUp, false); + window.removeEventListener('touchend', onUp, false); }; + var hasWindow = null, temp = []; + for(var i=0; i rect.bottom - self.margin){ + }else if(point.y > rect.bottom - self.margin){ autoScrollV(el, 1, rect); } - if(self.point.x < rect.left + self.margin){ + if(point.x < rect.left + self.margin){ autoScrollH(el, -1, rect); - }else if(self.point.x > rect.right - self.margin){ + }else if(point.x > rect.right - self.margin){ autoScrollH(el, 1, rect); } - }); + } + + function autoScrollV(el, amount, rect){ - //if(!self.down) return; + if(!self.autoScroll()) return; - if(!self.scrollWhenOutside && self.point.outside(el)) return; + if(!self.scrollWhenOutside && !inside(point, el, rect)) return; + if(el === window){ window.scrollTo(el.pageXOffset, el.pageYOffset + amount); }else{ + el.scrollTop = el.scrollTop + amount; } setTimeout(function(){ - if(self.point.y < rect.top + self.margin){ + if(point.y < rect.top + self.margin){ autoScrollV(el, amount, rect); - }else if(self.point.y > rect.bottom - self.margin){ + }else if(point.y > rect.bottom - self.margin){ autoScrollV(el, amount, rect); } }, self.interval); } function autoScrollH(el, amount, rect){ - //if(!self.down) return; + if(!self.autoScroll()) return; - if(!self.scrollWhenOutside && self.point.outside(el)) return; + if(!self.scrollWhenOutside && !inside(point, el, rect)) return; + if(el === window){ window.scrollTo(el.pageXOffset + amount, el.pageYOffset); }else{ @@ -559,9 +694,9 @@ } setTimeout(function(){ - if(self.point.x < rect.left + self.margin){ + if(point.x < rect.left + self.margin){ autoScrollH(el, amount, rect); - }else if(self.point.x > rect.right - self.margin){ + }else if(point.x > rect.right - self.margin){ autoScrollH(el, amount, rect); } }, self.interval); @@ -569,5 +704,36 @@ } + function getRect(el){ + if(el === window){ + return { + top: 0, + left: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight + }; + + }else{ + try{ + return el.getBoundingClientRect(); + }catch(e){ + throw new TypeError("Can't call getBoundingClientRect on "+el); + } + + } + } + + function inside(point, el, rect){ + rect = rect || getRect(el); + return (point.y > rect.top && point.y < rect.bottom && + point.x > rect.left && point.x < rect.right); + } + + function AutoScrollerFactory(element, options){ + return new AutoScroller(element, options); + } + window.autoScroll = AutoScrollerFactory; }()); diff --git a/app/js/dragula-drag-multiple.js b/app/js/dragula-drag-multiple.js index 1f7c521a..dba5e387 100644 --- a/app/js/dragula-drag-multiple.js +++ b/app/js/dragula-drag-multiple.js @@ -2,6 +2,7 @@ var multipleSortableClass = 'ui-multisortable-multiple'; var mainClass = 'main-drag-item'; var inProgress = false; + var removeEventFn = null; var reset = function(elm) { $(elm) @@ -59,7 +60,7 @@ var current = dragMultiple.items.elm; var container = dragMultiple.items.container; - $(window).off('mousemove.dragmultiple'); + document.documentElement.removeEventListener('mousemove', removeEventFn); // reset dragMultiple.items = {}; @@ -199,12 +200,14 @@ dragMultiple.start = function(item, container) { if (isMultiple(item, container)) { - $(window).on('mousemove.dragmultiple', function() { + document.documentElement.addEventListener('mousemove', function() { if (!inProgress) { dragMultiple.prepare(item, container); } drag(); + + removeEventFn = arguments.callee; }); } }; diff --git a/app/locales/taiga/locale-ca.json b/app/locales/taiga/locale-ca.json index 5645a9b8..faa9bfb7 100644 --- a/app/locales/taiga/locale-ca.json +++ b/app/locales/taiga/locale-ca.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "In item per línia", "NEW_BULK": "Nova inserció en grup", "RELATED_TASKS": "Tasques relacionades", + "PREVIOUS": "Previous", + "NEXT": "Següent", "LOGOUT": "Surt", "EXTERNAL_USER": "un usuari extern", "GENERIC_ERROR": "Un Oompa Loompas diu {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Aquest valor pareix invàlid.", "TYPE_EMAIL": "Deu ser un correu vàlid.", @@ -115,8 +122,9 @@ "USER_STORY": "Història d'usuari", "TASK": "Tasca", "ISSUE": "incidència", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Afegir tag", + "PLACEHOLDER": "Enter tag", "DELETE": "Elimina l'etiqueta", "ADD": "Afegeix l'etiqueta" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "filtres", + "TITLE": "Filtres", "INPUT_PLACEHOLDER": "Descripció o referència", "TITLE_ACTION_FILTER_BUTTON": "cerca", - "BREADCRUMB_TITLE": "tornar a categories", - "BREADCRUMB_FILTERS": "Filtres", - "BREADCRUMB_STATUS": "estats" + "INPUT_SEARCH_PLACEHOLDER": "Descripció o ref", + "TITLE_ACTION_SEARCH": "Cerca", + "ACTION_SAVE_CUSTOM_FILTER": "Guarda com a filtre", + "PLACEHOLDER_FILTER_NAME": "Escriu el filtre i pressiona Intro", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tipus", + "STATUS": "Estats", + "SEVERITY": "Severitat", + "PRIORITIES": "Prioritats", + "TAGS": "Etiquetes", + "ASSIGNED_TO": "Assignat a", + "CREATED_BY": "Creat per", + "CUSTOM_FILTERS": "Filtres personalitzats", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Esborrar filtre", + "MESSAGE": "el filtre '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Capçcalera de primer nivel", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Previsualitzar", "EDIT_BUTTON": "Editar", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Ajuda de Markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Vore sprints", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Vore istòries d'usuari", "ADD_USER_STORIES": "Afegir històries d'usuari", "MODIFY_USER_STORIES": "Editar història d'usuari", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Esborrar històries d'usuari" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Vore tasca", "ADD_TASKS": "Afegit tasques", "MODIFY_TASKS": "Modificar tasques", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Esborrar tasques" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Vore incidències", "ADD_ISSUES": "Afegeix incidències", "MODIFY_ISSUES": "Modifica incidències", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Elimina incidències" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Observant", "DASHBOARD": "Panell principal" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Sense assignar" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Vots", + "NAME": "Nom", + "PROJECT": "Projecte", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Estats", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Bloquejat", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Els meus projectes - Taiga", "PAGE_DESCRIPTION": "Una llista de tots els teus projects, que pots reordenar o crear nous.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Editar valor", - "TITLE_ACTION_DELETE_VALUE": "Borrar valor" + "TITLE_ACTION_DELETE_VALUE": "Borrar valor", + "TITLE_ACTION_DELETE_TAG": "Elimina l'etiqueta" }, "HELP": "Necessites ajuda? Mira la nosta pàgina de suport!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Mòdules", "ENABLE": "Activa", "DISABLE": "Desactiva", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Organitza les històries d'usuari per a mantindre una vista organitzada i prioritzada del treball.", "NUMBER_SPRINTS": "Expected number of sprints", @@ -471,9 +546,9 @@ "PRIVATE_PROJECT": "Projecte privat", "PRIVATE_OR_PUBLIC": "What's the difference between public and private projects?", "DELETE": "Esborra aquest projecte", - "LOGO_HELP": "The image will be scaled to 80x80px.", + "LOGO_HELP": "S'escalarà la imatge a 80x80px.", "CHANGE_LOGO": "Change logo", - "ACTION_USE_DEFAULT_LOGO": "Use default image", + "ACTION_USE_DEFAULT_LOGO": "Utilitza la imatge per defecte", "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects allowed by your current plan", "MAX_PRIVATE_PROJECTS_MEMBERS": "The maximum number of members for private projects has been exceeded", "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects allowed by your current plan", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Vas a canviar la URL d'accés al CSV. La URL previa no funcionarà. Estàs segur?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "informes d'històries d'usuari", "SECTION_TITLE_TASK": "infome de tasques", "SECTION_TITLE_ISSUE": "informe d'incidències", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Camps personalitzats", "SUBTITLE": "Especifiqueu els camps personalitzats del les vostres històries d'usuari, tasques i incidències", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Camps personalitzats d'històries d'usuari", "US_ADD": "Afegeix camps personalitzats en històries d'usuari", "TASK_DESCRIPTION": "Camps personalitzats de tasques", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Estat", "SUBTITLE": "Especifica els estats de les vostres històries d'usuari, tasques i incidències", - "US_TITLE": "Estats d'US", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Estats de tasques", "ISSUE_TITLE": "Estats d'incidències" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Tipus d'incidències", "ACTION_ADD": "Afegir now {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Etiquetes", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Afegeix l'etiqueta", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Rols - {{projectName}}", "WARNING_NO_ROLE": "Ves amb compte, cap rol en el teu projecte pot estimar punts per a les històries d'usuari", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "la invitació a '{{email}}'." }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Valor per defecte per a selector de punts", - "LABEL_US": "Valor per defecte per a selector d'estats d'US", "LABEL_TASK_STATUS": "Valor per defecte per a selector d'estats de tasques", - "LABEL_PRIORITY": "Valor per defecte per a selector de prioritat", - "LABEL_SEVERITY": "Valor per defecte per a selector de severitat", "LABEL_ISSUE_TYPE": "Valor per defecte per a selector de tipus", - "LABEL_ISSUE_STATUS": "Valor per defecte per a selector de estats" + "LABEL_ISSUE_STATUS": "Valor per defecte per a selector de estats", + "LABEL_PRIORITY": "Valor per defecte per a selector de prioritat", + "LABEL_SEVERITY": "Valor per defecte per a selector de severitat" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Escriu un nom per a nou estat" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioritats", "SEVERITIES": "severitats", "TYPES": "Tipus", - "CUSTOM_FIELDS": "Camps personalitzats" + "CUSTOM_FIELDS": "Camps personalitzats", + "TAGS": "Etiquetes" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Perfil de projecte" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Mostrar tot", "FILTER_TYPE_PROJECTS": "Projectes", "FILTER_TYPE_PROJECT_TITLES": "Mostra només projectes", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Históries", "FILTER_TYPE_USER_STORIES_TITLES": "Veure només històries d'usuari", "FILTER_TYPE_TASKS": "Tasques", @@ -821,7 +919,7 @@ "CHANGE_PASSWORD": "Canvi de contrasenya", "DASHBOARD_TITLE": "Tauler", "DISCOVER_TITLE": "Discover trending projects", - "NEW_ITEM": "New", + "NEW_ITEM": "Nova", "DISCOVER": "Descobreix", "ACTION_REORDER": "Arrossega els elements per endreçar" }, @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Opcional) Afegix un text personalizat a la invitació. Dis-li algo divertit als nous membres. ;-)", "PLACEHOLDER_TYPE_EMAIL": "Escriu un correu", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nova història d'usuari", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Descripció", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Història d'Usuari {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estat: {{userStoryStatus }}. Completat {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} de {{userStoryTotalTasks}} tasques tancades). Punts: {{userStoryPoints}}. Descripció: {{userStoryDescription}}", - "SECTION_NAME": "Detalls de la història d'usuari", + "SECTION_NAME": "Història d'usuari", "LINK_TASKBOARD": "Panell de tasques", "TITLE_LINK_TASKBOARD": "Anar a panell de tasques", "TOTAL_POINTS": "punts totals", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Aquesta US ha sigut creada desde", "GO_TO_EXTERNAL_REFERENCE": "Anar a l'orige", "BLOCKED": "Aquest història d'usuari està bloquejada", - "PREVIOUS": "previa història d'usuari", - "NEXT": "Pròxima història d'usuari", "TITLE_DELETE_ACTION": "Esborra història d'usuari", "LIGHTBOX_TITLE_BLOKING_US": "Bloquejant US", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tasques completades", "ASSIGN": "Assigna història d'usuari", "NOT_ESTIMATED": "Sense estimar", "TOTAL_US_POINTS": "Punts totals d'US", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Requeriment d'equip", "CLIENT_REQUIREMENT": "Requeriment de client", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Comentari esborrat per {{user}} el {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Comentaris", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Comentar", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Escriu un nou comentari ací", "SHOW_DELETED": "Mostra el comentari esborrat.", "HIDE_DELETED": "Amaga el comentari esborrat", "DELETE": "Esborrar comentari", - "RESTORE": "Resturar comentari." + "RESTORE": "Resturar comentari.", + "HISTORY": { + "TITLE": "Activitat" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Mostrar activitat", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Mostrar activitat anterior ({{showMore}} més)", "TITLE": "Activitat", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "Borrat", "ADDED": "Afegit", - "US_POINTS": "Punts d'US ({{name}})", - "NEW_ATTACHMENT": "Nou adjunt", - "DELETED_ATTACHMENT": "Adjunts esborrats", - "UPDATED_ATTACHMENT": "Actualitzat adjunt {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "Esborrar camps personalitzat", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Fet {size, plural, one{un canvi} other{# changes}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Requeriment d'equip", + "CLIENT_REQUIREMENT": "Requeriment de client", + "BLOCKED": "Bloquejat", "VALUES": { "YES": "si", "NO": "no", @@ -1052,12 +1198,14 @@ "TAGS": "Etiquetes", "ATTACHMENTS": "adjunts", "IS_DEPRECATED": "és obsolet", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "ordre", "BACKLOG_ORDER": "ordre de backlog", "SPRINT_ORDER": "ordre d'sprint", "KANBAN_ORDER": "ordre de kanban", "TASKBOARD_ORDER": "ordre de panell de tasques", - "US_ORDER": "ordre d'US" + "US_ORDER": "ordre d'US", + "COLOR": "color" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "tasques
tancades", "IOCAINE_DOSES": "dosis
iocaína", "SHOW_STATISTICS_TITLE": "Mostrar estadístiques", - "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "punts
projecte", @@ -1122,9 +1271,7 @@ "TITLE": "Filtres", "REMOVE": "Esborra filtres", "HIDE": "Amaga filtres", - "SHOW": "Mostra filtres", - "FILTER_CATEGORY_STATUS": "Estats", - "FILTER_CATEGORY_TAGS": "Etiquetes" + "SHOW": "Mostra filtres" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Tasca {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estat: {{taskStatus }}. Descripció: {{taskDescription}}", - "SECTION_NAME": "Detalls de la tasca", + "SECTION_NAME": "Tasca", "LINK_TASKBOARD": "Panell de tasques", "TITLE_LINK_TASKBOARD": "Anar a panell de tasques", "PLACEHOLDER_SUBJECT": "Afegix la descripció de la tasca", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Aquesta tasca ha sigut creada desde", "TITLE_LINK_GO_ORIGIN": "Anar a història d'usuari", "BLOCKED": "Aquesta tasca està bloquejada", - "PREVIOUS": "tasca prèvia", - "NEXT": "pròxima tasca", "TITLE_DELETE_ACTION": "Esborrar tasca", "LIGHTBOX_TITLE_BLOKING_TASK": "Bloquejant tasca", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Incidències - {{projectName}}", "PAGE_DESCRIPTION": "El panell d'incidències de {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Incidències", - "SECTION_NAME": "Detalls d'incidència", + "SECTION_NAME": "incidència", "ACTION_NEW_ISSUE": "+ NOVA INCIDÈNCIA", "ACTION_PROMOTE_TO_US": "Promocionar història d'usuari", - "PLACEHOLDER_FILTER_NAME": "Escriu el filtre i pressiona Intro", "PROMOTED": "Esta incidència ha sigut promcionada a US:", "EXTERNAL_REFERENCE": "Esta incidència ha sigut creada desde", "GO_TO_EXTERNAL_REFERENCE": "Anar a l'orige", "BLOCKED": "Aquesta incidència està bloquejada", - "TITLE_PREVIOUS_ISSUE": "incidència prèvia", - "TITLE_NEXT_ISSUE": "pròxima incidència", "ACTION_DELETE": "Esborrar incidència", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Bloquejant incidència", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Promociona aquesta incidència a història d'usuari", "MESSAGE": "Segur que vols crear una nova US desde aquesta incidència" }, - "FILTERS": { - "TITLE": "Filtres", - "INPUT_SEARCH_PLACEHOLDER": "Descripció o ref", - "TITLE_ACTION_SEARCH": "Busca", - "ACTION_SAVE_CUSTOM_FILTER": "Guarda com a filtre", - "BREADCRUMB": "Filtres", - "TITLE_BREADCRUMB": "Filtres", - "CATEGORIES": { - "TYPE": "Tipus", - "STATUS": "Estats", - "SEVERITY": "Severitat", - "PRIORITIES": "Prioritats", - "TAGS": "Etiquetes", - "ASSIGNED_TO": "Assignat a", - "CREATED_BY": "Creat per", - "CUSTOM_FILTERS": "Filtres personalitzats" - }, - "CONFIRM_DELETE": { - "TITLE": "Esborrar filtre", - "MESSAGE": "el filtre '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tipus", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Cerca - {{projectName}}", "PAGE_DESCRIPTION": "Busca qualsevol cosa al projecte {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Històries d'usuari", "FILTER_ISSUES": "Incidències", "FILTER_TASKS": "Tasca", @@ -1375,9 +1496,9 @@ } }, "USER_PROFILE": { - "IMAGE_HELP": "The image will be scaled to 80x80px.", + "IMAGE_HELP": "S'escalarà la imatge a 80x80px.", "ACTION_CHANGE_IMAGE": "Canviar", - "ACTION_USE_GRAVATAR": "Use default image", + "ACTION_USE_GRAVATAR": "Utilitza la imatge per defecte", "ACTION_DELETE_ACCOUNT": "Esborrar compte de Taiga", "CHANGE_EMAIL_SUCCESS": "Mira el teu correu!
Hem enviat un correu al teu conter
amb les instrucciones per a escriure una nova adreça de correu", "CHANGE_PHOTO": "Canviar foto", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Esborrar pàgina de Wiki", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Enllaços", - "ACTION_ADD_LINK": "Afegir link" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "voltes
editat", "LAST_EDIT": "última
edició", "LAST_MODIFICATION": "última modificació" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Creat", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} ha creat una nova tasca {{obj_name}} a {{project_name}} provinent de US {{us_name}}", "WIKI_CREATED": "{{username}} has created a new wiki page {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} has created a new sprint {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} ha creat el projecte {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} ha actualitzat l'atribut \"{{field_name}}\" de la tasca {{obj_name}} de la història d'usuari {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha actualitzat l'atribut \"{{field_name}}\" de la tasca {{obj_name}} de la història d'usuari {{us_name}} amb el valor {{new_value}}", "WIKI_UPDATED": "{{username}} ha actualitzat la pàgina wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} ha comentat la història d'usuari {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} ha comentat la incidència {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} ha comentat la tasca {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} te un nou membre", "US_ADDED_MILESTONE": "{{username}} ha afegit la història d'usuari {{obj_name}} a {{sprint_name}}", "US_MOVED": "{{username}} ha mogut la història d'usuari {{obj_name}}", diff --git a/app/locales/taiga/locale-de.json b/app/locales/taiga/locale-de.json index a4e87b4b..aef79320 100644 --- a/app/locales/taiga/locale-de.json +++ b/app/locales/taiga/locale-de.json @@ -5,8 +5,8 @@ "OR": "oder", "LOADING": "Wird geladen...", "LOADING_PROJECT": "Projekt wird geladen...", - "DATE": "DD MMM YYYY", - "DATETIME": "DD MMM YYYY HH:mm", + "DATE": "DD. MMM YYYY", + "DATETIME": "DD. MMM YYYY HH:mm", "SAVE": "Speichern", "CANCEL": "Abbrechen", "ACCEPT": "Akzeptieren", @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Ein Eintrag pro Zeile...", "NEW_BULK": "Neue Massenerstellung", "RELATED_TASKS": "Verbundene Aufgaben", + "PREVIOUS": "Previous", + "NEXT": "Weiter", "LOGOUT": "Ausloggen", "EXTERNAL_USER": "ein externer Benutzer", "GENERIC_ERROR": "Eins unserer Helferlein sagt {{error}}.", @@ -43,8 +45,13 @@ "TEAM_REQUIREMENT": "Team requirement is a requirement that must exist in the project but should have no cost for the client", "OWNER": "Projekteigentümer", "CAPSLOCK_WARNING": "Achtung! Sie verwenden Großbuchstaben in einem Eingabefeld, dass Groß- und Kleinschreibung berücksichtigt.", - "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", - "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Sind Sie sicher, dass Sie den Bearbeitungsmodus beenden möchten?", + "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Beachten Sie, dass alle Änderungen verloren gehen, wenn Sie den Bearbeitungsmodus schließen, ohne vorher zu speichern.", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Dieser Wert scheint ungültig zu sein.", "TYPE_EMAIL": "Dieser Wert sollte eine gültige E-Mail Adresse enthalten.", @@ -73,7 +80,7 @@ "PIKADAY": "Ungültiges Datumsformat. Bitte nutze DD MMM YYYY (etwa 23 März 1984)" }, "PICKERDATE": { - "FORMAT": "DD MMM YYYY", + "FORMAT": "DD. MMM YYYY", "IS_RTL": "falsch", "FIRST_DAY_OF_WEEK": "1", "PREV_MONTH": "Vorheriger Monat", @@ -115,8 +122,9 @@ "USER_STORY": "User-Story", "TASK": "Aufgabe", "ISSUE": "Ticket", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Schlagwort...", + "PLACEHOLDER": "Enter tag", "DELETE": "Schlagwort löschen", "ADD": "Schlagwort hinzufügen" }, @@ -196,9 +204,26 @@ "TITLE": "Filter", "INPUT_PLACEHOLDER": "Betreff oder Verweis", "TITLE_ACTION_FILTER_BUTTON": "suche", - "BREADCRUMB_TITLE": "zurück zu den Kategorien", - "BREADCRUMB_FILTERS": "Filter", - "BREADCRUMB_STATUS": "Status" + "INPUT_SEARCH_PLACEHOLDER": "Thema oder ref", + "TITLE_ACTION_SEARCH": "Suche", + "ACTION_SAVE_CUSTOM_FILTER": "Als Benutzerfilter speichern", + "PLACEHOLDER_FILTER_NAME": "Benennen Sie den Filter und drücken Sie die Eingabetaste", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Arten", + "STATUS": "Status", + "SEVERITY": "Gewichtung", + "PRIORITIES": "Prioritäten", + "TAGS": "Schlagwörter", + "ASSIGNED_TO": "Zugeordnet zu", + "CREATED_BY": "Erstellt durch", + "CUSTOM_FILTERS": "Benutzerfilter", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Benutzerfilter löschen", + "MESSAGE": "der Benutzerfilter '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Überschrift 1", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Vorschau", "EDIT_BUTTON": "Bearbeiten", "ATTACH_FILE_HELP": "Dateien per Drag & Drop auf das obere Textfeld anhängen.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Markdown syntax Hilfe" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Sprints ansehen", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "User-Stories ansehen", "ADD_USER_STORIES": "User-Stories hinzufügen", "MODIFY_USER_STORIES": "User-Stories modifizieren", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "User-Stories löschen" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Aufgaben ansehen", "ADD_TASKS": "Aufgaben hinzufügen", "MODIFY_TASKS": "Aufgaben ändern", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Aufgaben löschen" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Tickets ansehen", "ADD_ISSUES": "Tickets hinzufügen", "MODIFY_ISSUES": "Tickets ändern", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Tickets löschen" }, "WIKI": { @@ -289,7 +326,7 @@ "HEADER": "Ich bin bereits bei Taiga angemeldet", "PLACEHOLDER_AUTH_NAME": "Benutzername oder E-Mail-Adresse", "LINK_FORGOT_PASSWORD": "Haben Sie es vergessen?", - "TITLE_LINK_FORGOT_PASSWORD": "Did you forget your password?", + "TITLE_LINK_FORGOT_PASSWORD": "Haben Sie Ihr Passwort vergessen?", "ACTION_ENTER": "Eingabe", "ACTION_SIGN_IN": "Login", "PLACEHOLDER_AUTH_PASSWORD": "Passwort" @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Beobachtet", "DASHBOARD": "ProjeKte Dashboard" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ EPIC HINZUFÜGEN", + "UNASSIGNED": "Nicht zugeordnet" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Erfahren Sie mehr über Epics" + }, + "TABLE": { + "VOTES": "Stimmen", + "NAME": "Name", + "PROJECT": "Projekt", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Zugewiesen", + "STATUS": "Status", + "PROGRESS": "Fortschritt", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "Neues Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team-Anforderung", + "CLIENT_REQUIREMENT": "Kunden-Anforderung", + "BLOCKED": "Blockiert", + "BLOCKED_NOTE_PLACEHOLDER": "Warum ist dieses Epic geblockt?", + "CREATE_EPIC": "Epic erzeugen" + } + }, "PROJECTS": { "PAGE_TITLE": "Meine Projekte - Taiga", "PAGE_DESCRIPTION": "Eine Liste mit all Deinen Projekten. Du kannst sie ordnen oder ein Neues anlegen.", @@ -385,7 +457,7 @@ "HIDE_DEPRECATED": "- verworfene Anhänge verbergen", "COUNT_DEPRECATED": "({{ counter }} verworfen)", "MAX_UPLOAD_SIZE": "Die maximale Dateigröße beträgt {{maxFileSize}}", - "DATE": "DD MMM YYYY [um] hh:mm", + "DATE": "DD. MMM YYYY [um] hh:mm", "ERROR_UPLOAD_ATTACHMENT": "Das Hochladen war uns nicht möglich '{{fileName}}'. {{errorMessage}}", "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Anhang löschen...", "MSG_LIGHTBOX_DELETE_ATTACHMENT": "der Anhang '{{fileName}}'", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Wert bearbeiten", - "TITLE_ACTION_DELETE_VALUE": "Wert löschen" + "TITLE_ACTION_DELETE_VALUE": "Wert löschen", + "TITLE_ACTION_DELETE_TAG": "Schlagwort löschen" }, "HELP": "Wenn Sie Hilfe benötigen, besuchen Sie unsere Support-Seite!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Module", "ENABLE": "Aktivieren", "DISABLE": "Deaktivieren", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualisieren und verwalten Sie den strategischsten Teil Ihres Projektes", "BACKLOG": "Auftragsliste", "BACKLOG_DESCRIPTION": "Verwalten Sie Ihre User-Stories, um einen organisierten Überblick der anstehenden und priorisierten Aufgaben zu erhalten.", "NUMBER_SPRINTS": "Erwartete Anzahl an Sprints", @@ -452,9 +527,9 @@ "SELECT_VIDEOCONFERENCE": "Wählen Sie ein Videokonferenzsystem", "SALT_CHAT_ROOM": "Fügen Sie ein Präfix für den Chatraum-Namen hinzu", "JITSI_CHAT_ROOM": "Jitsi", - "APPEARIN_CHAT_ROOM": "Erscheint in", - "TALKY_CHAT_ROOM": "Gesprächig", - "CUSTOM_CHAT_ROOM": "Kunde", + "APPEARIN_CHAT_ROOM": "Appear.in", + "TALKY_CHAT_ROOM": "Talky.io", + "CUSTOM_CHAT_ROOM": "Benutzerdefiniert", "URL_CHAT_ROOM": "URL Ihres Chatrooms" }, "PROJECT_PROFILE": { @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Sie sind im Begriff, die CSV data access URL zu ändern. Die vorherige URL wird deaktiviert. Sind Sie sicher?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "User-Stories Berichte", "SECTION_TITLE_TASK": "Aufgabenberichte", "SECTION_TITLE_ISSUE": "Ticket Berichte", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Benutzerfelder", "SUBTITLE": "Spezifizieren Sie die Benutzerfelder für Ihre User-Stories, Aufgaben und Tickets.", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Benutzerdefinierte Felder der User-Story", "US_ADD": "Benutzerdefiniertes Feld bei User-Stories hinzufügen", "TASK_DESCRIPTION": "Aufgaben benutzerdefinierte Felder", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Spezifizieren Sie die Status, die Ihre User-Stories, Aufgaben und Tickets durchlaufen werden.", - "US_TITLE": "User-Story Status", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Aufgaben-Status", "ISSUE_TITLE": "Ticket-Status" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Ticketarten", "ACTION_ADD": "Neu hinzufügen {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Schlagwörter", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "Es sieht so aus, als konnte zu Ihren Suchkriterien nichts passendes gefunden werden.", + "ACTION_ADD": "Schlagwort hinzufügen", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Rollen - {{projectName}}", "WARNING_NO_ROLE": "Beachten Sie, keine Rolle in Ihrem Projekt wird in der Lage sein, die Punktevergabe für User-Stories einzuschätzen.", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -607,7 +701,7 @@ "HEADERS": "Überschriften", "PAYLOAD": "Ladung", "RESPONSE": "Rückmeldung", - "DATE": "DD MMM YYYY [um] hh:mm:ss", + "DATE": "DD. MMM YYYY [um] hh:mm:ss", "ACTION_HIDE_HISTORY": "(Chronik verbergen)", "ACTION_HIDE_HISTORY_TITLE": "Chronik Details verbergen", "ACTION_SHOW_HISTORY": "(Chronik anzeigen)", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "die Einladung an {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Vorgegebener Wert für Punkteauswahl", - "LABEL_US": "Vorgegebener Wert für User-Story-Status Auswahl", "LABEL_TASK_STATUS": "Vorgegebene Auswahl für den Aufgaben-Status", - "LABEL_PRIORITY": "Vorgegebener Wert für Prioritätsauswahl", - "LABEL_SEVERITY": "Vorgegebener Wert für Gewichtungsauswahl", "LABEL_ISSUE_TYPE": "Vorgegebener Wert für Ticketartauswahl", - "LABEL_ISSUE_STATUS": "Vorgegebene Auswahl für den Ticket-Status" + "LABEL_ISSUE_STATUS": "Vorgegebene Auswahl für den Ticket-Status", + "LABEL_PRIORITY": "Vorgegebener Wert für Prioritätsauswahl", + "LABEL_SEVERITY": "Vorgegebener Wert für Gewichtungsauswahl" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Benennen Sie den neuen Status" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioritäten", "SEVERITIES": "Schweregrade", "TYPES": "Typen", - "CUSTOM_FIELDS": "Benutzerdefinierte Felder" + "CUSTOM_FIELDS": "Benutzerdefinierte Felder", + "TAGS": "Schlagwörter" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Projektprofil" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Alle anzeigen", "FILTER_TYPE_PROJECTS": "Projekte", "FILTER_TYPE_PROJECT_TITLES": "Nur Projekte anzeigen", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Stories", "FILTER_TYPE_USER_STORIES_TITLES": "Nur User-Stories anzeigen", "FILTER_TYPE_TASKS": "Aufgaben", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Optional) Fügen Sie einen persönlichen Text zur Einladung hinzu. Erzählen Sie Ihren neuen Mitgliedern etwas Schönes. ;-)", "PLACEHOLDER_TYPE_EMAIL": "Geben Sie eine E-Mail ein", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Leider kann dieses Projekt nicht mehr als {{maxMembers}} Mitglieder haben. Wenn Sie die derzeitige Grenze erhöhen möchten, kontaktieren Sie den Administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Leider kann dieses Projekt nicht mehr als {{maxMembers}} Mitglieder haben." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Das Projekt kann nicht ohne einen Projektleiter existieren.", @@ -970,10 +1068,30 @@ "BUTTON": "Fragen Sie dieses Projektmitglied, um Projektleiter zu werden" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Neue User-Story", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Thema", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - User-Story {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Abgeschlossen {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} von {{userStoryTotalTasks}} Aufgaben geschlossen). Punkte: {{userStoryPoints}}. Beschreibung: {{userStoryDescription}}", - "SECTION_NAME": "User-Story Details", + "SECTION_NAME": "User-Story", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Zu Taskboard wechseln", "TOTAL_POINTS": "Gesamtpunkte", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Dies User-Story wurde angelegt von", "GO_TO_EXTERNAL_REFERENCE": "Zur Quelle wechseln", "BLOCKED": "Diese User-Story wird blockiert", - "PREVIOUS": "Vorherige User-Story", - "NEXT": "nächste User-Story", "TITLE_DELETE_ACTION": "User-Story löschen", "LIGHTBOX_TITLE_BLOKING_US": "Blockiert uns", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} Aufgaben fertiggestellt", "ASSIGN": "Zugeordnete User-Story", "NOT_ESTIMATED": "Nicht eingeschätzt", "TOTAL_US_POINTS": "User-Story-Punkte insgesamt", + "TRIBE": { + "PUBLISH": "Als Gig in Taiga Tribe veröffentlichen", + "PUBLISH_INFO": "Weitere Infos", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story veröffentlicht als Gig in Taiga Tribe", + "EDIT_LINK": "Link bearbeiten", + "CLOSE": "Schließen", + "SYNCHRONIZE_LINK": "mit Taiga Tribe synchronisieren", + "PUBLISH_MORE_INFO_TITLE": "Brauchen Sie jemanden für diese Aufgabe?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Team Anforderung", "CLIENT_REQUIREMENT": "Kundenanforderung", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Kommentar gelöscht von {{user}} am {{date}}", + "DELETED_INFO": "Kommentar gelöscht von {{user}}", "TITLE": "Kommentare", + "COMMENTS_COUNT": "{{comments}} Kommentare", + "ORDER": "Reihenfolge", + "OLDER_FIRST": "Ältere zuerst", + "RECENT_FIRST": "Letzte zuerst", "COMMENT": "Kommentieren", + "EDIT_COMMENT": "Kommentar bearbeiten", + "EDITED_COMMENT": "Bearbeitet:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Geben Sie hier einen neuen Kommentar ein", "SHOW_DELETED": "Gelöschten Kommentar anzeigen", "HIDE_DELETED": "Gelöschten Kommentar ausblenden", "DELETE": "Kommentar löschen", - "RESTORE": "Kommentar wiederherstellen" + "RESTORE": "Kommentar wiederherstellen", + "HISTORY": { + "TITLE": "Aktivität" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Aktivitäten zeigen", - "DATETIME": "DD MMM YYYY HH:mm", + "DATETIME": "DD. MMM YYYY HH:mm", "SHOW_MORE": "+ Vorherige Einträge zeigen ({{showMore}} vorhanden)", "TITLE": "Aktivität", + "ACTIVITIES_COUNT": "{{activities}} Aktivitäten", "REMOVED": "entfernt", "ADDED": "hinzugefügt", - "US_POINTS": "User-Story Punkte ({{name}})", - "NEW_ATTACHMENT": "Neuer Anhang", - "DELETED_ATTACHMENT": "Gelöschter Anhang", - "UPDATED_ATTACHMENT": "aktualisierter Anhang {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "gelöschtes Kundenattribut", + "TAGS_ADDED": "Tags hinzugefügt:", + "TAGS_REMOVED": "Tags entfernt:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "neuer Anhang:", + "DELETED_ATTACHMENT": "gelöschter Anhang:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Machte {size, plural, one{eine Änderung} other{# Änderungen}}", + "BECAME_DEPRECATED": "ist veraltet", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Team Anforderung", + "CLIENT_REQUIREMENT": "Kundenanforderung", + "BLOCKED": "Blockiert", "VALUES": { "YES": "ja", "NO": "nein", @@ -1052,12 +1198,14 @@ "TAGS": "Schlagwörter", "ATTACHMENTS": "Anhänge", "IS_DEPRECATED": "ist veraltet", + "IS_NOT_DEPRECATED": "ist nicht verworfen", "ORDER": "Befehl", "BACKLOG_ORDER": "Backlog Befehl", "SPRINT_ORDER": "Sprint Befehl", "KANBAN_ORDER": "Kanban Befehl", "TASKBOARD_ORDER": "Taskboard Befehl", - "US_ORDER": "User-Story Befehl" + "US_ORDER": "User-Story Befehl", + "COLOR": "Farbe" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "geschlossene
Aufgaben", "IOCAINE_DOSES": "Iocaine
Dosen", "SHOW_STATISTICS_TITLE": "Statistik anzeigen", - "TOGGLE_BAKLOG_GRAPH": "Zeige/Verstecke Burndowngraph" + "TOGGLE_BAKLOG_GRAPH": "Zeige/Verstecke Burndowngraph", + "POINTS_PER_ROLE": "Points pro Rolle" }, "SUMMARY": { "PROJECT_POINTS": "Projekt
Punkte", @@ -1122,13 +1271,11 @@ "TITLE": "Filter", "REMOVE": "Filter entfernen", "HIDE": "Filter verbergen", - "SHOW": "Filter anzeigen", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Schlagwörter" + "SHOW": "Filter anzeigen" }, "SPRINTS": { "TITLE": "SPRINTS", - "DATE": "DD MMM YYYY", + "DATE": "DD. MMM YYYY", "LINK_TASKBOARD": "Sprint Taskboard", "TITLE_LINK_TASKBOARD": "Gehe zu Taskboard von \"{{name}}\"", "NUMBER_SPRINTS": "
Sprints", @@ -1173,13 +1320,13 @@ "YAXIS_LABEL": "Punkte", "OPTIMAL": "Optimale unerledigte Punkte für Tag {{formattedDate}} sollten sein {{roundedValue}}", "REAL": "Tatsächliche Anzahl unerledigter Punkte für Tag {{formattedDate}} ist {{roundedValue}}", - "DATE": "DD MMMM YYYY" + "DATE": "DD. MMMM YYYY" } }, "TASK": { "PAGE_TITLE": "{{taskSubject}} - Aufgabe {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Beschreibung: {{taskDescription}}", - "SECTION_NAME": "Aufgabendetails", + "SECTION_NAME": "Aufgabe", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Zu Taskboard wechseln", "PLACEHOLDER_SUBJECT": "Betreff...", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Diese Aufgabe wurde erstellt durch", "TITLE_LINK_GO_ORIGIN": "Zu User-Story wechseln", "BLOCKED": "Diese Aufgabe wird blockiert", - "PREVIOUS": "vorherige Aufgabe", - "NEXT": "nächste Aufgabe", "TITLE_DELETE_ACTION": "Aufgabe löschen", "LIGHTBOX_TITLE_BLOKING_TASK": "Blockierende Aufgabe", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Tickets - {{projectName}}", "PAGE_DESCRIPTION": "Das Ticket-Listen Panel des Projekts {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Tickets", - "SECTION_NAME": "Ticket Details", + "SECTION_NAME": "Ticket", "ACTION_NEW_ISSUE": "+ NEUES TICKET", "ACTION_PROMOTE_TO_US": "Zur User-Story aufwerten", - "PLACEHOLDER_FILTER_NAME": "Benennen Sie den Filter und drücken Sie die Eingabetaste", "PROMOTED": "Dieses Ticket wurde aufgewertet zu User-Story:", "EXTERNAL_REFERENCE": "Dieses Ticket wurde erstellt durch", "GO_TO_EXTERNAL_REFERENCE": "Zur Quelle wechseln", "BLOCKED": "Dieses Ticket wird blockiert", - "TITLE_PREVIOUS_ISSUE": "vorheriges Ticket", - "TITLE_NEXT_ISSUE": "nächstes Ticket", "ACTION_DELETE": "Ticket löschen", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blockierendes Ticket", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Dieses Problem zur User-Story aufwerten", "MESSAGE": "Sind Sie sicher, dass Sie aus diesem Ticket eine neue User-Story erstellen möchten?" }, - "FILTERS": { - "TITLE": "Filter", - "INPUT_SEARCH_PLACEHOLDER": "Thema oder ref", - "TITLE_ACTION_SEARCH": "Suche", - "ACTION_SAVE_CUSTOM_FILTER": "Als Benutzerfilter speichern", - "BREADCRUMB": "Filter", - "TITLE_BREADCRUMB": "Filter", - "CATEGORIES": { - "TYPE": "Arten", - "STATUS": "Status", - "SEVERITY": "Gewichtung", - "PRIORITIES": "Prioritäten", - "TAGS": "Schlagwörter", - "ASSIGNED_TO": "Zugeordnet", - "CREATED_BY": "Erstellt durch", - "CUSTOM_FILTERS": "Benutzerfilter" - }, - "CONFIRM_DELETE": { - "TITLE": "Benutzerfilter löschen", - "MESSAGE": "der Benutzerfilter '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Arten", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Suche - {{projectName}}", "PAGE_DESCRIPTION": "Suchen Sie User-Stories, Tickets, Aufgaben oder Wiki Seiten im Projekt {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "User-Stories", "FILTER_ISSUES": "Tickets", "FILTER_TASKS": "Aufgaben", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Wiki Seite löschen", "DELETE_LINK_TITLE": "Entferne Wiki Link", "NAVIGATION": { - "SECTION_NAME": "Links", - "ACTION_ADD_LINK": "Link hinzufügen" + "HOME": "Hauptseite", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Bookmark hinzufügen", + "ALL_PAGES": "Alle Wiki-Seiten" }, "SUMMARY": { - "TIMES_EDITED": "Zeiten
bearbeitet", + "TIMES_EDITED": "mal
bearbeitet", "LAST_EDIT": "letzte
Bearbeitung", "LAST_MODIFICATION": "letzte Änderung" + }, + "SECTION_PAGES_LIST": "Alle Seiten", + "PAGES_LIST_COLUMNS": { + "TITLE": "Titel", + "EDITIONS": "Editions", + "CREATED": "Erstellt", + "MODIFIED": "Geändert", + "CREATOR": "Ersteller", + "LAST_MODIFIER": "Letzter Bearbeiter" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} erstellte die neue Aufgabe {{obj_name}} in {{project_name}}, die zur User-Story {{us_name}} gehört", "WIKI_CREATED": "{{username}} erstellte die neue Wiki Seite {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} erstellte den neuen Sprint {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} erstellte das Projekt {{project_name}}", "MILESTONE_UPDATED": "{{username}} aktualisierte den Sprint {{obj_name}}", "US_UPDATED": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der User-Story {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} von User-Story {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} die zu der User-Story gehört {{us_name}} zu {{new_value}}", "WIKI_UPDATED": "{{username}} aktualisierte die WIKI Seite {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} schrieb einen Kommentar in der User-Story {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} schrieb einen Kommentar im Ticket {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} schrieb einen Kommentar in der Aufgabe {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} hat ein neues Mitglied", "US_ADDED_MILESTONE": "{{username}} fügte dem Sprint {{sprint_name}} die User-Story {{obj_name}} hinzu", "US_MOVED": "{{username}} wurde in die Story {{obj_name}} verschoben", diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 3015c13d..99a17c12 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "One item per line...", "NEW_BULK": "New bulk insert", "RELATED_TASKS": "Related tasks", + "PREVIOUS": "Previous", + "NEXT": "Next", "LOGOUT": "Logout", "EXTERNAL_USER": "an external user", "GENERIC_ERROR": "One of our Oompa Loompas says {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "This value seems to be invalid.", "TYPE_EMAIL": "This value should be a valid email.", @@ -115,8 +122,9 @@ "USER_STORY": "User story", "TASK": "Task", "ISSUE": "Issue", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "I'm it! Tag me...", + "PLACEHOLDER": "Enter tag", "DELETE": "Delete tag", "ADD": "Add tag" }, @@ -196,9 +204,27 @@ "TITLE": "filters", "INPUT_PLACEHOLDER": "Subject or reference", "TITLE_ACTION_FILTER_BUTTON": "search", - "BREADCRUMB_TITLE": "back to categories", - "BREADCRUMB_FILTERS": "Filters", - "BREADCRUMB_STATUS": "status" + "TITLE": "Filters", + "INPUT_SEARCH_PLACEHOLDER": "Subject or ref", + "TITLE_ACTION_SEARCH": "Search", + "ACTION_SAVE_CUSTOM_FILTER": "save as custom filter", + "PLACEHOLDER_FILTER_NAME": "Write the filter name and press enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Type", + "STATUS": "Status", + "SEVERITY": "Severity", + "PRIORITIES": "Priorities", + "TAGS": "Tags", + "ASSIGNED_TO": "Assigned to", + "CREATED_BY": "Created by", + "CUSTOM_FILTERS": "Custom filters", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Delete custom filter", + "MESSAGE": "the custom filter '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "First Level Heading", @@ -228,9 +254,18 @@ "PREVIEW_BUTTON": "Preview", "EDIT_BUTTON": "Edit", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Markdown syntax help" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "View sprints", @@ -243,6 +278,7 @@ "VIEW_USER_STORIES": "View user stories", "ADD_USER_STORIES": "Add user stories", "MODIFY_USER_STORIES": "Modify user stories", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Delete user stories" }, "TASKS": { @@ -250,6 +286,7 @@ "VIEW_TASKS": "View tasks", "ADD_TASKS": "Add tasks", "MODIFY_TASKS": "Modify tasks", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Delete tasks" }, "ISSUES": { @@ -257,6 +294,7 @@ "VIEW_ISSUES": "View issues", "ADD_ISSUES": "Add issues", "MODIFY_ISSUES": "Modify issues", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Delete issues" }, "WIKI": { @@ -366,6 +404,41 @@ "WATCHING_SECTION": "Watching", "DASHBOARD": "Projects Dashboard" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Unassigned" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Votes", + "NAME": "Name", + "PROJECT": "Project", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Status", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Blocked", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "My projects - Taiga", "PAGE_DESCRIPTION": "A list with all your projects, you can reorder or create a new one.", @@ -402,7 +475,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Edit value", - "TITLE_ACTION_DELETE_VALUE": "Delete value" + "TITLE_ACTION_DELETE_VALUE": "Delete value", + "TITLE_ACTION_DELETE_TAG": "Delete tag" }, "HELP": "Do you need help? Check out our support page!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +509,8 @@ "TITLE": "Modules", "ENABLE": "Enable", "DISABLE": "Disable", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Manage your user stories to maintain an organized view of upcoming and prioritized work.", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +573,7 @@ "REGENERATE_SUBTITLE": "You going to change the CSV data access url. The previous url will be disabled. Are you sure?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "user stories reports", "SECTION_TITLE_TASK": "tasks reports", "SECTION_TITLE_ISSUE": "issues reports", @@ -509,6 +586,8 @@ "CUSTOM_FIELDS": { "TITLE": "Custom Fields", "SUBTITLE": "Specify the custom fields for your user stories, tasks and issues", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "User stories custom fields", "US_ADD": "Add a custom field in user stories", "TASK_DESCRIPTION": "Tasks custom fields", @@ -546,7 +625,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Specify the statuses your user stories, tasks and issues will go through", - "US_TITLE": "US Statuses", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Task Statuses", "ISSUE_TITLE": "Issue Statuses" }, @@ -556,6 +636,17 @@ "ISSUE_TITLE": "Issues types", "ACTION_ADD": "Add new {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Tags", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Add tag", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Be careful, no role in your project will be able to estimate the point value for user stories", @@ -588,6 +679,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +738,14 @@ "DEFAULT_DELETE_MESSAGE": "the invitation to {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Default value for points selector", - "LABEL_US": "Default value for US status selector", "LABEL_TASK_STATUS": "Default value for task status selector", - "LABEL_PRIORITY": "Default value for priority selector", - "LABEL_SEVERITY": "Default value for severity selector", "LABEL_ISSUE_TYPE": "Default value for issue type selector", - "LABEL_ISSUE_STATUS": "Default value for issue status selector" + "LABEL_ISSUE_STATUS": "Default value for issue status selector", + "LABEL_PRIORITY": "Default value for priority selector", + "LABEL_SEVERITY": "Default value for severity selector" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Write a name for the new status" @@ -681,7 +777,8 @@ "PRIORITIES": "Priorities", "SEVERITIES": "Severities", "TYPES": "Types", - "CUSTOM_FIELDS": "Custom fields" + "CUSTOM_FIELDS": "Custom fields", + "TAGS": "Tags" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Project Profile" @@ -751,6 +848,8 @@ "FILTER_TYPE_ALL_TITLE": "Show all", "FILTER_TYPE_PROJECTS": "Projects", "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Stories", "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", "FILTER_TYPE_TASKS": "Tasks", @@ -950,8 +1049,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)", "PLACEHOLDER_TYPE_EMAIL": "Type an Email", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1069,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "New user story", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Subject", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}", - "SECTION_NAME": "User story details", + "SECTION_NAME": "User story", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Go to the taskboard", "TOTAL_POINTS": "total points", @@ -984,8 +1103,6 @@ "EXTERNAL_REFERENCE": "This US has been created from", "GO_TO_EXTERNAL_REFERENCE": "Go to origin", "BLOCKED": "This user story is blocked", - "PREVIOUS": "previous user story", - "NEXT": "next user story", "TITLE_DELETE_ACTION": "Delete User Story", "LIGHTBOX_TITLE_BLOKING_US": "Blocking us", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tasks completed", @@ -1010,28 +1127,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Comment deleted by {{user}} on {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Comments", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Comment", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Type a new comment here", "SHOW_DELETED": "Show deleted comment", "HIDE_DELETED": "Hide deleted comment", "DELETE": "Delete comment", - "RESTORE": "Restore comment" + "RESTORE": "Restore comment", + "HISTORY": { + "TITLE": "Activity" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Show activity", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Show previous entries ({{showMore}} more)", "TITLE": "Activity", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "removed", "ADDED": "added", - "US_POINTS": "US points ({{name}})", - "NEW_ATTACHMENT": "new attachment", - "DELETED_ATTACHMENT": "deleted attachment", - "UPDATED_ATTACHMENT": "updated attachment {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "deleted custom attribute", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}): ", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Made {size, plural, one{one change} other{# changes}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Team Requirement", + "CLIENT_REQUIREMENT": "Client Requirement", + "BLOCKED": "Blocked", "VALUES": { "YES": "yes", "NO": "no", @@ -1063,12 +1199,14 @@ "TAGS": "tags", "ATTACHMENTS": "attachments", "IS_DEPRECATED": "is deprecated", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "order", "BACKLOG_ORDER": "backlog order", "SPRINT_ORDER": "sprint order", "KANBAN_ORDER": "kanban order", "TASKBOARD_ORDER": "taskboard order", - "US_ORDER": "us order" + "US_ORDER": "us order", + "COLOR": "color" } }, "BACKLOG": { @@ -1120,7 +1258,8 @@ "CLOSED_TASKS": "closed
tasks", "IOCAINE_DOSES": "iocaine
doses", "SHOW_STATISTICS_TITLE": "Show statistics", - "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "project
points", @@ -1133,9 +1272,7 @@ "TITLE": "Filters", "REMOVE": "Remove Filters", "HIDE": "Hide Filters", - "SHOW": "Show Filters", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Tags" + "SHOW": "Show Filters" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1190,7 +1327,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Task {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Description: {{taskDescription}}", - "SECTION_NAME": "Task details", + "SECTION_NAME": "Task", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Go to the taskboard", "PLACEHOLDER_SUBJECT": "Type the new task subject", @@ -1200,8 +1337,6 @@ "ORIGIN_US": "This task has been created from", "TITLE_LINK_GO_ORIGIN": "Go to user story", "BLOCKED": "This task is blocked", - "PREVIOUS": "previous task", - "NEXT": "next task", "TITLE_DELETE_ACTION": "Delete Task", "LIGHTBOX_TITLE_BLOKING_TASK": "Blocking task", "FIELDS": { @@ -1239,16 +1374,13 @@ "PAGE_TITLE": "Issues - {{projectName}}", "PAGE_DESCRIPTION": "The issues list panel of the project {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Issues", - "SECTION_NAME": "Issue details", + "SECTION_NAME": "Issue", "ACTION_NEW_ISSUE": "+ NEW ISSUE", "ACTION_PROMOTE_TO_US": "Promote to User Story", - "PLACEHOLDER_FILTER_NAME": "Write the filter name and press enter", "PROMOTED": "This issue has been promoted to US:", "EXTERNAL_REFERENCE": "This issue has been created from", "GO_TO_EXTERNAL_REFERENCE": "Go to origin", "BLOCKED": "This issue is blocked", - "TITLE_PREVIOUS_ISSUE": "previous issue", - "TITLE_NEXT_ISSUE": "next issue", "ACTION_DELETE": "Delete issue", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blocking issue", "FIELDS": { @@ -1260,28 +1392,6 @@ "TITLE": "Promote this issue to a new user story", "MESSAGE": "Are you sure you want to create a new US from this Issue?" }, - "FILTERS": { - "TITLE": "Filters", - "INPUT_SEARCH_PLACEHOLDER": "Subject or ref", - "TITLE_ACTION_SEARCH": "Search", - "ACTION_SAVE_CUSTOM_FILTER": "save as custom filter", - "BREADCRUMB": "Filters", - "TITLE_BREADCRUMB": "Filters", - "CATEGORIES": { - "TYPE": "Type", - "STATUS": "Status", - "SEVERITY": "Severity", - "PRIORITIES": "Priorities", - "TAGS": "Tags", - "ASSIGNED_TO": "Assigned to", - "CREATED_BY": "Created by", - "CUSTOM_FILTERS": "Custom filters" - }, - "CONFIRM_DELETE": { - "TITLE": "Delete custom filter", - "MESSAGE": "the custom filter '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Type", @@ -1327,6 +1437,7 @@ "SEARCH": { "PAGE_TITLE": "Search - {{projectName}}", "PAGE_DESCRIPTION": "Search anything, user stories, issues, tasks or wiki pages, in the project {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "User Stories", "FILTER_ISSUES": "Issues", "FILTER_TASKS": "Tasks", @@ -1428,13 +1539,24 @@ "DELETE_LIGHTBOX_TITLE": "Delete Wiki Page", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Links", - "ACTION_ADD_LINK": "Add link" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "times
edited", "LAST_EDIT": "last
edit", "LAST_MODIFICATION": "last modification" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Created", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1458,6 +1580,8 @@ "TASK_CREATED_WITH_US": "{{username}} has created a new task {{obj_name}} in {{project_name}} which belongs to the US {{us_name}}", "WIKI_CREATED": "{{username}} has created a new wiki page {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} has created a new sprint {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} created the project {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", @@ -1470,9 +1594,13 @@ "TASK_UPDATED_WITH_US": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}} to {{new_value}}", "WIKI_UPDATED": "{{username}} has updated the wiki page {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} has commented in the US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} has commented in the issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} has commented in the task {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} has a new member", "US_ADDED_MILESTONE": "{{username}} has added the US {{obj_name}} to {{sprint_name}}", "US_MOVED": "{{username}} has moved the US {{obj_name}}", diff --git a/app/locales/taiga/locale-es.json b/app/locales/taiga/locale-es.json index 08b5a14a..b142068e 100644 --- a/app/locales/taiga/locale-es.json +++ b/app/locales/taiga/locale-es.json @@ -35,16 +35,23 @@ "ONE_ITEM_LINE": "Un elemento por línea...", "NEW_BULK": "Nueva inserción en bloque", "RELATED_TASKS": "Tareas relacionadas", + "PREVIOUS": "Previous", + "NEXT": "Siguiente", "LOGOUT": "Cerrar sesión", "EXTERNAL_USER": "un usuario externo", "GENERIC_ERROR": "Uno de nuestros Oompa Loompas dice {{error}}.", "IOCAINE_TEXT": "¿Te sientes fuera de tu zona de confort en una tarea? Asegúrate de que los demás están al tanto de ello, marca el check de la Iocaína al editar una tarea. Igual eu era posible llegar a ser inmune a este veneno mortal a base de consumir pequeñas dosis a lo largo del tiempo, es posible conseguir mejor en lo que estás haciendo si afrontas de vez en cuando esta clase de retos!", "CLIENT_REQUIREMENT": "Requerimiento de cliente es un nuevo requisito que no se esperaba y es necesario que forme parte del proyecto.", "TEAM_REQUIREMENT": "Requerimiento del equipo es un nuevo requisito que debe existir en el proyecto pero que no conllevará ningún coste para el cliente.", - "OWNER": "Project Owner", - "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", - "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", - "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "OWNER": "Dueño del proyecto", + "CAPSLOCK_WARNING": "¡Cuidado!. Esta usando mayusculas en un campo sensible a mayusculas", + "CONFIRM_CLOSE_EDIT_MODE_TITLE": "¿Seguro que desea cerrar el modo de edición?", + "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Recuerde que si cierra el modo de edicion sin guardar todos los cambios se perderán", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Este valor parece inválido.", "TYPE_EMAIL": "El valor debe ser un email.", @@ -69,8 +76,8 @@ "MAX_CHECK": "Debes seleccionar %s o menos.", "RANGE_CHECK": "Debes seleccionar de %s a %s.", "EQUAL_TO": "Este valor debe ser el mismo.", - "LINEWIDTH": "One or more lines is perhaps too long. Try to keep under %s characters.", - "PIKADAY": "Invalid date format, please use DD MMM YYYY (like 23 Mar 1984)" + "LINEWIDTH": "Una o más líneas es tal vez demasiado tiempo. Trate de mantener bajo %s caracteres.", + "PIKADAY": "Formato de fecha no válida, por favor utilice DD MMM AAAA (como 23 Mar 1984)" }, "PICKERDATE": { "FORMAT": "DD MMM YYYY", @@ -115,8 +122,9 @@ "USER_STORY": "Historia de usuario", "TASK": "Tarea", "ISSUE": "Petición", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "¿Qué soy? Etiquétame...", + "PLACEHOLDER": "Enter tag", "DELETE": "Borrar etiqueta", "ADD": "Añadir etiqueta" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Se borrarán todos los valores de este atributo personalizado. \n¿Estás seguro de que quieres continuar?" }, "FILTERS": { - "TITLE": "filtros", + "TITLE": "Filtros", "INPUT_PLACEHOLDER": "Asunto o referencia", "TITLE_ACTION_FILTER_BUTTON": "busqueda", - "BREADCRUMB_TITLE": "Regresar a categorias", - "BREADCRUMB_FILTERS": "Filtros", - "BREADCRUMB_STATUS": "estado" + "INPUT_SEARCH_PLACEHOLDER": "Asunto o referencia", + "TITLE_ACTION_SEARCH": "Buscar", + "ACTION_SAVE_CUSTOM_FILTER": "guardar como filtro personalizado", + "PLACEHOLDER_FILTER_NAME": "Escribe un nombre para el filtro y pulsa enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tipo", + "STATUS": "Estado", + "SEVERITY": "Gravedad", + "PRIORITIES": "Prioridades", + "TAGS": "Etiquetas", + "ASSIGNED_TO": "Asignado a", + "CREATED_BY": "Creada por", + "CUSTOM_FILTERS": "Filtros personalizados", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Eliminar filtros personalizados", + "MESSAGE": "el filtro personalizado '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Título de primer nivel", @@ -227,10 +252,19 @@ "CODE_BLOCK_SAMPLE_TEXT": "Tu texto aquí...", "PREVIEW_BUTTON": "Previsualizar", "EDIT_BUTTON": "Editar", - "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP": "Adjunte archivos arrastrando y soltando dentro del area de texto", + "ATTACH_FILE_HELP_SAVE_FIRST": "Si desea guardar adjuntos guarde primero, luego arrastre y suelte los archivos en el area de texto mas arriba", "MARKDOWN_HELP": "Ayuda de sintaxis Markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Épicas", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Ver sprints", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Ver historias de usuario", "ADD_USER_STORIES": "Crear historias de usuario", "MODIFY_USER_STORIES": "Editar historias de usuario", + "COMMENT_USER_STORIES": "Comentar historias de usuario", "DELETE_USER_STORIES": "Borrar historias de usuario" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Ver tareas", "ADD_TASKS": "Crear tareas", "MODIFY_TASKS": "Editar tareas", + "COMMENT_TASKS": "Comentar tareas", "DELETE_TASKS": "Borrar tareas" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Ver peticiones", "ADD_ISSUES": "Crear peticiones", "MODIFY_ISSUES": "Editar peticiones", + "COMMENT_ISSUES": "Comentar problemas", "DELETE_ISSUES": "Borrar peticiones" }, "WIKI": { @@ -322,8 +359,8 @@ "PLACEHOLDER_FIELD": "Nombre de usuario o email", "ACTION_RESET_PASSWORD": "Restablecer Contraseña", "LINK_CANCEL": "Nah, llévame de vuelta, creo que lo recordé.", - "SUCCESS_TITLE": "Check your inbox!", - "SUCCESS_TEXT": "We sent you an email with the instructions to set a new password", + "SUCCESS_TITLE": "¡Revisa tu bandeja de entrada!", + "SUCCESS_TEXT": "Te hemos enviado un correo con las instrucciones para restablecer tu contraseña", "ERROR": "Según nuestros Oompa Loompas tú no estás registrado" }, "CHANGE_PASSWORD": { @@ -359,13 +396,48 @@ "HOME": { "PAGE_TITLE": "Inicio - Taiga", "PAGE_DESCRIPTION": "Página de inicio de Taiga, con tus proyectos principales y tus historias de usuario, tareas y peticiones en progreso asignadas y las que observas.", - "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are working on.", + "EMPTY_WORKING_ON": "Parece vacío, ¿no? Empieza a trabajar con Taiga y verás aquí las historias, tareas e incidentes en los que estás trabajando.", "EMPTY_WATCHING": "Sigue Historias de Usuario, Tareas y Peticiones en tus proyectos y se te notificará sobre sus cambios :)", "EMPTY_PROJECT_LIST": "Todavía no tienes ningún proyecto", "WORKING_ON_SECTION": "Trabajando en", "WATCHING_SECTION": "Observando", "DASHBOARD": "Dashboard de proyecto" }, + "EPICS": { + "TITLE": "ÉPICAS", + "SECTION_NAME": "Épicas", + "EPIC": "Épica", + "PAGE_TITLE": "Épicas - {{projectName}}", + "PAGE_DESCRIPTION": "El listado de épicas del proyecto {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ AÑADIR ÉPICA", + "UNASSIGNED": "No asignado" + }, + "EMPTY": { + "TITLE": "Parece que todavía no hay épicas.", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Aprende más sobre Épicas" + }, + "TABLE": { + "VOTES": "Votos", + "NAME": "Nombre", + "PROJECT": "Proyecto", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Estado", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "Nueva Épica", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Bloqueada", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Mis proyectos - Taiga", "PAGE_DESCRIPTION": "Una lista con todos tus proyectos, puedes reordenarla o crear un proyecto nuevo.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Editar valor", - "TITLE_ACTION_DELETE_VALUE": "Eliminar valor" + "TITLE_ACTION_DELETE_VALUE": "Eliminar valor", + "TITLE_ACTION_DELETE_TAG": "Borrar etiqueta" }, "HELP": "¿Necesitas ayuda? ¡Revisa nuestra pagina de soporte! ", "PROJECT_DEFAULT_VALUES": { @@ -414,8 +487,8 @@ "PAGE_TITLE": "Miembros - {{projectName}}", "ADD_BUTTON": "+ Nuevo miembro", "ADD_BUTTON_TITLE": "Añadir un nuevo miembro", - "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Unfortunately, this project has reached its limit of ({{members}}) allowed members.", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project has reached its limit of ({{members}}) allowed members. If you would like to increase that limit please contact the administrator." + "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Desafortunadamente, este proyecto ha alcanzado su límite de ({{members}}) miembros permitidos.", + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Este proyecto ha llegado a su límite de ({{members}}) miembros permitidos. Si se desea aumentar ese límite póngase en contacto con el administrador." }, "PROJECT_EXPORT": { "TITLE": "Exportar", @@ -435,12 +508,14 @@ "TITLE": "Módulos", "ENABLE": "Activado", "DISABLE": "Desactivado", + "EPICS": "Épicas", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Gestiona tus historias de usuario para mantener una vista organizada y priorizada de los próximos trabajos que deberás afrontar. ", - "NUMBER_SPRINTS": "Expected number of sprints", - "NUMBER_SPRINTS_HELP": "0 for an undetermined number", - "NUMBER_US_POINTS": "Expected total of story points", - "NUMBER_US_POINTS_HELP": "0 for an undetermined number", + "NUMBER_SPRINTS": "Numero esperado de sprints", + "NUMBER_SPRINTS_HELP": "0 para un numero indeterminado", + "NUMBER_US_POINTS": "Total esperado de puntos historicos", + "NUMBER_US_POINTS_HELP": "0 para un numero indeterminado", "KANBAN": "Kanban", "KANBAN_DESCRIPTION": "Organiza tus proyectos de una manera flexible con este panel.", "ISSUES": "Peticiones", @@ -448,9 +523,9 @@ "WIKI": "Wiki", "WIKI_DESCRIPTION": "Añade, modifica o borra contenido en colaboración con otros miembros. Este es el lugar adecuado para la documentación de tu proyecto.", "MEETUP": "Meet Up", - "MEETUP_DESCRIPTION": "Choose your videoconference system.", + "MEETUP_DESCRIPTION": "Elegir su sistema de videoconferencia.", "SELECT_VIDEOCONFERENCE": "Elige un sistema de videoconferencia", - "SALT_CHAT_ROOM": "Add a prefix to the chatroom name", + "SALT_CHAT_ROOM": "Agregar prefijo al nombre de la sala de chat", "JITSI_CHAT_ROOM": "Jitsi", "APPEARIN_CHAT_ROOM": "AppearIn", "TALKY_CHAT_ROOM": "Talky", @@ -474,19 +549,19 @@ "LOGO_HELP": "La imagen se escalará a 80x80px.", "CHANGE_LOGO": "Cambia el logo", "ACTION_USE_DEFAULT_LOGO": "Usar imagen por defecto", - "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects allowed by your current plan", - "MAX_PRIVATE_PROJECTS_MEMBERS": "The maximum number of members for private projects has been exceeded", - "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects allowed by your current plan", - "MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds your maximum number of members for public projects", - "PROJECT_OWNER": "Project owner", - "REQUEST_OWNERSHIP": "Request ownership", - "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Do you want to become the new project owner?", - "REQUEST_OWNERSHIP_DESC": "Request that current project owner {{name}} transfer ownership of this project to you.", + "MAX_PRIVATE_PROJECTS": "Has alcanzado el número máximo de proyectos privados permitidos por su actual plan", + "MAX_PRIVATE_PROJECTS_MEMBERS": "El numero máximo de miembros para proyectos privados se ha excedido", + "MAX_PUBLIC_PROJECTS": "Desafortunadamente, usted ha alcanzado el número máximo de proyectos públicos permitidos por su plan actual", + "MAX_PUBLIC_PROJECTS_MEMBERS": "El proyecto excede el numero maximo de usuarios para proyectos publicos", + "PROJECT_OWNER": "Dueño del proyecto", + "REQUEST_OWNERSHIP": "Solicitar de dueño", + "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "¿Desea convertirse en el nuevo dueño del proyecto?", + "REQUEST_OWNERSHIP_DESC": "Solicitar que el actual project owner {{name}} te transfiera la propiedad de este proyecto a ti.", "REQUEST_OWNERSHIP_BUTTON": "Solicitud", - "REQUEST_OWNERSHIP_SUCCESS": "We'll notify the project owner", - "CHANGE_OWNER": "Change owner", - "CHANGE_OWNER_SUCCESS_TITLE": "Ok, your request has been sent!", - "CHANGE_OWNER_SUCCESS_DESC": "We will notify you by email if the project ownership request is accepted or declined" + "REQUEST_OWNERSHIP_SUCCESS": "Notificaremos al dueño del proyecto", + "CHANGE_OWNER": "Cambiar dueño", + "CHANGE_OWNER_SUCCESS_TITLE": "Ok, su solicitud ha sido enviado!", + "CHANGE_OWNER_SUCCESS_DESC": "Le notificaremos por correo si la solicitud para ser dueño del proyecto fue aceptada o rechazada" }, "REPORTS": { "TITLE": "Informes", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Vas a cambiar la url de acceso a los datos en formato CSV. La url anterior se deshabilitará. ¿Estás seguro?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "informes de historias de usuario", "SECTION_TITLE_TASK": "Informes de tareas", "SECTION_TITLE_ISSUE": "informes de peticiones", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Atributos personalizados", "SUBTITLE": "Especifica los atributos personalizados para las historias de usuario, tareas y peticiones", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Atributos personalizados de historias de usuario", "US_ADD": "Añadir un atributo personalizado en las historias de usuario", "TASK_DESCRIPTION": "Atributos personalizados de tareas", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Estado", "SUBTITLE": "Especifica los estado que atravesarán tus historias de usuario, tareas y peticiones", - "US_TITLE": "Estados de historias", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Estados de Tarea", "ISSUE_TITLE": "Estados de la petición" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Tipos de la petición", "ACTION_ADD": "Añadir nuevo {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Etiquetas", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Actualmente no hay etiquetas", + "EMPTY_SEARCH": "Parece que no se encontro nada con este criterio de busqueda", + "ACTION_ADD": "Añadir etiqueta", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Ojo, ningún rol en tu proyecto podrá estimar historias de usuario", @@ -565,7 +655,7 @@ "COUNT_MEMBERS": "{{ role.members_count }} miembros con este rol", "TITLE_DELETE_ROLE": "Borrar Rol", "REPLACEMENT_ROLE": "Todos los usuarios con este rol serán movidos a", - "WARNING_DELETE_ROLE": "Be careful! All role estimations will be removed", + "WARNING_DELETE_ROLE": "¡Ten cuidado! Todas las estimaciones de roles serán eliminados", "ERROR_DELETE_ALL": "No puedes eliminar todos los valores", "EXTERNAL_USER": "Usuario externo" }, @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "la invitación enviada a" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Valor por defecto para el selector de puntos", - "LABEL_US": "Valor por defecto para el selector de estado de historia", "LABEL_TASK_STATUS": "Valor por defecto para el selector de estado de tarea", - "LABEL_PRIORITY": "Valor por defecto para el selector de prioridad", - "LABEL_SEVERITY": "Valor por defecto para el selector de gravedad", "LABEL_ISSUE_TYPE": "Valor por defecto para el selector de tipo de la petición", - "LABEL_ISSUE_STATUS": "Valor por defecto para el selector de estado de petición" + "LABEL_ISSUE_STATUS": "Valor por defecto para el selector de estado de petición", + "LABEL_PRIORITY": "Valor por defecto para el selector de prioridad", + "LABEL_SEVERITY": "Valor por defecto para el selector de gravedad" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Escribe un nombre para el nuevo estado" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioridades", "SEVERITIES": "Gravedades", "TYPES": "Tipos", - "CUSTOM_FIELDS": "Atributos personalizados" + "CUSTOM_FIELDS": "Atributos personalizados", + "TAGS": "Etiquetas" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Perfil de proyecto" @@ -695,21 +791,21 @@ "TITLE": "Servicios" }, "PROJECT_TRANSFER": { - "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Would you like to become the new project owner?", - "PRIVATE": "Private", - "ACCEPTED_PROJECT_OWNERNSHIP": "Congratulations! You're now the new project owner.", - "REJECTED_PROJECT_OWNERNSHIP": "OK. We'll contact the current project owner", + "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "¿Te gustaría ser el nuevo project owner?", + "PRIVATE": "Privado", + "ACCEPTED_PROJECT_OWNERNSHIP": "¡Felicitaciones! Usted es ahora el nuevo propietario del proyecto.", + "REJECTED_PROJECT_OWNERNSHIP": "Ok. Nos pondremos en contacto con el propietario actual del proyecto", "ACCEPT": "Aceptar", - "REJECT": "Reject", - "PROPOSE_OWNERSHIP": "{{owner}}, the current owner of the project {{project}} has asked that you become the new project owner.", - "ADD_COMMENT": "Would you like to add a comment for the project owner?", - "UNLIMITED_PROJECTS": "Unlimited", + "REJECT": "Rechazar", + "PROPOSE_OWNERSHIP": "{{owner}}, el dueño del proyecto {{project}} pregunta si es el nuevo dueño del proyecto.", + "ADD_COMMENT": "¿Te gustaría añadir un comentario para el project owner?", + "UNLIMITED_PROJECTS": "Sin límite", "OWNER_MESSAGE": { - "PRIVATE": "Please remember that you can own up to {{maxProjects}} private projects. You currently own {{currentProjects}} private projects", - "PUBLIC": "Please remember that you can own up to {{maxProjects}} public projects. You currently own {{currentProjects}} public projects" + "PRIVATE": "Por favor recuerde que puede ser dueño de maximo {{maxProjects}} proyectos privados. Usted tiene actualmente {{currentProjects}} proyectos privados bajo su poder", + "PUBLIC": "Pro favor recuerde que solo puede ser dueño de maximo {{maxProjects}} proyectos publicos. Actualmente tiene {{currentProjects}} proyectos publicos bajo su poder" }, - "CANT_BE_OWNED": "At the moment you cannot become an owner of a project of this type. If you would like to become the owner of this project, please contact the administrator so they change your account settings to enable project ownership.", - "CHANGE_MY_PLAN": "Change my plan" + "CANT_BE_OWNED": "en el momento no puede ser el dueño de un proyecto de este tipo. Si desea ser el dueño de este proyecto, por favor contacte al administrador para cambiar la configuracion que le permita ser dueño del proyecto", + "CHANGE_MY_PLAN": "Cambiar mi plan" } }, "USER": { @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Mostrar todos", "FILTER_TYPE_PROJECTS": "Proyectos", "FILTER_TYPE_PROJECT_TITLES": "Mostrar sólo proyectos", + "FILTER_TYPE_EPICS": "Épicas", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Historias", "FILTER_TYPE_USER_STORIES_TITLES": "Mostrar sólo historias de usuario", "FILTER_TYPE_TASKS": "Tareas", @@ -771,9 +869,9 @@ "WATCHERS_COUNTER_TITLE": "{total, plural, one{un observador} other{# observadores}}", "MEMBERS_COUNTER_TITLE": "{total, plural, one{un miembro} other{# miembros}}", "BLOCKED_PROJECT": { - "BLOCKED": "Blocked project", - "THIS_PROJECT_IS_BLOCKED": "This project is temporarily blocked", - "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "In order to unblock your projects, contact the administrator." + "BLOCKED": "Proyecto bloqueado", + "THIS_PROJECT_IS_BLOCKED": "Este proyecto esta temporalmente bloqueado", + "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "Para desbloquear sus proyectos, contacte al administrador." }, "STATS": { "PROJECT": "puntos
proyecto", @@ -838,28 +936,28 @@ "ERROR_MAX_SIZE_EXCEEDED": "El fichero '{{fileName}}' ({{fileSize}}) es demasiado pesado para nuestros Oompa Loompas, prueba con uno de menos de ({{maxFileSize}}).", "SYNC_SUCCESS": "Tu proyecto se ha importado con éxito.", "PROJECT_RESTRICTIONS": { - "PROJECT_MEMBERS_DESC": "The project you are trying to import has {{members}} members, unfortunately, your current plan allows for a maximum of {{max_memberships}} members per project. If you would like to increase that limit please contact the administrator.", + "PROJECT_MEMBERS_DESC": "El proyecto que esta tratando de importar tiene {{members}} miembros, desafortunadamente, su plna actual solo le permite un maximo de {{max_memberships}} miembros por proyecto. si desea aumentar este limite por favor contacte al administrador.", "PRIVATE_PROJECTS_SPACE": { - "TITLE": "Unfortunately, your current plan does not allow for additional private projects", - "DESC": "The project you are trying to import is private. Unfortunately, your current plan does not allow for additional private projects." + "TITLE": "Desafortunadamente, su plan actual no permite a los proyectos privados adicionales", + "DESC": "El proyecto que trata de importar es privado. Desafortunadamente, su plan actual no le permite adicionar mas proyectos privados" }, "PUBLIC_PROJECTS_SPACE": { - "TITLE": "Unfortunately, your current plan does not allow for additional public projects", - "DESC": "The project you are trying to import is public. Unfortunately, your current plan does not allow additional public projects." + "TITLE": "Desafortunadamente, su plan actual no permite adicionar mas proyectos publicos", + "DESC": "El proyecto que estás intento importar es público. Desafortunadamente, tu plan actual no permite proyectos públicos adicionales." }, "PRIVATE_PROJECTS_MEMBERS": { - "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per private project" + "TITLE": "Su plan actual solo permite un numero maximo de {{max_memberships}} miembros por proyecto privado" }, "PUBLIC_PROJECTS_MEMBERS": { - "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per public project." + "TITLE": "Su plan actual solo permite un maximo de {{max_memberships}} miembros por proyecto publico." }, "PRIVATE_PROJECTS_SPACE_MEMBERS": { - "TITLE": "Unfortunately your current plan doesn't allow additional private projects or an increase of more than {{max_memberships}} members per private project", - "DESC": "The project that you are trying to import is private and has {{members}} members." + "TITLE": "Desafortunadamente tu plan actual no permite proyectos privados adicionales o un incremento de más de {{max_memberships}} miembros por proyecto privado", + "DESC": "El proyecto que estás intentando importar es privado y tiene {{members}} miembros." }, "PUBLIC_PROJECTS_SPACE_MEMBERS": { - "TITLE": "Unfortunately your current plan doesn't allow additional public projects or an increase of more than {{max_memberships}} members per public project", - "DESC": "The project that you are trying to import is public and has more than {{members}} members." + "TITLE": "Desafortunadamente su plan actual no le permite adicionar proyectos publicos o un aumento de {{max_memberships}} miembros por proyecto publico", + "DESC": "El proyecto que estás intentando importar es público y tiene más de {{members}} miembros." } } }, @@ -890,10 +988,10 @@ "SECTION_NAME": "Eliminar cuenta de Taiga", "CONFIRM": "¿Está seguro que deseas eliminar tu cuenta de Taiga?", "NEWSLETTER_LABEL_TEXT": "No quiero recibir la newsletter nunca más.", - "CANCEL": "Back to settings", - "ACCEPT": "Delete account", - "BLOCK_PROJECT": "Note that all the projects you own projects will be blocked after you delete your account. If you do want a project blocked, transfer ownership to another member of each project prior to deleting your account.", - "SUBTITLE": "Sorry to see you go. We'll be here if you should ever consider us again! :(" + "CANCEL": "Volver a los ajustes", + "ACCEPT": "Eliminar cuenta", + "BLOCK_PROJECT": "Recuerde que todos los proyectos de los cuales usted es dueño seran bloqueados despues de eliminar su cuenta. si desea mantener un proyecto bloqueado, transfiera el dominio a otro usuario en cada proyecto antes de eliminar la cuenta.", + "SUBTITLE": "Sentimos mucho verte ir. Estaremos aquí por si alguna vez nos consideras de nuevo! :(" }, "DELETE_PROJECT": { "TITLE": "Borrar proyecto", @@ -950,30 +1048,50 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Opcional) Añade un texto personalizado a la invitación. Dile algo encantador a tus nuevos miembros ;-)", "PLACEHOLDER_TYPE_EMAIL": "Escribe un email", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { - "TITLE": "Unfortunately, this project can't be left without an owner", + "TITLE": "Por desgracia, este proyecto no puede ser dejado sin dueño", "CURRENT_USER_OWNER": { - "DESC": "You are the current owner of this project. Before leaving, please transfer ownership to someone else.", - "BUTTON": "Change the project owner" + "DESC": "Usted es el dueño actual de este proyecto. Antes de salir, tranfiera el dominio de su proyecto a alguien mas.", + "BUTTON": "Cambiar el dueño del proyecto" }, "OTHER_USER_OWNER": { - "DESC": "Unfortunately, you can't delete a member who is also the current project owner. First, please assign a new project owner.", - "BUTTON": "Request project owner change" + "DESC": "Desafortunadamente, usted no puede eliminar un miembro que es a su vez el dueño actual del proyecto. Primero, por favor asigne un nuevo dueño del proyecto.", + "BUTTON": "Solicitud del cambio del dueño del proyecto" } }, "CHANGE_OWNER": { - "TITLE": "Who do you want to be the new project owner?", - "ADD_COMMENT": "Add comment", - "BUTTON": "Ask this project member to become the new project owner" + "TITLE": "¿A quién quiere ser el nuevo dueño del proyecto?", + "ADD_COMMENT": "Añadir comentario", + "BUTTON": "Pregunte a este usuario para convertirlo en el nuero dueño del proyecto" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Épica {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nueva historia de usuario", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Asunto", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Historia de Usuario {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estado: {{userStoryStatus }}. Completado el {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} de {{userStoryTotalTasks}} tareas cerradas). Puntos: {{userStoryPoints}}. Descripción: {{userStoryDescription}}", - "SECTION_NAME": "Detalles de historia de usuario", + "SECTION_NAME": "Historia de usuario", "LINK_TASKBOARD": "Panel de tareas", "TITLE_LINK_TASKBOARD": "Ir al panel de tareas", "TOTAL_POINTS": "puntos totales", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Esta historia ha sido creada desde", "GO_TO_EXTERNAL_REFERENCE": "Ir al origen", "BLOCKED": "Esta historia de usuario está bloqueada", - "PREVIOUS": "anterior historia de usuario", - "NEXT": "siguiente historia de usuario", "TITLE_DELETE_ACTION": "Borrar Historia de Usuario", "LIGHTBOX_TITLE_BLOKING_US": "Historia bloqueada", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tareas completadas", "ASSIGN": "Asignar Historia de Usuario", "NOT_ESTIMATED": "No estimada", "TOTAL_US_POINTS": "Total puntos de historia", + "TRIBE": { + "PUBLISH": "Publicar como Gig en la Tribu Taiga", + "PUBLISH_INFO": "Mas información", + "PUBLISH_TITLE": "Mas informacion para publicar en la Tribu Taiga", + "PUBLISHED_AS_GIG": "Historia publicada como Gig en la Tribu Taiga", + "EDIT_LINK": "Editar link", + "CLOSE": "Cerrar", + "SYNCHRONIZE_LINK": "sincronizar con la Tribu Taiga", + "PUBLISH_MORE_INFO_TITLE": "Necesita a alguien para esta tarea?", + "PUBLISH_MORE_INFO_TEXT": "

SI necesita ayuda con una parte especifica del trabajo puede crear facilmente gigs en la Tribu Taiga y recibir ayuda de todo el mundo. Tendra la habilidad de manejar y controlar su gig disfrutando de una comunidad dispuesta a colaborar.

la Tribu Taiga nacio como una rama de Taiga. Ambas plataformas viven separadas Pero creemos que hay mucho poder al usarlas combinadamente y asi aseguramos que la integracion funciona muy bien.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Requerido por el Equipo", "CLIENT_REQUIREMENT": "Requerido por el Cliente", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Comentario borrado por {{user}} el {{date}}", + "DELETED_INFO": "Comentario borrado por {{user}}", "TITLE": "Comentarios", + "COMMENTS_COUNT": "{{comments}} Comentarios", + "ORDER": "Orden", + "OLDER_FIRST": "Mas antiguo primero", + "RECENT_FIRST": "Mas reciente primero", "COMMENT": "Comentar", + "EDIT_COMMENT": "Editar comentario", + "EDITED_COMMENT": "Editado:", + "SHOW_HISTORY": "Ver histórico", "TYPE_NEW_COMMENT": "Escribe un nuevo comentario aquí", "SHOW_DELETED": "Mostrar comentarios eliminados", "HIDE_DELETED": "Ocultar comentarios eliminados", "DELETE": "Borrar comentario", - "RESTORE": "Restaurar comentario" + "RESTORE": "Restaurar comentario", + "HISTORY": { + "TITLE": "Actividad" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Mostrar actividad", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Ver entradas anteriores ({{showMore}} más)", "TITLE": "Actividad", + "ACTIVITIES_COUNT": "{{activities}} Actividades", "REMOVED": "borrado", "ADDED": "agregado", - "US_POINTS": "Puntos de historia ({{name}})", + "TAGS_ADDED": "etiquetas añadidas", + "TAGS_REMOVED": "Etiquetas borradas:", + "US_POINTS": "{{role}} puntos", "NEW_ATTACHMENT": "nuevo adjunto", - "DELETED_ATTACHMENT": "adjunto eliminado", - "UPDATED_ATTACHMENT": "Adjunto {{filename}} actualizado", - "DELETED_CUSTOM_ATTRIBUTE": "eliminar atributos personalizados", + "DELETED_ATTACHMENT": "adjunto borrado", + "UPDATED_ATTACHMENT": "actualizar adjunto ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "atributo personalizado creado", + "UPDATED_CUSTOM_ATTRIBUTE": "atributo personalizado actualizado", "SIZE_CHANGE": "{size, plural, one{Un cambio realizado} other{# cambios realizados}}", + "BECAME_DEPRECATED": "esta obsoleto", + "BECAME_UNDEPRECATED": "no esta obsoleto", + "TEAM_REQUIREMENT": "Requerido por el Equipo", + "CLIENT_REQUIREMENT": "Requerido por el Cliente", + "BLOCKED": "Bloqueada", "VALUES": { "YES": "sí", "NO": "no", @@ -1052,12 +1198,14 @@ "TAGS": "etiquetas", "ATTACHMENTS": "adjuntos", "IS_DEPRECATED": "está desactualizado", + "IS_NOT_DEPRECATED": "No es obsoleto", "ORDER": "orden", "BACKLOG_ORDER": "orden en backlog", "SPRINT_ORDER": "orden en sprint", "KANBAN_ORDER": "orden en kanban", "TASKBOARD_ORDER": "orden en panel de tareas", - "US_ORDER": "orden en historia" + "US_ORDER": "orden en historia", + "COLOR": "color" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "tareas
cerradas", "IOCAINE_DOSES": "dosis de
iocaína", "SHOW_STATISTICS_TITLE": "Ver estadísticas", - "TOGGLE_BAKLOG_GRAPH": "Ver/Ocultar gráfica de burndown" + "TOGGLE_BAKLOG_GRAPH": "Ver/Ocultar gráfica de burndown", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "puntos
proyecto", @@ -1122,9 +1271,7 @@ "TITLE": "Filtros", "REMOVE": "Borrar Filtros", "HIDE": "Ocultar filtros", - "SHOW": "Ver Filtros", - "FILTER_CATEGORY_STATUS": "Estado", - "FILTER_CATEGORY_TAGS": "Etiquetas" + "SHOW": "Ver Filtros" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Tarea {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estado: {{taskStatus }}. Descripción: {{taskDescription}}", - "SECTION_NAME": "Detalles de tarea", + "SECTION_NAME": "Tarea", "LINK_TASKBOARD": "Panel de tareas", "TITLE_LINK_TASKBOARD": "Ir al panel de tareas", "PLACEHOLDER_SUBJECT": "Escribe el asunto de la nueva tarea", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Esta tarea pertenece a ", "TITLE_LINK_GO_ORIGIN": "Ir a historia de usuario", "BLOCKED": "Esta tarea está bloqueada", - "PREVIOUS": "tarea anterior", - "NEXT": "tarea siguiente", "TITLE_DELETE_ACTION": "Eliminar Tarea", "LIGHTBOX_TITLE_BLOKING_TASK": "Tarea bloqueada", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Peticiones - {{projectName}}", "PAGE_DESCRIPTION": "El panel de peticiones del proyecto {{projectName}}: {{projectDescription}}\n", "LIST_SECTION_NAME": "Peticiones", - "SECTION_NAME": "Detalles de petición", + "SECTION_NAME": "Petición", "ACTION_NEW_ISSUE": "+ NUEVA PETICIÓN", "ACTION_PROMOTE_TO_US": "Promover a Historia de Usuario", - "PLACEHOLDER_FILTER_NAME": "Escribe un nombre para el filtro y pulsa enter", "PROMOTED": "Esta petición ha sido promovida a la historia:", "EXTERNAL_REFERENCE": "Esta petición ha sido creada a partir de ", "GO_TO_EXTERNAL_REFERENCE": "Ir al origen", "BLOCKED": "La petición está bloqueada", - "TITLE_PREVIOUS_ISSUE": "petición anterior", - "TITLE_NEXT_ISSUE": "petición siguiente", "ACTION_DELETE": "Borrar petición", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Petición bloqueada", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Promover esta petición a una nueva historia de usuario", "MESSAGE": "¿Está seguro de que desea crear una nueva Historia de Usuario a partir de esta Petición?" }, - "FILTERS": { - "TITLE": "Filtros", - "INPUT_SEARCH_PLACEHOLDER": "Asunto o referencia", - "TITLE_ACTION_SEARCH": "Buscar", - "ACTION_SAVE_CUSTOM_FILTER": "guardar como filtro personalizado", - "BREADCRUMB": "Filtros", - "TITLE_BREADCRUMB": "Filtros", - "CATEGORIES": { - "TYPE": "Tipo", - "STATUS": "Estado", - "SEVERITY": "Gravedad", - "PRIORITIES": "Prioridad", - "TAGS": "Etiquetas", - "ASSIGNED_TO": "Asignado a", - "CREATED_BY": "Creada por", - "CUSTOM_FILTERS": "Filtros personalizados" - }, - "CONFIRM_DELETE": { - "TITLE": "Eliminar filtros personalizados", - "MESSAGE": "el filtro personalizado '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tipo", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Buscar - {{projectName}}", "PAGE_DESCRIPTION": "Busca cualquier cosa: historias de usuario, peticiones, tareas o páginas del wiki en el proyecto {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Épicas", "FILTER_USER_STORIES": "Historias de Usuario", "FILTER_ISSUES": "Peticiones", "FILTER_TASKS": "Tareas", @@ -1397,16 +1518,16 @@ "WIZARD": { "SECTION_TITLE_CREATE_PROJECT": "Crear Proyecto", "CREATE_PROJECT_TEXT": "Fresco y claro. ¡Es emocionante!", - "CHOOSE_TEMPLATE": "Which template fits your project best?", - "CHOOSE_TEMPLATE_TITLE": "More info about project templates", - "CHOOSE_TEMPLATE_INFO": "More info", - "PROJECT_DETAILS": "Project Details", - "PUBLIC_PROJECT": "Public Project", - "PRIVATE_PROJECT": "Private Project", + "CHOOSE_TEMPLATE": "¿Que plantilla se ajusta mejor con tu proyecto?", + "CHOOSE_TEMPLATE_TITLE": "Mas informacion acerca de la plantillas del proyecto", + "CHOOSE_TEMPLATE_INFO": "Mas información", + "PROJECT_DETAILS": "Detalles del proyecto", + "PUBLIC_PROJECT": "Proyecto público", + "PRIVATE_PROJECT": "Proyecto privado", "CREATE_PROJECT": "Crear proyecto", - "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects", - "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects", - "CHANGE_PLANS": "change plans" + "MAX_PRIVATE_PROJECTS": "Has alcanzado el número máximo de proyectos privados", + "MAX_PUBLIC_PROJECTS": "Desafortunadamente, has alcanzado el número máximo de proyectos públicos", + "CHANGE_PLANS": "cambiar planes" }, "WIKI": { "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", @@ -1415,15 +1536,26 @@ "PLACEHOLDER_PAGE": "Escribe el contenido de tu página", "REMOVE": "Eliminar esta página del wiki", "DELETE_LIGHTBOX_TITLE": "Eliminar Página del Wiki", - "DELETE_LINK_TITLE": "Delete Wiki link", + "DELETE_LINK_TITLE": "Eliminar enlace de la Wiki", "NAVIGATION": { - "SECTION_NAME": "Enlaces", - "ACTION_ADD_LINK": "Añadir enlace" + "HOME": "Principal", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "veces
editada", "LAST_EDIT": "última
edición", "LAST_MODIFICATION": "ultima modificación" + }, + "SECTION_PAGES_LIST": "Todas", + "PAGES_LIST_COLUMNS": { + "TITLE": "Título", + "EDITIONS": "Ediciones", + "CREATED": "Creado", + "MODIFIED": "Modificado", + "CREATOR": "Creador", + "LAST_MODIFIER": "Ultimo modificador" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} ha creado una nueva tarea {{obj_name}} en {{project_name}} que proviene de la historia {{us_name}}", "WIKI_CREATED": "{{username}} ha creado una nueva página de wiki {{obj_name}} en {{project_name}}\n", "MILESTONE_CREATED": "{{username}} ha creado un nuevo sprint {{obj_name}} en {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} creó el proyecto {{project_name}}", "MILESTONE_UPDATED": "{{username}} ha actualizado el sprint {{obj_name}}", "US_UPDATED": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la historia {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} que proviene de la historia {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} que pertenece a la historia {{us_name}} a {{new_value}}", "WIKI_UPDATED": "{{username}} ha actualizado la página del wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} ha añadido un comentado en la historia {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} ha añadido un comentado en la petición {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} ha añadido un comentado en la tarea {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} tiene un nuevo miembro", "US_ADDED_MILESTONE": "{{username}} ha añadido la historia {{obj_name}} a {{sprint_name}}", "US_MOVED": "{{username}} ha movido la historia {{obj_name}}", diff --git a/app/locales/taiga/locale-fi.json b/app/locales/taiga/locale-fi.json index 319aa4e4..a933a9b0 100644 --- a/app/locales/taiga/locale-fi.json +++ b/app/locales/taiga/locale-fi.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Yksi riviä kohti...", "NEW_BULK": "Lisää monta", "RELATED_TASKS": "Liittyvät tehtävät", + "PREVIOUS": "Previous", + "NEXT": "Seuraava", "LOGOUT": "Kirjaudu ulos", "EXTERNAL_USER": "ulkoinen käyttäjä", "GENERIC_ERROR": "Oompa Loompas havaitsivat virheen {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Tämä arvo vaikuttaa virheelliseltä.", "TYPE_EMAIL": "Tämän pitäisi olla toimiva sähköpostiosoite.", @@ -115,8 +122,9 @@ "USER_STORY": "Käyttäjätarina", "TASK": "Task", "ISSUE": "Issue", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Anna avainsana...", + "PLACEHOLDER": "Enter tag", "DELETE": "Poista avainsana", "ADD": "Lisää avainsana" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "suodattimet", + "TITLE": "Suodattimet", "INPUT_PLACEHOLDER": "Aihe tai viittaus", "TITLE_ACTION_FILTER_BUTTON": "hae", - "BREADCRUMB_TITLE": "takaisin kategorioihin", - "BREADCRUMB_FILTERS": "Suodattimet", - "BREADCRUMB_STATUS": "tila" + "INPUT_SEARCH_PLACEHOLDER": "Otsikko tai viittaus", + "TITLE_ACTION_SEARCH": "Hae", + "ACTION_SAVE_CUSTOM_FILTER": "tallenna omaksi suodattimeksi", + "PLACEHOLDER_FILTER_NAME": "Anna suodattimen nimi ja paina enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tyyppi", + "STATUS": "Tila", + "SEVERITY": "Vakavuus", + "PRIORITIES": "Kiireellisyydet", + "TAGS": "Avainsanat", + "ASSIGNED_TO": "Tekijä", + "CREATED_BY": "Luoja", + "CUSTOM_FILTERS": "Omat suodattimet", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Poista oma suodatin", + "MESSAGE": "oma suodatin '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Päätason otsikko", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Esikatselu", "EDIT_BUTTON": "Muokkaa", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Merkintätavan ohjeet" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Kierrokset", "VIEW_SPRINTS": "Katso kierroksia", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Katso käyttäjätarinoita", "ADD_USER_STORIES": "Lisää käyttäjätarinoita", "MODIFY_USER_STORIES": "Muokkaa käyttäjätarinoita", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Poista käyttäjätarinoita" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Katsot tehtäviä", "ADD_TASKS": "Lisää tehtäviä", "MODIFY_TASKS": "Muokkaa tehtäviä", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Poista tehtäviä" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Katso pyyntöjä", "ADD_ISSUES": "Lisää pyyntöjä", "MODIFY_ISSUES": "Muokkaa pyyntöjä", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Poista pyyntöjä" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Watching", "DASHBOARD": "Projects Dashboard" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Tekijä puuttuu" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Ääniä", + "NAME": "Nimi", + "PROJECT": "Projekti", + "SPRINT": "Kierros", + "ASSIGNED_TO": "Assigned", + "STATUS": "Tila", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Suljettu", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "My projects - Taiga", "PAGE_DESCRIPTION": "A list with all your projects, you can reorder or create a new one.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Muokkaa arvoa", - "TITLE_ACTION_DELETE_VALUE": "Poista arvo" + "TITLE_ACTION_DELETE_VALUE": "Poista arvo", + "TITLE_ACTION_DELETE_TAG": "Poista avainsana" }, "HELP": "Tarvitsetko apua? Katso tukisivuilta.", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Modulit", "ENABLE": "Aktivoi", "DISABLE": "Passivoi", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Odottavat", "BACKLOG_DESCRIPTION": "Hallinnoi käyttäjätarinoita: järjestele ja priorisoi työtä.", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Jos muutata CSV-datan URLia, edellien lakkaa toimimasta. Oletko varma?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "käyttäjätarinoiden raportit", "SECTION_TITLE_TASK": "tehtävien raportit", "SECTION_TITLE_ISSUE": "pyyntöjen raportit", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Omat kentät", "SUBTITLE": "Määritele omia kenttiä käyttäjätarinoihin, tehtäviin ja pyytöihin", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Käyttäjätarinoiden omat kentät", "US_ADD": "Lisää käyttäjätarinoihin oma kenttä", "TASK_DESCRIPTION": "Tehtävien omat kentät", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Tila", "SUBTITLE": "Määrittele tilat joiden kautta käyttäjätarinasi, tehtäväsi ja pyyntösi kulkevat", - "US_TITLE": "Kt tilat", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Tehtävien tilat", "ISSUE_TITLE": "Pyyntöjen tilat" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Pyyntöjen tyypit", "ACTION_ADD": "Lisää uusi {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Avainsanat", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Lisää avainsana", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Ole varovainen, yksikään rooli projektissasi ei voi arvioida käyttäjätarinoidesi kokoa", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhookit", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "kutsu sähköpostiin {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Oletukset pisteiden valintaan", - "LABEL_US": "Oletukset käyttäjätarinoiden tiloiksi", "LABEL_TASK_STATUS": "Oletukset tehtävien tilaksi", - "LABEL_PRIORITY": "Oletus arvo tärkeyden valiintaan", - "LABEL_SEVERITY": "Oletukset vakavuudeksi", "LABEL_ISSUE_TYPE": "Oletukset pyyntöjen tyypeiksi", - "LABEL_ISSUE_STATUS": "Oletukset pyyntöjen statuksiksi" + "LABEL_ISSUE_STATUS": "Oletukset pyyntöjen statuksiksi", + "LABEL_PRIORITY": "Oletus arvo tärkeyden valiintaan", + "LABEL_SEVERITY": "Oletukset vakavuudeksi" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Anna uuden tilan nimi" @@ -681,7 +776,8 @@ "PRIORITIES": "Tärkeydet", "SEVERITIES": "Vakavuudet", "TYPES": "Tyypit", - "CUSTOM_FIELDS": "Omat kentät" + "CUSTOM_FIELDS": "Omat kentät", + "TAGS": "Avainsanat" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Projektin profiili" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Show all", "FILTER_TYPE_PROJECTS": "Projektit", "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Stories", "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", "FILTER_TYPE_TASKS": "Tehtävät", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Vapaaehtoinen) Lisää oma kuvaus kutsuusi uusille jäsenille ;-)", "PLACEHOLDER_TYPE_EMAIL": "Anna sähköposti", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Uusi käyttäjätarina", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Aihe", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}", - "SECTION_NAME": "Käyttäjätarinan tiedot", + "SECTION_NAME": "Käyttäjätarina", "LINK_TASKBOARD": "Tehtävätaulu", "TITLE_LINK_TASKBOARD": "Siirry tehtävätauluun", "TOTAL_POINTS": "total points", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Tämä Kt oon luotu täältä: ", "GO_TO_EXTERNAL_REFERENCE": "Palaa alkuun", "BLOCKED": "Tämä käyttäjätarina on suljettu", - "PREVIOUS": "edellinen käyttäjätarina", - "NEXT": "seuraava käyttäjätarina", "TITLE_DELETE_ACTION": "Poista käyttäjätarina", "LIGHTBOX_TITLE_BLOKING_US": "Meitä estää", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tehtyä tehtävää", "ASSIGN": "Käyttäjätarinan tekijä", "NOT_ESTIMATED": "Ei arvioitu", "TOTAL_US_POINTS": "Kt pisteet yhteensä", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Tiimin vaatimus", "CLIENT_REQUIREMENT": "Asiakkaan vaatimus", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "{{user}} poisti kommentin {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Kommentit", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Kommentti", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Lisää uusi kommentti tässä", "SHOW_DELETED": "Näytä poistettu kommentti", "HIDE_DELETED": "Piilota poistettu kommentti", "DELETE": "Delete comment", - "RESTORE": "Palauta kommentti" + "RESTORE": "Palauta kommentti", + "HISTORY": { + "TITLE": "Aktiivisuus" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Näytä tapahtumat", "DATETIME": "DD.MM.YY - HH:mm", "SHOW_MORE": "+ Näytä edelliset rivit ({{showMore}} lisää)", "TITLE": "Aktiivisuus", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "poistettu", "ADDED": "lisätty", - "US_POINTS": "Kt pisteet ({{name}})", - "NEW_ATTACHMENT": "uusi liite", - "DELETED_ATTACHMENT": "poistettu liite", - "UPDATED_ATTACHMENT": "päivitetty liite {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "poista oma attribuutti", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Tehty {size, plural, one{muutos} other{# muutosta}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Tiimin vaatimus", + "CLIENT_REQUIREMENT": "Asiakkaan vaatimus", + "BLOCKED": "Suljettu", "VALUES": { "YES": "Kyllä", "NO": "ei", @@ -1052,12 +1198,14 @@ "TAGS": "avainsanat", "ATTACHMENTS": "liitteet", "IS_DEPRECATED": "on vanhentunut", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "järjestys", "BACKLOG_ORDER": "odottavien järjestys", "SPRINT_ORDER": "kierroksen järjestys", "KANBAN_ORDER": "kanban järjestys", "TASKBOARD_ORDER": "Tehtävätaulun järjestys", - "US_ORDER": "kt järjestys" + "US_ORDER": "kt järjestys", + "COLOR": "väri" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "suljettu
tehtävää", "IOCAINE_DOSES": "myrkkye-
annosta", "SHOW_STATISTICS_TITLE": "Näytä tilastot", - "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "projekti
pistettä", @@ -1122,9 +1271,7 @@ "TITLE": "Suodattimet", "REMOVE": "Poista suodattimet", "HIDE": "Piilota suodattimet", - "SHOW": "Näytä suodattimet", - "FILTER_CATEGORY_STATUS": "Tila", - "FILTER_CATEGORY_TAGS": "Avainsanat" + "SHOW": "Näytä suodattimet" }, "SPRINTS": { "TITLE": "KIERROKSET", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Task {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Description: {{taskDescription}}", - "SECTION_NAME": "Tehtävän tiedot", + "SECTION_NAME": "Task", "LINK_TASKBOARD": "Tehtävätaulu", "TITLE_LINK_TASKBOARD": "Siirry tehtävätauluun", "PLACEHOLDER_SUBJECT": "Anna tehtävän aihe", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Tämä tehtävä on luotu", "TITLE_LINK_GO_ORIGIN": "Siirry käyttäjätarinaan", "BLOCKED": "Tämä tehtävä on suljettu", - "PREVIOUS": "edellinen tehtävä", - "NEXT": "seuraava tehtävä", "TITLE_DELETE_ACTION": "Poista tehtävä", "LIGHTBOX_TITLE_BLOKING_TASK": "Estävä tehtävä", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Issues - {{projectName}}", "PAGE_DESCRIPTION": "The issues list panel of the project {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Pyynnöt", - "SECTION_NAME": "Pyynnön tiedot", + "SECTION_NAME": "Issue", "ACTION_NEW_ISSUE": "+ UUSI PYYNTÖ", "ACTION_PROMOTE_TO_US": "Liitä käyttäjätarinaan", - "PLACEHOLDER_FILTER_NAME": "Anna suodattimen nimi ja paina enter", "PROMOTED": "Tämä pyyntö on liitetty Kthen:", "EXTERNAL_REFERENCE": "Tämä pyyntö on luotu täältä:", "GO_TO_EXTERNAL_REFERENCE": "Palaa alkuun", "BLOCKED": "Tämä pyyntö on estetty", - "TITLE_PREVIOUS_ISSUE": "edellinen pyyntö", - "TITLE_NEXT_ISSUE": "seuraava pyyntö", "ACTION_DELETE": "Poista pyyntö", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Estävä pyyntö", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Liitä tämä pyyntö uuteen käyttäjätarinaan", "MESSAGE": "Haluatko varmasti lisätä uuden käyttäjätarinan tästä pyynnöstä?" }, - "FILTERS": { - "TITLE": "Suodattimet", - "INPUT_SEARCH_PLACEHOLDER": "Otsikko tai viittaus", - "TITLE_ACTION_SEARCH": "Hae", - "ACTION_SAVE_CUSTOM_FILTER": "tallenna omaksi suodattimeksi", - "BREADCRUMB": "Suodattimet", - "TITLE_BREADCRUMB": "Suodattimet", - "CATEGORIES": { - "TYPE": "Tyyppi", - "STATUS": "Tila", - "SEVERITY": "Vakavuus", - "PRIORITIES": "Tärkeydet", - "TAGS": "Avainsanat", - "ASSIGNED_TO": "Tekijä", - "CREATED_BY": "Luoja", - "CUSTOM_FILTERS": "Omat suodattimet" - }, - "CONFIRM_DELETE": { - "TITLE": "Poista oma suodatin", - "MESSAGE": "oma suodatin '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tyyppi", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Search - {{projectName}}", "PAGE_DESCRIPTION": "Search anything, user stories, issues, tasks or wiki pages, in the project {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Käyttäjätarinat", "FILTER_ISSUES": "Pyynnöt", "FILTER_TASKS": "Tehtävät", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Poista wiki-sivu", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Linkit", - "ACTION_ADD_LINK": "Lisää linkki" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "kertaa
muokattu", "LAST_EDIT": "viimeinen
muokkaus", "LAST_MODIFICATION": "viimeinen muokkaus" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Luotu", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} has created a new task {{obj_name}} in {{project_name}} which belongs to the US {{us_name}}", "WIKI_CREATED": "{{username}} has created a new wiki page {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} has created a new sprint {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} created the project {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}} to {{new_value}}", "WIKI_UPDATED": "{{username}} has updated the wiki page {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} has commented in the US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} has commented in the issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} has commented in the task {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} has a new member", "US_ADDED_MILESTONE": "{{username}} has added the US {{obj_name}} to {{sprint_name}}", "US_MOVED": "{{username}} has moved the US {{obj_name}}", diff --git a/app/locales/taiga/locale-fr.json b/app/locales/taiga/locale-fr.json index 114da4b1..175404bd 100644 --- a/app/locales/taiga/locale-fr.json +++ b/app/locales/taiga/locale-fr.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Un élément par ligne...", "NEW_BULK": "Nouvel ajout en bloc", "RELATED_TASKS": "Tâches associées", + "PREVIOUS": "Précédent", + "NEXT": "Suivant", "LOGOUT": "Déconnexion", "EXTERNAL_USER": "un utilisateur externe", "GENERIC_ERROR": "L'un de nos Oompa Loompas dit {{error}}.", @@ -43,8 +45,13 @@ "TEAM_REQUIREMENT": "Un besoin projet est un besoin qui est nécessaire au projet mais qui ne doit avoir aucun impact pour le client", "OWNER": "Propriétaire du Projet", "CAPSLOCK_WARNING": "Attention ! Vous utilisez des majuscules dans un champ qui est sensible à la casse.", - "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", - "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Êtes-vous sûr de vouloir fermer le mode Édition ?", + "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Souvenez-vous que si vous fermez le mode édition sans enregistrer, toutes vos modifications seront perdues", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Affecter à", + "EDIT": "Modifier la carte" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Cette valeur semble être invalide.", "TYPE_EMAIL": "Cette valeur devrait être une adresse courriel valide.", @@ -115,8 +122,9 @@ "USER_STORY": "Récit utilisateur", "TASK": "Tâche", "ISSUE": "Ticket", + "EPIC": "Épopée", "TAGS": { - "PLACEHOLDER": "Taggez moi !", + "PLACEHOLDER": "Enter tag", "DELETE": "Supprimer le mot-clé", "ADD": "Ajouter un mot-clé" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Souvenez-vous que toutes les valeurs de ce champ personnalisé vont être effacées.\nEtes-vous sûr de vouloir continuer ?" }, "FILTERS": { - "TITLE": "filtres", + "TITLE": "Filtres", "INPUT_PLACEHOLDER": "Objet ou référence", "TITLE_ACTION_FILTER_BUTTON": "recherche", - "BREADCRUMB_TITLE": "retour aux catégories", - "BREADCRUMB_FILTERS": "Filtres", - "BREADCRUMB_STATUS": "état" + "INPUT_SEARCH_PLACEHOLDER": "Objet ou réf.", + "TITLE_ACTION_SEARCH": "Rechercher", + "ACTION_SAVE_CUSTOM_FILTER": "sauvegarder en tant que filtre personnalisé", + "PLACEHOLDER_FILTER_NAME": "Écrivez le nom du filtre et appuyez sur \"Entrée\"", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Type", + "STATUS": "Statut", + "SEVERITY": "Gravité", + "PRIORITIES": "Priorités", + "TAGS": "Mots-clés", + "ASSIGNED_TO": "Affecté à", + "CREATED_BY": "Créé par", + "CUSTOM_FILTERS": "Filtres personnalisés", + "EPIC": "Épopée" + }, + "CONFIRM_DELETE": { + "TITLE": "Supprime le filtre personnalisé", + "MESSAGE": "le filtre personnalisé '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Premier niveau de titre", @@ -227,10 +252,19 @@ "CODE_BLOCK_SAMPLE_TEXT": "Votre texte ici…", "PREVIEW_BUTTON": "Aperçu", "EDIT_BUTTON": "Modifier", - "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP": "Joindre des fichiers en glissant et déposant ceux-ci sur la zone de texte ci-dessus.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Enregistrez d'abord si vous voulez joindre des fichiers en glissant et déposant ceux-ci sur la zone de texte ci-dessus.", "MARKDOWN_HELP": "Aide sur la syntaxe Markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Épopées", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Voir les sprints", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Afficher les récits utilisateur", "ADD_USER_STORIES": "Ajouter des récits utilisateur", "MODIFY_USER_STORIES": "Modifier les récits utilisateur", + "COMMENT_USER_STORIES": "Commenter les histoires utilisateur", "DELETE_USER_STORIES": "Supprimer des récits utilisateur" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Voir les tâches", "ADD_TASKS": "Ajouter des tâches", "MODIFY_TASKS": "Modifier des tâches", + "COMMENT_TASKS": "Commenter les tâches", "DELETE_TASKS": "Supprimer des tâches" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Voir les tickets", "ADD_ISSUES": "Ajouter des tickets", "MODIFY_ISSUES": "Modifier des tickets", + "COMMENT_ISSUES": "Commenter les tickets", "DELETE_ISSUES": "Supprimer des tickets" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Suivi", "DASHBOARD": "Tableau de bord des projets" }, + "EPICS": { + "TITLE": "ÉPOPÉES", + "SECTION_NAME": "Épopées", + "EPIC": "ÉPOPÉE", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ AJOUTER ÉPOPÉE", + "UNASSIGNED": "Non affecté" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "En savoir plus sur les épopées" + }, + "TABLE": { + "VOTES": "Votes", + "NAME": "Nom", + "PROJECT": "Projet", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Affecté", + "STATUS": "Statut", + "PROGRESS": "Avancement", + "VIEW_OPTIONS": "Voir les options" + }, + "CREATE": { + "TITLE": "Nouvelle épopée", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Besoin projet", + "CLIENT_REQUIREMENT": "Besoin client", + "BLOCKED": "Bloqué", + "BLOCKED_NOTE_PLACEHOLDER": "Pourquoi cette épopée est-elle bloquée ?", + "CREATE_EPIC": "Créer une épopée" + } + }, "PROJECTS": { "PAGE_TITLE": "Mes projets - Taiga", "PAGE_DESCRIPTION": "Une liste de tous vos projets, vous pouvez la trier ou en créer une nouvelle.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Modifier la valeur", - "TITLE_ACTION_DELETE_VALUE": "Supprimer" + "TITLE_ACTION_DELETE_VALUE": "Supprimer", + "TITLE_ACTION_DELETE_TAG": "Supprimer le mot-clé" }, "HELP": "Avez-vous besoin d'aide ? Consultez notre page d'assistance !", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Modules", "ENABLE": "Activer", "DISABLE": "Désactiver", + "EPICS": "Épopées", + "EPICS_DESCRIPTION": "Visualiser et gérer les aspects les plus stratégiques de votre projet", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Gérez votre récits utilisateur pour garder une vue organisée des travaux à venir et priorisés.", "NUMBER_SPRINTS": "Nombre prévu de sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Vous êtes sur le point de changer l'url d'accès aux données CSV. L'url précédente sera désactivée. Êtes-vous sûr ?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "rapports des récits utilisateur", "SECTION_TITLE_TASK": "rapports des tâches", "SECTION_TITLE_ISSUE": "Rapports des tickets", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Champs personnalisés", "SUBTITLE": "Spécifiez les champs personnalisés de vos récits utilisateur, tâches et tickets", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Champs personnalisés des récits utilisateur", "US_ADD": "Ajouter un champ personnalisé dans les récits utilisateur", "TASK_DESCRIPTION": "Champs personnalisés de tâches", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Statut", "SUBTITLE": "Spécifiez les statuts que vont prendre vos récits utilisateur, tâches et tickets", - "US_TITLE": "Statuts des RU", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Statuts des tâches", "ISSUE_TITLE": "Statuts des Tickets" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Types de tickets", "ACTION_ADD": "Ajouter un nouveau {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Mots-clés", + "SUBTITLE": "Voir et modifier la couleur de vos mots-clés", + "EMPTY": "Il n'y a pas de mots-clés pour l'instant", + "EMPTY_SEARCH": "Il semble qu'aucun résultat ne correspond à vos critères de recherche", + "ACTION_ADD": "Ajouter un mot-clé", + "NEW_TAG": "Nouveau mot-clé", + "MIXING_HELP_TEXT": "Sélectionnez les mots-clés que vous voulez fusionner", + "MIXING_MERGE": "Fusionner des mots-clés", + "SELECTED": "Sélectionné" + }, "ROLES": { "PAGE_TITLE": "Rôles - {{projectName}}", "WARNING_NO_ROLE": "Attention, aucun rôle dans votre projet ne pourra estimer la valeur du point pour les récits utilisateurs", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "l'invitation à {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Valeur par défaut pour la sélection des points", - "LABEL_US": "Valeur par défaut pour la sélection du récit utilisateur", "LABEL_TASK_STATUS": "Valeur par défaut pour la sélection de l'état des tâches", - "LABEL_PRIORITY": "Valeur par défaut de la sélection des priorités", - "LABEL_SEVERITY": "Valeur par défaut pour le sélecteur de gravité", "LABEL_ISSUE_TYPE": "Valeur par défaut pour le sélecteur de type", - "LABEL_ISSUE_STATUS": "Valeur par défaut pour le sélecteur de statut de bug" + "LABEL_ISSUE_STATUS": "Valeur par défaut pour le sélecteur de statut de bug", + "LABEL_PRIORITY": "Valeur par défaut de la sélection des priorités", + "LABEL_SEVERITY": "Valeur par défaut pour le sélecteur de gravité" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Entrez le nom du nouvel état" @@ -681,7 +776,8 @@ "PRIORITIES": "Priorités", "SEVERITIES": "Gravité", "TYPES": "Types", - "CUSTOM_FIELDS": "Champs personnalisés" + "CUSTOM_FIELDS": "Champs personnalisés", + "TAGS": "Mots-clés" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Profil projet" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Voir tous", "FILTER_TYPE_PROJECTS": "Projets", "FILTER_TYPE_PROJECT_TITLES": "Voir uniquement les projets", + "FILTER_TYPE_EPICS": "Épopées", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Récits", "FILTER_TYPE_USER_STORIES_TITLES": "Voir uniquement les user stories", "FILTER_TYPE_TASKS": "Tâches", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Optionnel) Ajoutez un texte personnalisé à l'invitation. Dites quelque chose de gentil à vos nouveaux membres ;-)", "PLACEHOLDER_TYPE_EMAIL": "Saisissez une adresse courriel", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Désolé, ce projet ne peut avoir plus de {{maxMembers}} membres.
Si vous désirez augmenter cette limite, merci de contacter l'administrateur.", - "LIMIT_USERS_WARNING_MESSAGE": "Désolé, ce projet ne peut avoir plus de {{maxMembers}} membres." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Malheureusement, ce projet ne peut pas être laissé sans propriétaire", @@ -970,10 +1068,30 @@ "BUTTON": "Demander à ce membre du projet de devenir le nouveau propriétaire" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Épopée", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nouveau récit utilisateur", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Objet", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Récit utilisateur {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "État : {{userStoryStatus }}. Achevé {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} sur {{userStoryTotalTasks}} tâches fermées). Points : {{userStoryPoints}}. Description : {{userStoryDescription}}", - "SECTION_NAME": "Détails du récit utilisateur", + "SECTION_NAME": "Récit utilisateur", "LINK_TASKBOARD": "Tableau des tâches", "TITLE_LINK_TASKBOARD": "Aller au tableau des tâches", "TOTAL_POINTS": "total des points", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Ce récit utilisateur a été créé depuis", "GO_TO_EXTERNAL_REFERENCE": "Allez à l'origine", "BLOCKED": "Ce récit utilisateur est bloqué", - "PREVIOUS": "récit utilisateur précédent", - "NEXT": "récit utilisateur suivant", "TITLE_DELETE_ACTION": "Supprimer le récit utilisateur", "LIGHTBOX_TITLE_BLOKING_US": "Bloque le RU", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tâches complétées", "ASSIGN": "Affecter le récit utilisateur", "NOT_ESTIMATED": "Non estimé", "TOTAL_US_POINTS": "Total des points RU", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "Plus d'informations", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Modifier le lien", + "CLOSE": "Fermer", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Avez-vous besoin de quelqu'un pour cette tâche ?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Besoin projet", "CLIENT_REQUIREMENT": "Besoin client", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Commentaire supprimé par {{user}} le {{date}}", + "DELETED_INFO": "Commentaire supprimé par {{user}}", "TITLE": "Commentaires", + "COMMENTS_COUNT": "{{comments}} commentaires", + "ORDER": "Trier", + "OLDER_FIRST": "Plus ancien d'abord", + "RECENT_FIRST": "Plus récent d'abord", "COMMENT": "Commentaire", + "EDIT_COMMENT": "Modifier le commentaire", + "EDITED_COMMENT": "Modifié :", + "SHOW_HISTORY": "Voir l'historique", "TYPE_NEW_COMMENT": "Entrez un nouveau commentaire ici", "SHOW_DELETED": "Afficher le commentaire supprimé", "HIDE_DELETED": "Cacher le commentaire supprimé", "DELETE": "Supprimer le commentaire", - "RESTORE": "Restaurer le commentaire" + "RESTORE": "Restaurer le commentaire", + "HISTORY": { + "TITLE": "Activité" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Afficher l'activité", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Montrer les entrées précédentes ({{showMore}} plus)", "TITLE": "Activité", + "ACTIVITIES_COUNT": "{{activities}} activités", "REMOVED": "supprimé", "ADDED": "ajouté", - "US_POINTS": "Points du récit utilisateur ({{name}})", - "NEW_ATTACHMENT": "nouvelle pièce jointe", - "DELETED_ATTACHMENT": "Pièce jointe supprimée", - "UPDATED_ATTACHMENT": "Pièce jointe {{filename}} modifiée", - "DELETED_CUSTOM_ATTRIBUTE": "Attribut personnalisé supprimé", + "TAGS_ADDED": "Mots-clés ajoutés :", + "TAGS_REMOVED": "Mots-clés supprimés", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "Nouvelle pièce jointe", + "DELETED_ATTACHMENT": "Pièces jointes supprimées :", + "UPDATED_ATTACHMENT": "Pièces jointes mises à jour ({{filename}}) :", + "CREATED_CUSTOM_ATTRIBUTE": "Attribut personnalisé créé", + "UPDATED_CUSTOM_ATTRIBUTE": "Attribut personnalisé mis à jour", "SIZE_CHANGE": "A fait {size, plural, one{une modification} other{# modifications}}", + "BECAME_DEPRECATED": "devenu obsolète", + "BECAME_UNDEPRECATED": "n'est plus obsolète", + "TEAM_REQUIREMENT": "Besoin projet", + "CLIENT_REQUIREMENT": "Besoin client", + "BLOCKED": "Bloqué", "VALUES": { "YES": "oui", "NO": "no", @@ -1052,12 +1198,14 @@ "TAGS": "mots-clés", "ATTACHMENTS": "pièces jointes", "IS_DEPRECATED": "est obsolète", + "IS_NOT_DEPRECATED": "n'est pas obsolète", "ORDER": "classement", "BACKLOG_ORDER": "classement du backlog", "SPRINT_ORDER": "classement du sprint", "KANBAN_ORDER": "Classement du Kanban", "TASKBOARD_ORDER": "trier le tableau des tâches", - "US_ORDER": "classement des récits utilisateur" + "US_ORDER": "classement des récits utilisateur", + "COLOR": "couleur" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "tâches
fermées", "IOCAINE_DOSES": "doses
de iocaine", "SHOW_STATISTICS_TITLE": "Afficher les statistiques", - "TOGGLE_BAKLOG_GRAPH": "Afficher/masquer le graphique d'avancement" + "TOGGLE_BAKLOG_GRAPH": "Afficher/masquer le graphique d'avancement", + "POINTS_PER_ROLE": "Points par rôle" }, "SUMMARY": { "PROJECT_POINTS": "projet
points", @@ -1122,9 +1271,7 @@ "TITLE": "Filtres", "REMOVE": "Supprimer les filtres", "HIDE": "Cacher les filtres", - "SHOW": "Afficher les filtres", - "FILTER_CATEGORY_STATUS": "Etat", - "FILTER_CATEGORY_TAGS": "Mots-clés" + "SHOW": "Afficher les filtres" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Tâche {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "État : {{taskStatus }}. Description : {{taskDescription}}", - "SECTION_NAME": "Détails de la tâche", + "SECTION_NAME": "Tâche", "LINK_TASKBOARD": "Tableau des tâches", "TITLE_LINK_TASKBOARD": "Aller au tableau des tâches", "PLACEHOLDER_SUBJECT": "Entrez l'objet de la nouvelle tâche", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Cette tâche a été créée par", "TITLE_LINK_GO_ORIGIN": "Aller au récit utilisateur", "BLOCKED": "Cette tâche est bloquée", - "PREVIOUS": "tâche précédente", - "NEXT": "tâche suivante", "TITLE_DELETE_ACTION": "Supprimer une tâche", "LIGHTBOX_TITLE_BLOKING_TASK": "Tâche bloquante", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Tickets - {{projectName}}", "PAGE_DESCRIPTION": "Le panneau de la liste des tickets du projet {{projectName}} : {{projectDescription}}", "LIST_SECTION_NAME": "Tickets", - "SECTION_NAME": "Détails du ticket", + "SECTION_NAME": "Ticket", "ACTION_NEW_ISSUE": "+ NOUVEAU TICKET", "ACTION_PROMOTE_TO_US": "Promouvoir en récit utilisateur", - "PLACEHOLDER_FILTER_NAME": "Écrivez le nom du filtre et appuyez sur \"Entrée\"", "PROMOTED": "Le ticket a été promu en récit utilisateur", "EXTERNAL_REFERENCE": "Ce ticket a été créé à partir de", "GO_TO_EXTERNAL_REFERENCE": "Aller à l'origine", "BLOCKED": "Ce bug est bloqué", - "TITLE_PREVIOUS_ISSUE": "ticket précédent", - "TITLE_NEXT_ISSUE": "ticket suivant", "ACTION_DELETE": "Supprimer le ticket", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Ticket bloquant", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Promouvoir ce ticket en nouveau récit utilisateur", "MESSAGE": "Êtes-vous sure de vouloir créer un nouveau récit utilisateur à partir de ce ticket ?" }, - "FILTERS": { - "TITLE": "Filtres", - "INPUT_SEARCH_PLACEHOLDER": "Objet ou réf.", - "TITLE_ACTION_SEARCH": "Rechercher", - "ACTION_SAVE_CUSTOM_FILTER": "sauvegarder en tant que filtre personnalisé", - "BREADCRUMB": "Filtres", - "TITLE_BREADCRUMB": "Filtres", - "CATEGORIES": { - "TYPE": "Type", - "STATUS": "Statut", - "SEVERITY": "Gravité", - "PRIORITIES": "Priorités", - "TAGS": "Mots-clés", - "ASSIGNED_TO": "Affecté à", - "CREATED_BY": "Créé par", - "CUSTOM_FILTERS": "Filtres personnalisés" - }, - "CONFIRM_DELETE": { - "TITLE": "Supprime le filtre personnalisé", - "MESSAGE": "le filtre personnalisé '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Type", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Chercher - {{projectName}}", "PAGE_DESCRIPTION": "Chercher tout, récits utilisateurs, tickets, tâches ou pages de wiki, dans le projet {{projectName}} : {{projectDescription}}", + "FILTER_EPICS": "Épopées", "FILTER_USER_STORIES": "Récits utilisateur", "FILTER_ISSUES": "Tickets", "FILTER_TASKS": "Tâches", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Supprimer la page Wiki", "DELETE_LINK_TITLE": "Supprimer un lien Wiki", "NAVIGATION": { - "SECTION_NAME": "Liens", - "ACTION_ADD_LINK": "Ajouter un lien" + "HOME": "Page principale", + "SECTION_NAME": "SIGNETS", + "ACTION_ADD_LINK": "Ajouter un signet", + "ALL_PAGES": "Toutes les pages wiki" }, "SUMMARY": { "TIMES_EDITED": "modifications", "LAST_EDIT": "dernière
modification", "LAST_MODIFICATION": "dernière modification" + }, + "SECTION_PAGES_LIST": "Toutes les pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Titre", + "EDITIONS": "Modifications", + "CREATED": "Créé le", + "MODIFIED": "Modifié", + "CREATOR": "Créateur", + "LAST_MODIFIER": "Dernier modificateur" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} a créé une nouvelle tâche {{obj_name}} dans le projet {{project_name}} pour le récit utilisateur {{us_name}}", "WIKI_CREATED": "{{username}} a créé une nouvelle page wiki {{obj_name}} dans {{project_name}}", "MILESTONE_CREATED": "{{username}} a créé un nouveau sprint {{obj_name}} dans {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} a créé le projet {{project_name}}", "MILESTONE_UPDATED": "{{username}} a mis à jour le sprint {{obj_name}}", "US_UPDATED": "{{username}} a mis à jour l'attribut «{{field_name}}» du récit utilisateur {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} a mis à jour l'attribut «{{field_name}}» de la tâche {{obj_name}} qui appartient au récit utilisateur {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour la tâche {{obj_name}} appartenant au récit utilisateur {{us_name}}", "WIKI_UPDATED": "{{username}} a mis à jour la page wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} a commenté le récit utilisateur {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} a commenté le ticket {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} a commenté la tâche {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} a un nouveau membre", "US_ADDED_MILESTONE": "{{username}} a ajouté le récit utilisateur {{obj_name}} à {{sprint_name}}", "US_MOVED": "{{username}} a déplacé le RU {{obj_name}}", diff --git a/app/locales/taiga/locale-it.json b/app/locales/taiga/locale-it.json index 11b45bd6..f8d9b1d1 100644 --- a/app/locales/taiga/locale-it.json +++ b/app/locales/taiga/locale-it.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Un elemento per riga...", "NEW_BULK": "Nuovo inserimento nel carico", "RELATED_TASKS": "Compiti correlati", + "PREVIOUS": "Previous", + "NEXT": "Successivo", "LOGOUT": "Esci", "EXTERNAL_USER": "un utente esterno", "GENERIC_ERROR": "C'é uno dei nostri Oompa Loompa che dice {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Questo valore non è valido.", "TYPE_EMAIL": "Questo valore dovrebbe corrispondere ad una mail valida", @@ -115,8 +122,9 @@ "USER_STORY": "Storia utente", "TASK": "Compito", "ISSUE": "Problema", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Eccomi! taggami", + "PLACEHOLDER": "Enter tag", "DELETE": "Elimina tag", "ADD": "Aggiungi un tag" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Ricorda che tutti i valori in questo campo custom andranno persi.\nSei sicuro di voler continuare?" }, "FILTERS": { - "TITLE": "filtri", + "TITLE": "Filtri", "INPUT_PLACEHOLDER": "Soggetto o referenza", "TITLE_ACTION_FILTER_BUTTON": "cerca", - "BREADCRUMB_TITLE": "Indietro alle categorie", - "BREADCRUMB_FILTERS": "Filtri", - "BREADCRUMB_STATUS": "stato" + "INPUT_SEARCH_PLACEHOLDER": "Soggetto o referenza", + "TITLE_ACTION_SEARCH": "Cerca", + "ACTION_SAVE_CUSTOM_FILTER": "salva come filtro personalizzato", + "PLACEHOLDER_FILTER_NAME": "Scrivi il nome del filtro e premi invio", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tipo", + "STATUS": "Stato", + "SEVERITY": "Gravità", + "PRIORITIES": "Priorità", + "TAGS": "Tag", + "ASSIGNED_TO": "Assegnato a", + "CREATED_BY": "Creato da", + "CUSTOM_FILTERS": "Filtri personalizzati", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Elimina il filtro personalizzato", + "MESSAGE": "Il filtro personalizzato '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Intestazione di primo livello", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Anteprima", "EDIT_BUTTON": "Modifica", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Aiuto per la sintassi Markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Vedi gli sprint", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Vai alle storie utente", "ADD_USER_STORIES": "Aggiungi le storie utente", "MODIFY_USER_STORIES": "Modifica le storie utente", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Elimina le storie utente" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Guarda i compiti", "ADD_TASKS": "Aggiungi i compiti", "MODIFY_TASKS": "Modifica i compiti", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Elimina i compiti" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Guarda i problemi", "ADD_ISSUES": "Aggiungi un problema", "MODIFY_ISSUES": "Modifica i problemi", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Elimina i problemi" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Osservando", "DASHBOARD": "Dashboard Progetti" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Non assegnato" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Voti", + "NAME": "Nome", + "PROJECT": "Progetto", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Stato", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Bloccato", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "I miei progetti - Taiga", "PAGE_DESCRIPTION": "Una lista di tutti i tuoi progetti, la puoi riordinare o crearne una nuova.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Modifica valore", - "TITLE_ACTION_DELETE_VALUE": "Elimina valore" + "TITLE_ACTION_DELETE_VALUE": "Elimina valore", + "TITLE_ACTION_DELETE_TAG": "Elimina tag" }, "HELP": "Hai bisogno di aiuto? Controlla la nostra pagina di supporto!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Moduli", "ENABLE": "Abilita", "DISABLE": "Disabilita", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Amministra le storie degli utenti per mantenere una visione organizzata dei lavori in arrivo e di quelli ad alta priorità ", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Stai per modificare l'url di accesso al CSV. il precedente url verrá disabilitato. Sei sicuro?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "Report delle storie utente", "SECTION_TITLE_TASK": "Analisi dei compiti", "SECTION_TITLE_ISSUE": "Report criticitá", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Campi Personalizzati", "SUBTITLE": "Specifica i campi personalizzati per le tue Storie Utente, compiti e problemi", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Campi personalizzati delle storie utente", "US_ADD": "Aggiungi un campo personalizzato nelle storie utente", "TASK_DESCRIPTION": "Campi personalizzati dei Compiti", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Stato", "SUBTITLE": "Specifica lo stato delle storie utente, i compiti e i problemi saranno affrontati", - "US_TITLE": "Stato delle storie utente", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Stato dei compiti", "ISSUE_TITLE": "Stato dei problemi" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Tipi di criticitá", "ACTION_ADD": "Aggiungi {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Tag", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Aggiungi un tag", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Ruoli - {{projectName}}", "WARNING_NO_ROLE": "Attento, nessun ruolo, all'interno del tuo progetto, potrà stimare i punti valore per le storie utente", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "L'invito a {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Valore standard per punti di selezione", - "LABEL_US": "Valore predefinito per la selezione di stati delle storie utente", "LABEL_TASK_STATUS": "Valore predefinito per la selezione degli stati del compito", - "LABEL_PRIORITY": "Valore predefinito per la selezione prioritaria", - "LABEL_SEVERITY": "Valore predefinito per la selezione di rigore", "LABEL_ISSUE_TYPE": "Valore predefinito per il tipo di selezione del problema", - "LABEL_ISSUE_STATUS": "Valore predefinito per la selezione di stato del problema" + "LABEL_ISSUE_STATUS": "Valore predefinito per la selezione di stato del problema", + "LABEL_PRIORITY": "Valore predefinito per la selezione prioritaria", + "LABEL_SEVERITY": "Valore predefinito per la selezione di rigore" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Scrivi un nome per il nuovo status" @@ -681,7 +776,8 @@ "PRIORITIES": "priorità", "SEVERITIES": "Severitá", "TYPES": "Tipi", - "CUSTOM_FIELDS": "Campi personalizzati" + "CUSTOM_FIELDS": "Campi personalizzati", + "TAGS": "Tag" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Profilo progetto" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Mostra tutto", "FILTER_TYPE_PROJECTS": "Progetti", "FILTER_TYPE_PROJECT_TITLES": "Mostra solo i progetti", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Resoconti", "FILTER_TYPE_USER_STORIES_TITLES": "Mostra solo resoconti utente", "FILTER_TYPE_TASKS": "Compiti", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(facoltativo) aggiungi un testo personalizzato all'invito. Di qualcosa di simpatico ai tuoi nuovi membri ;-)", "PLACEHOLDER_TYPE_EMAIL": "Scrivi una mail", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nuova storia utente", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Oggetto", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completata per il {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} di {{userStoryTotalTasks}} tasks closed). Punti: {{userStoryPoints}}. Descrizione: {{userStoryDescription}}", - "SECTION_NAME": "Dettagli della storia utente", + "SECTION_NAME": "Storia utente", "LINK_TASKBOARD": "Pannello dei compiti", "TITLE_LINK_TASKBOARD": "Vai al pannello dei compiti", "TOTAL_POINTS": "totale punti", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Questo US é stato creato da", "GO_TO_EXTERNAL_REFERENCE": "Ritorna all'inizio", "BLOCKED": "Questa storia utente è bloccata", - "PREVIOUS": "Storia utente precedente ", - "NEXT": "Prossima storia utente", "TITLE_DELETE_ACTION": "Elimina la storia utente", "LIGHTBOX_TITLE_BLOKING_US": "Blocco la storia utente", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} compiti completati", "ASSIGN": "Assegna la storia utente", "NOT_ESTIMATED": "Non stimato", "TOTAL_US_POINTS": "Totale punti della storia utente", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Requisito del team", "CLIENT_REQUIREMENT": "Requisito del client", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Commento cancellato da {{user}} il {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Commenti", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Commento", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Scrivi un nuovo commento qui", "SHOW_DELETED": "Visualizza commento cancellato", "HIDE_DELETED": "Nascondi commento cancellato", "DELETE": "Cancella commento", - "RESTORE": "Ripristina commento" + "RESTORE": "Ripristina commento", + "HISTORY": { + "TITLE": "Attività" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Mostra attività", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "Mostra gli inserimenti precedenti ({{showMore}} more)", "TITLE": "Attività", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "rimosso", "ADDED": "aggiunto", - "US_POINTS": "punti storia utente ({{name}})", - "NEW_ATTACHMENT": "nuovo allegato", - "DELETED_ATTACHMENT": "allegato eliminato", - "UPDATED_ATTACHMENT": "Aggiornato l'allegato {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "elimina un attributo personalizzato", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Fatto {size, plural, one{un cambiamento} other{# cambiamenti}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Requisito del team", + "CLIENT_REQUIREMENT": "Requisito del client", + "BLOCKED": "Bloccato", "VALUES": { "YES": "si", "NO": "no", @@ -1052,12 +1198,14 @@ "TAGS": "tag", "ATTACHMENTS": "allegati", "IS_DEPRECATED": "è deprecato", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "ordine", "BACKLOG_ORDER": "Ordine di backlog", "SPRINT_ORDER": "Ordine dello sprint", "KANBAN_ORDER": "ordina kanban", "TASKBOARD_ORDER": "Ordine del pannello dei compiti", - "US_ORDER": "Ordine delle storie utente" + "US_ORDER": "Ordine delle storie utente", + "COLOR": "colore" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "
compiti chiusi", "IOCAINE_DOSES": "
pasticche di aspirina", "SHOW_STATISTICS_TITLE": "Mostra statistiche", - "TOGGLE_BAKLOG_GRAPH": "Mostra/nascondi i grafici burndown" + "TOGGLE_BAKLOG_GRAPH": "Mostra/nascondi i grafici burndown", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "
punti di progetto", @@ -1122,9 +1271,7 @@ "TITLE": "Filtri", "REMOVE": "Rimuovi filtri", "HIDE": "Nascondi Filtri", - "SHOW": "Mostra Filtri", - "FILTER_CATEGORY_STATUS": "Stato", - "FILTER_CATEGORY_TAGS": "Tag" + "SHOW": "Mostra Filtri" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Compiti {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Stato: {{taskStatus }}. Descrizione: {{taskDescription}}", - "SECTION_NAME": "Dettagli del compito", + "SECTION_NAME": "Compito", "LINK_TASKBOARD": "Pannello dei compiti", "TITLE_LINK_TASKBOARD": "Vai al pannello dei compiti", "PLACEHOLDER_SUBJECT": "Inserisci il soggetto del nuovo compito", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Questo compito è stato creato da", "TITLE_LINK_GO_ORIGIN": "Vai alla storia utente", "BLOCKED": "Questo compito è bloccato", - "PREVIOUS": "Compito precedente", - "NEXT": "Prossimo compito", "TITLE_DELETE_ACTION": "Rimuovi compito", "LIGHTBOX_TITLE_BLOKING_TASK": "Sto bloccando il compito", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Criticitá - {{projectName}}", "PAGE_DESCRIPTION": "Il pannello con la lista dei problemi del progetto {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "problemi", - "SECTION_NAME": "Dettagli della criticitá", + "SECTION_NAME": "Problema", "ACTION_NEW_ISSUE": "+ NUOVA CRITICITÁ", "ACTION_PROMOTE_TO_US": "Promuovi la storia utente", - "PLACEHOLDER_FILTER_NAME": "Scrivi il nome del filtro e premi invio", "PROMOTED": "Il problema è stato promosso a storia utente", "EXTERNAL_REFERENCE": "Questo problema è stato creato da ", "GO_TO_EXTERNAL_REFERENCE": "Ritorna all'inizio", "BLOCKED": "Questo problema è bloccato", - "TITLE_PREVIOUS_ISSUE": "problema precedente", - "TITLE_NEXT_ISSUE": "Problema successivo", "ACTION_DELETE": "Elimina problema", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Issue bloccante", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Promuovi questo problema come nuova storia utente", "MESSAGE": "Sei sicuro di voler creare una nuova storia utente da questo problema?" }, - "FILTERS": { - "TITLE": "Filtri", - "INPUT_SEARCH_PLACEHOLDER": "Soggetto o referenza", - "TITLE_ACTION_SEARCH": "Cerca", - "ACTION_SAVE_CUSTOM_FILTER": "salva come filtro personalizzato", - "BREADCRUMB": "Filtri", - "TITLE_BREADCRUMB": "Filtri", - "CATEGORIES": { - "TYPE": "Tipo", - "STATUS": "Stato", - "SEVERITY": "Gravità", - "PRIORITIES": "priorità", - "TAGS": "Tag", - "ASSIGNED_TO": "Assegna a", - "CREATED_BY": "Creato da", - "CUSTOM_FILTERS": "Filtri personalizzati" - }, - "CONFIRM_DELETE": { - "TITLE": "Elimina il filtro personalizzato", - "MESSAGE": "Il filtro personalizzato '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tipo", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Cerca - {{projectName}}", "PAGE_DESCRIPTION": "Cerca storie utenti, problemi, compiti o pagine wiki, all'interno del progetto {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Storie Utente", "FILTER_ISSUES": "problemi", "FILTER_TASKS": "Compiti", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Elimina Pagina Wiki", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Link", - "ACTION_ADD_LINK": "Aggiungi link" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "tempo
modificato", "LAST_EDIT": "
ultima modifica", "LAST_MODIFICATION": "ultima modifica" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Creato", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} ha creato un nuovo compito {{obj_name}} in {{project_name}} che appartiene alla storia utente {{us_name}}", "WIKI_CREATED": "{{username}} ha creato una nuova pagina wiki {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} ha creato un nuovo sprint {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} ha creato il progetto {{project_name}}", "MILESTONE_UPDATED": "{{username}} ha aggiornato lo sprint {{obj_name}}", "US_UPDATED": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" alla storia utente {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} che appartiene alla storia utente {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} che appartiene alla storia utente {{us_name}} a {{new_value}}", "WIKI_UPDATED": "{{username}} ha aggiornato la pagina wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} ha commentato nella storia utente {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} ha commentato nel problema {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} ha commentato nel compito {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} ha un nuovo membro", "US_ADDED_MILESTONE": "{{username}} ha aggiunto la storia utente {{obj_name}} a {{sprint_name}}", "US_MOVED": "{{username}} ha spostato la storia utente {{obj_name}}", diff --git a/app/locales/taiga/locale-nb.json b/app/locales/taiga/locale-nb.json new file mode 100644 index 00000000..4e9bad0e --- /dev/null +++ b/app/locales/taiga/locale-nb.json @@ -0,0 +1,1716 @@ +{ + "COMMON": { + "YES": "Ja", + "NO": "Nei", + "OR": "eller", + "LOADING": "Laster...", + "LOADING_PROJECT": "Laster prosjekt...", + "DATE": "DD MMM YYYY", + "DATETIME": "DD MMM YYYY HH:mm", + "SAVE": "Lagre", + "CANCEL": "Avbryt", + "ACCEPT": "Aksepter", + "DELETE": "Slett", + "CREATE": "Opprett", + "ADD": "Legg til", + "COPY_TO_CLIPBOARD": "Kopier til utklippstavlen: Ctrl+C", + "EDIT": "Endre", + "DRAG": "Dra", + "TAG_LINE": "Ditt smidige, gratis og åpen kildekode prosjektstyringsverktøy", + "TAG_LINE_2": "ELSK DITT PROSJEKT", + "BLOCK": "Blokker", + "BLOCK_TITLE": "Blokker elementet for eksempel hvis det har en avhengighet som ikke kan oppfylles", + "BLOCKED": "Blokkert", + "UNBLOCK": "Opphev blokkeringen", + "UNBLOCK_TITLE": "Avblokker dette elementet", + "BLOCKED_NOTE": "Hvorfor er dette blokkert?", + "BLOCKED_REASON": "Venligst forklar årsaken", + "CREATED_BY": "Opprettet av {{fullDisplayName}}", + "FROM": "fra", + "TO": "til", + "CLOSE": "lukk", + "GO_HOME": "Led meg hjem", + "PLUGINS": "Programtillegg", + "BETA": "Vi er i beta!", + "ONE_ITEM_LINE": "En enhet per linje...", + "NEW_BULK": "Ny sett (mange)", + "RELATED_TASKS": "Relaterte oppgaver", + "PREVIOUS": "Previous", + "NEXT": "Neste", + "LOGOUT": "Logg ut", + "EXTERNAL_USER": "en ekstern bruker", + "GENERIC_ERROR": "En av våre Oompa Loompaer sier {{error}}.", + "IOCAINE_TEXT": "Føler du deg litt overveldet av en oppgave? Sørg for at andre vet om det ved å klikke på \"Iocane\" når du redigerer en oppgave. Det er mulig å bli immun mot denne (fiktive) dødelige giften ved å konsumere små mengder over tid, akkurat som det er mulig å bli bedre på det du gjør ved av og til å ta på deg ekstra utfordringer!", + "CLIENT_REQUIREMENT": "Klientkravet er nytt krav som tidligere ikke var forventet, og det er nødt til å være en del av prosjektet", + "TEAM_REQUIREMENT": "TeamBehov er et behov som må eksistere i prosjektet, men som ikke har noen kostnad for klienten", + "OWNER": "Prosjekteier", + "CAPSLOCK_WARNING": "Vær forsiktig! Du bruker blokkbokstaver i et felt som er for store og små bokstaver.", + "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", + "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, + "FORM_ERRORS": { + "DEFAULT_MESSAGE": "Denne verdien virker å være ugyldig", + "TYPE_EMAIL": "Dette skal være en gyldig epost.", + "TYPE_URL": "Denne verdien skal være en URL", + "TYPE_URLSTRICT": "Denne verdien skal være en URL", + "TYPE_NUMBER": "Denne verdien skal være et gyldig tall.", + "TYPE_DIGITS": "Denne verdien skal være sifre.", + "TYPE_DATEISO": "Denne verdien skal være en gyldig dato (YYYY-MM-DD).", + "TYPE_ALPHANUM": "Denne verdien skal være alfanumerisk.", + "TYPE_PHONE": "Denne verdien skal være et gyldig telefonnummer.", + "NOTNULL": "Denne verdien skal ikke være \"null\"", + "NOT_BLANK": "Denne verdien skal ikke være blank.", + "REQUIRED": "Denne verdien er nødvendig.", + "REGEXP": "Denne verdien virker å være ugyldig", + "MIN": "Denne verdien skal være større eller lik %s.", + "MAX": "Denne verdien skal være mindre eller lik %s.", + "RANGE": "Denne verdien skal være mellom %s og %s.", + "MIN_LENGTH": "Denne verdien er for kort. Den skal ha %s eller flere bokstaver.", + "MAX_LENGTH": "Denne verdien er for lang. Den skal ha %s eller færre bokstaver.", + "RANGE_LENGTH": "Lengden på denne verdien er ikke gydlig. Den skal være mellom %s og %s teng lang.", + "MIN_CHECK": "Du må velge minst %s valg.", + "MAX_CHECK": "Du må velge %s valg eller mindre.", + "RANGE_CHECK": "Du må velge mellom %s og %s valg.", + "EQUAL_TO": "Denne verdien skal være den samme.", + "LINEWIDTH": "En eller flere av linjene er kanskje for lang. Prøv å hold de under %s tegn.", + "PIKADAY": "Ugyldig datoformat , kan du bruke DD MMM YYYY (eks: 23 mars 1984)" + }, + "PICKERDATE": { + "FORMAT": "DD MMM YYYY", + "IS_RTL": "falsk", + "FIRST_DAY_OF_WEEK": "1", + "PREV_MONTH": "Forrige Måned", + "NEXT_MONTH": "Neste Måned", + "MONTHS": { + "JAN": "Januar", + "FEB": "Februar", + "MAR": "Mars", + "APR": "April", + "MAY": "Mai", + "JUN": "Juni", + "JUL": "Juli", + "AUG": "August", + "SEP": "September", + "OCT": "Oktober", + "NOV": "November", + "DEC": "Desember" + }, + "WEEK_DAYS": { + "SUN": "Søndag", + "MON": "Mandag", + "TUE": "Tirsdag", + "WED": "Onsdag", + "THU": "Torsdag", + "FRI": "Fredag", + "SAT": "Lørdag" + }, + "WEEK_DAYS_SHORT": { + "SUN": "Man", + "MON": "Man", + "TUE": "Tirs", + "WED": "Ons", + "THU": "Tors", + "FRI": "Fre", + "SAT": "Lør" + } + }, + "SEE_USER_PROFILE": "Se See {{username }} profil", + "USER_STORY": "Brukerhistorie", + "TASK": "Oppgave", + "ISSUE": "Hendelse", + "EPIC": "Epic", + "TAGS": { + "PLACEHOLDER": "Enter tag", + "DELETE": "Slett etikett", + "ADD": "Legg til etikett" + }, + "DESCRIPTION": { + "EMPTY": "Tomme felter er så kjedelig... kom igjen, vær kreativ...", + "NO_DESCRIPTION": "Ingen beskrivelse enda" + }, + "FIELDS": { + "SUBJECT": "Subjekt", + "NAME": "Navn", + "URL": "URL", + "DESCRIPTION": "Beskrivelse", + "VALUE": "Verdi", + "SLUG": "Slug", + "COLOR": "Farge", + "IS_CLOSED": "Er lukket?", + "STATUS": "Status", + "TYPE": "Type", + "SEVERITY": "Alvorlighetsgrad", + "PRIORITY": "Prioritet", + "ASSIGNED_TO": "Tildelt til", + "POINTS": "Poeng", + "BLOCKED_NOTE": "blokkert notat", + "IS_BLOCKED": "er blokkert", + "REF": "Ref", + "VOTES": "Stemmer", + "SPRINT": "Sprint" + }, + "ROLES": { + "ALL": "Alle" + }, + "ASSIGNED_TO": { + "NOT_ASSIGNED": "Ikke tildelt", + "ASSIGN": "Tildel", + "DELETE_ASSIGNMENT": "Slett tildeling", + "REMOVE_ASSIGNED": "Fjern tildeling", + "TOO_MANY": "...for mange brukere, forstett å filtrere", + "CONFIRM_UNASSIGNED": "Er du sikker på at du vil la den være ikke tildelt?", + "TITLE_ACTION_EDIT_ASSIGNMENT": "Rediger tildeling", + "SELF": "Tildel til meg" + }, + "STATUS": { + "CLOSED": "Lukket", + "OPEN": "Åpen" + }, + "WATCHERS": { + "WATCHERS": "Følgere", + "ADD": "Legg til følgere", + "TITLE_ADD": "Legg til et prosjektmedlem til følgerlisten", + "DELETE": "Slett følger", + "TITLE_LIGHTBOX_DELETE_WARTCHER": "Slett følger..." + }, + "WATCH_BUTTON": { + "WATCH": "Følg", + "WATCHING": "Følger med på", + "UNWATCH": "Ikke overvåk", + "WATCHERS": "Følgere", + "BUTTON_TITLE": "Følg/Ikke følg denne enheten", + "COUNTER_TITLE": "{total, plural, one{en følger} other{# følgere}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Stem på", + "UPVOTED": "Stemt på", + "DOWNVOTE": "Stem ned", + "VOTERS": "Stemmegivere", + "BUTTON_TITLE": "Stem opp / Stem ned dette elementet", + "COUNTER_TITLE": "{total, plural, one{en stemme} other{# stemmer}}" + }, + "CUSTOM_ATTRIBUTES": { + "CUSTOM_FIELDS": "Egendefinerte felter", + "SAVE": "Lagre Egendefinert Felt", + "EDIT": "Endre Egendefinert Felt", + "DELETE": "Slett egendefinert egenskap", + "CONFIRM_DELETE": "Husk at alle verdier i dette egendefinerte feltet vil bli slettet.\nEr du sikker på at du vil fortsette?" + }, + "FILTERS": { + "TITLE": "Filtre", + "INPUT_PLACEHOLDER": "Subjekt eller referanse", + "TITLE_ACTION_FILTER_BUTTON": "Søk", + "INPUT_SEARCH_PLACEHOLDER": "Subjekt eller referanse", + "TITLE_ACTION_SEARCH": "Søk", + "ACTION_SAVE_CUSTOM_FILTER": "lagre som egendefinert filter", + "PLACEHOLDER_FILTER_NAME": "Skriv filternavnet og trykk enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Type", + "STATUS": "Status", + "SEVERITY": "Alvorlighetsgrad", + "PRIORITIES": "Prioriteter", + "TAGS": "Etiketter", + "ASSIGNED_TO": "Tildelt til", + "CREATED_BY": "Laget av", + "CUSTOM_FILTERS": "Egendefinert filtre", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Slett egendefinert filter", + "MESSAGE": "det egendefinerte filteret '{{customFilterName}}'" + } + }, + "WYSIWYG": { + "H1_BUTTON": "Første Nivå Overskrift", + "H1_SAMPLE_TEXT": "Din tittel her...", + "H2_BUTTON": "Andre Nivå Overskrift", + "H2_SAMPLE_TEXT": "Din tittel her...", + "H3_BUTTON": "Tredje Nivå Overskrift", + "H3_SAMPLE_TEXT": "Din tittel her...", + "BOLD_BUTTON": "Fet", + "BOLD_BUTTON_SAMPLE_TEXT": "Din tekst her...", + "ITALIC_BUTTON": "Kursiv", + "ITALIC_SAMPLE_TEXT": "Din tekst her...", + "STRIKE_BUTTON": "Gjennomstrek", + "STRIKE_SAMPLE_TEXT": "Din tekst her...", + "BULLETED_LIST_BUTTON": "Punktliste", + "BULLETED_LIST_SAMPLE_TEXT": "Din tekst her...", + "NUMERIC_LIST_BUTTON": "Numerisk liste", + "NUMERIC_LIST_SAMPLE_TEXT": "Din tekst her...", + "PICTURE_BUTTON": "Bilde", + "PICTURE_SAMPLE_TEXT": "Din alternative tekst til bildet her...", + "LINK_BUTTON": "Lenke", + "LINK_SAMPLE_TEXT": "Din tekst til lenken her...", + "QUOTE_BLOCK_BUTTON": "Sitatblokk", + "QUOTE_BLOCK_SAMPLE_TEXT": "Din tekst her...", + "CODE_BLOCK_BUTTON": "Kodeblokk", + "CODE_BLOCK_SAMPLE_TEXT": "Din tekst her...", + "PREVIEW_BUTTON": "Forhåndsvisning", + "EDIT_BUTTON": "Endre", + "ATTACH_FILE_HELP": "Legg ved filer ved å dra og slippe på tekstområdet ovenfor.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", + "MARKDOWN_HELP": "Markdown syntaks hjelp" + }, + "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, + "SPRINTS": { + "NAME": "Sprinter", + "VIEW_SPRINTS": "Se sprinter", + "ADD_SPRINTS": "Legg til sprinter", + "MODIFY_SPRINTS": "Rediger sprinter", + "DELETE_SPRINTS": "Slett sprinter" + }, + "USER_STORIES": { + "NAME": "Brukerhistorie", + "VIEW_USER_STORIES": "Se på brukerhistorier", + "ADD_USER_STORIES": "Legg til brukerhistorier", + "MODIFY_USER_STORIES": "Rediger brukerhistorier", + "COMMENT_USER_STORIES": "Comment user stories", + "DELETE_USER_STORIES": "Slett brukerhistorier" + }, + "TASKS": { + "NAME": "Oppgaver", + "VIEW_TASKS": "Se på oppgaver", + "ADD_TASKS": "Legg til oppgave", + "MODIFY_TASKS": "Rediger oppgaver", + "COMMENT_TASKS": "Comment tasks", + "DELETE_TASKS": "Slett oppgaver" + }, + "ISSUES": { + "NAME": "Hendelser", + "VIEW_ISSUES": "Vis hendelser", + "ADD_ISSUES": "Legg til hendelser", + "MODIFY_ISSUES": "Endre hendelser", + "COMMENT_ISSUES": "Comment issues", + "DELETE_ISSUES": "Slett hendelser" + }, + "WIKI": { + "NAME": "Wiki", + "VIEW_WIKI_PAGES": "Se wiki-sider", + "ADD_WIKI_PAGES": "Legg til wiki-sider", + "MODIFY_WIKI_PAGES": "Endre wiki-sider", + "DELETE_WIKI_PAGES": "Slett wiki-sider", + "VIEW_WIKI_LINKS": "Se wiki-lenker", + "ADD_WIKI_LINKS": "Legg til wiki-lenker", + "DELETE_WIKI_LINKS": "Slett wiki-lenker" + } + }, + "META": { + "PAGE_TITLE": "Taiga", + "PAGE_DESCRIPTION": "Taiga er en prosjektstyringsplatform for oppstartsbedrifter og agile utviklere & designere som vil ha et enkelt, nydelig verktøy som gjør arbeidet virkelig hyggelig." + } + }, + "LOGIN": { + "PAGE_TITLE": "Login - Taiga", + "PAGE_DESCRIPTION": "Logg på Taiga, prosjektledelsesplattformen for startups, smidige utviklere og designere som ønsker et enkelt og pent verktøy som gjør arbeidet en fornøyelse." + }, + "AUTH": { + "INVITED_YOU": "har invitert deg til å delta i prosjektet", + "NOT_REGISTERED_YET": "Ikke registrert?", + "REGISTER": "Registrer", + "CREATE_ACCOUNT": "opprett gratis konto her" + }, + "LOGIN_COMMON": { + "HEADER": "Jeg har allerede en konto", + "PLACEHOLDER_AUTH_NAME": "Brukernavn eller e-post (case sensitiv)", + "LINK_FORGOT_PASSWORD": "Glemte det?", + "TITLE_LINK_FORGOT_PASSWORD": "Har du glemt ditt passord?", + "ACTION_ENTER": "Enter", + "ACTION_SIGN_IN": "Login", + "PLACEHOLDER_AUTH_PASSWORD": "Passord (case sensitiv)" + }, + "LOGIN_FORM": { + "ERROR_AUTH_INCORRECT": "Ifølge våre Oompa Loompaer er ditt brukernavn, e-post eller passord feil.", + "SUCCESS": "Våre Oompa Loompaer er glade, velkommen til Taiga ." + }, + "REGISTER": { + "PAGE_TITLE": "Registrer - Taiga", + "PAGE_DESCRIPTION": "Opprett en konto i Taiga , prosjektledelsesplattformen for startups, smidige utviklere og designere som ønsker et enkelt og pent verktøy som gjør arbeidet en fornøyelse." + }, + "REGISTER_FORM": { + "TITLE": "Registrer en ny Taiga konto (gratis)", + "PLACEHOLDER_NAME": "Velg et brukernavn (case sensitiv)", + "PLACEHOLDER_FULL_NAME": "Skriv inn fullt navn", + "PLACEHOLDER_EMAIL": "Din e-post", + "PLACEHOLDER_PASSWORD": "Set et passord (case sensitiv)", + "ACTION_SIGN_UP": "Registrer", + "TITLE_LINK_LOGIN": "Logg inn", + "LINK_LOGIN": "Er du allerede registrert? Logg inn" + }, + "FORGOT_PASSWORD": { + "PAGE_TITLE": "Glemt passord - Taiga", + "PAGE_DESCRIPTION": "Skriv inn brukernavn eller e-post for å få et nytt passord, og du kan få tilgang til Taiga igjen." + }, + "FORGOT_PASSWORD_FORM": { + "TITLE": "Obs, har du glemt passordet ditt?", + "SUBTITLE": "Skriv inn ditt brukernavn eller e-post for å få et nytt", + "PLACEHOLDER_FIELD": "Brukernavn eller e-post", + "ACTION_RESET_PASSWORD": "Nullstill passord", + "LINK_CANCEL": "Eh, ta meg tilbake. Jeg tror jeg husker det.", + "SUCCESS_TITLE": "Sjekk innboksen din!", + "SUCCESS_TEXT": "\nVi har sendte deg en e-post med instruksjoner for å sette et nytt passord", + "ERROR": "I følge våre Oompa Loompas har du ikke registert deg ennå." + }, + "CHANGE_PASSWORD": { + "PAGE_TITLE": "Endre ditt passord - Taiga", + "PAGE_DESCRIPTION": "Angi et nytt passord for din Taiga konto og hei!, det kan være lurt å spise litt mer jern-rik mat, det er godt for hjernen din :P", + "SECTION_NAME": "Endre passord", + "FIELD_CURRENT_PASSWORD": "Nåværende passord", + "PLACEHOLDER_CURRENT_PASSWORD": "Din nåværende passord (eller tomt hvis du ikke har ett)", + "FIELD_NEW_PASSWORD": "Nytt passord", + "PLACEHOLDER_NEW_PASSWORD": "Skriv inn nytt passord", + "FIELD_RETYPE_PASSWORD": "Gjenta nytt passord", + "PLACEHOLDER_RETYPE_PASSWORD": "Skriv inn passordet på nytt", + "ERROR_PASSWORD_MATCH": "Passordene er ikke like" + }, + "CHANGE_PASSWORD_RECOVERY_FORM": { + "TITLE": "Opprett ett nytt Taiga pass", + "SUBTITLE": "Og hei, det kan være lurt å spise litt mer jern-rik mat, det er godt for hjernen din :P", + "PLACEHOLDER_NEW_PASSWORD": "Nytt passord", + "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Skriv inn passord på nytt", + "ACTION_RESET_PASSWORD": "Nullstill passord", + "ERROR": "Våre Oompa Loompaer finner ikke forespørselen om å gjenopprette passordet ditt. Prøv å å gjennopprette en gang til.", + "SUCCESS": "Oompa Loompaene våre lagret ditt nye passord.
Prøv å logge inn med det." + }, + "INVITATION": { + "PAGE_TITLE": "Invitasjonsbekreftelse - Taiga", + "PAGE_DESCRIPTION": "Godta invitasjonen til å delta i et prosjekt i Taiga, prosjektledelsesplattformen for startups, smidige utviklere og designere som ønsker et enkelt og pent verktøy som gjør arbeidet fornøyelig." + }, + "INVITATION_LOGIN_FORM": { + "NOT_FOUND": "Våre Oompa Loompas can ikke finne invitasjonen din.", + "SUCCESS": "Du er nå sluttet til prosjektet {{project_name}}, velkommen!", + "ERROR": "I følge våre Oompa Loompas har du ikke registert deg ennå eller skrevet et ugyldig passord" + }, + "HOME": { + "PAGE_TITLE": "Hjem - Taiga", + "PAGE_DESCRIPTION": "Taiga hjemmeside med dine viktigste prosjekter og alle dine tildelte og overvåkede brukerhistorier, oppgaver og hendelser", + "EMPTY_WORKING_ON": "Det føles tom, gjør det ikke? Start å jobbe med Taiga og du vil se her brukerhistorier, opggaver og hendelser du jobber med.", + "EMPTY_WATCHING": "Følg Brukerhistorier, Oppgaver, Hendelser i dine projekter og bli varslet når noe blir endret :)", + "EMPTY_PROJECT_LIST": "Du har ikke noen prosjekter enda", + "WORKING_ON_SECTION": "Arbeider med", + "WATCHING_SECTION": "Følger med på", + "DASHBOARD": "Prosjektoversikt" + }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Ikke tildelt" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Stemmer", + "NAME": "Navn", + "PROJECT": "Prosjekt", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Status", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Blokkert", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, + "PROJECTS": { + "PAGE_TITLE": "Mine prosjekter - Taiga", + "PAGE_DESCRIPTION": "Liste med alle dine prosjekter du kan endre rekkefølgen på eller opprette et nytt.", + "MY_PROJECTS": "Mine prosjekter" + }, + "ATTACHMENT": { + "SECTION_NAME": "vedlegg", + "TITLE": "{{ fileName }} lastet opp {{ date }}", + "LIST_VIEW_MODE": "Listevisnings-modus", + "GALLERY_VIEW_MODE": "Gallerivisningsmodus", + "DESCRIPTION": "Skriv en kort beskrivelse", + "DEPRECATED": "(avviklet)", + "DEPRECATED_FILE": "Avviklet?", + "ADD": "Legg til nytt vedlegg. {{maxFileSizeMsg}}", + "DROP": "Slipp vedlegg her!", + "SHOW_DEPRECATED": "Vis foreldete vedlegg", + "HIDE_DEPRECATED": "- skjul foreldete vedlegg", + "COUNT_DEPRECATED": "({{ counter }} avviklet)", + "MAX_UPLOAD_SIZE": "Maksimal størrelse for opplastning er {{maxFileSize}}", + "DATE": "DD MMM YYYY [at] hh:mm", + "ERROR_UPLOAD_ATTACHMENT": "Vi var ikke kapable til å laste opp '{{fileName}}'. {{errorMessage}}", + "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Slett vedlegg...", + "MSG_LIGHTBOX_DELETE_ATTACHMENT": "vedlegget '{{fileName}}'", + "ERROR_DELETE_ATTACHMENT": "Vi hadde ikke mulighet til å slette: {{errorMessage}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) er for stor for våre Oompa Loompaer. Prøv med en mindre enn ({{maxFileSize}})", + "FIELDS": { + "IS_DEPRECATED": "Er foreldet" + } + }, + "PAGINATION": { + "PREVIOUS": "Forrige", + "NEXT": "Neste" + }, + "ADMIN": { + "COMMON": { + "TITLE_ACTION_EDIT_VALUE": "Rediger verdi", + "TITLE_ACTION_DELETE_VALUE": "Slett verdi", + "TITLE_ACTION_DELETE_TAG": "Slett etikett" + }, + "HELP": "Trenger du hjelp? Sjekk ut vår hjelpeside!", + "PROJECT_DEFAULT_VALUES": { + "TITLE": "Standard verdier", + "SUBTITLE": "Sett standard verdi for alle valgte felter." + }, + "MEMBERSHIPS": { + "TITLE": "Konfigurer medlemmer", + "PAGE_TITLE": "Medlemskap - {{projectName}}", + "ADD_BUTTON": "+ Nytt medlem", + "ADD_BUTTON_TITLE": "Legg til nytt medlem", + "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Dessverre, dette prosjektet har nådd sin grense på ({{members}}) tillatte medlemmer.", + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Dette prosjektet har nådd sin grense for ({{members}}) tillatte medlemmer. Dersom du ønsker å øke den grensen, venligst kontakt en administrator." + }, + "PROJECT_EXPORT": { + "TITLE": "Eksport", + "SUBTITLE": "Eksportér prosjektet for å lagre en backup, eller for å lage et nytt en basert på dette.", + "EXPORT_BUTTON": "Eksport", + "EXPORT_BUTTON_TITLE": "Eksporter ditt prosjekt", + "LOADING_TITLE": "Vi genererer din eksportfil", + "DUMP_READY": "Din data-dump-fil er klar!", + "LOADING_MESSAGE": "Vennligst ikke lukk denne siden.", + "ASYNC_MESSAGE": "Vi sender deg en epost når det er klart.", + "SYNC_MESSAGE": "Hvis nedlastningen ikke starter automatisk, klikk her.", + "ERROR": "Våre Oompa Loompaer har problemer med å generere din data dump. Vennligst prøv på nytt.", + "ERROR_BUSY": "Beklager, våre Oompa Loomper er svært opptatt akkuratt nå. Venligst prøv igjen om noen få minutter.", + "ERROR_MESSAGE": "Våre Oompa Loompaer har problemer med å generere din dump: {{message}}" + }, + "MODULES": { + "TITLE": "Moduler", + "ENABLE": "Aktiver", + "DISABLE": "Deaktiver", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", + "BACKLOG": "Backlog", + "BACKLOG_DESCRIPTION": "Hold styr på dine brukerhistorier for å vedlikeholde en organisert visning over kommende og prioritert arbeid.", + "NUMBER_SPRINTS": "Antatt antall sprinter", + "NUMBER_SPRINTS_HELP": "0 for et ubestemt antall", + "NUMBER_US_POINTS": "Antatt total av brukerhistoriepoeng", + "NUMBER_US_POINTS_HELP": "0 for et ubestemt antall", + "KANBAN": "Kanban", + "KANBAN_DESCRIPTION": "Organiser prosjektet ditt på en avabalansert måte med dette panelet.", + "ISSUES": "Hendelser", + "ISSUES_DESCRIPTION": "Forfølg feil, spørsmål og forbedringer relatert til ditt prosjekt. Ikke gå glipp av noe!", + "WIKI": "Wiki", + "WIKI_DESCRIPTION": "Legg til, endre eller slette innhold i samarbeid med andre. Dette er det rette stedet for din prosjektdokumentasjon.", + "MEETUP": "Møtes", + "MEETUP_DESCRIPTION": "Velg ditt videokonferansesystem", + "SELECT_VIDEOCONFERENCE": "Velg et videokonferansesystem", + "SALT_CHAT_ROOM": "Legg et prefiks til navnet til chatterommet", + "JITSI_CHAT_ROOM": "Jitsl", + "APPEARIN_CHAT_ROOM": "Vises", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Egendefinert", + "URL_CHAT_ROOM": "URL til ditt chatrom" + }, + "PROJECT_PROFILE": { + "PAGE_TITLE": "{{sectionName}} - Prosjektprofil - {{projectName}}", + "PROJECT_DETAILS": "Prosjektdetaljer", + "PROJECT_NAME": "Prosjektnavn", + "PROJECT_SLUG": "Prosjekt slug", + "TAGS": "Etiketter", + "DESCRIPTION": "Beskrivelse", + "RECRUITING": "Leter dette prosjektet etter folk?", + "RECRUITING_MESSAGE": "Hva leter du etter?", + "RECRUITING_PLACEHOLDER": "Definer profilene du leter etter", + "PUBLIC_PROJECT": "Offentlig prosjekt", + "PRIVATE_PROJECT": "Privat prosjekt", + "PRIVATE_OR_PUBLIC": "Hva er forskjellen mellom offentlige og private prosjekt?", + "DELETE": "Slett dette prosjektet", + "LOGO_HELP": "Bildet vil bli skalert til 80x80px.", + "CHANGE_LOGO": "Endre logo", + "ACTION_USE_DEFAULT_LOGO": "Bruk standardbilde", + "MAX_PRIVATE_PROJECTS": "Du har nådd maksimalt antall private prosjekter som tillates med din gjeldende plan", + "MAX_PRIVATE_PROJECTS_MEMBERS": "Det maksimale antall medlemmer for private prosjekter er overskredet", + "MAX_PUBLIC_PROJECTS": "Dessverre , du har nådd det maksimale antallet offentlige prosjekter som tillates av din gjeldende plan", + "MAX_PUBLIC_PROJECTS_MEMBERS": "Prosjektet overskrider maksimalt antall medlemmer for offentlige prosjekter", + "PROJECT_OWNER": "Prosjekteier", + "REQUEST_OWNERSHIP": "Be om eierskap", + "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Ønsker du å bli den nye prosjekteieren?", + "REQUEST_OWNERSHIP_DESC": "Be om at den nåværende prosjekteieren {{name}} overfører eierskapet av dette prosjektet til deg.", + "REQUEST_OWNERSHIP_BUTTON": "Be om", + "REQUEST_OWNERSHIP_SUCCESS": "Vi vil varsle prosjekteieren", + "CHANGE_OWNER": "Endre eier", + "CHANGE_OWNER_SUCCESS_TITLE": "Ok, din henvendelse har blitt sendt!", + "CHANGE_OWNER_SUCCESS_DESC": "Vi vil varsle deg via epost hvis anmodningen om eierskap til prosjektet ble godtatt eller avslått" + }, + "REPORTS": { + "TITLE": "Rapporter", + "SUBTITLE": "Eksporter dine prosjektdata i CSV-format og lag dine egne rapporter.", + "DESCRIPTION": "Last ned en CSV fil eller kopier den genererte URL'en og åpne den i din favoritt tekstbehandler for å lage dine egne prosjektdatarapporter. Du vil kunne visualisere og analysere alle dine data enkelt.", + "HELP": "Hvordan bruke dette i mitt eget regneark?", + "REGENERATE_TITLE": "Endre URL", + "REGENERATE_SUBTITLE": "Dette vil endre CSV datatilgangsURLen. Den forrige URLen vil bli deaktivert. Er du sikker?" + }, + "CSV": { + "SECTION_TITLE_EPIC": "epics reports", + "SECTION_TITLE_US": "brukerhistorie-rapporter", + "SECTION_TITLE_TASK": "oppgaverapport", + "SECTION_TITLE_ISSUE": "Hendelsesrapport", + "DOWNLOAD": "Last ned CSV", + "URL_FIELD_PLACEHOLDER": "Venligst regenerer CSV url", + "TITLE_REGENERATE_URL": "Regenerer CSV url", + "ACTION_GENERATE_URL": "Generer URL", + "ACTION_REGENERATE": "Generer på nytt" + }, + "CUSTOM_FIELDS": { + "TITLE": "Egendefinerte felter", + "SUBTITLE": "Spesifiser egendefinerte felter for dine brukerhistorier, oppgaver og hendelser", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", + "US_DESCRIPTION": "Brukerhistorier tilpassede felter", + "US_ADD": "Legg til et egendefinert felt i Brukerhistorier", + "TASK_DESCRIPTION": "Oppgaver egendefinerte felter", + "TASK_ADD": "Legg til et egendefinert felt i flere oppgaver", + "ISSUE_DESCRIPTION": "Hendelser egendefinerte felter", + "ISSUE_ADD": "Legg til et egendefinert felt i Hendelser", + "FIELD_TYPE_TEXT": "Tekst", + "FIELD_TYPE_MULTI": "Flere linjer", + "FIELD_TYPE_DATE": "Dato", + "FIELD_TYPE_URL": "Url" + }, + "PROJECT_VALUES": { + "PAGE_TITLE": "{{sectionName}} - Prosjekt verdier - {{projectName}}", + "REPLACEMENT": "Alle elementer med denne verdien vil bli endre til", + "ERROR_DELETE_ALL": "Du kan ikke slette alle verdier." + }, + "PROJECT_VALUES_POINTS": { + "TITLE": "Poeng", + "SUBTITLE": "Spesifiser poengene dine brukerhistorier kan estimeres som", + "US_TITLE": "BH poeng", + "ACTION_ADD": "Legg til nytt poeng" + }, + "PROJECT_VALUES_PRIORITIES": { + "TITLE": "Prioriteter", + "SUBTITLE": "Angi prioriteringen som dine hendelser vil ha", + "ISSUE_TITLE": "Hendelse prioriteringer", + "ACTION_ADD": "Legg til ny prioritet" + }, + "PROJECT_VALUES_SEVERITIES": { + "TITLE": "Alvorlighetsgrad", + "SUBTITLE": "Spesifiser alvorlighetsgraden som hendelsen vil ha", + "ISSUE_TITLE": "Hendelse alvorlighetsgrad", + "ACTION_ADD": "Legg til ny alvorlighetsgrad" + }, + "PROJECT_VALUES_STATUS": { + "TITLE": "Status", + "SUBTITLE": "Angi statusene som dine brukerhistorier, oppgaver og hendelser vil følge", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", + "TASK_TITLE": "Oppgave Statuser", + "ISSUE_TITLE": "Hendelsesstatuser" + }, + "PROJECT_VALUES_TYPES": { + "TITLE": "Typer", + "SUBTITLE": "Angi hvilke typer dine hendelser kan være", + "ISSUE_TITLE": "Hendelsestyper", + "ACTION_ADD": "Legg til {{objName}}" + }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Etiketter", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Legg til etikett", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, + "ROLES": { + "PAGE_TITLE": "Roller - {{projectName}}", + "WARNING_NO_ROLE": "Vær forsiktig, ingen roller i ditt prosjekt vil kunne estimere poengverdier for brukerhistorier", + "HELP_ROLE_ENABLED": "Når aktivert; medlemmer tildelt denne rollen vil kunne estimere poengverdier for brukerhistorier", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Er du sikker på at du vil deaktivere denne rollens beregninger?", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "Hvis du deaktiverer estimeringstillatelser for rollen {{roleName}} vil alle tidligere anslag gjort av denne rollen bli fjernet", + "COUNT_MEMBERS": "{{ role.members_count }} medlemmer med denne rollen", + "TITLE_DELETE_ROLE": "Slett rolle", + "REPLACEMENT_ROLE": "Alle brukerene med denne rollen vil bli flyttet til", + "WARNING_DELETE_ROLE": "Vær forsiktig! Alle rolle-estimatene vil bli fjernet", + "ERROR_DELETE_ALL": "Du kan ikke slette alle verdier", + "EXTERNAL_USER": "Ekstern bruker" + }, + "THIRD_PARTIES": { + "SECRET_KEY": "Hemmelig nøkkel", + "PAYLOAD_URL": "Nyttelast URL", + "VALID_IPS": "Gyldige opprinnelses IP-adresser (atskilt med,)" + }, + "BITBUCKET": { + "SECTION_NAME": "Bitbucket", + "PAGE_TITLE": "Bitbucket - {{projectName}}", + "INFO_VERIFYING_IP": "Bitbucket forespørsler er ikke signert så den beste måten å verifisere opphavet på er med IP. Hvis feltet er tomt blir det ingen IP validering." + }, + "GITLAB": { + "SECTION_NAME": "Gitlab", + "PAGE_TITLE": "Gitlab - {{projectName}}", + "INFO_VERIFYING_IP": "GitHub-forespørsler er ikke signert så den beste måten å verifisere opprinnelsen på er med IP. Hvis feltet er tomt blir det ingen IP validering." + }, + "GITHUB": { + "SECTION_NAME": "Github", + "PAGE_TITLE": "Github - {{projectName}}" + }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, + "WEBHOOKS": { + "PAGE_TITLE": "Webhooks - {{projectName}}", + "SECTION_NAME": "Webkoblinger", + "SUBTITLE": "Webkoblinger varsler eksterne tjenester om hendelser i Taiga, som kommentarer, brukerhistorier ....", + "ADD_NEW": "Legg til en ny webkobling", + "TYPE_NAME": "Skriv inn tjenestenavn", + "TYPE_PAYLOAD_URL": "Skriv inn tjenestens payload url", + "TYPE_SERVICE_SECRET": "Skriv inn tjenesten hemmelige nøkkel", + "SAVE": "Lagre webkobling", + "CANCEL": "Fjern Webkobling", + "SHOW_HISTORY": "(Vis historie)", + "TEST": "Test Webkobling", + "EDIT": "Rediger Webkobling", + "DELETE": "Slett Webkobling", + "REQUEST": "Be om", + "RESEND_REQUEST": "Send forespørsel på nytt", + "HEADERS": "Overskrifter", + "PAYLOAD": "Payload", + "RESPONSE": "Respons", + "DATE": "DD MMM YYYY [at] hh:mm:ss", + "ACTION_HIDE_HISTORY": "(Skjul historikk)", + "ACTION_HIDE_HISTORY_TITLE": "Skjul historikk detaljer", + "ACTION_SHOW_HISTORY": "(Vis historie)", + "ACTION_SHOW_HISTORY_TITLE": "Vis historiedetaljer", + "WEBHOOK_NAME": "Webkobling \"{{name}}\"" + }, + "CUSTOM_ATTRIBUTES": { + "PAGE_TITLE": "{{Seksjon navn}} - Egendefinerte felter - {{prosjekt Name}}", + "ADD": "Legg til et egendefinert felt", + "EDIT": "Endre Egendefinert Felt", + "DELETE": "Slett Egendefinert felt", + "SAVE_TITLE": "Lagre Egendefinert Felt", + "CANCEL_TITLE": "Avbryt opprettelsen", + "SET_FIELD_NAME": "Sett navnet til ditt egendefinerte felt", + "SET_FIELD_DESCRIPTION": "Gi en beskrivelse til ditt egendefinerte felt", + "FIELD_TYPE_DEFAULT": "-- velg en --", + "ACTION_UPDATE": "Oppdater Egendefinert Felt", + "ACTION_CANCEL_EDITION": "Avbryt redigering" + }, + "MEMBERSHIP": { + "COLUMN_MEMBER": "Medlem", + "COLUMN_ADMIN": "Admin", + "COLUMN_ROLE": "Rolle", + "COLUMN_STATUS": "Status", + "STATUS_ACTIVE": "Aktiv", + "STATUS_PENDING": "Avventer", + "DELETE_MEMBER": "Slett medlem", + "RESEND": "Send igjen", + "SUCCESS_SEND_INVITATION": "Vi har sendt invitasjonen på nytt til '{{email}}'.", + "ERROR_SEND_INVITATION": "Vi har ikke sendt invitasjonen.", + "SUCCESS_DELETE": "Vi har slettet {{message}}.", + "ERROR_DELETE": "Vi har ikke vært i stand til å slette {{message}}.", + "DEFAULT_DELETE_MESSAGE": "invitasjonen til {{epost}}" + }, + "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", + "LABEL_POINTS": "Standardverdi for poengvelger", + "LABEL_TASK_STATUS": "Standardverdi for oppgave statusvelger", + "LABEL_ISSUE_TYPE": "Standard verdi for hendelses type-velger", + "LABEL_ISSUE_STATUS": "Standard verdi for hendelse statusvelger", + "LABEL_PRIORITY": "Standardverdi for prioritetsvelger", + "LABEL_SEVERITY": "Standardverdi for alvorlighetsgradsvelgeren" + }, + "STATUS": { + "PLACEHOLDER_WRITE_STATUS_NAME": "Skriv et navn til den nye statusen" + }, + "TYPES": { + "PLACEHOLDER_WRITE_NAME": "Skriv et navn til det nye elementet" + }, + "US_STATUS": { + "ACTION_ADD_STATUS": "Legg til ny status", + "IS_ARCHIVED_COLUMN": "Er arkivert?", + "WIP_LIMIT_COLUMN": "WIP Grense", + "PLACEHOLDER_WRITE_NAME": "Skriv et navn til den nye statusen" + }, + "MENU": { + "TITLE": "Admin", + "PROJECT": "Prosjekt", + "ATTRIBUTES": "Egenskaper", + "MEMBERS": "Medlemmer", + "PERMISSIONS": "Tilganger", + "INTEGRATIONS": "Integrasjoner", + "PLUGINS": "Programtillegg" + }, + "SUBMENU_PROJECT_ATTRIBUTES": { + "TITLE": "Egenskaper" + }, + "SUBMENU_PROJECT_VALUES": { + "STATUS": "Status", + "POINTS": "Poeng", + "PRIORITIES": "Prioriteter", + "SEVERITIES": "Alvorlighetsgrad", + "TYPES": "Typer", + "CUSTOM_FIELDS": "Egendefinerte felter", + "TAGS": "Etiketter" + }, + "SUBMENU_PROJECT_PROFILE": { + "TITLE": "Prosjektprofil" + }, + "SUBMENU_ROLES": { + "TITLE": "Roller", + "ACTION_NEW_ROLE": "+ Ny rolle", + "TITLE_ACTION_NEW_ROLE": "Legg til ny rolle" + }, + "SUBMENU_THIDPARTIES": { + "TITLE": "Tjenester" + }, + "PROJECT_TRANSFER": { + "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Har du lyst til å bli den nye prosjekteieren?", + "PRIVATE": "Privat", + "ACCEPTED_PROJECT_OWNERNSHIP": "Gratulerer! Du er nå den nye prosjekteieren.", + "REJECTED_PROJECT_OWNERNSHIP": "OK. Vi kontakter den nåværende prosjekteieren", + "ACCEPT": "Aksepter", + "REJECT": "Avvis", + "PROPOSE_OWNERSHIP": "{{owner}}, den nåværende eiere av prosjekt {{project}} har spurt om du vil bli den nye prosjekteieren.", + "ADD_COMMENT": "Har du lyst til å gi en kommentar til prosjekteieren?", + "UNLIMITED_PROJECTS": "Ubegrenset", + "OWNER_MESSAGE": { + "PRIVATE": "Husk, du kan eie opp til {{maxProjects}} private prosjekter. Du eier for tiden {{currentProjects}} private prosjekter", + "PUBLIC": "Husk, du kan eie opp til {{maxProjects}} offentlige prosjekter. Du eier foreløpig {{currentProjects}} offentlige prosjekter" + }, + "CANT_BE_OWNED": "For øyeblikket kan du ikke bli eier av et prosjekt av denne typen. Dersom du ønsker å bli eier av dette prosjektet, venligst kontakt en administrator så de kan endre dine kontoinstillinger slik at du kan eie et prosjekt.", + "CHANGE_MY_PLAN": "Endre min plan" + } + }, + "USER": { + "PROFILE": { + "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", + "EDIT": "Endre profil", + "FOLLOW": "Følg", + "CLOSED_US": "Lukket historie", + "PROJECTS": "Prosjekter", + "PROJECTS_EMPTY": "{{username}} har ikke prosjekter enda", + "CONTACTS": "Kontakter", + "CONTACTS_EMPTY": "{{username}} har ingen kontakter enda", + "CURRENT_USER_CONTACTS_EMPTY": "Du har ingen kontakter enda", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Personene som du jobber sammen med i Taiga vil automatisk bli dine kontakter", + "REPORT": "Rapporter misbruk", + "TABS": { + "ACTIVITY_TAB": "Tidslinje", + "ACTIVITY_TAB_TITLE": "Vis all aktiviteten til denne brukeren", + "PROJECTS_TAB": "Prosjekter", + "PROJECTS_TAB_TITLE": "Liste over alle prosjekter som brukeren er medlem av", + "LIKES_TAB": "Liker", + "LIKES_TAB_TITLE": "List alle \"liker\" gjort av denne brukeren", + "VOTES_TAB": "Stemmer", + "VOTES_TAB_TITLE": "List alle stemmer gitt av denne brukeren", + "WATCHED_TAB": "Overvåket", + "WATCHED_TAB_TITLE": "List alle element overvåket av denne brukeren", + "CONTACTS_TAB": "Kontakter", + "CONTACTS_TAB_TITLE": "List alle kontakter laget av denne brukeren" + } + }, + "PROFILE_SIDEBAR": { + "TITLE": "Din profil", + "DESCRIPTION": "Alle kan se alt du gjør og hva du jobber med. Legg til en informativ biografi av deg selv.", + "ADD_INFO": "Endre bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Skriv noe...", + "FILTER_TYPE_ALL": "Alle", + "FILTER_TYPE_ALL_TITLE": "Vis alle", + "FILTER_TYPE_PROJECTS": "Prosjekter", + "FILTER_TYPE_PROJECT_TITLES": "Vis kun prosjekter", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", + "FILTER_TYPE_USER_STORIES": "Historier", + "FILTER_TYPE_USER_STORIES_TITLES": "Vis kun brukerhistorier", + "FILTER_TYPE_TASKS": "Oppgaver", + "FILTER_TYPE_TASK_TITLES": "Vis kun oppgaver", + "FILTER_TYPE_ISSUES": "Hendelser", + "FILTER_TYPE_ISSUES_TITLE": "Vis kun hendelser", + "EMPTY_TITLE": "Det ser ut som om det ikke er noe å vise her." + } + }, + "PROJECT": { + "PAGE_TITLE": "{{projectName}}", + "WELCOME": "Velkommen", + "SECTION_PROJECTS": "Prosjekter", + "HELP": "Reorganiser prosjektene dine etter de mest brukte.
De topp 10 mest brukte prosjektene vil vises i navigasjonslenken på toppen.", + "PRIVATE": "Privat prosjekt", + "LOOKING_FOR_PEOPLE": "Dette prosjekter søker etter mennesker", + "FANS_COUNTER_TITLE": "{total, plural, one{en fan} other{# fans}}", + "WATCHERS_COUNTER_TITLE": "{total, plural, one{en følger} other{# følgere}}", + "MEMBERS_COUNTER_TITLE": "{total, plural, one{ett medlem} other{# medlemmer}}", + "BLOCKED_PROJECT": { + "BLOCKED": "Blokkert prosjekt", + "THIS_PROJECT_IS_BLOCKED": "Dette prosjektet er midlertidig blokkert", + "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "For å avblokkere dine prosjekter, kontakt administratoren" + }, + "STATS": { + "PROJECT": "prosjekt
poeng", + "DEFINED": "definerte
poeng", + "ASSIGNED": "tildelte
poeng", + "CLOSED": "lukkede
poeng" + }, + "SECTION": { + "SEARCH": "Søk", + "TIMELINE": "Tidslinje", + "BACKLOG": "Backlog", + "KANBAN": "Kanban", + "ISSUES": "Hendelser", + "WIKI": "Wiki", + "TEAM": "Team", + "MEETUP": "Møtes", + "ADMIN": "Admin" + }, + "NAVIGATION": { + "SECTION_TITLE": "Dine prosjekter", + "PLACEHOLDER_SEARCH": "Søk i...", + "ACTION_CREATE_PROJECT": "Opprett prosjekt", + "ACTION_IMPORT_PROJECT": "Importer prosjekt", + "MANAGE_PROJECTS": "Håndter prosjekter", + "TITLE_CREATE_PROJECT": "Opprett prosjekt", + "TITLE_IMPORT_PROJECT": "Importer prosjekt", + "TITLE_PRVIOUS_PROJECT": "Vis forrige prosjekter", + "TITLE_NEXT_PROJECT": "Vis neste prosjekter", + "HELP_TITLE": "Taiga Brukerstøtte", + "HELP": "Hjelp", + "HOMEPAGE": "Hjemmeside", + "FEEDBACK_TITLE": "Gi tilbakemelding", + "FEEDBACK": "Tilbakemelding", + "NOTIFICATIONS_TITLE": "Endre dine varslingsinstillinger", + "NOTIFICATIONS": "Varsler", + "ORGANIZATIONS_TITLE": "Rediger dine organisasjoner", + "ORGANIZATIONS": "Endre organisasjoner", + "SETTINGS_TITLE": "Endre dine instillinger", + "SETTINGS": "Instillinger", + "VIEW_PROFILE_TITLE": "Vis Profil", + "VIEW_PROFILE": "Vis Profil", + "EDIT_PROFILE_TITLE": "Rediger din profil", + "EDIT_PROFILE": "Rediger Profil", + "CHANGE_PASSWORD_TITLE": "Endre passord", + "CHANGE_PASSWORD": "Endre passord", + "DASHBOARD_TITLE": "Dashbord", + "DISCOVER_TITLE": "Oppdag populære prosjekter", + "NEW_ITEM": "Ny", + "DISCOVER": "Oppdag", + "ACTION_REORDER": "Dra & slipp for å omorganisere" + }, + "IMPORT": { + "TITLE": "Importer prosjekt", + "UPLOADING_FILE": "Laster opp dump-fil", + "DESCRIPTION": "Denne prosessen kan ta en stund, vennligst hold vinduet åpent.", + "ASYNC_IN_PROGRESS_TITLE": "Våre Oompa Loompaer importerer ditt prosjekt", + "ASYNC_IN_PROGRESS_MESSAGE": "Denne prosessen kan ta noen minutter
Vi vil sende deg en e-post når den er klar", + "UPLOAD_IN_PROGRESS_MESSAGE": "Opplastet {{uploadedSize}} av {{totalSize}}", + "ERROR": "Våre Oompa Loompaer har problemer med å importere din data-dump. Vennligst prøv på nytt.", + "ERROR_TOO_MANY_REQUEST": "Beklager, våre Oompa Loomper er svært opptatt akkuratt nå. Venligst prøv igjen om noen få minutter.", + "ERROR_MESSAGE": "Våre Oompa Loompaer har problemer med å importere din data-dump: {{error_message}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) er for stor for våre Oompa Loompaer. Prøv med en mindre enn ({{maxFileSize}})", + "SYNC_SUCCESS": "Importen av ditt prosjekt var vellykket", + "PROJECT_RESTRICTIONS": { + "PROJECT_MEMBERS_DESC": "Prosjektet du prøver å importere har {{members}} medlemmer. Dessverre, din nåværende plan tillater kun {{max_memberships}} medlemmer per prosjekt. Hvis du ønsker å øke denne grensen, vennligst kontakt administratoren.", + "PRIVATE_PROJECTS_SPACE": { + "TITLE": "Dessverre, din nåværende plan tillater ikke private prosjekter", + "DESC": "Prosjektet du prøver å importere er privat. Dessverre tillater ikke din gjeldende flere private prosjekter." + }, + "PUBLIC_PROJECTS_SPACE": { + "TITLE": "Dessverre din gjeldende plan tillatee ikke flere offentlige prosjekter", + "DESC": "Prosjektet du prøver å importere er offentlig. Dessverre, din nåværende plan tillater ikke offentlige prosjekter." + }, + "PRIVATE_PROJECTS_MEMBERS": { + "TITLE": "Din nåværende plan tillater et maksimalt antall av {{max_memberships}} medlemmer per private prosjekt" + }, + "PUBLIC_PROJECTS_MEMBERS": { + "TITLE": "Din nåværende plan tillater maksimum {{max_memberships}} medlemmer per offentlige prosjekt." + }, + "PRIVATE_PROJECTS_SPACE_MEMBERS": { + "TITLE": "Dessverre, din nåværende plan tillater ikke flere private prosjekter eller mer enn {{max_memberships}} medlemmer per private prosjekt", + "DESC": "Prosjektet som du prøver å importere er privat og har {{members}} medlemmer." + }, + "PUBLIC_PROJECTS_SPACE_MEMBERS": { + "TITLE": "Dessverre, din nåværende plan tillater ikke flere offentlige prosjekter eller en økning til fler enn {{max_memberships}} medlemmer per offentlige prosjekt", + "DESC": "Prosjektet du prøver å importere er offentlig og har fler enn {{members}} medlemmer." + } + } + }, + "LIKE_BUTTON": { + "LIKE": "Liker", + "LIKED": "Likt", + "UNLIKE": "Ikke lik", + "BUTTON_TITLE": "Like eller ikke like dette prosjektet", + "COUNTER_TITLE": "{total, plural, one{en fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Overvåk dette prosjektet og sett varslingsstrategi", + "WATCH": "Følg", + "WATCHING": "Følger med på", + "COUNTER_TITLE": "{total, plural, one{en følger} other{# følgere}}", + "OPTIONS": { + "NOTIFY_ALL": "Motta alle varsler", + "NOTIFY_ALL_TITLE": "Motta alle varsler for dette prosjektet", + "NOTIFY_INVOLVED": "Kun involvert", + "NOTIFY_INVOLVED_TITLE": "Motta varsler kun når du er involvert", + "UNWATCH": "Ikke overvåk", + "UNWATCH_TITLE": "Ikke overvåk dette prosjektet" + } + } + }, + "LIGHTBOX": { + "DELETE_ACCOUNT": { + "SECTION_NAME": "Slett din Taiga konto", + "CONFIRM": "Er du sikker på at du vil slette din Taiga konto?", + "NEWSLETTER_LABEL_TEXT": "Jeg vil ikke motta flere nyhetsbrev", + "CANCEL": "Tilbake til instillinger", + "ACCEPT": "Slett konto", + "BLOCK_PROJECT": "Merk at alle prosjektene der du står som eier vil bli blokkert etter at du sletter din konto. Dersom du ikke ønsker dette, overfør eierskapet til et annet medlem før du sletter kontoen.", + "SUBTITLE": "Det er synd å se at du forlater oss. Vi er her, skulle du noen sinne vurdere oss igjen! :(" + }, + "DELETE_PROJECT": { + "TITLE": "Slett prosjekt", + "QUESTION": "Er du sikker på at du vil slette dette prosjektet?", + "SUBTITLE": "All prosjekt-data (burkerhistorier, oppgaver, saker, sprinter og wiki-sider) vil gå tapt! :(", + "CONFIRM": "Ja, jeg er virkelig sikker" + }, + "ASSIGNED_TO": { + "SELECT": "Velg ansvarlig", + "SEARCH": "Søk etter brukere" + }, + "ADD_MEMBER": { + "TITLE": "Nytt medlem", + "HELP_TEXT": "Hvis brukere allerede er registrerte på Taiga, vil de bli lagt til automatisk. Ellers vil de motta en invitasjon." + }, + "CREATE_ISSUE": { + "TITLE": "Legg til hendelse" + }, + "FEEDBACK": { + "TITLE": "Fortell oss noe...", + "COMMENT": "...en feil, noen forslag, noe kult... eller til og med ditt værste mareritt med Taiga", + "ACTION_SEND": "Gi tilbakemelding" + }, + "SEARCH": { + "TITLE": "Søk", + "PLACEHOLDER_SEARCH": "Hva ser du etter?" + }, + "ADD_EDIT_SPRINT": { + "TITLE": "Ny sprint", + "PLACEHOLDER_SPRINT_NAME": "sprint navn", + "PLACEHOLDER_SPRINT_START": "Estimert Start", + "PLACEHOLDER_SPRINT_END": "Estimert Slutt", + "ACTION_DELETE_SPRINT": "Vil du slette denne sprinten?", + "TITLE_ACTION_DELETE_SPRINT": "slett sprint", + "LAST_SPRINT_NAME": "siste sprinten er {{lastSprint}} ;-) " + }, + "CREATE_EDIT_TASK": { + "TITLE": "Ny oppgave", + "PLACEHOLDER_SUBJECT": "Subjekt til en oppgave", + "PLACEHOLDER_STATUS": "Oppgavestatus", + "OPTION_UNASSIGNED": "Ikke tildelt", + "PLACEHOLDER_SHORT_DESCRIPTION": "Skriv en kort beskrivelse", + "ACTION_EDIT": "Rediger oppgave" + }, + "CREATE_EDIT_US": { + "TITLE": "Ny BH", + "PLACEHOLDER_DESCRIPTION": "Vennligst legg til en beskrivende tekst for bedre å hjelpe andre til å forstå denne BH", + "NEW_US": "Ny brukerhistorie", + "EDIT_US": "Rediger brukerhistorie" + }, + "DELETE_SPRINT": { + "TITLE": "Slett sprint" + }, + "CREATE_MEMBER": { + "PLACEHOLDER_INVITATION_TEXT": "(Valgfritt) Legg til en egen tekst til invitasjonen. Fortell dine nye medlemmer noe fantastisk ;-)", + "PLACEHOLDER_TYPE_EMAIL": "Skriv en epost", + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." + }, + "LEAVE_PROJECT_WARNING": { + "TITLE": "Dessverre, dette prosjektet kan ikke stå uten en eier", + "CURRENT_USER_OWNER": { + "DESC": "Du er den nåværende eieren av dette prosjektet. Før du forlater, vennligst overfør eierskapet til noen andre.", + "BUTTON": "Endre eieren av prosjektet" + }, + "OTHER_USER_OWNER": { + "DESC": "Dessverre, du kan ikke slette et medlem som også er den nåværende prosjekteieren. Vennligst tildel eierskapet til et annet medlem først.", + "BUTTON": "Be om endring av prosjekteier" + } + }, + "CHANGE_OWNER": { + "TITLE": "Hvem vil du at skal være den nye eieren av prosjektet?", + "ADD_COMMENT": "Legg til kommentar", + "BUTTON": "Spør dette prosjektetmedlemet om å bli den nye prosjekteieren" + } + }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Ny brukerhistorie", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Subjekt", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, + "US": { + "PAGE_TITLE": "{{userStorySubject}} - Brukerhistorie {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Ferdigstilt: {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} av {{userStoryTotalTasks}} lukkede hendelser). Poeng: {{userStoryPoints}}. Beskrivelse: {{userStoryDescription}}", + "SECTION_NAME": "Brukerhistorie", + "LINK_TASKBOARD": "Oppgavetavle", + "TITLE_LINK_TASKBOARD": "Gå til oppgavetavlen", + "TOTAL_POINTS": "totale poeng", + "ADD": "+ Legg til en ny brukerhistorie", + "ADD_BULK": "Legg til mange nye Brukerhistorier", + "PROMOTED": "Denne Brukerhistorien har blitt oppgradert fra Hendelse:", + "TITLE_LINK_GO_TO_ISSUE": "Gå til hendelse", + "EXTERNAL_REFERENCE": "Denne BH ble opprettet av", + "GO_TO_EXTERNAL_REFERENCE": "Gå til opphav", + "BLOCKED": "Denne brukerhistorien er blokkert", + "TITLE_DELETE_ACTION": "Slett Brukerhistorie", + "LIGHTBOX_TITLE_BLOKING_US": "Blokkerer oss", + "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} oppgaver ferdigstilt", + "ASSIGN": "Tildel Brukerhistorie", + "NOT_ESTIMATED": "Ikke estimert", + "TOTAL_US_POINTS": "Total BH poeng", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "Mer info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, + "FIELDS": { + "TEAM_REQUIREMENT": "Team behov", + "CLIENT_REQUIREMENT": "Klientkrav", + "FINISH_DATE": "Sluttdato" + } + }, + "COMMENTS": { + "DELETED_INFO": "Comment deleted by {{user}}", + "TITLE": "Kommentarer", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", + "COMMENT": "Kommentar", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", + "TYPE_NEW_COMMENT": "Skriv en ny kommentar her", + "SHOW_DELETED": "Vis slettede kommentarer", + "HIDE_DELETED": "Skjul slettede kommentarer", + "DELETE": "Slett kommentar", + "RESTORE": "Gjennoprett kommentarer", + "HISTORY": { + "TITLE": "Aktivitet" + } + }, + "ACTIVITY": { + "SHOW_ACTIVITY": "Vis aktivitet", + "DATETIME": "DD MMM YYYY HH:mm", + "SHOW_MORE": "+ Vis tidligere innlegg ({{showMore}} mer)", + "TITLE": "Aktivitet", + "ACTIVITIES_COUNT": "{{activities}} Activities", + "REMOVED": "fjernet", + "ADDED": "lagt til", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", + "SIZE_CHANGE": "Gjorde {size, plural, one{en endring} other{# endringer}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Team behov", + "CLIENT_REQUIREMENT": "Klientkrav", + "BLOCKED": "Blokkert", + "VALUES": { + "YES": "ja", + "NO": "nei", + "EMPTY": "tomt", + "UNASSIGNED": "ikke tildelt" + }, + "FIELDS": { + "SUBJECT": "subjekt", + "NAME": "navn", + "DESCRIPTION": "beskrivelse", + "CONTENT": "innhold", + "STATUS": "status", + "IS_CLOSED": "er lukket", + "FINISH_DATE": "Sluttdato", + "TYPE": "type", + "PRIORITY": "prioritet", + "SEVERITY": "Alvorlighetsgrad", + "ASSIGNED_TO": "tildelt til", + "WATCHERS": "følgere", + "MILESTONE": "sprint", + "USER_STORY": "brukerhistorie", + "PROJECT": "prosjekt", + "IS_BLOCKED": "er blokkert", + "BLOCKED_NOTE": "blokkert notat", + "POINTS": "poeng", + "CLIENT_REQUIREMENT": "klientkrav", + "TEAM_REQUIREMENT": "team behov", + "IS_IOCAINE": "Er Iocaine", + "TAGS": "etiketter", + "ATTACHMENTS": "vedlegg", + "IS_DEPRECATED": "Er foreldet", + "IS_NOT_DEPRECATED": "is not deprecated", + "ORDER": "rekkefølge", + "BACKLOG_ORDER": "backlog rekkefølge", + "SPRINT_ORDER": "sprint rekkefølge", + "KANBAN_ORDER": "kanban rekkefølge", + "TASKBOARD_ORDER": "Oppgavetavle rekkefølge", + "US_ORDER": "BH rekkefølge", + "COLOR": "farge" + } + }, + "BACKLOG": { + "PAGE_TITLE": "Backlog - {{projectName}}", + "PAGE_DESCRIPTION": "Backlogpanelet, med brukerhistorier og sprinter for prosjektet {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Tilpass din backlog-graf", + "CUSTOMIZE_GRAPH_TEXT": "For å ha en flott graf som hjelper deg å følge utviklingen av prosjektet må du sette opp poeng og sprinter gjennom", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Sett opp poengene og sprintene gjennom Admin-panelet", + "MOVE_US_TO_CURRENT_SPRINT": "Gå til nåværende sprint", + "MOVE_US_TO_LATEST_SPRINT": "Flytt til siste Sprint", + "SHOW_FILTERS": "Vis filtre", + "SHOW_TAGS": "Vis etiketter", + "EMPTY": "Backlogen er tom!", + "CREATE_NEW_US": "Opprett en ny BH", + "CREATE_NEW_US_EMPTY_HELP": "Det kan være lurt å opprette en ny brukerhistorie", + "EXCESS_OF_POINTS": "Overskudd av poeng", + "PENDING_POINTS": "Ventende poeng", + "CLOSED_POINTS": "lukket", + "COMPACT_SPRINT": "Kompakt Sprint", + "GO_TO_TASKBOARD": "Gå til oppgavepanelet for {{::name}}", + "EDIT_SPRINT": "Rediger Sprint", + "TOTAL_POINTS": "total", + "STATUS_NAME": "Status Navn", + "SORTABLE_FILTER_ERROR": "Du kan ikke slippe på backlog når filtere er åpen", + "DOOMLINE": "Prosjektomfang [Dommedagslinje]", + "CHART": { + "XAXIS_LABEL": "Sprinter", + "YAXIS_LABEL": "Poeng", + "OPTIMAL": "Optimal mengde avventede poeng for sprint \"{{sprintName}}\" skal være {{value}}", + "REAL": "Ekte avventende poeng for sprint \"{{sprintName}}\" er {{value}}", + "INCREMENT_TEAM": "Inkrementelle poeng ved gruppekrav for sprint \"{{sprintName}}\" er {{value}}", + "INCREMENT_CLIENT": "Inkrementelle poeng med klientkrav for sprint \"{{sprintNam}}\" er {{verdi}}" + }, + "TAGS": { + "TOGGLE": "Endre synligheten til etiketter", + "SHOW": "Vis etiketter", + "HIDE": "Skjul etiketter" + }, + "TABLE": { + "COLUMN_US": "Brukerhistorie", + "TITLE_COLUMN_POINTS": "Velg visning per Rolle" + }, + "SPRINT_SUMMARY": { + "TOTAL_POINTS": "total
poeng", + "COMPLETED_POINTS": "fullførte
poeng", + "OPEN_TASKS": "åpne
oppgaver", + "CLOSED_TASKS": "lukkede
oppgaver", + "IOCAINE_DOSES": "iocaine
doser", + "SHOW_STATISTICS_TITLE": "Vis statistikker", + "TOGGLE_BAKLOG_GRAPH": "Vis/Skjul Burndown Graf", + "POINTS_PER_ROLE": "Points per role" + }, + "SUMMARY": { + "PROJECT_POINTS": "prosjekt
poeng", + "DEFINED_POINTS": "definerte
poeng", + "CLOSED_POINTS": "lukkede
poeng", + "POINTS_PER_SPRINT": "poeng /
sprint" + }, + "FILTERS": { + "TOGGLE": "Veksle filteres synlighet", + "TITLE": "Filter", + "REMOVE": "Fjern Filtere", + "HIDE": "Skjul Filtere", + "SHOW": "Vis Filtere" + }, + "SPRINTS": { + "TITLE": "SPRINTER", + "DATE": "DD MMM YYYY", + "LINK_TASKBOARD": "Sprint Oppgavepanel", + "TITLE_LINK_TASKBOARD": "Gå til Oppgavepanel for \"{{name}}\"", + "NUMBER_SPRINTS": "
sprinter", + "EMPTY": "Det er ingen sprinter enda", + "WARNING_EMPTY_SPRINT_ANONYMOUS": "Denne sprinten har ingen Brukerhistorier", + "WARNING_EMPTY_SPRINT": "Slipp Brukerhistorier her fra din backlog for å starte en ny sprint", + "TITLE_ACTION_NEW_SPRINT": "Legg til ny sprint", + "TEXT_ACTION_NEW_SPRINT": "Det kan være lurt å lage en ny sprint i prosjektet ditt", + "ACTION_SHOW_CLOSED_SPRINTS": "Vis lukkede sprinter", + "ACTION_HIDE_CLOSED_SPRINTS": "Skjul lukkede sprinter" + } + }, + "ERROR": { + "TEXT1": "Noe skjedde og våre Oompa Loompaer jobber med det.", + "NOT_FOUND": "Ikke funnet", + "NOT_FOUND_TEXT": "Error 404. Siden du ser etter eksisterer ikke lenger. Kanskje du kan returnere til TAIGA-hjemmesiden og se om du kan finne det du leter etter.", + "PERMISSION_DENIED": "Tilgang nektet", + "PERMISSION_DENIED_TEXT": "Du har ikke tilgang til å aksessere denne siden", + "VERSION_ERROR": "Noen andre som bruker Taiga har endret dette før, og våre Oompa Loompaer kan ikke lagre dine endringer. Vennligst last om og lagre dine endringer igjen (de vil gå tapt)." + }, + "TASKBOARD": { + "PAGE_TITLE": "{{sprintName}} - Sprint oppgavepanel - {{projectName}}", + "PAGE_DESCRIPTION": "Sprint {{sprintName}} (from {{startDate}} til {{endDate}}) med {{projectName}}. Fullført {{completedPercentage}}% ({{completedPoints}} av {{totalPoints}} poeng). {{openTasks}} åpne oppgaver av {{totalTasks}}.", + "SECTION_NAME": "Oppgavetavle", + "TITLE_ACTION_ADD": "Legg til en ny oppgave", + "TITLE_ACTION_ADD_BULK": "Legg til noen nye Oppgaver samlet", + "TITLE_ACTION_ASSIGN": "Tildel oppgave", + "TITLE_ACTION_EDIT": "Rediger oppgave", + "PLACEHOLDER_CARD_TITLE": "Dette kunne vært en oppgave", + "PLACEHOLDER_CARD_TEXT": "Splitt historier inn i oppgaver og følg dem separat", + "TABLE": { + "COLUMN": "Brukerhistorie", + "TITLE_ACTION_FOLD": "Slå sammen kolonne", + "TITLE_ACTION_UNFOLD": "Brett ut kolonne", + "TITLE_ACTION_FOLD_ROW": "Brett Rad", + "TITLE_ACTION_UNFOLD_ROW": "Brett ut Rad", + "FIELD_POINTS": "poeng", + "ROW_UNASSIGED_TASKS_TITLE": "\nIkke tildelte oppgaver" + }, + "CHARTS": { + "XAXIS_LABEL": "Dager", + "YAXIS_LABEL": "Poeng", + "OPTIMAL": "Optimal mengde ventende poeng for dagen {{formattedDate}} bør være {{roundedValue}}", + "REAL": "Reelle ventende poeng for dagen {{formattedDate}} er {{roundedValue}}", + "DATE": "DD MMMM YYYY" + } + }, + "TASK": { + "PAGE_TITLE": "{{taskSubject}} - Oppgave {{taskRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Beskrivelse: {{taskDescription}}", + "SECTION_NAME": "Oppgave", + "LINK_TASKBOARD": "Oppgavetavle", + "TITLE_LINK_TASKBOARD": "Gå til oppgavetavlen", + "PLACEHOLDER_SUBJECT": "Skriv inn subjektet til den nye oppgaven", + "TITLE_SELECT_STATUS": "Status Navn", + "OWNER_US": "Denne oppgaven tilhører", + "TITLE_LINK_GO_OWNER": "Gå til brukerhistorie", + "ORIGIN_US": "Denne oppgaven har blitt opprettet fra", + "TITLE_LINK_GO_ORIGIN": "Gå til brukerhistorie", + "BLOCKED": "Denne oppgaven er blokkert", + "TITLE_DELETE_ACTION": "Slett oppgave", + "LIGHTBOX_TITLE_BLOKING_TASK": "Blokkerende oppgave", + "FIELDS": { + "MILESTONE": "Sprint", + "USER_STORY": "Brukerhistorie", + "IS_IOCAINE": "Er Iocaine" + }, + "ACTION_IOCAINE": "Iocaine", + "TITLE_ACTION_IOCAINE": "Føler du deg litt overveldet av en oppgave? Sørg for at andre vet om det ved å klikke på \"Iocane\" når du redigerer en oppgave. Det er mulig å bli immun mot denne (fiktive) dødelige giften ved å konsumere små mengder over tid, akkurat som det er mulig å bli bedre på det du gjør ved av og til å ta på deg ekstra utfordringer!" + }, + "NOTIFICATION": { + "OK": "Alt er ok", + "WARNING": "Opps, noe skjedde...", + "WARNING_TEXT": "Våre Oompa Loompaer er lei seg :-( , dine endringer ble ikke lagret!", + "SAVED": "Våre Oompa Loompaer har lagret alle endringer!", + "CLOSE": "Lukk melding", + "MAIL": "Varsler per Epost", + "ASK_DELETE": "Er du sikker på at du vil slette?" + }, + "CANCEL_ACCOUNT": { + "TITLE": "Kanseler din konto", + "SUBTITLE": "Vi er lei oss for at du forlater Taiga, vi håper du nøt oppholdet :)", + "PLACEHOLDER_INPUT_TOKEN": "kanseler konto token", + "ACTION_LEAVING": "Ja, jeg går!", + "SUCCESS": "Våre Oompa Loompaer har slettet din konto" + }, + "CHANGE_EMAIL_FORM": { + "TITLE": "Bytt din epost", + "SUBTITLE": "Ett klikk til og din epost vil være oppdatert!", + "PLACEHOLDER_INPUT_TOKEN": "skift epost token", + "ACTION_CHANGE_EMAIL": "Endre epost", + "SUCCESS": "Våre Oompa Loompaer oppdaterte din epost" + }, + "ISSUES": { + "PAGE_TITLE": "Hendelser - {{projectName}}", + "PAGE_DESCRIPTION": "Hendelsespanelet for prosjekt {{projectName}}: {{projectDescription}}", + "LIST_SECTION_NAME": "Hendelser", + "SECTION_NAME": "Hendelse", + "ACTION_NEW_ISSUE": "+ NY HENDELSE", + "ACTION_PROMOTE_TO_US": "Oppgrader til Brukerhistorie", + "PROMOTED": "Denne hendelsen har blitt oppgradert til BH", + "EXTERNAL_REFERENCE": "Denne hendelsen har blitt opprettet av", + "GO_TO_EXTERNAL_REFERENCE": "Gå til opphav", + "BLOCKED": "Denne hendelsen er blokkert", + "ACTION_DELETE": "Slett hendelse", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blokker hendelse", + "FIELDS": { + "PRIORITY": "Prioritet", + "SEVERITY": "Alvorlighetsgrad", + "TYPE": "Type" + }, + "CONFIRM_PROMOTE": { + "TITLE": "Oppgrader denne henelsen til en ny brukerhistorie", + "MESSAGE": "Er du sikker på at du vil lage en ny BH fra denne hendelsen?" + }, + "TABLE": { + "COLUMNS": { + "TYPE": "Type", + "SEVERITY": "Alvorlighetsgrad", + "PRIORITY": "Prioritet", + "SUBJECT": "Subjekt", + "VOTES": "Stemmer", + "STATUS": "Status", + "CREATED": "Opprettet", + "ASSIGNED_TO": "Tildelt til" + }, + "TITLE_ACTION_CHANGE_STATUS": "Endre status", + "TITLE_ACTION_ASSIGNED_TO": "Tildelt til", + "BLOCKED": "Blokkert", + "EMPTY": { + "TITLE": "Det er ingen hendelser å jobbe med :-)", + "SUBTITLE": "Oppdaget du en hendelse?" + } + } + }, + "ISSUE": { + "PAGE_TITLE": "{{issueSubject}} - Hendelse {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{issueStatus }}. Type: {{issueType}}, Prioritet: {{issuePriority}}. Alvorlighetsgrad: {{issueSeverity}}. Beskrivelse: {{issueDescription}}" + }, + "KANBAN": { + "PAGE_TITLE": "Kanban - {{projectName}}", + "PAGE_DESCRIPTION": "Kanbanpanelet, med brukerhistorier til prosjektet {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Kanban", + "TITLE_ACTION_FOLD": "Slå sammen kolonne", + "TITLE_ACTION_UNFOLD": "Brett ut kolonne", + "TITLE_ACTION_FOLD_CARDS": "Brett kort", + "TITLE_ACTION_UNFOLD_CARDS": "Brett ut kort", + "TITLE_ACTION_ADD_US": "Legg til ny Brukerhistorie", + "TITLE_ACTION_ADD_BULK": "Legg til ny samling", + "ACTION_SHOW_ARCHIVED": "Vis arkiverte", + "ACTION_HIDE_ARCHIVED": "Skjul arkivert", + "HIDDEN_USER_STORIES": "Brukerhistoriene med denne statusen er skjult som standard", + "ARCHIVED": "Du har arkivert", + "UNDO_ARCHIVED": "Dra & slipp igjen for å angre", + "PLACEHOLDER_CARD_TITLE": "Dette er dine Brukerhistorier", + "PLACEHOLDER_CARD_TEXT": "Historier kan også ha underoppgaver med egne krav" + }, + "SEARCH": { + "PAGE_TITLE": "Søk - {{projectName}}", + "PAGE_DESCRIPTION": "Søk etter hva som helst, brukerhistorier, hendelser, oppgaver or wiki-sider i prosjektet {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", + "FILTER_USER_STORIES": "Brukerhistorie", + "FILTER_ISSUES": "Hendelser", + "FILTER_TASKS": "Oppgaver", + "FILTER_WIKI": "Wiki-sider", + "PLACEHOLDER_SEARCH": "Søk i...", + "TITLE_ACTION_SEARCH": "søk", + "EMPTY_TITLE": "Det ser ut som ingenting ble funnet med søkekriteriene dine", + "EMPTY_DESCRIPTION": "Kanskje prøve en av fanene ovenfor eller søk på nytt" + }, + "TEAM": { + "PAGE_TITLE": "Team - {{projectName}}", + "PAGE_DESCRIPTION": "Teampanelet for å vise alle medlemmene i prosjektet {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Team", + "APP_TITLE": "TEAM - {{projectName}}", + "PLACEHOLDER_INPUT_SEARCH": "Søk på fult navn---", + "COLUMN_MR_WOLF": "Mr. Wolf", + "EXPLANATION_COLUMN_MR_WOLF": "Lukkede hendelser", + "COLUMN_IOCAINE": "Iocaine Drikker", + "EXPLANATION_COLUMN_IOCAINE": "Iocainedoser inntatt", + "COLUMN_CERVANTES": "Cervantes", + "EXPLANATION_COLUMN_CERVANTES": "Wiki-sider redigert", + "COLUMN_BUG_HUNTER": "Bug Jeger", + "EXPLANATION_COLUMN_BUG_HUNTER": "Hendelser som er medlt inn", + "COLUMN_NIGHT_SHIFT": "Nattevakt", + "EXPLANATION_COLUMN_NIGHT_SHIFT": "Oppgaver lukket", + "COLUMN_TOTAL_POWER": "Total Styrke", + "EXPLANATION_COLUMN_TOTAL_POWER": "Totalt Antall Poeng", + "SECTION_TITLE_TEAM": "Team >", + "SECTION_FILTER_ALL": "Alle", + "CONFIRM_LEAVE_PROJECT": "Er du sikker på at du vil forlate prosjektet?", + "ACTION_LEAVE_PROJECT": "Forlat dette prosjektet" + }, + "USER_SETTINGS": { + "AVATAR_MAX_SIZE": "[Max. størrelse: {{maxFileSize}}]", + "MENU": { + "SECTION_TITLE": "Brukerinstillinger", + "USER_PROFILE": "Brukerprofil", + "CHANGE_PASSWORD": "Endre passord", + "EMAIL_NOTIFICATIONS": "Epost-varsler" + }, + "NOTIFICATIONS": { + "SECTION_NAME": "E-postvarsler", + "COLUMN_PROJECT": "Prosjekt", + "COLUMN_RECEIVE_ALL": "Motta alle", + "COLUMN_ONLY_INVOLVED": "Kun involvert", + "COLUMN_NO_NOTIFICATIONS": "Ingen varsler", + "OPTION_ALL": "Alle", + "OPTION_INVOLVED": "Involvert", + "OPTION_NONE": "Ingen" + }, + "POPOVER": { + "USER_PROFILE": "Brukerprofil", + "CHANGE_PASSWORD": "Endre passord", + "NOTIFICATIONS": "Varsler", + "FEEDBACK": "Tilbakemelding", + "TITLE_AVATAR": "Brukerpreferanser" + } + }, + "USER_PROFILE": { + "IMAGE_HELP": "Bildet vil bli skalert til 80x80px.", + "ACTION_CHANGE_IMAGE": "Endre", + "ACTION_USE_GRAVATAR": "Bruk standardbilde", + "ACTION_DELETE_ACCOUNT": "Slett Taiga-konto", + "CHANGE_EMAIL_SUCCESS": "Sjekk din innboks!
Vi har sent en epost til din konto
med instruksjonene for å velge ny epostadresse.", + "CHANGE_PHOTO": "Endre bilde", + "FIELD": { + "USERNAME": "Brukernavn", + "EMAIL": "Epost", + "FULL_NAME": "Fullt navn", + "PLACEHOLDER_FULL_NAME": "Skriv ditt fulle navn (f.eks: Ola Nordmann)", + "BIO": "Bio (max. 210 tegn)", + "PLACEHOLDER_BIO": "Fortell oss noe om deg selv", + "LANGUAGE": "Språk", + "LANGUAGE_DEFAULT": "-- bruk standard språk --", + "THEME": "Tema", + "THEME_DEFAULT": "-- bruk standard tema --" + } + }, + "WIZARD": { + "SECTION_TITLE_CREATE_PROJECT": "Opprett prosjekt", + "CREATE_PROJECT_TEXT": "Friskt og stilrent. Så spennende!", + "CHOOSE_TEMPLATE": "Hvilken mal passer ditt prosjekt best?", + "CHOOSE_TEMPLATE_TITLE": "Mer info om prosjektmaler", + "CHOOSE_TEMPLATE_INFO": "Mer info", + "PROJECT_DETAILS": "Prosjektdetaljer", + "PUBLIC_PROJECT": "Offentlig Prosjekt", + "PRIVATE_PROJECT": "Privat Prosjekt", + "CREATE_PROJECT": "Opprett prosjekt", + "MAX_PRIVATE_PROJECTS": "Du har nådd maksimalt antall private prosjekter", + "MAX_PUBLIC_PROJECTS": "Dessverre, du har nådd maksimalt antall offentlige prosjekter", + "CHANGE_PLANS": "endre planer" + }, + "WIKI": { + "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", + "PAGE_DESCRIPTION": "Siste versjon: {{lastModifiedDate}} ({{totalEditions}} versjoner totalt) Innhold: {{ wikiPageContent }}", + "DATETIME": "DD MMM YYYY HH:mm", + "PLACEHOLDER_PAGE": "Skriv din wiki-side", + "REMOVE": "Fjern denne wiki-siden", + "DELETE_LIGHTBOX_TITLE": "Slett wiki-siden", + "DELETE_LINK_TITLE": "Slett wiki-lenke", + "NAVIGATION": { + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" + }, + "SUMMARY": { + "TIMES_EDITED": "ganger
redigert", + "LAST_EDIT": "siste
endring", + "LAST_MODIFICATION": "siste endring" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Opprettet", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" + } + }, + "HINTS": { + "SECTION_NAME": "Tips", + "LINK": "Hvis du vil vite hvordan du bruker den, besøk vår brukerstøtte", + "LINK_TITLE": "Besøk vår brukerstøtte", + "HINT1_TITLE": "Viste du at du kan importere og eksportere prosjekter?", + "HINT1_TEXT": "Dette gir deg muligheten til å ta ut alle dine data fra en Taiga og flytte den til en annen.", + "HINT2_TITLE": "Viste du at du kan opprette egendefinerte felter?", + "HINT2_TEXT": "Team kan nå lage egendefinerte felter som en fleksibel måte å legge til spesifik data nyttig for deres egne arbeidsflyt.", + "HINT3_TITLE": "Endre rekkefølge på dine prosjekter for å fremvise de som er mest relevante for deg.", + "HINT3_TEXT": "De 10 prosjektene er listet opp i navigasjonslinjen på toppen.", + "HINT4_TITLE": "Glemte du det du arbeidet med?", + "HINT4_TEXT": "Ikke fortvil, på ditt dashboard finner du åpne oppgaver, hendelser og brukerhistorier i den rekkefølgen du jobbet med de." + }, + "TIMELINE": { + "UPLOAD_ATTACHMENT": "{{username}} har lastet opp ett nytt vedlegg i {{obj_name}}", + "US_CREATED": "{{username}} har opprettet en ny BH {{obj_name}} i {{project_name}}", + "ISSUE_CREATED": "{{username}} har opprettet en ny hendelse {{obj_name}} i {{project_name}}", + "TASK_CREATED": "{{username}} har opprettet en ny oppgave {{obj_name}} i {{project_name}}", + "TASK_CREATED_WITH_US": "{{username}} har opprettet en ny oppgave {{obj_name}} i {{project_name}} som tilhører BH {{us_name}}", + "WIKI_CREATED": "{{username}} har opprettet en ny wiki-side {{obj_name}} i {{project_name}}", + "MILESTONE_CREATED": "{{username}} har opprettet en ny sprint {{obj_name}} i {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", + "NEW_PROJECT": "{{username}} opprettet prosjektet {{project_name}}", + "MILESTONE_UPDATED": "{{username}} har oppdatert sprint {{obj_name}}", + "US_UPDATED": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til BH {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til BH {{obj_name}} til {{new_value}}", + "US_UPDATED_POINTS": "{{username}} har oppdatert '{{role_name}}' poeng for BH {{obj_name}} til {{new_value}}", + "ISSUE_UPDATED": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til hendelsen {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til hendelsen {{obj_name}} til {{new_value}}", + "TASK_UPDATED": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til oppgave {{obj_name}} til {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til oppgave {{obj_name}} til {{new_value}}", + "TASK_UPDATED_WITH_US": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til oppgave {{obj_name}} som tilhører BH {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} har oppdatert egenskapen \"{{field_name}}\" til oppgaven {{obj_name}} som tilhører BH {{us_name}} til {{new_value}}", + "WIKI_UPDATED": "{{username}} har oppdatert wiki-siden {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", + "NEW_COMMENT_US": "{{username}} har kommentert på BH {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} har kommentert på hendelsen {{obj_name}}", + "NEW_COMMENT_TASK": "{{username}} har kommentert på oppgave {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", + "NEW_MEMBER": "{{project_name}} har et nytt medlem", + "US_ADDED_MILESTONE": "{{username}} har lagt til BH {{obj_name}} til {{sprint_name}}", + "US_MOVED": "{{username}} har flyttet BH {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "{{username}} har lagt til BH {{obj_name}} til backlogen", + "BLOCKED": "{{username}} har blokkert {{obj_name}}", + "UNBLOCKED": "{{username}} har avblokkert {{obj_name}}", + "NEW_USER": "{{username}} har sluttet seg til Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "Når du lager en ny konto godtar du våre
brukervilkår og personvernregler." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "En ekstern app krever autentisering", + "PAGE_DESCRIPTION": "En ekstern app krever autentisering", + "AUTHORIZATION_REQUEST": "Tillat {{application}} til å bruke din Taiga konto?", + "LOGIN_WITH_ANOTHER_USER": "Logg inn med en annen bruker", + "AUTHORIZE_APP": "Godkjenn app", + "CANCEL": "Avbryt" + }, + "JOYRIDE": { + "NAV": { + "NEXT": "Neste", + "BACK": "Tilbake", + "SKIP": "Hopp over", + "DONE": "Ferdig" + }, + "DASHBOARD": { + "STEP1": { + "TITLE": "Ditt prosjekt", + "TEXT": "Velkommen! Her finner du prosjektene du er involvert i." + }, + "STEP2": { + "TITLE": "Arbeider med", + "TEXT": "Her vil du finne Brukerhistorier, Oppgaver og Hendelser som du jobber med." + }, + "STEP3": { + "TITLE": "Følger med på", + "TEXT1": "Og akkuratt her vil du finne de i prosjektet ditt som du vil vite om.", + "TEXT2": "Du arbeider allerede med Taiga ;)" + }, + "STEP4": { + "TITLE": "La oss begynne", + "TEXT1": "Du kan starte med å opprette ditt første Taiga prosjekt", + "TEXT2": "Lykke til!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Prosjektsammendrag", + "TEXT1": "Her vil du se tilstanden til ditt prosjekt.", + "TEXT2": "Du kan endre alle prosjektegenskaper gjennom administrasjonpanelet" + }, + "STEP2": { + "TITLE": "Produkt backlog", + "TEXT": "Backlogen er listen over kravene (Brukerhistorier) til prosjektet. Her kan du planlegge dine sprinter." + }, + "STEP3": { + "TITLE": "Sprinter", + "TEXT": "Sprinter er korte tidsperioder (vanligvis 2 uker) der spesifikt arbeide må ferdigstilles og leveres." + }, + "STEP4": { + "TITLE": "Brukerhistorie", + "TEXT": "Dette er kravene på et overordnet nivå. Du kan legge dem til backlogen og dra dem inn i den sprinten der det skal leveres." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Tilpass din arbeidsflyt", + "TEXT": "Sett opp kolonnene du trenger for å kartlegge statusene til din arbeidsflyt gjennom administrasjonspanelet." + }, + "STEP2": { + "TITLE": "Brukerhistorier & Oppgaver", + "TEXT": "Brukerhistorier er krav på et overordnet nivå. Du kan dra de inn i ulike kolonner." + }, + "STEP3": { + "TITLE": "Legger til Brukerhistorier", + "TEXT1": "Det kan være lurt å legge til en Brukerhistorie (legg til BH ikon) eller en samling av dem (bulk ikon)", + "TEXT2": "Lykke til!" + } + } + }, + "DISCOVER": { + "PAGE_TITLE": "Oppdag prosjekter -Taiga", + "PAGE_DESCRIPTION": "Søkbar katalog med offentlige prosjekter i Taiga. Utforsk backlogger, tidslinjer, hendelser og team. Sjekk ut de mest likte og mest aktive prosjektene. Filtrer etter Kanban og Scrum.", + "DISCOVER_TITLE": "Oppdag prosjekter", + "DISCOVER_SUBTITLE": "{projects, plural, one{Ett offentlig prosjekt å oppdage} other{# offentlige prosjekt å oppdage}}", + "MOST_ACTIVE": "Mest aktiv", + "MOST_ACTIVE_EMPTY": "Det er ingen AKTIVE prosjekter enda", + "MOST_LIKED": "Mest likt", + "MOST_LIKED_EMPTY": "Det er ingen LIKTE prosjekter enda", + "VIEW_MORE": "Vis mer", + "RECRUITING": "Dette prosjekter søker etter mennesker", + "FEATURED": "Utvalgte Prosjekter", + "EMPTY": "Det er ingen prosjekter å vise med dette søkekriteriet.
Prøv igjen!", + "FILTERS": { + "ALL": "Alle", + "KANBAN": "Kanban", + "SCRUM": "Scrum", + "PEOPLE": "Søker etter folk", + "WEEK": "Forrige uke", + "MONTH": "Forrige måned", + "YEAR": "Forrige år", + "ALL_TIME": "All tid", + "CLEAR": "Fjern filtrene" + }, + "SEARCH": { + "PAGE_TITLE": "Søk - Oppdag prosjekter - Taiga", + "PAGE_DESCRIPTION": "Søkbar katalog med offentlige prosjekter i Taiga. Utforsk backlogger, tidslinjer, hendelser og team. Sjekk ut de mest likte og mest aktive prosjektene. Filtrer etter Kanban og Scrum.", + "INPUT_PLACEHOLDER": "Skriv noe...", + "ACTION_TITLE": "Søk", + "RESULTS": "Søkeresultater" + } + } +} \ No newline at end of file diff --git a/app/locales/taiga/locale-nl.json b/app/locales/taiga/locale-nl.json index 750107c5..b6484a18 100644 --- a/app/locales/taiga/locale-nl.json +++ b/app/locales/taiga/locale-nl.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Eén item per regel...", "NEW_BULK": "Nieuwe bulk toevoeging", "RELATED_TASKS": "Gerelateerde taken", + "PREVIOUS": "Previous", + "NEXT": "Volgende", "LOGOUT": "Afmelden", "EXTERNAL_USER": "een extern gebruiker", "GENERIC_ERROR": "Een van onze Oempa Loempa's zegt {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Deze waarde lijkt ongeldig te zijn", "TYPE_EMAIL": "Deze waarde moet een geldig emailadres bevatten", @@ -115,8 +122,9 @@ "USER_STORY": "User story", "TASK": "Taak", "ISSUE": "Issue", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Ik ben 'm! Tag me...", + "PLACEHOLDER": "Enter tag", "DELETE": "Verwijder tag", "ADD": "Tag tovoegen" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "filters", + "TITLE": "Filters", "INPUT_PLACEHOLDER": "Onderwerp of referentie", "TITLE_ACTION_FILTER_BUTTON": "zoek", - "BREADCRUMB_TITLE": "terug naar categorieën", - "BREADCRUMB_FILTERS": "Filters", - "BREADCRUMB_STATUS": "status" + "INPUT_SEARCH_PLACEHOLDER": "Onderwerp of ref.", + "TITLE_ACTION_SEARCH": "Zoek", + "ACTION_SAVE_CUSTOM_FILTER": "Als eigen filter opslaan", + "PLACEHOLDER_FILTER_NAME": "Geef de filternaam in en druk op enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Type", + "STATUS": "Status", + "SEVERITY": "Ernst", + "PRIORITIES": "Prioriteit", + "TAGS": "Tags", + "ASSIGNED_TO": "Toegewezen aan", + "CREATED_BY": "Aangemaakt door", + "CUSTOM_FILTERS": "Eigen filters", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Verwijder eigen filter", + "MESSAGE": "de eigen filter '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Eerste niveau heading", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Voorbeeld", "EDIT_BUTTON": "Bewerk", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Markdown syntax help" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Sprints bekijken", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Bekijk user stories", "ADD_USER_STORIES": "User stories toevoegen", "MODIFY_USER_STORIES": "user stories bewerken", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Verwijderd user stories" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Bekijk taken", "ADD_TASKS": "Taak toevoegen", "MODIFY_TASKS": "Bewerk taken", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Verwijder taken" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Bekijk issues", "ADD_ISSUES": "Issues toevoegen", "MODIFY_ISSUES": "Bewerk issues", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Issues verwijderen" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Volgers", "DASHBOARD": "Projects Dashboard" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Niet toegewezen" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Stemmen", + "NAME": "Naam", + "PROJECT": "Project", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Status", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Geblokkeerd", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Mijn projecten - Taiga", "PAGE_DESCRIPTION": "Een lijst met al jouw projecten, je kunt deze herodenen of een nieuwe aanmaken.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Bewerk waarde", - "TITLE_ACTION_DELETE_VALUE": "Verwijder waarde" + "TITLE_ACTION_DELETE_VALUE": "Verwijder waarde", + "TITLE_ACTION_DELETE_TAG": "Verwijder tag" }, "HELP": "Help je hulp nodig? Bekijk onze support pagina!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Modules", "ENABLE": "Inschakelen", "DISABLE": "Uitschakelen", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Organiseer je user stories om een duidelijk overzicht van aankomend en geprioritiseerd werk te behouden.", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Je staat op het punt de CSV data toegang url te veranderen. De vorige url zal worden uitgeschakeld. Ben je zeker dat je ermee door wil gaan?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "user stories rapporten", "SECTION_TITLE_TASK": "taak rapporten", "SECTION_TITLE_ISSUE": "Issues rapport", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Eigen velden", "SUBTITLE": "Specifieer de aangepaste velden voor je user stories, taken en issues", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Eigen velden user stories", "US_ADD": "Voeg eigen velden toe in user stories", "TASK_DESCRIPTION": "Eigen velden taken", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Specifieer de statussen waar je user stories, taken en issues door zullen gaan", - "US_TITLE": "US statussen", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Taak statussen", "ISSUE_TITLE": "Issue statussen" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Issues types", "ACTION_ADD": "Voeg nieuwe {{objName}} toe" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Tags", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Tag tovoegen", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Rollen - {{projectName}}", "WARNING_NO_ROLE": "Wees voorzichtig, geen enkele rol in je project zal de puntenwaarde van een user story kunnen estimeren", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "de uitnodiging naar {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Standaard waarde voor punten selectie", - "LABEL_US": "Standaard waarde voor US status selectie", "LABEL_TASK_STATUS": "Standaard waarde voor taak status selectie", - "LABEL_PRIORITY": "Standaard waarde voor prioriteit selectie", - "LABEL_SEVERITY": "Standaard waarde voor ernst selectie", "LABEL_ISSUE_TYPE": "Standaard waarde voor issue type selectie", - "LABEL_ISSUE_STATUS": "Standaard waarde voor issue status selectie" + "LABEL_ISSUE_STATUS": "Standaard waarde voor issue status selectie", + "LABEL_PRIORITY": "Standaard waarde voor prioriteit selectie", + "LABEL_SEVERITY": "Standaard waarde voor ernst selectie" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Geef een naam voor de nieuwe status" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioriteiten", "SEVERITIES": "Ernst", "TYPES": "Types", - "CUSTOM_FIELDS": "Eigen velden" + "CUSTOM_FIELDS": "Eigen velden", + "TAGS": "Tags" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Project profiel" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Alles weergeven", "FILTER_TYPE_PROJECTS": "Projecten", "FILTER_TYPE_PROJECT_TITLES": "Enkel projecten weergeven", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Verhalen", "FILTER_TYPE_USER_STORIES_TITLES": "Enkel verhalen van gebruikers weergeven", "FILTER_TYPE_TASKS": "Taken", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Optioneel) Voeg een gepersonaliseerd bericht toe aan je uitnodiging. Vertel iets leuks aan je nieuwe leden ;-)", "PLACEHOLDER_TYPE_EMAIL": "Type en E-mail", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nieuwe user story", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Onderwerp", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Voltooid {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} van {{userStoryTotalTasks}} taken gesloten). Punten: {{userStoryPoints}}. Omschrijving: {{userStoryDescription}}", - "SECTION_NAME": "User story details", + "SECTION_NAME": "User story", "LINK_TASKBOARD": "Taakbord", "TITLE_LINK_TASKBOARD": "Ga naar het dashboard", "TOTAL_POINTS": "totaal aantal punten", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Deze US is aangemaakt vanaf", "GO_TO_EXTERNAL_REFERENCE": "Ga naar bron", "BLOCKED": "Deze user story is geblokkeerd", - "PREVIOUS": "Vorige user story", - "NEXT": "volgende user story", "TITLE_DELETE_ACTION": "Verwijder user story", "LIGHTBOX_TITLE_BLOKING_US": "User story blokkeren", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} taken afgewerkt", "ASSIGN": "User story toewijzen", "NOT_ESTIMATED": "Niet ingeschat", "TOTAL_US_POINTS": "Totaal US punten", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Eisen team", "CLIENT_REQUIREMENT": "Requirement van de klant", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Opmerking verwijderd door {{user}} op {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Reacties", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Reageer", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Type hier nieuw commentaar", "SHOW_DELETED": "Toon verwijderd commentaar", "HIDE_DELETED": "Verberg verwijderde opmerkingen", "DELETE": "Delete comment", - "RESTORE": "Opmerking herstellen" + "RESTORE": "Opmerking herstellen", + "HISTORY": { + "TITLE": "Activiteit" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Toon activiteit", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Toon vorige items ({{showMore}} meer)", "TITLE": "Activiteit", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "verwijderd", "ADDED": "toegevoegd", - "US_POINTS": "US punten ({{name}})", - "NEW_ATTACHMENT": "Nieuwe bijlage", - "DELETED_ATTACHMENT": "verwijderd bijlage", - "UPDATED_ATTACHMENT": "bijlage {{filename}} bijgewerkt", - "DELETED_CUSTOM_ATTRIBUTE": "eigen attribuut verwijderen", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "{size, plural, one{één verandering} other{# veranderingen}} gemaakt", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Eisen team", + "CLIENT_REQUIREMENT": "Requirement van de klant", + "BLOCKED": "Geblokkeerd", "VALUES": { "YES": "ja", "NO": "nee", @@ -1052,12 +1198,14 @@ "TAGS": "tags", "ATTACHMENTS": "bijlagen", "IS_DEPRECATED": "is verourderd", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "volgorde", "BACKLOG_ORDER": "backlog volgorde", "SPRINT_ORDER": "sprint volgorde", "KANBAN_ORDER": "kanban volgorde", "TASKBOARD_ORDER": "taakbord volgorde", - "US_ORDER": "us volgorde" + "US_ORDER": "us volgorde", + "COLOR": "kleur" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "gesloten
taken", "IOCAINE_DOSES": "iocaine
dosissen", "SHOW_STATISTICS_TITLE": "Toon statistieken", - "TOGGLE_BAKLOG_GRAPH": "Toon/Verstop burndown grafiek" + "TOGGLE_BAKLOG_GRAPH": "Toon/Verstop burndown grafiek", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "project
punten", @@ -1122,9 +1271,7 @@ "TITLE": "Filters", "REMOVE": "Filters verwijderd", "HIDE": "Filters verbergen", - "SHOW": "Toon filters", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Tags" + "SHOW": "Toon filters" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Taak {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Omschrijving: {{taskDescription}}", - "SECTION_NAME": "Taak details", + "SECTION_NAME": "Taak", "LINK_TASKBOARD": "Taakbord", "TITLE_LINK_TASKBOARD": "Ga naar het taakbord", "PLACEHOLDER_SUBJECT": "Type het nieuwe onderwerp voor de taak", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Deze taak werd aangemaakt vanaf", "TITLE_LINK_GO_ORIGIN": "Ga naar user story", "BLOCKED": "Deze taak is geblokkeerd", - "PREVIOUS": "vorige taak", - "NEXT": "volgende taak", "TITLE_DELETE_ACTION": "Verwijder taak", "LIGHTBOX_TITLE_BLOKING_TASK": "Blokkerende taak", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Issues - {{projectName}}", "PAGE_DESCRIPTION": "Het issue lijst overzicht van het project {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Issues", - "SECTION_NAME": "Issue details", + "SECTION_NAME": "Issue", "ACTION_NEW_ISSUE": "+ nieuw probleem", "ACTION_PROMOTE_TO_US": "Promoveer tot User Story", - "PLACEHOLDER_FILTER_NAME": "Geef de filternaam in en druk op enter", "PROMOTED": "Dit issue is gepromoveerd tot US:", "EXTERNAL_REFERENCE": "Dit issue is aangemaakt vanaf", "GO_TO_EXTERNAL_REFERENCE": "Ga naar bron", "BLOCKED": "Dit issue is geblokkeerd", - "TITLE_PREVIOUS_ISSUE": "vorig issue", - "TITLE_NEXT_ISSUE": "volgend issue", "ACTION_DELETE": "Verwijderd issue", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blokkerend issue", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Bevorder dit issue tot een nieuwe user story", "MESSAGE": "Weet je zeker dat je een nieuw US van dit issue wilt maken?" }, - "FILTERS": { - "TITLE": "Filters", - "INPUT_SEARCH_PLACEHOLDER": "Onderwerp of ref.", - "TITLE_ACTION_SEARCH": "Zoek", - "ACTION_SAVE_CUSTOM_FILTER": "Als eigen filter opslaan", - "BREADCRUMB": "Filters", - "TITLE_BREADCRUMB": "Filters", - "CATEGORIES": { - "TYPE": "Type", - "STATUS": "Status", - "SEVERITY": "Ernst", - "PRIORITIES": "Prioriteiten", - "TAGS": "Tags", - "ASSIGNED_TO": "Toegewezen aan", - "CREATED_BY": "Aangemaakt door", - "CUSTOM_FILTERS": "Eigen filters" - }, - "CONFIRM_DELETE": { - "TITLE": "Verwijder eigen filter", - "MESSAGE": "de eigen filter '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Type", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Zoek - {{projectName}}", "PAGE_DESCRIPTION": "Zoek op alles, user stories, issues, taken, wiki pagina's, in het project {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "User Stories", "FILTER_ISSUES": "Issues", "FILTER_TASKS": "Taken", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Verwijderd wiki pagina", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Links", - "ACTION_ADD_LINK": "Link toevoegen" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "keer
bewerkt", "LAST_EDIT": "laatst
bewerkt", "LAST_MODIFICATION": "laatste wijziging" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Aangemaakt", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} heeft de nieuwe taak {{obj_name}} aangemakt in {{project_name}} die hoort bij de US {{us_name}}", "WIKI_CREATED": "{{username}} heeft een nieuwe Wiki-pagina aangemaakt {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} heeft een nieuwe sprint aangemaakt {{obj_name}} in {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} heeft een nieuw project aangemaakt {{project_name}}", "MILESTONE_UPDATED": "{{username}} heeft de sprint {{obj_name}} bijgewerkt", "US_UPDATED": "{{username}} heeft de eigenschap \"{{field_name}}\" van de US {{obj_name}} bijgewerkt", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} heeft de eigenschap \"{{field_name}}\" van de taak {{obj_name}} die behoort tot de US {{us_name}} bijgewerkt", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} heeft de eigenschap \"{{field_name}}\" van de taak {{obj_name}} die behoort tot de US {{us_name}} gewijzigd naar {{new_value}}", "WIKI_UPDATED": "{{username}} heeft de wiki pagina {{obj_name}} bijgewerkt", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} heeft gereageerd op de US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} heeft gereageerd op het issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} heeft gereageerd op de taak {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} heeft een nieuw lid", "US_ADDED_MILESTONE": "{{username}} heeft de US {{obj_name}} toegevoegd aan {{sprint_name}}", "US_MOVED": "{{username}} heeft de user story {{obj_name}} verplaatst", diff --git a/app/locales/taiga/locale-pl.json b/app/locales/taiga/locale-pl.json index 377fc4db..a4f7c8d9 100644 --- a/app/locales/taiga/locale-pl.json +++ b/app/locales/taiga/locale-pl.json @@ -19,11 +19,11 @@ "TAG_LINE": "Twoje zwinne, wolne, otwartoźródłowe narzędzie do zarządzania projektem", "TAG_LINE_2": "Pokochaj swój projekt!", "BLOCK": "Blokuj", - "BLOCK_TITLE": "Block this item for example if it has a dependency that can not be satisfied", + "BLOCK_TITLE": "Zablokuj to np. jeżeli posiada zależności, które nie mogą być zrealizowane", "BLOCKED": "Zablokowane", "UNBLOCK": "Odblokuj", - "UNBLOCK_TITLE": "Unblock this item", - "BLOCKED_NOTE": "Why is this blocked?", + "UNBLOCK_TITLE": "Odblokuj", + "BLOCKED_NOTE": "Dlaczego jest zabokowane?", "BLOCKED_REASON": "Wyjaśnij powód", "CREATED_BY": "Utworzone przez {{fullDisplayName}}", "FROM": "od", @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Jedna pozycja na wiersz...", "NEW_BULK": "Nowe zbiorcze dodawanie", "RELATED_TASKS": "Zadania pokrewne", + "PREVIOUS": "Previous", + "NEXT": "Następny", "LOGOUT": "Wyloguj", "EXTERNAL_USER": "zewnętrzny użytkownik", "GENERIC_ERROR": "Umpa Lumpa mówi {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Nieprawidłowa wartość", "TYPE_EMAIL": "Podaj prawidłowy adres email.", @@ -115,8 +122,9 @@ "USER_STORY": "Historyjka użytkownika", "TASK": "Zadania", "ISSUE": "Zgłoszenie", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Otaguj mnie!...", + "PLACEHOLDER": "Enter tag", "DELETE": "Usuń tag", "ADD": "Dodaj tag" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "filtry", + "TITLE": "Filtry", "INPUT_PLACEHOLDER": "Temat lub odniesienie", "TITLE_ACTION_FILTER_BUTTON": "szukaj", - "BREADCRUMB_TITLE": "wróć do kategorii", - "BREADCRUMB_FILTERS": "Filtry", - "BREADCRUMB_STATUS": "status" + "INPUT_SEARCH_PLACEHOLDER": "Temat lub referencja", + "TITLE_ACTION_SEARCH": "Szukaj", + "ACTION_SAVE_CUSTOM_FILTER": "zapisz jako filtr niestandardowy", + "PLACEHOLDER_FILTER_NAME": "Wpisz nazwę filtru i kliknij enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Typ", + "STATUS": "Statusy", + "SEVERITY": "Ważność", + "PRIORITIES": "Priorytety", + "TAGS": "Tagi", + "ASSIGNED_TO": "Przypisane do", + "CREATED_BY": "Stworzona przez", + "CUSTOM_FILTERS": "Filtry niestandardowe", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Usuń filtr niestandardowy", + "MESSAGE": "filtr niestandardowy '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Nagłówek pierwszego poziomu", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Podgląd", "EDIT_BUTTON": "Edycja", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Składnia Markdown pomoc" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprinty", "VIEW_SPRINTS": "Przeglądaj Sprinty", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Przeglądaj historyjki użytkownika", "ADD_USER_STORIES": "Dodawaj historyjki użytkownika", "MODIFY_USER_STORIES": "Modyfikuj historyjki użytkownika", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Usuwaj historyjki użytkownika" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Przeglądaj zadania", "ADD_TASKS": "Dodawaj zadania", "MODIFY_TASKS": "Modyfikuj zadania", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Usuwaj zadania" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Przeglądaj zgłoszenia", "ADD_ISSUES": "Dodawaj zgłoszenia", "MODIFY_ISSUES": "Modyfikuj zgłoszenia", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Usuwaj zgłoszenia" }, "WIKI": { @@ -359,13 +396,48 @@ "HOME": { "PAGE_TITLE": "Strona główna - Taiga", "PAGE_DESCRIPTION": "Główna strona Taiga, z Twoimi głównymi projektami i wszystkimi przypisanymi Tobie i obserwowanymi historyjkami użytkownika, zadaniami i zgłoszeniami.", - "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are working on.", + "EMPTY_WORKING_ON": "Trochę pusto, nieprawdaż? Zacznij pracować z Taiga a tutaj pojawią się historie, zadania i zgłoszenia nad którymi pracujesz.", "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues in your projects and be notified about its changes :)", "EMPTY_PROJECT_LIST": "Nie masz jeszcze żadnych projektów", "WORKING_ON_SECTION": "Pracujesz nad", "WATCHING_SECTION": "Obserwujesz", "DASHBOARD": "Projects Dashboard" }, + "EPICS": { + "TITLE": "EPIKI", + "SECTION_NAME": "Epics", + "EPIC": "EPIK", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+DODAJ EPIK", + "UNASSIGNED": "Nieprzypisane" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Głosy", + "NAME": "Nazwa", + "PROJECT": "Projekt", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Statusy", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "Nowy epik", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Wymaganie zespołu", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Zablokowane", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Moje projekty - Taiga", "PAGE_DESCRIPTION": "Lista wszystkich Twoich projektów, możesz zmieniać ich kolejność lub tworzyć nowe.", @@ -374,13 +446,13 @@ "ATTACHMENT": { "SECTION_NAME": "załączniki", "TITLE": "{{ plik }} załadowany dnia {{ data }}", - "LIST_VIEW_MODE": "List view mode", - "GALLERY_VIEW_MODE": "Gallery view mode", + "LIST_VIEW_MODE": "Tryb listy", + "GALLERY_VIEW_MODE": "Tryb galerii", "DESCRIPTION": "Wpisz krótki opis", "DEPRECATED": "(przestarzały)", "DEPRECATED_FILE": "Przestarzałe?", "ADD": "Dodaj nowy załącznik. {{maxFileSizeMsg}}", - "DROP": "Drop attachments here!", + "DROP": "Upuść załączniki tutaj", "SHOW_DEPRECATED": "+ pokaż przestarzałe załączniki", "HIDE_DEPRECATED": "- ukryj przestarzałe załączniki", "COUNT_DEPRECATED": "({{ counter }} przestarzałych", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Edytuj wartość", - "TITLE_ACTION_DELETE_VALUE": "Usuń wartość" + "TITLE_ACTION_DELETE_VALUE": "Usuń wartość", + "TITLE_ACTION_DELETE_TAG": "Usuń tag" }, "HELP": "Potrzebujesz pomocy? Sprawdź naszą stronę wsparcia!", "PROJECT_DEFAULT_VALUES": { @@ -414,7 +487,7 @@ "PAGE_TITLE": "Członkostwa - {{projectName}}", "ADD_BUTTON": "+ Nowy członek", "ADD_BUTTON_TITLE": "Dodaj nowego członka", - "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Unfortunately, this project has reached its limit of ({{members}}) allowed members.", + "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Niestety ten projekt osiągnął maksymalną liczbę {{{members}}} dozwolonych użytkowników.", "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project has reached its limit of ({{members}}) allowed members. If you would like to increase that limit please contact the administrator." }, "PROJECT_EXPORT": { @@ -435,12 +508,14 @@ "TITLE": "Moduły", "ENABLE": "Włącz", "DISABLE": "Wyłącz", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Dziennik", "BACKLOG_DESCRIPTION": "Zarządzaj swoimi historyjkami użytkownika aby utrzymać zorganizowany widok i priorytety zadań", "NUMBER_SPRINTS": "Expected number of sprints", - "NUMBER_SPRINTS_HELP": "0 for an undetermined number", + "NUMBER_SPRINTS_HELP": "0 dla nieokreślonej liczby", "NUMBER_US_POINTS": "Expected total of story points", - "NUMBER_US_POINTS_HELP": "0 for an undetermined number", + "NUMBER_US_POINTS_HELP": "0 dla nieokreślonej liczby", "KANBAN": "Kanban", "KANBAN_DESCRIPTION": "Organizuj swój projekt przy użyciu metody lean.", "ISSUES": "Zgłoszenia", @@ -464,8 +539,8 @@ "PROJECT_SLUG": "Szczegóły projektu", "TAGS": "Tagi", "DESCRIPTION": "Opis", - "RECRUITING": "Is this project looking for people?", - "RECRUITING_MESSAGE": "Who are you looking for?", + "RECRUITING": "Czy ten pojekt szuka uczestników?", + "RECRUITING_MESSAGE": "Kogo szukasz?", "RECRUITING_PLACEHOLDER": "Define the profiles you are looking for", "PUBLIC_PROJECT": "Projekt publiczny", "PRIVATE_PROJECT": "Projekt prywatny", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Zamierzasz zmienić link dostępu do danych CSV. Poprzedni link będzie niedostępny. Czy jesteś pewien?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "raporty historii użytkownika", "SECTION_TITLE_TASK": "Raporty zadań", "SECTION_TITLE_ISSUE": "raporty zgłoszeń", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Własne Pola", "SUBTITLE": "Zdefiniuj własne dodatkowe pola dla historyjek użytkownika, zadań i zgłoszeń.", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Własne pola dla historyjek użytkownika", "US_ADD": "Dodaj własne pole dla historyjek użytkownika", "TASK_DESCRIPTION": "Własne pola dla zadań", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Zdefiniuj statusy dla historyjek użytkownika, zadań i zgłoszeń.", - "US_TITLE": "Statusy", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Statusy zadań", "ISSUE_TITLE": "Statusy zgłoszeń" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Typy zgłoszeń", "ACTION_ADD": "Dodaj nowy {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Tagi", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Dodaj tag", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Role - {{projectName}}", "WARNING_NO_ROLE": "Bez przydzielenia ról w projekcie nie ma możliwości oceniania historyjek użytkownika. Umpa Lumpy nie będą wiedziały komu wolno to zrobić :)", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "zaproszenie do {{e-mail}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Domyślna wartość dla selektora punktów", - "LABEL_US": "Domyślna wartość dla selektora statusu historyjek użytkownika", "LABEL_TASK_STATUS": "Domyśla wartość dla selektora statusu zadań", - "LABEL_PRIORITY": "Domyślna wartość dla selektora priorytetu", - "LABEL_SEVERITY": "Domyślna wartość dla selektora ważności", "LABEL_ISSUE_TYPE": "Domyślna wartość dla selektora typu zgłoszenia", - "LABEL_ISSUE_STATUS": "Domyślna wartość dla selektora statusu zgłoszenia" + "LABEL_ISSUE_STATUS": "Domyślna wartość dla selektora statusu zgłoszenia", + "LABEL_PRIORITY": "Domyślna wartość dla selektora priorytetu", + "LABEL_SEVERITY": "Domyślna wartość dla selektora ważności" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Wpisz nazwę nowego statusu" @@ -681,7 +776,8 @@ "PRIORITIES": "Priorytety", "SEVERITIES": "Ważność", "TYPES": "Typy", - "CUSTOM_FIELDS": "Niestandardowe pola" + "CUSTOM_FIELDS": "Niestandardowe pola", + "TAGS": "Tagi" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Profil projektu" @@ -727,9 +823,9 @@ "REPORT": "Zgłoś naruszenie", "TABS": { "ACTIVITY_TAB": "Oś czasu", - "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "ACTIVITY_TAB_TITLE": "Wyświetl całą aktywność użytkownika", "PROJECTS_TAB": "Projekty", - "PROJECTS_TAB_TITLE": "List of all projects in which the user is a member", + "PROJECTS_TAB_TITLE": "Lista wszystkich projektów, do których należy użytkownik", "LIKES_TAB": "Likes", "LIKES_TAB_TITLE": "List all likes made by this user", "VOTES_TAB": "Głosy", @@ -743,7 +839,7 @@ "PROFILE_SIDEBAR": { "TITLE": "Twój profil", "DESCRIPTION": "People can see everything you do and what you are working on. Add a nice bio to give an enhanced version of your information.", - "ADD_INFO": "Edit bio" + "ADD_INFO": "Edytuj biografię" }, "PROFILE_FAVS": { "FILTER_INPUT_PLACEHOLDER": "Type something...", @@ -751,13 +847,15 @@ "FILTER_TYPE_ALL_TITLE": "Show all", "FILTER_TYPE_PROJECTS": "Projekty", "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Stories", "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", "FILTER_TYPE_TASKS": "Zadania", "FILTER_TYPE_TASK_TITLES": "Show only tasks", "FILTER_TYPE_ISSUES": "Zgłoszenia", "FILTER_TYPE_ISSUES_TITLE": "Show only issues", - "EMPTY_TITLE": "It looks like there's nothing to show here." + "EMPTY_TITLE": "Wygląda na to, że nie ma niczego do wyświetlenia tutaj." } }, "PROJECT": { @@ -766,14 +864,14 @@ "SECTION_PROJECTS": "Projekty", "HELP": "Ustal kolejność Twoich projektów tak, aby na górze znalazły się te najważniejsze.
Pierwsze 10 projektów pojawi się w liście projektów na górnym pasku nawigacji.", "PRIVATE": "Projekt prywatny", - "LOOKING_FOR_PEOPLE": "This project is looking for people", + "LOOKING_FOR_PEOPLE": "Ten projekt szuka uczestników", "FANS_COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}", "WATCHERS_COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", "MEMBERS_COUNTER_TITLE": "{total, plural, one{one member} other{# members}}", "BLOCKED_PROJECT": { - "BLOCKED": "Blocked project", - "THIS_PROJECT_IS_BLOCKED": "This project is temporarily blocked", - "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "In order to unblock your projects, contact the administrator." + "BLOCKED": "Projekt zablokowany", + "THIS_PROJECT_IS_BLOCKED": "Ten projekt jest tymczasowo zablokowany", + "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "Aby odblokować swój projekt, skontaktuj się z administacją" }, "STATS": { "PROJECT": "projekt
punkty", @@ -890,8 +988,8 @@ "SECTION_NAME": "Usuń konto z Taiga", "CONFIRM": "Czy na pewno chcesz usunąć swoje konto z Taiga?", "NEWSLETTER_LABEL_TEXT": "Nie chcę więcej otrzymywać waszego newslettera", - "CANCEL": "Back to settings", - "ACCEPT": "Delete account", + "CANCEL": "Powrót do ustawień", + "ACCEPT": "Usuń konto", "BLOCK_PROJECT": "Note that all the projects you own projects will be blocked after you delete your account. If you do want a project blocked, transfer ownership to another member of each project prior to deleting your account.", "SUBTITLE": "Sorry to see you go. We'll be here if you should ever consider us again! :(" }, @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Opcjonalne) Dodaj spersonalizowany tekst do zaproszenia. Napisz coś słodziachnego do nowego członka zespołu :)", "PLACEHOLDER_TYPE_EMAIL": "Wpisz Email", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nowa historyjka użytkownika", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Temat", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Historyjka użytkownika {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Zakończono {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} z {{userStoryTotalTasks}} zadań). Punktów: {{userStoryPoints}}. Opis: {{userStoryDescription}}", - "SECTION_NAME": "Szczegóły historyjki użytkownika", + "SECTION_NAME": "Historyjka użytkownika", "LINK_TASKBOARD": "Tablica zadań", "TITLE_LINK_TASKBOARD": "Idź do listy zadań", "TOTAL_POINTS": "total points", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Ta historyjka została utworzona z", "GO_TO_EXTERNAL_REFERENCE": "Idź do źródła", "BLOCKED": "Ta historia użytkownika jest zablokowana", - "PREVIOUS": "poprzednia historia użytkownika", - "NEXT": "następna historia użytkownika", "TITLE_DELETE_ACTION": "Usuń historyjkę użytkownika", "LIGHTBOX_TITLE_BLOKING_US": "Blokuje nas", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} zadanie zakończone", "ASSIGN": "Przypisz historyjkę użytkownika", "NOT_ESTIMATED": "Nie oszacowane", "TOTAL_US_POINTS": "Łącznie punktów", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Wymaganie zespołu", "CLIENT_REQUIREMENT": "Wymaganie klienta", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Komentarz usunięty przez {{user}} w dniu {{date}}", + "DELETED_INFO": "Komentarz usunięty przez {{user}}", "TITLE": "Komentarze", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Ostatnie najpierw", "COMMENT": "Komentarz", + "EDIT_COMMENT": "Edytuj komentarz", + "EDITED_COMMENT": "Edytowano", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Tutaj wpisz nowy komentarz", "SHOW_DELETED": "Pokaż usunięty komentarz", "HIDE_DELETED": "Ukryj skasowane komentarze", "DELETE": "Delete comment", - "RESTORE": "Przywróć komentarz" + "RESTORE": "Przywróć komentarz", + "HISTORY": { + "TITLE": "Aktywność" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Pokaż aktywność", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Pokaż poprzednie wpisy ({{showMore}} więcej)", "TITLE": "Aktywność", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "usunięty", "ADDED": "dodany", - "US_POINTS": "Punkty HU ({{name}})", + "TAGS_ADDED": "dodano klucz", + "TAGS_REMOVED": "usunięto tag:", + "US_POINTS": "{{role}} points", "NEW_ATTACHMENT": "nowy załącznik", - "DELETED_ATTACHMENT": "Usunięty załącznik", - "UPDATED_ATTACHMENT": "Zaktualizowany załącznik {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "Usunięty niestandardowy atrybut", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Dokonano {size, plural, one{one change} other{# changes}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Wymaganie zespołu", + "CLIENT_REQUIREMENT": "Wymaganie klienta", + "BLOCKED": "Zablokowane", "VALUES": { "YES": "tak", "NO": "nie", @@ -1052,12 +1198,14 @@ "TAGS": "tagi", "ATTACHMENTS": "załączniki", "IS_DEPRECATED": "jest przedawniony", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "kolejność", "BACKLOG_ORDER": "kolejność backlogu", "SPRINT_ORDER": "kolejność sprintów", "KANBAN_ORDER": "kolejność kanban", "TASKBOARD_ORDER": "kolejność tablicy zadań", - "US_ORDER": "Kolejność HU" + "US_ORDER": "Kolejność HU", + "COLOR": "kolor" } }, "BACKLOG": { @@ -1069,7 +1217,7 @@ "CUSTOMIZE_GRAPH_ADMIN": "Admin", "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Przejdź do bieżącego sprintu", - "MOVE_US_TO_LATEST_SPRINT": "Move to latest Sprint", + "MOVE_US_TO_LATEST_SPRINT": "Przejdź do ostatniego sprintu", "SHOW_FILTERS": "Pokaż filtry", "SHOW_TAGS": "Pokaż tagi", "EMPTY": "The backlog is empty!", @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "zamkniętych
zadań", "IOCAINE_DOSES": "dawek
Iokainy", "SHOW_STATISTICS_TITLE": "Pokaż statystyki", - "TOGGLE_BAKLOG_GRAPH": "Pokaż/Ukryj wykres spalania" + "TOGGLE_BAKLOG_GRAPH": "Pokaż/Ukryj wykres spalania", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "punktów w
projekcie", @@ -1122,9 +1271,7 @@ "TITLE": "Filtry", "REMOVE": "Usuń filtry", "HIDE": "Ukryj filtry", - "SHOW": "Pokaż filtry", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Tagi" + "SHOW": "Pokaż filtry" }, "SPRINTS": { "TITLE": "SPRINTY", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Zadanie {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Opis: {{taskDescription}}", - "SECTION_NAME": "Szczegóły zadania", + "SECTION_NAME": "Zadania", "LINK_TASKBOARD": "Tablica zadań", "TITLE_LINK_TASKBOARD": "Idź do listy zadań", "PLACEHOLDER_SUBJECT": "Wpisz temat zadania", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Źródło tego zadania to", "TITLE_LINK_GO_ORIGIN": "Idź do historyjki użytkownika", "BLOCKED": "To zadanie jest zablokowane", - "PREVIOUS": "poprzednie zadanie", - "NEXT": "następne zadanie", "TITLE_DELETE_ACTION": "Usuń zadanie", "LIGHTBOX_TITLE_BLOKING_TASK": "Blokowanie zadania", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Zgłoszenia - {{projectName}}", "PAGE_DESCRIPTION": "Lista zgłoszeń w projekcie {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Zgłoszenia", - "SECTION_NAME": "Szczegóły zgłoszenia", + "SECTION_NAME": "Zgłoszenie", "ACTION_NEW_ISSUE": "+ NOWE ZGŁOSZENIE", "ACTION_PROMOTE_TO_US": "Awansuj na historyjkę użytkownika", - "PLACEHOLDER_FILTER_NAME": "Wpisz nazwę filtru i kliknij enter", "PROMOTED": "To zgłoszenie zostało wypromowane na HU:", "EXTERNAL_REFERENCE": "Źródło zgłoszenia", "GO_TO_EXTERNAL_REFERENCE": "Idź do źródła", "BLOCKED": "To zgłoszenie jest zablokowane", - "TITLE_PREVIOUS_ISSUE": "poprzednie zgłoszenie", - "TITLE_NEXT_ISSUE": "następne zgłoszenie", "ACTION_DELETE": "Usuń zgłoszenie", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blokowanie zgłoszenia", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Awansuj to zgłoszenie na historyjkę użytkownika", "MESSAGE": "Jesteś pewny, że chcesz wypromować to zgłoszenie na historyjkę użytkownika?" }, - "FILTERS": { - "TITLE": "Filtry", - "INPUT_SEARCH_PLACEHOLDER": "Temat lub referencja", - "TITLE_ACTION_SEARCH": "Szukaj", - "ACTION_SAVE_CUSTOM_FILTER": "zapisz jako filtr niestandardowy", - "BREADCRUMB": "Filtry", - "TITLE_BREADCRUMB": "Filtry", - "CATEGORIES": { - "TYPE": "Typy", - "STATUS": "Statusy", - "SEVERITY": "Ważność", - "PRIORITIES": "Priorytety", - "TAGS": "Tagi", - "ASSIGNED_TO": "Przypisane do", - "CREATED_BY": "Stworzona przez", - "CUSTOM_FILTERS": "Filtry niestandardowe" - }, - "CONFIRM_DELETE": { - "TITLE": "Usuń filtr niestandardowy", - "MESSAGE": "filtr niestandardowy '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Typ", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Szukaj - {{projectName}}", "PAGE_DESCRIPTION": "Możesz przeszukiwać wszystko, historyjki użytkownika, zgłoszenia, zadania oraz strony Wiki w projekcie {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Historyjki użytkownika", "FILTER_ISSUES": "Zgłoszenia", "FILTER_TASKS": "Zadania", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Usuń tą stronę Wiki", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Linki", - "ACTION_ADD_LINK": "Dodaj link" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "razy
edytowano", "LAST_EDIT": "ostatnia
edycja", "LAST_MODIFICATION": "ostatnia modyfikacja" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Utworzone", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "Użytkownik {{username}} utworzył nowe zadanie {{obj_name}} w projekcie {{project_name}} należące do HU {{us_name}}", "WIKI_CREATED": "Użytkownik {{username}} utworzył nową stronę Wiki {{obj_name}} w projekcie {{project_name}}", "MILESTONE_CREATED": "Użytkownik {{username}} utworzył nowy sprint {{obj_name}} w projekcie {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "Użytkownik {{username}} utworzył projekt {{project_name}}", "MILESTONE_UPDATED": "Użytkownik {{username}} zaktualizował sprint {{obj_name}}", "US_UPDATED": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} historyjki użytkownika {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} należącego do HU {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} należącego do HU {{us_name}} na {{new_value}}", "WIKI_UPDATED": "Użytkownik {{username}} zaktualizował stronę Wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "Użytkownik {{username}} skomentował historyjkę użytkownika {{obj_name}}", "NEW_COMMENT_ISSUE": "Użytkownik {{username}} skomentował zgłoszenie {{obj_name}}", "NEW_COMMENT_TASK": "Użytkownik {{username}} skomentował zadanie {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "Projekt {{project_name}} ma nowego członka", "US_ADDED_MILESTONE": "Użytkownik{{username}} dodał HU {{obj_name}} do {{sprint_name}}", "US_MOVED": "{{username}} przeniósł historyjkę użytkownika {{obj_name}}", @@ -1553,7 +1691,7 @@ "MOST_LIKED": "Most liked", "MOST_LIKED_EMPTY": "There are no LIKED projects yet", "VIEW_MORE": "View more", - "RECRUITING": "This project is looking for people", + "RECRUITING": "Ten projekt szuka ludzi", "FEATURED": "Featured Projects", "EMPTY": "There are no projects to show with this search criteria.
Try again!", "FILTERS": { diff --git a/app/locales/taiga/locale-pt-br.json b/app/locales/taiga/locale-pt-br.json index b888bba8..992f5e69 100644 --- a/app/locales/taiga/locale-pt-br.json +++ b/app/locales/taiga/locale-pt-br.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Um item por linha...", "NEW_BULK": "Nova inserção em lote", "RELATED_TASKS": "Tarefas relacionadas", + "PREVIOUS": "Anterior", + "NEXT": "Próximo", "LOGOUT": "Sair", "EXTERNAL_USER": "um usuário externo", "GENERIC_ERROR": "Um Oompa Loompas disse {{error}}.", @@ -43,8 +45,13 @@ "TEAM_REQUIREMENT": "Requisito de time é um requisito que deve existir no projeto, mas que não deve ter nenhum custo para o cliente.", "OWNER": "Dono do Projeto", "CAPSLOCK_WARNING": "Seja cuidadoso! Você está escrevendo em letras maiúsculas e esse campo é case sensitive, ou seja, trata com distinção as letras maiúsculas das minúsculas.", - "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", - "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Você tem certeza que quer fechar o modo de edição?", + "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Lembre-se que se você fechar o modo de edição sem salvar, todas as mudanças serão perdidas", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Este valor parece ser inválido.", "TYPE_EMAIL": "Este valor deve ser um e-mail válido.", @@ -69,7 +76,7 @@ "MAX_CHECK": "Você deve selecionar %s escolhas ou menos.", "RANGE_CHECK": "Você deve selecionar entre %s e %s escolhas.", "EQUAL_TO": "Esse valor deveria ser o mesmo.", - "LINEWIDTH": "One or more lines is perhaps too long. Try to keep under %s characters.", + "LINEWIDTH": "Talvez uma ou mais linhas estejam muito grandes. Tente usar menos de %s caracteres.", "PIKADAY": "Formato de data inválido, por favor, use DD MMM YYYY (exemplo: 23 Mar 1984)" }, "PICKERDATE": { @@ -115,8 +122,9 @@ "USER_STORY": "História de usuário", "TASK": "Tarefa", "ISSUE": "Problema", + "EPIC": "Épico", "TAGS": { - "PLACEHOLDER": "Adicionar tags...", + "PLACEHOLDER": "Enter tag", "DELETE": "Apagar tag", "ADD": "Adicionar tag" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "filtros", + "TITLE": "Filtros", "INPUT_PLACEHOLDER": "Assunto ou referência", "TITLE_ACTION_FILTER_BUTTON": "procurar", - "BREADCRUMB_TITLE": "voltar para categorias", - "BREADCRUMB_FILTERS": "Filtros", - "BREADCRUMB_STATUS": "status" + "INPUT_SEARCH_PLACEHOLDER": "Assunto ou ref", + "TITLE_ACTION_SEARCH": "Procurar", + "ACTION_SAVE_CUSTOM_FILTER": "salve como filtro personalizado", + "PLACEHOLDER_FILTER_NAME": "Digite o nome do filtro e pressione Enter", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tipo", + "STATUS": "Status", + "SEVERITY": "Gravidade", + "PRIORITIES": "Prioridades", + "TAGS": "Tags", + "ASSIGNED_TO": "Atribuído a", + "CREATED_BY": "Criado por", + "CUSTOM_FILTERS": "Filtros personalizados", + "EPIC": "Épico" + }, + "CONFIRM_DELETE": { + "TITLE": "Apagar filtro personalizado", + "MESSAGE": "O filtro personalizado '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Primeira caixa de cabeçalho", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Pré Visualizar", "EDIT_BUTTON": "Editar", "ATTACH_FILE_HELP": "Anexe arquivos arrastando e soltando na área de texto acima.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Ajuda de sintaxe markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Épicos", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprints", "VIEW_SPRINTS": "Ver sprints", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Ver histórias de usuários", "ADD_USER_STORIES": "Adicionar histórias de usuários", "MODIFY_USER_STORIES": "Modificar histórias de usuários", + "COMMENT_USER_STORIES": "Comentar histórias de usuário", "DELETE_USER_STORIES": "Apagar histórias de usuários" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Ver tarefas", "ADD_TASKS": "Adicionar uma nova Tarefa", "MODIFY_TASKS": "Modificar tarefa", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Apagar tarefas" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Ver problemas", "ADD_ISSUES": "Adicionar problemas", "MODIFY_ISSUES": "Modificar problemas", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Apagar problemas" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Observando", "DASHBOARD": "Painel de Projetos" }, + "EPICS": { + "TITLE": "ÉPICOS", + "SECTION_NAME": "Épicos", + "EPIC": "ÉPICO", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADICIONAR ÉPICO", + "UNASSIGNED": "Não-atribuído" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Saiba mais sobre épicos" + }, + "TABLE": { + "VOTES": "Votos", + "NAME": "Nome", + "PROJECT": "Projeto", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Status", + "PROGRESS": "Progresso", + "VIEW_OPTIONS": "Ver opções" + }, + "CREATE": { + "TITLE": "Novo Épico", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Bloqueado", + "BLOCKED_NOTE_PLACEHOLDER": "Por que esse épico está bloqueado?", + "CREATE_EPIC": "Criar épico" + } + }, "PROJECTS": { "PAGE_TITLE": "Meus projetos - Taiga", "PAGE_DESCRIPTION": "Uma lista com todos os seus projetos, você pode reorganizá-los ou criar um novo.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Editar valor", - "TITLE_ACTION_DELETE_VALUE": "Apagar valor" + "TITLE_ACTION_DELETE_VALUE": "Apagar valor", + "TITLE_ACTION_DELETE_TAG": "Apagar tag" }, "HELP": "Você precisa de ajuda? Verifique nossa pagina de suporte!", "PROJECT_DEFAULT_VALUES": { @@ -414,8 +487,8 @@ "PAGE_TITLE": "Filiados - {{projectName}}", "ADD_BUTTON": "+ Novo Membro", "ADD_BUTTON_TITLE": "Adicionar novo membro", - "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Unfortunately, this project has reached its limit of ({{members}}) allowed members.", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project has reached its limit of ({{members}}) allowed members. If you would like to increase that limit please contact the administrator." + "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "Infelizmente, este projeto atingiu o número máximo de ({{membros}}) membros permitidos.", + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Este projeto atingiu o limite de ({{members}}) membros permitidos. Se você deseja aumentar este limite entre em contato com o administrador." }, "PROJECT_EXPORT": { "TITLE": "Exportar", @@ -435,6 +508,8 @@ "TITLE": "Modulos", "ENABLE": "Habilitar", "DISABLE": "Desabilitar", + "EPICS": "Épicos", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Backlog", "BACKLOG_DESCRIPTION": "Gerencie suas histórias de usuários para manter uma visualização organizada de trabalhos futuros e priorizados.", "NUMBER_SPRINTS": "Número de sprints esperadas", @@ -450,7 +525,7 @@ "MEETUP": "Reunião", "MEETUP_DESCRIPTION": "Selecione seu sistema de videoconferência.", "SELECT_VIDEOCONFERENCE": "Selecione um sistema de videoconferência", - "SALT_CHAT_ROOM": "Add a prefix to the chatroom name", + "SALT_CHAT_ROOM": "Adicionar um prefixo ao nome da sala de chat", "JITSI_CHAT_ROOM": "Jitsi", "APPEARIN_CHAT_ROOM": "AppearIn", "TALKY_CHAT_ROOM": "Talky", @@ -474,18 +549,18 @@ "LOGO_HELP": "A imagem deve ser na escala de 80x80px.", "CHANGE_LOGO": "Alterar logo", "ACTION_USE_DEFAULT_LOGO": "Usar imagem padrão", - "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects allowed by your current plan", - "MAX_PRIVATE_PROJECTS_MEMBERS": "The maximum number of members for private projects has been exceeded", - "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects allowed by your current plan", - "MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds your maximum number of members for public projects", + "MAX_PRIVATE_PROJECTS": "Você atingiu o número máximo de projetos privados permitidos para seu plano atual.", + "MAX_PRIVATE_PROJECTS_MEMBERS": "O número máximo de membros para projetos privados foi excedido.", + "MAX_PUBLIC_PROJECTS": "Infelizmente você atingiu o número máximo de projetos público permitidos para seu plano atual", + "MAX_PUBLIC_PROJECTS_MEMBERS": "Este projeto atingiu o seu limite atual de membros para projetos públicos", "PROJECT_OWNER": "Dono do projeto", "REQUEST_OWNERSHIP": "Solicitar propriedade", "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Gostaria de se tornar o novo dono do projeto?", "REQUEST_OWNERSHIP_DESC": "Solicitar ao atual dono de projeto {{name}} a transferência da propriedade deste projeto para você.", "REQUEST_OWNERSHIP_BUTTON": "Solicitação", - "REQUEST_OWNERSHIP_SUCCESS": "We'll notify the project owner", - "CHANGE_OWNER": "Change owner", - "CHANGE_OWNER_SUCCESS_TITLE": "Ok, your request has been sent!", + "REQUEST_OWNERSHIP_SUCCESS": "Vamos notificar o dono do projeto", + "CHANGE_OWNER": "Mudar dono", + "CHANGE_OWNER_SUCCESS_TITLE": "Ok, sua requisição foi enviada!", "CHANGE_OWNER_SUCCESS_DESC": "We will notify you by email if the project ownership request is accepted or declined" }, "REPORTS": { @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Você está prestes a alterar a url de acesso a dados do CSV. A URL anterior será desabilitada. Você está certo disso?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "Relatórios de histórias de usuários", "SECTION_TITLE_TASK": "relatórios de tarefas", "SECTION_TITLE_ISSUE": "relatórios de problemas", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Campos Personalizados", "SUBTITLE": "Especificar campos personalizados para histórias de usuários, tarefas e problemas", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Campos personalizados das histórias de usuários", "US_ADD": "Adicionar campo personalizado nas histórias de usuários", "TASK_DESCRIPTION": "Campos personalizados das Tarefas", @@ -534,7 +612,7 @@ "PROJECT_VALUES_PRIORITIES": { "TITLE": "Prioridades", "SUBTITLE": "Especifique as prioridades que seus problemas terão", - "ISSUE_TITLE": "Prioridades do problema", + "ISSUE_TITLE": "Severidade dos apontamentos", "ACTION_ADD": "Adicionar nova prioridade" }, "PROJECT_VALUES_SEVERITIES": { @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Especifique os status pelos quais suas histórias de usuários, tarefas e problemas passarão", - "US_TITLE": "Estados das Histórias de Usuários", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Estados da Tarefa", "ISSUE_TITLE": "Estados do problema" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Tipos de problemas", "ACTION_ADD": "Adicionar novo {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Tags", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Atualmente não há tags", + "EMPTY_SEARCH": "Parece que nada foi encontrado com os critérios de sua pesquisa.", + "ACTION_ADD": "Adicionar tag", + "NEW_TAG": "Nova tag", + "MIXING_HELP_TEXT": "Selecione as tags que você quer mesclar", + "MIXING_MERGE": "Mesclar Tags", + "SELECTED": "Selecionado" + }, "ROLES": { "PAGE_TITLE": "Funções - {{projectName}}", "WARNING_NO_ROLE": "Seja cuidadoso, nenhuma função em seu projeto será capaz de estimar o valor dos pontos para as histórias de usuários", @@ -565,7 +655,7 @@ "COUNT_MEMBERS": "{{ role.members_count }} membros com a mesma função", "TITLE_DELETE_ROLE": "Apagar Função", "REPLACEMENT_ROLE": "Todos os usuários com essa função serão movidos para", - "WARNING_DELETE_ROLE": "Be careful! All role estimations will be removed", + "WARNING_DELETE_ROLE": "Cuidado! Todas as estimativas de papéis serão removidas", "ERROR_DELETE_ALL": "Você não pode apagar todos os valores", "EXTERNAL_USER": "Usuário externo" }, @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "o convite para {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Valores padrões para o seletor de pontos", - "LABEL_US": "Valor padrão para seletor de status da História de Usuário", "LABEL_TASK_STATUS": "Valor padrão para seletor de status de tarefa", - "LABEL_PRIORITY": "Valor padão para seletor de prioridade", - "LABEL_SEVERITY": "Valor padrão para seletor de gravidade", "LABEL_ISSUE_TYPE": "Valor padrão para seletor de tipo de problema ", - "LABEL_ISSUE_STATUS": "Valor padrão para seletor de status de problema" + "LABEL_ISSUE_STATUS": "Valor padrão para seletor de status de problema", + "LABEL_PRIORITY": "Valor padão para seletor de prioridade", + "LABEL_SEVERITY": "Valor padrão para seletor de gravidade" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Digite um nome para o novo status" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioridades", "SEVERITIES": "Gravidades", "TYPES": "Tipos", - "CUSTOM_FIELDS": "Campos personalizados" + "CUSTOM_FIELDS": "Campos personalizados", + "TAGS": "Tags" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Perfil do Projeto" @@ -695,21 +791,21 @@ "TITLE": "Serviços" }, "PROJECT_TRANSFER": { - "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Would you like to become the new project owner?", - "PRIVATE": "Private", - "ACCEPTED_PROJECT_OWNERNSHIP": "Congratulations! You're now the new project owner.", - "REJECTED_PROJECT_OWNERNSHIP": "OK. We'll contact the current project owner", + "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Você gostaria de se tornar o novo dono do projeto?", + "PRIVATE": "Privado", + "ACCEPTED_PROJECT_OWNERNSHIP": "Parabéns! Você é o proprietário do projeto agora.", + "REJECTED_PROJECT_OWNERNSHIP": "OK. Entraremos em contato com o atual dono do projeto.", "ACCEPT": "Aceitar", - "REJECT": "Reject", + "REJECT": "Rejeitar", "PROPOSE_OWNERSHIP": "{{owner}}, the current owner of the project {{project}} has asked that you become the new project owner.", "ADD_COMMENT": "Would you like to add a comment for the project owner?", - "UNLIMITED_PROJECTS": "Unlimited", + "UNLIMITED_PROJECTS": "Ilimitado", "OWNER_MESSAGE": { "PRIVATE": "Please remember that you can own up to {{maxProjects}} private projects. You currently own {{currentProjects}} private projects", "PUBLIC": "Please remember that you can own up to {{maxProjects}} public projects. You currently own {{currentProjects}} public projects" }, "CANT_BE_OWNED": "At the moment you cannot become an owner of a project of this type. If you would like to become the owner of this project, please contact the administrator so they change your account settings to enable project ownership.", - "CHANGE_MY_PLAN": "Change my plan" + "CHANGE_MY_PLAN": "Mudar meu plano" } }, "USER": { @@ -751,12 +847,14 @@ "FILTER_TYPE_ALL_TITLE": "Mostrar tudo", "FILTER_TYPE_PROJECTS": "Projetos", "FILTER_TYPE_PROJECT_TITLES": "Mostrar somente projetos", + "FILTER_TYPE_EPICS": "Épicos", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Histórias", "FILTER_TYPE_USER_STORIES_TITLES": "Mostrar apenas histórias de usuários", "FILTER_TYPE_TASKS": "Tarefas", "FILTER_TYPE_TASK_TITLES": "Mostrar apenas tarefas", "FILTER_TYPE_ISSUES": "Problemas", - "FILTER_TYPE_ISSUES_TITLE": "mostrar apenas problemas", + "FILTER_TYPE_ISSUES_TITLE": "mostrar apenas apontamentos", "EMPTY_TITLE": "Parece que não há nada para exibir aqui." } }, @@ -772,8 +870,8 @@ "MEMBERS_COUNTER_TITLE": "{total, plural, one{one member} other{# members}}", "BLOCKED_PROJECT": { "BLOCKED": "Projeto bloqueado", - "THIS_PROJECT_IS_BLOCKED": "This project is temporarily blocked", - "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "In order to unblock your projects, contact the administrator." + "THIS_PROJECT_IS_BLOCKED": "Este projeto está temporariamente bloqueado", + "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "Para desbloquear seus projetos, contate o administrador." }, "STATS": { "PROJECT": "projetos
pontos", @@ -838,7 +936,7 @@ "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) é muito pesado para nossos Oompa Loompas, tente algo menor que ({{maxFileSize}})", "SYNC_SUCCESS": "Seu projeto foi importado com sucesso", "PROJECT_RESTRICTIONS": { - "PROJECT_MEMBERS_DESC": "The project you are trying to import has {{members}} members, unfortunately, your current plan allows for a maximum of {{max_memberships}} members per project. If you would like to increase that limit please contact the administrator.", + "PROJECT_MEMBERS_DESC": "O projeto que você está tentando importar tem {{members}} membros e infelizmente seu plano atual tem um limite máximo de {{max_memberships}} membros por projeto. Se você deseja aumentar este limite entre em contato com o administrador.", "PRIVATE_PROJECTS_SPACE": { "TITLE": "Unfortunately, your current plan does not allow for additional private projects", "DESC": "The project you are trying to import is private. Unfortunately, your current plan does not allow for additional private projects." @@ -855,7 +953,7 @@ }, "PRIVATE_PROJECTS_SPACE_MEMBERS": { "TITLE": "Unfortunately your current plan doesn't allow additional private projects or an increase of more than {{max_memberships}} members per private project", - "DESC": "The project that you are trying to import is private and has {{members}} members." + "DESC": "O projeto que você está tentando importar é privado e tem {{members}} membros." }, "PUBLIC_PROJECTS_SPACE_MEMBERS": { "TITLE": "Unfortunately your current plan doesn't allow additional public projects or an increase of more than {{max_memberships}} members per public project", @@ -892,7 +990,7 @@ "NEWSLETTER_LABEL_TEXT": "Eu não quero receber mais os informativos", "CANCEL": "Voltar para configurações", "ACCEPT": "Remover conta", - "BLOCK_PROJECT": "Note that all the projects you own projects will be blocked after you delete your account. If you do want a project blocked, transfer ownership to another member of each project prior to deleting your account.", + "BLOCK_PROJECT": "Note que todos os projetos em que você é o dono serão bloqueados depois que você apagar sua conta. Se você quer um projeto bloqueado, transfira a posse para outro membro de cada projeto antes de apagar sua conta.", "SUBTITLE": "É uma pena vê-lo partir. Estaremos aqui se você algum dia considerar-nos novamente! :(" }, "DELETE_PROJECT": { @@ -950,18 +1048,18 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Opcional) Adicione uma mensagem de texto ao convite. Diga algo animador para os novos membros ;-)", "PLACEHOLDER_TYPE_EMAIL": "Digite um Email", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { - "TITLE": "Unfortunately, this project can't be left without an owner", + "TITLE": "Infelizmente, este projeto não pode ficar sem um dono", "CURRENT_USER_OWNER": { - "DESC": "You are the current owner of this project. Before leaving, please transfer ownership to someone else.", - "BUTTON": "Change the project owner" + "DESC": "Você é o dono atual deste projeto. Antes de sair, por favor transfira o projeto para outra pessoa.", + "BUTTON": "Mude o dono do projeto" }, "OTHER_USER_OWNER": { - "DESC": "Unfortunately, you can't delete a member who is also the current project owner. First, please assign a new project owner.", - "BUTTON": "Request project owner change" + "DESC": "Infelizmnete, você não pode apagar um membro que também é o dono de um projeto. Primeiro designe um novo proprietário para o projeto.", + "BUTTON": "Solicite a mudança do dono do projeto" } }, "CHANGE_OWNER": { @@ -970,28 +1068,57 @@ "BUTTON": "Pedir a este membro do projeto para se tornar o novo dono do projeto" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Épico", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Nova história de usuário", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Assunto", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - História de Usuário {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estado: {{userStoryStatus }}. Completos {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} de {{userStoryTotalTasks}} tarefas encerradas). Pontos: {{userStoryPoints}}. Descrição: {{userStoryDescription}}", - "SECTION_NAME": "Detalhes da História de Usuário", + "SECTION_NAME": "História de usuário", "LINK_TASKBOARD": "Quadro de Tarefas", "TITLE_LINK_TASKBOARD": "Ir para o quadro de tarefas", "TOTAL_POINTS": "total de pontos", "ADD": "+ Adicionar uma nova História de Usuário", "ADD_BULK": "Adicionar Histórias de Usuários em lote", - "PROMOTED": "Esta história de usuário foi promovida do problema:", - "TITLE_LINK_GO_TO_ISSUE": "Ir para problema", + "PROMOTED": "Esta História de Usuário foi criada a partir do Problema:", + "TITLE_LINK_GO_TO_ISSUE": "Adicionar comentários aos apontamentos", "EXTERNAL_REFERENCE": "Esta História de Usuário foi criada de", "GO_TO_EXTERNAL_REFERENCE": "Ir para a origem", "BLOCKED": "Esta história de usuário está bloqueada", - "PREVIOUS": "história de usuário anterior", - "NEXT": "proxima história de usuário", "TITLE_DELETE_ACTION": "Apagar história de usuário", "LIGHTBOX_TITLE_BLOKING_US": "História de usuário bloqueadora", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tarefas completas", "ASSIGN": "Atribuir História de Usuário", "NOT_ESTIMATED": "Não estimado", "TOTAL_US_POINTS": "Total de pontos de histórias", + "TRIBE": { + "PUBLISH": "Publicar como Gig no Taiga Tribe", + "PUBLISH_INFO": "Mais informações", + "PUBLISH_TITLE": "Mais informações sobre como publicar na Tribo Taiga", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Editar link", + "CLOSE": "Fechar", + "SYNCHRONIZE_LINK": "sincronizar com a Tribo Taiga", + "PUBLISH_MORE_INFO_TITLE": "Você precisa de alguém para esta tarefa?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Requisitos da Equipe", "CLIENT_REQUIREMENT": "Requisitos do Cliente", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Comentário apagado por {{user}} em {{date}}", + "DELETED_INFO": "Comentário apagado por {{user}}", "TITLE": "Comentários", + "COMMENTS_COUNT": "{{comments}} comentários", + "ORDER": "Ordenação", + "OLDER_FIRST": "Antigos primeiro", + "RECENT_FIRST": "Recentes primeiro", "COMMENT": "Comentário", + "EDIT_COMMENT": "Editar comentário", + "EDITED_COMMENT": "Editado:", + "SHOW_HISTORY": "Ver histórico", "TYPE_NEW_COMMENT": "Escreva um novo comentário aqui", "SHOW_DELETED": "Mostrar comentários apagados", "HIDE_DELETED": "Esconder comentário apagado", "DELETE": "Apagar comentário", - "RESTORE": "Restaurar comentário" + "RESTORE": "Restaurar comentário", + "HISTORY": { + "TITLE": "Atividade" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Exibir atividade", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Mostrar entradas anteriores (mais {{showMore}})", "TITLE": "Atividade", + "ACTIVITIES_COUNT": "{{activities}} atividades", "REMOVED": "removido", "ADDED": "adicionado", - "US_POINTS": "pontos de história ({{name}})", - "NEW_ATTACHMENT": "novo anexo", - "DELETED_ATTACHMENT": "apagar anexo", - "UPDATED_ATTACHMENT": "anexo atualizado {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "atributo personalizado apagado", + "TAGS_ADDED": "tags adicionadas:", + "TAGS_REMOVED": "tags removidas:", + "US_POINTS": "{{role}} pontos", + "NEW_ATTACHMENT": "novo anexo:", + "DELETED_ATTACHMENT": "anexo apagado:", + "UPDATED_ATTACHMENT": "anexo atualizado ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "atributo personalizado criado", + "UPDATED_CUSTOM_ATTRIBUTE": "atributo personalizado atualizado", "SIZE_CHANGE": "Feito {size, plural, one{one change} other{# changes}}", + "BECAME_DEPRECATED": "foi depreciado", + "BECAME_UNDEPRECATED": "foi depreciado", + "TEAM_REQUIREMENT": "Requisitos da Equipe", + "CLIENT_REQUIREMENT": "Requisitos do Cliente", + "BLOCKED": "Bloqueado", "VALUES": { "YES": "sim", "NO": "não", @@ -1052,12 +1198,14 @@ "TAGS": "tags", "ATTACHMENTS": "anexos", "IS_DEPRECATED": "está obsoleto", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "ordem", "BACKLOG_ORDER": "requisição do backlog", "SPRINT_ORDER": "ordem de sprint ", "KANBAN_ORDER": "pedido kanban", "TASKBOARD_ORDER": "Ordem de quadro de tarefa", - "US_ORDER": "ordem da história de usuário" + "US_ORDER": "ordem da história de usuário", + "COLOR": "cor" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "tarefas
fechadas", "IOCAINE_DOSES": "iocaine
doses", "SHOW_STATISTICS_TITLE": "Mostrar estatísticas", - "TOGGLE_BAKLOG_GRAPH": "Mostrar/Esconder gráfico de burndown" + "TOGGLE_BAKLOG_GRAPH": "Mostrar/Esconder gráfico de burndown", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "pontos do
projeto", @@ -1122,9 +1271,7 @@ "TITLE": "Filtros", "REMOVE": "Remover filtros", "HIDE": "Esconder Filtros", - "SHOW": "Mostrar Filtros", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Tags" + "SHOW": "Mostrar Filtros" }, "SPRINTS": { "TITLE": "SPRINTS", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Tarefa {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Estado: {{taskStatus }}. Descrição: {{taskDescription}}", - "SECTION_NAME": "Detalhes da Tarefa", + "SECTION_NAME": "Tarefa", "LINK_TASKBOARD": "Quadro de Tarefas", "TITLE_LINK_TASKBOARD": "Ir para o quadro de tarefas", "PLACEHOLDER_SUBJECT": "Digite um novo titulo para tarefa", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Essa tarefa foi criada a partir de", "TITLE_LINK_GO_ORIGIN": "Ir para história de usuário", "BLOCKED": "Esta tarefa está bloqueada", - "PREVIOUS": "tarefa anterior", - "NEXT": "nova tarefa", "TITLE_DELETE_ACTION": "Apagar Tarefa", "LIGHTBOX_TITLE_BLOKING_TASK": "Tarefa bloqueadora", "FIELDS": { @@ -1225,19 +1370,16 @@ "SUCCESS": "Nossos Oompa Loompas atualizaram seu email" }, "ISSUES": { - "PAGE_TITLE": "Problemas - {{projectName}}", + "PAGE_TITLE": "Apontamentos - {{projectName}}", "PAGE_DESCRIPTION": "O painel de problemas do projeto {{projectName}}: {{projectDescription}}", - "LIST_SECTION_NAME": "Problemas", - "SECTION_NAME": "Detalhes do problema", + "LIST_SECTION_NAME": "Tipos de problemas", + "SECTION_NAME": "Problema", "ACTION_NEW_ISSUE": "+ NOVO PROBLEMA", "ACTION_PROMOTE_TO_US": "Promover para História de Usuário", - "PLACEHOLDER_FILTER_NAME": "Digite o nome do filtro e pressione Enter", "PROMOTED": "Esse problema foi promovido para história de usuário", "EXTERNAL_REFERENCE": "Esse problema foi criado a partir de", "GO_TO_EXTERNAL_REFERENCE": "Ir para a origem", - "BLOCKED": "Esse problema está bloqueado", - "TITLE_PREVIOUS_ISSUE": "problema anterior", - "TITLE_NEXT_ISSUE": "próximo problema", + "BLOCKED": "Esse apontamento está bloqueado", "ACTION_DELETE": "Problema apagado", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Problema que está bloqueando", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Promover esse problema para nova história de usuário", "MESSAGE": "Você tem certeza que deseja criar uma nova História de Usuário a partir desse problema?" }, - "FILTERS": { - "TITLE": "Filtros", - "INPUT_SEARCH_PLACEHOLDER": "Assunto ou ref", - "TITLE_ACTION_SEARCH": "Procurar", - "ACTION_SAVE_CUSTOM_FILTER": "salve como filtro personalizado", - "BREADCRUMB": "Filtros", - "TITLE_BREADCRUMB": "Filtros", - "CATEGORIES": { - "TYPE": "Tipo", - "STATUS": "Status", - "SEVERITY": "Gravidade", - "PRIORITIES": "Prioridades", - "TAGS": "Tags", - "ASSIGNED_TO": "Atribuído a", - "CREATED_BY": "Criado por", - "CUSTOM_FILTERS": "Filtros personalizados" - }, - "CONFIRM_DELETE": { - "TITLE": "Apagar filtro personalizado", - "MESSAGE": "O filtro personalizado '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tipo", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Buscar - {{projectName}}", "PAGE_DESCRIPTION": "Busque qualquer coisa, histórias de usuários, problemas, tarefas, ou páginas da wiki, no projeto {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Épicos", "FILTER_USER_STORIES": "Histórias de Usuários", "FILTER_ISSUES": "Problemas", "FILTER_TASKS": "Tarefas", @@ -1332,7 +1453,7 @@ "APP_TITLE": "EQUIPE - {{projectName}}", "PLACEHOLDER_INPUT_SEARCH": "Procurar pelo nome completo...", "COLUMN_MR_WOLF": "Sr. Wolf", - "EXPLANATION_COLUMN_MR_WOLF": "Problemas fechados", + "EXPLANATION_COLUMN_MR_WOLF": "Adicionar apontamentos", "COLUMN_IOCAINE": "Bebedor de Iocaine", "EXPLANATION_COLUMN_IOCAINE": "Doses de Iocaine ingeridas", "COLUMN_CERVANTES": "Pero Vaz de Caminha", @@ -1397,7 +1518,7 @@ "WIZARD": { "SECTION_TITLE_CREATE_PROJECT": "Criar Projeto", "CREATE_PROJECT_TEXT": "Novo em folha. Tão excitante!", - "CHOOSE_TEMPLATE": "Which template fits your project best?", + "CHOOSE_TEMPLATE": "Qual template se encaixa melhor no seu projeto?", "CHOOSE_TEMPLATE_TITLE": "Mais informações sobre templates de projeto", "CHOOSE_TEMPLATE_INFO": "Mais informações", "PROJECT_DETAILS": "Detalhes do Projeto", @@ -1405,7 +1526,7 @@ "PRIVATE_PROJECT": "Projeto Privado", "CREATE_PROJECT": "Criar projeto", "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects", - "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects", + "MAX_PUBLIC_PROJECTS": "Infelizmente, você atingiu o número máximo de projetos públicos", "CHANGE_PLANS": "change plans" }, "WIKI": { @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Apagar página Wiki", "DELETE_LINK_TITLE": "Remover link de Wiki", "NAVIGATION": { - "SECTION_NAME": "Links", - "ACTION_ADD_LINK": "Adicionar link" + "HOME": "Página principal", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "Todas as páginas wiki" }, "SUMMARY": { "TIMES_EDITED": "vezes
editadas", "LAST_EDIT": "última
edição", "LAST_MODIFICATION": "ultima modificação" + }, + "SECTION_PAGES_LIST": "Todas as páginas", + "PAGES_LIST_COLUMNS": { + "TITLE": "Título", + "EDITIONS": "Edições", + "CREATED": "Criado", + "MODIFIED": "Modificado", + "CREATOR": "Criador", + "LAST_MODIFIER": "Último modificador" } }, "HINTS": { @@ -1447,21 +1579,27 @@ "TASK_CREATED_WITH_US": "{{username}} criou nova tarefa {{obj_name}} em {{project_name}} que pertence a História de Usuário {{us_name}}", "WIKI_CREATED": "{{username}} criou uma página wiki {{obj_name}} em {{project_name}}", "MILESTONE_CREATED": "{{username}} criou uma nova sprint {{obj_name}} em {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} criou o projeto {{project_name}}", "MILESTONE_UPDATED": "{{username}} atualizou a sprint {{obj_name}}", "US_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" da História de Usuário {{obj_name}}", - "US_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o trabalho \"{{field_name}}\" da US {{obj_name}} para {{new_value}}", - "US_UPDATED_POINTS": "{{username}} atualizou pontos de '{{role_name}}' da História de Usuário {{obj_name}} para {{new_value}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" da História de Usuário {{obj_name}} para {{new_value}}", + "US_UPDATED_POINTS": "{{username}} atualizou os pontos de '{{role_name}}' da História de Usuário {{obj_name}} para {{new_value}}", "ISSUE_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" do problema {{obj_name}}", "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" do problema {{obj_name}} para {{new_value}}", "TASK_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} para {{new_value}}", - "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} Atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} para {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} para {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} que pertence à História de Usuário {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} que pertence à História de Usuário {{us_name}} para {{new_value}}", "WIKI_UPDATED": "{{username}} atualizou a página wiki {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} comentou na História de Usuário {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} comentou no problema {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} comentou na tarefa {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} tem um membro novo", "US_ADDED_MILESTONE": "{{username}} adicionou a História de Usuário {{obj_name}} a {{sprint_name}}", "US_MOVED": "{{username}} moveu a História de Usuário {{obj_name}}", @@ -1499,7 +1637,7 @@ }, "STEP3": { "TITLE": "Observando", - "TEXT1": "And right here you will find the ones in your projects that you want to know about.", + "TEXT1": "E aqui você vai encontrar os do seu projeto que você escolheu seguir.", "TEXT2": "Você já está trabalhando com Taiga ;)" }, "STEP4": { @@ -1555,7 +1693,7 @@ "VIEW_MORE": "Visualizar mais", "RECRUITING": "Este projeto esta procurando colaboradores", "FEATURED": "Featured Projects", - "EMPTY": "There are no projects to show with this search criteria.
Try again!", + "EMPTY": "Não há projetos para exibir sob esse critério de pesquisa.
Tente novamente!", "FILTERS": { "ALL": "Tudo", "KANBAN": "Kanban", diff --git a/app/locales/taiga/locale-ru.json b/app/locales/taiga/locale-ru.json index 89e9c995..0f41251b 100644 --- a/app/locales/taiga/locale-ru.json +++ b/app/locales/taiga/locale-ru.json @@ -35,19 +35,26 @@ "ONE_ITEM_LINE": "Один объект на строку...", "NEW_BULK": "Добавить пакетно", "RELATED_TASKS": "Связанные задачи", + "PREVIOUS": "Предыдущий", + "NEXT": "Следующий", "LOGOUT": "Выйти", "EXTERNAL_USER": "внешний пользователь", "GENERIC_ERROR": "Один из Умпа-Лумп говорит {{error}}.", "IOCAINE_TEXT": "Чувствуете, что задание берет верх над вами? Дайте другим знать об этом, нажав на \"Иокаин\", когда редактируете задание. Возможно стать неуязвимым к этому (выдуманному) смертельном яду, потребляя небольшие количества время от времени, так же как возможно стать лучше в том, что вы делаете, временами беря на себя дополнительные препятствия!", "CLIENT_REQUIREMENT": "Client requirement is new requirement that was not previously expected and it is required to be part of the project", "TEAM_REQUIREMENT": "Team requirement is a requirement that must exist in the project but should have no cost for the client", - "OWNER": "Project Owner", + "OWNER": "Владелец проекта", "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Кажется, это значение некорректно.", - "TYPE_EMAIL": "Это значение должно быть корректным email-адресом.", + "TYPE_EMAIL": "Значение должно быть корректной электронной почтой.", "TYPE_URL": "Это значение должно быть корректным URL-адресом.", "TYPE_URLSTRICT": "Это значение должно быть корректным URL-адресом.", "TYPE_NUMBER": "Это значение должно быть правильным числом", @@ -115,8 +122,9 @@ "USER_STORY": "Пользовательская история", "TASK": "Задача", "ISSUE": "Запрос", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Назначьте тэг", + "PLACEHOLDER": "Enter tag", "DELETE": "Удалить тэг", "ADD": "Добавить тэг" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "фильтры", + "TITLE": "Фильтры", "INPUT_PLACEHOLDER": "Название ссылки", "TITLE_ACTION_FILTER_BUTTON": "поиск", - "BREADCRUMB_TITLE": "назад к категориям", - "BREADCRUMB_FILTERS": "Фильтры", - "BREADCRUMB_STATUS": "cтатус" + "INPUT_SEARCH_PLACEHOLDER": "Название ссылки", + "TITLE_ACTION_SEARCH": "Поиск", + "ACTION_SAVE_CUSTOM_FILTER": "сохранить как специальный фильтр", + "PLACEHOLDER_FILTER_NAME": "Введите название фильтра и нажмите \"ввод\"", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Тип", + "STATUS": "Статус", + "SEVERITY": "Важность", + "PRIORITIES": "Приоритеты", + "TAGS": "Тэги", + "ASSIGNED_TO": "Назначено", + "CREATED_BY": "Создано", + "CUSTOM_FILTERS": "Собственные фильтры", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Удалить фильтр", + "MESSAGE": "специальный фильтр '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Заголовок первого уровня", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Предварительный просмотр", "EDIT_BUTTON": "Редактировать", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Помощь по синтаксису Markdown" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Спринты", "VIEW_SPRINTS": "Посмотреть спринты", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Просматривать пользовательские истории", "ADD_USER_STORIES": "Добавлять пользовательские истории", "MODIFY_USER_STORIES": "Изменять пользовательские истории", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Удалять пользовательские истории" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Просмотреть задачи", "ADD_TASKS": "Добавить задачи", "MODIFY_TASKS": "Редактировать задачи", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Удалить задачи" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Посмотреть запросы", "ADD_ISSUES": "Добавить запросы", "MODIFY_ISSUES": "Изменить запросы", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Удалить запросы" }, "WIKI": { @@ -287,7 +324,7 @@ }, "LOGIN_COMMON": { "HEADER": "У меня уже есть логин в Taiga", - "PLACEHOLDER_AUTH_NAME": "Логин или email (с учетом регистра)", + "PLACEHOLDER_AUTH_NAME": "Имя пользователя или электронная почта (с учётом регистра)", "LINK_FORGOT_PASSWORD": "Забыли?", "TITLE_LINK_FORGOT_PASSWORD": "Вы забыли свой пароль?", "ACTION_ENTER": "Ввод", @@ -295,7 +332,7 @@ "PLACEHOLDER_AUTH_PASSWORD": "Пароль (чувствителен к регистру)" }, "LOGIN_FORM": { - "ERROR_AUTH_INCORRECT": "Oompa Loompas считает, что Ваш логин, email или пароль неправильный.", + "ERROR_AUTH_INCORRECT": "Oompa Loompas считает, что ваше имя пользователя, электронная почта или пароль неправильные.", "SUCCESS": "Oompa Loompas счастлив, добро пожаловать в Тайгу!" }, "REGISTER": { @@ -306,7 +343,7 @@ "TITLE": "Зарегистрируйте аккаунт Taiga (бесплатно)", "PLACEHOLDER_NAME": "Выберите имя учётной записи (с учётом регистра)", "PLACEHOLDER_FULL_NAME": "Введите Ваше полное имя", - "PLACEHOLDER_EMAIL": "Ваш email", + "PLACEHOLDER_EMAIL": "Ваша электронная почта", "PLACEHOLDER_PASSWORD": "Задайте новый пароль (с учетом регистра)", "ACTION_SIGN_UP": "Зарегистрироваться", "TITLE_LINK_LOGIN": "Войти", @@ -318,12 +355,12 @@ }, "FORGOT_PASSWORD_FORM": { "TITLE": "Упс, забыли пароль?", - "SUBTITLE": "Введите Ваш логин или email для получения нового пароля", - "PLACEHOLDER_FIELD": "Логин или e-mail", + "SUBTITLE": "Введите ваше имя пользователя или электронную почту, чтобы получить новый пароль", + "PLACEHOLDER_FIELD": "Имя пользователя или электронная почта", "ACTION_RESET_PASSWORD": "Сбросить пароль", "LINK_CANCEL": "Не, давай назад, думаю я вспомню.", - "SUCCESS_TITLE": "Check your inbox!", - "SUCCESS_TEXT": "We sent you an email with the instructions to set a new password", + "SUCCESS_TITLE": "Проверьте Вашу почту!", + "SUCCESS_TEXT": "Мы отправили вам письмо с инструкциями по восстановлению пароля", "ERROR": "Умпа-Лумпы говорят, что вы еще не зарегистрированы." }, "CHANGE_PASSWORD": { @@ -359,13 +396,48 @@ "HOME": { "PAGE_TITLE": "Домашняя страница - Taiga", "PAGE_DESCRIPTION": "Главная страница Taiga с вашими основными проектами, назначенными и отслеживаемыми ПИ, задачами и запросами", - "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are working on.", + "EMPTY_WORKING_ON": "Тут кажется пусто, не правда ли? Начинайте использовать Taiga и вы увидите здесь истории, задачи и запросы над которыми вы сейчас работаете.", "EMPTY_WATCHING": "Следите за пользовательскими историями, задачами, запросами в ваших проектах и будьте уведомлены об изменениях :)", "EMPTY_PROJECT_LIST": "У Вас пока нет проектов", "WORKING_ON_SECTION": "Работает над", "WATCHING_SECTION": "Отслеживаемые", "DASHBOARD": "Рабочий стол с проектами" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Не назначено" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Голоса", + "NAME": "Имя", + "PROJECT": "Проект", + "SPRINT": "Спринт", + "ASSIGNED_TO": "Assigned", + "STATUS": "Статус", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Заблокирован", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Мои проекты", "PAGE_DESCRIPTION": "Список Ваших проектов, отсортируйте их или создайте новый.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Изменить значение", - "TITLE_ACTION_DELETE_VALUE": "Удалить значение" + "TITLE_ACTION_DELETE_VALUE": "Удалить значение", + "TITLE_ACTION_DELETE_TAG": "Удалить тэг" }, "HELP": "Вам нужна помощь? Проверьте нашу страницу техподдержки!", "PROJECT_DEFAULT_VALUES": { @@ -425,7 +498,7 @@ "LOADING_TITLE": "Мы создали ваш файл резервной копии", "DUMP_READY": "Файл резервной копии готов!", "LOADING_MESSAGE": "Пожалуйста, не закрывайте эту страницу", - "ASYNC_MESSAGE": "Мы отправим вам email когда будет готово.", + "ASYNC_MESSAGE": "Мы отправим вам письмо когда будет готово.", "SYNC_MESSAGE": "Если загрузка не начинается самостоятельно, нажмите здесь.", "ERROR": "У Oompa Loompas возникли проблемы при создании резервной копии. Повторите еще раз.", "ERROR_BUSY": "Извините, Oompa Loompas очень загружен сейчас. Повторите через несколько минут.", @@ -435,9 +508,11 @@ "TITLE": "Модули", "ENABLE": "Включить", "DISABLE": "Выключить", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Список задач", "BACKLOG_DESCRIPTION": "Управляйте пользовательскими историями, чтобы поддерживать организованное видение важных и приоритетных задач.", - "NUMBER_SPRINTS": "Expected number of sprints", + "NUMBER_SPRINTS": "Ожидаемое количество спринтов", "NUMBER_SPRINTS_HELP": "0 for an undetermined number", "NUMBER_US_POINTS": "Expected total of story points", "NUMBER_US_POINTS_HELP": "0 for an undetermined number", @@ -448,9 +523,9 @@ "WIKI": "Вики", "WIKI_DESCRIPTION": "Добавляйте, изменяйте или удаляйте контент совместно с остальными. Это самое правильное место для документации вашего проекта.", "MEETUP": "Созвониться", - "MEETUP_DESCRIPTION": "Choose your videoconference system.", + "MEETUP_DESCRIPTION": "Выберите Вашу систему видеоконференций", "SELECT_VIDEOCONFERENCE": "Выберите систему видеоконференций", - "SALT_CHAT_ROOM": "Add a prefix to the chatroom name", + "SALT_CHAT_ROOM": "Добавить префикс к имени чата", "JITSI_CHAT_ROOM": "Jitsi", "APPEARIN_CHAT_ROOM": "AppearIn", "TALKY_CHAT_ROOM": "Talky", @@ -472,20 +547,20 @@ "PRIVATE_OR_PUBLIC": "В чём разница между публичными и приватными проектами?", "DELETE": "Удалить проект", "LOGO_HELP": "Изображение будет отмасштабировано до 80x80px.", - "CHANGE_LOGO": "Change logo", + "CHANGE_LOGO": "Изменить лого", "ACTION_USE_DEFAULT_LOGO": "Использовать картинку по умолчанию", - "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects allowed by your current plan", - "MAX_PRIVATE_PROJECTS_MEMBERS": "The maximum number of members for private projects has been exceeded", + "MAX_PRIVATE_PROJECTS": "Вы достигли максимального числа приватных проектов которое разрешено вашим планом.", + "MAX_PRIVATE_PROJECTS_MEMBERS": "Максимальное количество участников в приватном проекте достигло лимита", "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects allowed by your current plan", "MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds your maximum number of members for public projects", - "PROJECT_OWNER": "Project owner", - "REQUEST_OWNERSHIP": "Request ownership", - "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Do you want to become the new project owner?", + "PROJECT_OWNER": "Владелец проекта", + "REQUEST_OWNERSHIP": "Запрос владельца", + "REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Хотите стать новым владельцем проекта?", "REQUEST_OWNERSHIP_DESC": "Request that current project owner {{name}} transfer ownership of this project to you.", "REQUEST_OWNERSHIP_BUTTON": "Запрос", - "REQUEST_OWNERSHIP_SUCCESS": "We'll notify the project owner", - "CHANGE_OWNER": "Change owner", - "CHANGE_OWNER_SUCCESS_TITLE": "Ok, your request has been sent!", + "REQUEST_OWNERSHIP_SUCCESS": "Мы уведомим владельца проекта", + "CHANGE_OWNER": "Сменить владельца", + "CHANGE_OWNER_SUCCESS_TITLE": "ОК, ваш запрос был отправлен!", "CHANGE_OWNER_SUCCESS_DESC": "We will notify you by email if the project ownership request is accepted or declined" }, "REPORTS": { @@ -497,18 +572,21 @@ "REGENERATE_SUBTITLE": "Вы собираетесь изменить ссылку доступа к данным CSV. Прежний вариант ссылки перестанет работать. Вы уверены?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "Отчёты по пользовательским историям", "SECTION_TITLE_TASK": "отчёты о задачах", "SECTION_TITLE_ISSUE": "отчёты о запросах", "DOWNLOAD": "Скачать CSV", "URL_FIELD_PLACEHOLDER": "Упс, забыли пароль?", - "TITLE_REGENERATE_URL": " Сделать CSV ссылку ещё раз", + "TITLE_REGENERATE_URL": "Сделать CSV ссылку ещё раз", "ACTION_GENERATE_URL": "Сгенерировать ссылку", "ACTION_REGENERATE": "Создать заново" }, "CUSTOM_FIELDS": { "TITLE": "Пользовательские поля", "SUBTITLE": "Укажите специальные поля для ваших пользовательских историй, задач и запросов", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Специальные поля для пользовательских историй", "US_ADD": "Добавить специальное поле для пользовательских историй", "TASK_DESCRIPTION": "Специальные поля задач", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Статус", "SUBTITLE": "Укажите, какие статусы будут принимать ваши пользовательские истории, задачи и запросы", - "US_TITLE": "Статусы ПИ", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Статус задач", "ISSUE_TITLE": "Статусы запроса" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Типы запросов", "ACTION_ADD": "Добавить новый" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Тэги", + "SUBTITLE": "Просмотреть и изменить цвет ваших тэгов", + "EMPTY": "В данный момент тэги отсутствуют", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Добавить тэг", + "NEW_TAG": "Новый тэг", + "MIXING_HELP_TEXT": "Выберите тэги которые вы хотели бы объединить", + "MIXING_MERGE": "Объединить Тэги", + "SELECTED": "Выбранные" + }, "ROLES": { "PAGE_TITLE": "Роли - {{projectName}}", "WARNING_NO_ROLE": "Осторожнее: ни с какими ролями на вашем проекте участники не смогут оценить очки для пользовательских историй.", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Веб-хуки - {{projectName}}", "SECTION_NAME": "Веб-хуки", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "приглашение на {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Значения по умолчанию для выбора очков", - "LABEL_US": "Значение по умолчанию для статуса ПИ", "LABEL_TASK_STATUS": "Значение по умолчанию для статуса задачи", - "LABEL_PRIORITY": "Значение по умолчанию для выбора приоритета", - "LABEL_SEVERITY": "Значение важности по умолчанию", "LABEL_ISSUE_TYPE": "Значение по умолчанию для типа запроса", - "LABEL_ISSUE_STATUS": "Значение по умолчанию для статуса запроса" + "LABEL_ISSUE_STATUS": "Значение по умолчанию для статуса запроса", + "LABEL_PRIORITY": "Значение по умолчанию для выбора приоритета", + "LABEL_SEVERITY": "Значение важности по умолчанию" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Укажите название для нового статуса" @@ -681,7 +776,8 @@ "PRIORITIES": "Приоритет", "SEVERITIES": "Степени важности", "TYPES": "Типы", - "CUSTOM_FIELDS": "Собственные поля" + "CUSTOM_FIELDS": "Собственные поля", + "TAGS": "Тэги" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Профиль проекта" @@ -697,8 +793,8 @@ "PROJECT_TRANSFER": { "DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Would you like to become the new project owner?", "PRIVATE": "Private", - "ACCEPTED_PROJECT_OWNERNSHIP": "Congratulations! You're now the new project owner.", - "REJECTED_PROJECT_OWNERNSHIP": "OK. We'll contact the current project owner", + "ACCEPTED_PROJECT_OWNERNSHIP": "Поздравляем! Вы новый владелец проекта.", + "REJECTED_PROJECT_OWNERNSHIP": "Хорошо. Мы свяжемся с текущим владельцем проекта", "ACCEPT": "Принимаю", "REJECT": "Reject", "PROPOSE_OWNERSHIP": "{{owner}}, the current owner of the project {{project}} has asked that you become the new project owner.", @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Показать все", "FILTER_TYPE_PROJECTS": "Проекты", "FILTER_TYPE_PROJECT_TITLES": "Показать только проекты", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Истории", "FILTER_TYPE_USER_STORIES_TITLES": "Показывать только пользовательские истории", "FILTER_TYPE_TASKS": "Задачи", @@ -771,8 +869,8 @@ "WATCHERS_COUNTER_TITLE": "{total, plural, one{один наблюдатель} other{# наблюдателя (-ей)}}", "MEMBERS_COUNTER_TITLE": "{total, plural, one{one member} other{# members}}", "BLOCKED_PROJECT": { - "BLOCKED": "Blocked project", - "THIS_PROJECT_IS_BLOCKED": "This project is temporarily blocked", + "BLOCKED": "Заблокированный проект", + "THIS_PROJECT_IS_BLOCKED": "Этот проект временно заблокирован", "TO_UNBLOCK_CONTACT_THE_ADMIN_STAFF": "In order to unblock your projects, contact the administrator." }, "STATS": { @@ -890,8 +988,8 @@ "SECTION_NAME": "Удалить аккаунт", "CONFIRM": "Вы уверены, что хотите удалить ваш аккаунт?", "NEWSLETTER_LABEL_TEXT": "Я больше не хочу получать вашу новостную рассылку", - "CANCEL": "Back to settings", - "ACCEPT": "Delete account", + "CANCEL": "Вернуться к настройкам", + "ACCEPT": "Удалить аккаунт", "BLOCK_PROJECT": "Note that all the projects you own projects will be blocked after you delete your account. If you do want a project blocked, transfer ownership to another member of each project prior to deleting your account.", "SUBTITLE": "Sorry to see you go. We'll be here if you should ever consider us again! :(" }, @@ -949,31 +1047,51 @@ }, "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Необязательно) Добавьте персональный текст в приглашение. Скажите что-нибудь приятное вашим новым участникам ;-)", - "PLACEHOLDER_TYPE_EMAIL": "Укажите e-mail", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "PLACEHOLDER_TYPE_EMAIL": "Введите электронную почту", + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", "CURRENT_USER_OWNER": { "DESC": "You are the current owner of this project. Before leaving, please transfer ownership to someone else.", - "BUTTON": "Change the project owner" + "BUTTON": "Сменить владельца проекта" }, "OTHER_USER_OWNER": { "DESC": "Unfortunately, you can't delete a member who is also the current project owner. First, please assign a new project owner.", - "BUTTON": "Request project owner change" + "BUTTON": "Запрос смены владельца проекта" } }, "CHANGE_OWNER": { - "TITLE": "Who do you want to be the new project owner?", - "ADD_COMMENT": "Add comment", - "BUTTON": "Ask this project member to become the new project owner" + "TITLE": "Кого вы хотите назначить новым владельцем проекта?", + "ADD_COMMENT": "Добавить комментарий", + "BUTTON": "Предложить участнику проекта стать его новым владельцем" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Новая пользовательская история", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Тема", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Пользовательская История {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Статус: {{userStoryStatus }}. Выполнено {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} из {{userStoryTotalTasks}} задач). Очки: {{userStoryPoints}}. Описание: {{userStoryDescription}}", - "SECTION_NAME": "Детали пользовательских историй", + "SECTION_NAME": "Пользовательская история", "LINK_TASKBOARD": "Панель задач", "TITLE_LINK_TASKBOARD": "Перейти к панели задач", "TOTAL_POINTS": "общее число очков", @@ -983,15 +1101,24 @@ "TITLE_LINK_GO_TO_ISSUE": "Перейти к запросу", "EXTERNAL_REFERENCE": "Эта ПИ была создана из:", "GO_TO_EXTERNAL_REFERENCE": "Перейти в начало", - "BLOCKED": "Эта пользовательская история заблокирована ", - "PREVIOUS": "предыдущая пользовательская история", - "NEXT": "следующая пользовательская история", + "BLOCKED": "Эта пользовательская история заблокирована", "TITLE_DELETE_ACTION": "Удалить пользовательскую историю", "LIGHTBOX_TITLE_BLOKING_US": "Блокирующая ПИ", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} задач выполнено", "ASSIGN": "Поручить пользовательскую историю", "NOT_ESTIMATED": "Не оценено", "TOTAL_US_POINTS": "Всего очков за ПИ", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "Больше инфо", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Редактировать ссылку", + "CLOSE": "Закрыть", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Требование от Команды", "CLIENT_REQUIREMENT": "Требование клиента", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Комментарий удален {{user}} {{date}}", + "DELETED_INFO": "Комментарий удалён {{user}}", "TITLE": "Комментарии", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Порядок", + "OLDER_FIRST": "Сначала старые", + "RECENT_FIRST": "Сначала новые", "COMMENT": "Комментарий", + "EDIT_COMMENT": "Редактировать комментарий", + "EDITED_COMMENT": "Изменено:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Добавить комментарий", "SHOW_DELETED": "Показать удаленный комментарий", "HIDE_DELETED": "Скрыть удаленный комментарий", "DELETE": "Удалить комментарий", - "RESTORE": "Показать удаленный комментарий" + "RESTORE": "Показать удаленный комментарий", + "HISTORY": { + "TITLE": "Действия" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Показать действия", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Показать предыдущие записи (ещё {{showMore}})", "TITLE": "Действия", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "удален", "ADDED": "добавлено", - "US_POINTS": "ПИ очки ({{name}})", - "NEW_ATTACHMENT": "новое вложение", - "DELETED_ATTACHMENT": "удаленное вложение", - "UPDATED_ATTACHMENT": "обновлено приложение {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "удалить атрибут", + "TAGS_ADDED": "тэги добавлены:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "новое вложение:", + "DELETED_ATTACHMENT": "удалённое вложение:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Сделано {size, plural, one{изменение} other{# изменений}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Требование от Команды", + "CLIENT_REQUIREMENT": "Требование клиента", + "BLOCKED": "Заблокирован", "VALUES": { "YES": "да", "NO": "нет", @@ -1052,12 +1198,14 @@ "TAGS": "тэги", "ATTACHMENTS": "Вложения", "IS_DEPRECATED": "рекомендовано", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "порядок", "BACKLOG_ORDER": "порядок списка задач", "SPRINT_ORDER": "порядок спринтов", "KANBAN_ORDER": "порядок kanban", "TASKBOARD_ORDER": "порядок панели задач", - "US_ORDER": "порядок ПИ" + "US_ORDER": "порядок ПИ", + "COLOR": "цвет" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "завершённые
задачи", "IOCAINE_DOSES": "иокаина
дозы", "SHOW_STATISTICS_TITLE": "Показать статистику", - "TOGGLE_BAKLOG_GRAPH": "Показать/Скрыть график решения задач" + "TOGGLE_BAKLOG_GRAPH": "Показать/Скрыть график решения задач", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "проектные
очки", @@ -1122,9 +1271,7 @@ "TITLE": "Фильтры", "REMOVE": "Сбросить фильтры", "HIDE": "Спрятать фильтры", - "SHOW": "Показать фильтры", - "FILTER_CATEGORY_STATUS": "Статус", - "FILTER_CATEGORY_TAGS": "Тэги" + "SHOW": "Показать фильтры" }, "SPRINTS": { "TITLE": "СПРИНТЫ", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Задача {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Статус: {{taskStatus }}. Описание: {{taskDescription}}", - "SECTION_NAME": "Детали задачи", + "SECTION_NAME": "Задача", "LINK_TASKBOARD": "Панель задач", "TITLE_LINK_TASKBOARD": "Перейти к панели задач", "PLACEHOLDER_SUBJECT": "Укажите новое название задачи", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Эта задача была создана из", "TITLE_LINK_GO_ORIGIN": "Перейти к пользовательской истории", "BLOCKED": "Эта задача заблокирована", - "PREVIOUS": "предыдущая задача", - "NEXT": "следующая задача", "TITLE_DELETE_ACTION": "Удалить задачу", "LIGHTBOX_TITLE_BLOKING_TASK": "Блокирующее задание", "FIELDS": { @@ -1218,26 +1363,23 @@ "SUCCESS": "Oompa Loompas удалил ваш аккаунт" }, "CHANGE_EMAIL_FORM": { - "TITLE": "Изменить e-mail", + "TITLE": "Сменить вашу электронную почту", "SUBTITLE": "Ещё один клик и Ваш email будет обновлён!", "PLACEHOLDER_INPUT_TOKEN": "изменить идентификатор email", - "ACTION_CHANGE_EMAIL": "изменить почту", - "SUCCESS": "Oompa Loompas обновил ваш e-mail" + "ACTION_CHANGE_EMAIL": "Сменить электронную почту", + "SUCCESS": "Наш Oompa Loompas обновил вашу электронную почту" }, "ISSUES": { "PAGE_TITLE": "Запросы - {{projectName}}", "PAGE_DESCRIPTION": "Панель запросов проекта {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Запросы", - "SECTION_NAME": "Детали запроса", + "SECTION_NAME": "Запрос", "ACTION_NEW_ISSUE": "+НОВЫЙ ЗАПРОС", "ACTION_PROMOTE_TO_US": "Повысить до пользовательской истории", - "PLACEHOLDER_FILTER_NAME": "Введите название фильтра и нажмите \"ввод\"", "PROMOTED": "Этот запрос был переделан в ПИ:", "EXTERNAL_REFERENCE": "Этот запрос был создан из", "GO_TO_EXTERNAL_REFERENCE": "Перейти в начало", "BLOCKED": "Этот запрос заблокирована", - "TITLE_PREVIOUS_ISSUE": "предыдущий запрос", - "TITLE_NEXT_ISSUE": "следующий запрос", "ACTION_DELETE": "Удалить запрос", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Блокирующий запрос", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Превратить этот запрос в новую пользовательскую историю", "MESSAGE": "Вы уверены, что хотите создать новую ПИ из этого запроса?" }, - "FILTERS": { - "TITLE": "Фильтры", - "INPUT_SEARCH_PLACEHOLDER": "Название ссылки", - "TITLE_ACTION_SEARCH": "Поиск", - "ACTION_SAVE_CUSTOM_FILTER": "сохранить как специальный фильтр", - "BREADCRUMB": "Фильтры", - "TITLE_BREADCRUMB": "Фильтры", - "CATEGORIES": { - "TYPE": "Тип", - "STATUS": "Статус", - "SEVERITY": "Важность", - "PRIORITIES": "Приоритет", - "TAGS": "Тэги", - "ASSIGNED_TO": "Назначено", - "CREATED_BY": "Создано", - "CUSTOM_FILTERS": "Собственные фильтры" - }, - "CONFIRM_DELETE": { - "TITLE": "Удалить фильтр", - "MESSAGE": "специальный фильтр '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Тип", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Поиск - {{projectName}}", "PAGE_DESCRIPTION": "Ищите что угодно, пользовательские истории, задачи, запросы и вики-страницы, в проекте {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Пользовательские Истории", "FILTER_ISSUES": "Запросы", "FILTER_TASKS": "Задачи", @@ -1383,7 +1504,7 @@ "CHANGE_PHOTO": "Изменить фото", "FIELD": { "USERNAME": "Имя пользователя", - "EMAIL": "Email", + "EMAIL": "Электронная почта", "FULL_NAME": "Полное имя", "PLACEHOLDER_FULL_NAME": "Полное имя (например, Игорь Николаев)", "BIO": "Биография (не более 210 символов)", @@ -1399,10 +1520,10 @@ "CREATE_PROJECT_TEXT": "Свежий и чистый! Так здóрово!", "CHOOSE_TEMPLATE": "Which template fits your project best?", "CHOOSE_TEMPLATE_TITLE": "More info about project templates", - "CHOOSE_TEMPLATE_INFO": "More info", + "CHOOSE_TEMPLATE_INFO": "Больше инфо", "PROJECT_DETAILS": "Project Details", "PUBLIC_PROJECT": "Public Project", - "PRIVATE_PROJECT": "Private Project", + "PRIVATE_PROJECT": "Частный проект", "CREATE_PROJECT": "Создать проект", "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects", "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects", @@ -1415,15 +1536,26 @@ "PLACEHOLDER_PAGE": "Создать вики страницу", "REMOVE": "Удалить эту вики страницу", "DELETE_LIGHTBOX_TITLE": "Удалить вики страницу", - "DELETE_LINK_TITLE": "Delete Wiki link", + "DELETE_LINK_TITLE": "Удалить ссылку wiki", "NAVIGATION": { - "SECTION_NAME": "Ссылки", - "ACTION_ADD_LINK": "Добавить ссылку" + "HOME": "Главная страница", + "SECTION_NAME": "ЗАКЛАДКИ", + "ACTION_ADD_LINK": "Добавить закладку", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "раз
отредактировано", "LAST_EDIT": "последняя
правка", "LAST_MODIFICATION": "последнее изменение" + }, + "SECTION_PAGES_LIST": "Все страницы", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Создан", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} создал новую задачу {{obj_name}} в {{project_name}}, которая принадлежит ПИ {{us_name}}", "WIKI_CREATED": "{{username}} создал новую вики-страницу {{obj_name}} в {{project_name}}", "MILESTONE_CREATED": "{{username}} создал новый спринт {{obj_name}} в {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} создал проект {{project_name}}", "MILESTONE_UPDATED": "{{username}} обновил спринт {{obj_name}}", "US_UPDATED": "{{username}} обновил атрибут \"{{field_name}}\" ПИ {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} изменил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}, на {{new_value}}", "WIKI_UPDATED": "{{username}} обновил вики-страницу {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} прокомментировал ПИ {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} прокомментировал запрос {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} прокомментировал задачу {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "У {{project_name}} появился новый участник", "US_ADDED_MILESTONE": "{{username}} добавил ПИ {{obj_name}} для {{sprint_name}}", "US_MOVED": "{{username}} переместил ПИ {{obj_name}}", diff --git a/app/locales/taiga/locale-sv.json b/app/locales/taiga/locale-sv.json index 1d63b9da..33aae0c2 100644 --- a/app/locales/taiga/locale-sv.json +++ b/app/locales/taiga/locale-sv.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "En post per rad ...", "NEW_BULK": "Lägg till flera nya", "RELATED_TASKS": "Besläktade uppgifter", + "PREVIOUS": "Previous", + "NEXT": "Nästa", "LOGOUT": "Logga ut", "EXTERNAL_USER": "en extern användare", "GENERIC_ERROR": "En av våra Oompa Loompier säger {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Det här värdet är felaktigt. ", "TYPE_EMAIL": "Värdet måste vara en giltig e-postadress", @@ -115,8 +122,9 @@ "USER_STORY": "Användarhistorie", "TASK": "Uppgift", "ISSUE": "ärende", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Det är jag! Tagga mig ...", + "PLACEHOLDER": "Enter tag", "DELETE": "Ta bort etikett", "ADD": "Lägg till etikett" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?" }, "FILTERS": { - "TITLE": "filter", + "TITLE": "Filter", "INPUT_PLACEHOLDER": "Titel eller referens", "TITLE_ACTION_FILTER_BUTTON": "sök", - "BREADCRUMB_TITLE": "tillbaka till kategorierna", - "BREADCRUMB_FILTERS": "Filter", - "BREADCRUMB_STATUS": "status" + "INPUT_SEARCH_PLACEHOLDER": "Titel eller referens", + "TITLE_ACTION_SEARCH": "Sök", + "ACTION_SAVE_CUSTOM_FILTER": "spara som anpassad filter", + "PLACEHOLDER_FILTER_NAME": "Skriv filternamnet och tryck på ", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Typ", + "STATUS": "Status", + "SEVERITY": "Allvarsgrad", + "PRIORITIES": "Prioritet", + "TAGS": "Etiketter", + "ASSIGNED_TO": "Tilldelad till", + "CREATED_BY": "Skapad av", + "CUSTOM_FILTERS": "Anpassad filter", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Ta bort anpassad filter.", + "MESSAGE": "anpassad filter '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "Första nivån snart klar", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Förhandsvisa", "EDIT_BUTTON": "Redigera", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Hjälp för markeringssyntax" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Sprintar", "VIEW_SPRINTS": "Visa sprintar", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Visa användarhistorier", "ADD_USER_STORIES": "Lägg till användarhistorier", "MODIFY_USER_STORIES": "Modifiera användarhistorier", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Ta bort användarhistorier" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Visa uppgifter", "ADD_TASKS": "Lägg till uppgifter", "MODIFY_TASKS": "Modifiera uppgifter", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Ta bort uppgift" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Visa ärenden", "ADD_ISSUES": "Lägg till ärenden", "MODIFY_ISSUES": "Modifiera ärenden", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Ta bort uppgifter" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "Bevakar", "DASHBOARD": "Projects Dashboard" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Ej tilldelad" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Röster", + "NAME": "Namn", + "PROJECT": "Projekt", + "SPRINT": "Sprint", + "ASSIGNED_TO": "Assigned", + "STATUS": "Status", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Blockerad", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Mina projekt - Taiga", "PAGE_DESCRIPTION": "En lista med alla dina projekt som du kan organisera eller skapa ett nytt. ", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Redigera", - "TITLE_ACTION_DELETE_VALUE": "Ta bort" + "TITLE_ACTION_DELETE_VALUE": "Ta bort", + "TITLE_ACTION_DELETE_TAG": "Ta bort etikett" }, "HELP": "Behöver du hjälp? Besök hjälpsidorna!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Moduler", "ENABLE": "Aktivera", "DISABLE": "Avvaktivera", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Inkorg", "BACKLOG_DESCRIPTION": "Hantera dina användarhistorier för att organisera visningar av kommande och prioriterade jobb. ", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "Du kan ändra CSV för datalänken. Den tidigare länken tas bort. Är du säker på det? " }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "rapporter för användarhistorier", "SECTION_TITLE_TASK": "Rapport för uppgifter", "SECTION_TITLE_ISSUE": "Rapporter för ärenden", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Anpassade fält", "SUBTITLE": "Specificera anpassade fält för användarhistorier, uppgifter och ärenden. ", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Användarhistorier för anpassade fält", "US_ADD": "Lägg till ett anpassad fält i användarhistorien", "TASK_DESCRIPTION": "Anpassade fält för uppgifter", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Status", "SUBTITLE": "Specificera status för dina användarhistorier, uppgifter och ärenden ska ha i olika faser. ", - "US_TITLE": "US statuser", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Status för uppgifter", "ISSUE_TITLE": "Status för ärenden" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Ärendetyper", "ACTION_ADD": "Lägg till ny {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Etiketter", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Lägg till etikett", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Roller - {{projectName}}", "WARNING_NO_ROLE": "Var försiktig. Inga roller i ditt projekt kan estimera poängvärden för användarhistorier", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webbkrok - {{projectName}}", "SECTION_NAME": "Webbkrokar", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "den här invitationen till {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Standardvärde för poängväljaren", - "LABEL_US": "Standardvärde för US-statusväljare", "LABEL_TASK_STATUS": "Standardvärdet för val av uppgiftsstatus", - "LABEL_PRIORITY": "Standardvärde för val av prioritet", - "LABEL_SEVERITY": "Standardvärde för val av allvarlighet", "LABEL_ISSUE_TYPE": "Standardvärde för ärendetyp-väljare", - "LABEL_ISSUE_STATUS": "Standardvärde för väljare för ärendestatus" + "LABEL_ISSUE_STATUS": "Standardvärde för väljare för ärendestatus", + "LABEL_PRIORITY": "Standardvärde för val av prioritet", + "LABEL_SEVERITY": "Standardvärde för val av allvarlighet" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Skriv ett namn för den nya statusen" @@ -681,7 +776,8 @@ "PRIORITIES": "Prioritet", "SEVERITIES": "Allvarsgrad", "TYPES": "Typ", - "CUSTOM_FIELDS": "Anpassade fält" + "CUSTOM_FIELDS": "Anpassade fält", + "TAGS": "Etiketter" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Projektprofil" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Visa alla", "FILTER_TYPE_PROJECTS": "Projekt", "FILTER_TYPE_PROJECT_TITLES": "Visa bara projekt", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Berättelser", "FILTER_TYPE_USER_STORIES_TITLES": "Visa endast användarhistorier", "FILTER_TYPE_TASKS": "Uppgift", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Valfritt) Lägg till en personlig hälsning till invitationen. Berätta något trevligt till din nya projektmedlem ;-)", "PLACEHOLDER_TYPE_EMAIL": "Skriv in en e-postadress", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Ny användarhistorie", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Titel", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Användarhistorier {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. avslutad{{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} av {{userStoryTotalTasks}} tasks closed). Poäng: {{userStoryPoints}}. Beskrivning: {{userStoryDescription}}", - "SECTION_NAME": "Detaljer för användarhistorier", + "SECTION_NAME": "Användarhistorie", "LINK_TASKBOARD": "Uppgiftstavla", "TITLE_LINK_TASKBOARD": "Gå till uppgiftstavlan", "TOTAL_POINTS": "totalpoäng", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Denna användarhistorien är skapat från", "GO_TO_EXTERNAL_REFERENCE": "Gå till början", "BLOCKED": "Användarhistorien är blockerad", - "PREVIOUS": "tidigare användarhistorie", - "NEXT": "nästa användarhistorie", "TITLE_DELETE_ACTION": "Ta bort användarhistorien", "LIGHTBOX_TITLE_BLOKING_US": "Blockera oss", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} uppgifter kompletta", "ASSIGN": "Lägg till användarhistorie", "NOT_ESTIMATED": "Ej beräknad", "TOTAL_US_POINTS": "Total US-poäng", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Teamets behov", "CLIENT_REQUIREMENT": "Kräver beställare", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Kommentar raderad av {{user}} den {{date}}", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Kommentarer", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Kommentarer", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Skriv en ny kommentar här", "SHOW_DELETED": "Visa raderade kommentarer", "HIDE_DELETED": "Dölj raderade kommentarer", "DELETE": "Ta bort kommentar", - "RESTORE": "Hämta tillbaka tidigare kommentarer" + "RESTORE": "Hämta tillbaka tidigare kommentarer", + "HISTORY": { + "TITLE": "Aktiviteter" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Visa aktiviteter", "DATETIME": "YYYY-MM-DD HH:mm", "SHOW_MORE": "+ Visa tidigare poster ({{showMore}} more)", "TITLE": "Aktiviteter", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "borttaget", "ADDED": "lagt till", - "US_POINTS": "US-poäng ({{name}})", - "NEW_ATTACHMENT": "ny bilaga", - "DELETED_ATTACHMENT": "ta bort bifogad fil", - "UPDATED_ATTACHMENT": "uppdaterad bilaga {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "raderad anpassad atribut", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Gjorde {size, plural, one{one change} other{# changes}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Teamets behov", + "CLIENT_REQUIREMENT": "Kräver beställare", + "BLOCKED": "Blockerad", "VALUES": { "YES": "ja", "NO": "nej", @@ -1052,12 +1198,14 @@ "TAGS": "etiketter", "ATTACHMENTS": "bilagor", "IS_DEPRECATED": "undviks", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "sortera", "BACKLOG_ORDER": "sortera inkorgen", "SPRINT_ORDER": "sortera sprintar", "KANBAN_ORDER": "kanban-sortering", "TASKBOARD_ORDER": "Sortera uppgiftstavlan", - "US_ORDER": "sortera US" + "US_ORDER": "sortera US", + "COLOR": "färg" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "stängd
uppgifer", "IOCAINE_DOSES": "iocaine
doser", "SHOW_STATISTICS_TITLE": "Visa statistik", - "TOGGLE_BAKLOG_GRAPH": "Visa/Dölj burn down-graf" + "TOGGLE_BAKLOG_GRAPH": "Visa/Dölj burn down-graf", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "projekt
poäng", @@ -1122,9 +1271,7 @@ "TITLE": "Filter", "REMOVE": "Ta bort filter", "HIDE": "Dölj filter", - "SHOW": "Visa filter", - "FILTER_CATEGORY_STATUS": "Status", - "FILTER_CATEGORY_TAGS": "Etiketter" + "SHOW": "Visa filter" }, "SPRINTS": { "TITLE": "SPRINTAR", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Uppgift {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Beskrivning: {{taskDescription}}", - "SECTION_NAME": "Detaljer för uppgiften", + "SECTION_NAME": "Uppgift", "LINK_TASKBOARD": "Uppgiftstavla", "TITLE_LINK_TASKBOARD": "Gå till uppgiftstavlan", "PLACEHOLDER_SUBJECT": "Skriv in den nya uppgiftens titel", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Den här uppgiften är skapad från", "TITLE_LINK_GO_ORIGIN": "Gå till användarhistorie", "BLOCKED": "Uppgiften är blockerad", - "PREVIOUS": "tidigare uppgift", - "NEXT": "ny uppgift", "TITLE_DELETE_ACTION": "Ta bort uppgift", "LIGHTBOX_TITLE_BLOKING_TASK": "Blockerad uppgift", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Ärenden - {{projectName}}", "PAGE_DESCRIPTION": "Ärenden som listas för projektet {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Ärenden", - "SECTION_NAME": "Detaljer för ärenden ", + "SECTION_NAME": "ärende", "ACTION_NEW_ISSUE": "+ NYTT ÄRENDE", "ACTION_PROMOTE_TO_US": "Flytta till användarhistorie", - "PLACEHOLDER_FILTER_NAME": "Skriv filternamnet och tryck på ", "PROMOTED": "Ärendet har flyttats till US:", "EXTERNAL_REFERENCE": "Den här uppgiften är skapat från", "GO_TO_EXTERNAL_REFERENCE": "Gå till början", "BLOCKED": "Det här ärendet är blockerad", - "TITLE_PREVIOUS_ISSUE": "tidigare ärende", - "TITLE_NEXT_ISSUE": "nästa ärende", "ACTION_DELETE": "Ta bort ärende", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blockerad ärende", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Flytta det här ärendet till en ny användarhistorie", "MESSAGE": "Är du säker på att du vill skapa en ny US från det här ärendet?" }, - "FILTERS": { - "TITLE": "Filter", - "INPUT_SEARCH_PLACEHOLDER": "Titel eller referens", - "TITLE_ACTION_SEARCH": "Sök", - "ACTION_SAVE_CUSTOM_FILTER": "spara som anpassad filter", - "BREADCRUMB": "Filter", - "TITLE_BREADCRUMB": "Filter", - "CATEGORIES": { - "TYPE": "Typ", - "STATUS": "Status", - "SEVERITY": "Allvarsgrad", - "PRIORITIES": "Prioritet", - "TAGS": "Etiketter", - "ASSIGNED_TO": "Tilldelad till", - "CREATED_BY": "Skapad av", - "CUSTOM_FILTERS": "Anpassad filter" - }, - "CONFIRM_DELETE": { - "TITLE": "Ta bort anpassad filter.", - "MESSAGE": "anpassad filter '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Typ", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Sök - {{projectName}}", "PAGE_DESCRIPTION": "Sök på vad som helst, användarhistorier, uppgifter, ärenden och wiki-innehåll i projektet {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Användarhistorier", "FILTER_ISSUES": "Ärenden", "FILTER_TASKS": "Uppgift", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Ta bort Wiki-sida", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Länkar", - "ACTION_ADD_LINK": "Lägg till länk" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "gånger
ändrad", "LAST_EDIT": "senaste
ändring", "LAST_MODIFICATION": "senast modifierad" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Skapad", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} har skapat en ny uppgift {{obj_name}} i {{project_name}} som hör till US {{us_name}}", "WIKI_CREATED": "{{username}} skapade en ny wiki-sida {{obj_name}} i {{project_name}}", "MILESTONE_CREATED": "{{username}} har skapad en ny sprint {{obj_name}} i {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} skapade projektet {{project_name}}", "MILESTONE_UPDATED": "{{username}} har uppdaterad sprinten {{obj_name}}", "US_UPDATED": "{{username}} har uppdaterad egenskapen \"{{field_name}}\" i US {{obj_name}}", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} har uppdaterad egenskapen \"{{field_name}}\" för uppgiften {{obj_name}} som tillhör US {{us_name}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} har uppdaterad egenskapen \"{{field_name}}\" för uppgiften {{obj_name}} som tillhör US {{us_name}} till {{new_value}}", "WIKI_UPDATED": "{{username}} har uppdaterad wiki-sidan {{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} har kommenterad i {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} har kommenterad i ärendet {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} har kommenterad uppgiften {{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} har en ny medlem", "US_ADDED_MILESTONE": "{{username}} har lagt till US {{obj_name}} till {{sprint_name}}", "US_MOVED": "{{username}} har flyttat US {{obj_name}}", diff --git a/app/locales/taiga/locale-tr.json b/app/locales/taiga/locale-tr.json index 97ce06a5..029bd85e 100644 --- a/app/locales/taiga/locale-tr.json +++ b/app/locales/taiga/locale-tr.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "Her satıra bir kalem...", "NEW_BULK": "Yeni toplu ekleme", "RELATED_TASKS": "İlişkili görevler", + "PREVIOUS": "Previous", + "NEXT": "İleri", "LOGOUT": "Çıkış", "EXTERNAL_USER": "bir dış kullanıcı", "GENERIC_ERROR": "Honki ponkilerimizden biri derki; {{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "Bu değer geçersiz gözüküyor", "TYPE_EMAIL": "Bu değer geçerli bir e-posta adresi olmalı.", @@ -115,8 +122,9 @@ "USER_STORY": "Kullanıcı hikayesi", "TASK": "Görev", "ISSUE": "Sorun", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "Ben O'yum! Etiketle beni...", + "PLACEHOLDER": "Enter tag", "DELETE": "Etiket sil", "ADD": "Etiket ekle" }, @@ -193,12 +201,29 @@ "CONFIRM_DELETE": "Bu özel alandaki tüm bilgiler silinecek.\nDevam etmek istediğinize emin misiniz?" }, "FILTERS": { - "TITLE": "filtreler", + "TITLE": "Filtreler", "INPUT_PLACEHOLDER": "Konu yada referans", "TITLE_ACTION_FILTER_BUTTON": "ara", - "BREADCRUMB_TITLE": "kategorilere dön", - "BREADCRUMB_FILTERS": "Filtreler", - "BREADCRUMB_STATUS": "durum" + "INPUT_SEARCH_PLACEHOLDER": "Konu ya da ref", + "TITLE_ACTION_SEARCH": "Ara", + "ACTION_SAVE_CUSTOM_FILTER": "özel filtre olarak kaydet", + "PLACEHOLDER_FILTER_NAME": "Filtre adı yazın ve enter a basın", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "Tip", + "STATUS": "Durum ", + "SEVERITY": "Önem Derecesi", + "PRIORITIES": "Öncelikler", + "TAGS": "Etiketler ", + "ASSIGNED_TO": "Atanmış", + "CREATED_BY": "Oluşturan", + "CUSTOM_FILTERS": "Özel filtreler", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "Özel filtre sil", + "MESSAGE": "'{{customFilterName}}' özel filtresi" + } }, "WYSIWYG": { "H1_BUTTON": "İlk Düzey Başlık", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "Ön izleme", "EDIT_BUTTON": "Düzenle", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Markdown yazım kılavuzu" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "Koşular", "VIEW_SPRINTS": "Koşuları gör", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "Kullanıcı hikayelerini gör", "ADD_USER_STORIES": "Kullanıcı hikayeleri ekle", "MODIFY_USER_STORIES": "Kullanıcı hikayelerini değiştir", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "Kullanıcı hikayelerini sil" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "Görevler gör", "ADD_TASKS": "Görevleri ekle", "MODIFY_TASKS": "Görevleri değiştir", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "Görevleri sil" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "Sorunları gör", "ADD_ISSUES": "Sorunları ekle", "MODIFY_ISSUES": "Sorunları değiştir", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "Sorunları sil" }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "İzleniyor", "DASHBOARD": "Proje Panosu" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "Atama Yok" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "Oylar", + "NAME": "İsim", + "PROJECT": "Proje", + "SPRINT": "Koşu", + "ASSIGNED_TO": "Assigned", + "STATUS": "Durum ", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "Engelli", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "Projelerim - Taiga", "PAGE_DESCRIPTION": "Tüm projelerinizi içeren bir liste, yenide düzenle ya da yeni bir tane yarat.", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "Değeri düzenle", - "TITLE_ACTION_DELETE_VALUE": "Değer sil" + "TITLE_ACTION_DELETE_VALUE": "Değer sil", + "TITLE_ACTION_DELETE_TAG": "Etiket sil" }, "HELP": "Yardıma mı ihtiyacın var? Destek sayfamızı kontrol edin!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "Modüller", "ENABLE": "Etkinleştir", "DISABLE": "Pasifleştir", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "Havuz", "BACKLOG_DESCRIPTION": "Yeni gelen ve önceliklendirilmiş işler için düzenli bir görünüm elde etmek için kullanıcı hikayelerinizi yönetin.", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "CSV veri erişim linkini değiştireceksiniz. Önceki link kapatılacak. Emin misiniz?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "kullanıcı hikayeleri raporları", "SECTION_TITLE_TASK": "görevlere ait raporlar", "SECTION_TITLE_ISSUE": "sorun raporları", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "Özel Alanlar", "SUBTITLE": "Hikayeleriniz, işleriniz ve sorunlarınız için özel alanları tanımlayın", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "Kullanıcı hikayeleri özel alanları", "US_ADD": "Kullanıcı hikayelerine özel bir alan ekleyin", "TASK_DESCRIPTION": "Görevlere ait özel alanlar", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "Durum", "SUBTITLE": "Hikayeleriniz, işleriniz ve sorunlarınızın alabileceği durumları tanımlayın", - "US_TITLE": "KH Durumları", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "Görev Durumları", "ISSUE_TITLE": "Sorun Durumları" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "Sorun türleri", "ACTION_ADD": "Ekle yeni {{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "Etiketler ", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "Etiket ekle", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "Roller - {{projectName}}", "WARNING_NO_ROLE": "Dikkat edin, projenizdeki rollerden hiçbiri kullanıcı hikayeleri için puan değeri kestirimi yapma yetkisine sahip değil.", @@ -588,6 +678,10 @@ "SECTION_NAME": "Github", "PAGE_TITLE": "Github - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks - {{projectName}}", "SECTION_NAME": "Webhooks", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "davetiye {{email}} " }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "Puan seçici için varsayılan değer", - "LABEL_US": "KH durum seçici için varsayılan değer", "LABEL_TASK_STATUS": "Görev durum seçici için varsayılan değer", - "LABEL_PRIORITY": "Önceli seçicisi için varsayılan değer", - "LABEL_SEVERITY": "Önem derecesi seçicisi için varsayılan değer", "LABEL_ISSUE_TYPE": "Sorun tipi seçici için varsayılan değer", - "LABEL_ISSUE_STATUS": "Sorun durumu seçici için varsayılan değer" + "LABEL_ISSUE_STATUS": "Sorun durumu seçici için varsayılan değer", + "LABEL_PRIORITY": "Önceli seçicisi için varsayılan değer", + "LABEL_SEVERITY": "Önem derecesi seçicisi için varsayılan değer" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "Yeni durum için bir isim yaz" @@ -681,7 +776,8 @@ "PRIORITIES": "Öncelikler", "SEVERITIES": "Önem Dereceleri", "TYPES": "Tipler", - "CUSTOM_FIELDS": "Özel alanlar" + "CUSTOM_FIELDS": "Özel alanlar", + "TAGS": "Etiketler " }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "Proje Profili" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "Hepsini göster", "FILTER_TYPE_PROJECTS": "Projeler", "FILTER_TYPE_PROJECT_TITLES": "Sadece projeleri görüntüle", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "Hikayeler", "FILTER_TYPE_USER_STORIES_TITLES": "Sadece kullanıcı hikayelerini göster", "FILTER_TYPE_TASKS": "Görevler", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(Opsiyonel) Davetinize kişiselleştirilmiş bir metin ekleyin. Yeni üyelerinize tatlı bir şeyler söyleyin ;-)", "PLACEHOLDER_TYPE_EMAIL": "Bir e-posta girin", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "Yeni kullanıcı hikayesi", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "Konu", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - Kullanıcı Hikayesi {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Durum: {{userStoryStatus }}. Tamamlanan {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Puanlar: {{userStoryPoints}}. Tanım: {{userStoryDescription}}", - "SECTION_NAME": "Kullanıcı hikayesi detayları", + "SECTION_NAME": "Kullanıcı hikayesi", "LINK_TASKBOARD": "Görev Panosu", "TITLE_LINK_TASKBOARD": "Görev panosuna git", "TOTAL_POINTS": "toplam puanlar", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "Bu KH 'ni oluşturulduğu", "GO_TO_EXTERNAL_REFERENCE": "Kökenine git ", "BLOCKED": "Bu kullanıcı hikayesi engelli", - "PREVIOUS": "önceki kullanıcı hikayesi", - "NEXT": "sonraki kullanıcı hikayesi", "TITLE_DELETE_ACTION": "Kullanıcı Hikayesi Sil", "LIGHTBOX_TITLE_BLOKING_US": "Bizi engelleyen", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tamamlanan görevler", "ASSIGN": "Kullanıcı Hikayesini Ata", "NOT_ESTIMATED": "Kestirim yapılmamış", "TOTAL_US_POINTS": "Toplam KH puanları", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "Takım Gereksinimi", "CLIENT_REQUIREMENT": "İstemci Gereksinimi", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "Yorum {{date}} tarihinde {{user}} tarafından silindi", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "Yorumlar", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "Yorum Yap", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "Buraya yeni bir yorum yazın", "SHOW_DELETED": "Silinmiş yorumları göster", "HIDE_DELETED": "Silinmiş yorumu gizle", "DELETE": "Yorumu sil", - "RESTORE": "Yorumu geri yükle" + "RESTORE": "Yorumu geri yükle", + "HISTORY": { + "TITLE": "Aktivite" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "Eylemleri göster", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ Önceki girdileri göster ({{showMore}} daha fazla)", "TITLE": "Aktivite", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "silindi", "ADDED": "eklendi", - "US_POINTS": "KH puanları ({{name}})", - "NEW_ATTACHMENT": "yeni ek", - "DELETED_ATTACHMENT": "ek sil", - "UPDATED_ATTACHMENT": "güncellenmiş ek {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "silinmiş özel öznitelik", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "Yapılan {size, plural, one{tek değişiklik} other{# değişiklikler}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "Takım Gereksinimi", + "CLIENT_REQUIREMENT": "İstemci Gereksinimi", + "BLOCKED": "Engelli", "VALUES": { "YES": "evet", "NO": "hayır", @@ -1052,12 +1198,14 @@ "TAGS": "etiketler", "ATTACHMENTS": "ekler", "IS_DEPRECATED": "kaldırıldı", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "sıra", "BACKLOG_ORDER": "havuz sıralaması", "SPRINT_ORDER": "koşu sırası", "KANBAN_ORDER": "kanban sırası", "TASKBOARD_ORDER": "Görev panosu sırası", - "US_ORDER": "kh sırası" + "US_ORDER": "kh sırası", + "COLOR": "renk" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "kapatılmış
görevler", "IOCAINE_DOSES": "baldıran zehri
dozu", "SHOW_STATISTICS_TITLE": "İstatistikleri göster", - "TOGGLE_BAKLOG_GRAPH": "Eritme grafiğini göster/gizle" + "TOGGLE_BAKLOG_GRAPH": "Eritme grafiğini göster/gizle", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "proje
puanları", @@ -1122,9 +1271,7 @@ "TITLE": "Filtreler", "REMOVE": "Filtreleri Sil", "HIDE": "Filtreleri Gizle", - "SHOW": "Filtreleri Göster", - "FILTER_CATEGORY_STATUS": "Durum", - "FILTER_CATEGORY_TAGS": "Etiketler " + "SHOW": "Filtreleri Göster" }, "SPRINTS": { "TITLE": "KOŞULAR", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - Görev {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "Durum: {{taskStatus }}. Tanım: {{taskDescription}}", - "SECTION_NAME": "Görev detayları", + "SECTION_NAME": "Görev", "LINK_TASKBOARD": "Görev Panosu", "TITLE_LINK_TASKBOARD": "Görev panosuna git", "PLACEHOLDER_SUBJECT": "Yeni görev konusunu gir", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "Bu görevin oluşturulduğu", "TITLE_LINK_GO_ORIGIN": "Kullanıcı hikayesine git", "BLOCKED": "Bu iş engelli", - "PREVIOUS": "önceki görev", - "NEXT": "sonraki görev", "TITLE_DELETE_ACTION": "Görev Sil", "LIGHTBOX_TITLE_BLOKING_TASK": "Engelleyen iş", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "Sorunlar - {{projectName}}", "PAGE_DESCRIPTION": "{{projectName}} projesinin sorun listesi paneli: {{projectDescription}}", "LIST_SECTION_NAME": "Sorunlar", - "SECTION_NAME": "Sorun detayları", + "SECTION_NAME": "Sorun", "ACTION_NEW_ISSUE": "+ YENİ SORUN", "ACTION_PROMOTE_TO_US": "Kullanıcı Hikayesine Terfi Ettir", - "PLACEHOLDER_FILTER_NAME": "Filtre adı yazın ve enter a basın", "PROMOTED": "Bu sorun, kullanıcı hikayesine yükseltildi:", "EXTERNAL_REFERENCE": "Bu talebin oluşturulduğu ", "GO_TO_EXTERNAL_REFERENCE": "Kökenine git", "BLOCKED": "Bu sorun engelli", - "TITLE_PREVIOUS_ISSUE": "önceki sorun", - "TITLE_NEXT_ISSUE": "sonraki sorun", "ACTION_DELETE": "Sorun sil", "LIGHTBOX_TITLE_BLOKING_ISSUE": "Engelleyen sorun", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "Bu talebi yeni bir kullanıcı hikayesi olacak şekilde terfi ettirin", "MESSAGE": "Bu sorundan yeni bir hikaye oluşturmak istediğinize emin misiniz?" }, - "FILTERS": { - "TITLE": "Filtreler", - "INPUT_SEARCH_PLACEHOLDER": "Konu ya da ref", - "TITLE_ACTION_SEARCH": "Ara", - "ACTION_SAVE_CUSTOM_FILTER": "özel filtre olarak kaydet", - "BREADCRUMB": "Filtreler", - "TITLE_BREADCRUMB": "Filtreler", - "CATEGORIES": { - "TYPE": "Tip", - "STATUS": "Durum", - "SEVERITY": "Önem Derecesi", - "PRIORITIES": "Öncelikler", - "TAGS": "Etiketler", - "ASSIGNED_TO": "Atanmış", - "CREATED_BY": "Oluşturan", - "CUSTOM_FILTERS": "Özel filtreler" - }, - "CONFIRM_DELETE": { - "TITLE": "Özel filtre sil", - "MESSAGE": "'{{customFilterName}}' özel filtresi" - } - }, "TABLE": { "COLUMNS": { "TYPE": "Tip", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "Ara - {{projectName}}", "PAGE_DESCRIPTION": "Projedeki hikayeleri, sorunları, işleri, viki sayfalarını ya da herhangi bir şeyi arayın {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "Kullanıcı Hikayeleri", "FILTER_ISSUES": "Sorunlar ", "FILTER_TASKS": "Görevler", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "Wiki Sayfası Sil", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "Bağlantılar", - "ACTION_ADD_LINK": "Bağlantı ekle" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "kere
düzenlendi", "LAST_EDIT": "son
düzenleme", "LAST_MODIFICATION": "son düzenleme" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "Oluşturuldu", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": " {{project_name}} projesinde yer alan {{us_name}} adlı KH ya ait yeni bir görev {{obj_name}}, {{username}} tarafından oluşturuldu", "WIKI_CREATED": "{{project_name}} projesindeki yeni wiki sayfası {{obj_name}}, {{username}} tarafından oluşturuldu", "MILESTONE_CREATED": "{{project_name}} projesinde {{obj_name}} koşusu, {{username}} tarafından oluşturuldu", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{project_name}} proje {{username}} tarafından oluşturuldu", "MILESTONE_UPDATED": " {{obj_name}} koşusu {{username}} tarafından güncellendi", "US_UPDATED": "{{username}} kullanıcısı, {{obj_name}} KH 'sinin \"{{field_name}}\" alanını güncelledi. ", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{us_name}} adlı KH'ye ait {{obj_name}} talebinin \"{{field_name}}\" özniteliği {{username}} tarafından güncellendi. ", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}}, {{us_name}} hikayesindeki {{obj_name}} işinin {{field_name}} özelliğini {{new_value}} olacak şekilde değiştirdi", "WIKI_UPDATED": "{{obj_name}} adlı wiki sayfası {{username}} tarafından güncellendi", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": " {{obj_name}} KH'sine {{username}} tarafından yorum yapıldı", "NEW_COMMENT_ISSUE": " {{obj_name}} talebine {{username}} tarafından yorum yapıldı", "NEW_COMMENT_TASK": " {{obj_name}} görevine {{username}} tarafından yorum yapıldı", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} projesi yeni bir üyeye sahip", "US_ADDED_MILESTONE": " {{username}}, {{sprint_name}} koşusuna {{obj_name}} hikayesini ekledi", "US_MOVED": "{{username}}, {{obj_name}} hikayesini taşıdı", diff --git a/app/locales/taiga/locale-zh-hant.json b/app/locales/taiga/locale-zh-hant.json index 2abcfee7..6af41fbe 100644 --- a/app/locales/taiga/locale-zh-hant.json +++ b/app/locales/taiga/locale-zh-hant.json @@ -35,6 +35,8 @@ "ONE_ITEM_LINE": "一行一物 ", "NEW_BULK": "新批次插入", "RELATED_TASKS": "相關任務 ", + "PREVIOUS": "Previous", + "NEXT": "下一個", "LOGOUT": "登出", "EXTERNAL_USER": "外部使用者", "GENERIC_ERROR": "我們的系統指出{{error}}.", @@ -45,6 +47,11 @@ "CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.", "CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?", "CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost", + "RELATED_USERSTORIES": "Related user stories", + "CARD": { + "ASSIGN_TO": "Assign To", + "EDIT": "Edit card" + }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "該數值似乎為無效", "TYPE_EMAIL": "該電子郵件應為有效地址", @@ -115,8 +122,9 @@ "USER_STORY": "使用者故事", "TASK": "任務", "ISSUE": "問題", + "EPIC": "Epic", "TAGS": { - "PLACEHOLDER": "我在這裏,請標注我", + "PLACEHOLDER": "Enter tag", "DELETE": "刪除Tag", "ADD": "新增標籤" }, @@ -196,9 +204,26 @@ "TITLE": "過濾器", "INPUT_PLACEHOLDER": "主旨或參考", "TITLE_ACTION_FILTER_BUTTON": "搜尋", - "BREADCRUMB_TITLE": "回到類別", - "BREADCRUMB_FILTERS": "過濾器", - "BREADCRUMB_STATUS": "狀態" + "INPUT_SEARCH_PLACEHOLDER": "主旨或參考", + "TITLE_ACTION_SEARCH": "搜尋", + "ACTION_SAVE_CUSTOM_FILTER": "儲存為客製過濾器 ", + "PLACEHOLDER_FILTER_NAME": "寫入過濾器名稱後按下enter ", + "APPLIED_FILTERS_NUM": "filters applied", + "CATEGORIES": { + "TYPE": "類型", + "STATUS": "狀態", + "SEVERITY": "急迫性", + "PRIORITIES": "優先性", + "TAGS": "標籤", + "ASSIGNED_TO": "指派給 ", + "CREATED_BY": "由創建", + "CUSTOM_FILTERS": "客製過濾器 ", + "EPIC": "Epic" + }, + "CONFIRM_DELETE": { + "TITLE": "刪除客製過濾器 ", + "MESSAGE": "預設過濾器 '{{customFilterName}}'" + } }, "WYSIWYG": { "H1_BUTTON": "第一層標頭 ", @@ -228,9 +253,18 @@ "PREVIEW_BUTTON": "預視 ", "EDIT_BUTTON": "編輯", "ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.", + "ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.", "MARKDOWN_HELP": "Markdown 語法協助" }, "PERMISIONS_CATEGORIES": { + "EPICS": { + "NAME": "Epics", + "VIEW_EPICS": "View epics", + "ADD_EPICS": "Add epics", + "MODIFY_EPICS": "Modify epics", + "COMMENT_EPICS": "Comment epics", + "DELETE_EPICS": "Delete epics" + }, "SPRINTS": { "NAME": "衝刺任務", "VIEW_SPRINTS": "檢視衝刺任務 ", @@ -243,6 +277,7 @@ "VIEW_USER_STORIES": "檢視使用者故事", "ADD_USER_STORIES": "新增使用者故事", "MODIFY_USER_STORIES": "修正使用者故事", + "COMMENT_USER_STORIES": "Comment user stories", "DELETE_USER_STORIES": "刪除使用者故事" }, "TASKS": { @@ -250,6 +285,7 @@ "VIEW_TASKS": "檢視任務 ", "ADD_TASKS": "新增任務 ", "MODIFY_TASKS": "修正任務 ", + "COMMENT_TASKS": "Comment tasks", "DELETE_TASKS": "刪除任務" }, "ISSUES": { @@ -257,6 +293,7 @@ "VIEW_ISSUES": "檢視問題 ", "ADD_ISSUES": "新增問題 ", "MODIFY_ISSUES": "修正議題 ", + "COMMENT_ISSUES": "Comment issues", "DELETE_ISSUES": "刪除問題 " }, "WIKI": { @@ -366,6 +403,41 @@ "WATCHING_SECTION": "觀看中", "DASHBOARD": "專案控制台" }, + "EPICS": { + "TITLE": "EPICS", + "SECTION_NAME": "Epics", + "EPIC": "EPIC", + "PAGE_TITLE": "Epics - {{projectName}}", + "PAGE_DESCRIPTION": "The epics list of the project {{projectName}}: {{projectDescription}}", + "DASHBOARD": { + "ADD": "+ ADD EPIC", + "UNASSIGNED": "未指派" + }, + "EMPTY": { + "TITLE": "It looks like there aren't any epics yet", + "EXPLANATION": "Epics are items at a higher level that encompass user stories.
Epics are at the top of the hierarchy and can be used to group user stories together.", + "HELP": "Learn more about epics" + }, + "TABLE": { + "VOTES": "投票數", + "NAME": "名稱 ", + "PROJECT": "專案", + "SPRINT": "衝刺任務", + "ASSIGNED_TO": "Assigned", + "STATUS": "狀態", + "PROGRESS": "Progress", + "VIEW_OPTIONS": "View options" + }, + "CREATE": { + "TITLE": "New Epic", + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this epic", + "TEAM_REQUIREMENT": "Team requirement", + "CLIENT_REQUIREMENT": "Client requirement", + "BLOCKED": "已封鎖", + "BLOCKED_NOTE_PLACEHOLDER": "Why is this epic blocked?", + "CREATE_EPIC": "Create epic" + } + }, "PROJECTS": { "PAGE_TITLE": "我的專案 - Taiga", "PAGE_DESCRIPTION": "你的專案列表,你可以記錄或創建新專案。", @@ -402,7 +474,8 @@ "ADMIN": { "COMMON": { "TITLE_ACTION_EDIT_VALUE": "編輯數值", - "TITLE_ACTION_DELETE_VALUE": "删除值" + "TITLE_ACTION_DELETE_VALUE": "删除值", + "TITLE_ACTION_DELETE_TAG": "刪除Tag" }, "HELP": "需要幫助嗎?看看我們的支援頁面吧!", "PROJECT_DEFAULT_VALUES": { @@ -435,6 +508,8 @@ "TITLE": "模組", "ENABLE": "啟用", "DISABLE": "停用", + "EPICS": "Epics", + "EPICS_DESCRIPTION": "Visualize and manage the most strategic part of your project", "BACKLOG": "待辦任務優先表", "BACKLOG_DESCRIPTION": "管理你的 User Story 讓接下來的及優先的工作能被有條理地檢視 ", "NUMBER_SPRINTS": "Expected number of sprints", @@ -497,6 +572,7 @@ "REGENERATE_SUBTITLE": "你將要改變CSV資料的連結網址,之前的網址將失效。你確定要這樣做嗎?" }, "CSV": { + "SECTION_TITLE_EPIC": "epics reports", "SECTION_TITLE_US": "使用者故事報告", "SECTION_TITLE_TASK": "任務報告", "SECTION_TITLE_ISSUE": "問題報告", @@ -509,6 +585,8 @@ "CUSTOM_FIELDS": { "TITLE": "客製化欄位", "SUBTITLE": "指定使用者故事,任務與問題一些客製化欄位", + "EPIC_DESCRIPTION": "Epics custom fields", + "EPIC_ADD": "Add a custom field in epics", "US_DESCRIPTION": "使用者客製欄位", "US_ADD": "在使用者故事中加入客制欄位", "TASK_DESCRIPTION": "任務客製化欄位", @@ -546,7 +624,8 @@ "PROJECT_VALUES_STATUS": { "TITLE": "狀態", "SUBTITLE": "指明你的使用者故事狀態,任務以及經歷問題 ", - "US_TITLE": "使用者故事狀態", + "EPIC_TITLE": "Epic Statuses", + "US_TITLE": "User Story Statuses", "TASK_TITLE": "任務狀態", "ISSUE_TITLE": "問題狀態" }, @@ -556,6 +635,17 @@ "ISSUE_TITLE": "問題類型", "ACTION_ADD": "新增{{objName}}" }, + "PROJECT_VALUES_TAGS": { + "TITLE": "標籤", + "SUBTITLE": "View and edit the color of your tags", + "EMPTY": "Currently there are no tags", + "EMPTY_SEARCH": "It looks like nothing was found with your search criteria", + "ACTION_ADD": "新增標籤", + "NEW_TAG": "New tag", + "MIXING_HELP_TEXT": "Select the tags that you want to merge", + "MIXING_MERGE": "Merge Tags", + "SELECTED": "Selected" + }, "ROLES": { "PAGE_TITLE": "角色- {{projectName}}", "WARNING_NO_ROLE": "注意,你的專案中無角色可以評估使用者故事的點數", @@ -588,6 +678,10 @@ "SECTION_NAME": "Githun", "PAGE_TITLE": "Gitlab - {{projectName}}" }, + "GOGS": { + "SECTION_NAME": "Gogs", + "PAGE_TITLE": "Gogs - {{projectName}}" + }, "WEBHOOKS": { "PAGE_TITLE": "Webhooks- {{projectName}}", "SECTION_NAME": "網頁觸發 ", @@ -643,13 +737,14 @@ "DEFAULT_DELETE_MESSAGE": "邀請 {{email}}" }, "DEFAULT_VALUES": { + "LABEL_EPIC_STATUS": "Default value for epic status selector", + "LABEL_US_STATUS": "Default value for user story status selector", "LABEL_POINTS": "點數選擇器預設值", - "LABEL_US": "使用者故事狀態選擇器預設值", "LABEL_TASK_STATUS": "任務狀態選擇器預設值", - "LABEL_PRIORITY": "優先選擇器預設值", - "LABEL_SEVERITY": "急迫性選擇器預設值", "LABEL_ISSUE_TYPE": "問題類型選擇器預設值", - "LABEL_ISSUE_STATUS": "問題狀態選擇器預設值" + "LABEL_ISSUE_STATUS": "問題狀態選擇器預設值", + "LABEL_PRIORITY": "優先選擇器預設值", + "LABEL_SEVERITY": "急迫性選擇器預設值" }, "STATUS": { "PLACEHOLDER_WRITE_STATUS_NAME": "為此新狀態命名" @@ -681,7 +776,8 @@ "PRIORITIES": "優先性", "SEVERITIES": "急迫性", "TYPES": "類型", - "CUSTOM_FIELDS": "客製化欄位" + "CUSTOM_FIELDS": "客製化欄位", + "TAGS": "標籤" }, "SUBMENU_PROJECT_PROFILE": { "TITLE": "專案檔案" @@ -751,6 +847,8 @@ "FILTER_TYPE_ALL_TITLE": "顯示全部", "FILTER_TYPE_PROJECTS": "專案", "FILTER_TYPE_PROJECT_TITLES": "只顯示專案", + "FILTER_TYPE_EPICS": "Epics", + "FILTER_TYPE_EPIC_TITLES": "Show only epics", "FILTER_TYPE_USER_STORIES": "故事", "FILTER_TYPE_USER_STORIES_TITLES": "只顯視使用者故事", "FILTER_TYPE_TASKS": "任務 ", @@ -950,8 +1048,8 @@ "CREATE_MEMBER": { "PLACEHOLDER_INVITATION_TEXT": "(非必要) 加上一段私人文字在邀請信,告訴你的新成員一些好事 ;-)", "PLACEHOLDER_TYPE_EMAIL": "輸入一個電郵地址", - "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "Unfortunately, this project can't have more than {{maxMembers}} members.
If you would like to increase the current limit, please contact the administrator.", - "LIMIT_USERS_WARNING_MESSAGE": "Unfortunately, this project can't have more than {{maxMembers}} members." + "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members. If you would like to increase the current limit, please contact the administrator.", + "LIMIT_USERS_WARNING_MESSAGE": "You are about to reach the maximum number of members allowed for this project, {{maxMembers}} members." }, "LEAVE_PROJECT_WARNING": { "TITLE": "Unfortunately, this project can't be left without an owner", @@ -970,10 +1068,30 @@ "BUTTON": "Ask this project member to become the new project owner" } }, + "EPIC": { + "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}", + "SECTION_NAME": "Epic", + "TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY": "Unlink related userstory", + "MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY": "It will delete the link to the related userstory '{{subject}}'", + "ERROR_UNLINK_RELATED_USERSTORY": "We have not been able to unlink: {{errorMessage}}", + "CREATE_RELATED_USERSTORIES": "Create a relationship with", + "NEW_USERSTORY": "新使用者故事", + "EXISTING_USERSTORY": "Existing user story", + "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "SUBJECT": "主旨", + "SUBJECT_BULK_MODE": "Subject (bulk insert)", + "CHOOSE_PROJECT_FROM": "What's the project?", + "CHOOSE_USERSTORY": "What's the user story?", + "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", + "FILTER_USERSTORIES": "Filter user stories", + "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic", + "ACTION_DELETE": "Delete epic" + }, "US": { "PAGE_TITLE": "{{userStorySubject}} - 使用者故事 {{userStoryRef}} - {{projectName}}", "PAGE_DESCRIPTION": "狀態: {{userStoryStatus }}.已完成 {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). 點數: {{userStoryPoints}}. 描述: {{userStoryDescription}}", - "SECTION_NAME": "使用者故事細節", + "SECTION_NAME": "使用者故事", "LINK_TASKBOARD": "任務板", "TITLE_LINK_TASKBOARD": "到任務板去", "TOTAL_POINTS": "所有點數", @@ -984,14 +1102,23 @@ "EXTERNAL_REFERENCE": "此使用者故事創造者是", "GO_TO_EXTERNAL_REFERENCE": "回到一開始", "BLOCKED": "這個使用者故事已被封鎖", - "PREVIOUS": "之前的使用者故事", - "NEXT": "下一個使用者故事", "TITLE_DELETE_ACTION": "刪除使用者故事", "LIGHTBOX_TITLE_BLOKING_US": "封鎖中的使用者故事", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} 任務完成", "ASSIGN": "指派使用者故事", "NOT_ESTIMATED": "無預估", "TOTAL_US_POINTS": "全部使用者故事點數", + "TRIBE": { + "PUBLISH": "Publish as Gig in Taiga Tribe", + "PUBLISH_INFO": "More info", + "PUBLISH_TITLE": "More info on publishing in Taiga Tribe", + "PUBLISHED_AS_GIG": "Story published as Gig in Taiga Tribe", + "EDIT_LINK": "Edit link", + "CLOSE": "Close", + "SYNCHRONIZE_LINK": "synchronize with Taiga Tribe", + "PUBLISH_MORE_INFO_TITLE": "Do you need somebody for this task?", + "PUBLISH_MORE_INFO_TEXT": "

If you need help with a particular piece of work you can easily create gigs on Taiga Tribe and receive help from all over the world. You will be able to control and manage the gig enjoying a great community eager to contribute.

TaigaTribe was born as a Taiga sibling. Both platforms can live separately but we believe that there is much power in using them combined so we are making sure the integration works like a charm.

" + }, "FIELDS": { "TEAM_REQUIREMENT": "團隊要求", "CLIENT_REQUIREMENT": "客戶要求", @@ -999,28 +1126,47 @@ } }, "COMMENTS": { - "DELETED_INFO": "評論被 {{user}} 於 {{date}}刪除 ", + "DELETED_INFO": "Comment deleted by {{user}}", "TITLE": "評論", + "COMMENTS_COUNT": "{{comments}} Comments", + "ORDER": "Order", + "OLDER_FIRST": "Older first", + "RECENT_FIRST": "Recent first", "COMMENT": "評論", + "EDIT_COMMENT": "Edit comment", + "EDITED_COMMENT": "Edited:", + "SHOW_HISTORY": "View historic", "TYPE_NEW_COMMENT": "在此輸入一個新的評論", "SHOW_DELETED": "顯示遭刪除的評論 ", "HIDE_DELETED": "隱藏已刪除之評論 ", "DELETE": "刪除評論 ", - "RESTORE": "恢復原評論 " + "RESTORE": "恢復原評論 ", + "HISTORY": { + "TITLE": "動態" + } }, "ACTIVITY": { "SHOW_ACTIVITY": "顯示動態", "DATETIME": "DD MMM YYYY HH:mm", "SHOW_MORE": "+ 顯示過去條目 ({{showMore}} more)", "TITLE": "動態", + "ACTIVITIES_COUNT": "{{activities}} Activities", "REMOVED": "已移除", "ADDED": "已加入", - "US_POINTS": "使用者故事點數({{name}})", - "NEW_ATTACHMENT": "新附件", - "DELETED_ATTACHMENT": "已刪除附件", - "UPDATED_ATTACHMENT": "更新附件 {{filename}}", - "DELETED_CUSTOM_ATTRIBUTE": "刪除客製屬性", + "TAGS_ADDED": "tags added:", + "TAGS_REMOVED": "tags removed:", + "US_POINTS": "{{role}} points", + "NEW_ATTACHMENT": "new attachment:", + "DELETED_ATTACHMENT": "deleted attachment:", + "UPDATED_ATTACHMENT": "updated attachment ({{filename}}):", + "CREATED_CUSTOM_ATTRIBUTE": "created custom attribute", + "UPDATED_CUSTOM_ATTRIBUTE": "updated custom attribute", "SIZE_CHANGE": "使 {size, plural, one{更改} other{變化}}", + "BECAME_DEPRECATED": "became deprecated", + "BECAME_UNDEPRECATED": "became undeprecated", + "TEAM_REQUIREMENT": "團隊要求", + "CLIENT_REQUIREMENT": "客戶要求", + "BLOCKED": "已封鎖", "VALUES": { "YES": "yes", "NO": "no", @@ -1052,12 +1198,14 @@ "TAGS": "標籤", "ATTACHMENTS": "附件", "IS_DEPRECATED": "被棄用", + "IS_NOT_DEPRECATED": "is not deprecated", "ORDER": "次序", "BACKLOG_ORDER": "待辦任務先後次序", "SPRINT_ORDER": "衝刺任務次序", "KANBAN_ORDER": "kanban看板次序", "TASKBOARD_ORDER": "任務板次序", - "US_ORDER": "使用者故事次序" + "US_ORDER": "使用者故事次序", + "COLOR": "顏色" } }, "BACKLOG": { @@ -1109,7 +1257,8 @@ "CLOSED_TASKS": "已關閉
任務", "IOCAINE_DOSES": "毒物(全新任務挑戰)
劑量", "SHOW_STATISTICS_TITLE": "顯示統計", - "TOGGLE_BAKLOG_GRAPH": "顯示/隱藏 剩餘工作量圖" + "TOGGLE_BAKLOG_GRAPH": "顯示/隱藏 剩餘工作量圖", + "POINTS_PER_ROLE": "Points per role" }, "SUMMARY": { "PROJECT_POINTS": "專案
點數", @@ -1122,9 +1271,7 @@ "TITLE": "過濾器", "REMOVE": "移除過濾器", "HIDE": "隱藏過濾器", - "SHOW": "顯示過濾器", - "FILTER_CATEGORY_STATUS": "狀態", - "FILTER_CATEGORY_TAGS": "標籤" + "SHOW": "顯示過濾器" }, "SPRINTS": { "TITLE": "衝刺任務", @@ -1179,7 +1326,7 @@ "TASK": { "PAGE_TITLE": "{{taskSubject}} - 任務 {{taskRef}} - {{projectName}}", "PAGE_DESCRIPTION": "狀態: {{taskStatus }}.描述: {{taskDescription}}", - "SECTION_NAME": "任務細節", + "SECTION_NAME": "任務", "LINK_TASKBOARD": "任務板", "TITLE_LINK_TASKBOARD": "到任務板去", "PLACEHOLDER_SUBJECT": "鍵入新任務主旨", @@ -1189,8 +1336,6 @@ "ORIGIN_US": "此任務創造者是", "TITLE_LINK_GO_ORIGIN": "到使用者故事", "BLOCKED": "這任務已被封鎖", - "PREVIOUS": "之前的任務 ", - "NEXT": "下一個任務 ", "TITLE_DELETE_ACTION": "刪除任務", "LIGHTBOX_TITLE_BLOKING_TASK": "封鎖中的任務 ", "FIELDS": { @@ -1228,16 +1373,13 @@ "PAGE_TITLE": "問題 - {{projectName}}", "PAGE_DESCRIPTION": " {{projectName}}的問題清單看版: {{projectDescription}}", "LIST_SECTION_NAME": "問題 ", - "SECTION_NAME": "問題細節", + "SECTION_NAME": "問題", "ACTION_NEW_ISSUE": "+ 新問題 ", "ACTION_PROMOTE_TO_US": "提昇到使用者故事", - "PLACEHOLDER_FILTER_NAME": "寫入過濾器名稱後按下enter ", "PROMOTED": "此問題已提昇成使用者故事 ", "EXTERNAL_REFERENCE": "此問題的提供者是", "GO_TO_EXTERNAL_REFERENCE": "回到一開始", "BLOCKED": "這個議題已被封鎖", - "TITLE_PREVIOUS_ISSUE": "之前的問題 ", - "TITLE_NEXT_ISSUE": "下一個問題 ", "ACTION_DELETE": "删除議題 ", "LIGHTBOX_TITLE_BLOKING_ISSUE": "封鎖中的問題", "FIELDS": { @@ -1249,28 +1391,6 @@ "TITLE": "將此問題提到使用者故事", "MESSAGE": "你確定此問題要創建一個新的使用者故事?" }, - "FILTERS": { - "TITLE": "過濾器", - "INPUT_SEARCH_PLACEHOLDER": "主旨或參考", - "TITLE_ACTION_SEARCH": "搜尋", - "ACTION_SAVE_CUSTOM_FILTER": "儲存為客製過濾器 ", - "BREADCRUMB": "過濾器", - "TITLE_BREADCRUMB": "過濾器", - "CATEGORIES": { - "TYPE": "類型", - "STATUS": "狀態", - "SEVERITY": "急迫性", - "PRIORITIES": "優先性", - "TAGS": "標籤", - "ASSIGNED_TO": "指派給 ", - "CREATED_BY": "由創建", - "CUSTOM_FILTERS": "客製過濾器 " - }, - "CONFIRM_DELETE": { - "TITLE": "刪除客製過濾器 ", - "MESSAGE": "預設過濾器 '{{customFilterName}}'" - } - }, "TABLE": { "COLUMNS": { "TYPE": "類型", @@ -1316,6 +1436,7 @@ "SEARCH": { "PAGE_TITLE": "搜尋 - {{projectName}}", "PAGE_DESCRIPTION": "專案搜尋(使用者故事, 問題, 任務或維基頁等資訊) {{projectName}}: {{projectDescription}}", + "FILTER_EPICS": "Epics", "FILTER_USER_STORIES": "使用者故事", "FILTER_ISSUES": "問題 ", "FILTER_TASKS": "任務 ", @@ -1417,13 +1538,24 @@ "DELETE_LIGHTBOX_TITLE": "刪除維基頁", "DELETE_LINK_TITLE": "Delete Wiki link", "NAVIGATION": { - "SECTION_NAME": "連結", - "ACTION_ADD_LINK": "新增連結" + "HOME": "Main Page", + "SECTION_NAME": "BOOKMARKS", + "ACTION_ADD_LINK": "Add bookmark", + "ALL_PAGES": "All wiki pages" }, "SUMMARY": { "TIMES_EDITED": "次數
編輯 ", "LAST_EDIT": "上次
編輯 ", "LAST_MODIFICATION": "上回修改" + }, + "SECTION_PAGES_LIST": "All pages", + "PAGES_LIST_COLUMNS": { + "TITLE": "Title", + "EDITIONS": "Editions", + "CREATED": "已創建", + "MODIFIED": "Modified", + "CREATOR": "Creator", + "LAST_MODIFIER": "Last modifier" } }, "HINTS": { @@ -1447,6 +1579,8 @@ "TASK_CREATED_WITH_US": "{{username}} 創建新任務 {{obj_name}} 於 {{project_name}} ,其為{{us_name}}之使用者故事", "WIKI_CREATED": "{{username}} 創建新維基頁 {{obj_name}} 於 {{project_name}}", "MILESTONE_CREATED": "{{username}} 創建新衝刺任務 {{obj_name}} 於 {{project_name}}", + "EPIC_CREATED": "{{username}} has created a new epic {{obj_name}} in {{project_name}}", + "EPIC_RELATED_USERSTORY_CREATED": "{{username}} has related the userstory {{related_us_name}} to the epic {{epic_name}} in {{project_name}}", "NEW_PROJECT": "{{username}} 創建專案 {{project_name}}", "MILESTONE_UPDATED": "{{username}}更新衝刺任務 {{obj_name}} ", "US_UPDATED": "{{username}} 已更新 {{obj_name}}使用者故事之 \"{{field_name}}\"屬性。", @@ -1459,9 +1593,13 @@ "TASK_UPDATED_WITH_US": "{{username}} 更新了 {{obj_name}} 任務之\"{{field_name}}\" 屬性,其為 {{us_name}} 之使用者故事", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} 已更新了 {{obj_name}} 下的 {{us_name}} 使用者故事\"{{field_name}}\"屬性到{{new_value}}", "WIKI_UPDATED": "\n{{username}} 更新了維基頁{{obj_name}}", + "EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", + "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}} to {{new_value}}", + "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} has updated the \"{{field_name}}\" of the epic {{obj_name}} to ", "NEW_COMMENT_US": "{{username}} 評論了 {{obj_name}}使用者故事", "NEW_COMMENT_ISSUE": "{{username}}評論了此問題 {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} 評論了此任務{{obj_name}}", + "NEW_COMMENT_EPIC": "{{username}} has commented in the epic {{obj_name}}", "NEW_MEMBER": "{{project_name}} 有新成員", "US_ADDED_MILESTONE": "{{username}} 增加使用者故事 {{obj_name}} 給 {{sprint_name}}", "US_MOVED": "{{username}} 搬移了使用者故事 {{obj_name}}", diff --git a/app/modules/attachments/attachments.scss b/app/modules/attachments/attachments.scss index 16e465cf..fc1bfac8 100644 --- a/app/modules/attachments/attachments.scss +++ b/app/modules/attachments/attachments.scss @@ -20,7 +20,7 @@ .attachments-header { align-content: center; align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; min-height: 36px; @@ -127,6 +127,26 @@ } .attachment-preview { + .attachment-preview-container { + svg { + @include svg-size(3rem); + fill: $gray-light; + &:hover { + fill: $primary-light; + transition: fill .3s linear; + } + } + } + .previous { + left: 3rem; + position: absolute; + top: calc(50% - 3rem); + } + .next { + position: absolute; + right: 3rem; + top: calc(50% - 3rem); + } img { max-height: 80vh; max-width: 80vw; diff --git a/app/modules/components/assigned-to/assigned-item/assigned-item.directive.coffee b/app/modules/components/assigned-to/assigned-item/assigned-item.directive.coffee new file mode 100644 index 00000000..709ba6cc --- /dev/null +++ b/app/modules/components/assigned-to/assigned-item/assigned-item.directive.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: assigned-to-selector.directive.coffee +### + +AssignedItemDirective = () -> + + link = (scope, el, attrs) -> + + return { + templateUrl: "components/assigned-to/assigned-item/assigned-item.html", + scope: { + member: "=" + }, + link: link + } + +AssignedItemDirective.$inject = [] + +angular.module("taigaComponents").directive("tgAssignedItem", AssignedItemDirective) diff --git a/app/modules/components/assigned-to/assigned-item/assigned-item.jade b/app/modules/components/assigned-to/assigned-item/assigned-item.jade new file mode 100644 index 00000000..b0c06515 --- /dev/null +++ b/app/modules/components/assigned-to/assigned-item/assigned-item.jade @@ -0,0 +1,3 @@ +.assignable-member-single + img.assignable-member-avatar(tg-avatar="member") + .assignable-member-name {{member.full_name}} diff --git a/app/modules/components/assigned-to/assigned-item/assigned-item.scss b/app/modules/components/assigned-to/assigned-item/assigned-item.scss new file mode 100644 index 00000000..132d34aa --- /dev/null +++ b/app/modules/components/assigned-to/assigned-item/assigned-item.scss @@ -0,0 +1,22 @@ +.assignable-member-single { + align-items: center; + display: flex; + padding: .25rem 0; + .assigned-members-option & { + background: $white; + border-bottom: 1px solid $whitish; + cursor: pointer; + } + &:hover { + background: rgba($primary-light, .05); + } + .assignable-member-avatar { + flex-basis: 3rem; + margin-right: .5rem; + max-height: 3rem; + max-width: 3rem; + } + .assignable-member-name { + flex: 1; + } +} diff --git a/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.controller.coffee b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.controller.coffee new file mode 100644 index 00000000..4e70615b --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.controller.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: assigned-to-selector.controller.coffee +### + +class AssignedToSelectorController + @.$inject = [] + + constructor: () -> + if @.assigned + @._getAssignedMember() + @._filterAssignedMember() + + _getAssignedMember: () -> + @.assignedMember = _.filter(@.project.members, (member) => + return member.id == @.assigned.get('id') + ) + + _filterAssignedMember: () -> + if @.assigned + @.nonAssignedMembers = _.filter(@.project.members, (member) => + return member.id != @.assigned.get('id') + ) + else + @.nonAssignedMembers = @.project.members + +angular.module('taigaComponents').controller('AssignedToSelectorCtrl', AssignedToSelectorController) diff --git a/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.directive.coffee b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.directive.coffee new file mode 100644 index 00000000..b840e856 --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.directive.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: assigned-to-selector.directive.coffee +### + +AssignedToSelectorDirective = () -> + + return { + controller: "AssignedToSelectorCtrl", + controllerAs: "vm", + bindToController: true, + templateUrl: "components/assigned-to/assigned-to-selector/assigned-to-selector.html", + scope: { + assigned: "=", + project: "=", + onRemoveAssigned: "&", + onAssignTo: "&" + } + } + +AssignedToSelectorDirective.$inject = [] + +angular.module("taigaComponents").directive("tgAssignedToSelector", AssignedToSelectorDirective) diff --git a/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.jade b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.jade new file mode 100644 index 00000000..435a7979 --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.jade @@ -0,0 +1,27 @@ +tg-lightbox-close + +.assigned-to-container + h2.title(translate="LIGHTBOX.ASSIGNED_TO.SELECT") + input.assign-input( + type="text" + placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}" + autofocus + ng-model="vm.assignToMember.name" + ng-model-options="{debounce: 200}" + ) + ul.assignable-member-list + li.assigned-member( + ng-repeat="member in vm.assignedMember" + ng-if="vm.assigned" + ) + tg-assigned-item(member="member") + tg-svg.unassign-epic.e2e-unassign( + svg-icon="icon-close" + svg-title-translate="COMMON.ASSIGNED_TO.REMOVE_ASSIGNED" + ng-click="vm.onRemoveAssigned()" + ) + li.e2e-assigned-to-selector(ng-repeat="member in vm.nonAssignedMembers | filter: vm.assignToMember.name | limitTo:6") + tg-assigned-item.assigned-members-option( + member="member" + ng-click="vm.onAssignTo({'member': member})" + ) diff --git a/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.scss b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.scss new file mode 100644 index 00000000..ac54aa3a --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to-selector/assigned-to-selector.scss @@ -0,0 +1,27 @@ +.assigned-to-container { + width: 600px; +} + +.assignable-member-list { + margin-top: 1rem; + .assigned-member { + align-items: center; + background: rgba($primary-light, .05); + border-bottom: 1px solid $whitish; + display: flex; + justify-content: space-between; + margin-bottom: 1rem; + } + .unassign-epic { + cursor: pointer; + margin-right: 1rem; + } + .icon { + fill: $red-light; + transition: fill .2s; + &:hover { + cursor: pointer; + fill: $red; + } + } +} diff --git a/app/modules/components/assigned-to/assigned-to.controller.coffee b/app/modules/components/assigned-to/assigned-to.controller.coffee new file mode 100644 index 00000000..dc69b30e --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to.controller.coffee @@ -0,0 +1,51 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: assigned-to.controller.coffee +### + +class AssignedToController + @.$inject = [ + "tgLightboxFactory", + "lightboxService", + ] + + constructor: (@lightboxFactory, @lightboxService) -> + @.has_permissions = _.includes(@.project.my_permissions, 'modify_epic') + + _closeAndRemoveAssigned: () -> + @lightboxService.closeAll() + @.onRemoveAssigned() + + _closeAndAssign: (member) -> + @lightboxService.closeAll() + @.onAssignTo({'member': member}) + + onSelectAssignedTo: (assigned, project) -> + @lightboxFactory.create('tg-assigned-to-selector', { + "class": "lightbox lightbox-assigned-to-selector open", + "assigned": "assigned", + "project": "project", + "on-remove-assigned": "onRemoveAssigned()" + "on-assign-to": "assignTo(member)" + }, { + "assigned": @.assignedTo, + "project": @.project, + "onRemoveAssigned": @._closeAndRemoveAssigned.bind(this), + "assignTo": @._closeAndAssign.bind(this) + }) + +angular.module('taigaComponents').controller('AssignedToCtrl', AssignedToController) diff --git a/app/modules/components/assigned-to/assigned-to.directive.coffee b/app/modules/components/assigned-to/assigned-to.directive.coffee new file mode 100644 index 00000000..a6ec47aa --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to.directive.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: assigned-to.directive.coffee +### + +AssignedToDirective = () -> + + return { + controller: "AssignedToCtrl", + controllerAs: "vm", + bindToController: true, + templateUrl: "components/assigned-to/assigned-to.html", + scope: { + assignedTo: "=", + project: "=", + onRemoveAssigned: "&", + onAssignTo: "&" + } + } + +AssignedToDirective.$inject = [] + +angular.module("taigaComponents").directive("tgAssignedToComponent", AssignedToDirective) diff --git a/app/modules/components/assigned-to/assigned-to.jade b/app/modules/components/assigned-to/assigned-to.jade new file mode 100644 index 00000000..a03af5a3 --- /dev/null +++ b/app/modules/components/assigned-to/assigned-to.jade @@ -0,0 +1,24 @@ +img.assigned-to.e2e-assigned-to-image( + ng-if="vm.assignedTo && vm.has_permissions" + tg-avatar="vm.assignedTo" + alt="{{vm.assignedTo.get('full_name_display')}}" + title="{{vm.assignedTo.get('full_name_display')}}" + ng-click="vm.onSelectAssignedTo(vm.assignedTo, vm.project)" +) +img.assigned-to.e2e-assigned-to-image( + ng-if="vm.assignedTo && !vm.has_permissions" + tg-avatar="vm.assignedTo" + alt="{{vm.assignedTo.get('full_name_display')}}" + title="{{vm.assignedTo.get('full_name_display')}}" +) +img.assigned-to.e2e-assigned-to-image( + ng-if="!vm.assignedTo && vm.has_permissions" + src="/#{v}/images/unnamed.png" + alt="{{'EPICS.DASHBOARD.UNASSIGNED' | translate}}" + ng-click="vm.onSelectAssignedTo(vm.assignedTo, vm.project)" +) +img.assigned-to.e2e-assigned-to-image( + ng-if="!vm.assignedTo && !vm.has_permissions" + src="/#{v}/images/unnamed.png" + alt="{{'EPICS.DASHBOARD.UNASSIGNED' | translate}}" +) diff --git a/app/modules/components/attachment-link/attachment-link.directive.coffee b/app/modules/components/attachment-link/attachment-link.directive.coffee index 50cd109e..d1a5d299 100644 --- a/app/modules/components/attachment-link/attachment-link.directive.coffee +++ b/app/modules/components/attachment-link/attachment-link.directive.coffee @@ -17,7 +17,7 @@ # File: attachment-link.directive.coffee ### -AttachmentLinkDirective = ($parse, lightboxFactory) -> +AttachmentLinkDirective = ($parse, attachmentsPreviewService, lightboxService) -> link = (scope, el, attrs) -> attachment = $parse(attrs.tgAttachmentLink)(scope) @@ -26,11 +26,11 @@ AttachmentLinkDirective = ($parse, lightboxFactory) -> event.preventDefault() scope.$apply -> - lightboxFactory.create('tg-lb-attachment-preview', { - class: 'lightbox lightbox-block' - }, { - file: attachment.get('file') - }) + lightboxService.open($('tg-attachments-preview')) + attachmentsPreviewService.fileId = attachment.getIn(['file', 'id']) + else if taiga.isPdf(attachment.getIn(['file', 'name'])) + event.preventDefault() + window.open(attachment.getIn(['file', 'url'])) scope.$on "$destroy", -> el.off() return { @@ -39,7 +39,8 @@ AttachmentLinkDirective = ($parse, lightboxFactory) -> AttachmentLinkDirective.$inject = [ "$parse", - "tgLightboxFactory" + "tgAttachmentsPreviewService", + "lightboxService" ] angular.module("taigaComponents").directive("tgAttachmentLink", AttachmentLinkDirective) diff --git a/app/modules/components/attachment/attachment-gallery.jade b/app/modules/components/attachment/attachment-gallery.jade index eda74610..4a131ef2 100644 --- a/app/modules/components/attachment/attachment-gallery.jade +++ b/app/modules/components/attachment/attachment-gallery.jade @@ -2,7 +2,7 @@ ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}", ng-if="vm.attachment.getIn(['file', 'id'])", ) - a.attachment-image( + a.attachment-image.e2e-attachment-link( tg-attachment-link="vm.attachment" href="{{::vm.attachment.getIn(['file', 'url'])}}" title="{{::vm.attachment.getIn(['file', 'name'])}}" diff --git a/app/modules/components/attachment/attachment.jade b/app/modules/components/attachment/attachment.jade index ec3b885b..842a9a7f 100644 --- a/app/modules/components/attachment/attachment.jade +++ b/app/modules/components/attachment/attachment.jade @@ -5,7 +5,7 @@ form.single-attachment( ) .attachment-name - a( + a.e2e-attachment-link( tg-attachment-link="vm.attachment" href="{{::vm.attachment.getIn(['file', 'url'])}}" title="{{::vm.attachment.get(['file', 'name'])}}" diff --git a/app/modules/components/attachments-drop/attachments-drop.directive.coffee b/app/modules/components/attachments-drop/attachments-drop.directive.coffee index 7fb995a5..a236828a 100644 --- a/app/modules/components/attachments-drop/attachments-drop.directive.coffee +++ b/app/modules/components/attachments-drop/attachments-drop.directive.coffee @@ -29,7 +29,7 @@ AttachmentsDropDirective = ($parse) -> e.stopPropagation() e.preventDefault() - dataTransfer = e.dataTransfer || (e.originalEvent && e.originalEvent.dataTransfer); + dataTransfer = e.dataTransfer || (e.originalEvent && e.originalEvent.dataTransfer) scope.$apply () -> eventAttr(scope, {files: dataTransfer.files}) diff --git a/app/modules/components/attachments-full/attachments-full.controller.coffee b/app/modules/components/attachments-full/attachments-full.controller.coffee index 400ee65f..32a10cb6 100644 --- a/app/modules/components/attachments-full/attachments-full.controller.coffee +++ b/app/modules/components/attachments-full/attachments-full.controller.coffee @@ -26,10 +26,11 @@ class AttachmentsFullController "$tgConfig", "$tgStorage", "tgAttachmentsFullService", - "tgProjectService" + "tgProjectService", + "tgAttachmentsPreviewService" ] - constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService) -> + constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService, @attachmentsPreviewService) -> @.mode = @storage.get('attachment-mode', 'list') @.maxFileSize = @config.get("maxUploadFileSize", null) @@ -64,6 +65,8 @@ class AttachmentsFullController @attachmentsFullService.loadAttachments(@.type, @.objId, @.projectId) deleteAttachment: (toDeleteAttachment) -> + @attachmentsPreviewService.fileId = null + title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT") message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", { fileName: toDeleteAttachment.getIn(['file', 'name']) diff --git a/app/modules/components/attachments-full/attachments-full.controller.spec.coffee b/app/modules/components/attachments-full/attachments-full.controller.spec.coffee index 6c623174..2c94948d 100644 --- a/app/modules/components/attachments-full/attachments-full.controller.spec.coffee +++ b/app/modules/components/attachments-full/attachments-full.controller.spec.coffee @@ -158,7 +158,7 @@ describe "AttachmentsController", -> deleteFile = Immutable.Map() mocks.attachmentsFullService.deleteAttachment = sinon.stub() - mocks.attachmentsFullService.deleteAttachment.withArgs(deleteFile, 'us').promise().reject() + mocks.attachmentsFullService.deleteAttachment.withArgs(deleteFile, 'us').promise().reject(new Error('error')) askResponse = { finish: sinon.spy() diff --git a/app/modules/components/attachments-full/attachments-full.jade b/app/modules/components/attachments-full/attachments-full.jade index d441def1..ce78603f 100644 --- a/app/modules/components/attachments-full/attachments-full.jade +++ b/app/modules/components/attachments-full/attachments-full.jade @@ -94,3 +94,8 @@ section.attachments( alt="{{'COMMON.LOADING' | translate}}" ) .attachment-data {{file.progressMessage}} + +tg-attachments-preview.lightbox.lightbox-block( + ng-show="vm.showAttachments()", + attachments="vm.attachments" +) diff --git a/app/modules/components/attachments-full/attachments-full.service.coffee b/app/modules/components/attachments-full/attachments-full.service.coffee index 8f9a83a3..2635f7c4 100644 --- a/app/modules/components/attachments-full/attachments-full.service.coffee +++ b/app/modules/components/attachments-full/attachments-full.service.coffee @@ -73,7 +73,7 @@ class AttachmentsFullService extends taiga.Service resolve(attachment) else - reject(file) + reject(new Error(file)) loadAttachments: (type, objId, projectId)-> @attachmentsService.list(type, objId, projectId).then (files) => @@ -109,7 +109,7 @@ class AttachmentsFullService extends taiga.Service patch = {order: attachment.getIn(['file', 'order'])} promises.push @attachmentsService.patch(attachment.getIn(['file', 'id']), type, patch) - + return Promise.all(promises).then () => @._attachments = attachments diff --git a/app/modules/components/attachments-preview/attachments-preview.controller.coffee b/app/modules/components/attachments-preview/attachments-preview.controller.coffee new file mode 100644 index 00000000..b1876b5d --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.controller.coffee @@ -0,0 +1,75 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: attchments-preview.controller.coffee +### + +class AttachmentsPreviewController + @.$inject = [ + "tgAttachmentsPreviewService" + ] + + constructor: (@attachmentsPreviewService) -> + taiga.defineImmutableProperty @, "current", () => + if !@attachmentsPreviewService.fileId + return null + + return @.getCurrent() + + hasPagination: () -> + images = @.attachments.filter (attachment) => + return taiga.isImage(attachment.getIn(['file', 'name'])) + + return images.size > 1 + + getCurrent: () -> + attachment = @.attachments.find (attachment) => + @attachmentsPreviewService.fileId == attachment.getIn(['file', 'id']) + + file = attachment.get('file') + + return file + + getIndex: () -> + return @.attachments.findIndex (attachment) => + @attachmentsPreviewService.fileId == attachment.getIn(['file', 'id']) + + next: () -> + attachmentIndex = @.getIndex() + + image = @.attachments.slice(attachmentIndex + 1).find (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + if !image + image = @.attachments.find (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + + @attachmentsPreviewService.fileId = image.getIn(['file', 'id']) + + previous: () -> + attachmentIndex = @.getIndex() + + image = @.attachments.slice(0, attachmentIndex).findLast (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + if !image + image = @.attachments.findLast (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + @attachmentsPreviewService.fileId = image.getIn(['file', 'id']) + +angular.module('taigaComponents').controller('AttachmentsPreview', AttachmentsPreviewController) diff --git a/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee b/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee new file mode 100644 index 00000000..40a6108d --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee @@ -0,0 +1,346 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: attachments-preview.controller.spec.coffee +### + +describe "AttachmentsPreviewController", -> + $provide = null + $controller = null + scope = null + mocks = {} + + _mockAttachmentsPreviewService = -> + mocks.attachmentsPreviewService = {} + + $provide.value("tgAttachmentsPreviewService", mocks.attachmentsPreviewService) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockAttachmentsPreviewService() + + return null + + _inject = -> + inject (_$controller_, $rootScope) -> + $controller = _$controller_ + scope = $rootScope.$new() + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + + _setup() + + it "get current file", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1 + } + }, + { + file: { + id: 2 + } + }, + { + file: { + id: 3 + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 2 + + current = ctrl.getCurrent() + + expect(current.get('id')).to.be.equal(2) + expect(ctrl.current.get('id')).to.be.equal(2) + + + it "has pagination", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1 + + pagination = ctrl.hasPagination() + + expect(pagination).to.be.false + + ctrl.attachments = ctrl.attachments.push(Immutable.fromJS({ + file: { + id: 4, + name: "xx.jpg" + } + })) + + pagination = ctrl.hasPagination() + + expect(pagination).to.be.true + + it "get index", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1 + } + }, + { + file: { + id: 2 + } + }, + { + file: { + id: 3 + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 2 + + currentIndex = ctrl.getIndex() + + expect(currentIndex).to.be.equal(1) + + it "next", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1 + + currentIndex = ctrl.next() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3) + + it "next infinite", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(2) + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 3 + + currentIndex = ctrl.next() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1) + + it "previous", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(2) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 3 + + currentIndex = ctrl.previous() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1) + + it "previous infinite", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1 + + currentIndex = ctrl.previous() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3) diff --git a/app/modules/components/attachments-preview/attachments-preview.directive.coffee b/app/modules/components/attachments-preview/attachments-preview.directive.coffee new file mode 100644 index 00000000..4e6b48cf --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.directive.coffee @@ -0,0 +1,48 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: attachments-preview.directive.coffee +### + +AttachmentPreviewLightboxDirective = (lightboxService, attachmentsPreviewService) -> + link = ($scope, el, attrs, ctrl) -> + $(document.body).on "keydown.image-preview", (e) -> + if attachmentsPreviewService.fileId + if e.keyCode == 39 + ctrl.next() + else if e.keyCode == 37 + ctrl.previous() + + $scope.$digest() + + $scope.$on '$destroy', () -> + $(document.body).off('.image-preview') + + return { + scope: {}, + controller: 'AttachmentsPreview', + templateUrl: 'components/attachments-preview/attachments-preview.html', + link: link, + controllerAs: "vm", + bindToController: { + attachments: "=" + } + } + +angular.module('taigaComponents').directive("tgAttachmentsPreview", [ + "lightboxService", + "tgAttachmentsPreviewService", + AttachmentPreviewLightboxDirective]) diff --git a/app/modules/components/attachments-preview/attachments-preview.jade b/app/modules/components/attachments-preview/attachments-preview.jade new file mode 100644 index 00000000..0be41189 --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.jade @@ -0,0 +1,21 @@ +.attachment-preview(ng-if="vm.attachments.size && vm.current") + tg-lightbox-close + + .attachment-preview-container + a.previous( + href="#", + ng-click="vm.previous()", + ng-if="vm.hasPagination()" + ) + tg-svg(svg-icon="icon-arrow-left") + + a(href="{{vm.current.get('url')}}", title="{{vm.current.get('description')}}", target="_blank", download="{{vm.current.get('name')}}") + tg-preload-image(preload-src="{{vm.getCurrent().get('url')}}") + img(ng-src="{{vm.getCurrent().get('url')}}") + + a.next( + href="#", + ng-click="vm.next()", + ng-if="vm.hasPagination()" + ) + tg-svg(svg-icon="icon-arrow-right") diff --git a/app/modules/components/attachments-preview/attachments-preview.service.coffee b/app/modules/components/attachments-preview/attachments-preview.service.coffee new file mode 100644 index 00000000..5739e8a0 --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.service.coffee @@ -0,0 +1,25 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: attachments-preview.service.coffee +### + +class AttachmentsPreviewService extends taiga.Service + @.$inject = [] + + constructor: () -> + +angular.module("taigaComponents").service("tgAttachmentsPreviewService", AttachmentsPreviewService) diff --git a/app/modules/components/attachments-simple/attachment-simple.scss b/app/modules/components/attachments-simple/attachment-simple.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/app/modules/components/attachments-sortable/attachments-sortable.directive.coffee b/app/modules/components/attachments-sortable/attachments-sortable.directive.coffee index dcbc615f..f1e91147 100644 --- a/app/modules/components/attachments-sortable/attachments-sortable.directive.coffee +++ b/app/modules/components/attachments-sortable/attachments-sortable.directive.coffee @@ -42,7 +42,7 @@ AttachmentSortableDirective = ($parse) -> pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) diff --git a/app/modules/components/avatar/avatar.directive.coffee b/app/modules/components/avatar/avatar.directive.coffee new file mode 100644 index 00000000..8d8aa8b0 --- /dev/null +++ b/app/modules/components/avatar/avatar.directive.coffee @@ -0,0 +1,47 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: avatar.directive.coffee +### + +AvatarDirective = (avatarService) -> + link = (scope, el, attrs) -> + if attrs.tgAvatarBig + attributeName = 'avatarBig' + else + attributeName = 'avatar' + + scope.$watch attributeName, (user) -> + avatar = avatarService.getAvatar(user, attributeName) + + el.attr('src', avatar.url) + if avatar.bg + el.css('background', avatar.bg) + + return { + link: link + scope: { + avatar: "=tgAvatar" + avatarBig: "=tgAvatarBig" + } + } + +AvatarDirective.$inject = [ + 'tgAvatarService' +] + +angular.module("taigaComponents").directive("tgAvatar", AvatarDirective) +angular.module("taigaComponents").directive("tgAvatarBig", AvatarDirective) diff --git a/app/modules/components/belong-to-epics/belong-to-epics-pill.jade b/app/modules/components/belong-to-epics/belong-to-epics-pill.jade new file mode 100644 index 00000000..fd861905 --- /dev/null +++ b/app/modules/components/belong-to-epics/belong-to-epics-pill.jade @@ -0,0 +1,6 @@ +- var hash = "#"; +span.belong-to-epic-pill-wrapper(tg-repeat="epic in epics track by epic.get('id')") + .belong-to-epic-pill( + ng-style="{'background': epic.get('color'), 'border-color': '{{ epic.get('color') | darker: -0.2 }}'}" + title="#{hash}{{epic.get('id')}} {{epic.get('subject')}}" + ) diff --git a/app/modules/components/belong-to-epics/belong-to-epics-text.jade b/app/modules/components/belong-to-epics/belong-to-epics-text.jade new file mode 100644 index 00000000..7d23bdbe --- /dev/null +++ b/app/modules/components/belong-to-epics/belong-to-epics-text.jade @@ -0,0 +1,10 @@ +- var hash = "#"; +span.belong-to-epic-text-wrapper(tg-repeat="epic in epics track by epic.get('id')") + a.belong-to-epic-text( + href="" + tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')" + ) #{hash}{{epic.get('id')}} {{epic.get('subject')}} + span.belong-to-epic-label( + ng-style="::{'background-color': epic.get('color')}" + translate="EPICS.EPIC" + ) diff --git a/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee b/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee new file mode 100644 index 00000000..91ffe19e --- /dev/null +++ b/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: belong-to-epics.directive.coffee +### + +module = angular.module('taigaEpics') + +BelongToEpicsDirective = () -> + + link = (scope, el, attrs) -> + scope.$watch 'epics', (epics) -> + if epics && !epics.isIterable + scope.epics = Immutable.fromJS(epics) + + templateUrl = (el, attrs) -> + if attrs.format + return "components/belong-to-epics/belong-to-epics-" + attrs.format + ".html" + return "components/belong-to-epics/belong-to-epics-pill.html" + + return { + link: link, + scope: { + epics: '=' + }, + templateUrl: templateUrl + } + + +module.directive("tgBelongToEpics", BelongToEpicsDirective) diff --git a/app/modules/components/belong-to-epics/belong-to-epics.scss b/app/modules/components/belong-to-epics/belong-to-epics.scss new file mode 100644 index 00000000..01e05872 --- /dev/null +++ b/app/modules/components/belong-to-epics/belong-to-epics.scss @@ -0,0 +1,36 @@ +.belong-to-epic-pill-wrapper { + display: inline-block; + position: relative; + &:hover { + .belong-to-epic-pill-data { + display: block; + } + } +} + +.belong-to-epic-pill { + background-color: $mass-white; + border-radius: 50%; + display: inline-block; + height: .7rem; + margin: 0 .1rem; + position: relative; + width: .7rem; +} + +.belong-to-epic-text-wrapper { + margin-right: 1rem; +} + +.belong-to-epic-text { + margin-left: .25rem; +} +.belong-to-epic-label { + @include font-type(light); + @include font-size(xsmall); + background: $grayer; + border-radius: .25rem; + color: $white; + margin: 0 .5rem; + padding: .1rem .25rem; +} diff --git a/app/modules/components/board-zoom/board-zoom.directive.coffee b/app/modules/components/board-zoom/board-zoom.directive.coffee new file mode 100644 index 00000000..cba9fdcf --- /dev/null +++ b/app/modules/components/board-zoom/board-zoom.directive.coffee @@ -0,0 +1,29 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: board-zoom.directive.coffee +### + +BoardZoomDirective = () -> + return { + scope: { + levels: "=", + value: "=" + }, + templateUrl: 'components/board-zoom/board-zoom.html' + } + +angular.module('taigaComponents').directive("tgBoardZoom", [BoardZoomDirective]) diff --git a/app/modules/components/board-zoom/board-zoom.jade b/app/modules/components/board-zoom/board-zoom.jade new file mode 100644 index 00000000..e6067dbb --- /dev/null +++ b/app/modules/components/board-zoom/board-zoom.jade @@ -0,0 +1,9 @@ +input.range-slider( + type="range", + min="0", + max="{{levels - 1}}", + step="1" + ng-model="value" + ng-model-options="{ debounce: 200 }" + tg-bind-scope +) diff --git a/app/modules/components/board-zoom/board-zoom.scss b/app/modules/components/board-zoom/board-zoom.scss new file mode 100644 index 00000000..5e5d7eb2 --- /dev/null +++ b/app/modules/components/board-zoom/board-zoom.scss @@ -0,0 +1,108 @@ +$track-color: $whitish; +$thumb-color: $grayer; +$thumb-shadow: rgba($thumb-color, .3); + +$thumb-radius: 50%; +$thumb-height: 14px; +$thumb-width: 14px; +$thumb-border-width: 0; +$thumb-border-color: transparent; + +$track-width: 200px; +$track-height: 3px; +$track-border-width: 0; +$track-border-color: transparent; + +$track-radius: 1px; +$contrast: 2; + +@mixin track() { + width: $track-width; + height: $track-height; + cursor: pointer; + transition: all .2s ease; +} + +@mixin thumb() { + border: $thumb-border-width solid $thumb-border-color; + height: $thumb-height; + width: $thumb-width; + border-radius: $thumb-radius; + background: $thumb-color; + cursor: pointer; + box-shadow: 0 0 0 2px $thumb-shadow; + transition: box-shadow .2s; +} + +.range-slider { + -webkit-appearance: none; + margin: $thumb-height / 2 0; + width: $track-width; + + &:focus { + &::-webkit-slider-runnable-track { + background: lighten($track-color, $contrast); + } + &::-webkit-slider-thumb { + box-shadow: 0 0 0 4px $thumb-shadow; + } + &::-moz-range-thumb { + box-shadow: 0 0 0 4px $thumb-shadow; + } + &::-ms-fill-lower { + background: $track-color; + } + &::-ms-fill-upper { + background: lighten($track-color, $contrast); + } + } + + &::-webkit-slider-runnable-track { + @include track(); + background: $track-color; + border: $track-border-width solid $track-border-color; + border-radius: $track-radius; + } + + &::-webkit-slider-thumb { + @include thumb(); + -webkit-appearance: none; + margin-top: ((-$track-border-width * 2 + $track-height) / 2) - ($thumb-height / 2); + } + + &::-moz-range-track { + @include track(); + background: $track-color; + border: $track-border-width solid $track-border-color; + border-radius: $track-radius; + } + + &::-moz-range-thumb { + @include thumb(); + } + + &::-ms-track { + @include track(); + background: transparent; + border-color: transparent; + border-width: $thumb-width 0; + color: transparent; + } + + &::-ms-fill-lower { + background: darken($track-color, $contrast); + border: $track-border-width solid $track-border-color; + border-radius: $track-radius * 2; + } + + &::-ms-fill-upper { + background: $track-color; + border: $track-border-width solid $track-border-color; + border-radius: $track-radius * 2; + } + + &::-ms-thumb { + @include thumb(); + } + +} diff --git a/app/modules/components/card-slideshow/card-slideshow.controller.coffee b/app/modules/components/card-slideshow/card-slideshow.controller.coffee new file mode 100644 index 00000000..552f65bb --- /dev/null +++ b/app/modules/components/card-slideshow/card-slideshow.controller.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: card-slideshow.controller.coffee +### + +class CardSlideshowController + @.$inject = [] + + constructor: () -> + @.index = 0 + + next: () -> + @.index++ + + if @.index >= @.images.size + @.index = 0 + + previous: () -> + @.index-- + + if @.index < 0 + @.index = @.images.size - 1 + +angular.module('taigaComponents').controller('CardSlideshow', CardSlideshowController) diff --git a/app/modules/components/card-slideshow/card-slideshow.directive.coffee b/app/modules/components/card-slideshow/card-slideshow.directive.coffee new file mode 100644 index 00000000..bbce104b --- /dev/null +++ b/app/modules/components/card-slideshow/card-slideshow.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: card.directive.coffee +### + +module = angular.module("taigaComponents") + +cardSlideshowDirective = () -> + return { + controller: "CardSlideshow", + templateUrl: "components/card-slideshow/card-slideshow.html", + bindToController: true, + controllerAs: "vm", + scope: { + images: "=" + } + } + +module.directive('tgCardSlideshow', cardSlideshowDirective) diff --git a/app/modules/components/card-slideshow/card-slideshow.jade b/app/modules/components/card-slideshow/card-slideshow.jade new file mode 100644 index 00000000..0f42c061 --- /dev/null +++ b/app/modules/components/card-slideshow/card-slideshow.jade @@ -0,0 +1,18 @@ +.card-slideshow(ng-if="vm.images.size") + tg-svg.slideshow-icon.slideshow-left( + ng-click="vm.previous()" + ng-if="vm.images.size > 1" + svg-icon="icon-arrow-left" + ) + tg-svg.slideshow-icon.slideshow-right( + ng-click="vm.next()" + ng-if="vm.images.size > 1" + svg-icon="icon-arrow-right" + ) + + .card-slideshow-wrapper( + ng-if="$index == vm.index" + tg-repeat="image in vm.images track by image.get('id')" + ) + tg-preload-image(preload-src="{{image.get('thumbnail_card_url')}}") + img(ng-src="{{image.get('thumbnail_card_url')}}") diff --git a/app/modules/components/card/card-templates/card-completion.jade b/app/modules/components/card/card-templates/card-completion.jade new file mode 100644 index 00000000..1f2fa662 --- /dev/null +++ b/app/modules/components/card/card-templates/card-completion.jade @@ -0,0 +1,4 @@ +.card-completion(ng-if="vm.visible('extra_info') && vm.item.getIn(['model', 'tasks']).size") + .card-completion-bar + .card-completion-percentage(ng-style="{width: vm.closedTasksPercent() + '%'}" ) + span.card-tooltip tasks {{vm.getClosedTasks().size}}/{{vm.item.getIn(['model', 'tasks']).size}} diff --git a/app/modules/components/card/card-templates/card-data.jade b/app/modules/components/card/card-templates/card-data.jade new file mode 100644 index 00000000..e918b9fc --- /dev/null +++ b/app/modules/components/card/card-templates/card-data.jade @@ -0,0 +1,21 @@ +.card-data( + ng-if="vm.visible('extra_info')" + ng-class="{'empty-tasks': !vm.item.getIn(['model', 'tasks']).size}" +) + span.card-estimation( + ng-if="vm.item.getIn(['model', 'total_points']) === null", + translate="US.NOT_ESTIMATED" + ) + span.card-estimation( + ng-if="vm.item.getIn(['model', 'total_points'])" + ) {{"COMMON.FIELDS.POINTS" | translate}} {{vm.item.getIn(['model', 'total_points'])}} + .card-statistics + .statistic.card-votes(ng-class="{'active': vm.item.getIn(['model', 'is_voter'])}") + tg-svg(svg-icon="icon-upvote") + span {{vm.item.getIn(['model', 'total_voters'])}} + .statistic.card-watchers + tg-svg(svg-icon="icon-watch") + span {{vm.item.getIn(['model', 'watchers']).size}} + .statistic.card-attachments(ng-if="vm.item.getIn(['model', 'attachments']).size") + tg-svg(svg-icon="icon-attachment") + span {{vm.item.getIn(['model', 'attachments']).size}} diff --git a/app/modules/components/card/card-templates/card-owner.jade b/app/modules/components/card/card-templates/card-owner.jade new file mode 100644 index 00000000..682e0c4a --- /dev/null +++ b/app/modules/components/card/card-templates/card-owner.jade @@ -0,0 +1,43 @@ +.card-owner + .card-owner-info(ng-if="vm.item.get('assigned_to')") + .card-owner-avatar + img( + ng-class="{'is-iocaine': vm.item.getIn(['model', 'is_iocaine'])}" + tg-avatar="vm.item.get('assigned_to')" + ) + tg-svg( + ng-if="vm.item.getIn(['model', 'is_iocaine'])" + svg-icon="icon-iocaine" + svg-title="COMMON.IOCAINE_TEXT" + ) + span.card-owner-name(ng-if="vm.visible('owner')") {{vm.item.getIn(['assigned_to', 'full_name'])}} + div(ng-if="!vm.visible('owner')") + include card-title + + .card-owner-info(ng-if="!vm.item.get('assigned_to')") + img(ng-src="/#{v}/images/unnamed.png") + span.card-owner-name( + ng-if="vm.visible('owner')", + translate="COMMON.ASSIGNED_TO.NOT_ASSIGNED" + ) + div(ng-if="!vm.visible('owner')") + include card-title + + .card-owner-actions( + ng-if="vm.visible('owner')" + tg-check-permission="{{vm.getPermissionsKey()}}" + ) + a.e2e-assign.card-owner-assign( + ng-click="vm.onClickAssignedTo({id: vm.item.get('id')})" + href="" + ) + tg-svg(svg-icon="icon-add-user") + span(translate="COMMON.CARD.ASSIGN_TO") + + a.e2e-edit.card-edit( + href="" + ng-click="vm.onClickEdit({id: vm.item.get('id')})" + tg-loading="vm.item.get('loading')" + ) + tg-svg(svg-icon="icon-edit") + span(translate="COMMON.CARD.EDIT") diff --git a/app/modules/components/card/card-templates/card-tags.jade b/app/modules/components/card/card-templates/card-tags.jade new file mode 100644 index 00000000..cb18254d --- /dev/null +++ b/app/modules/components/card/card-templates/card-tags.jade @@ -0,0 +1,7 @@ +.card-tags(ng-if="vm.visible('tags')") + span.card-tag( + tg-repeat="tag in vm.item.get('colorized_tags') track by tag.get('name')" + style="background-color: {{tag.get('color')}}" + title="{{tag.get('name')}}" + ng-if="tag.get('color')" + ) diff --git a/app/modules/components/card/card-templates/card-tasks.jade b/app/modules/components/card/card-templates/card-tasks.jade new file mode 100644 index 00000000..d029bc1e --- /dev/null +++ b/app/modules/components/card/card-templates/card-tasks.jade @@ -0,0 +1,7 @@ +ul.card-tasks(ng-if="vm.isRelatedTasksVisible()") + li.card-task(tg-repeat="task in vm.item.getIn(['model', 'tasks'])") + a( + href="#" + tg-nav="project-tasks-detail:project=vm.project.slug,ref=task.get('ref')", + ng-class="{'closed-task': task.get('is_closed'), 'blocked-task': task.get('is_blocked')}" + ) {{"#" + task.get('ref')}} {{task.get('subject')}} diff --git a/app/modules/components/card/card-templates/card-title.jade b/app/modules/components/card/card-templates/card-title.jade new file mode 100644 index 00000000..94df8825 --- /dev/null +++ b/app/modules/components/card/card-templates/card-title.jade @@ -0,0 +1,14 @@ +h2.card-title + a( + href="" + tg-nav="{{vm.getNavKey()}}:project=vm.project.slug,ref=vm.item.getIn(['model', 'ref'])", + tg-nav-get-params="{\"kanban-status\": {{vm.item.getIn(['model', 'status'])}}}" + title="#{{ ::vm.item.getIn(['model', 'ref']) }} {{ vm.item.getIn(['model', 'subject'])}}" + ) + span(ng-if="vm.visible('ref')") {{::"#" + vm.item.getIn(['model', 'ref'])}} + span.e2e-title(ng-if="vm.visible('subject')") {{vm.item.getIn(['model', 'subject'])}} + tg-belong-to-epics( + format="pill" + ng-if="vm.item.getIn(['model', 'epics'])" + epics="vm.item.getIn(['model', 'epics'])" + ) diff --git a/app/modules/components/card/card-templates/card-unfold.jade b/app/modules/components/card/card-templates/card-unfold.jade new file mode 100644 index 00000000..5b734660 --- /dev/null +++ b/app/modules/components/card/card-templates/card-unfold.jade @@ -0,0 +1,6 @@ +.card-unfold.ng-animate-disabled( + ng-click="vm.toggleFold()" + ng-if="vm.visible('unfold') && (vm.item.getIn(['model', 'tasks']).size || vm.item.get('images').size)" + role="button" +) + tg-svg(svg-icon="icon-view-more") diff --git a/app/modules/components/card/card.controller.coffee b/app/modules/components/card/card.controller.coffee new file mode 100644 index 00000000..97ec971b --- /dev/null +++ b/app/modules/components/card/card.controller.coffee @@ -0,0 +1,82 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: card.controller.coffee +### + +class CardController + @.$inject = [] + + visible: (name) -> + return @.zoom.indexOf(name) != -1 + + toggleFold: () -> + @.onToggleFold({id: @.item.get('id')}) + + getClosedTasks: () -> + return @.item.getIn(['model', 'tasks']).filter (task) -> return task.get('is_closed') + + closedTasksPercent: () -> + return @.getClosedTasks().size * 100 / @.item.getIn(['model', 'tasks']).size + + getPermissionsKey: () -> + if @.type == 'task' + return 'modify_task' + else + return 'modify_us' + + _setVisibility: () -> + visibility = { + related: @.visible('related_tasks'), + slides: @.visible('attachments') + } + + if!_.isUndefined(@.item.get('foldStatusChanged')) + if @.visible('related_tasks') && @.visible('attachments') + visibility.related = !@.item.get('foldStatusChanged') + visibility.slides = !@.item.get('foldStatusChanged') + else if @.visible('attachments') + visibility.related = @.item.get('foldStatusChanged') + visibility.slides = @.item.get('foldStatusChanged') + else if !@.visible('related_tasks') && !@.visible('attachments') + visibility.related = @.item.get('foldStatusChanged') + visibility.slides = @.item.get('foldStatusChanged') + + if !@.item.getIn(['model', 'tasks']) || !@.item.getIn(['model', 'tasks']).size + visibility.related = false + + if !@.item.get('images') || !@.item.get('images').size + visibility.slides = false + + return visibility + + isRelatedTasksVisible: () -> + visibility = @._setVisibility() + + return visibility.related + + isSlideshowVisible: () -> + visibility = @._setVisibility() + + return visibility.slides + + getNavKey: () -> + if @.type == 'task' + return 'project-tasks-detail' + else + return 'project-userstories-detail' + +angular.module('taigaComponents').controller('Card', CardController) diff --git a/app/modules/components/card/card.controller.spec.coffee b/app/modules/components/card/card.controller.spec.coffee new file mode 100644 index 00000000..3e2ed30c --- /dev/null +++ b/app/modules/components/card/card.controller.spec.coffee @@ -0,0 +1,142 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: card.controller.spec.coffee +### + +describe "Card", -> + $provide = null + $controller = null + mocks = {} + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _inject() + + beforeEach -> + module "taigaComponents" + + _setup() + + it "toggle fold callback", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({id: 2}) + ctrl.onToggleFold = sinon.spy() + + ctrl.toggleFold() + + expect(ctrl.onToggleFold).to.have.been.calledWith({id: 2}) + + it "get closed tasks", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({ + id: 2, + model: { + tasks: [ + {is_closed: true}, + {is_closed: false}, + {is_closed: true} + ] + } + }) + + tasks = ctrl.getClosedTasks() + expect(tasks.size).to.be.equal(2) + + it "get closed percent", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({ + id: 2, + model: { + tasks: [ + {is_closed: true}, + {is_closed: false}, + {is_closed: false}, + {is_closed: true} + ] + } + }) + + percent = ctrl.closedTasksPercent() + expect(percent).to.be.equal(50) + + describe "check if related task and slides visibility", () -> + it "no content", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({ + id: 2, + images: [], + model: { + tasks: [] + } + }) + + ctrl.visible = () => return true + + visibility = ctrl._setVisibility() + + expect(visibility).to.be.eql({ + related: false, + slides: false + }) + + it "with content", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({ + id: 2, + images: [3,4], + model: { + tasks: [1,2] + } + }) + + ctrl.visible = () => return true + + visibility = ctrl._setVisibility() + + expect(visibility).to.be.eql({ + related: true, + slides: true + }) + + it "fold", () -> + ctrl = $controller("Card") + + ctrl.item = Immutable.fromJS({ + foldStatusChanged: true, + id: 2, + images: [3,4], + model: { + tasks: [1,2] + } + }) + + ctrl.visible = () => return true + + visibility = ctrl._setVisibility() + + expect(visibility).to.be.eql({ + related: false, + slides: false + }) diff --git a/app/modules/components/card/card.directive.coffee b/app/modules/components/card/card.directive.coffee new file mode 100644 index 00000000..4ad69505 --- /dev/null +++ b/app/modules/components/card/card.directive.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: card.directive.coffee +### + +module = angular.module("taigaComponents") + +cardDirective = () -> + return { + link: (scope) -> + + controller: "Card", + controllerAs: "vm", + bindToController: true, + templateUrl: "components/card/card.html", + scope: { + onToggleFold: "&", + onClickAssignedTo: "&", + onClickEdit: "&", + project: "=", + item: "=", + zoom: "=", + zoomLevel: "=", + archived: "=", + type: "@" + } + } + +module.directive('tgCard', cardDirective) diff --git a/app/modules/components/card/card.jade b/app/modules/components/card/card.jade new file mode 100644 index 00000000..556c3c82 --- /dev/null +++ b/app/modules/components/card/card.jade @@ -0,0 +1,16 @@ +.card-inner( + class="{{'zoom-' + vm.zoomLevel}}" + ng-class="{'card-blocked': vm.item.getIn(['model', 'is_blocked']), 'archived': vm.archived}" +) + include card-templates/card-tags + include card-templates/card-owner + div(ng-if="vm.visible('owner')") + include card-templates/card-title + include card-templates/card-data + include card-templates/card-completion + include card-templates/card-tasks + tg-card-slideshow( + ng-if="vm.isSlideshowVisible()" + images="vm.item.get('images')" + ) + include card-templates/card-unfold diff --git a/app/modules/components/card/card.scss b/app/modules/components/card/card.scss new file mode 100644 index 00000000..b31f11a3 --- /dev/null +++ b/app/modules/components/card/card.scss @@ -0,0 +1,326 @@ +.card { + box-shadow: 2px 2px 4px darken($whitish, 10%); + cursor: move; + display: block; + margin: 0 .6rem .6rem; + overflow: hidden; + transition: box-shadow .2s ease-in; + &:hover { + box-shadow: 3px 3px 6px darken($whitish, 10%); + } +} + +.card-inner { + background: $white; + border-radius: .25rem; + &.zoom-0, + &.zoom-1 { + .card-title { + flex: 1; + margin: 0; + padding: .25rem; + } + } + &.zoom-1 { + .card-owner-info { + align-items: flex-start; + } + } + &.card-blocked { + background: $red-light; + .statistic, + .card-title a, + .card-owner-name, + .card-estimation { + color: $white; + } + .card-owner-actions { + background: rgba($red-light, .9); + } + svg { + fill: $white; + } + .statistic { + &.active { + color: $white; + } + } + .card-unfold { + &:hover { + background: rgba($red-light, .9); + } + } + &.zoom-0, + &.zoom-1 { + .card-title { + color: $white; + } + } + } +} + +.card-tags { + display: flex; + .card-tag { + display: block; + flex: 1; + height: .5rem; + + } +} + +.card-owner { + position: relative; + .card-owner-info { + align-items: center; + display: flex; + } + .card-owner-avatar { + line-height: 0; + position: relative; + } + .icon-iocaine { + @include svg-size(1.2rem); + background: rgba($blackish, .8); + border-radius: 4px 0 0; + bottom: .25rem; + fill: $whitish; + padding: .25rem; + position: absolute; + right: .5rem; + } + .is-iocaine { + filter: hue-rotate(265deg) saturate(3); + } + &:hover { + .card-owner-actions { + opacity: 1; + } + } + img { + flex-shrink: 0; + height: 2.5rem; + margin-right: .5rem; + width: 2.5rem; + } + .card-owner-name { + color: $gray-light; + } +} + +.card-owner-actions { + background: rgba($white, .9); + display: flex; + justify-content: space-between; + left: 0; + opacity: 0; + position: absolute; + top: 0; + transition: all .2s; + width: 100%; + &:hover { + color: $primary-light; + svg { + fill: currentColor; + } + } + .icon { + @include svg-size(1.2rem); + display: inline-block; + margin-right: .25rem; + padding: 0; + } + a { + align-items: center; + cursor: pointer; + display: flex; + padding: .6rem 1rem; + } +} + +.card-title { + @include font-size(normal); + line-height: 1.25; + margin-bottom: .25rem; + padding: 1rem 1rem 0; + span { + padding-right: .25rem; + } +} + +.card-data { + color: $gray-light; + display: flex; + font-size: 14px; + justify-content: space-between; + padding: 0 1rem .5rem; +} + +.card-statistics { + @include font-size(small); + color: lighten($gray-light, 25%); + display: flex; + margin-left: auto; + .statistic { + align-content: center; + display: flex; + margin-left: .75rem; + &.active { + color: $primary-light; + svg { + fill: currentColor; + } + } + } + .icon { + @include svg-size(.75rem); + fill: lighten($gray-light, 25%); + margin-right: .2rem; + } +} + +.card-completion { + margin: 0 1rem .5rem; + position: relative; + .card-completion-bar { + background: $whitish; + height: .4rem; + width: 100%; + } + .card-completion-percentage { + background: $primary-light; + cursor: pointer; + height: .4rem; + left: 0; + position: absolute; + top: 0; + &:hover { + + .card-tooltip { + opacity: 1; + } + } + } + .card-tooltip { + background: $blackish; + border-radius: 5px; + color: $white; + font-size: 14px; + left: calc(25% - 50px); + opacity: 0; + padding: .25rem 1rem; + position: absolute; + text-align: center; + top: -2.25rem; + transition: opacity .2s; + width: 100px; + &::after { + background: $black; + content: ''; + height: 10px; + left: 50%; + position: absolute; + top: 70%; + transform: rotate(45deg); + width: 10px; + } + } +} + +.card-unfold { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + margin: 0; + padding: .25rem; + &:hover { + background: linear-gradient(to bottom, $white, darken($white, 1%)); + } + svg { + @include svg-size($width: 2rem, $height: .3rem); + fill: $whitish; + } +} + +.card-tasks { + border-top: 1px solid $whitish; + margin: 0; + margin-top: .5rem; + padding: 0; +} + +.card-task { + @include font-size(xsmall); + border-bottom: 1px solid $whitish; + list-style: none; + a { + color: $gray-light; + display: block; + overflow: hidden; + padding: .5rem .75rem; + text-overflow: ellipsis; + transition: color .2s; + white-space: nowrap; + &.blocked-task { + color: $red-light; + } + &.closed-task { + color: $gray-light; + text-decoration: line-through; + } + &:hover { + color: $primary; + } + } +} + +.card-slideshow { + position: relative; + &:hover { + .slideshow-left, + .slideshow-right { + background: rgba($white, .2); + padding: .25rem; + transition: background .2s; + } + } + .slideshow-icon { + cursor: pointer; + position: absolute; + top: 35%; + &:hover { + background: rgba($primary-light, .5); + transition: background .2s; + } + } + svg { + @include svg-size(1.2rem); + transition: fill .2s; + } + .slideshow-left, + .slideshow-right { + background: transparent; + padding: .25rem; + } + .slideshow-left { + left: 0; + } + .slideshow-right { + right: 0; + } + img { + width: 100%; + } +} + +.card-slideshow-wrapper { + align-items: center; + display: flex; + height: 120px; + justify-content: center; + overflow: hidden; + .loading-spinner { + min-height: 3rem; + min-width: 3rem; + } +} diff --git a/app/modules/components/color-selector/color-selector.controller.coffee b/app/modules/components/color-selector/color-selector.controller.coffee new file mode 100644 index 00000000..47838f23 --- /dev/null +++ b/app/modules/components/color-selector/color-selector.controller.coffee @@ -0,0 +1,65 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: color-selector.controller.coffee +### + +taiga = @.taiga +getDefaulColorList = taiga.getDefaulColorList + + +class ColorSelectorController + @.$inject = [ + "tgProjectService", + ] + + constructor: (@projectService) -> + @.colorList = getDefaulColorList() + @.checkIsColorRequired() + @.displayColorList = false + + userCanChangeColor: () -> + return true if not @.requiredPerm + return @projectService.hasPermission(@.requiredPerm) + + checkIsColorRequired: () -> + if !@.isColorRequired + @.colorList = _.dropRight(@.colorList) + + setColor: (color) -> + @.color = @.initColor + + resetColor: () -> + if @.isColorRequired and not @.color + @.color = @.initColor + + toggleColorList: () -> + @.displayColorList = !@.displayColorList + @.resetColor() + + onSelectDropdownColor: (color) -> + @.color = color + @.onSelectColor({color: color}) + @.toggleColorList() + + onKeyDown: (event) -> + if event.which == 13 # ENTER + if @.color or not @.isColorRequired + @.onSelectDropdownColor(@.color) + event.preventDefault() + + +angular.module('taigaComponents').controller("ColorSelectorCtrl", ColorSelectorController) diff --git a/app/modules/components/color-selector/color-selector.controller.spec.coffee b/app/modules/components/color-selector/color-selector.controller.spec.coffee new file mode 100644 index 00000000..5fe727f6 --- /dev/null +++ b/app/modules/components/color-selector/color-selector.controller.spec.coffee @@ -0,0 +1,83 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: color-selector.controller.spec.coffee +### + +describe "ColorSelector", -> + provide = null + controller = null + colorSelectorCtrl = null + mocks = {} + + _mockTgProjectService = () -> + mocks.tgProjectService = { + hasPermission: sinon.stub() + } + provide.value "tgProjectService", mocks.tgProjectService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgProjectService() + + return null + + beforeEach -> + module "taigaComponents" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "require Color on Selector", () -> + colorSelectorCtrl = controller "ColorSelectorCtrl" + colorSelectorCtrl.colorList = ["#000000", "#123123"] + colorSelectorCtrl.isColorRequired = false + colorSelectorCtrl.checkIsColorRequired() + expect(colorSelectorCtrl.colorList).to.be.eql(["#000000"]) + + it "display Color Selector", () -> + colorSelectorCtrl = controller "ColorSelectorCtrl" + colorSelectorCtrl.toggleColorList() + expect(colorSelectorCtrl.displayColorList).to.be.true + + it "on select Color", () -> + colorSelectorCtrl = controller "ColorSelectorCtrl" + colorSelectorCtrl.toggleColorList = sinon.stub() + + color = '#FFFFFF' + + colorSelectorCtrl.onSelectColor = sinon.spy() + + colorSelectorCtrl.onSelectDropdownColor(color) + expect(colorSelectorCtrl.toggleColorList).have.been.called + expect(colorSelectorCtrl.onSelectColor).to.have.been.calledWith({color: color}) + + it "save on keydown Enter", () -> + colorSelectorCtrl = controller "ColorSelectorCtrl" + colorSelectorCtrl.onSelectDropdownColor = sinon.stub() + + event = {which: 13, preventDefault: sinon.stub()} + color = "#fabada" + + colorSelectorCtrl.color = color + + colorSelectorCtrl.onKeyDown(event) + expect(event.preventDefault).have.been.called + expect(colorSelectorCtrl.onSelectDropdownColor).have.been.called + expect(colorSelectorCtrl.onSelectDropdownColor).have.been.calledWith(color) diff --git a/app/modules/components/color-selector/color-selector.directive.coffee b/app/modules/components/color-selector/color-selector.directive.coffee new file mode 100644 index 00000000..f020e408 --- /dev/null +++ b/app/modules/components/color-selector/color-selector.directive.coffee @@ -0,0 +1,70 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: color-selector.directive.coffee +### + +bindOnce = @.taiga.bindOnce + +ColorSelectorDirective = ($timeout) -> + link = (scope, el, attrs, ctrl) -> + # Animation + _timeout = null + + cancel = () -> + $timeout.cancel(_timeout) + _timeout = null + + close = () -> + return if _timeout + + _timeout = $timeout (() -> + ctrl.displayColorList = false + ctrl.resetColor() + ), 400 + + el.find('.color-selector') + .mouseenter(cancel) + .mouseleave(close) + + el.find('.color-selector-dropdown') + .mouseenter(cancel) + .mouseleave(close) + + scope.$watch 'vm.initColor', (color) -> + # We can't just bind once because sometimes the initial color is reset from the outside + ctrl.setColor(color) + + return { + link: link, + templateUrl:"components/color-selector/color-selector.html", + controller: "ColorSelectorCtrl", + controllerAs: "vm", + bindToController: { + isColorRequired: "=", + onSelectColor: "&", + initColor: "=", + requiredPerm: "@" + }, + scope: {}, + } + + +ColorSelectorDirective.$inject = [ + "$timeout" +] + +angular.module('taigaComponents').directive("tgColorSelector", ColorSelectorDirective) diff --git a/app/modules/components/color-selector/color-selector.jade b/app/modules/components/color-selector/color-selector.jade new file mode 100644 index 00000000..dda5becc --- /dev/null +++ b/app/modules/components/color-selector/color-selector.jade @@ -0,0 +1,43 @@ +//- Read only mode +.color-selector(ng-if="!vm.userCanChangeColor()") + .tag-color.disabled.e2e-open-color-selector( + ng-class="{'empty-color': !vm.color}" + ng-style="{'background': vm.color}" + ) + +//- Read & Edit mode +.color-selector(ng-if="vm.userCanChangeColor()") + .tag-color.e2e-open-color-selector( + ng-click="vm.toggleColorList()" + ng-class="{'empty-color': !vm.color}" + ng-style="{'background': vm.color}" + ) + .color-selector-dropdown(ng-if="vm.displayColorList") + ul.color-selector-dropdown-list.e2e-color-dropdown + li.color-selector-option( + ng-repeat="color in vm.colorList" + ng-style="{'background': color}" + ng-title="color" + ng-click="vm.onSelectDropdownColor(color)" + ) + li.empty-color( + ng-if="!vm.isColorRequired" + ng-click="vm.onSelectDropdownColor(null)" + ) + .custom-color-selector + .display-custom-color.empty-color( + ng-if="!vm.color" + ) + .display-custom-color-wrapper + .display-custom-color( + ng-if="vm.color" + ng-style="{'background': vm.color}" + ng-click="vm.onSelectDropdownColor(vm.color)" + ) + input.custom-color-input( + type="text" + maxlength="7" + placeholder="Type hex code" + ng-model="vm.color" + ng-keydown="vm.onKeyDown($event)" + ) diff --git a/app/modules/components/color-selector/color-selector.scss b/app/modules/components/color-selector/color-selector.scss new file mode 100644 index 00000000..98cf62fd --- /dev/null +++ b/app/modules/components/color-selector/color-selector.scss @@ -0,0 +1,74 @@ +@mixin color-selector-option { + border-radius: 2px; + cursor: pointer; + height: 2.25rem; + width: 2.25rem; + min-width: 2.25rem; + margin: 0 .5rem .5rem 0; + &:nth-child(7n) { + margin-right: 0; + } +} + +.color-selector { + position: relative; + .tag-color { + @include color-selector-option; + border: 1px solid $gray-light; + border-radius: 0; + margin: 0; + transition: background .3s ease-out; + &.disabled { + cursor: auto; + } + &.empty-color { + @include empty-color(34); + } + } +} + +.color-selector-dropdown { + background: $blackish; + left: 0; + padding: 1rem; + position: absolute; + top: 2.25rem; + width: 332px; + z-index: 99; +} + +.color-selector-dropdown-list { + display: flex; + flex-wrap: wrap; + list-style-type: none; + margin-bottom: 0; + .color-selector-option { + @include color-selector-option; + } + .empty-color { + @include color-selector-option; + @include empty-color(34); + } +} + +.custom-color-selector { + align-items: center; + display: flex; + .custom-color-input { + margin: 0; + width: 100%; + } + .display-custom-color-wrapper { + background: $mass-white; + margin-right: .5rem; + } + .display-custom-color { + @include color-selector-option; + flex-shrink: 0; + margin: 0; + &.empty-color { + @include empty-color(34); + cursor: default; + } + } +} diff --git a/app/modules/components/detail/header/detail-header.controller.coffee b/app/modules/components/detail/header/detail-header.controller.coffee new file mode 100644 index 00000000..2d2b5437 --- /dev/null +++ b/app/modules/components/detail/header/detail-header.controller.coffee @@ -0,0 +1,90 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: story-header.controller.coffee +### + +module = angular.module("taigaUserStories") + +class StoryHeaderController + @.$inject = [ + "$rootScope", + "$tgConfirm", + "$tgQueueModelTransformation", + "$tgNavUrls", + "$window" + ] + + constructor: (@rootScope, @confirm, @modelTransform, @navUrls, @window) -> + @.editMode = false + @.loadingSubject = false + @.originalSubject = @.item.subject + + _checkNav: () -> + if @.item.neighbors.previous?.ref? + ctx = { + project: @.project.slug + ref: @.item.neighbors.previous.ref + } + @.previousUrl = @navUrls.resolve("project-" + @.item._name + "-detail", ctx) + + if @.item.neighbors.next?.ref? + ctx = { + project: @.project.slug + ref: @.item.neighbors.next.ref + } + @.nextUrl = @navUrls.resolve("project-" + @.item._name + "-detail", ctx) + + _checkPermissions: () -> + @.permissions = { + canEdit: _.includes(@.project.my_permissions, @.requiredPerm) + } + + editSubject: (value) -> + selection = @window.getSelection() + if selection.type != "Range" + if value + @.editMode = true + if !value + @.editMode = false + + onKeyDown: (event) -> + if event.which == 13 + @.saveSubject() + + if event.which == 27 + @.item.subject = @.originalSubject + @.editSubject(false) + + saveSubject: () -> + onEditSubjectSuccess = () => + @.loadingSubject = false + @rootScope.$broadcast("object:updated") + @confirm.notify('success') + @.originalSubject = @.item.subject + + onEditSubjectError = () => + @.loadingSubject = false + @confirm.notify('error') + + @.editMode = false + @.loadingSubject = true + item = @.item + transform = @modelTransform.save (item) -> + return item + return transform.then(onEditSubjectSuccess, onEditSubjectError) + +module.controller("StoryHeaderCtrl", StoryHeaderController) diff --git a/app/modules/components/detail/header/detail-header.controller.spec.coffee b/app/modules/components/detail/header/detail-header.controller.spec.coffee new file mode 100644 index 00000000..31ee9c96 --- /dev/null +++ b/app/modules/components/detail/header/detail-header.controller.spec.coffee @@ -0,0 +1,144 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: wiki-history.controller.spec.coffee +### + +describe "StoryHeaderComponent", -> + headerDetailCtrl = null + provide = null + controller = null + rootScope = null + mocks = {} + + _mockRootScope = () -> + mocks.rootScope = { + $broadcast: sinon.stub() + } + + provide.value "$rootScope", mocks.rootScope + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgQueueModelTransformation = () -> + mocks.modelTransform = { + save: sinon.stub() + } + + provide.value "$tgQueueModelTransformation", mocks.tgQueueModelTransformation + + _mockTgNav = () -> + mocks.navUrls = { + resolve: sinon.stub().returns('project-issues-detail') + } + + provide.value "$tgNavUrls", mocks.navUrls + + _mockWindow = () -> + mocks.window = { + getSelection: sinon.stub() + } + + provide.value "$window", mocks.window + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockRootScope() + _mockTgConfirm() + _mockTgQueueModelTransformation() + _mockTgNav() + _mockWindow() + + return null + + beforeEach -> + module "taigaUserStories" + + _mocks() + + inject ($controller) -> + controller = $controller + + headerDetailCtrl = controller "StoryHeaderCtrl", {}, { + item: { + subject: 'Example subject' + } + } + + headerDetailCtrl.originalSubject = headerDetailCtrl.item.subject + + it "previous item neighbor", () -> + headerDetailCtrl.project = { + slug: 'example_subject' + } + headerDetailCtrl.item.neighbors = { + previous: { + ref: 42 + } + } + headerDetailCtrl._checkNav() + headerDetailCtrl.previousUrl = mocks.navUrls.resolve("project-issues-detail") + expect(headerDetailCtrl.previousUrl).to.be.equal("project-issues-detail") + + it "check permissions", () -> + headerDetailCtrl.project = { + my_permissions: ['view_us'] + } + headerDetailCtrl.requiredPerm = 'view_us' + headerDetailCtrl._checkPermissions() + expect(headerDetailCtrl.permissions).to.be.eql({canEdit: true}) + + it "edit subject without selection", () -> + mocks.window.getSelection.returns({ + type: 'Range' + }) + headerDetailCtrl.editSubject(true) + expect(headerDetailCtrl.editMode).to.be.false + + it "edit subject on click", () -> + mocks.window.getSelection.returns({ + type: 'potato' + }) + headerDetailCtrl.editSubject(true) + expect(headerDetailCtrl.editMode).to.be.true + + it "do not edit subject", () -> + mocks.window.getSelection.returns({ + type: 'Range' + }) + headerDetailCtrl.editSubject(false) + expect(headerDetailCtrl.editMode).to.be.false + + it "save on keydown Enter", () -> + event = {} + event.which = 13 + headerDetailCtrl.saveSubject = sinon.stub() + headerDetailCtrl.onKeyDown(event) + expect(headerDetailCtrl.saveSubject).have.been.called + + it "don't save on keydown ESC", () -> + event = {} + event.which = 27 + headerDetailCtrl.editSubject = sinon.stub() + headerDetailCtrl.onKeyDown(event) + expect(headerDetailCtrl.item.subject).to.be.equal(headerDetailCtrl.originalSubject) + expect(headerDetailCtrl.editSubject).have.been.calledWith(false) diff --git a/app/modules/components/detail/header/detail-header.directive.coffee b/app/modules/components/detail/header/detail-header.directive.coffee new file mode 100644 index 00000000..10267d0f --- /dev/null +++ b/app/modules/components/detail/header/detail-header.directive.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: story-header.directive.coffee +### + +module = angular.module('taigaUserStories') + +DetailHeaderDirective = () -> + @.$inject = [] + + link = (scope, el, attrs, ctrl) -> + ctrl._checkPermissions() + ctrl._checkNav() + + return { + link: link, + controller: "StoryHeaderCtrl", + bindToController: true, + scope: { + item: "=", + project: "=", + requiredPerm: "@" + }, + controllerAs: "vm", + templateUrl:"components/detail/header/detail-header.html" + } + + +module.directive("tgDetailHeader", DetailHeaderDirective) diff --git a/app/modules/components/detail/header/detail-header.jade b/app/modules/components/detail/header/detail-header.jade new file mode 100644 index 00000000..317065c0 --- /dev/null +++ b/app/modules/components/detail/header/detail-header.jade @@ -0,0 +1,113 @@ +.detail-title-wrapper.e2e-story-header + h2.detail-title-text.ng-animate-disabled( + ng-show="!vm.editMode" + ng-hide="vm.editMode" + ) + span.detail-number {{'#' + vm.item.ref}} + span.detail-subject.e2e-title-subject( + ng-click="vm.editSubject(true)" + ng-if="vm.permissions.canEdit" + ) {{vm.item.subject}} + span.detail-subject.e2e-title-subject( + ng-if="!vm.permissions.canEdit" + ) {{vm.item.subject}} + tg-svg.detail-edit.e2e-detail-edit( + ng-if="vm.permissions.canEdit" + svg-icon="icon-edit" + ng-click="vm.editSubject(true)" + ) + + .edit-title-wrapper(ng-if="vm.editMode") + input.edit-title-input.e2e-title-input( + type="text" + ng-model="vm.item.subject" + maxlength="500" + autofocus + required + ng-keydown="vm.onKeyDown($event)" + ) + button.edit-title-button.e2e-title-button( + ng-click="vm.saveSubject()" + tg-loading="vm.loadingSubject" + ) + tg-svg( + svg-icon="icon-save" + ) + +//- User Story belongs to epic +.belong-to-epics-wrapper(ng-if="vm.item.epics") + span This User Story belongs to + tg-belong-to-epics( + ng-if="::vm.item.epics" + epics="::vm.item.epics" + format="text" + ) + +//- Task belongs to US +.task-belongs-to( + ng-if="vm.item.user_story_extra_info" + tg-check-permission="view_us" +) + span(translate="TASK.OWNER_US") + a( + tg-nav="project-userstories-detail:project=vm.project.slug,ref=vm.item.user_story_extra_info.ref" + title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}" + ) + span.item-ref {{'#' + vm.item.user_story_extra_info.ref}} + span {{::vm.item.user_story_extra_info.subject}} + tg-belong-to-epics( + ng-if="::vm.item.user_story_extra_info.epics" + epics="::vm.item.user_story_extra_info.epics" + format="pill" + ) + +//- User Stories generated from issue +.item-generated-us(ng-if="vm.item.generated_user_stories.length") + span(translate="ISSUES.PROMOTED") + a( + ng-repeat="userstory in vm.item.generated_user_stories track by userstory.id" + tg-check-permission="view_us" + tg-nav="project-userstories-detail:project=vm.project.slug,ref=userstory.ref" + ) {{'#' + userstory.ref}} {{userstory.subject}} + +//- Issue origin from github +.issue-external-reference(ng-if="vm.item.external_reference") + span(translate="ISSUES.EXTERNAL_REFERENCE") + a( + target="_blank" + ng-href="::vm.item.external_reference[1]" + ng-title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}" + ) + span {{ ::vm.item.external_reference[1] }} + +//- User Story promoted from issue +.item-origin-issue(ng-if="vm.item.origin_issue") + span(translate="US.PROMOTED") + a( + href="" + tg-check-permission="view_us" + tg-nav="project-issues-detail:project=vm.project.slug,ref=vm.item.origin_issue.ref" + title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}" + ) + span.item-ref {{'#' + vm.item.origin_issue.ref}} + span {{vm.item.origin_issue.subject}} + +//- Blocked description +.block-desc-container(ng-show="vm.item.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description(ng-if="vm.item.blocked_note") {{vm.item.blocked_note}} + +//- Navigation +.issue-nav + a( + ng-if="vm.previousUrl" + ng-href="{{vm.previousUrl}}" + title="{{'COMMON.PREVIOUS' | translate}}" + ) + tg-svg(svg-icon="icon-arrow-left") + a( + ng-if="vm.nextUrl" + ng-href="{{vm.nextUrl}}" + title="{{'COMMON.NEXT' | translate}}" + ) + tg-svg(svg-icon="icon-arrow-right") diff --git a/app/modules/components/detail/header/detail-header.scss b/app/modules/components/detail/header/detail-header.scss new file mode 100644 index 00000000..122e1cd3 --- /dev/null +++ b/app/modules/components/detail/header/detail-header.scss @@ -0,0 +1,120 @@ +.detail-header-container { + background: $mass-white; + flex: 1; + padding: 1rem; + position: relative; + &:hover { + .detail-edit { + opacity: 1; + } + } + &.blocked { + background: $red; + color: $white; + transition: all .2s linear; + a, + .detail-number, + .detail-subject { + color: $white; + } + svg { + fill: $white; + } + } + .item-generated-us, + .item-origin-issue, + .task-belongs-to, + .belong-to-epics-wrapper, + .block-desc-container { + @include font-size(small); + margin-top: .5rem; + } + .item-generated-us, + .task-belongs-to, + .item-origin-issue { + a { + cursor: pointer; + padding: 0 .2rem; + } + .item-ref { + padding: 0 .2rem; + } + } +} + +.detail-title-wrapper { + @include font-size(larger); + @include font-type(text); + align-content: center; + display: flex; + max-width: 95%; + position: relative; + transition: all .2s linear; + &.blocked { + background: $red; + transition: all .2s linear; + } + .detail-title-text { + line-height: normal; + margin: 0; + } + .detail-number { + color: $gray-light; + flex-shrink: 0; + margin-right: .5rem; + } + .detail-subject { + color: $gray; + flex-grow: 1; + } + .detail-edit { + cursor: pointer; + margin-left: .75rem; + opacity: 0; + transition: opacity .2s; + svg { + @include svg-size(1.25rem); + } + } +} + +.edit-title-wrapper { + @include font-size(larger); + @include font-type(text); + display: flex; + flex: 1; + .edit-title-input { + background: $white; + flex: 1; + } + .edit-title-button { + background: none; + display: inline; + margin-left: 1rem; + transition: fill .2s; + &:hover { + fill: $primary; + } + } +} + +.block-desc-container { + .block-description-title { + @include font-type(bold); + margin-right: .5rem; + + } +} + +.issue-nav { + position: absolute; + right: 1rem; + top: 1rem; + a { + display: inline-block; + } + svg { + @include svg-size(1.2rem); + fill: currentColor; + } +} diff --git a/app/modules/components/filter/filter-remote.service.coffee b/app/modules/components/filter/filter-remote.service.coffee new file mode 100644 index 00000000..95435432 --- /dev/null +++ b/app/modules/components/filter/filter-remote.service.coffee @@ -0,0 +1,68 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: filter-utils.service.coffee +### + +generateHash = taiga.generateHash + +class FilterRemoteStorageService extends taiga.Service + @.$inject = [ + "$q", + "$tgUrls", + "$tgHttp" + ] + + constructor: (@q, @urls, @http) -> + + storeFilters: (projectId, myFilters, filtersHashSuffix) -> + deferred = @q.defer() + url = @urls.resolve("user-storage") + ns = "#{projectId}:#{filtersHashSuffix}" + hash = generateHash([projectId, ns]) + if _.isEmpty(myFilters) + promise = @http.delete("#{url}/#{hash}", {key: hash, value:myFilters}) + promise.then -> + deferred.resolve() + promise.then null, -> + deferred.reject() + else + promise = @http.put("#{url}/#{hash}", {key: hash, value:myFilters}) + promise.then (data) -> + deferred.resolve() + promise.then null, (data) => + innerPromise = @http.post("#{url}", {key: hash, value:myFilters}) + innerPromise.then -> + deferred.resolve() + innerPromise.then null, -> + deferred.reject() + return deferred.promise + + getFilters: (projectId, filtersHashSuffix) -> + deferred = @q.defer() + url = @urls.resolve("user-storage") + ns = "#{projectId}:#{filtersHashSuffix}" + hash = generateHash([projectId, ns]) + + promise = @http.get("#{url}/#{hash}") + promise.then (data) -> + deferred.resolve(data.data.value) + promise.then null, (data) -> + deferred.resolve({}) + + return deferred.promise + +angular.module("taigaComponents").service("tgFilterRemoteStorageService", FilterRemoteStorageService) diff --git a/app/modules/components/filter/filter-slide-down.directive.coffee b/app/modules/components/filter/filter-slide-down.directive.coffee new file mode 100644 index 00000000..d0f4dbaf --- /dev/null +++ b/app/modules/components/filter/filter-slide-down.directive.coffee @@ -0,0 +1,45 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: filter.-slide-down.controller.coffee +### + +FilterSlideDownDirective = () -> + link = (scope, el, attrs, ctrl) -> + filter = $('tg-filter') + + scope.$watch attrs.ngIf, (value) -> + if value + filter.find('.filter-list').hide() + + wrapperHeight = filter.height() + contentHeight = 0 + + filter.children().each () -> + contentHeight += $(this).outerHeight(true) + + $(el.context.nextSibling) + .css({ + "max-height": wrapperHeight - contentHeight, + "display": "block" + }) + + return { + priority: 900, + link: link + } + +angular.module('taigaComponents').directive("tgFilterSlideDown", [FilterSlideDownDirective]) diff --git a/app/modules/components/filter/filter.controller.coffee b/app/modules/components/filter/filter.controller.coffee new file mode 100644 index 00000000..0830f63d --- /dev/null +++ b/app/modules/components/filter/filter.controller.coffee @@ -0,0 +1,70 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: filter.controller.coffee +### + +class FilterController + @.$inject = [] + + constructor: () -> + @.opened = null + @.customFilterForm = false + @.customFilterName = '' + + toggleFilterCategory: (filterName) -> + if @.opened == filterName + @.opened = null + else + @.opened = filterName + + isOpen: (filterName) -> + return @.opened == filterName + + saveCustomFilter: () -> + @.onSaveCustomFilter({name: @.customFilterName}) + @.customFilterForm = false + @.opened = 'custom-filter' + @.customFilterName = '' + + changeQ: () -> + @.onChangeQ({q: @.q}) + + unselectFilter: (filter) -> + @.onRemoveFilter({filter: filter}) + + unselectFilter: (filter) -> + @.onRemoveFilter({filter: filter}) + + selectFilter: (filterCategory, filter) -> + filter = { + category: filterCategory + filter: filter + } + + @.onAddFilter({filter: filter}) + + removeCustomFilter: (filter) -> + @.onRemoveCustomFilter({filter: filter}) + + selectCustomFilter: (filter) -> + @.onSelectCustomFilter({filter: filter}) + + isFilterSelected: (filterCategory, filter) -> + return !!_.find @.selectedFilters, (it) -> + return filter.id == it.id && filterCategory.dataType == it.dataType + +angular.module('taigaComponents').controller('Filter', FilterController) diff --git a/app/modules/components/filter/filter.controller.spec.coffee b/app/modules/components/filter/filter.controller.spec.coffee new file mode 100644 index 00000000..bb9a9140 --- /dev/null +++ b/app/modules/components/filter/filter.controller.spec.coffee @@ -0,0 +1,87 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: filter.controller.spec.coffee +### + +describe "Filter", -> + $provide = null + $controller = null + mocks = {} + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _inject() + + beforeEach -> + module "taigaComponents" + + _setup() + + it "toggle filter category", () -> + ctrl = $controller("Filter") + + ctrl.toggleFilterCategory('filter1') + + expect(ctrl.opened).to.be.equal('filter1') + + ctrl.toggleFilterCategory('filter1') + + expect(ctrl.opened).to.be.null + + it "is filter open", () -> + ctrl = $controller("Filter") + ctrl.opened = 'filter1' + + isOpen = ctrl.isOpen('filter1') + + expect(isOpen).to.be.true + + it "save custom filter", () -> + ctrl = $controller("Filter") + ctrl.customFilterName = "custom-name" + ctrl.customFilterForm = true + ctrl.onSaveCustomFilter = sinon.spy() + + ctrl.saveCustomFilter() + + expect(ctrl.onSaveCustomFilter).to.have.been.calledWith({name: "custom-name"}) + expect(ctrl.customFilterForm).to.be.false + expect(ctrl.opened).to.be.equal('custom-filter') + expect(ctrl.customFilterName).to.be.equal('') + + it "is filter selected", () -> + ctrl = $controller("Filter") + ctrl.selectedFilters = [ + {id: 1, dataType: "1"}, + {id: 2, dataType: "2"}, + {id: 3, dataType: "3"} + ] + + filterCategory = {dataType: "x"} + filter = {id: 1} + isFilterSelected = ctrl.isFilterSelected(filterCategory, filter) + + expect(isFilterSelected).to.be.false + + filterCategory = {dataType: "1"} + filter = {id: 1} + isFilterSelected = ctrl.isFilterSelected(filterCategory, filter) + + expect(isFilterSelected).to.be.true diff --git a/app/modules/components/filter/filter.directive.coffee b/app/modules/components/filter/filter.directive.coffee new file mode 100644 index 00000000..92dca69f --- /dev/null +++ b/app/modules/components/filter/filter.directive.coffee @@ -0,0 +1,44 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: filter.directive.coffee +### + +FilterDirective = () -> + link = (scope, el, attrs, ctrl) -> + + return { + scope: { + onChangeQ: "&", + onAddFilter: "&", + onSelectCustomFilter: "&", + onRemoveFilter: "&", + onRemoveCustomFilter: "&", + onSaveCustomFilter: "&", + customFilters: "<", + q: "<", + filters: "<" + customFilters: "<" + selectedFilters: "<" + }, + bindToController: true, + controller: "Filter", + controllerAs: "vm", + templateUrl: 'components/filter/filter.html', + link: link + } + +angular.module('taigaComponents').directive("tgFilter", [FilterDirective]) diff --git a/app/modules/components/filter/filter.jade b/app/modules/components/filter/filter.jade new file mode 100644 index 00000000..db7f53b1 --- /dev/null +++ b/app/modules/components/filter/filter.jade @@ -0,0 +1,116 @@ +h1 + span.title(translate="COMMON.FILTERS.TITLE") + +form + fieldset + input.e2e-filter-q( + type="text", + placeholder="{{'COMMON.FILTERS.INPUT_PLACEHOLDER' | translate}}", + ng-model="vm.q" + ng-model-options="{ debounce: 200 }" + ng-change="vm.changeQ()" + ) + tg-svg.search-action( + svg-icon="icon-search", + title="{{'COMMON.FILTERS.TITLE_ACTION_SEARCH' | translate}}" + ) + +.filters-step-cat + .filters-applied + .single-filter.ng-animate-disabled(ng-repeat="it in vm.selectedFilters track by it.key") + span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}} + a.remove-filter.e2e-remove-filter( + ng-click="vm.unselectFilter(it)" + href="" + ) + tg-svg(svg-icon="icon-close") + + a.button.button-gray.save-filters.ng-animate-disabled.e2e-open-custom-filter-form( + ng-click="vm.customFilterForm = true" + ng-if="vm.selectedFilters.length && !vm.customFilterForm" + href="", + title="{{'COMMON.SAVE' | translate}}", + translate="COMMON.FILTERS.ACTION_SAVE_CUSTOM_FILTER" + ) + + form( + ng-if="vm.customFilterForm" + ng-submit="vm.saveCustomFilter()" + ) + input.my-filter-name.e2e-filter-name-input( + tg-autofocus + ng-model="vm.customFilterName" + type="text" + placeholder="{{'COMMON.FILTERS.PLACEHOLDER_FILTER_NAME' | translate}}" + ) + + .filters-cats + ul + li( + ng-class="{selected: vm.isOpen(filter.dataType)}" + ng-repeat="filter in vm.filters track by filter.dataType" + ng-if="!(filter.hideEmpty && filter.totalTaggedElements === 0)" + ) + + a.filters-cat-single.e2e-category( + ng-class="{selected: vm.isOpen(filter.dataType)}" + ng-click="vm.toggleFilterCategory(filter.dataType)" + href="" + title="{{::filter.title}}" + ) + span.title {{::filter.title}} + tg-svg.ng-animate-disabled( + ng-if="!vm.isOpen(filter.dataType)" + svg-icon="icon-arrow-right" + ) + tg-svg.ng-animate-disabled( + ng-if="vm.isOpen(filter.dataType)" + svg-icon="icon-arrow-down" + ) + + .filter-list( + ng-if="vm.isOpen(filter.dataType)", + tg-filter-slide-down + ) + .single-filter.ng-animate-disabled( + ng-repeat="it in filter.content" + ng-if="!vm.isFilterSelected(filter, it) && !(it.count == 0 && filter.hideEmpty)" + ng-click="vm.selectFilter(filter, it)" + ) + span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}} + span.number.e2e-filter-count(ng-if="it.count > 0") {{it.count}} + + li.custom-filters.e2e-custom-filters( + ng-class="{selected: vm.isOpen('custom-filter')}" + ng-if="vm.customFilters.length > 0" + ) + + a.filters-cat-single( + ng-class="{selected: vm.isOpen('custom-filter')}" + ng-click="vm.toggleFilterCategory('custom-filter')" + href="" + title="{{'COMMON.FILTERS.CATEGORIES.CUSTOM_FILTERS' | translate}}" + ) + span.title(translate="COMMON.FILTERS.CATEGORIES.CUSTOM_FILTERS") + tg-svg.ng-animate-disabled( + ng-if="!vm.isOpen('custom-filter')" + svg-icon="icon-arrow-right" + ) + tg-svg.ng-animate-disabled( + ng-if="vm.isOpen('custom-filter')" + svg-icon="icon-arrow-down" + ) + .filter-list( + ng-if="vm.isOpen('custom-filter')", + tg-filter-slide-down + ) + .single-filter.ng-animate-disabled.e2e-custom-filter( + ng-repeat="it in vm.customFilters" + ng-click="vm.selectCustomFilter(it)" + ) + span.name {{it.name}} + a.remove-filter.e2e-remove-custom-filter( + ng-click="vm.removeCustomFilter(it)" + href="" + ) + tg-svg(svg-icon="icon-trash") diff --git a/app/modules/components/filter/filter.scss b/app/modules/components/filter/filter.scss new file mode 100644 index 00000000..3782d855 --- /dev/null +++ b/app/modules/components/filter/filter.scss @@ -0,0 +1,150 @@ +tg-filter { + background-color: $mass-white; + box-shadow: 1px 1px 5px rgbag($primary, .2); + display: block; + left: 0; + min-height: 100%; + padding: 1rem 0; + position: absolute; + top: 0; + width: 260px; + z-index: 1; + .filters-applied { + padding: 0 1rem 1rem; + } + h1, + form { + padding: 0 1rem; + } + input { + background: $grayer; + color: $white; + @include placeholder { + color: $gray-light; + } + } + .search-action { + position: absolute; + right: .7rem; + top: .7rem; + } + &.ng-hide-add { + transform: translateX(0); + transition-duration: .5s; + } + &.ng-hide-add-active { + transform: translateX(-260px); + } + &.ng-hide-remove { + transform: translateX(-260px); + transition-duration: .5s; + } + &.ng-hide-remove-active { + transform: translateX(0); + } +} + +.filter-list { + overflow-y: auto; + padding: 1rem; +} + +.filters-step-cat { + margin-top: 2rem; +} + +.filters-cats { + ul { + margin-bottom: 0; + } + li { + border-bottom: 1px solid $gray-light; + text-transform: uppercase; + &.selected { + border-bottom: 0; + } + } + .custom-filters { + .title { + color: $primary; + } + } + .filters-cat-single { + align-items: center; + color: $grayer; + display: flex; + justify-content: space-between; + padding: .5rem .5rem .5rem 1.5rem; + transition: color .2s ease-in; + &:hover, + &.selected { + background-color: rgba(darken($whitish, 20%), 1); + color: $grayer; + transition: background-color .2s ease-in; + .icon { + opacity: 1; + transition: opacity .2s ease-in; + } + } + } + .icon-arrow-down { + fill: currentColor; + float: right; + height: .9rem; + opacity: 0; + transition: opacity .2s ease-in; + width: .9rem; + } +} + +.single-filter { + @include font-type(text); + @include clearfix; + align-items: center; + background: darken($whitish, 10%); // Fallback + cursor: pointer; + display: flex; + justify-content: space-between; + margin-bottom: .5rem; + opacity: .5; + padding-right: .5rem; + position: relative; + &:hover { + color: $grayer; + opacity: 1; + transition: opacity .2s linear; + } + &.selected, + &.active { + color: $grayer; + opacity: 1; + transition: opacity .2s linear; + } + .name, + .number { + padding: 8px 10px; + } + .name { + @include ellipsis(100%); + display: block; + width: 100%; + } + .number { + background: darken($whitish, 20%); // Fallback + position: absolute; + right: 0; + top: 0; + } + .remove-filter { + display: block; + svg { + fill: $gray; + transition: fill .2s linear; + } + &:hover { + svg { + fill: $red; + } + } + } +} diff --git a/app/modules/components/joy-ride/joy-ride.service.coffee b/app/modules/components/joy-ride/joy-ride.service.coffee index 396ec30d..bf22b54b 100644 --- a/app/modules/components/joy-ride/joy-ride.service.coffee +++ b/app/modules/components/joy-ride/joy-ride.service.coffee @@ -138,7 +138,7 @@ class JoyRideService extends taiga.Service if @checkPermissionsService.check('add_us') steps.push({ - element: '.icon-plus', + element: '.add-action', position: 'bottom', joyride: { title: @translate.instant('JOYRIDE.KANBAN.STEP3.TITLE') diff --git a/app/modules/components/kanban-board-zoom/kanban-board-zoom.directive.coffee b/app/modules/components/kanban-board-zoom/kanban-board-zoom.directive.coffee new file mode 100644 index 00000000..26e60a0f --- /dev/null +++ b/app/modules/components/kanban-board-zoom/kanban-board-zoom.directive.coffee @@ -0,0 +1,69 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: kanban-board-zoom.directive.coffee +### + +KanbanBoardZoomDirective = (storage, projectService) -> + link = (scope, el, attrs, ctrl) -> + scope.zoomIndex = storage.get("kanban_zoom") or 2 + scope.levels = 5 + + zooms = [ + ["ref"], + ["subject"], + ["owner", "tags", "extra_info", "unfold"], + ["attachments"], + ["related_tasks"] + ] + + getZoomView = (zoomIndex = 0) -> + if storage.get("kanban_zoom") != zoomIndex + storage.set("kanban_zoom", zoomIndex) + + return _.reduce zooms, (result, value, key) -> + if key <= zoomIndex + result = result.concat(value) + + return result + + scope.$watch 'zoomIndex', (zoomLevel) -> + zoom = getZoomView(zoomLevel) + scope.onZoomChange({zoomLevel: zoomLevel, zoom: zoom}) + + unwatch = scope.$watch () -> + return projectService.project + , (project) -> + if project + if project.get('my_permissions').indexOf("view_tasks") == -1 + scope.levels = 4 + unwatch() + + return { + scope: { + onZoomChange: "&" + }, + template: """ + + """, + link: link + } + +angular.module('taigaComponents').directive("tgKanbanBoardZoom", ["$tgStorage", "tgProjectService", KanbanBoardZoomDirective]) diff --git a/app/modules/components/project-menu/project-menu.controller.coffee b/app/modules/components/project-menu/project-menu.controller.coffee index 23fc3591..9ef67946 100644 --- a/app/modules/components/project-menu/project-menu.controller.coffee +++ b/app/modules/components/project-menu/project-menu.controller.coffee @@ -52,12 +52,16 @@ class ProjectMenuController _setMenuPermissions: () -> @.menu = Immutable.Map({ + epics: false, backlog: false, kanban: false, issues: false, wiki: false }) + if @.project.get("is_epics_activated") && @.project.get("my_permissions").indexOf("view_epics") != -1 + @.menu = @.menu.set("epics", true) + if @.project.get("is_backlog_activated") && @.project.get("my_permissions").indexOf("view_us") != -1 @.menu = @.menu.set("backlog", true) diff --git a/app/modules/components/project-menu/project-menu.controller.spec.coffee b/app/modules/components/project-menu/project-menu.controller.spec.coffee index 2e3e1829..65601c88 100644 --- a/app/modules/components/project-menu/project-menu.controller.spec.coffee +++ b/app/modules/components/project-menu/project-menu.controller.spec.coffee @@ -111,6 +111,7 @@ describe "ProjectMenu", -> menu = ctrl.menu.toJS() expect(menu).to.be.eql({ + epics: false, backlog: false, kanban: false, issues: false, @@ -119,11 +120,12 @@ describe "ProjectMenu", -> it "all options enabled", () -> project = Immutable.fromJS({ + is_epics_activated: true, is_backlog_activated: true, is_kanban_activated: true, is_issues_activated: true, is_wiki_activated: true, - my_permissions: ["view_us", "view_issues", "view_wiki_pages"] + my_permissions: ["view_epics", "view_us", "view_issues", "view_wiki_pages"] }) mocks.projectService.project = project @@ -136,6 +138,7 @@ describe "ProjectMenu", -> menu = ctrl.menu.toJS() expect(menu).to.be.eql({ + epics: true, backlog: true, kanban: true, issues: true, @@ -144,6 +147,7 @@ describe "ProjectMenu", -> it "all options disabled because the user doesn't have permissions", () -> project = Immutable.fromJS({ + is_epics_activated: true, is_backlog_activated: true, is_kanban_activated: true, is_issues_activated: true, @@ -161,6 +165,7 @@ describe "ProjectMenu", -> menu = ctrl.menu.toJS() expect(menu).to.be.eql({ + epics: false, backlog: false, kanban: false, issues: false, diff --git a/app/modules/components/project-menu/project-menu.jade b/app/modules/components/project-menu/project-menu.jade index ddb4e109..62fc2b9c 100644 --- a/app/modules/components/project-menu/project-menu.jade +++ b/app/modules/components/project-menu/project-menu.jade @@ -15,17 +15,32 @@ nav.menu( tg-svg(svg-icon="icon-search") span.helper(translate="PROJECT.SECTION.SEARCH") - li#nav-timeline - a( + li#nav-timeline + a( tg-nav="project:project=vm.project.get('slug')" ng-class="{active: vm.active == 'project-timeline'}" aria-label="{{'PROJECT.SECTION.TIMELINE' | translate}}" tabindex="2" - ) + ) tg-svg(svg-icon="icon-timeline") span.helper(translate="PROJECT.SECTION.TIMELINE") - li#nav-backlog(ng-if="vm.menu.get('backlog')") + li#nav-epics(ng-if="vm.menu.get('epics')") + a( + tg-nav="project-epics:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'epics'}" + aria-label="{{'EPICS.TITLE' | translate}}" + tabindex="2" + ) + tg-svg(svg-icon="icon-epics") + span.helper(translate="EPICS.TITLE") + + li#nav-backlog( + ng-if="vm.menu.get('backlog')" + ng-mouseover="backlogHover = true" + ng-mouseleave="backlogHover = false" + ng-init="backlogHover = false" + ) a( tg-nav="project-backlog:project=vm.project.get('slug')" ng-class="{active: vm.active == 'backlog'}" @@ -33,7 +48,14 @@ nav.menu( tabindex="2" ) tg-svg(svg-icon="icon-scrum") - span.helper(translate="PROJECT.SECTION.BACKLOG") + + span.backlog-sprints-menu(ng-show="backlogHover") + span(translate="PROJECT.SECTION.BACKLOG") + a( + tg-repeat="sprint in vm.project.get('milestones') track by sprint.get('id')" + ng-if="!sprint.get('closed')" + tg-nav="project-taskboard:project=vm.project.get('slug'),sprint=sprint.get('slug')" + ) {{::sprint.get('name')}} li#nav-kanban(ng-if="vm.menu.get('kanban')") a( diff --git a/app/modules/components/tags/components/add-tag-button.jade b/app/modules/components/tags/components/add-tag-button.jade new file mode 100644 index 00000000..88bf6bd6 --- /dev/null +++ b/app/modules/components/tags/components/add-tag-button.jade @@ -0,0 +1,11 @@ +a.add-tag-button.ng-animate-disabled.e2e-show-tag-input( + ng-if="!vm.addTag && vm.checkPermissions()" + href="#" + title="{{'COMMON.TAGS.ADD' | translate}}" + ng-click="vm.displayTagInput()" +) + tg-svg( + svg-icon="icon-add" + svg-title-translate="COMMON.TAGS.ADD" + ) + span.add-tag-text(translate="COMMON.TAGS.ADD") diff --git a/app/modules/components/tags/components/add-tag-input.jade b/app/modules/components/tags/components/add-tag-input.jade new file mode 100644 index 00000000..62b8cf1c --- /dev/null +++ b/app/modules/components/tags/components/add-tag-input.jade @@ -0,0 +1,33 @@ +.add-tag-input( + novalidate + ng-if="vm.addTag && vm.checkPermissions()" + tg-loading="vm.loadingAddTag" +) + input.tag-input.e2e-add-tag-input( + type="text" + placeholder="{{'COMMON.TAGS.PLACEHOLDER' | translate}}" + autofocus + ng-model="vm.newTag.name" + ng-model-options="{debounce: 200}" + ) + + tg-tags-dropdown( + ng-if="!vm.disableColorSelection" + ng-show="vm.newTag.name.length", + color-array="vm.colorArray", + tag="vm.newTag", + on-select-tag="vm.addNewTag(name, color)" + ) + + tg-color-selector( + ng-if="!vm.disableColorSelection" + on-select-color="vm.selectColor(color)" + is-color-required="false" + ) + + tg-svg.save( + ng-show="vm.newTag.name.length" + svg-icon="icon-save" + svg-title-translate="COMMON.TAGS.ADD" + ng-click="vm.addNewTag(vm.newTag.name, vm.newTag.color)" + ) diff --git a/app/modules/components/tags/components/add-tag.scss b/app/modules/components/tags/components/add-tag.scss new file mode 100644 index 00000000..bccccb5b --- /dev/null +++ b/app/modules/components/tags/components/add-tag.scss @@ -0,0 +1,59 @@ +$tag-input-width: 250px; + +.add-tag-input { + align-items: flex-start; + display: flex; + flex-grow: 0; + flex-shrink: 0; + position: relative; + width: $tag-input-width; + input { + border-color: $gray-light; + padding: 6px; + width: 14rem; + } + .save { + cursor: pointer; + display: inline-block; + fill: $grayer; + margin: .5rem 0 0 .5rem; + transition: .2s linear; + &:hover { + fill: $primary; + } + } + .tags-dropdown { + @include font-size(small); + background: $white; + border: 1px solid $gray-light; + border-top: 0; + box-shadow: 2px 2px 3px rgba($black, .2); + left: 0; + max-height: 20vh; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + top: 2.25rem; + width: 85%; + z-index: 99; + } + .tags-dropdown-option { + display: flex; + justify-content: space-between; + padding: .5rem; + } + .tags-dropdown-color { + height: 1rem; + width: 1rem; + } + li { + &:hover, + &.selected { + background: lighten($primary-light, 50%); + cursor: pointer; + transition: .2s; + transition-delay: .1s; + } + } +} diff --git a/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee b/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee new file mode 100644 index 00000000..c1386e2f --- /dev/null +++ b/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee @@ -0,0 +1,84 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.directive.coffee +### + +module = angular.module('taigaCommon') + +TagOptionDirective = () -> + select = (selected) -> + selected.addClass('selected') + + selectedPosition = selected.position().top + selected.outerHeight() + containerHeight = selected.parent().outerHeight() + + if selectedPosition > containerHeight + diff = selectedPosition - containerHeight + selected.parent().scrollTop(selected.parent().scrollTop() + diff) + else if selected.position().top < 0 + selected.parent().scrollTop(selected.parent().scrollTop() + selected.position().top) + + dispatch = (el, code, scope) -> + activeElement = el.find(".selected") + + # Key: down + if code == 40 + if not activeElement.length + select(el.find('li:first')) + else + next = activeElement.next('li') + if next.length + activeElement.removeClass('selected') + select(next) + # Key: up + else if code == 38 + if not activeElement.length + select(el.find('li:last')) + else + prev = activeElement.prev('li') + + if prev.length + activeElement.removeClass('selected') + select(prev) + + stop = -> + $(document).off(".tags-keyboard-navigation") + + link = (scope, el) -> + stop() + + $(document).on "keydown.tags-keyboard-navigation", (event) => + code = if event.keyCode then event.keyCode else event.which + + if code == 40 || code == 38 + event.preventDefault() + + dispatch(el, code, scope) + + scope.$on("$destroy", stop) + + return { + link: link, + templateUrl:"components/tags/tag-dropdown/tag-dropdown.html", + scope: { + onSelectTag: "&", + colorArray: "=", + tag: "=" + } + } + +module.directive("tgTagsDropdown", TagOptionDirective) diff --git a/app/modules/components/tags/tag-dropdown/tag-dropdown.jade b/app/modules/components/tags/tag-dropdown/tag-dropdown.jade new file mode 100644 index 00000000..e25ad740 --- /dev/null +++ b/app/modules/components/tags/tag-dropdown/tag-dropdown.jade @@ -0,0 +1,12 @@ +ul.tags-dropdown + li( + ng-repeat="tag in colorArray | filter: tag.name", + ng-click="onSelectTag({name: tag[0], color: tag[1]})" + ) + .tags-dropdown-option + span.tags-dropdown-name {{tag[0]}} + span.tags-dropdown-color( + ng-if="tag[1]" + ng-style="{'background': tag[1]}" + ng-title="tag[1]" + ) diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee new file mode 100644 index 00000000..dde598ea --- /dev/null +++ b/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee @@ -0,0 +1,61 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.controller.coffee +### + +trim = @.taiga.trim + +module = angular.module('taigaCommon') + +class TagLineCommonController + + @.$inject = [ + "tgTagLineService" + ] + + constructor: (@tagLineService) -> + @.disableColorSelection = false + @.newTag = {name: "", color: null} + @.colorArray = [] + @.addTag = false + + checkPermissions: () -> + return @tagLineService.checkPermissions(@.project.my_permissions, @.permissions) + + _createColorsArray: (projectTagColors) -> + @.colorArray = @tagLineService.createColorsArray(projectTagColors) + + displayTagInput: () -> + @.addTag = true + + addNewTag: (name, color) -> + @.newTag.name = "" + @.newTag.color = null + + return if not name.length + + if @.disableColorSelection + @.onAddTag({name: name, color: color}) if name.length + else + if @.project.tags_colors[name] + color = @.project.tags_colors[name] + @.onAddTag({name: name, color: color}) + + selectColor: (color) -> + @.newTag.color = color + +module.controller("TagLineCommonCtrl", TagLineCommonController) diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee new file mode 100644 index 00000000..188df081 --- /dev/null +++ b/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee @@ -0,0 +1,96 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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:tag-line-common.controller.spec.coffee +### + +describe "TagLineCommon", -> + provide = null + controller = null + TagLineCommonCtrl = null + mocks = {} + + _mockTgTagLineService = () -> + mocks.tgTagLineService = { + checkPermissions: sinon.stub() + createColorsArray: sinon.stub() + renderTags: sinon.stub() + } + + provide.value "tgTagLineService", mocks.tgTagLineService + + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgTagLineService() + return null + + beforeEach -> + module "taigaCommon" + + _mocks() + + inject ($controller) -> + controller = $controller + + TagLineCommonCtrl = controller "TagLineCommonCtrl" + TagLineCommonCtrl.tags = [] + TagLineCommonCtrl.colorArray = [] + TagLineCommonCtrl.addTag = false + + it "check permissions", () -> + TagLineCommonCtrl.project = { + } + TagLineCommonCtrl.project.my_permissions = [ + 'permission1', + 'permission2' + ] + TagLineCommonCtrl.permissions = 'permissions1' + + TagLineCommonCtrl.checkPermissions() + expect(mocks.tgTagLineService.checkPermissions).have.been.calledWith(TagLineCommonCtrl.project.my_permissions, TagLineCommonCtrl.permissions) + + it "create Colors Array", () -> + projectTagColors = 'string' + mocks.tgTagLineService.createColorsArray.withArgs(projectTagColors).returns(true) + TagLineCommonCtrl._createColorsArray(projectTagColors) + expect(TagLineCommonCtrl.colorArray).to.be.equal(true) + + it "display tag input", () -> + TagLineCommonCtrl.addTag = false + TagLineCommonCtrl.displayTagInput() + expect(TagLineCommonCtrl.addTag).to.be.true + + it "on add tag", () -> + TagLineCommonCtrl.loadingAddTag = true + tag = 'tag1' + tags = ['tag1', 'tag2'] + color = "CC0000" + + TagLineCommonCtrl.project = { + tags: ['tag1', 'tag2'], + tags_colors: ["#CC0000", "CCBB00"] + } + + TagLineCommonCtrl.onAddTag = sinon.spy() + TagLineCommonCtrl.newTag = {name: "11", color: "22"} + + TagLineCommonCtrl.addNewTag(tag, color) + + expect(TagLineCommonCtrl.onAddTag).have.been.calledWith({name: tag, color: color}) + expect(TagLineCommonCtrl.newTag.name).to.be.eql("") + expect(TagLineCommonCtrl.newTag.color).to.be.null diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee new file mode 100644 index 00000000..1fd9e8f6 --- /dev/null +++ b/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee @@ -0,0 +1,71 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.directive.coffee +### + +module = angular.module('taigaCommon') + +TagLineCommonDirective = () -> + link = (scope, el, attr, ctrl) -> + if !_.isUndefined(attr.disableColorSelection) + ctrl.disableColorSelection = true + + unwatch = scope.$watch "vm.project", (project) -> + return if !project || !Object.keys(project).length + + unwatch() + + if not ctrl.disableColorSelection + ctrl.colorArray = ctrl._createColorsArray(ctrl.project.tags_colors) + + el.on "keydown", ".tag-input", (event) -> + if event.keyCode == 27 + ctrl.addTag = false + + ctrl.newTag.name = "" + ctrl.newTag.color = "" + + event.stopPropagation() + else if event.keyCode == 13 + event.preventDefault() + + if el.find('.tags-dropdown .selected').length + tagName = $('.tags-dropdown .selected .tags-dropdown-name').text() + ctrl.addNewTag(tagName, null) + else + ctrl.addNewTag(ctrl.newTag.name, ctrl.newTag.color) + + scope.$apply() + + return { + link: link, + scope: { + permissions: "@", + loadingAddTag: "=", + loadingRemoveTag: "=", + tags: "=", + project: "=", + onAddTag: "&", + onDeleteTag: "&" + }, + templateUrl:"components/tags/tag-line-common/tag-line-common.html", + controller: "TagLineCommonCtrl", + controllerAs: "vm", + bindToController: true + } + +module.directive("tgTagLineCommon", TagLineCommonDirective) diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.jade b/app/modules/components/tags/tag-line-common/tag-line-common.jade new file mode 100644 index 00000000..e5c44ae3 --- /dev/null +++ b/app/modules/components/tags/tag-line-common/tag-line-common.jade @@ -0,0 +1,26 @@ +.tags-container + .tag( + ng-if="tag[1] && !vm.disableColorSelection" + ng-repeat="tag in vm.tags" + ng-style="{'border-left': '.3rem solid' + tag[1]}" + ) + tg-tag( + tag="tag" + loading-remove-tag="vm.loadingRemoveTag" + project="vm.project" + on-delete-tag="vm.onDeleteTag({tag: tag})" + has-permissions="{{vm.checkPermissions()}}" + ) + .tag( + ng-if="!tag[1] || vm.disableColorSelection" + ng-repeat="tag in vm.tags" + ) + tg-tag( + tag="tag" + loading-remove-tag="vm.loadingRemoveTag" + on-delete-tag="vm.onDeleteTag({tag: tag})" + has-permissions="{{vm.checkPermissions()}}" + ) + +include ../components/add-tag-button +include ../components/add-tag-input diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee new file mode 100644 index 00000000..5130cac6 --- /dev/null +++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee @@ -0,0 +1,88 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.controller.coffee +### + +trim = @.taiga.trim + +module = angular.module('taigaCommon') + +class TagLineController + + @.$inject = [ + "$rootScope", + "$tgConfirm", + "$tgQueueModelTransformation", + ] + + constructor: (@rootScope, @confirm, @modelTransform) -> + @.loadingAddTag = false + + onDeleteTag: (tag) -> + @.loadingRemoveTag = tag[0] + + onDeleteTagSuccess = (item) => + @rootScope.$broadcast("object:updated") + @.loadingRemoveTag = false + + return item + + onDeleteTagError = () => + @confirm.notify("error") + @.loadingRemoveTag = false + + tagName = trim(tag[0].toLowerCase()) + + transform = @modelTransform.save (item) -> + itemtags = _.clone(item.tags) + + _.remove itemtags, (tag) -> tag[0] == tagName + + item.tags = itemtags + + return item + + return transform.then(onDeleteTagSuccess, onDeleteTagError) + + onAddTag: (tag, color) -> + @.loadingAddTag = true + + onAddTagSuccess = (item) => + @rootScope.$broadcast("object:updated") #its a kind of magic. + @.addTag = false + @.loadingAddTag = false + + return item + + onAddTagError = () => + @.loadingAddTag = false + @confirm.notify("error") + + transform = @modelTransform.save (item) => + value = trim(tag.toLowerCase()) + + itemtags = _.clone(item.tags) + + itemtags.push([tag , color]) + + item.tags = itemtags + + return item + + return transform.then(onAddTagSuccess, onAddTagError) + +module.controller("TagLineCtrl", TagLineController) diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee new file mode 100644 index 00000000..8b323169 --- /dev/null +++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee @@ -0,0 +1,157 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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:tag-line-detail.controller.spec.coffee +### + +describe "TagLineDetail", -> + provide = null + controller = null + TagLineController = null + mocks = {} + + _mockRootScope = () -> + mocks.rootScope = { + $broadcast: sinon.stub() + } + + provide.value "$rootScope", mocks.rootScope + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgQueueModelTransformation = () -> + mocks.tgQueueModelTransformation = { + save: sinon.stub() + } + + provide.value "$tgQueueModelTransformation", mocks.tgQueueModelTransformation + + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockRootScope() + _mockTgConfirm() + _mockTgQueueModelTransformation() + + return null + + beforeEach -> + module "taigaCommon" + + _mocks() + + inject ($controller) -> + controller = $controller + + TagLineController = controller "TagLineCtrl" + + it "on delete tag success", (done) -> + tag = { + name: 'tag1' + } + tagName = tag.name + + item = { + tags: [ + ['tag1'], + ['tag2'], + ['tag3'] + ] + } + + mocks.tgQueueModelTransformation.save.callsArgWith(0, item) + mocks.tgQueueModelTransformation.save.promise().resolve(item) + + TagLineController.onDeleteTag(['tag1', '#000']).then (item) -> + expect(item.tags).to.be.eql([ + ['tag2'], + ['tag3'] + ]) + expect(TagLineController.loadingRemoveTag).to.be.false + expect(mocks.rootScope.$broadcast).to.be.calledWith("object:updated") + done() + + it "on delete tag error", (done) -> + mocks.tgQueueModelTransformation.save.promise().reject(new Error('error')) + + TagLineController.onDeleteTag(['tag1']).finally () -> + expect(TagLineController.loadingRemoveTag).to.be.false + expect(mocks.tgConfirm.notify).to.be.calledWith("error") + done() + + it "on add tag success", (done) -> + tag = 'tag1' + tagColor = '#eee' + + item = { + tags: [ + ['tag2'], + ['tag3'] + ] + } + + mockPromise = mocks.tgQueueModelTransformation.save.promise() + + mocks.tgQueueModelTransformation.save.callsArgWith(0, item) + promise = TagLineController.onAddTag(tag, tagColor) + + expect(TagLineController.loadingAddTag).to.be.true + + mockPromise.resolve(item) + + promise.then (item) -> + expect(item.tags).to.be.eql([ + ['tag2'], + ['tag3'], + ['tag1', '#eee'] + ]) + + expect(mocks.rootScope.$broadcast).to.be.calledWith("object:updated") + expect(TagLineController.addTag).to.be.false + expect(TagLineController.loadingAddTag).to.be.false + + done() + + it "on add tag error", (done) -> + tag = 'tag1' + tagColor = '#eee' + + item = { + tags: [ + ['tag2'], + ['tag3'] + ] + } + + mockPromise = mocks.tgQueueModelTransformation.save.promise() + + mocks.tgQueueModelTransformation.save.callsArgWith(0, item) + promise = TagLineController.onAddTag(tag, tagColor) + + expect(TagLineController.loadingAddTag).to.be.true + + mockPromise.reject(new Error('error')) + + promise.then (item) -> + expect(TagLineController.loadingAddTag).to.be.false + expect(mocks.tgConfirm.notify).to.be.calledWith("error") + done() diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee new file mode 100644 index 00000000..50976ab6 --- /dev/null +++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.directive.coffee +### + +module = angular.module('taigaCommon') + +TagLineDirective = () -> + return { + scope: { + item: "=", + permissions: "@", + project: "=" + }, + templateUrl:"components/tags/tag-line-detail/tag-line-detail.html", + controller: "TagLineCtrl", + controllerAs: "vm", + bindToController: true + } + +module.directive("tgTagLine", TagLineDirective) diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.jade b/app/modules/components/tags/tag-line-detail/tag-line-detail.jade new file mode 100644 index 00000000..3a688aa6 --- /dev/null +++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.jade @@ -0,0 +1,9 @@ +tg-tag-line-common.tags-block( + project="vm.project" + tags="vm.item.tags" + permissions="{{vm.permissions}}" + loading-remove-tag="vm.loadingRemoveTag" + loading-add-tag="vm.loadingAddTag" + on-add-tag="vm.onAddTag(name, color)" + on-delete-tag="vm.onDeleteTag(tag)" +) diff --git a/app/modules/components/tags/tag-line.scss b/app/modules/components/tags/tag-line.scss new file mode 100644 index 00000000..292458b0 --- /dev/null +++ b/app/modules/components/tags/tag-line.scss @@ -0,0 +1,22 @@ +.tags-block { + align-content: center; + display: flex; + flex-wrap: wrap; +} + +.add-tag-button { + color: $gray-light; + cursor: pointer; + display: inline-block; + &:hover { + color: $primary-light; + } + .icon-add { + @include svg-size(.9rem); + fill: currentColor; + margin: .5rem .25rem 0 0; + } + .add-tag-text { + @include font-size(small); + } +} diff --git a/app/modules/components/tags/tag-line.service.coffee b/app/modules/components/tags/tag-line.service.coffee new file mode 100644 index 00000000..f8257b8a --- /dev/null +++ b/app/modules/components/tags/tag-line.service.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.service.coffee +### + +module = angular.module('taigaCommon') + +class TagLineService extends taiga.Service + @.$inject = [] + + constructor: () -> + + checkPermissions: (myPermissions, projectPermissions) -> + return _.includes(myPermissions, projectPermissions) + + createColorsArray: (projectTagColors) -> + return _.map(projectTagColors, (index, value) -> + return [value, index] + ) + +module.service("tgTagLineService", TagLineService) diff --git a/app/modules/components/tags/tag/tag.directive.coffee b/app/modules/components/tags/tag/tag.directive.coffee new file mode 100644 index 00000000..ccd366f1 --- /dev/null +++ b/app/modules/components/tags/tag/tag.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: tag-line.directive.coffee +### + +module = angular.module('taigaCommon') + +TagDirective = () -> + return { + templateUrl:"components/tags/tag/tag.html", + scope: { + tag: "<", + loadingRemoveTag: "<", + onDeleteTag: "&", + hasPermissions: "@" + } + } + +module.directive("tgTag", TagDirective) diff --git a/app/modules/components/tags/tag/tag.jade b/app/modules/components/tags/tag/tag.jade new file mode 100644 index 00000000..acd7692a --- /dev/null +++ b/app/modules/components/tags/tag/tag.jade @@ -0,0 +1,8 @@ +span {{ tag[0] }} +tg-svg.icon-close.e2e-delete-tag( + ng-if="hasPermissions" + svg-icon="icon-close" + svg-title-translate="COMMON.TAG.DELETE" + ng-click="onDeleteTag(tag)" + tg-loading="loadingRemoveTag == tag[0]" +) diff --git a/app/modules/components/tags/tag/tag.scss b/app/modules/components/tags/tag/tag.scss new file mode 100644 index 00000000..185940e8 --- /dev/null +++ b/app/modules/components/tags/tag/tag.scss @@ -0,0 +1,21 @@ +.tag { + @include font-type(light); + @include font-size(small); + background: $mass-white; + border-radius: 0 5px 5px 0; + color: $grayer; + display: inline-block; + margin: 0 .5rem .5rem 0; + padding: .5rem; + text-align: center; + .icon-close { + @include svg-size(.7rem); + cursor: pointer; + fill: $red-light; + margin-left: .25rem; + } + .loading-spinner { + height: 1rem; + width: 1rem; + } +} diff --git a/app/modules/components/taskboard-zoom/taskboard-zoom.directive.coffee b/app/modules/components/taskboard-zoom/taskboard-zoom.directive.coffee new file mode 100644 index 00000000..7db01d43 --- /dev/null +++ b/app/modules/components/taskboard-zoom/taskboard-zoom.directive.coffee @@ -0,0 +1,62 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: taskboard-zoom.directive.coffee +### + +TaskboardZoomDirective = (storage) -> + link = (scope, el, attrs, ctrl) -> + scope.zoomIndex = storage.get("taskboard_zoom") or 2 + + scope.levels = 4 + + zooms = [ + ["ref"], + ["subject"], + ["owner", "tags", "extra_info", "unfold"], + ["attachments"], + ["related_tasks"] + ] + + getZoomView = (zoomIndex = 0) -> + if storage.get("taskboard_zoom") != zoomIndex + storage.set("taskboard_zoom", zoomIndex) + + return _.reduce zooms, (result, value, key) -> + if key <= zoomIndex + result = result.concat(value) + + return result + + scope.$watch 'zoomIndex', (zoomLevel) -> + zoom = getZoomView(zoomLevel) + scope.onZoomChange({zoomLevel: zoomLevel, zoom: zoom}) + + return { + scope: { + onZoomChange: "&" + }, + template: """ + + """, + link: link + } + +angular.module('taigaComponents').directive("tgTaskboardZoom", ["$tgStorage", TaskboardZoomDirective]) diff --git a/app/modules/components/tribe-button/tribe-button.directive.coffee b/app/modules/components/tribe-button/tribe-button.directive.coffee index 4f961f06..03925461 100644 --- a/app/modules/components/tribe-button/tribe-button.directive.coffee +++ b/app/modules/components/tribe-button/tribe-button.directive.coffee @@ -17,11 +17,16 @@ # File: tribe-button.directive.coffee ### -TribeButtonDirective = (configService) -> +TribeButtonDirective = (configService, locationService) -> link = (scope, el, attrs) -> scope.vm = {} scope.vm.tribeHost = configService.config.tribeHost + scope.vm.url = "#{locationService.protocol()}://#{locationService.host()}" + if (locationService.protocol() == "http" and locationService.port() != 80) + scope.vm.url = "#{scope.vm.url}:#{locationService.port()}" + else if (locationService.protocol() == "https" and locationService.port() != 443) + scope.vm.url = "#{scope.vm.url}:#{locationService.port()}" return { scope: {usId: "=", projectSlug: "="} @@ -31,7 +36,7 @@ TribeButtonDirective = (configService) -> } TribeButtonDirective.$inject = [ - "$tgConfig" + "$tgConfig", "$tgLocation" ] angular.module("taigaComponents").directive("tgTribeButton", TribeButtonDirective) diff --git a/app/modules/components/tribe-button/tribe-button.jade b/app/modules/components/tribe-button/tribe-button.jade index 1d1cad90..7d2b09e2 100644 --- a/app/modules/components/tribe-button/tribe-button.jade +++ b/app/modules/components/tribe-button/tribe-button.jade @@ -1,5 +1,5 @@ a.button-tribe( - ng-href="{{::vm.tribeHost}}/gigs/import-from-taiga?project={{projectSlug}}&us={{usId}}", + ng-href="{{::vm.tribeHost}}/taiga-integration/receive?url={{::vm.url}}&project={{projectSlug}}&us={{usId}}", title="{{ 'US.TRIBE.PUBLISH' | translate }}" target="_blank" ) diff --git a/app/modules/components/vote-button/vote-button.controller.spec.coffee b/app/modules/components/vote-button/vote-button.controller.spec.coffee index 241210e0..a0201853 100644 --- a/app/modules/components/vote-button/vote-button.controller.spec.coffee +++ b/app/modules/components/vote-button/vote-button.controller.spec.coffee @@ -67,13 +67,13 @@ describe "VoteButton", -> promise = ctrl.toggleVote() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.onUpvote.resolve() promise.finally () -> expect(mocks.onUpvote).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() @@ -90,12 +90,12 @@ describe "VoteButton", -> promise = ctrl.toggleVote() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.onDownvote.resolve() promise.finally () -> expect(mocks.onDownvote).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() diff --git a/app/modules/components/vote-button/vote-button.jade b/app/modules/components/vote-button/vote-button.jade index 3ae180ac..1259c10d 100644 --- a/app/modules/components/vote-button/vote-button.jade +++ b/app/modules/components/vote-button/vote-button.jade @@ -8,17 +8,15 @@ a.vote-inner( ng-mouseover="vm.showTextWhenMouseIsOver()" ng-mouseleave="vm.showTextWhenMouseIsLeave()" ) - span.track-icon - tg-svg(svg-icon="icon-upvote") - span.track-button-counter( + tg-svg(svg-icon="icon-upvote") + span( title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.total_voters||0}:'messageformat' }}", tg-loading="vm.loading" ) {{ vm.item.total_voters }} //- Anonymous user button span.vote-inner(ng-if="::!vm.user") - span.track-icon - tg-svg(svg-icon="icon-watch") - span.track-button-counter( + tg-svg(svg-icon="icon-upvote") + span( title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.total_voters||0}:'messageformat' }}" ) {{ ::vm.item.total_voters }} diff --git a/app/modules/components/watch-button/watch-button.controller.coffee b/app/modules/components/watch-button/watch-button.controller.coffee index 99514424..e7cbae9c 100644 --- a/app/modules/components/watch-button/watch-button.controller.coffee +++ b/app/modules/components/watch-button/watch-button.controller.coffee @@ -45,7 +45,8 @@ class WatchButtonController perms = { userstories: 'modify_us', issues: 'modify_issue', - tasks: 'modify_task' + tasks: 'modify_task', + epics: 'modify_epic' } return perms[name] diff --git a/app/modules/components/watch-button/watch-button.controller.spec.coffee b/app/modules/components/watch-button/watch-button.controller.spec.coffee index 41e95efb..77247468 100644 --- a/app/modules/components/watch-button/watch-button.controller.spec.coffee +++ b/app/modules/components/watch-button/watch-button.controller.spec.coffee @@ -68,13 +68,13 @@ describe "WatchButton", -> promise = ctrl.toggleWatch() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.onWatch.resolve() promise.finally () -> expect(mocks.onWatch).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() @@ -91,13 +91,13 @@ describe "WatchButton", -> promise = ctrl.toggleWatch() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.onUnwatch.resolve() promise.finally () -> expect(mocks.onUnwatch).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() diff --git a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee index ebecd4ab..6b00be7e 100644 --- a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee +++ b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee @@ -44,7 +44,6 @@ class DiscoverHomeOrderByController orderBy: (type) -> @.currentOrderBy = type @.is_open = false - @.onChange({orderBy: @.currentOrderBy}) angular.module("taigaDiscover").controller("DiscoverHomeOrderBy", DiscoverHomeOrderByController) diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee index 36b5dd4e..fbde731f 100644 --- a/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee +++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee @@ -57,8 +57,8 @@ describe "DiscoverSearchBarController", -> ctrl.selectFilter('text') - expect(mocks.discoverProjectsService.fetchStats).to.have.been.called; - expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'text', q: 'query'})); + expect(mocks.discoverProjectsService.fetchStats).to.have.been.called + expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'text', q: 'query'})) it "submit filter", () -> ctrl = $controller("DiscoverSearchBar") @@ -68,5 +68,5 @@ describe "DiscoverSearchBarController", -> ctrl.submitFilter() - expect(mocks.discoverProjectsService.fetchStats).to.have.been.called; - expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'all', q: 'query'})); + expect(mocks.discoverProjectsService.fetchStats).to.have.been.called + expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'all', q: 'query'})) diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss index 2633faed..3a88d3d2 100644 --- a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss +++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss @@ -35,9 +35,9 @@ } .discover-search-subfilter { - @include arrow('bottom', $whitish, $whitish, 1, 8); + @include arrow('bottom', $mass-white, $mass-white, 1, 8); align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; position: relative; @@ -80,7 +80,7 @@ } &.active { background: $primary-light; - color: $whitish; + color: $white; } } } diff --git a/app/modules/discover/discover-search/discover-search.jade b/app/modules/discover/discover-search/discover-search.jade index 1290ab83..e15ce454 100644 --- a/app/modules/discover/discover-search/discover-search.jade +++ b/app/modules/discover/discover-search/discover-search.jade @@ -6,9 +6,9 @@ div(tg-discover-search) on-change="vm.onChangeFilter(filter, q)" ) - .empty-discover-results(ng-if="!vm.searchResult.size && !vm.loadingGlobal && !vm.loadingList") + .empty-large(ng-if="!vm.searchResult.size && !vm.loadingGlobal && !vm.loadingList") img( - src="/#{v}/images/issues-empty.png", + src="/#{v}/images/empty/empty_tex.png", alt="{{ DISCOVER.EMPTY | translate }}" ) p.title(translate="DISCOVER.EMPTY") diff --git a/app/modules/discover/discover-search/discover-search.scss b/app/modules/discover/discover-search/discover-search.scss index fa6eee89..8e50ad3c 100644 --- a/app/modules/discover/discover-search/discover-search.scss +++ b/app/modules/discover/discover-search/discover-search.scss @@ -105,18 +105,3 @@ } } } - -.empty-discover-results { - @include centered; - margin-top: 4rem; - text-align: center; - img { - margin-bottom: 1rem; - } - .title { - @include font-size(large); - @include font-type(light); - margin: 0; - text-transform: uppercase; - } -} diff --git a/app/modules/discover/services/discover-projects.service.spec.coffee b/app/modules/discover/services/discover-projects.service.spec.coffee index 75c13e43..971502c7 100644 --- a/app/modules/discover/services/discover-projects.service.spec.coffee +++ b/app/modules/discover/services/discover-projects.service.spec.coffee @@ -174,6 +174,6 @@ describe "tgDiscoverProjectsService", -> expect(result).to.have.length(5) - expect(result[4].decorate).to.be.ok; + expect(result[4].decorate).to.be.ok done() diff --git a/app/modules/epics/create-epic/create-epic.controller.coffee b/app/modules/epics/create-epic/create-epic.controller.coffee new file mode 100644 index 00000000..8ab9ef15 --- /dev/null +++ b/app/modules/epics/create-epic/create-epic.controller.coffee @@ -0,0 +1,82 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: create-epic.controller.coffee +### + +taiga = @.taiga +trim = taiga.trim +getRandomDefaultColor = taiga.getRandomDefaultColor + + +class CreateEpicController + @.$inject = [ + "$tgConfirm" + "tgProjectService", + "tgEpicsService" + ] + + constructor: (@confirm, @projectService, @epicsService) -> + # NOTE: To use Checksley setFormErrors() and validateForm() + # are defined in the directive. + + # NOTE: We use project as no inmutable object to make + # the code compatible with the old code + @.project = @projectService.project.toJS() + + @.newEpic = { + color: getRandomDefaultColor() + status: @.project.default_epic_status + tags: [] + } + @.attachments = Immutable.List() + + @.loading = false + + createEpic: () -> + return if not @.validateForm() + + @.loading = true + + @epicsService.createEpic(@.newEpic, @.attachments) + .then (response) => # On success + @.onCreateEpic() + @.loading = false + .catch (response) => # On error + @.loading = false + @.setFormErrors(response.data) + if response.data._error_message + @confirm.notify("error", response.data._error_message) + + # Color selector + selectColor: (color) -> + @.newEpic.color = color + + # Tags + addTag: (name, color) -> + name = trim(name.toLowerCase()) + + if not _.find(@.newEpic.tags, (it) -> it[0] == name) + @.newEpic.tags.push([name, color]) + + deleteTag: (tag) -> + _.remove @.newEpic.tags, (it) -> it[0] == tag[0] + + # Attachments + addAttachment: (attachment) -> + @.attachments.push(attachment) + +angular.module("taigaEpics").controller("CreateEpicCtrl", CreateEpicController) diff --git a/app/modules/epics/create-epic/create-epic.controller.spec.coffee b/app/modules/epics/create-epic/create-epic.controller.spec.coffee new file mode 100644 index 00000000..cd588888 --- /dev/null +++ b/app/modules/epics/create-epic/create-epic.controller.spec.coffee @@ -0,0 +1,108 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epic-row.controller.spec.coffee +### + +describe "EpicRow", -> + createEpicCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgProjectService = () -> + mocks.tgProjectService = { + project: { + toJS: sinon.stub() + } + } + provide.value "tgProjectService", mocks.tgProjectService + + _mockTgEpicsService = () -> + mocks.tgEpicsService = { + createEpic: sinon.stub() + } + provide.value "tgEpicsService", mocks.tgEpicsService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgConfirm() + _mockTgProjectService() + _mockTgEpicsService() + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "create Epic with invalid form", () -> + mocks.tgProjectService.project.toJS.withArgs().returns( + {id: 1, default_epic_status: 1} + ) + + data = { + validateForm: sinon.stub() + setFormErrors: sinon.stub() + onCreateEpic: sinon.stub() + } + createEpicCtrl = controller "CreateEpicCtrl", null, data + createEpicCtrl.attachments = Immutable.List([{file: "file1"}, {file: "file2"}]) + + data.validateForm.withArgs().returns(false) + + createEpicCtrl.createEpic() + + expect(data.validateForm).have.been.called + expect(mocks.tgEpicsService.createEpic).not.have.been.called + + it "create Epic successfully", (done) -> + mocks.tgProjectService.project.toJS.withArgs().returns( + {id: 1, default_epic_status: 1} + ) + + data = { + validateForm: sinon.stub() + setFormErrors: sinon.stub() + onCreateEpic: sinon.stub() + } + createEpicCtrl = controller "CreateEpicCtrl", null, data + createEpicCtrl.attachments = Immutable.List([{file: "file1"}, {file: "file2"}]) + + data.validateForm.withArgs().returns(true) + mocks.tgEpicsService.createEpic + .withArgs( + createEpicCtrl.newEpic, + createEpicCtrl.attachments) + .promise() + .resolve( + {data: {id: 1, project: 1}} + ) + + createEpicCtrl.createEpic().then () -> + expect(data.validateForm).have.been.called + expect(createEpicCtrl.onCreateEpic).have.been.called + done() diff --git a/app/modules/epics/create-epic/create-epic.directive.coffee b/app/modules/epics/create-epic/create-epic.directive.coffee new file mode 100644 index 00000000..fda1525d --- /dev/null +++ b/app/modules/epics/create-epic/create-epic.directive.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: create-epic.directive.coffee +### + +CreateEpicDirective = () -> + link = (scope, el, attrs, ctrl) -> + form = el.find("form").checksley() + + ctrl.validateForm = => + return form.validate() + + ctrl.setFormErrors = (errors) => + form.setErrors(errors) + + return { + link: link, + templateUrl:"epics/create-epic/create-epic.html", + controller: "CreateEpicCtrl", + controllerAs: "vm", + bindToController: { + onCreateEpic: '&' + }, + scope: {} + } + +angular.module('taigaEpics').directive("tgCreateEpic", CreateEpicDirective) diff --git a/app/modules/epics/create-epic/create-epic.jade b/app/modules/epics/create-epic/create-epic.jade new file mode 100644 index 00000000..e93a63fe --- /dev/null +++ b/app/modules/epics/create-epic/create-epic.jade @@ -0,0 +1,101 @@ +tg-lightbox-close + +.create-epic-container + h2.title(translate="EPICS.CREATE.TITLE") + form( + ng-submit="vm.createEpic()" + ) + .subject-container + .color-selector + fieldset + tg-color-selector( + is-color-required="true" + init-color="vm.newEpic.color" + on-select-color="vm.selectColor(color)" + ) + .subject + fieldset + input.e2e-create-epic-subject( + type="text" + name="subject" + ng-model="vm.newEpic.subject" + tg-auto-select + placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}" + data-required="true" + data-maxlength="140" + ) + fieldset + select.e2e-create-epic-status( + id="epic-status" + name="status" + ng-model="vm.newEpic.status" + ng-options="s.id as s.name for s in vm.project.epic_statuses | orderBy:'order'" + ) + fieldset.tags-block + tg-tag-line-common( + project="vm.project" + tags="vm.newEpic.tags" + permissions="add_epic" + on-add-tag="vm.addTag(name, color)" + on-delete-tag="vm.deleteTag(tag)" + ) + fieldset + textarea.e2e-create-epic-description( + ng-attr-placeholder="{{'EPICS.CREATE.PLACEHOLDER_DESCRIPTION' | translate}}" + ng-model="vm.newEpic.description" + ) + fieldset + tg-attachments-simple( + attachments="vm.attachments" + on-add="vm.addAttachment(attachment)" + ) + .settings + fieldset.team-requirement + input( + type="checkbox" + name="team_requirement" + ng-model="vm.newEpic.team_requirement" + id="team-requirement" + ) + label.requirement.trans-button.e2e-create-epic-team-requirement( + for="team-requirement" + translate="EPICS.CREATE.TEAM_REQUIREMENT" + ) + fieldset.client-requirement + input( + type="checkbox" + name="client_requirement" + ng-model="vm.newEpic.client_requirement" + id="client-requirement" + ) + label.requirement.trans-button.e2e-create-epic-client-requirement( + for="client-requirement" + translate="EPICS.CREATE.CLIENT_REQUIREMENT" + ) + fieldset + input( + type="checkbox" + name="blocked" + ng-model="vm.newEpic.is_blocked" + id="blocked" + ng-click="displayBlockedReason = !displayBlockedReason" + ) + label.requirement.trans-button.blocked.e2e-create-epic-blocked( + for="blocked" + translate="EPICS.CREATE.BLOCKED" + ) + fieldset(ng-if="displayBlockedReason") + input.e2e-create-epic-blocked-note( + type="text" + name="blocked_note" + maxlength="140" + ng-model="vm.newEpic.blocked_note" + placeholder="{{'EPICS.CREATE.BLOCKED_NOTE_PLACEHOLDER' | translate}}" + ) + fieldset + button.button-green.create-epic-button.e2e-create-epic-button( + type="submit" + tg-loading="vm.loading" + title="{{ 'EPICS.CREATE.CREATE_EPIC' | translate }}" + translate="EPICS.CREATE.CREATE_EPIC" + ) diff --git a/app/modules/epics/create-epic/create-epic.scss b/app/modules/epics/create-epic/create-epic.scss new file mode 100644 index 00000000..e778e047 --- /dev/null +++ b/app/modules/epics/create-epic/create-epic.scss @@ -0,0 +1,75 @@ +.lightbox-create-epic { + align-items: center; + display: flex; + justify-content: center; + opacity: 1; + .create-epic-container { + max-width: 700px; + width: 90%; + } + .subject-container { + align-items: center; + display: flex; + .subject { + padding-left: 1rem; + width: 100%; + } + } + .attachments { + margin-bottom: 0; + } + .settings { + display: flex; + justify-content: center; + fieldset { + margin-right: .5rem; + &:hover { + color: $white; + transition: all .2s ease-in; + transition-delay: .2s; + } + &:last-child { + margin: 0; + } + } + input { + display: none; + &:checked+label { + background: $primary; + border: 1px solid $primary; + color: $white; + } + &:checked+.blocked { + background: $red; + border: 1px solid $red; + color: $white; + } + } + } + label { + @include font-size(small); + background: $mass-white; + border: 1px solid $gray-light; + color: $gray-light; + cursor: pointer; + display: block; + padding: .5rem 3rem; + text-transform: none; + transition: all .2s ease-in; + &:hover { + background: $primary-light; + border: 1px solid $primary; + color: $white; + } + &.blocked { + &:hover { + background: $red-light; + border: 1px solid $red; + } + } + } + .create-epic-button { + display: block; + width: 100%; + } +} diff --git a/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee b/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee new file mode 100644 index 00000000..76c8ca24 --- /dev/null +++ b/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee @@ -0,0 +1,82 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epics-table.controller.coffee +### + +class EpicRowController + @.$inject = [ + "$tgConfirm", + "tgProjectService", + "tgEpicsService" + ] + + constructor: (@confirm, @projectService, @epicsService) -> + @.displayUserStories = false + @.displayAssignedTo = false + @.displayStatusList = false + @.loadingStatus = false + + # NOTE: We use project as no inmutable object to make + # the code compatible with the old code + @.project = @projectService.project.toJS() + + @._calculateProgressBar() + + _calculateProgressBar: () -> + if @.epic.getIn(['status_extra_info', 'is_closed']) == true + @.percentage = "100%" + else + opened = @.epic.getIn(['user_stories_counts', 'opened']) + closed = @.epic.getIn(['user_stories_counts', 'closed']) + total = opened + closed + if total == 0 + @.percentage = "0%" + else + @.percentage = "#{closed * 100 / total}%" + + canEditEpics: () -> + return @projectService.hasPermission("modify_epic") + + toggleUserStoryList: () -> + if !@.displayUserStories + @epicsService.listRelatedUserStories(@.epic) + .then (userStories) => + @.epicStories = userStories + @.displayUserStories = true + .catch => + @confirm.notify('error') + else + @.displayUserStories = false + + updateStatus: (statusId) -> + @.displayStatusList = false + @.loadingStatus = true + return @epicsService.updateEpicStatus(@.epic, statusId) + .catch () => + @confirm.notify('error') + .finally () => + @.loadingStatus = false + + updateAssignedTo: (member) -> + @.assignLoader = true + return @epicsService.updateEpicAssignedTo(@.epic, member?.id or null) + .catch () => + @confirm.notify('error') + .then () => + @.assignLoader = false + +angular.module("taigaEpics").controller("EpicRowCtrl", EpicRowController) diff --git a/app/modules/epics/dashboard/epic-row/epic-row.controller.spec.coffee b/app/modules/epics/dashboard/epic-row/epic-row.controller.spec.coffee new file mode 100644 index 00000000..f3df7c64 --- /dev/null +++ b/app/modules/epics/dashboard/epic-row/epic-row.controller.spec.coffee @@ -0,0 +1,198 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epic-row.controller.spec.coffee +### + +describe "EpicRow", -> + epicRowCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgProjectService = () -> + mocks.tgProjectService = { + project: { + toJS: sinon.stub() + } + } + provide.value "tgProjectService", mocks.tgProjectService + + _mockTgEpicsService = () -> + mocks.tgEpicsService = { + listRelatedUserStories: sinon.stub() + updateEpicStatus: sinon.stub() + updateEpicAssignedTo: sinon.stub() + } + provide.value "tgEpicsService", mocks.tgEpicsService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgConfirm() + _mockTgProjectService() + _mockTgEpicsService() + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "calculate progress bar in open US", () -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + status_extra_info: { + is_closed: false + } + user_stories_counts: { + opened: 10, + closed: 10 + } + }) + } + + expect(ctrl.percentage).to.be.equal("50%") + + it "calculate progress bar in zero US", () -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + status_extra_info: { + is_closed: false + } + user_stories_counts: { + opened: 0, + closed: 0 + } + }) + } + expect(ctrl.percentage).to.be.equal("0%") + + it "calculate progress bar in zero US", () -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + status_extra_info: { + is_closed: true + } + }) + } + expect(ctrl.percentage).to.be.equal("100%") + + it "Update Epic Status Success", (done) -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + id: 1 + version: 1 + }) + } + + statusId = 1 + + promise = mocks.tgEpicsService.updateEpicStatus + .withArgs(ctrl.epic, statusId) + .promise() + .resolve() + + ctrl.loadingStatus = true + ctrl.displayStatusList = true + + ctrl.updateStatus(statusId).then () -> + expect(ctrl.loadingStatus).to.be.false + expect(ctrl.displayStatusList).to.be.false + done() + + it "Update Epic Status Error", (done) -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + id: 1 + version: 1 + }) + } + + statusId = 1 + + promise = mocks.tgEpicsService.updateEpicStatus + .withArgs(ctrl.epic, statusId) + .promise() + .reject(new Error('error')) + + ctrl.updateStatus(statusId).then () -> + expect(ctrl.loadingStatus).to.be.false + expect(ctrl.displayStatusList).to.be.false + expect(mocks.tgConfirm.notify).have.been.calledWith('error') + done() + + it "display User Stories", (done) -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + id: 1 + }) + } + + ctrl.displayUserStories = false + + data = Immutable.List() + + promise = mocks.tgEpicsService.listRelatedUserStories + .withArgs(ctrl.epic) + .promise() + .resolve(data) + + ctrl.toggleUserStoryList().then () -> + expect(ctrl.displayUserStories).to.be.true + expect(ctrl.epicStories).is.equal(data) + done() + + it "display User Stories error", (done) -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + id: 1 + }) + } + + ctrl.displayUserStories = false + + promise = mocks.tgEpicsService.listRelatedUserStories + .withArgs(ctrl.epic) + .promise() + .reject(new Error('error')) + + ctrl.toggleUserStoryList().then () -> + expect(ctrl.displayUserStories).to.be.false + expect(mocks.tgConfirm.notify).have.been.calledWith('error') + done() + + it "display User Stories error", -> + ctrl = controller "EpicRowCtrl", null, { + epic: Immutable.fromJS({ + id: 1 + }) + } + + ctrl.displayUserStories = true + + ctrl.toggleUserStoryList() + + expect(ctrl.displayUserStories).to.be.false diff --git a/app/modules/epics/dashboard/epic-row/epic-row.directive.coffee b/app/modules/epics/dashboard/epic-row/epic-row.directive.coffee new file mode 100644 index 00000000..cf85a32b --- /dev/null +++ b/app/modules/epics/dashboard/epic-row/epic-row.directive.coffee @@ -0,0 +1,32 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-table.directive.coffee +### + +EpicRowDirective = () -> + return { + templateUrl:"epics/dashboard/epic-row/epic-row.html", + controller: "EpicRowCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + epic: '=', + column: '=' + } + } + +angular.module('taigaEpics').directive("tgEpicRow", EpicRowDirective) diff --git a/app/modules/epics/dashboard/epic-row/epic-row.jade b/app/modules/epics/dashboard/epic-row/epic-row.jade new file mode 100644 index 00000000..7497be1c --- /dev/null +++ b/app/modules/epics/dashboard/epic-row/epic-row.jade @@ -0,0 +1,86 @@ + +.epic-row.e2e-epic-row( + ng-class="{'is-blocked': vm.epic.get('is_blocked'), 'is-closed': vm.epic.get('is_closed'), 'unfold': vm.displayUserStories, 'not-empty': vm.epic.getIn(['user_stories_counts', 'opened']) || vm.epic.getIn(['user_stories_counts', 'closed'])}" + ng-click="vm.toggleUserStoryList()" +) + tg-svg.icon-drag( + svg-icon="icon-drag" + ng-if="vm.canEditEpics()" + ) + + .vote( + ng-if="vm.column.votes" + ng-class="{'is-voter': vm.epic.get('is_voter')}" + ) + tg-svg(svg-icon='icon-upvote') + span {{::vm.epic.get('total_voters')}} + + .name(ng-if="vm.column.name") + - var hash = "#"; + a( + tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')" + ng-attr-title="{{::vm.epic.get('subject')}}" + ) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}} + span.epic-pill( + ng-style="::{'background-color': vm.epic.get('color')}" + translate="EPICS.EPIC" + ) + tg-svg( + svg-icon="icon-arrow-down" + ng-if="vm.epic.getIn(['user_stories_counts', 'opened']) || vm.epic.getIn(['user_stories_counts', 'closed'])" + ) + + .project(ng-if="vm.column.project") + + .sprint(ng-if="vm.column.sprint") + + .assigned.e2e-assigned-to( + ng-if="vm.column.assigned" + tg-loading="vm.assignLoader" + ) + tg-assigned-to-component( + assigned-to="vm.epic.get('assigned_to_extra_info')" + project="vm.project" + on-remove-assigned="vm.updateAssignedTo()" + on-assign-to="vm.updateAssignedTo(member)" + tg-isolate-click + ) + + .status( + ng-if="vm.column.status && !vm.canEditEpics()" + ) + span {{vm.epic.getIn(['status_extra_info', 'name'])}} + .status( + ng-if="vm.column.status && vm.canEditEpics()" + ng-mouseleave="vm.displayStatusList = false" + tg-isolate-click + ) + button( + ng-click="vm.displayStatusList = true" + ng-style="{'color': vm.epic.getIn(['status_extra_info', 'color'])}" + tg-loading="vm.loadingStatus" + ) + span.e2e-epic-status {{vm.epic.getIn(['status_extra_info', 'name'])}} + tg-svg( + svg-icon="icon-arrow-down" + ) + + ul.epic-statuses(ng-if="vm.displayStatusList") + li.e2e-edit-epic-status( + ng-repeat="status in vm.project.epic_statuses | orderBy:'order'" + ng-click="vm.updateStatus(status.id)" + ) {{status.name}} + + .progress(ng-if="vm.column.progress") + .progress-bar + .progress-status( + ng-if="::vm.percentage" + ng-style="{'width':vm.percentage}" + ) + +.epic-stories-wrapper(ng-if="vm.displayUserStories && vm.epicStories") + .epic-story(tg-repeat="story in vm.epicStories track by story.get('id')") + tg-story-row.e2e-story( + story="story" + column="vm.column" + ) diff --git a/app/modules/epics/dashboard/epic-row/epic-row.scss b/app/modules/epics/dashboard/epic-row/epic-row.scss new file mode 100644 index 00000000..0f7205a0 --- /dev/null +++ b/app/modules/epics/dashboard/epic-row/epic-row.scss @@ -0,0 +1,135 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + +.epic-row { + @include epics-table; + @include font-size(small); + align-items: center; + background: $white; + border-bottom: 1px solid $whitish; + cursor: move; + display: flex; + transition: background .2s; + &:hover { + background: rgba($primary-light, .05); + .icon-drag { + opacity: 1; + } + } + &.not-empty { + cursor: pointer; + } + &.is-blocked { + background: rgba($red-light, .5); + } + &.is-closed { + .name a { + color: lighten($gray-light, 15%); + text-decoration: line-through; + } + } + &.unfold { + .name { + .icon { + transform: rotate(0deg); + } + } + } + .name { + .icon { + transform: rotate(180deg); + transition: all .2s; + } + } + .icon-drag { + @include svg-size(.75rem); + cursor: move; + fill: $whitish; + opacity: 0; + transition: opacity .1s; + } + .epic-pill { + @include font-type(light); + @include font-size(xsmall); + background: $grayer; + border-radius: .25rem; + color: $white; + margin: 0 .5rem; + padding: .1rem .25rem; + } + .status { + cursor: pointer; + position: relative; + button { + background: none; + } + } + .icon-arrow-down { + @include svg-size(.7rem); + fill: $gray-light; + margin-left: .1rem; + } + .progress-bar, + .progress-status { + height: 1.5rem; + left: 0; + position: absolute; + top: .25rem; + } + .progress-bar { + background: $mass-white; + max-width: 40vw; + padding-right: 1rem; + width: 100%; + } + .progress-status { + background: $primary-light; + width: 10vw; + } + .vote { + color: $gray; + &.is-voter { + color: $primary-light; + fill: $primary-light; + } + } + .assigned { + img { + width: 40px; + } + } + .icon-upvote { + @include svg-size(.75rem); + fill: $gray; + margin-right: .25rem; + vertical-align: middle; + } + .is-unassigned { + color: $gray-light; + } + .epic-statuses { + @include font-type(light); + @include font-size(small); + background: rgba($blackish, .9); + border-bottom: 1px solid $grayer; + box-shadow: 3px 3px 2px rgba($black, .1); + color: $white; + left: 0; + list-style-type: none; + margin: 0; + position: absolute; + text-align: left; + top: 2.5rem; + width: 200px; + z-index: 99; + &:last-child { + border: 0; + } + li { + padding: .5rem; + &:hover { + color: $primary-light; + transition: color .3s linear; + } + } + } +} diff --git a/app/modules/epics/dashboard/epics-dashboard.controller.coffee b/app/modules/epics/dashboard/epics-dashboard.controller.coffee new file mode 100644 index 00000000..040a1d47 --- /dev/null +++ b/app/modules/epics/dashboard/epics-dashboard.controller.coffee @@ -0,0 +1,86 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epics.dashboard.controller.coffee +### + +taiga = @.taiga + + +class EpicsDashboardController + @.$inject = [ + "$routeParams", + "tgErrorHandlingService", + "tgLightboxFactory", + "lightboxService", + "$tgConfirm", + "tgProjectService", + "tgEpicsService", + "tgAppMetaService", + "$translate" + ] + + constructor: (@params, @errorHandlingService, @lightboxFactory, @lightboxService, + @confirm, @projectService, @epicsService, @appMetaService, @translate) -> + + @.sectionName = "EPICS.SECTION_NAME" + + taiga.defineImmutableProperty @, 'project', () => return @projectService.project + taiga.defineImmutableProperty @, 'epics', () => return @epicsService.epics + + @appMetaService.setfn @._setMeta.bind(this) + + _setMeta: () -> + return null if !@.project + + ctx = { + projectName: @.project.get("name") + projectDescription: @.project.get("description") + } + + return { + title: @translate.instant("EPICS.PAGE_TITLE", ctx) + description: @translate.instant("EPICS.PAGE_DESCRIPTION", ctx) + } + + loadInitialData: () -> + @epicsService.clear() + return @projectService.setProjectBySlug(@params.pslug) + .then () => + if not @projectService.isEpicsDashboardEnabled() + return @errorHandlingService.notFound() + if not @projectService.hasPermission("view_epics") + return @errorHandlingService.permissionDenied() + + return @epicsService.fetchEpics() + + canCreateEpics: () -> + return @projectService.hasPermission("add_epic") + + onCreateEpic: () -> + onCreateEpic = () => + @lightboxService.closeAll() + @confirm.notify("success") + return # To prevent error https://docs.angularjs.org/error/$parse/isecdom?p0=onCreateEpic() + + @lightboxFactory.create('tg-create-epic', { + "class": "lightbox lightbox-create-epic open" + "on-create-epic": "onCreateEpic()" + }, { + "onCreateEpic": onCreateEpic.bind(this) + }) + +angular.module("taigaEpics").controller("EpicsDashboardCtrl", EpicsDashboardController) diff --git a/app/modules/epics/dashboard/epics-dashboard.controller.spec.coffee b/app/modules/epics/dashboard/epics-dashboard.controller.spec.coffee new file mode 100644 index 00000000..66f1f11a --- /dev/null +++ b/app/modules/epics/dashboard/epics-dashboard.controller.spec.coffee @@ -0,0 +1,184 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epic-row.controller.spec.coffee +### + +describe "EpicsDashboard", -> + provide = null + controller = null + mocks = {} + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgProjectService = () -> + mocks.tgProjectService = { + setProjectBySlug: sinon.stub() + hasPermission: sinon.stub() + isEpicsDashboardEnabled: sinon.stub() + project: Immutable.Map({ + "name": "testing name" + "description": "testing description" + }) + } + provide.value "tgProjectService", mocks.tgProjectService + + _mockTgEpicsService = () -> + mocks.tgEpicsService = { + clear: sinon.stub() + fetchEpics: sinon.stub() + } + provide.value "tgEpicsService", mocks.tgEpicsService + + _mockRouteParams = () -> + mocks.routeParams = { + pslug: sinon.stub() + } + + provide.value "$routeParams", mocks.routeParams + + _mockTgErrorHandlingService = () -> + mocks.tgErrorHandlingService = { + permissionDenied: sinon.stub() + notFound: sinon.stub() + } + + provide.value "tgErrorHandlingService", mocks.tgErrorHandlingService + + _mockTgLightboxFactory = () -> + mocks.tgLightboxFactory = { + create: sinon.stub() + } + + provide.value "tgLightboxFactory", mocks.tgLightboxFactory + + _mockLightboxService = () -> + mocks.lightboxService = { + closeAll: sinon.stub() + } + + provide.value "lightboxService", mocks.lightboxService + + _mockTgAppMetaService = () -> + mocks.tgAppMetaService = { + setfn: sinon.stub() + } + + provide.value "tgAppMetaService", mocks.tgAppMetaService + + _mockTranslate = () -> + mocks.translate = sinon.stub() + + provide.value "$translate", mocks.translate + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgConfirm() + _mockTgProjectService() + _mockTgEpicsService() + _mockRouteParams() + _mockTgErrorHandlingService() + _mockTgLightboxFactory() + _mockLightboxService() + _mockTgAppMetaService() + _mockTranslate() + + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "metada is set", () -> + ctrl = controller("EpicsDashboardCtrl") + expect(mocks.tgAppMetaService.setfn).have.been.called + + it "load data because epics panel is enabled and user has permissions", (done) -> + ctrl = controller("EpicsDashboardCtrl") + + mocks.tgProjectService.setProjectBySlug + .promise() + .resolve("ok") + mocks.tgProjectService.hasPermission + .returns(true) + mocks.tgProjectService.isEpicsDashboardEnabled + .returns(true) + + ctrl.loadInitialData().then () -> + expect(mocks.tgErrorHandlingService.permissionDenied).not.have.been.called + expect(mocks.tgErrorHandlingService.notFound).not.have.been.called + expect(mocks.tgEpicsService.fetchEpics).have.been.called + done() + + it "not load data because epics panel is not enabled", (done) -> + ctrl = controller("EpicsDashboardCtrl") + + mocks.tgProjectService.setProjectBySlug + .promise() + .resolve("ok") + mocks.tgProjectService.hasPermission + .returns(true) + mocks.tgProjectService.isEpicsDashboardEnabled + .returns(false) + + ctrl.loadInitialData().then () -> + expect(mocks.tgErrorHandlingService.permissionDenied).not.have.been.called + expect(mocks.tgErrorHandlingService.notFound).have.been.called + expect(mocks.tgEpicsService.fetchEpics).not.have.been.called + done() + + it "not load data because user has not permissions", (done) -> + ctrl = controller("EpicsDashboardCtrl") + + mocks.tgProjectService.setProjectBySlug + .promise() + .resolve("ok") + mocks.tgProjectService.hasPermission + .returns(false) + mocks.tgProjectService.isEpicsDashboardEnabled + .returns(true) + + ctrl.loadInitialData().then () -> + expect(mocks.tgErrorHandlingService.permissionDenied).have.been.called + expect(mocks.tgErrorHandlingService.notFound).not.have.been.called + expect(mocks.tgEpicsService.fetchEpics).not.have.been.called + done() + + it "not load data because epics panel is not enabled and user has not permissions", (done) -> + ctrl = controller("EpicsDashboardCtrl") + + mocks.tgProjectService.setProjectBySlug + .promise() + .resolve("ok") + mocks.tgProjectService.hasPermission + .returns(false) + mocks.tgProjectService.isEpicsDashboardEnabled + .returns(false) + + ctrl.loadInitialData().then () -> + expect(mocks.tgErrorHandlingService.permissionDenied).not.have.been.called + expect(mocks.tgErrorHandlingService.notFound).have.been.called + expect(mocks.tgEpicsService.fetchEpics).not.have.been.called + done() diff --git a/app/modules/epics/dashboard/epics-dashboard.jade b/app/modules/epics/dashboard/epics-dashboard.jade new file mode 100644 index 00000000..46f751a3 --- /dev/null +++ b/app/modules/epics/dashboard/epics-dashboard.jade @@ -0,0 +1,39 @@ +.wrapper(ng-init="vm.loadInitialData()") + tg-project-menu + section.main.epics(role="main") + header.header-with-actions + h1( + tg-main-title + project-name="vm.project.get('name')" + i18n-section-name="{{vm.sectionName}}" + ) + .action-buttons(ng-if="vm.epics.size && vm.canCreateEpics()") + button.button-green.e2e-create-epic( + translate="EPICS.DASHBOARD.ADD" + title="{{ EPICS.DASHBOARD.ADD_TITLE | translate }}", + ng-click="vm.onCreateEpic()" + ) + + tg-epics-table( + ng-if="vm.epics.size" + ) + + section.empty-epics.empty-large(ng-if="!vm.epics.size") + img( + src="/#{v}/images/empty/empty_des.png" + ng-title="EPICS.EMPTY.HELP | translate" + ) + h1.title(translate="EPICS.EMPTY.TITLE") + p(translate="EPICS.EMPTY.EXPLANATION") + a( + translate="EPICS.EMPTY.HELP" + href="https://tree.taiga.io/support/epics/what-is-an-epic/" + target="_blank" + ng-title="EPICS.EMPTY.HELP | translate" + ) + button.create-epic.button-green( + ng-if="vm.canCreateEpics()" + translate="EPICS.DASHBOARD.ADD" + title="{{ EPICS.DASHBOARD.ADD_TITLE | translate }}" + ng-click="vm.onCreateEpic()" + ) diff --git a/app/modules/epics/dashboard/epics-dashboard.scss b/app/modules/epics/dashboard/epics-dashboard.scss new file mode 100644 index 00000000..159d1671 --- /dev/null +++ b/app/modules/epics/dashboard/epics-dashboard.scss @@ -0,0 +1,8 @@ +.empty-epics { + text-align: center; + a { + color: $primary; + display: block; + margin-bottom: 2rem; + } +} diff --git a/app/modules/epics/dashboard/epics-sortable/epics-sortable.directive.coffee b/app/modules/epics/dashboard/epics-sortable/epics-sortable.directive.coffee new file mode 100644 index 00000000..53063281 --- /dev/null +++ b/app/modules/epics/dashboard/epics-sortable/epics-sortable.directive.coffee @@ -0,0 +1,64 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-sortable.directive.coffee +### + +EpicsSortableDirective = ($parse, projectService) -> + link = (scope, el, attrs) -> + return if not projectService.hasPermission("modify_epic") + + callback = $parse(attrs.tgEpicsSortable) + + drake = dragula([el[0]], { + copySortSource: false + copy: false + mirrorContainer: el[0] + moves: (item) -> + return $(item).is('div.epics-table-body-row') + }) + + drake.on 'dragend', (item) -> + itemEl = $(item) + + epic = itemEl.scope().epic + newIndex = itemEl.index() + + scope.$apply () -> + callback(scope, {epic: epic, newIndex: newIndex}) + + scroll = autoScroll(window, { + margin: 20, + pixels: 30, + scrollWhenOutside: true, + autoScroll: () -> + return this.down && drake.dragging + }) + + scope.$on "$destroy", -> + el.off() + drake.destroy() + + return { + link: link + } + +EpicsSortableDirective.$inject = [ + "$parse", + "tgProjectService" +] + +angular.module("taigaComponents").directive("tgEpicsSortable", EpicsSortableDirective) diff --git a/app/modules/epics/dashboard/epics-table/epics-table.controller.coffee b/app/modules/epics/dashboard/epics-table/epics-table.controller.coffee new file mode 100644 index 00000000..4934b4d9 --- /dev/null +++ b/app/modules/epics/dashboard/epics-table/epics-table.controller.coffee @@ -0,0 +1,60 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epics-table.controller.coffee +### + +taiga = @.taiga + + +class EpicsTableController + @.$inject = [ + "$tgConfirm", + "tgEpicsService", + "$timeout" + ] + + constructor: (@confirm, @epicsService, @timeout) -> + @.displayOptions = false + @.displayVotes = true + @.column = { + votes: true, + name: true, + project: true, + sprint: true, + assigned: true, + status: true, + progress: true + } + + taiga.defineImmutableProperty @, 'epics', () => return @epicsService.epics + + toggleEpicTableOptions: () -> + @.displayOptions = !@.displayOptions + + reorderEpic: (epic, newIndex) -> + @epicsService.reorderEpic(epic, newIndex) + .then null, () => # on error + @confirm.notify("error") + + hoverEpicTableOption: () -> + if @.timer + @timeout.cancel(@.timer) + + hideEpicTableOption: () -> + return @.timer = @timeout (=> @.displayOptions = false), 400 + +angular.module("taigaEpics").controller("EpicsTableCtrl", EpicsTableController) diff --git a/app/modules/epics/dashboard/epics-table/epics-table.controller.spec.coffee b/app/modules/epics/dashboard/epics-table/epics-table.controller.spec.coffee new file mode 100644 index 00000000..cdd83c6c --- /dev/null +++ b/app/modules/epics/dashboard/epics-table/epics-table.controller.spec.coffee @@ -0,0 +1,57 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epic-row.controller.spec.coffee +### + +describe "EpicTable", -> + epicTableCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgConfirm = () -> + mocks.tgConfirm = { + notify: sinon.stub() + } + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgEpicsService = () -> + mocks.tgEpicsService = { + createEpic: sinon.stub() + } + provide.value "tgEpicsService", mocks.tgEpicsService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgConfirm() + _mockTgEpicsService() + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "toggle table options", () -> + epicTableCtrl = controller "EpicsTableCtrl" + epicTableCtrl.displayOptions = true + epicTableCtrl.toggleEpicTableOptions() + expect(epicTableCtrl.displayOptions).to.be.false diff --git a/app/modules/epics/dashboard/epics-table/epics-table.directive.coffee b/app/modules/epics/dashboard/epics-table/epics-table.directive.coffee new file mode 100644 index 00000000..f072a3e4 --- /dev/null +++ b/app/modules/epics/dashboard/epics-table/epics-table.directive.coffee @@ -0,0 +1,29 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-table.directive.coffee +### + +EpicsTableDirective = () -> + return { + templateUrl:"epics/dashboard/epics-table/epics-table.html", + controller: "EpicsTableCtrl", + controllerAs: "vm", + scope: {} + } + + +angular.module('taigaEpics').directive("tgEpicsTable", EpicsTableDirective) diff --git a/app/modules/epics/dashboard/epics-table/epics-table.jade b/app/modules/epics/dashboard/epics-table/epics-table.jade new file mode 100644 index 00000000..fe99fcbd --- /dev/null +++ b/app/modules/epics/dashboard/epics-table/epics-table.jade @@ -0,0 +1,97 @@ +mixin epicSwitch(name, model) + div.check + input.activate-input( + id= name + name= name + type="checkbox" + ng-checked= model + ng-model= model + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") + +.epics-table.e2e-epic-table + .epics-table-header.e2e-epics-table-header + .vote( + translate="EPICS.TABLE.VOTES" + ng-if="vm.column.votes" + ) + .name( + translate="EPICS.TABLE.NAME" + ) + .project( + translate="EPICS.TABLE.PROJECT" + ng-if="vm.column.project" + ) + .sprint( + translate="EPICS.TABLE.SPRINT" + ng-if="vm.column.sprint" + ) + .assigned( + translate="EPICS.TABLE.ASSIGNED_TO" + ng-if="vm.column.assigned" + ) + .status( + translate="EPICS.TABLE.STATUS" + ng-if="vm.column.status" + ) + .progress( + translate="EPICS.TABLE.PROGRESS" + ng-if="vm.column.progress" + ) + .epics-table-options-wrapper( + ng-mouseleave="vm.hideEpicTableOption()" + ) + button.epics-table-option-button.e2e-epics-column-button(ng-click="vm.displayOptions = true") + span(translate="EPICS.TABLE.VIEW_OPTIONS") + tg-svg(svg-icon="icon-arrow-down") + form.epics-table-dropdown.e2e-epics-column-dropdown( + ng-show="vm.displayOptions" + ng-mouseenter="vm.keepEpicTableOption()" + ) + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.VOTES" + for="epicSwitch-votes" + ) + +epicSwitch('switch-votes', 'vm.column.votes') + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.PROJECT" + for="switch-project" + ) + +epicSwitch('switch-project', 'vm.column.project') + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.SPRINT" + for="switch-sprint" + ) + +epicSwitch('switch-sprint', 'vm.column.sprint') + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.ASSIGNED_TO" + for="switch-assigned" + ) + +epicSwitch('switch-assigned', 'vm.column.assigned') + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.STATUS" + for="switch-status" + ) + +epicSwitch('switch-status', 'vm.column.status') + .fieldset + label.epics-table-options-vote( + translate="EPICS.TABLE.PROGRESS" + for="switch-progress" + ) + +epicSwitch('switch-progress', 'vm.column.progress') + .epics-table-body(tg-epics-sortable="vm.reorderEpic(epic, newIndex)") + .epics-table-body-row( + tg-repeat="epic in vm.epics track by epic.get('id')" + tg-bind-scope + ) + tg-epic-row.e2e-epic( + epic="epic" + column="vm.column" + ) diff --git a/app/modules/epics/dashboard/epics-table/epics-table.scss b/app/modules/epics/dashboard/epics-table/epics-table.scss new file mode 100644 index 00000000..8814fbc0 --- /dev/null +++ b/app/modules/epics/dashboard/epics-table/epics-table.scss @@ -0,0 +1,64 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + +.epics-table { + margin-top: 2rem; +} + +.epics-table-header { + @include epics-table; + @include font-type(bold); + border-bottom: 1px solid $gray-light; + display: flex; + padding: .5rem; + position: relative; + .project, + .assigned { + padding: 1rem .5rem; + } +} + +.epics-table-options-wrapper { + bottom: 1rem; + position: absolute; + right: .5rem; +} + +.epics-table-option-button { + @include font-type(light); + @include font-size(small); + background: none; + .icon { + @include svg-size(.7rem); + } +} + +.epics-table-dropdown { + background: $white; + border-bottom: 1px solid rgba($black, .1); + border-left: 1px solid rgba($black, .1); + border-right: 1px solid rgba($black, .1); + box-shadow: 3px 3px 2px rgba($black, .1); + padding: .5rem; + position: absolute; + right: 0; + top: 1.3rem; + width: 250px; + z-index: 99; + &.ng-hide-remove { + animation: dropdownFade .2s; + } + &.ng-hide-add { + animation: dropdownFade .2s reverse; + } + .fieldset { + @include font-size(small); + border-bottom: 1px solid $whitish; + color: $gray-light; + display: flex; + justify-content: space-between; + padding: .5rem 0; + &:last-child { + border: 0; + } + } +} diff --git a/app/modules/epics/dashboard/story-row/story-row.controller.coffee b/app/modules/epics/dashboard/story-row/story-row.controller.coffee new file mode 100644 index 00000000..ce959248 --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.controller.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epics-table.controller.coffee +### + +module = angular.module("taigaEpics") + +class StoryRowController + @.$inject = [] + + constructor: () -> + @._calculateProgressBar() + + _calculateProgressBar: () -> + if @.story.get('is_closed') == true + @.percentage = "100%" + else + totalTasks = @.story.get('tasks').size + totalTasksCompleted = @.story.get('tasks').filter((it) -> it.get("is_closed")).size + if totalTasks == 0 + @.percentage = "0%" + else + @.percentage = "#{totalTasksCompleted * 100 / totalTasks}%" + +module.controller("StoryRowCtrl", StoryRowController) diff --git a/app/modules/epics/dashboard/story-row/story-row.controller.spec.coffee b/app/modules/epics/dashboard/story-row/story-row.controller.spec.coffee new file mode 100644 index 00000000..9f6fe6b5 --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.controller.spec.coffee @@ -0,0 +1,71 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: story-row.controller.spec.coffee +### + +describe "StoryRowCtrl", -> + controller = null + + beforeEach -> + module "taigaEpics" + + inject ($controller) -> + controller = $controller + + it "calculate percentage for some closed tasks", () -> + data = { + story: Immutable.fromJS( + tasks: [ + {is_closed: true}, + {is_closed: true}, + {is_closed: true}, + {is_closed: false}, + {is_closed: false}, + ] + ) + } + + ctrl = controller "StoryRowCtrl", null, data + expect(ctrl.percentage).to.be.equal("60%") + + it "calculate percentage for closed story", () -> + data = { + story: Immutable.fromJS( + tasks: [ + {is_closed: true}, + {is_closed: true}, + {is_closed: true}, + {is_closed: false}, + {is_closed: false}, + ] + is_closed: true + ) + } + + ctrl = controller "StoryRowCtrl", null, data + expect(ctrl.percentage).to.be.equal("100%") + + it "calculate percentage for closed story", () -> + data = { + story: Immutable.fromJS( + tasks: [] + ) + } + + ctrl = controller "StoryRowCtrl", null, data + expect(ctrl.percentage).to.be.equal("0%") + diff --git a/app/modules/epics/dashboard/story-row/story-row.directive.coffee b/app/modules/epics/dashboard/story-row/story-row.directive.coffee new file mode 100644 index 00000000..13195c0a --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.directive.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-table.directive.coffee +### + +module = angular.module('taigaEpics') + +StoryRowDirective = () -> + return { + templateUrl:"epics/dashboard/story-row/story-row.html", + controller: "StoryRowCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + story: '=', + column: '=' + } + } + +module.directive("tgStoryRow", StoryRowDirective) diff --git a/app/modules/epics/dashboard/story-row/story-row.jade b/app/modules/epics/dashboard/story-row/story-row.jade new file mode 100644 index 00000000..ae5bee27 --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.jade @@ -0,0 +1,47 @@ +.story-row( + ng-class="{'is-blocked': vm.story.get('is_blocked'), 'is-closed': vm.story.get('is_closed')}" +) + .vote( + ng-if="vm.column.votes" + ng-class="{'is-voter': vm.story.get('is_voter')}" + ) + tg-svg(svg-icon='icon-upvote') + span {{::vm.story.get('total_voters')}} + + .name(ng-if="vm.column.name") + - var hash = "#"; + a( + tg-nav="project-userstories-detail:project=vm.story.getIn(['project_extra_info', 'slug']),ref=vm.story.get('ref')" + ng-attr-title="{{::vm.story.get('subject')}}" + ) #{hash}{{::vm.story.get('ref')}} {{::vm.story.get('subject')}} + tg-belong-to-epics( + ng-if="vm.story.get('epics')" + format="pill" + epics="vm.story.get('epics')" + ) + .project( + ng-if="vm.column.project" + tg-nav="project:project=vm.story.getIn(['project_extra_info', 'slug'])" + ) + img( + tg-project-logo-small-src="::vm.story.get('project_extra_info')" + alt="{{::vm.story.getIn(['project_extra_info', 'name'])}}" + ) + .sprint(ng-if="vm.column.sprint") {{::vm.story.get('milestone_name')}} + .assigned(ng-if="vm.column.assigned && vm.story.get('assigned_to')") + img( + tg-avatar="vm.story.get('assigned_to_extra_info')" + alt="{{::vm.story.getIn(['assigned_to_extra_info', 'full_name_display'])}}" + ) + .assigned(ng-if="vm.column.assigned && !vm.story.get('assigned_to')") + img( + src="/#{v}/images/unnamed.png" + alt="{{EPICS.DASHBOARD.UNASSIGNED | translate}}" + ) + .status(ng-if="vm.column.status") {{vm.story.getIn(['status_extra_info', 'name'])}} + .progress(ng-if="vm.column.progress") + .progress-bar + .progress-status( + ng-if="::vm.percentage" + ng-style="{'width':vm.percentage}" + ) diff --git a/app/modules/epics/dashboard/story-row/story-row.scss b/app/modules/epics/dashboard/story-row/story-row.scss new file mode 100644 index 00000000..9ecc112c --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.scss @@ -0,0 +1,68 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + +.story-row { + @include font-size(small); + @include epics-table; + align-items: center; + background: $white; + border-bottom: 1px solid $whitish; + display: flex; + margin-left: 4rem; + transition: background .2s; + &:hover { + background: rgba($primary-light, .05); + } + &.is-blocked { + background: rgba($red-light, .5); + } + &.is-closed { + .name { + color: $gray-light; + text-decoration: line-through; + } + } + .name { + flex-basis: 17.5vw; + a { + cursor: pointer; + } + } + .progress-bar, + .progress-status { + height: 1.5rem; + left: 0; + position: absolute; + top: .25rem; + } + .progress-bar { + background: $mass-white; + max-width: 40vw; + width: 100%; + } + .progress-status { + background: $primary-light; + width: 10vw; + } + .vote { + color: $gray; + &.is-voter { + color: $primary-light; + fill: $primary-light; + } + } + .project { + cursor: pointer; + } + .project, + .assigned { + img { + width: 40px; + } + } + .icon-upvote { + @include svg-size(.75rem); + fill: $gray; + margin-right: .25rem; + vertical-align: middle; + } +} diff --git a/app/modules/epics/epics.service.coffee b/app/modules/epics/epics.service.coffee new file mode 100644 index 00000000..4095bbf1 --- /dev/null +++ b/app/modules/epics/epics.service.coffee @@ -0,0 +1,121 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics.service.coffee +### + +taiga = @.taiga + +class EpicsService + @.$inject = [ + 'tgProjectService', + 'tgAttachmentsService' + 'tgResources', + 'tgXhrErrorService' + ] + + constructor: (@projectService, @attachmentsService, @resources, @xhrError) -> + @._epics = Immutable.List() + taiga.defineImmutableProperty @, 'epics', () => return @._epics + + clear: () -> + @._epics = Immutable.List() + + fetchEpics: () -> + return @resources.epics.list(@projectService.project.get('id')) + .then (epics) => + @._epics = epics + .catch (xhr) => + @xhrError.response(xhr) + + listRelatedUserStories: (epic) -> + return @resources.userstories.listInEpic(epic.get('id')) + + createEpic: (epicData, attachments) -> + epicData.project = @projectService.project.get('id') + + return @resources.epics.post(epicData) + .then (epic) => + promises = _.map attachments.toJS(), (attachment) => + @attachmentsService.upload(attachment.file, epic.get('id'), epic.get('project'), 'epic') + + Promise.all(promises).then () => + @.fetchEpics() + + reorderEpic: (epic, newIndex) -> + withoutMoved = @.epics.filter (it) => it.get('id') != epic.get('id') + beforeDestination = withoutMoved.slice(0, newIndex) + + previous = beforeDestination.last() + newOrder = if !previous then 0 else previous.get('epics_order') + 1 + + previousWithTheSameOrder = beforeDestination.filter (it) => + it.get('epics_order') == previous.get('epics_order') + setOrders = _.fromPairs previousWithTheSameOrder.map((it) => + [it.get('id'), it.get('epics_order')] + ).toJS() + + data = { + epics_order: newOrder, + version: epic.get('version') + } + return @resources.epics.reorder(epic.get('id'), data, setOrders) + .then () => + @.fetchEpics() + + reorderRelatedUserstory: (epic, epicUserstories, userstory, newIndex) -> + withoutMoved = epicUserstories.filter (it) => it.get('id') != userstory.get('id') + beforeDestination = withoutMoved.slice(0, newIndex) + + previous = beforeDestination.last() + newOrder = if !previous then 0 else previous.get('epic_order') + 1 + + previousWithTheSameOrder = beforeDestination.filter (it) => + it.get('epic_order') == previous.get('epic_order') + setOrders = _.fromPairs previousWithTheSameOrder.map((it) => + [it.get('id'), it.get('epic_order')] + ).toJS() + + data = { + order: newOrder + } + epicId = epic.get('id') + userstoryId = userstory.get('id') + return @resources.epics.reorderRelatedUserstory(epicId, userstoryId, data, setOrders) + .then () => + return @.listRelatedUserStories(epic) + + updateEpicStatus: (epic, statusId) -> + data = { + status: statusId, + version: epic.get('version') + } + + return @resources.epics.patch(epic.get('id'), data) + .then () => + @.fetchEpics() + + updateEpicAssignedTo: (epic, userId) -> + data = { + assigned_to: userId, + version: epic.get('version') + } + + return @resources.epics.patch(epic.get('id'), data) + .then () => + @.fetchEpics() + +angular.module('taigaEpics').service('tgEpicsService', EpicsService) diff --git a/app/modules/epics/epics.service.spec.coffee b/app/modules/epics/epics.service.spec.coffee new file mode 100644 index 00000000..58efa075 --- /dev/null +++ b/app/modules/epics/epics.service.spec.coffee @@ -0,0 +1,233 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics.service.spec.coffee +### + +describe "tgEpicsService", -> + epicsService = provide = null + mocks = {} + + _mockTgProjectService = () -> + mocks.tgProjectService = { + project: Immutable.Map({ + "id": 1 + }) + } + + provide.value "tgProjectService", mocks.tgProjectService + + _mockTgAttachmentsService = () -> + mocks.tgAttachmentsService = { + upload: sinon.stub() + } + + provide.value "tgAttachmentsService", mocks.tgAttachmentsService + + _mockTgResources = () -> + mocks.tgResources = { + epics: { + list: sinon.stub() + post: sinon.stub() + patch: sinon.stub() + reorder: sinon.stub() + reorderRelatedUserstory: sinon.stub() + } + userstories: { + listInEpic: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _mockTgXhrErrorService = () -> + mocks.tgXhrErrorService = { + response: sinon.stub() + } + + provide.value "tgXhrErrorService", mocks.tgXhrErrorService + + _inject = (callback) -> + inject (_tgEpicsService_) -> + epicsService = _tgEpicsService_ + callback() if callback + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgProjectService() + _mockTgAttachmentsService() + _mockTgResources() + _mockTgXhrErrorService() + return null + + _setup = -> + _mocks() + + beforeEach -> + module "taigaEpics" + _setup() + _inject() + + it "clear epics", () -> + epicsService._epics = Immutable.List(Immutable.Map({ + 'id': 1 + })) + + epicsService.clear() + expect(epicsService._epics.size).to.be.equal(0) + + it "fetch epics success", () -> + epics = Immutable.fromJS([ + { id: 111 } + { id: 112 } + ]) + promise = mocks.tgResources.epics.list.withArgs(1).promise().resolve(epics) + epicsService.fetchEpics().then () -> + expect(epicsService.epics).to.be.equal(epics) + + it "fetch epics error", () -> + epics = Immutable.fromJS([ + { id: 111 } + { id: 112 } + ]) + promise = mocks.tgResources.epics.list.withArgs(1).promise().reject(new Error("error")) + epicsService.fetchEpics().then () -> + expect(mocks.tgXhrErrorService.response.withArgs(new Error("error"))).have.been.calledOnce + + it "list related userstories", () -> + epic = Immutable.fromJS({ + id: 1 + }) + epicsService.listRelatedUserStories(epic) + expect(mocks.tgResources.userstories.listInEpic.withArgs(epic.get('id'))).have.been.calledOnce + + it "createEpic", () -> + epicData = {} + epic = Immutable.fromJS({ + id: 111 + project: 1 + }) + attachments = Immutable.fromJS([ + {file: "f1"}, + {file: "f2"} + ]) + + mocks.tgResources.epics + .post + .withArgs({project: 1}) + .promise() + .resolve(epic) + + mocks.tgAttachmentsService + .upload + .promise() + .resolve() + + epicsService.fetchEpics = sinon.stub() + epicsService.createEpic(epicData, attachments).then () -> + expect(mocks.tgAttachmentsService.upload.withArgs("f1", 111, 1, "epic")).have.been.calledOnce + expect(mocks.tgAttachmentsService.upload.withArgs("f2", 111, 1, "epic")).have.been.calledOnce + expect(epicsService.fetchEpics).have.been.calledOnce + + it "Update epic status", () -> + epic = Immutable.fromJS({ + id: 1 + version: 1 + }) + + mocks.tgResources.epics + .patch + .withArgs(1, {status: 33, version: 1}) + .promise() + .resolve() + + epicsService.fetchEpics = sinon.stub() + epicsService.updateEpicStatus(epic, 33).then () -> + expect(epicsService.fetchEpics).have.been.calledOnce + + it "Update epic assigned to", () -> + epic = Immutable.fromJS({ + id: 1 + version: 1 + }) + + mocks.tgResources.epics + .patch + .withArgs(1, {assigned_to: 33, version: 1}) + .promise() + .resolve() + + epicsService.fetchEpics = sinon.stub() + epicsService.updateEpicAssignedTo(epic, 33).then () -> + expect(epicsService.fetchEpics).have.been.calledOnce + + it "reorder epic", () -> + epicsService._epics = Immutable.fromJS([ + { + id: 1 + epics_order: 1 + version: 1 + }, + { + id: 2 + epics_order: 2 + version: 1 + }, + { + id: 3 + epics_order: 3 + version: 1 + }, + ]) + + mocks.tgResources.epics.reorder + .withArgs(3, {epics_order: 2, version: 1}, {1: 1}) + .promise() + .resolve() + + epicsService.fetchEpics = sinon.stub() + epicsService.reorderEpic(epicsService._epics.get(2), 1).then () -> + expect(epicsService.fetchEpics).have.been.calledOnce + + it "reorder related userstory in epic", () -> + epic = Immutable.fromJS({ + id: 1 + }) + + epicUserstories = Immutable.fromJS([ + { + id: 1 + epic_order: 1 + }, + { + id: 2 + epic_order: 2 + }, + { + id: 3 + epic_order: 3 + }, + ]) + + mocks.tgResources.epics.reorderRelatedUserstory + .withArgs(1, 3, {order: 2}, {1: 1}) + .promise() + .resolve() + + epicsService.listRelatedUserStories = sinon.stub() + epicsService.reorderRelatedUserstory(epic, epicUserstories, epicUserstories.get(2), 1).then () -> + expect(epicsService.listRelatedUserStories.withArgs(epic)).have.been.calledOnce diff --git a/app/modules/epics/related-userstories/related-userstories-controller.coffee b/app/modules/epics/related-userstories/related-userstories-controller.coffee new file mode 100644 index 00000000..07319495 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-controller.coffee @@ -0,0 +1,48 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: related-userstories.controller.coffee +### + +module = angular.module("taigaEpics") + +class RelatedUserStoriesController + @.$inject = [ + "tgProjectService", + "tgEpicsService" + ] + + constructor: (@projectService, @epicsService) -> + @.sectionName = "Epics" + @.showCreateRelatedUserstoriesLightbox = false + + showRelatedUserStoriesSection: () -> + return @projectService.hasPermission("modify_epic") or @.userstories?.legth > 0 + + userCanSort: () -> + return @projectService.hasPermission("modify_epic") + + loadRelatedUserstories: () -> + @epicsService.listRelatedUserStories(@.epic) + .then (userstories) => + @.userstories = userstories + + reorderRelatedUserstory: (us, newIndex) -> + @epicsService.reorderRelatedUserstory(@.epic, @.userstories, us, newIndex) + .then (userstories) => + @.userstories = userstories + +module.controller("RelatedUserStoriesCtrl", RelatedUserStoriesController) diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee new file mode 100644 index 00000000..8b51a2c2 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee @@ -0,0 +1,92 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: related-userstory-create.controller.coffee +### + +module = angular.module("taigaEpics") + +class RelatedUserstoriesCreateController + @.$inject = [ + "tgCurrentUserService", + "tgResources", + "$tgConfirm", + "$tgAnalytics" + ] + + constructor: (@currentUserService, @rs, @confirm, @analytics) -> + @.projects = @currentUserService.projects.get("all") + @.projectUserstories = Immutable.List() + @.loading = false + + selectProject: (selectedProjectId, onSelectedProject) -> + @rs.userstories.listAllInProject(selectedProjectId).then (data) => + excludeIds = @.epicUserstories.map((us) -> us.get('id')) + filteredData = data.filter((us) -> excludeIds.indexOf(us.get('id')) == -1) + @.projectUserstories = filteredData + if onSelectedProject + onSelectedProject() + + saveRelatedUserStory: (selectedUserstoryId, onSavedRelatedUserstory) -> + # This method assumes the following methods are binded to the controller: + # - validateExistingUserstoryForm + # - setExistingUserstoryFormErrors + # - loadRelatedUserstories + return if not @.validateExistingUserstoryForm() + + @.loading = true + + onError = (data) => + @.loading = false + @confirm.notify("error") + @.setExistingUserstoryFormErrors(data) + + onSuccess = () => + @analytics.trackEvent("epic related user story", "create", "create related user story on epic", 1) + @.loading = false + if onSavedRelatedUserstory + onSavedRelatedUserstory() + @.loadRelatedUserstories() + + epicId = @.epic.get('id') + @rs.epics.addRelatedUserstory(epicId, selectedUserstoryId).then(onSuccess, onError) + + bulkCreateRelatedUserStories: (selectedProjectId, userstoriesText, onCreatedRelatedUserstory) -> + # This method assumes the following methods are binded to the controller: + # - validateNewUserstoryForm + # - setNewUserstoryFormErrors + # - loadRelatedUserstories + return if not @.validateNewUserstoryForm() + + @.loading = true + + onError = (data) => + @.loading = false + @confirm.notify("error") + @.setNewUserstoryFormErrors(data) + + onSuccess = () => + @analytics.trackEvent("epic related user story", "create", "create related user story on epic", 1) + @.loading = false + if onCreatedRelatedUserstory + onCreatedRelatedUserstory() + @.loadRelatedUserstories() + + epicId = @.epic.get('id') + @rs.epics.bulkCreateRelatedUserStories(epicId, selectedProjectId, userstoriesText).then(onSuccess, onError) + + +module.controller("RelatedUserstoriesCreateCtrl", RelatedUserstoriesCreateController) diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee new file mode 100644 index 00000000..f3bc84b1 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee @@ -0,0 +1,185 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: related-userstories-create.controller.spec.coffee +### + +describe "RelatedUserstoriesCreate", -> + RelatedUserstoriesCreateCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + projects: { + get: sinon.stub() + } + } + + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockTgConfirm = () -> + mocks.tgConfirm = { + askOnDelete: sinon.stub() + notify: sinon.stub() + } + + provide.value "$tgConfirm", mocks.tgConfirm + + + _mockTgResources = () -> + mocks.tgResources = { + userstories: { + listAllInProject: sinon.stub() + } + epics: { + deleteRelatedUserstory: sinon.stub() + addRelatedUserstory: sinon.stub() + bulkCreateRelatedUserStories: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _mockTgAnalytics = () -> + mocks.tgAnalytics = { + trackEvent: sinon.stub() + } + + provide.value "$tgAnalytics", mocks.tgAnalytics + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgCurrentUserService() + _mockTgConfirm() + _mockTgResources() + _mockTgAnalytics() + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + RelatedUserstoriesCreateCtrl = controller "RelatedUserstoriesCreateCtrl" + + it "select project", (done) -> + # This test tries to reproduce a project containing userstories 11 and 12 where 11 + # is yet related to the epic + RelatedUserstoriesCreateCtrl.epicUserstories = Immutable.fromJS([ + { + id: 11 + } + ]) + + onSelectedProjectCallback = sinon.stub() + userstories = Immutable.fromJS([ + { + id: 11 + }, + { + + id: 12 + } + ]) + filteredUserstories = Immutable.fromJS([ + { + + id: 12 + } + ]) + + promise = mocks.tgResources.userstories.listAllInProject.withArgs(1).promise().resolve(userstories) + RelatedUserstoriesCreateCtrl.selectProject(1, onSelectedProjectCallback).then () -> + expect(RelatedUserstoriesCreateCtrl.projectUserstories.toJS()).to.eql(filteredUserstories.toJS()) + done() + + it "save related user story success", (done) -> + RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm = sinon.stub() + RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm.returns(true) + onSavedRelatedUserstoryCallback = sinon.stub() + onSavedRelatedUserstoryCallback.returns(true) + RelatedUserstoriesCreateCtrl.loadRelatedUserstories = sinon.stub() + RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({ + id: 1 + }) + promise = mocks.tgResources.epics.addRelatedUserstory.withArgs(1, 11).promise().resolve(true) + RelatedUserstoriesCreateCtrl.saveRelatedUserStory(11, onSavedRelatedUserstoryCallback).then () -> + expect(RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm).have.been.calledOnce + expect(onSavedRelatedUserstoryCallback).have.been.calledOnce + expect(mocks.tgResources.epics.addRelatedUserstory).have.been.calledWith(1, 11) + expect(mocks.tgAnalytics.trackEvent).have.been.calledWith("epic related user story", "create", "create related user story on epic", 1) + expect(RelatedUserstoriesCreateCtrl.loadRelatedUserstories).have.been.calledOnce + done() + + it "save related user story error", (done) -> + RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm = sinon.stub() + RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm.returns(true) + onSavedRelatedUserstoryCallback = sinon.stub() + RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors = sinon.stub() + RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors.returns({}) + RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({ + id: 1 + }) + promise = mocks.tgResources.epics.addRelatedUserstory.withArgs(1, 11).promise().reject(new Error("error")) + RelatedUserstoriesCreateCtrl.saveRelatedUserStory(11, onSavedRelatedUserstoryCallback).then () -> + expect(RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm).have.been.calledOnce + expect(onSavedRelatedUserstoryCallback).to.not.have.been.called + expect(mocks.tgResources.epics.addRelatedUserstory).have.been.calledWith(1, 11) + expect(mocks.tgConfirm.notify).have.been.calledWith("error") + expect(RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors).have.been.calledOnce + done() + + it "bulk create related user stories success", (done) -> + RelatedUserstoriesCreateCtrl.validateNewUserstoryForm = sinon.stub() + RelatedUserstoriesCreateCtrl.validateNewUserstoryForm.returns(true) + onCreatedRelatedUserstoryCallback = sinon.stub() + onCreatedRelatedUserstoryCallback.returns(true) + RelatedUserstoriesCreateCtrl.loadRelatedUserstories = sinon.stub() + RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({ + id: 1 + }) + promise = mocks.tgResources.epics.bulkCreateRelatedUserStories.withArgs(1, 22, 'a\nb').promise().resolve(true) + RelatedUserstoriesCreateCtrl.bulkCreateRelatedUserStories(22, 'a\nb', onCreatedRelatedUserstoryCallback).then () -> + expect(RelatedUserstoriesCreateCtrl.validateNewUserstoryForm).have.been.calledOnce + expect(onCreatedRelatedUserstoryCallback).have.been.calledOnce + expect(mocks.tgResources.epics.bulkCreateRelatedUserStories).have.been.calledWith(1, 22, 'a\nb') + expect(mocks.tgAnalytics.trackEvent).have.been.calledWith("epic related user story", "create", "create related user story on epic", 1) + expect(RelatedUserstoriesCreateCtrl.loadRelatedUserstories).have.been.calledOnce + done() + + it "bulk create related user stories error", (done) -> + RelatedUserstoriesCreateCtrl.validateNewUserstoryForm = sinon.stub() + RelatedUserstoriesCreateCtrl.validateNewUserstoryForm.returns(true) + onCreatedRelatedUserstoryCallback = sinon.stub() + RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors = sinon.stub() + RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors.returns({}) + RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({ + id: 1 + }) + promise = mocks.tgResources.epics.bulkCreateRelatedUserStories.withArgs(1, 22, 'a\nb').promise().reject(new Error("error")) + RelatedUserstoriesCreateCtrl.bulkCreateRelatedUserStories(22, 'a\nb', onCreatedRelatedUserstoryCallback).then () -> + expect(RelatedUserstoriesCreateCtrl.validateNewUserstoryForm).have.been.calledOnce + expect(onCreatedRelatedUserstoryCallback).to.not.have.been.called + expect(mocks.tgResources.epics.bulkCreateRelatedUserStories).have.been.calledWith(1, 22, 'a\nb') + expect(mocks.tgConfirm.notify).have.been.calledWith("error") + expect(RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors).have.been.calledOnce + done() diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee new file mode 100644 index 00000000..9ecd4a03 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee @@ -0,0 +1,79 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: related-userstory-create.directive.coffee +### + +module = angular.module('taigaEpics') + +RelatedUserstoriesCreateDirective = (@lightboxService) -> + link = (scope, el, attrs, ctrl) -> + newUserstoryForm = el.find(".new-user-story-form").checksley() + existingUserstoryForm = el.find(".existing-user-story-form").checksley() + + ctrl.validateNewUserstoryForm = => + return newUserstoryForm.validate() + + ctrl.setNewUserstoryFormErrors = (errors) => + newUserstoryForm.setErrors(errors) + + ctrl.validateExistingUserstoryForm = => + return existingUserstoryForm.validate() + + ctrl.setExistingUserstoryFormErrors = (errors) => + existingUserstoryForm.setErrors(errors) + + scope.showLightbox = (selectedProjectId) -> + scope.selectProject(selectedProjectId).then () => + lightboxService.open(el.find(".lightbox-create-related-user-stories")) + + scope.closeLightbox = () -> + scope.selectedUserstory = null + scope.searchUserstory = "" + scope.relatedUserstoriesText = "" + lightboxService.close(el.find(".lightbox-create-related-user-stories")) + + scope.$watch 'vm.project', (project) -> + if project? + scope.selectedProject = project.get('id') + + scope.selectProject = (selectedProjectId) -> + scope.selectedUserstory = null + scope.searchUserstory = "" + ctrl.selectProject(selectedProjectId) + + scope.onUpdateSearchUserstory = () -> + scope.selectedUserstory = null + + return { + link: link, + templateUrl:"epics/related-userstories/related-userstories-create/related-userstories-create.html", + controller: "RelatedUserstoriesCreateCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + showCreateRelatedUserstoriesLightbox: "=" + project: "=" + epic: "=" + epicUserstories: "=" + loadRelatedUserstories:"&" + } + + } + +RelatedUserstoriesCreateDirective.$inject = ["lightboxService",] + +module.directive("tgRelatedUserstoriesCreate", RelatedUserstoriesCreateDirective) diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade new file mode 100644 index 00000000..f5cabb13 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade @@ -0,0 +1,152 @@ +a.add-button.e2e-add-userstory-button( + href="" + ng-click="showLightbox(selectedProject)" +) + tg-svg(svg-icon="icon-add") + +.lightbox.lightbox-create-related-user-stories + tg-lightbox-close + + .lightbox-create-related-user-stories-wrapper + h2.title(translate="EPIC.CREATE_RELATED_USERSTORIES") + + .related-with-selector + .related-with-selector-single + input( + type="radio" + name="related-with-selector" + id="new-user-story" + value="new-user-story" + ng-model="relatedWithSelector" + ng-init="relatedWithSelector='new-user-story'" + ) + label.e2e-new-userstory-label(for="new-user-story") + span.name {{ 'EPIC.NEW_USERSTORY' | translate}} + + .related-with-selector-single + input( + type="radio" + name="related-with-selector" + id="existing-user-story" + value="existing-user-story" + ng-model="relatedWithSelector" + ) + label.e2e-existing-user-story-label(for="existing-user-story") + span.name {{ 'EPIC.EXISTING_USERSTORY' | translate}} + + fieldset.project-selector + label( + ng-if="relatedWithSelector=='new-user-story'" + translate="EPIC.CHOOSE_PROJECT_FOR_CREATION" + for="project-selector-dropdown" + ) + label( + ng-if="relatedWithSelector=='existing-user-story'" + translate="EPIC.CHOOSE_PROJECT_FROM" + for="project-selector-dropdown" + ) + select( + ng-model="selectedProject" + ng-change="selectProject(selectedProject)" + data-required="true" + ng-options="p.id as p.name for p in vm.projects | toMutable" + id="project-selector-dropdown" + ) + + fieldset(ng-show="relatedWithSelector=='new-user-story'") + .new-user-story-title + label( + ng-show="creationMode=='single-new-user-story'" + translate="EPIC.SUBJECT" + ) + + label( + ng-show="creationMode=='bulk-new-user-stories'" + translate="EPIC.SUBJECT_BULK_MODE" + ) + .new-user-story-options + .new-user-story-option-single + input( + type="radio" + name="new-user-story-selector" + id="single-new-user-story" + value="single-new-user-story" + ng-model="creationMode" + ng-init="creationMode='single-new-user-story'" + ) + label.e2e-single-creation-label(for="single-new-user-story") + tg-svg(svg-icon="icon-add") + + .new-user-story-option-single + input( + type="radio" + name="new-user-story-selector" + id="bulk-new-user-stories" + value="bulk-new-user-stories" + ng-model="creationMode" + ) + label.e2e-bulk-creation-label(for="bulk-new-user-stories") + tg-svg(svg-icon="icon-bulk") + + + form.new-user-story-form + .single-creation(ng-show="creationMode=='single-new-user-story'") + input.e2e-new-userstory-input-text( + type="text" + ng-model="relatedUserstoriesText" + data-required="true" + ) + + .bulk-creation(ng-show="creationMode=='bulk-new-user-stories'") + textarea.e2e-new-userstories-input-textarea( + ng-model="relatedUserstoriesText" + data-required="true" + ) + + button.button-green.create-user-story.e2e-create-userstory-button.ng-animate-disabled( + href="" + ng-click="vm.bulkCreateRelatedUserStories(selectedProject, relatedUserstoriesText, closeLightbox)" + tg-loading="vm.loading" + translate="COMMON.SAVE" + ng-show="relatedWithSelector=='new-user-story'" + ) + + p( + ng-show="relatedWithSelector=='existing-user-story' && !vm.projectUserstories.size" + translate="EPIC.NO_USERSTORIES" + ) + + fieldset.existing-user-story(ng-show="relatedWithSelector=='existing-user-story' && vm.projectUserstories.size") + label( + translate="EPIC.CHOOSE_USERSTORY" + for="userstory-filter" + ) + input.userstory-filter.e2e-filter-userstories-input( + id="userstory-filter" + type="text" + placeholder="{{'EPIC.FILTER_USERSTORIES' | translate}}" + ng-model="searchUserstory" + ng-change="onUpdateSearchUserstory()" + ) + + form.existing-user-story-form + select.userstory.e2e-userstories-select( + size="5" + ng-model="selectedUserstory" + data-required="true" + ) + - var hash = "#"; + option.hidden( + value="" + ) + option( + ng-repeat="us in vm.projectUserstories | toMutable | byRef:searchUserstory track by us.id" + value="{{ ::us.id }}" + ) #{hash}{{::us.ref}} {{::us.subject}} + + button.button-green.e2e-select-related-userstory-button( + href="" + ng-click="vm.saveRelatedUserStory(selectedUserstory, closeLightbox)" + tg-loading="vm.loading" + translate="COMMON.SAVE" + ) diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss new file mode 100644 index 00000000..1e397dbf --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss @@ -0,0 +1,83 @@ +.lightbox-create-related-user-stories { + .lightbox-create-related-user-stories-wrapper { + max-width: 600px; + width: 90%; + } + .related-with-selector { + display: flex; + margin-bottom: 1rem; + input { + display: none; + &:checked+label { + background: $primary-light; + color: $white; + transition: background .2s ease-in; + } + &:checked+label:hover { + background: $primary-light; + } + +label { + background: rgba($whitish, .7); + cursor: pointer; + display: block; + padding: 2rem 1rem; + text-align: center; + text-transform: uppercase; + transition: background .2s ease-in; + } + +label:hover { + background: rgba($primary-light, .3); + transition: background .2s ease-in; + } + } + .related-with-selector-single { + flex: 1; + &:first-child { + margin-right: .5rem; + } + } + } + fieldset { + label { + display: inline-block; + margin-bottom: .5rem; + } + } + .new-user-story-title { + align-items: flex-end; + display: flex; + } + .existing-user-story-form, + .new-user-story-form { + margin-bottom: 1rem; + } + .new-user-story-options { + display: flex; + margin-left: auto; + input { + display: none; + &:checked+label { + background: $primary-light; + color: $white; + fill: $white; + transition: background .2s ease-in; + } + +label { + background: $mass-white; + color: $grayer; + cursor: pointer; + display: block; + padding: .5rem; + transition: background .2s ease-in; + } + +label:hover { + background: $primary-light; + color: $white; + fill: $white; + } + } + } + button { + width: 100%; + } +} diff --git a/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee b/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee new file mode 100644 index 00000000..1989e7d5 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee @@ -0,0 +1,65 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: related-userstories-sortable.directive.coffee +### + +module = angular.module('taigaEpics') + +RelatedUserstoriesSortableDirective = ($parse, projectService) -> + link = (scope, el, attrs) -> + return if not projectService.hasPermission("modify_epic") + + callback = $parse(attrs.tgRelatedUserstoriesSortable) + + drake = dragula([el[0]], { + copySortSource: false + copy: false + mirrorContainer: el[0] + moves: (item) -> + return $(item).is('tg-related-userstory-row') + }) + + drake.on 'dragend', (item) -> + itemEl = $(item) + us = itemEl.scope().us + newIndex = itemEl.index() + + scope.$apply () -> + callback(scope, {us: us, newIndex: newIndex}) + + scroll = autoScroll(window, { + margin: 20, + pixels: 30, + scrollWhenOutside: true, + autoScroll: () -> + return this.down && drake.dragging + }) + + scope.$on "$destroy", -> + el.off() + drake.destroy() + + return { + link: link + } + +RelatedUserstoriesSortableDirective.$inject = [ + "$parse", + "tgProjectService" +] + +module.directive("tgRelatedUserstoriesSortable", RelatedUserstoriesSortableDirective) diff --git a/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee new file mode 100644 index 00000000..6c82137e --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee @@ -0,0 +1,107 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: related-userstories.controller.spec.coffee +### + +describe "RelatedUserStories", -> + RelatedUserStoriesCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgEpicsService = () -> + mocks.tgEpicsService = { + listRelatedUserStories: sinon.stub() + reorderRelatedUserstory: sinon.stub() + } + + provide.value "tgEpicsService", mocks.tgEpicsService + + _mockTgProjectService = () -> + mocks.tgProjectService = { + hasPermission: sinon.stub() + } + provide.value "tgProjectService", mocks.tgProjectService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgEpicsService() + _mockTgProjectService() + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "load related userstories", (done) -> + ctrl = controller "RelatedUserStoriesCtrl" + userstories = Immutable.fromJS([ + { + id: 1 + } + ]) + + ctrl.epic = Immutable.fromJS({ + id: 66 + }) + + promise = mocks.tgEpicsService.listRelatedUserStories + .withArgs(ctrl.epic) + .promise() + .resolve(userstories) + + ctrl.loadRelatedUserstories().then () -> + expect(ctrl.userstories).is.equal(userstories) + done() + + it "reorderRelatedUserstory", (done) -> + ctrl = controller "RelatedUserStoriesCtrl" + userstories = Immutable.fromJS([ + { + id: 1 + }, + { + id: 2 + } + ]) + + reorderedUserstories = Immutable.fromJS([ + { + id: 2 + }, + { + id: 1 + } + ]) + + ctrl.epic = Immutable.fromJS({ + id: 66 + }) + + promise = mocks.tgEpicsService.reorderRelatedUserstory + .withArgs(ctrl.epic, ctrl.userstories, userstories.get(1), 0) + .promise() + .resolve(reorderedUserstories) + + ctrl.reorderRelatedUserstory(userstories.get(1), 0).then () -> + expect(ctrl.userstories).is.equal(reorderedUserstories) + done() diff --git a/app/modules/epics/related-userstories/related-userstories.directive.coffee b/app/modules/epics/related-userstories/related-userstories.directive.coffee new file mode 100644 index 00000000..e3db9be8 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories.directive.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: related-userstories.directive.coffee +### + +module = angular.module('taigaEpics') + +RelatedUserStoriesDirective = () -> + return { + templateUrl:"epics/related-userstories/related-userstories.html", + controller: "RelatedUserStoriesCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + userstories: '=', + project: '=' + epic: '=' + } + } + +RelatedUserStoriesDirective.$inject = [] + +module.directive("tgRelatedUserstories", RelatedUserStoriesDirective) diff --git a/app/modules/epics/related-userstories/related-userstories.jade b/app/modules/epics/related-userstories/related-userstories.jade new file mode 100644 index 00000000..1cbbe21b --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories.jade @@ -0,0 +1,28 @@ +section.related-userstories( + ng-if="vm.showRelatedUserStoriesSection()" +) + .related-userstories-header + span.related-userstories-title(translate="COMMON.RELATED_USERSTORIES") + tg-related-userstories-create( + tg-check-permission="modify_epic" + show-create-related-userstories-lightbox="vm.showCreateRelatedUserstoriesLightbox" + project="vm.project" + epic="vm.epic" + epic-userstories="vm.userstories" + load-related-userstories="vm.loadRelatedUserstories()" + ) + + .related-userstories-body( + tg-related-userstories-sortable="vm.reorderRelatedUserstory(us, newIndex)" + ) + tg-related-userstory-row.row( + tg-repeat="us in vm.userstories track by us.get('id')" + ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked'), sortable: vm.userCanSort()}" + userstory="us" + epic="vm.epic" + project="vm.project" + load-related-userstories="vm.loadRelatedUserstories()" + tg-bind-scope + ) + + div(tg-related-userstories-create-form) diff --git a/app/modules/epics/related-userstories/related-userstories.scss b/app/modules/epics/related-userstories/related-userstories.scss new file mode 100644 index 00000000..67ba81a0 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories.scss @@ -0,0 +1,39 @@ +.related-userstories { + margin-bottom: 2rem; + position: relative; +} + +.related-userstories-header { + align-content: center; + align-items: center; + background: $mass-white; + display: flex; + justify-content: space-between; + min-height: 36px; + .related-userstories-title { + @include font-size(medium); + @include font-type(bold); + margin-left: 1rem; + } + .add-button { + background: $grayer; + border: 0; + display: inline-block; + padding: .5rem; + transition: background .25s; + &:hover, + &.is-active { + background: $primary-light; + } + svg { + fill: $white; + height: 1.25rem; + margin-bottom: -.2rem; + width: 1.25rem; + } + } +} + +.related-userstories-body { + width: 100%; +} diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee new file mode 100644 index 00000000..ba583d8a --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee @@ -0,0 +1,62 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: reñated-userstory-row.controller.coffee +### + +module = angular.module("taigaEpics") + +class RelatedUserstoryRowController + @.$inject = [ + "tgAvatarService", + "$translate", + "$tgConfirm", + "tgResources" + ] + + constructor: (@avatarService, @translate, @confirm, @rs) -> + + setAvatarData: () -> + member = @.userstory.get('assigned_to_extra_info') + @.avatar = @avatarService.getAvatar(member) + + getAssignedToFullNameDisplay: () -> + if @.userstory.get('assigned_to') + return @.userstory.getIn(['assigned_to_extra_info', 'full_name_display']) + + return @translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED") + + onDeleteRelatedUserstory: () -> + title = @translate.instant('EPIC.TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY') + message = @translate.instant('EPIC.MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY', { + subject: @.userstory.get('subject') + }) + return @confirm.askOnDelete(title, message) + .then (askResponse) => + onError = () => + message = @translate.instant('EPIC.ERROR_UNLINK_RELATED_USERSTORY', {errorMessage: message}) + @confirm.notify("error", null, message) + askResponse.finish(false) + + onSuccess = () => + @.loadRelatedUserstories() + askResponse.finish() + + epicId = @.epic.get('id') + userstoryId = @.userstory.get('id') + @rs.epics.deleteRelatedUserstory(epicId, userstoryId).then(onSuccess, onError) + +module.controller("RelatedUserstoryRowCtrl", RelatedUserstoryRowController) diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee new file mode 100644 index 00000000..5b496b0e --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee @@ -0,0 +1,167 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: related-userstory-row.controller.spec.coffee +### + +describe "RelatedUserstoryRow", -> + RelatedUserstoryRowCtrl = null + provide = null + controller = null + mocks = {} + + _mockTgConfirm = () -> + mocks.tgConfirm = { + askOnDelete: sinon.stub() + notify: sinon.stub() + } + + provide.value "$tgConfirm", mocks.tgConfirm + + _mockTgAvatarService = () -> + mocks.tgAvatarService = { + getAvatar: sinon.stub() + } + + provide.value "tgAvatarService", mocks.tgAvatarService + + _mockTranslate = () -> + mocks.translate = { + instant: sinon.stub() + } + + provide.value "$translate", mocks.translate + + _mockTgResources = () -> + mocks.tgResources = { + epics: { + deleteRelatedUserstory: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgConfirm() + _mockTgAvatarService() + _mockTranslate() + _mockTgResources() + + return null + + beforeEach -> + module "taigaEpics" + + _mocks() + + inject ($controller) -> + controller = $controller + + RelatedUserstoryRowCtrl = controller "RelatedUserstoryRowCtrl" + + it "set avatar data", (done) -> + RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({ + assigned_to_extra_info: { + id: 3 + } + }) + member = RelatedUserstoryRowCtrl.userstory.get("assigned_to_extra_info") + avatar = { + url: "http://taiga.io" + bg: "#AAAAAA" + } + mocks.tgAvatarService.getAvatar.withArgs(member).returns(avatar) + RelatedUserstoryRowCtrl.setAvatarData() + expect(mocks.tgAvatarService.getAvatar).have.been.calledWith(member) + expect(RelatedUserstoryRowCtrl.avatar).is.equal(avatar) + done() + + it "get assigned to full name display for existing user", (done) -> + RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({ + assigned_to: 1 + assigned_to_extra_info: { + full_name_display: "Beta tester" + } + }) + + expect(RelatedUserstoryRowCtrl.getAssignedToFullNameDisplay()).is.equal("Beta tester") + done() + + it "get assigned to full name display for unassigned user story", (done) -> + RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({ + assigned_to: null + }) + mocks.translate.instant.withArgs("COMMON.ASSIGNED_TO.NOT_ASSIGNED").returns("Unassigned") + expect(RelatedUserstoryRowCtrl.getAssignedToFullNameDisplay()).is.equal("Unassigned") + done() + + it "delete related userstory success", (done) -> + RelatedUserstoryRowCtrl.epic = Immutable.fromJS({ + id: 123 + }) + RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({ + subject: "Deleting" + id: 124 + }) + + RelatedUserstoryRowCtrl.loadRelatedUserstories = sinon.stub() + + askResponse = { + finish: sinon.spy() + } + + mocks.translate.instant.withArgs("EPIC.TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY").returns("title") + mocks.translate.instant.withArgs("EPIC.MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY", {subject: "Deleting"}).returns("message") + + mocks.tgConfirm.askOnDelete = sinon.stub() + mocks.tgConfirm.askOnDelete.withArgs("title", "message").promise().resolve(askResponse) + + promise = mocks.tgResources.epics.deleteRelatedUserstory.withArgs(123, 124).promise().resolve(true) + RelatedUserstoryRowCtrl.onDeleteRelatedUserstory().then () -> + expect(RelatedUserstoryRowCtrl.loadRelatedUserstories).have.been.calledOnce + expect(askResponse.finish).have.been.calledOnce + done() + + it "delete related userstory error", (done) -> + RelatedUserstoryRowCtrl.epic = Immutable.fromJS({ + id: 123 + }) + RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({ + subject: "Deleting" + id: 124 + }) + + RelatedUserstoryRowCtrl.loadRelatedUserstories = sinon.stub() + + askResponse = { + finish: sinon.spy() + } + + mocks.translate.instant.withArgs("EPIC.TITLE_LIGHTBOX_UNLINK_RELATED_USERSTORY").returns("title") + mocks.translate.instant.withArgs("EPIC.MSG_LIGHTBOX_UNLINK_RELATED_USERSTORY", {subject: "Deleting"}).returns("message") + mocks.translate.instant.withArgs("EPIC.ERROR_UNLINK_RELATED_USERSTORY", {errorMessage: "message"}).returns("error message") + + mocks.tgConfirm.askOnDelete = sinon.stub() + mocks.tgConfirm.askOnDelete.withArgs("title", "message").promise().resolve(askResponse) + + promise = mocks.tgResources.epics.deleteRelatedUserstory.withArgs(123, 124).promise().reject(new Error("error")) + RelatedUserstoryRowCtrl.onDeleteRelatedUserstory().then () -> + expect(RelatedUserstoryRowCtrl.loadRelatedUserstories).to.not.have.been.called + expect(askResponse.finish).have.been.calledWith(false) + expect(mocks.tgConfirm.notify).have.been.calledWith("error", null, "error message") + done() diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee new file mode 100644 index 00000000..02ea4ebd --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee @@ -0,0 +1,42 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: related-userstory-row.directive.coffee +### + +module = angular.module('taigaEpics') + +RelatedUserstoryRowDirective = () -> + link = (scope, el, attrs, ctrl) -> + ctrl.setAvatarData() + + return { + link: link, + templateUrl:"epics/related-userstories/related-userstory-row/related-userstory-row.html", + controller: "RelatedUserstoryRowCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + userstory: '=' + epic: '=' + project: '=' + loadRelatedUserstories:"&" + } + } + +RelatedUserstoryRowDirective.$inject = [] + +module.directive("tgRelatedUserstoryRow", RelatedUserstoryRowDirective) diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade new file mode 100644 index 00000000..c54e09af --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade @@ -0,0 +1,51 @@ +tg-svg.icon-drag( + svg-icon="icon-drag" + tg-check-permission="modify_epic" +) + +.userstory-name + - var hash = "#"; + a( + tg-nav="project-userstories-detail:project=vm.userstory.getIn(['project_extra_info', 'slug']),ref=vm.userstory.get('ref')" + ng-attr-title="{{vm.userstory.get('subject')}}" + ) #{hash}{{vm.userstory.get('ref')}} {{vm.userstory.get('subject')}} + + tg-belong-to-epics( + format="pill" + ng-if="vm.userstory.get('epics')" + epics="vm.userstory.get('epics')" + ) + +.userstory-settings + a.delete-userstory.e2e-delete-userstory( + tg-check-permission="modify_epic" + title="{{'COMMON.DELETE' | translate}}" + href="" + ng-click="vm.onDeleteRelatedUserstory()" + ) + tg-svg(svg-icon="icon-broken-link") + +.project( + tg-nav="project:project=vm.userstory.getIn(['project_extra_info', 'slug'])" +) + img( + tg-project-logo-small-src="::vm.userstory.get('project_extra_info')" + alt="{{::vm.userstory.getIn(['project_extra_info', 'name'])}}" + ) + +.status + span.userstory-status( + ng-style="{'color': vm.userstory.getIn(['status_extra_info', 'color'])}" + ) {{vm.userstory.getIn(['status_extra_info', 'name'])}} + +.assigned-to-column + figure.avatar + img( + style="background-color: {{ vm.avatar.bg }}" + src="{{ vm.avatar.url }}" + alt="{{ vm.avatar.full_name_display }}" + ) + + figcaption {{ vm.getAssignedToFullNameDisplay() }} + +div(tg-related-userstories-create-form) diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss new file mode 100644 index 00000000..c9fc2f32 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss @@ -0,0 +1,118 @@ +tg-related-userstory-row { + @include font-size(small); + align-items: center; + border-bottom: 1px solid $whitish; + display: flex; + padding: .5rem 0 .5rem .5rem; + &.sortable { + cursor: move; + &:hover { + background: rgba($primary-light, .05); + .userstory-settings { + opacity: 1; + transition: all .2s ease-in; + } + .icon-drag { + opacity: 1; + } + } + .icon-drag { + @include svg-size(.75rem); + cursor: move; + fill: $whitish; + opacity: 0; + transition: opacity .1s; + } + } + .status { + flex-shrink: 0; + position: relative; + width: 125px; + } + .assigned-to-column { + flex-shrink: 0; + width: 150px; + img { + flex-basis: 35px; + height: 35px; + width: 35px; + } + } + .project { + cursor: pointer; + flex-basis: 100px; + img { + width: 40px; + } + } + .userstory-name { + display: flex; + flex: 1; + margin-right: 1rem; + a { + cursor: pointer; + } + span { + display: inline-block; + margin-left: .25rem; + } + } + .closed { + border-left: 10px solid $whitish; + color: $whitish; + a, + svg { + fill: $whitish; + } + .userstory-name a { + color: $whitish; + text-decoration: line-through; + + } + } + .blocked { + background: rgba($red-light, .2); + border-left: 10px solid $red-light; + } + .userstory-settings { + align-items: center; + display: flex; + flex-shrink: 0; + opacity: 0; + width: 60px; + svg { + @include svg-size(1.1rem); + fill: $gray-light; + margin-right: .5rem; + transition: fill .2s ease-in; + &:hover { + fill: $gray; + } + } + a { + &:hover { + cursor: pointer; + } + } + } + .delete-userstory { + &:hover { + .icon-trash { + fill: $red-light; + } + } + } + .avatar { + align-items: center; + display: flex; + img { + flex-basis: 35px; + // width & height they are only required for IE + height: 35px; + width: 35px; + } + figcaption { + margin-left: .5rem; + } + } +} diff --git a/app/modules/external-apps/external-app.controller.spec.coffee b/app/modules/external-apps/external-app.controller.spec.coffee index f6db0c86..e19c5b89 100644 --- a/app/modules/external-apps/external-app.controller.spec.coffee +++ b/app/modules/external-apps/external-app.controller.spec.coffee @@ -104,17 +104,15 @@ describe "ExternalAppController", -> mocks.routeParams.application = 6 mocks.routeParams.state = "testing-state" - xhr = { - status: 404 - } + error = new Error('404') - mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().reject(xhr) + mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().reject(error) ctrl = $controller("ExternalApp") setTimeout ( -> expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce - expect(mocks.tgXhrErrorService.response.withArgs(xhr)).to.be.calledOnce + expect(mocks.tgXhrErrorService.response.withArgs(error)).to.be.calledOnce done() ) diff --git a/app/modules/external-apps/external-app.jade b/app/modules/external-apps/external-app.jade index 59d42918..0cb5600f 100644 --- a/app/modules/external-apps/external-app.jade +++ b/app/modules/external-apps/external-app.jade @@ -9,7 +9,10 @@ section.external-app-wrapper div.user-card.avatar .card-inner div.user-image - img(ng-src="{{::vm.user.get('photo')}}", alt="{{::vm.user.get('full_name_display')}}") + img( + tg-avatar="vm.user" + alt="{{::vm.user.get('full_name_display')}}" + ) div.user-data h3 {{ ::vm.user.get("full_name_display") }} p {{ ::vm.user.get("email") }} diff --git a/app/modules/history/comments/comment.controller.coffee b/app/modules/history/comments/comment.controller.coffee new file mode 100644 index 00000000..531480ef --- /dev/null +++ b/app/modules/history/comments/comment.controller.coffee @@ -0,0 +1,60 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class CommentController + @.$inject = [ + "tgCurrentUserService", + "tgCheckPermissionsService", + "tgLightboxFactory" + ] + + constructor: (@currentUserService, @permissionService, @lightboxFactory) -> + @.hiddenDeletedComment = true + @.commentContent = angular.copy(@.comment) + + showDeletedComment: () -> + @.hiddenDeletedComment = false + + hideDeletedComment: () -> + @.hiddenDeletedComment = true + + checkCancelComment: (event) -> + if event.keyCode == 27 + @.onEditMode({commentId: @.comment.id}) + + canEditDeleteComment: () -> + if @currentUserService.getUser() + @.user = @currentUserService.getUser() + return @.user.get('id') == @.comment.user.pk || @permissionService.check('modify_project') + + displayCommentHistory: () -> + @lightboxFactory.create('tg-lb-display-historic', { + "class": "lightbox lightbox-display-historic" + "comment": "comment" + "name": "name" + "object": "object" + }, { + "comment": @.comment + "name": @.name + "object": @.object + }) + +module.controller("CommentCtrl", CommentController) diff --git a/app/modules/history/comments/comment.controller.spec.coffee b/app/modules/history/comments/comment.controller.spec.coffee new file mode 100644 index 00000000..f56c3c27 --- /dev/null +++ b/app/modules/history/comments/comment.controller.spec.coffee @@ -0,0 +1,129 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: subscriptions.controller.spec.coffee +### + +describe "CommentController", -> + provide = null + controller = null + mocks = {} + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockTgCheckPermissionsService = () -> + mocks.tgCheckPermissionsService = { + check: sinon.stub() + } + provide.value "tgCheckPermissionsService", mocks.tgCheckPermissionsService + + _mockTgLightboxFactory = () -> + mocks.tgLightboxFactory = { + create: sinon.stub() + } + + provide.value "tgLightboxFactory", mocks.tgLightboxFactory + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgCurrentUserService() + _mockTgCheckPermissionsService() + _mockTgLightboxFactory() + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.comment = "comment" + commentsCtrl.hiddenDeletedComment = true + commentsCtrl.commentContent = commentsCtrl.comment + + it "show deleted Comment", () -> + commentsCtrl = controller "CommentCtrl" + commentsCtrl.showDeletedComment() + expect(commentsCtrl.hiddenDeletedComment).to.be.false + + it "hide deleted Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.hiddenDeletedComment = false + commentsCtrl.hideDeletedComment() + expect(commentsCtrl.hiddenDeletedComment).to.be.true + + it "cancel comment on keyup", () -> + commentsCtrl = controller "CommentCtrl" + commentsCtrl.comment = { + id: 2 + } + event = { + keyCode: 27 + } + commentsCtrl.onEditMode = sinon.stub() + commentsCtrl.checkCancelComment(event) + + expect(commentsCtrl.onEditMode).have.been.called + + it "can Edit Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.user = Immutable.fromJS({ + id: 7 + }) + + mocks.tgCurrentUserService.getUser.returns(commentsCtrl.user) + + commentsCtrl.comment = { + user: { + pk: 7 + } + } + + mocks.tgCheckPermissionsService.check.withArgs('modify_project').returns(true) + + canEdit = commentsCtrl.canEditDeleteComment() + expect(canEdit).to.be.true + + it "cannot Edit Comment", () -> + commentsCtrl = controller "CommentCtrl" + + commentsCtrl.user = Immutable.fromJS({ + id: 8 + }) + + mocks.tgCurrentUserService.getUser.returns(commentsCtrl.user) + + commentsCtrl.comment = { + user: { + pk: 7 + } + } + + mocks.tgCheckPermissionsService.check.withArgs('modify_project').returns(false) + + canEdit = commentsCtrl.canEditDeleteComment() + expect(canEdit).to.be.false diff --git a/app/modules/history/comments/comment.directive.coffee b/app/modules/history/comments/comment.directive.coffee new file mode 100644 index 00000000..0d001c7d --- /dev/null +++ b/app/modules/history/comments/comment.directive.coffee @@ -0,0 +1,46 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +CommentDirective = () -> + + return { + scope: { + name: "@", + object: "@", + comment: "<", + type: "<", + loading: "<", + editing: "<", + deleting: "<", + objectId: "<", + editMode: "<", + onEditMode: "&", + onDeleteComment: "&", + onRestoreDeletedComment: "&", + onEditComment: "&" + }, + templateUrl:"history/comments/comment.html", + bindToController: true, + controller: 'CommentCtrl', + controllerAs: "vm", + } + +module.directive("tgComment", CommentDirective) diff --git a/app/modules/history/comments/comment.jade b/app/modules/history/comments/comment.jade new file mode 100644 index 00000000..78258e57 --- /dev/null +++ b/app/modules/history/comments/comment.jade @@ -0,0 +1,108 @@ +include ../../../partials/common/components/wysiwyg.jade + +.comment-wrapper(ng-if="!vm.comment.delete_comment_date") + img.comment-avatar( + tg-avatar="vm.comment.user" + ng-alt="{{vm.comment.user.name}}" + ) + .comment-main + .comment-data + span.comment-creator {{vm.comment.user.name}} + span.comment-date {{vm.comment.created_at | momentFormat:'DD MMM YYYY HH:mm'}} + .comment-edited(ng-if="vm.comment.edit_comment_date") + span(translate="COMMENTS.EDITED_COMMENT") + span {{vm.comment.edit_comment_date | momentFormat:'DD MMM YYYY HH:mm'}} + span.separator - + a( + href="" + title="COMMENTS.SHOW_HISTORY" + ng-click="vm.displayCommentHistory()" + ) + span(translate="COMMENTS.SHOW_HISTORY") + tg-svg(svg-icon="icon-bulk") + .comment-container + .comment-text.wysiwyg( + ng-if="!vm.editMode" + ng-bind-html="vm.comment.comment_html" + ) + .comment-editor( + ng-if="vm.editMode" + ng-keyup="vm.checkCancelComment($event)" + ) + .edit-comment(ng-model="vm.type") + textarea( + ng-model="vm.commentContent.comment" + ) + .save-comment-wrapper + button.button-green.save-comment( + type="button" + title="{{'COMMENTS.EDIT_COMMENT' | translate}}" + translate="COMMENTS.EDIT_COMMENT" + ng-disabled="!vm.commentContent.comment.length || vm.editing == vm.comment.id" + ng-click="vm.onEditComment({commentId: vm.comment.id, commentData: vm.commentContent.comment})" + tg-loading="vm.editing == vm.comment.id" + ) + .comment-options(ng-if="::vm.canEditDeleteComment()") + tg-svg.comment-option( + svg-icon="icon-edit" + svg-title-translate="COMMON.EDIT" + ng-click="vm.onEditMode({commentId: vm.comment.id})" + ng-if="!vm.editMode" + ) + tg-svg.comment-option( + svg-icon="icon-close" + svg-title-translate="COMMON.CANCEL" + ng-click="vm.onEditMode({commentId: vm.comment.id})" + ng-if="vm.editMode" + ) + tg-svg.comment-option( + svg-icon="icon-trash" + svg-title-translate="COMMON.DELETE" + ng-click="vm.onDeleteComment({commentId: vm.comment.id})" + ng-if="!vm.editMode" + tg-loading="vm.deleting == vm.comment.id" + ) + +.deleted-comment-wrapper( + ng-if="vm.comment.delete_comment_date" +) + .deleted-comment-main + span( + translate="COMMENTS.DELETED_INFO" + translate-values="{user: vm.comment.delete_comment_user.name }" + ) + span - {{vm.comment.delete_comment_date | momentFormat:'DD MMM YYYY HH:mm'}} + a.toggle-deleted-comment( + href="" + ng-click="vm.showDeletedComment()" + ng-if="vm.hiddenDeletedComment" + ) + span(translate="COMMENTS.SHOW_DELETED") + tg-svg( + svg-icon="icon-arrow-down" + svg-title-translate="COMMENTS.SHOW_DELETED" + ) + a.toggle-deleted-comment( + href="" + ng-click="vm.hideDeletedComment()" + ng-if="!vm.hiddenDeletedComment" + ) + span(translate="COMMENTS.HIDE_DELETED") + tg-svg( + svg-icon="icon-arrow-up" + svg-title-translate="COMMENTS.HIDE_DELETED" + ) + a.restore-comment( + href="" + ng-click="vm.onRestoreDeletedComment({commentId: vm.comment.id})" + tg-loading="vm.editing == vm.comment.id" + ) + tg-svg( + svg-icon="icon-reload" + svg-title-translate="COMMENTS.RESTORE" + ) + span(translate="COMMENTS.RESTORE") + p.deleted-comment-comment( + ng-if="!vm.hiddenDeletedComment" + ng-bind-html="vm.comment.comment_html" + ) diff --git a/app/modules/history/comments/comment.scss b/app/modules/history/comments/comment.scss new file mode 100644 index 00000000..5aef9e53 --- /dev/null +++ b/app/modules/history/comments/comment.scss @@ -0,0 +1,159 @@ +.comments { + clear: both; + .add-comment { + margin-top: 1rem; + textarea { + height: 3rem; + } + .preview-icon, + .edit { + position: absolute; + right: 1rem; + } + } + .save-comment-wrapper { + align-items: flex-end; + display: flex; + flex-direction: column; + } + .save-comment { + margin-top: 1rem; + padding: .5rem 4rem; + } + +} +.comment { + display: block; + .comment-wrapper { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + &:hover { + .comment-option { + opacity: 1; + } + } + } + .comment-main { + width: 100%; + } + .comment-avatar { + flex-shrink: 0; + margin-right: 1.5rem; + width: 60px; + } + .comment-data { + align-items: center; + display: flex; + justify-content: flex-start; + margin-bottom: 1rem; + } + .comment-creator { + color: $primary; + margin-right: .5rem; + } + .comment-date { + @include font-size(small); + color: $gray-light; + } + .comment-edited { + @include font-size(small); + background: $whitish; + margin: 0 .5rem; + padding: .25rem; + .separator { + margin: 0 .25rem; + } + a { + color: $primary; + fill: $primary; + } + svg { + @include svg-size(.75rem); + margin: 0 0 0 .25rem; + } + } + .comment-options { + align-items: center; + align-self: stretch; + display: flex; + flex-basis: 50px; + flex-shrink: 0; + margin-left: 1.5rem; + .comment-option { + cursor: pointer; + opacity: 0; + } + .icon-edit { + fill: $gray-light; + margin-right: .5rem; + &:hover { + fill: $gray; + } + } + .icon-close { + fill: $gray-light; + margin-right: .5rem; + &:hover { + fill: $red; + } + } + .icon-trash { + fill: $red-light; + &:hover { + fill: $red; + } + } + } + .deleted-comment-wrapper { + border-bottom: 1px solid $whitish; + padding: 1rem 0; + width: 100%; + } + .deleted-comment-main { + @include font-size(xsmall); + color: $gray-light; + display: flex; + width: 100%; + } + .toggle-deleted-comment { + color: $primary; + fill: $primary; + margin: 0 1rem; + transition: none; + .icon-arrow-down, + .icon-arrow-up { + @include svg-size(.8rem); + margin-left: .25rem; + } + } + .restore-comment { + margin-left: auto; + transition: all .2s; + &:hover { + color: $primary; + fill: $primary; + } + .icon-reload { + @include svg-size(.8rem); + margin-right: .25rem; + } + } + .deleted-comment-comment { + margin-top: 1rem; + } + .comment-editor { + textarea { + height: 5rem; + min-height: 5rem; + } + } +} + +.comment-text { + &.wysiwyg { + margin-bottom: 0; + padding: 0; + } +} diff --git a/app/modules/history/comments/comments.controller.coffee b/app/modules/history/comments/comments.controller.coffee new file mode 100644 index 00000000..6a986321 --- /dev/null +++ b/app/modules/history/comments/comments.controller.coffee @@ -0,0 +1,28 @@ +### +# 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: comments.controller.coffee +### + +module = angular.module("taigaHistory") + +class CommentsController + @.$inject = [] + + constructor: () -> + + initializePermissions: () -> + @.canAddCommentPermission = 'comment_' + @.name + +module.controller("CommentsCtrl", CommentsController) diff --git a/app/modules/history/comments/comments.controller.spec.coffee b/app/modules/history/comments/comments.controller.spec.coffee new file mode 100644 index 00000000..dff1233a --- /dev/null +++ b/app/modules/history/comments/comments.controller.spec.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: comments.controller.spec.coffee +### + +describe "CommentsController", -> + provide = null + controller = null + mocks = {} + + _mocks = () -> + module ($provide) -> + provide = $provide + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + it "set can add comment permission", () -> + commentsCtrl = controller "CommentsCtrl" + commentsCtrl.name = "us" + commentsCtrl.initializePermissions() + expect(commentsCtrl.canAddCommentPermission).to.be.equal("comment_us") diff --git a/app/modules/history/comments/comments.directive.coffee b/app/modules/history/comments/comments.directive.coffee new file mode 100644 index 00000000..67d04bd2 --- /dev/null +++ b/app/modules/history/comments/comments.directive.coffee @@ -0,0 +1,50 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: comments.directive.coffee +### + +module = angular.module('taigaHistory') + +CommentsDirective = () -> + link = (scope, el, attrs, ctrl) -> + ctrl.initializePermissions() + + return { + scope: { + type: "<", + name: "@", + object: "@", + comments: "<", + onEditMode: "&", + onDeleteComment: "&", + onRestoreDeletedComment: "&", + onAddComment: "&", + onEditComment: "&", + editMode: "<", + loading: "<", + deleting: "<", + editing: "<", + projectId: "=" + }, + templateUrl:"history/comments/comments.html", + bindToController: true, + controller: 'CommentsCtrl', + controllerAs: "vm" + link: link + } + +module.directive("tgComments", CommentsDirective) diff --git a/app/modules/history/comments/comments.jade b/app/modules/history/comments/comments.jade new file mode 100644 index 00000000..249863eb --- /dev/null +++ b/app/modules/history/comments/comments.jade @@ -0,0 +1,39 @@ +include ../../../partials/common/components/wysiwyg.jade + +section.comments + .comments-wrapper + tg-comment.comment( + ng-repeat="comment in vm.comments track by comment.id" + ng-class="{'deleted-comment': comment.delete_comment_date}" + comment="comment" + name="{{vm.name}}" + loading="vm.loading" + editing="vm.editing" + deleting="vm.deleting" + object="{{vm.object}}" + edit-mode="vm.editMode[comment.id]" + on-edit-mode="vm.onEditMode({commentId: commentId})" + on-delete-comment="vm.onDeleteComment({commentId: commentId})" + on-restore-deleted-comment="vm.onRestoreDeletedComment({commentId: commentId})" + on-edit-comment="vm.onEditComment({commentId: commentId, commentData: commentData})" + ) + tg-editable-wysiwyg.add-comment( + ng-model="vm.type" + tg-check-permission="{{::vm.canAddCommentPermission}}" + tg-toggle-comment + ) + textarea( + ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}" + tg-markitup="tg-markitup" + ng-model="vm.type.comment" + ) + +wysihelp + .save-comment-wrapper + button.button-green.save-comment( + type="button" + title="{{'COMMENTS.COMMENT' | translate}}" + translate="COMMENTS.COMMENT" + ng-disabled="!vm.type.comment.length || vm.loading" + ng-click="vm.onAddComment()" + tg-loading="vm.loading" + ) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee new file mode 100644 index 00000000..93f014f3 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.controller.coffee @@ -0,0 +1,37 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class LightboxDisplayHistoricController + @.$inject = [ + "$tgResources", + ] + + constructor: (@rs) -> + + _loadHistoric: () -> + type = @.name + objectId = @.object + activityId = @.comment.id + + @rs.history.getCommentHistory(type, objectId, activityId).then (data) => + @.commentHistoryEntries = data + +module.controller("LightboxDisplayHistoricCtrl", LightboxDisplayHistoricController) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee new file mode 100644 index 00000000..92167c96 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.controller.spec.coffee @@ -0,0 +1,63 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: subscriptions.controller.spec.coffee +### + +describe "LightboxDisplayHistoricController", -> + provide = null + controller = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + history: { + getCommentHistory: sinon.stub() + } + } + + provide.value "$tgResources", mocks.tgResources + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + return null + + beforeEach -> + module "taigaHistory" + _mocks() + + inject ($controller) -> + controller = $controller + + it "load historic", (done) -> + historicLbCtrl = controller "LightboxDisplayHistoricCtrl" + + historicLbCtrl.name = "type" + historicLbCtrl.object = 1 + historicLbCtrl.comment = {} + historicLbCtrl.comment.id = 1 + + type = historicLbCtrl.name + objectId = historicLbCtrl.object + activityId = historicLbCtrl.comment.id + + promise = mocks.tgResources.history.getCommentHistory.withArgs(type, objectId, activityId).promise().resolve() + + historicLbCtrl._loadHistoric().then (data) -> + expect(historicLbCtrl.commentHistoryEntries).is.equal(data) + done() diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee b/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee new file mode 100644 index 00000000..b09c99a0 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +LightboxDisplayHistoricDirective = (lightboxService) -> + link = (scope, el, attrs, ctrl) -> + ctrl._loadHistoric() + lightboxService.open(el) + + return { + scope: {}, + bindToController: { + name: '=', + object: '=', + comment: '=' + }, + templateUrl:"history/history-lightbox/comment-history-lightbox.html", + controller: "LightboxDisplayHistoricCtrl", + controllerAs: "vm", + link: link + } + +module.directive("tgLbDisplayHistoric", LightboxDisplayHistoricDirective) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.jade b/app/modules/history/history-lightbox/comment-history-lightbox.jade new file mode 100644 index 00000000..32127f9b --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.jade @@ -0,0 +1,9 @@ +tg-lightbox-close + +.history-container + h2.title(translate="COMMENTS.HISTORY.TITLE") + .history-wrapper + tg-history-entry.entry( + ng-repeat="entry in vm.commentHistoryEntries" + entry="entry" + ) diff --git a/app/modules/history/history-lightbox/comment-history-lightbox.scss b/app/modules/history/history-lightbox/comment-history-lightbox.scss new file mode 100644 index 00000000..47e79868 --- /dev/null +++ b/app/modules/history/history-lightbox/comment-history-lightbox.scss @@ -0,0 +1,13 @@ +.lightbox-display-historic { + display: none; + .history-container { + max-width: 800px; + width: 90%; + } + .history-wrapper { + max-height: 600px; + overflow-x: hidden; + overflow-y: auto; + padding: 2rem; + } +} diff --git a/app/modules/history/history-lightbox/history-entry.directive.coffee b/app/modules/history/history-lightbox/history-entry.directive.coffee new file mode 100644 index 00000000..42814d29 --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: comment.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryEntryDirective = (lightboxService) -> + + return { + scope: { + entry: "<" + }, + templateUrl:"history/history-lightbox/history-entry.html", + } + +module.directive("tgHistoryEntry", HistoryEntryDirective) diff --git a/app/modules/history/history-lightbox/history-entry.jade b/app/modules/history/history-lightbox/history-entry.jade new file mode 100644 index 00000000..b3c5ca4a --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.jade @@ -0,0 +1,19 @@ +.entry-wrapper + img.entry-avatar( + tg-avatar="entry.user" + ng-alt="{{entry.user.name}}" + ) + .entry-main + .entry-data + span.entry-creator {{entry.user.full_name_display}} + span.entry-date {{entry.date | momentFormat:'DD MMM YYYY HH:mm'}} + tg-svg.display-full-entry( + svg-icon="icon-arrow-down" + ng-class="{'inactive': !displayFullEntry}" + ng-click="displayFullEntry=!displayFullEntry" + ng-show="entry.comment.length >= 75" + ) + .entry-text( + ng-class="{'ellipsed': !displayFullEntry && entry.comment.length >= 75, 'blurry': entry.comment.length >= 75 && !displayFullEntry}" + ng-bind-html="entry.comment_html" + ) diff --git a/app/modules/history/history-lightbox/history-entry.scss b/app/modules/history/history-lightbox/history-entry.scss new file mode 100644 index 00000000..fa0a2763 --- /dev/null +++ b/app/modules/history/history-lightbox/history-entry.scss @@ -0,0 +1,63 @@ +.entry { + display: block; + .entry-wrapper { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + } + .entry-avatar { + flex-basis: 50px; + flex-grow: 0; + flex-shrink: 0; + margin-right: 1.5rem; + width: 50px; + } + .entry-main { + flex: 1; + max-width: calc(100% - 100px); + } + .entry-data { + align-items: flex-start; + display: flex; + margin-bottom: .5rem; + } + .entry-creator { + color: $primary; + margin-right: .5rem; + } + .entry-date { + @include font-size(small); + color: $gray-light; + } + .display-full-entry { + @include svg-size(1.25rem); + cursor: pointer; + fill: $primary; + margin-left: auto; + transform: rotate(0); + transition: transform .2s; + &.inactive { + transform: rotate(180deg); + } + } + .entry-text { + margin-bottom: 0; + &.ellipsed { + max-height: 3rem; + overflow: hidden; + } + &.blurry { + position: relative; + &::after { + background-image: linear-gradient(to top, $white, transparent); + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + } +} diff --git a/app/modules/history/history-tabs/history-tabs.directive.coffee b/app/modules/history/history-tabs/history-tabs.directive.coffee new file mode 100644 index 00000000..e7e48e74 --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.directive.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: history-tabs.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryTabsDirective = () -> + return { + templateUrl:"history/history-tabs/history-tabs.html", + scope: { + showCommentTab: "&", + showActivityTab: "&" + onActiveComments: "&", + onActiveActivities: "&", + onOrderComments: "&" + activeTab: "<", + commentsNum: "<", + activitiesNum: "<", + onReverse: "<" + } + } + +module.directive("tgHistoryTabs", HistoryTabsDirective) diff --git a/app/modules/history/history-tabs/history-tabs.jade b/app/modules/history/history-tabs/history-tabs.jade new file mode 100644 index 00000000..d9e25e30 --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.jade @@ -0,0 +1,43 @@ +nav.history-tabs + a.history-tab.e2e-comments-tab( + ng-if="showCommentTab()" + href="" + title="{{COMMENTS.COMMENT}}" + ng-click="onActiveComments()" + ng-class="{active: activeTab}" + translate="COMMENTS.COMMENTS_COUNT" + translate-values="{comments: commentsNum}" + ) + a.history-tab.e2e-activity-tab( + ng-if="showActivityTab()" + href="" + title="Activities" + ng-click="onActiveActivities()" + ng-class="{active: !activeTab}" + translate="ACTIVITY.ACTIVITIES_COUNT" + translate-values="{activities: activitiesNum}" + ) + a.order-comments( + href="" + title="Order Comments" + ng-click="onOrderComments()" + ng-class="{'new-first': top, 'old-first': !top}" + ng-if="commentsNum > 1 && activeTab" + ) + + span( + translate="COMMENTS.OLDER_FIRST" + ng-if="onReverse" + ) + tg-svg( + svg-icon="icon-arrow-down" + ng-if="onReverse" + ) + span( + translate="COMMENTS.RECENT_FIRST" + ng-if="!onReverse" + ) + tg-svg( + svg-icon="icon-arrow-up" + ng-if="!onReverse" + ) diff --git a/app/modules/history/history-tabs/history-tabs.scss b/app/modules/history/history-tabs/history-tabs.scss new file mode 100644 index 00000000..9c7bef56 --- /dev/null +++ b/app/modules/history/history-tabs/history-tabs.scss @@ -0,0 +1,11 @@ +.history-tabs { + .order-comments { + @include font-type(light); + margin-left: auto; + transition: none; + } + .icon-arrow-up, + .icon-arrow-down { + @include svg-size(.75rem); + } +} diff --git a/app/modules/history/history.controller.coffee b/app/modules/history/history.controller.coffee new file mode 100644 index 00000000..d80a5ec0 --- /dev/null +++ b/app/modules/history/history.controller.coffee @@ -0,0 +1,108 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class HistorySectionController + @.$inject = [ + "$tgResources", + "$tgRepo", + "$tgStorage", + "tgProjectService", + ] + + constructor: (@rs, @repo, @storage, @projectService) -> + @.editing = null + @.deleting = null + @.editMode = {} + @.viewComments = true + @._loadHistory() + @.reverse = @storage.get("orderComments") + + _loadHistory: () -> + @rs.history.get(@.name, @.id).then (history) => + @._getComments(history) + @._getActivities(history) + + _getComments: (comments) -> + @.comments = _.filter(comments, (item) -> item.comment != "") + if @.reverse + @.comments - _.reverse(@.comments) + @.commentsNum = @.comments.length + + _getActivities: (activities) -> + @.activities = _.filter(activities, (item) -> Object.keys(item.values_diff).length > 0) + @.activitiesNum = @.activities.length + + showHistorySection: () -> + return @.showCommentTab() or @.showActivityTab() + + showCommentTab: () -> + return @.commentsNum > 0 or @projectService.hasPermission("comment_#{@.name}") + + showActivityTab: () -> + return @.activitiesNum > 0 + + toggleEditMode: (commentId) -> + @.editMode[commentId] = !@.editMode[commentId] + + onActiveHistoryTab: (active) -> + @.viewComments = active + + deleteComment: (commentId) -> + type = @.name + objectId = @.id + activityId = commentId + @.deleting = commentId + return @rs.history.deleteComment(type, objectId, activityId).then => + @._loadHistory() + @.deleting = commentId + + editComment: (commentId, comment) -> + type = @.name + objectId = @.id + activityId = commentId + @.editing = commentId + return @rs.history.editComment(type, objectId, activityId, comment).then => + @._loadHistory() + @.toggleEditMode(commentId) + @.editing = null + + restoreDeletedComment: (commentId) -> + type = @.name + objectId = @.id + activityId = commentId + @.editing = commentId + return @rs.history.undeleteComment(type, objectId, activityId).then => + @._loadHistory() + @.editing = null + + addComment: () -> + type = @.type + @.loading = true + @repo.save(@.type).then => + @._loadHistory() + @.loading = false + + onOrderComments: () -> + @.reverse = !@.reverse + @storage.set("orderComments", @.reverse) + @._loadHistory() + +module.controller("HistorySection", HistorySectionController) diff --git a/app/modules/history/history.controller.spec.coffee b/app/modules/history/history.controller.spec.coffee new file mode 100644 index 00000000..2a97b2ca --- /dev/null +++ b/app/modules/history/history.controller.spec.coffee @@ -0,0 +1,221 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: subscriptions.controller.spec.coffee +### + +describe "HistorySection", -> + provide = null + controller = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + history: { + get: sinon.stub() + deleteComment: sinon.stub() + undeleteComment: sinon.stub() + editComment: sinon.stub() + } + } + + provide.value "$tgResources", mocks.tgResources + + _mockTgRepo = () -> + mocks.tgRepo = { + save: sinon.stub() + } + + provide.value "$tgRepo", mocks.tgRepo + + _mocktgStorage = () -> + mocks.tgStorage = { + get: sinon.stub() + set: sinon.stub() + } + provide.value "$tgStorage", mocks.tgStorage + + _mockTgProjectService = () -> + mocks.tgProjectService = { + hasPermission: sinon.stub() + } + provide.value "tgProjectService", mocks.tgProjectService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + _mockTgRepo() + _mocktgStorage() + _mockTgProjectService() + return null + + beforeEach -> + module "taigaHistory" + + _mocks() + + inject ($controller) -> + controller = $controller + promise = mocks.tgResources.history.get.promise().resolve() + + it "load historic", (done) -> + historyCtrl = controller "HistorySection" + + historyCtrl._getComments = sinon.stub() + historyCtrl._getActivities = sinon.stub() + + name = "name" + id = 4 + + promise = mocks.tgResources.history.get.withArgs(name, id).promise().resolve() + historyCtrl._loadHistory().then (data) -> + expect(historyCtrl._getComments).have.been.calledWith(data) + expect(historyCtrl._getActivities).have.been.calledWith(data) + done() + + it "get Comments older first", () -> + historyCtrl = controller "HistorySection" + + comments = ['comment3', 'comment2', 'comment1'] + historyCtrl.reverse = false + + historyCtrl._getComments(comments) + expect(historyCtrl.comments).to.be.eql(['comment3', 'comment2', 'comment1']) + expect(historyCtrl.commentsNum).to.be.equal(3) + + it "get Comments newer first", () -> + historyCtrl = controller "HistorySection" + + comments = ['comment3', 'comment2', 'comment1'] + historyCtrl.reverse = true + + historyCtrl._getComments(comments) + expect(historyCtrl.comments).to.be.eql(['comment1', 'comment2', 'comment3']) + expect(historyCtrl.commentsNum).to.be.equal(3) + + it "get activities", () -> + historyCtrl = controller "HistorySection" + activities = { + 'activity1': { + 'values_diff': {"k1": [0, 1]} + }, + 'activity2': { + 'values_diff': {"k2": [0, 1]} + }, + 'activity3': { + 'values_diff': {"k3": [0, 1]} + }, + } + + historyCtrl._getActivities(activities) + + historyCtrl.activities = activities + expect(historyCtrl.activitiesNum).to.be.equal(3) + + it "on active history tab", () -> + historyCtrl = controller "HistorySection" + active = true + historyCtrl.onActiveHistoryTab(active) + expect(historyCtrl.viewComments).to.be.true + + it "on inactive history tab", () -> + historyCtrl = controller "HistorySection" + active = false + historyCtrl.onActiveHistoryTab(active) + expect(historyCtrl.viewComments).to.be.false + + it "delete comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = 7 + + promise = mocks.tgResources.history.deleteComment.withArgs(type, objectId, commentId).promise().resolve() + + historyCtrl.deleting = true + historyCtrl.deleteComment(commentId).then () -> + expect(historyCtrl._loadHistory).have.been.called + expect(historyCtrl.deleting).to.be.equal(7) + + it "edit comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + activityId = 7 + comment = "blablabla" + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = activityId + + promise = mocks.tgResources.history.editComment.withArgs(type, objectId, activityId, comment).promise().resolve() + + historyCtrl.editing = 7 + historyCtrl.editComment(commentId, comment).then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.editing).to.be.null + + it "restore comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.name = "type" + historyCtrl.id = 1 + activityId = 7 + + type = historyCtrl.name + objectId = historyCtrl.id + commentId = activityId + + promise = mocks.tgResources.history.undeleteComment.withArgs(type, objectId, activityId).promise().resolve() + + historyCtrl.editing = 7 + historyCtrl.restoreDeletedComment(commentId).then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.editing).to.be.null + + it "add comment", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.type = "type" + type = historyCtrl.type + historyCtrl.loading = true + + promise = mocks.tgRepo.save.withArgs(type).promise().resolve() + + historyCtrl.addComment().then () -> + expect(historyCtrl._loadHistory).has.been.called + expect(historyCtrl.loading).to.be.false + + it "order comments", () -> + historyCtrl = controller "HistorySection" + historyCtrl._loadHistory = sinon.stub() + + historyCtrl.reverse = false + + historyCtrl.onOrderComments() + expect(historyCtrl.reverse).to.be.true + expect(mocks.tgStorage.set).has.been.calledWith("orderComments", historyCtrl.reverse) + expect(historyCtrl._loadHistory).has.been.called diff --git a/app/modules/history/history.directive.coffee b/app/modules/history/history.directive.coffee new file mode 100644 index 00000000..eebaf063 --- /dev/null +++ b/app/modules/history/history.directive.coffee @@ -0,0 +1,42 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistorySectionDirective = () -> + link = (scope, el, attr, ctrl) -> + scope.$on "object:updated", -> ctrl._loadHistory(scope.type, scope.id) + + return { + link: link, + templateUrl:"history/history.html", + controller: "HistorySection", + controllerAs: "vm", + bindToController: true, + scope: { + type: "=", + name: "@", + id: "=", + projectId: "=" + } + } + +HistorySectionDirective.$inject = [] + +module.directive("tgHistorySection", HistorySectionDirective) diff --git a/app/modules/history/history.jade b/app/modules/history/history.jade new file mode 100644 index 00000000..eaa9af8d --- /dev/null +++ b/app/modules/history/history.jade @@ -0,0 +1,36 @@ +section.history( + ng-if="vm.showHistorySection()" +) + tg-history-tabs( + show-comment-tab="vm.showCommentTab()" + show-activity-tab="vm.showActivityTab()" + on-active-comments="vm.onActiveHistoryTab(true)" + on-active-activities="vm.onActiveHistoryTab(false)" + active-tab="vm.viewComments", + on-order-comments="vm.onOrderComments()" + comments-num="vm.commentsNum" + activities-num="vm.activitiesNum" + on-reverse="vm.reverse" + ) + tg-comments( + ng-if="vm.viewComments" + comments="vm.comments" + on-delete-comment="vm.deleteComment(commentId)" + on-restore-deleted-comment="vm.restoreDeletedComment(commentId)" + on-edit-mode="vm.toggleEditMode(commentId)" + on-add-comment="vm.addComment()" + on-edit-comment="vm.editComment(commentId, commentData)" + edit-mode="vm.editMode" + + object="{{vm.id}}" + type="vm.type" + name="{{vm.name}}" + loading="vm.loading" + editing="vm.editing" + deleting="vm.deleting" + project-id="vm.projectId" + ) + tg-history( + ng-if="!vm.viewComments" + activities="vm.activities" + ) diff --git a/app/modules/history/history.module.coffee b/app/modules/history/history.module.coffee new file mode 100644 index 00000000..6089087a --- /dev/null +++ b/app/modules/history/history.module.coffee @@ -0,0 +1,20 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: history.module.coffee +### + +angular.module("taigaHistory", []) diff --git a/app/modules/history/history/history-diff.controller.coffee b/app/modules/history/history/history-diff.controller.coffee new file mode 100644 index 00000000..4096f44d --- /dev/null +++ b/app/modules/history/history/history-diff.controller.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: history.controller.coffee +### + +module = angular.module("taigaHistory") + +class ActivitiesDiffController + @.$inject = [ + ] + + constructor: () -> + + diffTags: () -> + if @.type == 'tags' + @.diffRemoveTags = _.difference(@.diff[0], @.diff[1]).toString() + @.diffAddTags = _.difference(@.diff[1], @.diff[0]).toString() + + +module.controller("ActivitiesDiffCtrl", ActivitiesDiffController) diff --git a/app/modules/history/history/history-diff.controller.spec.coffee b/app/modules/history/history/history-diff.controller.spec.coffee new file mode 100644 index 00000000..c04900d9 --- /dev/null +++ b/app/modules/history/history/history-diff.controller.spec.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: subscriptions.controller.spec.coffee +### + +describe "ActivitiesDiffController", -> + provide = null + controller = null + mocks = {} + + beforeEach -> + module "taigaHistory" + + inject ($controller) -> + controller = $controller + + it "Check diff between tags", () -> + activitiesDiffCtrl = controller "ActivitiesDiffCtrl" + + activitiesDiffCtrl.type = "tags" + + activitiesDiffCtrl.diff = [ + ["architecto", "perspiciatis", "testafo"], + ["architecto", "perspiciatis", "testafo", "fasto"] + ] + + activitiesDiffCtrl.diffTags() + expect(activitiesDiffCtrl.diffRemoveTags).to.be.equal('') + expect(activitiesDiffCtrl.diffAddTags).to.be.equal('fasto') diff --git a/app/modules/history/history/history-diff.directive.coffee b/app/modules/history/history/history-diff.directive.coffee new file mode 100644 index 00000000..481c27ec --- /dev/null +++ b/app/modules/history/history/history-diff.directive.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryDiffDirective = () -> + link = (scope, el, attrs, ctrl) -> + ctrl.diffTags() + + return { + scope: { + type: "<", + diff: "<" + }, + templateUrl:"history/history/history-diff.html", + controller: "ActivitiesDiffCtrl", + controllerAs: 'vm', + bindToController: true, + link: link + } + +module.directive("tgHistoryDiff", HistoryDiffDirective) diff --git a/app/modules/history/history/history-diff.jade b/app/modules/history/history/history-diff.jade new file mode 100644 index 00000000..58ea6d6a --- /dev/null +++ b/app/modules/history/history/history-diff.jade @@ -0,0 +1,64 @@ +.diff-wrapper( + ng-if="vm.type == 'points'" +) + include history-templates/history-points + +.diff-wrapper( + ng-if="vm.type == 'attachments'" +) + include history-templates/history-attachments + +.diff-wrapper( + ng-if="vm.type == 'milestone'" +) + include history-templates/history-milestone + +.diff-wrapper( + ng-if="vm.type == 'status'" +) + include history-templates/history-status + +.diff-wrapper( + ng-if="vm.type == 'subject'" +) + include history-templates/history-subject + +.diff-wrapper( + ng-if="vm.type == 'description_diff'" +) + include history-templates/history-description + +.diff-wrapper( + ng-if="vm.type == 'assigned_to'" +) + include history-templates/history-assigned + +.diff-wrapper( + ng-if="vm.type == 'tags'" +) + include history-templates/history-tags + +.diff-wrapper( + ng-if="vm.type == 'custom_attributes'" +) + include history-templates/history-custom-attributes + +.diff-wrapper( + ng-if="vm.type == 'color'" +) + include history-templates/history-color + +.diff-wrapper( + ng-if="vm.type == 'team_requirement'" +) + include history-templates/team-requirement + +.diff-wrapper( + ng-if="vm.type == 'client_requirement'" +) + include history-templates/client-requirement + +.diff-wrapper( + ng-if="vm.type == 'is_blocked'" +) + include history-templates/blocked diff --git a/app/modules/history/history/history-templates/blocked.jade b/app/modules/history/history/history-templates/blocked.jade new file mode 100644 index 00000000..7a2fb580 --- /dev/null +++ b/app/modules/history/history/history-templates/blocked.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.BLOCKED" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/client-requirement.jade b/app/modules/history/history/history-templates/client-requirement.jade new file mode 100644 index 00000000..10649a6a --- /dev/null +++ b/app/modules/history/history/history-templates/client-requirement.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.CLIENT_REQUIREMENT" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-assigned.jade b/app/modules/history/history/history-templates/history-assigned.jade new file mode 100644 index 00000000..64fb23a6 --- /dev/null +++ b/app/modules/history/history/history-templates/history-assigned.jade @@ -0,0 +1,13 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.FIELDS.ASSIGNED_TO" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + span.diff(ng-if="!vm.diff[0]" translate="ACTIVITY.VALUES.UNASSIGNED") + + tg-svg( + svg-icon="icon-arrow-right" + ) + + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} + span.diff(ng-if="!vm.diff[1]" translate="ACTIVITY.VALUES.UNASSIGNED") diff --git a/app/modules/history/history/history-templates/history-attachments.jade b/app/modules/history/history/history-templates/history-attachments.jade new file mode 100644 index 00000000..6deebc32 --- /dev/null +++ b/app/modules/history/history/history-templates/history-attachments.jade @@ -0,0 +1,37 @@ +.diff-attachments-new( + ng-if="vm.diff.new.length" + ng-repeat="newAttachment in vm.diff.new" +) + span.key(translate="ACTIVITY.NEW_ATTACHMENT") + span.diff {{newAttachment.filename}} +.diff-attachments-update( + ng-if="vm.diff.changed.length" + ng-repeat="editAttachment in vm.diff.changed" +) + span.key( + translate="ACTIVITY.UPDATED_ATTACHMENT" + translate-values="{filename: editAttachment.filename}" + ) + span.diff(ng-if="editAttachment.changes.is_deprecated") + span( + ng-if="editAttachment.changes.is_deprecated[1] == false" + translate="ACTIVITY.BECAME_UNDEPRECATED" + ) + span( + ng-if="editAttachment.changes.is_deprecated[1] == true" + translate="ACTIVITY.BECAME_DEPRECATED" + ) + span.diff(ng-if="editAttachment.changes.description") + span(ng-if='editAttachment.changes.description[0].length') {{editAttachment.changes.description[0]}} + span(ng-if='!editAttachment.changes.description[0].length') ... + tg-svg( + svg-icon="icon-arrow-right" + ) + span {{editAttachment.changes.description[1]}} + +.diff-attachments-deleted( + ng-if="vm.diff.deleted.length" + ng-repeat="deletedAttachment in vm.diff.deleted" +) + span.key(translate="ACTIVITY.DELETED_ATTACHMENT") + span.diff {{deletedAttachment.filename}} diff --git a/app/modules/history/history/history-templates/history-color.jade b/app/modules/history/history/history-templates/history-color.jade new file mode 100644 index 00000000..89b69d87 --- /dev/null +++ b/app/modules/history/history/history-templates/history-color.jade @@ -0,0 +1,17 @@ +.diff-color-wrapper + span.key( + translate="ACTIVITY.FIELDS.COLOR" + ) + span.diff( + ng-if="vm.diff[0]" + ng-style="{background: vm.diff[0]}" + title="{{vm.diff[0]}}" + ) + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff( + ng-if="vm.diff[1]" + ng-style="{background: vm.diff[1]}" + title="{{vm.diff[1]}}" + ) diff --git a/app/modules/history/history/history-templates/history-custom-attributes.jade b/app/modules/history/history/history-templates/history-custom-attributes.jade new file mode 100644 index 00000000..69d6a1bf --- /dev/null +++ b/app/modules/history/history/history-templates/history-custom-attributes.jade @@ -0,0 +1,19 @@ +.diff-custom-new( + ng-if="vm.diff.new.length" + ng-repeat="newCustom in vm.diff.new" +) + span.key(translate="ACTIVITY.CREATED_CUSTOM_ATTRIBUTE") + span.diff ({{newCustom.name}}) + span.diff {{newCustom.value}} + +.diff-custom-new( + ng-if="vm.diff.changed.length" + ng-repeat="changeCustom in vm.diff.changed" +) + span.key(translate="ACTIVITY.UPDATED_CUSTOM_ATTRIBUTE") + span.diff ({{changeCustom.name}}) + span.diff {{changeCustom.changes.value[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{changeCustom.changes.value[1]}} diff --git a/app/modules/history/history/history-templates/history-description.jade b/app/modules/history/history/history-templates/history-description.jade new file mode 100644 index 00000000..9c4dafb8 --- /dev/null +++ b/app/modules/history/history/history-templates/history-description.jade @@ -0,0 +1,12 @@ +.diff-status-wrapper + p.key( + translate="ACTIVITY.FIELDS.DESCRIPTION" + ) + p.diff( + ng-if="vm.diff[0]" + ng-bind-html="vm.diff[0]" + ) + p.diff( + ng-if="vm.diff[1]" + ng-bind-html="vm.diff[1]" + ) diff --git a/app/modules/history/history/history-templates/history-milestone.jade b/app/modules/history/history/history-templates/history-milestone.jade new file mode 100644 index 00000000..61b78d3d --- /dev/null +++ b/app/modules/history/history/history-templates/history-milestone.jade @@ -0,0 +1,11 @@ +.diff-milestone-wrapper + span.key( + translate="ACTIVITY.FIELDS.MILESTONE" + ) + span.diff(ng-if="vm.diff[0] != null") {{vm.diff[0]}} + span.diff(ng-if="vm.diff[0] == null") ... + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1] != null") {{vm.diff[1]}} + span.diff(ng-if="vm.diff[1] == null") ... diff --git a/app/modules/history/history/history-templates/history-points.jade b/app/modules/history/history/history-templates/history-points.jade new file mode 100644 index 00000000..85c99704 --- /dev/null +++ b/app/modules/history/history/history-templates/history-points.jade @@ -0,0 +1,11 @@ +.diff-points-wrapper(ng-repeat="(key, diff) in vm.diff") + span.key( + translate="ACTIVITY.US_POINTS" + translate-values="{role: vm.diff.key}" + ) + span.diff {{diff[0]}} + tg-svg.comment-option( + svg-icon="icon-arrow-right" + svg-title-translate="COMMON.EDIT" + ) + span.diff {{diff[1]}} diff --git a/app/modules/history/history/history-templates/history-status.jade b/app/modules/history/history/history-templates/history-status.jade new file mode 100644 index 00000000..33af2ea2 --- /dev/null +++ b/app/modules/history/history/history-templates/history-status.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.FIELDS.STATUS" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-subject.jade b/app/modules/history/history/history-templates/history-subject.jade new file mode 100644 index 00000000..e038ba01 --- /dev/null +++ b/app/modules/history/history/history-templates/history-subject.jade @@ -0,0 +1,9 @@ +.diff-subject-wrapper + span.key( + translate="ACTIVITY.FIELDS.SUBJECT" + ) + span.diff(ng-if="vm.diff[0]") {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff(ng-if="vm.diff[1]") {{vm.diff[1]}} diff --git a/app/modules/history/history/history-templates/history-tags.jade b/app/modules/history/history/history-templates/history-tags.jade new file mode 100644 index 00000000..32ec884b --- /dev/null +++ b/app/modules/history/history/history-templates/history-tags.jade @@ -0,0 +1,8 @@ +.diff-tags-wrapper + p(ng-if="vm.diffRemoveTags") + span.key(translate="ACTIVITY.TAGS_REMOVED") + span.diff {{vm.diffRemoveTags}} + + p(ng-if="vm.diffAddTags") + span.key(translate="ACTIVITY.TAGS_ADDED") + span.diff {{vm.diffAddTags}} diff --git a/app/modules/history/history/history-templates/history-templates.scss b/app/modules/history/history/history-templates/history-templates.scss new file mode 100644 index 00000000..504bd69a --- /dev/null +++ b/app/modules/history/history/history-templates/history-templates.scss @@ -0,0 +1,37 @@ +.activity-diff { + .key { + @include font-type(bold); + background: $whitish; + margin-right: .5rem; + padding: .25rem; + } + .diff { + line-height: 1.6; + } + .icon-arrow-right { + @include svg-size(.75rem); + fill: $gray-light; + margin: 0 .5rem; + } + .diff-status-wrapper { + p { + display: inline-block; + } + ins { + background: lighten(rgba($primary-light, .3), 20%); + text-decoration: underline; + } + del { + background: rgba($red-light, .3); + } + } + .diff-color-wrapper { + align-items: center; + display: flex; + .diff { + display: inline-block; + height: 1.2rem; + width: 1.2rem; + } + } +} diff --git a/app/modules/history/history/history-templates/team-requirement.jade b/app/modules/history/history/history-templates/team-requirement.jade new file mode 100644 index 00000000..592d1e50 --- /dev/null +++ b/app/modules/history/history/history-templates/team-requirement.jade @@ -0,0 +1,9 @@ +.diff-status-wrapper + span.key( + translate="ACTIVITY.TEAM_REQUIREMENT" + ) + span.diff {{vm.diff[0]}} + tg-svg( + svg-icon="icon-arrow-right" + ) + span.diff {{vm.diff[1]}} diff --git a/app/modules/history/history/history.directive.coffee b/app/modules/history/history/history.directive.coffee new file mode 100644 index 00000000..40862178 --- /dev/null +++ b/app/modules/history/history/history.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: history.directive.coffee +### + +module = angular.module('taigaHistory') + +HistoryDirective = () -> + link = (scope, el, attrs) -> + + return { + scope: { + activities: "<" + }, + templateUrl:"history/history/history.html", + link: link + } + +module.directive("tgHistory", HistoryDirective) diff --git a/app/modules/history/history/history.jade b/app/modules/history/history/history.jade new file mode 100644 index 00000000..9e3836fb --- /dev/null +++ b/app/modules/history/history/history.jade @@ -0,0 +1,19 @@ +section.activities + .activities-wrapper + .activity(ng-repeat="activity in activities track by activity.id") + img.activity-avatar( + tg-avatar="activity.user" + ng-alt="{{activity.user.name}}" + ) + .activity-main + .activity-data + span.activity-creator {{activity.user.name}} + span.activity-date {{activity.created_at | momentFormat:'DD MMM YYYY HH:mm'}} + p.activity-text(ng-if="activity.comment") {{activity.comment}} + + .activity-diff( + ng-repeat="(key, diff) in activity.values_diff" + tg-history-diff + type='key' + diff='diff' + ) diff --git a/app/modules/history/history/history.scss b/app/modules/history/history/history.scss new file mode 100644 index 00000000..6e985fee --- /dev/null +++ b/app/modules/history/history/history.scss @@ -0,0 +1,21 @@ +.activity { + align-items: flex-start; + border-bottom: 1px solid $whitish; + display: flex; + padding: 2rem 0; + .activity-avatar { + flex-shrink: 0; + margin-right: 1.5rem; + width: 60px; + } + .activity-data { + margin-bottom: 1rem; + } + .activity-creator { + color: $primary; + margin-right: .5rem; + } + .activity-date { + color: $gray-light; + } +} diff --git a/app/modules/home/duties/duty.directive.coffee b/app/modules/home/duties/duty.directive.coffee index 5ffbf653..e4f40268 100644 --- a/app/modules/home/duties/duty.directive.coffee +++ b/app/modules/home/duties/duty.directive.coffee @@ -21,9 +21,12 @@ DutyDirective = (navurls, $translate) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} scope.vm.duty = scope.duty + scope.vm.type = scope.type scope.vm.getDutyType = () -> if scope.vm.duty + if scope.vm.duty.get('_name') == "epics" + return $translate.instant("COMMON.EPIC") if scope.vm.duty.get('_name') == "userstories" return $translate.instant("COMMON.USER_STORY") if scope.vm.duty.get('_name') == "tasks" @@ -34,7 +37,8 @@ DutyDirective = (navurls, $translate) -> return { templateUrl: "home/duties/duty.html" scope: { - "duty": "=tgDuty" + "duty": "=tgDuty", + "type": "@" } link: link } diff --git a/app/modules/home/duties/duty.jade b/app/modules/home/duties/duty.jade index fa3b5411..970a3a4c 100644 --- a/app/modules/home/duties/duty.jade +++ b/app/modules/home/duties/duty.jade @@ -1,27 +1,33 @@ a.list-itemtype-ticket( href="{{ ::vm.duty.get('url') }}" title="{{ ::duty.get('subject') }}" - ng-class="{'blocked': vm.duty.get('is_blocked'), 'blocked-project': vm.duty.get('blockedProject')}" + ng-class="{'blocked': vm.duty.get('is_blocked'), 'blocked-project': vm.duty.getIn(['project', 'blocked_code'])}" ) - div.list-itemtype-avatar(ng-if="::vm.duty.get('assigned_to_extra_info')") + div.list-itemtype-avatar(ng-if="vm.type == 'working-on'") img( - ng-src="{{ ::vm.duty.get('assigned_to_extra_info').get('photo') }}" - title="{{ ::vm.duty.get('assigned_to_extra_info').get('full_name_display') }}" + tg-project-logo-small-src="::vm.duty.get('project')" + title="{{ ::vm.duty.getIn(['project', 'name']) }}" ) - div.list-itemtype-avatar(ng-if="::!vm.duty.get('assigned_to_extra_info')") + div.list-itemtype-avatar(ng-if="vm.type == 'watching'") img( + ng-if="vm.duty.get('assigned_to_extra_info')" + title="{{ ::vm.duty.get('assigned_to_extra_info').get('full_name_display') }}" + tg-avatar="vm.duty.get('assigned_to_extra_info')" + ) + img( + ng-if="!vm.duty.get('assigned_to_extra_info')" src="/#{v}/images/unnamed.png" title="{{'ACTIVITY.VALUES.UNASSIGNED' | translate}}" ) div.list-itemtype-ticket-data p - span.ticket-project {{ ::vm.duty.get('projectName')}} + span.ticket-project {{ ::vm.duty.getIn(['project', 'name']) }} span.ticket-type {{ ::vm.getDutyType() }} span.ticket-status(ng-style="{'color': vm.duty.get('status_extra_info').get('color')}") {{ ::vm.duty.get('status_extra_info').get('name') }} tg-svg( - ng-if="vm.duty.get('blockedProject')", + ng-if="vm.duty.getIn(['project', 'blocked_code'])" svg-icon="icon-blocked-project", svg-title-translate="PROJECT.BLOCKED_PROJECT.BLOCKED" ) diff --git a/app/modules/home/home.jade b/app/modules/home/home.jade index d1af66bd..c712c3d8 100644 --- a/app/modules/home/home.jade +++ b/app/modules/home/home.jade @@ -2,6 +2,8 @@ doctype html div.home-wrapper.centered div.duty-summary - div(tg-working-on) + h1 + span.green {{"HOME.DASHBOARD" | translate}} + tg-working-on.dashboard-container aside.project-list(tg-home-project-list) diff --git a/app/modules/home/home.scss b/app/modules/home/home.scss index c1fd038a..1aed3983 100644 --- a/app/modules/home/home.scss +++ b/app/modules/home/home.scss @@ -1,22 +1,52 @@ .home-wrapper { display: flex; + @include breakpoint(tablet) { + flex-direction: column; + } + @include breakpoint(mobile) { + flex-direction: column; + } .duty-summary { flex: 1; margin-right: 2rem; } + .dashboard-container { + display: flex; + flex-direction: row; + @include breakpoint(laptop) { + flex-direction: column; + } + @include breakpoint(tablet) { + flex-direction: column; + } + @include breakpoint(mobile) { + flex-direction: column; + } + } + .watching-container, + .working-on-container { + flex: 1; + padding-left: .5rem; + padding-right: .5rem; + } + .working-on-container { + margin-right: 1rem; + } .project-list { - width: 250px; + flex-basis: 250px; + flex-grow: 0; + flex-shrink: 0; } .see-more-projects-btn { display: block; } .title-bar { @include font-type(light); - @include font-size(larger); + @include font-size(large); align-content: center; - background: $whitish; + background: $mass-white; display: flex; margin: 0 0 .5rem; - padding: .9rem 1rem; + padding: .5rem 1rem; } } diff --git a/app/modules/home/home.service.coffee b/app/modules/home/home.service.coffee index fe068447..862e6b68 100644 --- a/app/modules/home/home.service.coffee +++ b/app/modules/home/home.service.coffee @@ -40,8 +40,7 @@ class HomeService extends taiga.Service url = @navurls.resolve("project-#{objType}-detail", ctx) duty = duty.set('url', url) - duty = duty.set('projectName', project.get('name')) - duty = duty.set('blockedProject', project.get('blocked_code')) + duty = duty.set('project', project) duty = duty.set("_name", objType) return duty @@ -58,6 +57,10 @@ class HomeService extends taiga.Service assignedTo = workInProgress.get("assignedTo") + if assignedTo.get("epics") + _duties = _getValidDutiesAndAttachProjectInfo(assignedTo.get("epics"), "epics") + assignedTo = assignedTo.set("epics", _duties) + if assignedTo.get("userStories") _duties = _getValidDutiesAndAttachProjectInfo(assignedTo.get("userStories"), "userstories") assignedTo = assignedTo.set("userStories", _duties) @@ -66,7 +69,6 @@ class HomeService extends taiga.Service _duties = _getValidDutiesAndAttachProjectInfo(assignedTo.get("tasks"), "tasks") assignedTo = assignedTo.set("tasks", _duties) - if assignedTo.get("issues") _duties = _getValidDutiesAndAttachProjectInfo(assignedTo.get("issues"), "issues") assignedTo = assignedTo.set("issues", _duties) @@ -74,6 +76,10 @@ class HomeService extends taiga.Service watching = workInProgress.get("watching") + if watching.get("epics") + _duties = _getValidDutiesAndAttachProjectInfo(watching.get("epics"), "epics") + watching = watching.set("epics", _duties) + if watching.get("userStories") _duties = _getValidDutiesAndAttachProjectInfo(watching.get("userStories"), "userstories") watching = watching.set("userStories", _duties) @@ -107,6 +113,14 @@ class HomeService extends taiga.Service assigned_to: userId } + params_epics = { + is_closed: false + assigned_to: userId + } + + assignedEpicsPromise = @rs.epics.listInAllProjects(params_epics).then (epics) -> + assignedTo = assignedTo.set("epics", epics) + assignedUserStoriesPromise = @rs.userstories.listInAllProjects(params_us).then (userstories) -> assignedTo = assignedTo.set("userStories", userstories) @@ -126,8 +140,16 @@ class HomeService extends taiga.Service watchers: userId } + params_epics = { + is_closed: false + watchers: userId + } + watching = Immutable.Map() + watchingEpicsPromise = @rs.epics.listInAllProjects(params_epics).then (epics) -> + watching = watching.set("epics", epics) + watchingUserStoriesPromise = @rs.userstories.listInAllProjects(params_us).then (userstories) -> watching = watching.set("userStories", userstories) @@ -140,12 +162,14 @@ class HomeService extends taiga.Service workInProgress = Immutable.Map() Promise.all([ - projectsPromise + projectsPromise, + assignedEpicsPromise, + watchingEpicsPromise, assignedUserStoriesPromise, - assignedTasksPromise, - assignedIssuesPromise, watchingUserStoriesPromise, + assignedTasksPromise, watchingTasksPromise, + assignedIssuesPromise, watchingIssuesPromise ]).then => workInProgress = workInProgress.set("assignedTo", assignedTo) diff --git a/app/modules/home/home.service.spec.coffee b/app/modules/home/home.service.spec.coffee index 41b162ff..a547feb8 100644 --- a/app/modules/home/home.service.spec.coffee +++ b/app/modules/home/home.service.spec.coffee @@ -24,10 +24,12 @@ describe "tgHome", -> _mockResources = () -> mocks.resources = {} + mocks.resources.epics = {} mocks.resources.userstories = {} mocks.resources.tasks = {} mocks.resources.issues = {} + mocks.resources.epics.listInAllProjects = sinon.stub() mocks.resources.userstories.listInAllProjects = sinon.stub() mocks.resources.tasks.listInAllProjects = sinon.stub() mocks.resources.issues.listInAllProjects = sinon.stub() @@ -73,11 +75,33 @@ describe "tgHome", -> it "get work in progress by user", (done) -> userId = 3 + project1 = {id: 1, name: "fake1", slug: "project-1"} + project2 = {id: 2, name: "fake2", slug: "project-2"} + mocks.projectsService.getProjectsByUserId .withArgs(userId) .resolve(Immutable.fromJS([ - {id: 1, name: "fake1", slug: "project-1"}, - {id: 2, name: "fake2", slug: "project-2"} + project1, + project2 + ])) + + mocks.resources.epics.listInAllProjects + .withArgs(sinon.match({ + is_closed: false + assigned_to: userId + })) + .promise() + .resolve(Immutable.fromJS([{id: 4, ref: 4, project: "1"}])) + + mocks.resources.epics.listInAllProjects + .withArgs(sinon.match({ + is_closed: false + watchers: userId + })) + .promise() + .resolve(Immutable.fromJS([ + {id: 4, ref: 4, project: "1"}, + {id: 5, ref: 5, project: "10"} # the user is not member of this project ])) mocks.resources.userstories.listInAllProjects @@ -106,6 +130,10 @@ describe "tgHome", -> .resolve(Immutable.fromJS([{id: 3, ref: 3, project: "1"}])) # mock urls + mocks.tgNavUrls.resolve + .withArgs("project-epics-detail", {project: "project-1", ref: 4}) + .returns("/testing-project/epic/1") + mocks.tgNavUrls.resolve .withArgs("project-userstories-detail", {project: "project-1", ref: 1}) .returns("/testing-project/us/1") @@ -122,60 +150,62 @@ describe "tgHome", -> .then (workInProgress) -> expect(workInProgress.toJS()).to.be.eql({ assignedTo: { + epics: [{ + id: 4, + ref: 4, + url: '/testing-project/epic/1', + project: project1, + _name: 'epics' + }] userStories: [{ id: 1, ref: 1, - project: '1', url: '/testing-project/us/1', - projectName: 'fake1', - blockedProject: undefined, + project: project1, _name: 'userstories' }] tasks: [{ id: 2, ref: 2, - project: '1', + project: project1, url: '/testing-project/tasks/1', - projectName: 'fake1', - blockedProject: undefined, _name: 'tasks' }] issues: [{ id: 3, ref: 3, - project: '1', url: '/testing-project/issues/1', - projectName: 'fake1', - blockedProject: undefined, + project: project1, _name: 'issues' }] } watching: { + epics: [{ + id: 4, + ref: 4, + url: '/testing-project/epic/1', + project: project1, + _name: 'epics' + }] userStories: [{ id: 1, ref: 1, - project: '1', url: '/testing-project/us/1', - projectName: 'fake1', - blockedProject: undefined, + project: project1, _name: 'userstories' }] tasks: [{ id: 2, ref: 2, - project: '1', url: '/testing-project/tasks/1', - projectName: 'fake1', - blockedProject: undefined, + project: project1, _name: 'tasks' }] issues: [{ id: 3, ref: 3, - project: '1', url: '/testing-project/issues/1', - projectName: 'fake1', - blockedProject: undefined, + project: project1, _name: 'issues' }] } diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade index 9ed81a65..beecb13c 100644 --- a/app/modules/home/projects/home-project-list.jade +++ b/app/modules/home/projects/home-project-list.jade @@ -5,12 +5,6 @@ section.home-project-list(ng-if="vm.projects.size") tg-repeat="project in vm.projects" ng-class="{'blocked-project': project.get('blocked_code')}" ) - .tags-container - .project-tag( - style="background: {{tag.get('color')}}" - title="{{tag.get('name')}}" - tg-repeat="tag in project.get('colorized_tags') track by tag.get('name')" - ) .project-card-inner( href="#" tg-nav="project:project=project.get('slug')" diff --git a/app/modules/home/working-on/empty.scss b/app/modules/home/working-on/empty.scss index aa10d7a3..28630947 100644 --- a/app/modules/home/working-on/empty.scss +++ b/app/modules/home/working-on/empty.scss @@ -3,7 +3,7 @@ margin-bottom: 4rem; p { @include font-type(light); - margin: 2rem 9rem 1rem; + margin: 2rem 2rem 1rem; text-align: center; } } @@ -31,13 +31,31 @@ } .line { - background: $whitish; + background: $mass-white; height: 1rem; margin-bottom: 1rem; - width: 40vw; + width: 8vw; + @include breakpoint(laptop) { + width: 30vw; + } + @include breakpoint(tablet) { + width: 30vw; + } + @include breakpoint(mobile) { + width: 30vw; + } &:last-child { margin: 0; - width: 20vw; + width: 18vw; + @include breakpoint(laptop) { + width: 50vw; + } + @include breakpoint(tablet) { + width: 50vw; + } + @include breakpoint(mobile) { + width: 50vw; + } } } } diff --git a/app/modules/home/working-on/working-on.controller.coffee b/app/modules/home/working-on/working-on.controller.coffee index dba27fc7..b02d341d 100644 --- a/app/modules/home/working-on/working-on.controller.coffee +++ b/app/modules/home/working-on/working-on.controller.coffee @@ -27,20 +27,22 @@ class WorkingOnController @.watching = Immutable.Map() _setAssignedTo: (workInProgress) -> + epics = workInProgress.get("assignedTo").get("epics") userStories = workInProgress.get("assignedTo").get("userStories") tasks = workInProgress.get("assignedTo").get("tasks") issues = workInProgress.get("assignedTo").get("issues") - @.assignedTo = userStories.concat(tasks).concat(issues) + @.assignedTo = userStories.concat(tasks).concat(issues).concat(epics) if @.assignedTo.size > 0 @.assignedTo = @.assignedTo.sortBy((elem) -> elem.get("modified_date")).reverse() _setWatching: (workInProgress) -> + epics = workInProgress.get("watching").get("epics") userStories = workInProgress.get("watching").get("userStories") tasks = workInProgress.get("watching").get("tasks") issues = workInProgress.get("watching").get("issues") - @.watching = userStories.concat(tasks).concat(issues) + @.watching = userStories.concat(tasks).concat(issues).concat(epics) if @.watching.size > 0 @.watching = @.watching.sortBy((elem) -> elem.get("modified_date")).reverse() diff --git a/app/modules/home/working-on/working-on.controller.spec.coffee b/app/modules/home/working-on/working-on.controller.spec.coffee index 39d4c1e5..d255ab18 100644 --- a/app/modules/home/working-on/working-on.controller.spec.coffee +++ b/app/modules/home/working-on/working-on.controller.spec.coffee @@ -51,14 +51,32 @@ describe "WorkingOn", -> workInProgress = Immutable.fromJS({ assignedTo: { - userStories: [{id: 1, modified_date: "2015-01-01"}, {id: 2, modified_date: "2015-01-04"}], - tasks: [{id: 3, modified_date: "2015-01-02"}, {id: 4, modified_date: "2015-01-05"}], - issues: [{id: 5, modified_date: "2015-01-03"}, {id: 6, modified_date: "2015-01-06"}] + epics: [ + {id: 7, modified_date: "2015-01-08"}, + {id: 8, modified_date: "2015-01-07"}], + userStories: [ + {id: 1, modified_date: "2015-01-01"}, + {id: 2, modified_date: "2015-01-04"}], + tasks: [ + {id: 3, modified_date: "2015-01-02"}, + {id: 4, modified_date: "2015-01-05"}], + issues: [ + {id: 5, modified_date: "2015-01-03"}, + {id: 6, modified_date: "2015-01-06"}] }, watching: { - userStories: [{id: 7, modified_date: "2015-01-01"}, {id: 8, modified_date: "2015-01-04"}], - tasks: [{id: 9, modified_date: "2015-01-02"}, {id: 10, modified_date: "2015-01-05"}], - issues: [{id: 11, modified_date: "2015-01-03"}, {id: 12, modified_date: "2015-01-06"}] + epics: [ + {id: 13, modified_date: "2015-01-07"}, + {id: 14, modified_date: "2015-01-08"}], + userStories: [ + {id: 7, modified_date: "2015-01-01"}, + {id: 8, modified_date: "2015-01-04"}], + tasks: [ + {id: 9, modified_date: "2015-01-02"}, + {id: 10, modified_date: "2015-01-05"}], + issues: [ + {id: 11, modified_date: "2015-01-03"}, + {id: 12, modified_date: "2015-01-06"}] } }) @@ -68,6 +86,8 @@ describe "WorkingOn", -> ctrl.getWorkInProgress(userId).then () -> expect(ctrl.assignedTo.toJS()).to.be.eql([ + {id: 7, modified_date: '2015-01-08'}, + {id: 8, modified_date: '2015-01-07'}, {id: 6, modified_date: '2015-01-06'}, {id: 4, modified_date: '2015-01-05'}, {id: 2, modified_date: '2015-01-04'}, @@ -77,6 +97,8 @@ describe "WorkingOn", -> ]) expect(ctrl.watching.toJS()).to.be.eql([ + {id: 14, modified_date: '2015-01-08'}, + {id: 13, modified_date: '2015-01-07'}, {id: 12, modified_date: '2015-01-06'}, {id: 10, modified_date: '2015-01-05'}, {id: 8, modified_date: '2015-01-04'}, diff --git a/app/modules/home/working-on/working-on.jade b/app/modules/home/working-on/working-on.jade index e434bfa5..dc001bd1 100644 --- a/app/modules/home/working-on/working-on.jade +++ b/app/modules/home/working-on/working-on.jade @@ -1,6 +1,3 @@ -h1 - span.green {{"HOME.DASHBOARD" | translate}} - section.working-on-container header h1.title-bar.working-on-title(translate="HOME.WORKING_ON_SECTION") @@ -8,6 +5,7 @@ section.working-on-container .working-on(ng-if="vm.assignedTo.size") .duty-single( tg-duty="duty" + type="working-on" tg-repeat="duty in vm.assignedTo" ) .working-on-empty(ng-if="vm.assignedTo != undefined && vm.assignedTo.size === 0") @@ -21,6 +19,7 @@ section.watching-container .watching(ng-if="vm.watching.size") .duty-single( tg-duty="duty" + type="watching" tg-repeat="duty in vm.watching" ng-class="{'blocked': duty.is_blocked}" ) diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.jade b/app/modules/navigation-bar/dropdown-user/dropdown-user.jade index 8131593f..2db8f69a 100644 --- a/app/modules/navigation-bar/dropdown-user/dropdown-user.jade +++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.jade @@ -3,11 +3,10 @@ a.user-avatar( title="{{ vm.user.get('full_name_display') }}" ) {{ vm.user.get('full_name_display') }} img( - ng-src="{{ vm.user.get('photo') }}" + tg-avatar="vm.user" alt="{{ vm.user.get('full_name_display') }}" - width="48px" height="40px" - ) + ) div.navbar-dropdown.dropdown-user ul diff --git a/app/modules/navigation-bar/navigation-bar.scss b/app/modules/navigation-bar/navigation-bar.scss index 8cdda49f..a04122cc 100644 --- a/app/modules/navigation-bar/navigation-bar.scss +++ b/app/modules/navigation-bar/navigation-bar.scss @@ -77,7 +77,7 @@ $dropdown-width: 350px; } img { height: 2.5rem; - padding-left: .5rem; + margin-left: .5rem; vertical-align: middle; } svg { diff --git a/app/modules/profile/profile-bar/profile-bar.jade b/app/modules/profile/profile-bar/profile-bar.jade index 8a53a4ad..a3ae25ec 100644 --- a/app/modules/profile/profile-bar/profile-bar.jade +++ b/app/modules/profile/profile-bar/profile-bar.jade @@ -1,6 +1,9 @@ section.profile-bar div.profile-image-wrapper(ng-class="::{'is-current-user': vm.isCurrentUser}") - img.profile-img(ng-src="{{::vm.user.get('big_photo')}}", alt="{{::vm.user.get('full_name')}}") + img.profile-img( + tg-avatar="vm.user" + alt="{{::vm.user.get('full_name')}}" + ) a.profile-edition(title="{{ 'USER.PROFILE.EDIT' | translate }}", tg-nav="user-settings-user-profile", translate="USER.PROFILE.EDIT") div.profile-data h1(ng-class="{'not-full-name': !vm.user.get('full_name')}") {{::vm.user.get("full_name_display")}} diff --git a/app/modules/profile/profile-contacts/profile-contacts.jade b/app/modules/profile/profile-contacts/profile-contacts.jade index 30306343..16c7d71e 100644 --- a/app/modules/profile/profile-contacts/profile-contacts.jade +++ b/app/modules/profile/profile-contacts/profile-contacts.jade @@ -3,24 +3,24 @@ section.profile-contacts div.spin img(src="/#{v}/svg/spinner-circle.svg", alt="Loading...") - div.empty-tab(ng-if="vm.contacts && !vm.contacts.size") - tg-svg(svg-icon="icon-unwatch") + div.empty-small(ng-if="vm.contacts && !vm.contacts.size") + img( + src="/#{v}/images/empty/empty_contact.png" + alt="{{ 'USER.PROFILE.CONTACTS_EMPTY' | translate }}" + ) div(ng-if="!vm.isCurrentUser") p(translate="USER.PROFILE.CONTACTS_EMPTY", translate-values="{username: vm.user.get('full_name_display')}") div(ng-if="vm.isCurrentUser") p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY") p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY_EXPLAIN") - //- - nav.profile-contact-filters - a.active(href="", title="No Filter") all - a(href="", title="Only show your team") team - a(href="", title="Only show people you follow") following - a(href="", title="Only show people follow you") followers div.list-itemtype-user(tg-repeat="contact in ::vm.contacts") a.list-itemtype-avatar(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('name')}}") - img(ng-src="{{::contact.get('photo')}}", alt="{{::contact.get('full_name')}}") + img( + tg-avatar="contact" + alt="{{::contact.get('full_name')}}" + ) div.list-itemtype-user-data h2 diff --git a/app/modules/profile/profile-favs/items/ticket.jade b/app/modules/profile/profile-favs/items/ticket.jade index 7f974ea4..60bee680 100644 --- a/app/modules/profile/profile-favs/items/ticket.jade +++ b/app/modules/profile/profile-favs/items/ticket.jade @@ -3,11 +3,11 @@ href="" ng-if="::vm.item.get('assigned_to')" tg-nav="user-profile:username=vm.item.get('assigned_to_username')" - title="{{ ::vm.item.get('assigned_to_full_name') }}" + title="{{ ::vm.item.getIn(['assigned_to_extra_info', 'full_name_display']) }}" ) img( - ng-src="{{ ::vm.item.get('assigned_to_photo') }}", - alt="{{ ::vm.item.get('assigned_to_full_name') }}" + tg-avatar="vm.item.get('assigned_to_extra_info')", + alt="{{ ::vm.item.getIn(['assigned_to_extra_info', 'full_name_display']) }}" ) a.list-itemtype-avatar( @@ -24,6 +24,10 @@ p span.ticket-project | {{:: vm.item.get('project_name') }} + span.ticket-type( + ng-if="::vm.item.get('type') === 'epic'" + translate="COMMON.EPIC" + ) span.ticket-type( ng-if="::vm.item.get('type') === 'userstory'" translate="COMMON.USER_STORY" @@ -44,6 +48,12 @@ ) h2 span.ticket-id(tg-bo-ref="vm.item.get('ref')") + a.ticket-title( + href="#" + ng-if="::vm.item.get('type') === 'epic'" + tg-nav="project-epics-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" + title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" + ) {{ ::vm.item.get('subject') }} a.ticket-title( href="#" ng-if="::vm.item.get('type') === 'userstory'" diff --git a/app/modules/profile/profile-favs/profile-favs.controller.coffee b/app/modules/profile/profile-favs/profile-favs.controller.coffee index 5bd5917d..2fc3ac7c 100644 --- a/app/modules/profile/profile-favs/profile-favs.controller.coffee +++ b/app/modules/profile/profile-favs/profile-favs.controller.coffee @@ -28,6 +28,7 @@ class FavsBaseController _init: -> @.enableFilterByAll = true @.enableFilterByProjects = true + @.enableFilterByEpics = true @.enableFilterByUserStories = true @.enableFilterByTasks = true @.enableFilterByIssues = true @@ -101,6 +102,12 @@ class FavsBaseController @._resetList() @.loadItems() + showEpicsOnly: -> + if @.type isnt "epic" + @.type = "epic" + @._resetList() + @.loadItems() + showUserStoriesOnly: -> if @.type isnt "userstory" @.type = "userstory" @@ -131,8 +138,10 @@ class ProfileLikedController extends FavsBaseController constructor: (@userService) -> super() + @.tabName = 'likes' @.enableFilterByAll = false @.enableFilterByProjects = false + @.enableFilterByEpics = false @.enableFilterByUserStories = false @.enableFilterByTasks = false @.enableFilterByIssues = false @@ -154,8 +163,10 @@ class ProfileVotedController extends FavsBaseController constructor: (@userService) -> super() + @.tabName = 'upvotes' @.enableFilterByAll = true @.enableFilterByProjects = false + @.enableFilterByEpics = true @.enableFilterByUserStories = true @.enableFilterByTasks = true @.enableFilterByIssues = true @@ -179,6 +190,7 @@ class ProfileWatchedController extends FavsBaseController constructor: (@userService) -> super() + @.tabName = 'watchers' @._getItems = @userService.getWatched diff --git a/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee index 89ccd289..a761fe64 100644 --- a/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee +++ b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee @@ -11,7 +11,7 @@ # 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 +# You showld have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # File: profile-favs.controller.spec.coffee @@ -127,7 +127,7 @@ describe "ProfileLiked", -> expect(ctrl.q).to.be.equal(textQuery) done() - it "shou loading spinner during the call to the api", (done) -> + it "show loading spinner during the call to the api", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileLiked", $scope, {user: user}) @@ -154,7 +154,7 @@ describe "ProfileLiked", -> expect(ctrl.isLoading).to.be.false done() - it "shou no results placeholder", (done) -> + it "show no results placeholder", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileLiked", $scope, {user: user}) @@ -282,6 +282,37 @@ describe "ProfileVoted", -> expect(ctrl.q).to.be.equal(textQuery) done() + it "show only items of epics", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + type = "epic" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showEpicsOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + it "show only items of user stories", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileVoted", $scope, {user: user}) @@ -375,7 +406,7 @@ describe "ProfileVoted", -> expect(ctrl.q).to.be.null done() - it "shou loading spinner during the call to the api", (done) -> + it "show loading spinner during the call to the api", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileVoted", $scope, {user: user}) @@ -402,7 +433,7 @@ describe "ProfileVoted", -> expect(ctrl.isLoading).to.be.false done() - it "shou no results placeholder", (done) -> + it "show no results placeholder", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileVoted", $scope, {user: user}) @@ -560,6 +591,37 @@ describe "ProfileWatched", -> expect(ctrl.q).to.be.null done() + it "show only items of epics", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + type = "epic" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showEpicsOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + it "show only items of user stories", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileWatched", $scope, {user: user}) @@ -653,7 +715,7 @@ describe "ProfileWatched", -> expect(ctrl.q).to.be.null done() - it "shou loading spinner during the call to the api", (done) -> + it "show loading spinner during the call to the api", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileWatched", $scope, {user: user}) @@ -680,7 +742,7 @@ describe "ProfileWatched", -> expect(ctrl.isLoading).to.be.false done() - it "shou no results placeholder", (done) -> + it "show no results placeholder", (done) -> $scope = $rootScope.$new() ctrl = $controller("ProfileWatched", $scope, {user: user}) diff --git a/app/modules/profile/profile-favs/profile-favs.jade b/app/modules/profile/profile-favs/profile-favs.jade index 880c25e6..7f4d1d59 100644 --- a/app/modules/profile/profile-favs/profile-favs.jade +++ b/app/modules/profile/profile-favs/profile-favs.jade @@ -26,6 +26,14 @@ section.profile-favs title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS_TITLE'|translate }}" translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS'|translate }}" ) + a( + href="" + ng-if="::vm.enableFilterByEpics" + ng-click="vm.showEpicsOnly()" + ng-class="{active: vm.type === 'epic'}" + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_EPICS_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_EPICS'|translate }}" + ) a( href="" ng-if="::vm.enableFilterByUserStories" @@ -64,6 +72,11 @@ section.profile-favs tg-fav-item="item" item-type="project" ) + div( + ng-switch-when="epic" + tg-fav-item="item" + item-type="epic" + ) div( ng-switch-when="userstory" tg-fav-item="item" @@ -87,9 +100,20 @@ section.profile-favs alt="{{ 'COMMON.LOADING'|translate }}" ) - .empty-search-results(ng-if="vm.hasNoResults && !vm.isLoading") + .empty-small(ng-if="vm.hasNoResults && !vm.isLoading") img( - src="/#{v}/images/search-empty.png" + ng-if="vm.tabName === 'likes'" + src="/#{v}/images/empty/empty_like.png" + alt="{{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}" + ) + img( + ng-if="vm.tabName === 'upvotes'" + src="/#{v}/images/empty/empty_upvote.png" + alt="{{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}" + ) + img( + ng-if="vm.tabName === 'watchers'" + src="/#{v}/images/empty/empty_watch.png" alt="{{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}" ) p.title {{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }} diff --git a/app/modules/profile/profile-projects/profile-projects.jade b/app/modules/profile/profile-projects/profile-projects.jade index a1d361f8..df8de196 100644 --- a/app/modules/profile/profile-projects/profile-projects.jade +++ b/app/modules/profile/profile-projects/profile-projects.jade @@ -63,4 +63,4 @@ section.profile-projects tg-nav="user-profile:username=contact.get('username')" title="{{::contact.get('full_name')}}" ) - img(ng-src="{{::contact.get('photo')}}") + tg-avatar="contact" diff --git a/app/modules/profile/profile.controller.spec.coffee b/app/modules/profile/profile.controller.spec.coffee index df37298b..c306682a 100644 --- a/app/modules/profile/profile.controller.spec.coffee +++ b/app/modules/profile/profile.controller.spec.coffee @@ -126,16 +126,14 @@ describe "ProfileController", -> mocks.routeParams.slug = "user-slug" - xhr = { - status: 404 - } + error = new Error('404') - mocks.userService.getUserByUserName.withArgs(mocks.routeParams.slug).promise().reject(xhr) + mocks.userService.getUserByUserName.withArgs(mocks.routeParams.slug).promise().reject(error) ctrl = $controller("Profile") setTimeout ( -> - expect(mocks.xhrErrorService.response.withArgs(xhr)).to.be.calledOnce + expect(mocks.xhrErrorService.response.withArgs(error)).to.be.calledOnce done() ) diff --git a/app/modules/profile/profile.jade b/app/modules/profile/profile.jade index 3b6caf8e..79ba4add 100644 --- a/app/modules/profile/profile.jade +++ b/app/modules/profile/profile.jade @@ -1,7 +1,10 @@ div.profile.centered(ng-if="vm.user") - div(tg-profile-bar, user="vm.user", isCurrentUser="vm.isCurrentUser") + tg-profile-bar( + user="vm.user" + isCurrentUser="vm.isCurrentUser" + ) div.main - div.timeline-wrapper(tg-profile-tabs) + tg-profile-tabs.timeline-wrapper div( tg-profile-tab="{{'USER.PROFILE.TABS.ACTIVITY_TAB' | translate}}" tab-title="{{'USER.PROFILE.TABS.ACTIVITY_TAB_TITLE' | translate}}" diff --git a/app/modules/profile/styles/profile-contacts.scss b/app/modules/profile/styles/profile-contacts.scss index 4479f9f7..a50fd678 100644 --- a/app/modules/profile/styles/profile-contacts.scss +++ b/app/modules/profile/styles/profile-contacts.scss @@ -3,20 +3,3 @@ display: flex; flex-direction: column; } - -.profile-contact-filters { - align-self: center; - display: flex; - a { - border-bottom: 2px solid $white; - color: $gray-light; - display: inline-block; - padding: 1rem 1.5rem; - transition: all .2s linear; - &:hover, - &.active { - border-bottom: 2px solid $gray-light; - color: $primary; - } - } -} diff --git a/app/modules/profile/styles/profile-sidebar.scss b/app/modules/profile/styles/profile-sidebar.scss index edfba05a..6ff361db 100644 --- a/app/modules/profile/styles/profile-sidebar.scss +++ b/app/modules/profile/styles/profile-sidebar.scss @@ -1,7 +1,7 @@ .profile-sidebar { h4 { @include font-type(bold); - background: $whitish; + background: $mass-white; color: $gray; margin-bottom: .5rem; padding: .5rem; @@ -19,7 +19,4 @@ a { color: $primary; } - .trans-button { - margin-bottom: 1rem; - } } diff --git a/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee b/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee index 4669fd9b..f29bbc33 100644 --- a/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee +++ b/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee @@ -72,13 +72,13 @@ describe "LikeProjectButton", -> promise = ctrl.toggleLike() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.tgLikeProjectButton.like.withArgs(project.get('id')).resolve() promise.finally () -> expect(mocks.tgLikeProjectButton.like).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() @@ -91,7 +91,7 @@ describe "LikeProjectButton", -> ctrl = $controller("LikeProjectButton") ctrl.project = project - mocks.tgLikeProjectButton.like.withArgs(project.get('id')).promise().reject() + mocks.tgLikeProjectButton.like.withArgs(project.get('id')).promise().reject(new Error('error')) ctrl.toggleLike().finally () -> expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce @@ -109,13 +109,13 @@ describe "LikeProjectButton", -> promise = ctrl.toggleLike() - expect(ctrl.loading).to.be.true; + expect(ctrl.loading).to.be.true mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).resolve() promise.finally () -> expect(mocks.tgLikeProjectButton.unlike).to.be.calledOnce - expect(ctrl.loading).to.be.false; + expect(ctrl.loading).to.be.false done() @@ -127,7 +127,7 @@ describe "LikeProjectButton", -> ctrl = $controller("LikeProjectButton") ctrl.project = project - mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).promise().reject() + mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).promise().reject(new Error('error')) ctrl.toggleLike().finally () -> expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce diff --git a/app/modules/projects/components/sort-projects.directive.coffee b/app/modules/projects/components/sort-projects.directive.coffee index b31bbf83..d432de2d 100644 --- a/app/modules/projects/components/sort-projects.directive.coffee +++ b/app/modules/projects/components/sort-projects.directive.coffee @@ -49,7 +49,7 @@ SortProjectsDirective = (currentUserService) -> pixels: 30, scrollWhenOutside: true, autoScroll: () -> - return this.down && drake.dragging; + return this.down && drake.dragging }) scope.$on "$destroy", -> diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee index c58e4d0e..1d6ce2fe 100644 --- a/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee +++ b/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee @@ -118,7 +118,7 @@ describe "WatchProjectButton", -> ctrl.project = project ctrl.showWatchOptions = true - mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).promise().reject() + mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).promise().reject(new Error('error')) ctrl.watch(notifyLevel).finally () -> expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce @@ -159,7 +159,7 @@ describe "WatchProjectButton", -> ctrl.project = project ctrl.showWatchOptions = true - mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).promise().reject() + mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).promise().reject(new Error('error')) ctrl.unwatch().finally () -> expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce diff --git a/app/modules/projects/listing/projects-listing.controller.spec.coffee b/app/modules/projects/listing/projects-listing.controller.spec.coffee index fe3d2b2b..d7a3f00d 100644 --- a/app/modules/projects/listing/projects-listing.controller.spec.coffee +++ b/app/modules/projects/listing/projects-listing.controller.spec.coffee @@ -77,4 +77,4 @@ describe "ProjectsListingController", -> pageCtrl.newProject() - expect(mocks.projectsService.newProject).to.be.calledOnce; + expect(mocks.projectsService.newProject).to.be.calledOnce diff --git a/app/modules/projects/listing/styles/project-list.scss b/app/modules/projects/listing/styles/project-list.scss index 4fa8d390..2bbf058f 100644 --- a/app/modules/projects/listing/styles/project-list.scss +++ b/app/modules/projects/listing/styles/project-list.scss @@ -2,7 +2,7 @@ position: relative; .project-list-title { align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; margin: 2rem 0 1rem; diff --git a/app/modules/projects/project/blocked-project.jade b/app/modules/projects/project/blocked-project.jade index 0cf464b8..bd7e8ad2 100644 --- a/app/modules/projects/project/blocked-project.jade +++ b/app/modules/projects/project/blocked-project.jade @@ -1,4 +1,4 @@ -.blocked-project-detail +.blocked-project-detail(ng-controller="Project as vm") .blocked-project-inner .blocked-project-title .project-image diff --git a/app/modules/projects/project/project.controller.coffee b/app/modules/projects/project/project.controller.coffee index 418afda7..18820098 100644 --- a/app/modules/projects/project/project.controller.coffee +++ b/app/modules/projects/project/project.controller.coffee @@ -27,7 +27,6 @@ class ProjectController ] constructor: (@routeParams, @appMetaService, @auth, @translate, @projectService) -> - projectSlug = @routeParams.pslug @.user = @auth.userData taiga.defineImmutableProperty @, "project", () => return @projectService.project @@ -35,16 +34,14 @@ class ProjectController @appMetaService.setfn @._setMeta.bind(this) - _setMeta: (project)-> + _setMeta: ()-> return null if !@.project - metas = {} - ctx = {projectName: @.project.get("name")} - metas.title = @translate.instant("PROJECT.PAGE_TITLE", ctx) - metas.description = @.project.get("description") - - return metas + return { + title: @translate.instant("PROJECT.PAGE_TITLE", ctx) + description: @.project.get("description") + } angular.module("taigaProjects").controller("Project", ProjectController) diff --git a/app/modules/projects/project/project.jade b/app/modules/projects/project/project.jade index 731d3d59..01feadbc 100644 --- a/app/modules/projects/project/project.jade +++ b/app/modules/projects/project/project.jade @@ -43,11 +43,8 @@ div.wrapper p.description {{vm.project.get('description')}} div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size") - span.tag( - style='border-left: 5px solid {{::tag.get("color")}};', - tg-repeat="tag in ::vm.project.get('colorized_tags')" - ) - span.tag-name {{::tag.get('name')}} + span.tag(tg-repeat="tag in ::vm.project.get('tags')") + span.tag-name {{::tag}} div.project-data section.timeline(ng-if="vm.project") @@ -60,7 +57,9 @@ div.wrapper title="{{'PROJECT.LOOKING_FOR_PEOPLE' | translate}}" ) h3 {{'PROJECT.LOOKING_FOR_PEOPLE' | translate}} - p(ng-if="vm.project.get('looking_for_people_note')") {{::vm.project.get('looking_for_people_note')}} + p(ng-if="vm.project.get('looking_for_people_note')") + | {{::vm.project.get('looking_for_people_note')}} + h2.title {{"PROJECT.SECTION.TEAM" | translate}} ul.involved-team li(tg-repeat="member in vm.members") @@ -68,7 +67,10 @@ div.wrapper tg-nav="user-profile:username=member.get('username')", title="{{::member.get('full_name')}}" ) - img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}") + img( + tg-avatar="member" + alt="{{::member.get('full_name')}}" + ) tg-svg( ng-if="member.get('id') == vm.project.getIn(['owner', 'id'])" svg-icon="icon-badge" diff --git a/app/modules/projects/projects.service.coffee b/app/modules/projects/projects.service.coffee index ab3973a3..3c1415d5 100644 --- a/app/modules/projects/projects.service.coffee +++ b/app/modules/projects/projects.service.coffee @@ -20,6 +20,7 @@ taiga = @.taiga groupBy = @.taiga.groupBy + class ProjectsService extends taiga.Service @.$inject = ["tgResources", "$projectUrl", "tgLightboxFactory"] @@ -42,16 +43,6 @@ class ProjectsService extends taiga.Service url = @projectUrl.get(project.toJS()) project = project.set("url", url) - colorized_tags = [] - - if project.get("tags") - tags = project.get("tags").sort() - - colorized_tags = tags.map (tag) -> - color = project.get("tags_colors").get(tag) - return Immutable.fromJS({name: tag, color: color}) - - project = project.set("colorized_tags", colorized_tags) return project diff --git a/app/modules/projects/projects.service.spec.coffee b/app/modules/projects/projects.service.spec.coffee index 1829e8a8..80a98389 100644 --- a/app/modules/projects/projects.service.spec.coffee +++ b/app/modules/projects/projects.service.spec.coffee @@ -129,8 +129,7 @@ describe "tgProjectsService", -> id: 2, url: 'url-2', tags: ['xx', 'yy', 'aa'], - tags_colors: {xx: "red", yy: "blue", aa: "white"}, - colorized_tags: [{name: 'aa', color: 'white'}, {name: 'xx', color: 'red'}, {name: 'yy', color: 'blue'}] + tags_colors: {xx: "red", yy: "blue", aa: "white"} } ) @@ -157,8 +156,7 @@ describe "tgProjectsService", -> id: 2, url: 'url-2', tags: ['xx', 'yy', 'aa'], - tags_colors: {xx: "red", yy: "blue", aa: "white"}, - colorized_tags: [{name: 'aa', color: 'white'}, {name: 'xx', color: 'red'}, {name: 'yy', color: 'blue'}] + tags_colors: {xx: "red", yy: "blue", aa: "white"} } ]) diff --git a/app/modules/projects/transfer/transfer-project.controller.coffee b/app/modules/projects/transfer/transfer-project.controller.coffee index 715d45b3..b7bed191 100644 --- a/app/modules/projects/transfer/transfer-project.controller.coffee +++ b/app/modules/projects/transfer/transfer-project.controller.coffee @@ -28,10 +28,11 @@ class TransferProject "tgCurrentUserService", "$tgNavUrls", "$translate", - "$tgConfirm" + "$tgConfirm", + "tgErrorHandlingService" ] - constructor: (@routeParams, @projectService, @location, @authService, @currentUserService, @navUrls, @translate, @confirmService) -> + constructor: (@routeParams, @projectService, @location, @authService, @currentUserService, @navUrls, @translate, @confirmService, @errorHandlingService) -> initialize: () -> @.projectId = @.project.get("id") @@ -41,7 +42,7 @@ class TransferProject _validateToken: () -> return @projectService.transferValidateToken(@.projectId, @.token).then null, (data, status) => - @location.path(@navUrls.resolve("not-found")) + @errorHandlingService.notfound() _refreshUserData: () -> return @authService.refresh().then () => diff --git a/app/modules/projects/transfer/transfer-project.controller.spec.coffee b/app/modules/projects/transfer/transfer-project.controller.spec.coffee index 6d6af88c..3a854312 100644 --- a/app/modules/projects/transfer/transfer-project.controller.spec.coffee +++ b/app/modules/projects/transfer/transfer-project.controller.spec.coffee @@ -28,6 +28,13 @@ describe "TransferProject", -> mocks.routeParams = {} provide.value "$routeParams", mocks.routeParams + _mockErrorHandlingService = () -> + mocks.errorHandlingService = { + notfound: sinon.stub() + } + + provide.value "tgErrorHandlingService", mocks.errorHandlingService + _mockProjectsService = () -> mocks.projectsService = { transferValidateToken: sinon.stub() @@ -90,6 +97,7 @@ describe "TransferProject", -> _mockTgNavUrls() _mockTranslate() _mockTgConfirm() + _mockErrorHandlingService() return null _inject = (callback) -> @@ -113,13 +121,13 @@ describe "TransferProject", -> mocks.auth.refresh.promise().resolve() mocks.routeParams.token = "BAD_TOKEN" mocks.currentUserService.getUser.returns(user) - mocks.projectsService.transferValidateToken.withArgs(1, "BAD_TOKEN").promise().reject() + mocks.projectsService.transferValidateToken.withArgs(1, "BAD_TOKEN").promise().reject(new Error('error')) mocks.tgNavUrls.resolve.withArgs("not-found").returns("/not-found") ctrl = $controller("TransferProjectController") ctrl.project = project ctrl.initialize().then () -> - expect(mocks.location.path).to.be.calledWith("/not-found") + expect(mocks.errorHandlingService.notfound).have.been.called done() it "valid token private project with max projects for user", (done) -> @@ -247,7 +255,7 @@ describe "TransferProject", -> expect(mocks.location.path).to.be.calledWith("/project/slug/") expect(mocks.tgConfirm.notify).to.be.calledWith("success", "ACCEPTED_PROJECT_OWNERNSHIP", '', 5000) - done() + done() it "transfer reject", (done) -> project = Immutable.fromJS({ @@ -262,7 +270,7 @@ describe "TransferProject", -> mocks.currentUserService.getUser.returns(user) mocks.projectsService.transferValidateToken.withArgs(1, "TOKEN").promise().resolve() mocks.projectsService.transferReject.withArgs(1, "TOKEN", "this is my reason").promise().resolve() - mocks.tgNavUrls.resolve.withArgs("project-admin-project-profile-details", {project: "slug"}).returns("/project/slug/") + mocks.tgNavUrls.resolve.withArgs("home", {project: "slug"}).returns("/project/slug/") mocks.translate.instant.withArgs("ADMIN.PROJECT_TRANSFER.REJECTED_PROJECT_OWNERNSHIP").returns("REJECTED_PROJECT_OWNERNSHIP") ctrl = $controller("TransferProjectController") @@ -272,4 +280,4 @@ describe "TransferProject", -> expect(mocks.location.path).to.be.calledWith("/project/slug/") expect(mocks.tgConfirm.notify).to.be.calledWith("success", "REJECTED_PROJECT_OWNERNSHIP", '', 5000) - done() + done() diff --git a/app/modules/resources/epics-resource.service.coffee b/app/modules/resources/epics-resource.service.coffee new file mode 100644 index 00000000..293830b9 --- /dev/null +++ b/app/modules/resources/epics-resource.service.coffee @@ -0,0 +1,101 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-resource.service.coffee +### + +Resource = (urlsService, http) -> + service = {} + + service.listInAllProjects = (params) -> + url = urlsService.resolve("epics") + + httpOptions = { + headers: { + "x-disable-pagination": "1" + } + } + + return http.get(url, params, httpOptions) + .then (result) -> + return Immutable.fromJS(result.data) + + service.list = (projectId) -> + url = urlsService.resolve("epics") + + params = {project: projectId} + + return http.get(url, params) + .then (result) -> Immutable.fromJS(result.data) + + service.patch = (id, patch) -> + url = urlsService.resolve("epics") + "/#{id}" + + return http.patch(url, patch) + .then (result) -> Immutable.fromJS(result.data) + + service.post = (params) -> + url = urlsService.resolve("epics") + + return http.post(url, params) + .then (result) -> Immutable.fromJS(result.data) + + service.reorder = (id, data, setOrders) -> + url = urlsService.resolve("epics") + "/#{id}" + + options = {"headers": {"set-orders": JSON.stringify(setOrders)}} + + return http.patch(url, data, null, options) + + service.addRelatedUserstory = (epicId, userstoryId) -> + url = urlsService.resolve("epic-related-userstories", epicId) + + params = { + user_story: userstoryId + epic: epicId + } + + return http.post(url, params) + + service.reorderRelatedUserstory = (epicId, userstoryId, data, setOrders) -> + url = urlsService.resolve("epic-related-userstories", epicId) + "/#{userstoryId}" + + options = {"headers": {"set-orders": JSON.stringify(setOrders)}} + + return http.patch(url, data, null, options) + + service.bulkCreateRelatedUserStories = (epicId, projectId, bulk_userstories) -> + url = urlsService.resolve("epic-related-userstories-bulk-create", epicId) + + params = { + bulk_userstories: bulk_userstories, + project_id: projectId + } + + return http.post(url, params) + + service.deleteRelatedUserstory = (epicId, userstoryId) -> + url = urlsService.resolve("epic-related-userstories", epicId) + "/#{userstoryId}" + + return http.delete(url) + + return () -> + return {"epics": service} + +Resource.$inject = ["$tgUrls", "$tgHttp"] + +module = angular.module("taigaResources2") +module.factory("tgEpicsResource", Resource) diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee index 226534ff..9df02c3a 100644 --- a/app/modules/resources/projects-resource.service.coffee +++ b/app/modules/resources/projects-resource.service.coffee @@ -54,7 +54,7 @@ Resource = (urlsService, http, paginateResponseService) -> "x-disable-pagination": "1" } - params = {"member": userId, "order_by": "memberships__user_order"} + params = {"member": userId, "order_by": "user_order"} return http.get(url, params, httpOptions) .then (result) -> diff --git a/app/modules/resources/resources.coffee b/app/modules/resources/resources.coffee index e8a4c33a..f55dd7cc 100644 --- a/app/modules/resources/resources.coffee +++ b/app/modules/resources/resources.coffee @@ -26,7 +26,9 @@ services = [ "tgIssuesResource", "tgExternalAppsResource", "tgAttachmentsResource", - "tgStatsResource" + "tgStatsResource", + "tgWikiHistory", + "tgEpicsResource" ] Resources = ($injector) -> diff --git a/app/modules/resources/userstories-resource.service.coffee b/app/modules/resources/userstories-resource.service.coffee index ce2b7cd4..d410036e 100644 --- a/app/modules/resources/userstories-resource.service.coffee +++ b/app/modules/resources/userstories-resource.service.coffee @@ -33,6 +33,35 @@ Resource = (urlsService, http) -> .then (result) -> return Immutable.fromJS(result.data) + service.listAllInProject = (projectId) -> + url = urlsService.resolve("userstories") + + httpOptions = { + headers: { + "x-disable-pagination": "1" + } + } + + params = { + project: projectId + } + return http.get(url, params, httpOptions) + .then (result) -> + return Immutable.fromJS(result.data) + + service.listInEpic = (epicIid) -> + url = urlsService.resolve("userstories") + + params = { + 'epic': epicIid, + 'order_by': 'epic_order', + 'include_tasks': true + } + + return http.get(url, params) + .then (result) -> + return Immutable.fromJS(result.data) + return () -> return {"userstories": service} diff --git a/app/modules/resources/wiki-resource.service.coffee b/app/modules/resources/wiki-resource.service.coffee new file mode 100644 index 00000000..6c3a67fe --- /dev/null +++ b/app/modules/resources/wiki-resource.service.coffee @@ -0,0 +1,42 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-resource.service.coffee +### + +Resource = (urlsService, http) -> + service = {} + + service.getWikiHistory = (wikiId) -> + url = urlsService.resolve("history/wiki", wikiId) + + httpOptions = { + headers: { + "x-disable-pagination": "1" + } + } + + return http.get(url, null, httpOptions) + .then (result) -> + return Immutable.fromJS(result.data) + + return () -> + return {"wikiHistory": service} + +Resource.$inject = ["$tgUrls", "$tgHttp"] + +module = angular.module("taigaResources2") +module.factory("tgWikiHistory", Resource) diff --git a/app/modules/services/avatar.service.coffee b/app/modules/services/avatar.service.coffee new file mode 100644 index 00000000..f2ba8739 --- /dev/null +++ b/app/modules/services/avatar.service.coffee @@ -0,0 +1,93 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: avatar.service.coffee +### + +class AvatarService + constructor: () -> + IMAGES = [ + "/#{window._version}/images/user-avatars/user-avatar-01.png" + "/#{window._version}/images/user-avatars/user-avatar-02.png" + "/#{window._version}/images/user-avatars/user-avatar-03.png" + "/#{window._version}/images/user-avatars/user-avatar-04.png" + "/#{window._version}/images/user-avatars/user-avatar-05.png" + ] + + COLORS = [ + "rgba( 178, 176, 204, 1 )" + "rgba( 183, 203, 131, 1 )" + "rgba( 210, 198, 139, 1 )" + "rgba( 214, 161, 212, 1 )" + "rgba( 247, 154, 154, 1 )" + ] + + @.logos = _.cartesianProduct(IMAGES, COLORS) + + getDefault: (key) -> + idx = murmurhash3_32_gc(key, 42) %% @.logos.length + logo = @.logos[idx] + + return { src: logo[0], color: logo[1] } + + getUnnamed: () -> + return { + url: "/#{window._version}/images/unnamed.png" + } + + getAvatar: (user, type) -> + return @.getUnnamed() if !user + + avatarParamName = 'photo' + + if type == 'avatarBig' + avatarParamName = 'big_photo' + + photo = null + + if user instanceof Immutable.Map + gravatar = user.get('gravatar_id') + photo = user.get(avatarParamName) + else + gravatar = user.gravatar_id + photo = user[avatarParamName] + + return @.getUnnamed() if !gravatar + + if photo + return { + url: photo + } + else if location.host.indexOf('localhost') != -1 + root = location.protocol + '//' + location.host + logo = @.getDefault(gravatar) + + return { + url: root + logo.src, + bg: logo.color + } + else + root = location.protocol + '//' + location.host + logo = @.getDefault(gravatar) + + logoUrl = encodeURIComponent(root + logo.src) + + return { + url: 'https://www.gravatar.com/avatar/' + gravatar + "?d=" + logoUrl, + bg: logo.color + } + +angular.module("taigaCommon").service("tgAvatarService", AvatarService) diff --git a/app/modules/services/check-permissions.service.coffee b/app/modules/services/check-permissions.service.coffee index 5ca652e3..ad0cd7e9 100644 --- a/app/modules/services/check-permissions.service.coffee +++ b/app/modules/services/check-permissions.service.coffee @@ -19,7 +19,7 @@ taiga = @.taiga -class ChekcPermissionsService +class CheckPermissionsService @.$inject = [ "tgProjectService" ] @@ -31,4 +31,4 @@ class ChekcPermissionsService return @projectService.project.get('my_permissions').indexOf(permission) != -1 -angular.module("taigaCommon").service("tgCheckPermissionsService", ChekcPermissionsService) +angular.module("taigaCommon").service("tgCheckPermissionsService", CheckPermissionsService) diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee index fba99a7f..96f4fff2 100644 --- a/app/modules/services/current-user.service.spec.coffee +++ b/app/modules/services/current-user.service.spec.coffee @@ -179,7 +179,7 @@ describe "tgCurrentUserService", -> backlog: false, kanban: false, dashboard: false - }); + }) it "load joyride config", (done) -> mocks.resources.user.getUserStorage.withArgs('joyride').promise().resolve(true) @@ -190,7 +190,7 @@ describe "tgCurrentUserService", -> done() it "create default joyride config", (done) -> - mocks.resources.user.getUserStorage.withArgs('joyride').promise().reject() + mocks.resources.user.getUserStorage.withArgs('joyride').promise().reject(new Error('error')) currentUserService.loadJoyRideConfig().then (config) -> joyride = { diff --git a/app/modules/services/error-handling.service.coffee b/app/modules/services/error-handling.service.coffee new file mode 100644 index 00000000..66576c39 --- /dev/null +++ b/app/modules/services/error-handling.service.coffee @@ -0,0 +1,48 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: error-handling.service.coffee +### + +taiga = @.taiga + +class ErrorHandlingService + @.$inject = [ + "$rootScope" + ] + + constructor: (@rootScope) -> + + init: () -> + @rootScope.errorHandling = {} + + notfound: -> + @rootScope.errorHandling.showingError = true + @rootScope.errorHandling.notfound = true + + error: -> + @rootScope.errorHandling.showingError = true + @rootScope.errorHandling.error = true + + permissionDenied: -> + @rootScope.errorHandling.showingError = true + @rootScope.errorHandling.permissionDenied = true + + block: -> + @rootScope.errorHandling.showingError = true + @rootScope.errorHandling.blocked = true + +angular.module("taigaCommon").service("tgErrorHandlingService", ErrorHandlingService) diff --git a/app/modules/services/project-logo.service.spec.coffee b/app/modules/services/project-logo.service.spec.coffee index 1c6cae74..d4858e42 100644 --- a/app/modules/services/project-logo.service.spec.coffee +++ b/app/modules/services/project-logo.service.spec.coffee @@ -38,5 +38,5 @@ describe "tgProjectLogoService", -> logo = projectLogoService.getDefaultProjectLogo('slug/slug', 2) - expect(logo.src).to.be.equal('/123/images/project-logos/project-logo-04.png'); - expect(logo.color).to.be.equal('rgba( 152, 224, 168, 1 )'); + expect(logo.src).to.be.equal('/123/images/project-logos/project-logo-04.png') + expect(logo.color).to.be.equal('rgba( 152, 224, 168, 1 )') diff --git a/app/modules/services/project.service.coffee b/app/modules/services/project.service.coffee index a6640ac5..649147b4 100644 --- a/app/modules/services/project.service.coffee +++ b/app/modules/services/project.service.coffee @@ -36,6 +36,12 @@ class ProjectService taiga.defineImmutableProperty @, "sectionsBreadcrumb", () => return @._sectionsBreadcrumb taiga.defineImmutableProperty @, "activeMembers", () => return @._activeMembers + cleanProject: () -> + @._project = null + @._activeMembers = Immutable.List() + @._section = null + @._sectionsBreadcrumb = Immutable.List() + setSection: (section) -> @._section = section @@ -44,6 +50,10 @@ class ProjectService else @._sectionsBreadcrumb = Immutable.List() + setProject: (project) -> + @._project = project + @._activeMembers = @._project.get('members').filter (member) -> member.get('is_active') + setProjectBySlug: (pslug) -> return new Promise (resolve, reject) => if !@.project || @.project.get('slug') != pslug @@ -57,23 +67,15 @@ class ProjectService else resolve() - setProject: (project) -> - @._project = project - @._activeMembers = @._project.get('members').filter (member) -> member.get('is_active') - - cleanProject: () -> - @._project = null - @._activeMembers = Immutable.List() - @._section = null - @._sectionsBreadcrumb = Immutable.List() - - hasPermission: (permission) -> - return @._project.get('my_permissions').indexOf(permission) != -1 - fetchProject: () -> pslug = @.project.get('slug') return @projectsService.getProjectBySlug(pslug).then (project) => @.setProject(project) + hasPermission: (permission) -> + return @._project.get('my_permissions').indexOf(permission) != -1 + + isEpicsDashboardEnabled: -> + return @._project.get("is_epics_activated") angular.module("taigaCommon").service("tgProjectService", ProjectService) diff --git a/app/modules/services/project.service.spec.coffee b/app/modules/services/project.service.spec.coffee index 8b4a11f9..20f529d1 100644 --- a/app/modules/services/project.service.spec.coffee +++ b/app/modules/services/project.service.spec.coffee @@ -89,7 +89,7 @@ describe "tgProjectService", -> .then () -> projectService.setProjectBySlug('slug-1') .then () -> projectService.setProjectBySlug('slug-2') .finally () -> - expect(projectService.setProject).to.be.called.twice; + expect(projectService.setProject).to.be.called.twice done() it "set project and set active members", () -> @@ -136,10 +136,10 @@ describe "tgProjectService", -> projectService.cleanProject() - expect(projectService.project).to.be.null; - expect(projectService.activeMembers.size).to.be.equal(0); - expect(projectService.section).to.be.null; - expect(projectService.sectionsBreadcrumb.size).to.be.equal(0); + expect(projectService.project).to.be.null + expect(projectService.activeMembers.size).to.be.equal(0) + expect(projectService.section).to.be.null + expect(projectService.sectionsBreadcrumb.size).to.be.equal(0) it "has permissions", () -> project = Immutable.Map({ diff --git a/app/modules/services/xhrError.service.coffee b/app/modules/services/xhrError.service.coffee index 9015e858..42de3515 100644 --- a/app/modules/services/xhrError.service.coffee +++ b/app/modules/services/xhrError.service.coffee @@ -20,19 +20,16 @@ class xhrError extends taiga.Service @.$inject = [ "$q", - "$location", - "$tgNavUrls" + "tgErrorHandlingService" ] - constructor: (@q, @location, @navUrls) -> + constructor: (@q, @errorHandlingService) -> notFound: () -> - @location.path(@navUrls.resolve("not-found")) - @location.replace() + @errorHandlingService.notfound() permissionDenied: () -> - @location.path(@navUrls.resolve("permission-denied")) - @location.replace() + @errorHandlingService.permissionDenied() response: (xhr) -> if xhr diff --git a/app/modules/services/xhrError.service.spec.coffee b/app/modules/services/xhrError.service.spec.coffee index f69ef84a..2efc12ef 100644 --- a/app/modules/services/xhrError.service.spec.coffee +++ b/app/modules/services/xhrError.service.spec.coffee @@ -28,20 +28,14 @@ describe "tgXhrErrorService", -> provide.value "$q", mocks.q - _mockLocation = () -> - mocks.location = { - path: sinon.spy(), - replace: sinon.spy() + + _mockErrorHandling = () -> + mocks.errorHandling = { + notfound: sinon.stub(), + permissionDenied: sinon.stub() } - provide.value "$location", mocks.location - - _mockNavUrls = () -> - mocks.navUrls = { - resolve: sinon.stub() - } - - provide.value "$tgNavUrls", mocks.navUrls + provide.value "tgErrorHandlingService", mocks.errorHandling _inject = (callback) -> inject (_tgXhrErrorService_) -> @@ -52,8 +46,7 @@ describe "tgXhrErrorService", -> module ($provide) -> provide = $provide _mockQ() - _mockLocation() - _mockNavUrls() + _mockErrorHandling() return null @@ -70,23 +63,17 @@ describe "tgXhrErrorService", -> status: 404 } - mocks.navUrls.resolve.withArgs("not-found").returns("not-found") - xhrErrorService.response(xhr) expect(mocks.q.reject.withArgs(xhr)).to.be.calledOnce - expect(mocks.location.path.withArgs("not-found")).to.be.calledOnce - expect(mocks.location.replace).to.be.calledOnce + expect(mocks.errorHandling.notfound).to.be.calledOnce it "403 status redirect to permission-denied page", () -> xhr = { status: 403 } - mocks.navUrls.resolve.withArgs("permission-denied").returns("permission-denied") - xhrErrorService.response(xhr) expect(mocks.q.reject.withArgs(xhr)).to.be.calledOnce - expect(mocks.location.path.withArgs("permission-denied")).to.be.calledOnce - expect(mocks.location.replace).to.be.calledOnce + expect(mocks.errorHandling.permissionDenied).to.be.calledOnce diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee index b19769ed..84a189c3 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee @@ -35,7 +35,8 @@ class UserTimelineItemTitle 'priority': 'ISSUES.FIELDS.PRIORITY', 'type': 'ISSUES.FIELDS.TYPE', 'is_iocaine': 'TASK.FIELDS.IS_IOCAINE', - 'is_blocked': 'COMMON.FIELDS.IS_BLOCKED' + 'is_blocked': 'COMMON.FIELDS.IS_BLOCKED', + 'color': 'COMMON.FIELDS.COLOR' } _params: { @@ -89,6 +90,18 @@ class UserTimelineItemTitle return @._getLink(url, text) + related_us_name: (timeline, event) -> + obj = timeline.getIn(["data", "userstory"]) + url = "project-userstories-detail:project=timeline.getIn(['data', 'userstory', 'project', 'slug']),ref=timeline.getIn(['data', 'userstory', 'ref'])" + text = '#' + obj.get('ref') + ' ' + obj.get('subject') + return @._getLink(url, text) + + epic_name: (timeline, event) -> + obj = timeline.getIn(["data", "epic"]) + url = "project-epics-detail:project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['data', 'epic', 'ref'])" + text = '#' + obj.get('ref') + ' ' + obj.get('subject') + return @._getLink(url, text) + obj_name: (timeline, event) -> obj = @._getTimelineObj(timeline, event) url = @._getDetailObjUrl(event) @@ -122,9 +135,9 @@ class UserTimelineItemTitle "task": ["project-tasks-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"], "userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"], "parent_userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'userstory', 'ref'])"], - "milestone": ["project-taskboard", ":project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['obj', 'slug'])"] + "milestone": ["project-taskboard", ":project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['obj', 'slug'])"], + "epic": ["project-epics-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"] } - return url[event.obj][0] + url[event.obj][1] _getLink: (url, text, title) -> @@ -153,7 +166,6 @@ class UserTimelineItemTitle timeline_type.translate_params.forEach (param) => params[param] = @._translateTitleParams(param, timeline, event) - return params getTitle: (timeline, event, type) -> diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee index 5306ac5f..9e33dcd0 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee @@ -82,6 +82,18 @@ timelineType = (timeline, event) -> key: 'TIMELINE.MILESTONE_CREATED', translate_params: ['username', 'project_name', 'obj_name'] }, + { # NewEpic + check: (timeline, event) -> + return event.obj == 'epic' && event.type == 'create' + key: 'TIMELINE.EPIC_CREATED', + translate_params: ['username', 'project_name', 'obj_name'] + }, + { # NewEpicRelatedUserstory + check: (timeline, event) -> + return event.obj == 'relateduserstory' && event.type == 'create' + key: 'TIMELINE.EPIC_RELATED_USERSTORY_CREATED', + translate_params: ['username', 'project_name', 'related_us_name', 'epic_name'] + }, { # NewUsComment check: (timeline, event) -> return timeline.getIn(['data', 'comment']) && event.obj == 'userstory' @@ -109,6 +121,15 @@ timelineType = (timeline, event) -> text = timeline.getIn(['data', 'comment_html']) return $($.parseHTML(text)).text() }, + { # NewEpicComment + check: (timeline, event) -> + return timeline.getIn(['data', 'comment']) && event.obj == 'epic' + key: 'TIMELINE.NEW_COMMENT_EPIC' + translate_params: ['username', 'obj_name'], + description: (timeline) -> + text = timeline.getIn(['data', 'comment_html']) + return $($.parseHTML(text)).text() + }, { # UsMove check: (timeline, event) -> return timeline.hasIn(['data', 'value_diff']) && @@ -258,6 +279,31 @@ timelineType = (timeline, event) -> key: 'TIMELINE.TASK_UPDATED_WITH_US_NEW_VALUE', translate_params: ['username', 'field_name', 'obj_name', 'us_name', 'new_value'] }, + { # EpicUpdated description + check: (timeline, event) -> + return event.obj == 'epic' && + event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'description_diff' + key: 'TIMELINE.EPIC_UPDATED', + translate_params: ['username', 'field_name', 'obj_name'] + }, + { # EpicUpdated color + check: (timeline, event) -> + return event.obj == 'epic' && + event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'color' + key: 'TIMELINE.EPIC_UPDATED_WITH_NEW_COLOR', + translate_params: ['username', 'field_name', 'obj_name', 'new_value'] + }, + { # EpicUpdated general + check: (timeline, event) -> + return event.obj == 'epic' && + event.type == 'change' + key: 'TIMELINE.EPIC_UPDATED_WITH_NEW_VALUE', + translate_params: ['username', 'field_name', 'obj_name', 'new_value'] + }, { # New User check: (timeline, event) -> return event.obj == 'user' && event.type == 'create' diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade index 38a492ee..a85bcf0b 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade @@ -1,16 +1,22 @@ -div.activity-item +.activity-item span.activity-date {{::timeline.get('created') | momentFromNow}} - div.activity-info(tg-user-timeline-title="timeline") + .activity-info(tg-user-timeline-title="timeline") - div.activity-info + .activity-info // profile image with url - div.profile-contact-picture(ng-if="timeline.getIn(['data', 'user', 'is_profile_visible'])") + .profile-contact-picture(ng-if="timeline.getIn(['data', 'user', 'is_profile_visible'])") a(tg-nav="user-profile:username=timeline.getIn(['data', 'user', 'username'])", title="{{::timeline.getIn(['data', 'user', 'name']) }}") - img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/#{v}/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}") + img( + tg-avatar="timeline.getIn(['data', 'user'])" + alt="{{::timeline.getIn(['data', 'user', 'name'])}}" + ) // profile image without url - div.profile-contact-picture(ng-if="!timeline.getIn(['data', 'user', 'is_profile_visible'])") - img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/#{v}/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}") + .profile-contact-picture(ng-if="!timeline.getIn(['data', 'user', 'is_profile_visible'])") + img( + tg-avatar="timeline.getIn(['data', 'user'])" + alt="{{::timeline.getIn(['data', 'user', 'name'])}}" + ) p(tg-compile-html="timeline.get('title_html')") @@ -19,7 +25,10 @@ div.activity-item .activity-member-view(ng-if="::timeline.has('member')") a.profile-member-picture(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member', 'user', 'name'])}}") - img(ng-src="{{::timeline.getIn(['member', 'user', 'photo'])}}", alt="{{::timeline.getIn(['member','user', 'name'])}}") + img( + tg-avatar="timeline.getIn(['member', 'user'])" + alt="{{::timeline.getIn(['member','user', 'name'])}}" + ) .activity-member-info a(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member','user', 'name'])}}") span {{::timeline.getIn(['member','user', 'name'])}} diff --git a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee index 3ee1bf8d..7baebf6e 100644 --- a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee @@ -144,7 +144,7 @@ describe "tgUserTimelinePaginationSequenceService", -> config.minItems = 1 - config.map = (item) => item + 1; + config.map = (item) => item + 1 seq = userTimelinePaginationSequenceService.generate(config) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee b/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee index e66cf9b3..519f79be 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee @@ -49,7 +49,7 @@ describe "UserTimelineController", -> $rootScope = _$rootScope_ it "timelineList should be an array", () -> - $scope = $rootScope.$new(); + $scope = $rootScope.$new() mocks.userTimelineService.getUserTimeline = sinon.stub().returns(true) @@ -63,7 +63,7 @@ describe "UserTimelineController", -> it "project timeline sequence", () -> mocks.userTimelineService.getProjectTimeline = sinon.stub().withArgs(4).returns(true) - $scope = $rootScope.$new(); + $scope = $rootScope.$new() myCtrl = controller("UserTimeline", $scope, { projectId: 4 @@ -74,7 +74,7 @@ describe "UserTimelineController", -> it "currentUser timeline sequence", () -> mocks.userTimelineService.getProfileTimeline = sinon.stub().withArgs(2).returns(true) - $scope = $rootScope.$new(); + $scope = $rootScope.$new() myCtrl = controller("UserTimeline", $scope, { currentUser: true, @@ -86,7 +86,7 @@ describe "UserTimelineController", -> it "currentUser timeline sequence", () -> mocks.userTimelineService.getUserTimeline = sinon.stub().withArgs(2).returns(true) - $scope = $rootScope.$new(); + $scope = $rootScope.$new() myCtrl = controller("UserTimeline", $scope, { user: Immutable.Map({id: 2}) @@ -99,7 +99,7 @@ describe "UserTimelineController", -> beforeEach () -> mocks.userTimelineService.getUserTimeline = sinon.stub().returns({}) - $scope = $rootScope.$new(); + $scope = $rootScope.$new() myCtrl = controller("UserTimeline", $scope, { user: Immutable.Map({id: 2}) }) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.jade b/app/modules/user-timeline/user-timeline/user-timeline.jade index b227d860..8c8bdcc1 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.jade +++ b/app/modules/user-timeline/user-timeline/user-timeline.jade @@ -1,7 +1,16 @@ section.profile-timeline div(ng-if="!vm.timelineList.size") div.spin - img(src="/#{v}/svg/spinner-circle.svg", alt="Loading...") + img( + src="/#{v}/svg/spinner-circle.svg" + alt="Loading..." + ) - div(infinite-scroll="vm.loadTimeline()", infinite-scroll-disabled="vm.scrollDisabled") - div(tg-repeat="timeline in vm.timelineList", tg-user-timeline-item="timeline") + div( + infinite-scroll="vm.loadTimeline()" + infinite-scroll-disabled="vm.scrollDisabled" + ) + div( + tg-repeat="timeline in vm.timelineList" + tg-user-timeline-item="timeline" + ) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.scss b/app/modules/user-timeline/user-timeline/user-timeline.scss index 54d03b71..bcb89710 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.scss +++ b/app/modules/user-timeline/user-timeline/user-timeline.scss @@ -56,6 +56,15 @@ width: 100%; } } + .new-color { + border-radius: 50%; + display: inline-block; + height: 1rem; + margin-left: .2rem; + position: relative; + top: .1rem; + width: 1rem; + } } .activity-member-view { display: flex; diff --git a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee index 61adc2a4..aecf5724 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee @@ -47,7 +47,8 @@ class UserTimelineService extends taiga.Service # customs 'blocked', 'moveInBacklog', - 'milestone' + 'milestone', + 'color' ] _invalid: [ diff --git a/app/modules/utils/isolate-click.directive.coffee b/app/modules/utils/isolate-click.directive.coffee new file mode 100644 index 00000000..df262794 --- /dev/null +++ b/app/modules/utils/isolate-click.directive.coffee @@ -0,0 +1,27 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: isolate-click.directive.coffee +### + +IsolateClickDirective = () -> + link = (scope, el, attrs) -> + el.on 'click', (e) => + e.stopPropagation() + + return {link: link} + +angular.module("taigaUtils").directive("tgIsolateClick", IsolateClickDirective) diff --git a/app/modules/utils/utils.module.coffee b/app/modules/utils/utils.module.coffee new file mode 100644 index 00000000..d39c4e08 --- /dev/null +++ b/app/modules/utils/utils.module.coffee @@ -0,0 +1,20 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: utils.module.coffee +### + +module = angular.module("taigaUtils", []) diff --git a/app/modules/wiki/history/history-templates/history-attachments.jade b/app/modules/wiki/history/history-templates/history-attachments.jade new file mode 100644 index 00000000..b9a9d074 --- /dev/null +++ b/app/modules/wiki/history/history-templates/history-attachments.jade @@ -0,0 +1,42 @@ + +//- New attachment added +.diff-attachments-new( + ng-if="diff.new.length" + ng-repeat="newAttachment in diff.new" +) + span.key(translate="ACTIVITY.NEW_ATTACHMENT") + span.diff {{newAttachment.filename}} + +//- Attachment updated +.diff-attachments-update( + ng-if="diff.changed.length" + ng-repeat="editAttachment in diff.changed" +) + span.key( + translate="ACTIVITY.UPDATED_ATTACHMENT" + translate-values="{filename: editAttachment.filename}" + ) + span.diff(ng-if="editAttachment.changes.is_deprecated") + span( + ng-if="editAttachment.changes.is_deprecated[1] == false" + translate="ACTIVITY.BECAME_UNDEPRECATED" + ) + span( + ng-if="editAttachment.changes.is_deprecated[1] == true" + translate="ACTIVITY.BECAME_DEPRECATED" + ) + span.diff(ng-if="editAttachment.changes.description") + span(ng-if='editAttachment.changes.description[0].length') {{editAttachment.changes.description[0]}} + span(ng-if='!editAttachment.changes.description[0].length') ... + tg-svg( + svg-icon="icon-arrow-right" + ) + span {{editAttachment.changes.description[1]}} + +//- Attachment deleted +.diff-attachments-deleted( + ng-if="diff.deleted.length" + ng-repeat="deletedAttachment in diff.deleted" +) + span.key(translate="ACTIVITY.DELETED_ATTACHMENT") + span.diff {{deletedAttachment.filename}} diff --git a/app/modules/wiki/history/wiki-history-diff.directive.coffee b/app/modules/wiki/history/wiki-history-diff.directive.coffee new file mode 100644 index 00000000..3388d969 --- /dev/null +++ b/app/modules/wiki/history/wiki-history-diff.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-history.directive.coffee +### + +module = angular.module('taigaWikiHistory') + +WikiHistoryDiffDirective = () -> + return { + templateUrl:"wiki/history/wiki-history-diff.html", + scope: { + key: "<", + diff: "<" + } + } + +module.directive("tgWikiHistoryDiff", WikiHistoryDiffDirective) diff --git a/app/modules/wiki/history/wiki-history-diff.jade b/app/modules/wiki/history/wiki-history-diff.jade new file mode 100644 index 00000000..4ae07bc0 --- /dev/null +++ b/app/modules/wiki/history/wiki-history-diff.jade @@ -0,0 +1,16 @@ +.diff-status-wrapper( + ng-if="key === 'attachments'" +) + include history-templates/history-attachments + +.diff-status-wrapper( + ng-if="key === 'content_diff'" +) + p.diff( + ng-if="diff[0]" + ng-bind-html="diff[0]" + ) + p.diff( + ng-if="diff[1]" + ng-bind-html="diff[1]" + ) diff --git a/app/modules/wiki/history/wiki-history-entry.directive.coffee b/app/modules/wiki/history/wiki-history-entry.directive.coffee new file mode 100644 index 00000000..7f0905ab --- /dev/null +++ b/app/modules/wiki/history/wiki-history-entry.directive.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-history.directive.coffee +### + +module = angular.module('taigaWikiHistory') + +WikiHistoryEntryDirective = () -> + link = (scope, el, attr) -> + scope.singleHistoryEntry = scope.historyEntry.toJS() + + return { + link: link, + templateUrl:"wiki/history/wiki-history-entry.html", + scope: { + historyEntry: "<" + } + } + +module.directive("tgWikiHistoryEntry", WikiHistoryEntryDirective) diff --git a/app/modules/wiki/history/wiki-history-entry.jade b/app/modules/wiki/history/wiki-history-entry.jade new file mode 100644 index 00000000..aa25e102 --- /dev/null +++ b/app/modules/wiki/history/wiki-history-entry.jade @@ -0,0 +1,16 @@ +.activity + img.activity-avatar( + tg-avatar="singleHistoryEntry.user" + ng-alt="{{singleHistoryEntry.user.name}}" + ) + .activity-main + .activity-data + span.activity-creator {{singleHistoryEntry.user.name}} + span.activity-date {{singleHistoryEntry.created_at | momentFormat:'DD MMM YYYY HH:mm'}} + + .activity-diff( + ng-repeat="(key, diff) in singleHistoryEntry.values_diff" + tg-wiki-history-diff + key='key' + diff='diff' + ) diff --git a/app/modules/wiki/history/wiki-history.controller.coffee b/app/modules/wiki/history/wiki-history.controller.coffee new file mode 100644 index 00000000..45f7b983 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.controller.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: wiki-history.controller.coffee +### + +taiga = @.taiga + +module = angular.module("taigaWikiHistory") + +class WikiHistoryController + @.$inject = [ + "tgWikiHistoryService" + ] + + constructor: (@wikiHistoryService) -> + taiga.defineImmutableProperty @, 'historyEntries', () => return @wikiHistoryService.historyEntries + + initializeHistoryEntries: (wikiId) -> + if wikiId + @wikiHistoryService.setWikiId(wikiId) + + @wikiHistoryService.loadHistoryEntries() + +module.controller("WikiHistoryCtrl", WikiHistoryController) diff --git a/app/modules/wiki/history/wiki-history.controller.spec.coffee b/app/modules/wiki/history/wiki-history.controller.spec.coffee new file mode 100644 index 00000000..d0a0fbc8 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.controller.spec.coffee @@ -0,0 +1,62 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: wiki-history.controller.spec.coffee +### + +describe "WikiHistorySection", -> + provide = null + controller = null + mocks = {} + + _mockTgWikiHistoryService = () -> + mocks.tgWikiHistoryService = { + setWikiId: sinon.stub(), + loadHistoryEntries: sinon.stub() + } + + provide.value "tgWikiHistoryService", mocks.tgWikiHistoryService + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgWikiHistoryService() + return null + + beforeEach -> + module "taigaWikiHistory" + + _mocks() + + inject ($controller) -> + controller = $controller + + it "initialize histori entries with id", -> + wikiId = 42 + + historyCtrl = controller "WikiHistoryCtrl" + historyCtrl.initializeHistoryEntries(wikiId) + + expect(mocks.tgWikiHistoryService.setWikiId).to.be.calledOnce + expect(mocks.tgWikiHistoryService.setWikiId).to.be.calledWith(wikiId) + expect(mocks.tgWikiHistoryService.loadHistoryEntries).to.be.calledOnce + + it "initialize history entries without id", -> + historyCtrl = controller "WikiHistoryCtrl" + historyCtrl.initializeHistoryEntries() + + expect(mocks.tgWikiHistoryService.setWikiId).to.not.be.calledOnce + expect(mocks.tgWikiHistoryService.loadHistoryEntries).to.be.calledOnce diff --git a/app/modules/wiki/history/wiki-history.directive.coffee b/app/modules/wiki/history/wiki-history.directive.coffee new file mode 100644 index 00000000..34d96c31 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.directive.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-history.directive.coffee +### + +bindOnce = @.taiga.bindOnce + +module = angular.module('taigaWikiHistory') + + +WikiHistoryDirective = () -> + link = (scope, el, attrs, ctrl) -> + bindOnce scope, 'vm.wikiId', (value) -> + ctrl.initializeHistoryEntries(value) + + return { + scope: {}, + bindToController: { + wikiId: "<" + } + controller: "WikiHistoryCtrl", + controllerAs: "vm", + templateUrl:"wiki/history/wiki-history.html", + link: link + } + +module.directive("tgWikiHistory", WikiHistoryDirective) diff --git a/app/modules/wiki/history/wiki-history.jade b/app/modules/wiki/history/wiki-history.jade new file mode 100644 index 00000000..131df94b --- /dev/null +++ b/app/modules/wiki/history/wiki-history.jade @@ -0,0 +1,12 @@ +nav.history-tabs(ng-if="vm.historyEntries.count()>0") + a.history-tab.active( + href="" + title="{{ACTIVITY.TITLE}}" + translate="ACTIVITY.TITLE" + ) + +section.wiki-history(ng-if="vm.historyEntries.count()>0") + tg-wiki-history-entry.wiki-history-entry( + tg-repeat="historyEntry in vm.historyEntries" + history-entry="historyEntry" + ) diff --git a/app/modules/wiki/history/wiki-history.module.coffee b/app/modules/wiki/history/wiki-history.module.coffee new file mode 100644 index 00000000..4ed5d8d0 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.module.coffee @@ -0,0 +1,20 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-history.module.coffee +### + +angular.module("taigaWikiHistory", []) diff --git a/app/modules/wiki/history/wiki-history.scss b/app/modules/wiki/history/wiki-history.scss new file mode 100644 index 00000000..3e0f5a02 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.scss @@ -0,0 +1,3 @@ +.wiki-history { + margin-bottom: 2rem; +} diff --git a/app/modules/wiki/history/wiki-history.service.coffee b/app/modules/wiki/history/wiki-history.service.coffee new file mode 100644 index 00000000..6b3107ad --- /dev/null +++ b/app/modules/wiki/history/wiki-history.service.coffee @@ -0,0 +1,51 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: wiki-history.service.coffee +### + +taiga = @.taiga + +module = angular.module('taigaWikiHistory') + +class WikiHistoryService extends taiga.Service + @.$inject = [ + "tgResources" + "tgXhrErrorService" + ] + + constructor: (@rs, @xhrError) -> + @._wikiId = null + @._historyEntries = Immutable.List() + + taiga.defineImmutableProperty @, "wikiId", () => return @._wikiId + taiga.defineImmutableProperty @, "historyEntries", () => return @._historyEntries + + setWikiId: (wikiId) -> + @._wikiId = wikiId + @._historyEntries = Immutable.List() + + loadHistoryEntries: () -> + return if not @._wikiId + + return @rs.wikiHistory.getWikiHistory(@._wikiId) + .then (historyEntries) => + if historyEntries.size + @._historyEntries = historyEntries.reverse() + .catch (xhr) => + @xhrError.response(xhr) + _ +module.service("tgWikiHistoryService", WikiHistoryService) diff --git a/app/modules/wiki/history/wiki-history.service.spec.coffee b/app/modules/wiki/history/wiki-history.service.spec.coffee new file mode 100644 index 00000000..26410361 --- /dev/null +++ b/app/modules/wiki/history/wiki-history.service.spec.coffee @@ -0,0 +1,92 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: wiki-history.service.spec.coffee +### + + +describe "tgWikiHistoryService", -> + $provide = null + wikiHistoryService = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + wikiHistory: { + getWikiHistory: sinon.stub() + } + } + $provide.value("tgResources", mocks.tgResources) + + _mockXhrErrorService = () -> + mocks.xhrErrorService = { + response: sinon.stub() + } + + $provide.value "tgXhrErrorService", mocks.xhrErrorService + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockTgResources() + _mockXhrErrorService() + + return null + + _inject = -> + inject (_tgWikiHistoryService_) -> + wikiHistoryService = _tgWikiHistoryService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaWikiHistory" + + _setup() + + it "populate history entries", (done) -> + wikiId = 42 + historyEntries = Immutable.List([ + {id: 1, name: 'history entrie 1'}, + {id: 2, name: 'history entrie 2'}, + {id: 3, name: 'history entrie 3'}, + ]) + + mocks.tgResources.wikiHistory.getWikiHistory.withArgs(wikiId).promise().resolve(historyEntries) + + wikiHistoryService.setWikiId(wikiId) + expect(wikiHistoryService.wikiId).to.be.equal(wikiId) + + expect(wikiHistoryService.historyEntries.size).to.be.equal(0) + wikiHistoryService.loadHistoryEntries().then () -> + expect(wikiHistoryService.historyEntries.size).to.be.equal(3) + done() + + it "reset history entries if wikiId change", () -> + wikiId = 42 + + wikiHistoryService._historyEntries = Immutable.List([ + {id: 1, name: 'history entrie 1'}, + {id: 2, name: 'history entrie 2'}, + {id: 3, name: 'history entrie 3'}, + ]) + + expect(wikiHistoryService.historyEntries.size).to.be.equal(3) + wikiHistoryService.setWikiId(wikiId) + expect(wikiHistoryService.historyEntries.size).to.be.equal(0) diff --git a/app/partials/admin/admin-project-change-owner.jade b/app/partials/admin/admin-project-change-owner.jade index b7a4766a..f29c67d5 100644 --- a/app/partials/admin/admin-project-change-owner.jade +++ b/app/partials/admin/admin-project-change-owner.jade @@ -1,5 +1,5 @@ .owner-avatar - img(ng-src="{{owner.photo || '/#{v}/images/user-noimage.png'}}", alt="{{::owner.full_name_display}}") + img(tg-avatar="owner", alt="{{::owner.full_name_display}}") .owner-info .owner-info-title {{ 'ADMIN.PROJECT_PROFILE.PROJECT_OWNER' | translate }} diff --git a/app/partials/admin/admin-project-modules.jade b/app/partials/admin/admin-project-modules.jade index 09f80d3a..037a231e 100644 --- a/app/partials/admin/admin-project-modules.jade +++ b/app/partials/admin/admin-project-modules.jade @@ -17,12 +17,30 @@ div.wrapper( include ../includes/components/mainTitle form.module-container + .module.module-epics(ng-class="{true:'active', false:''}[project.is_epics_activated]") + .module-icon + tg-svg(svg-icon="icon-epics") + .module-name(translate="ADMIN.MODULES.EPICS") + .module-desc + p(translate="ADMIN.MODULES.EPICS_DESCRIPTION") + .module-activation.module-direct-active + div.check + input.activate-input( + id="functionality-epics" + name="functionality-epics" + type="checkbox" + ng-checked="project.is_epics_activated" + ng-model="project.is_epics_activated" + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") .module.module-scrum(ng-class="{true:'active', false:''}[project.is_backlog_activated]") .module-icon tg-svg(svg-icon="icon-scrum") .module-name(translate="ADMIN.MODULES.BACKLOG") .module-desc - p(translate="ADMIN.MODULES.BACKLOG_DESCRIPTION") + p(translate="ADMIN.MODULES.BACKLOG_DESCRIPTION") .module-desc-options(ng-if="project.is_backlog_activated") fieldset label(for="total-sprints") {{ 'ADMIN.MODULES.NUMBER_SPRINTS' | translate }} diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade index b03e2890..50a41dc9 100644 --- a/app/partials/admin/admin-project-profile.jade +++ b/app/partials/admin/admin-project-profile.jade @@ -72,10 +72,14 @@ div.wrapper( ) fieldset label(for="tags") {{ 'ADMIN.PROJECT_PROFILE.TAGS' | translate }} - div.tags-block( - ng-if="project.id" - tg-lb-tag-line - ng-model="project.tags" + + tg-tag-line-common.tags-block( + disable-color-selection + project="project" + tags="projectTags" + permissions="modify_project" + on-add-tag="ctrl.addTag(name)" + on-delete-tag="ctrl.deleteTag(tag)" ) fieldset(ng-if="project.owner.id != user.id") diff --git a/app/partials/admin/admin-project-reports.jade b/app/partials/admin/admin-project-reports.jade index 1ad4c439..a3e669b9 100644 --- a/app/partials/admin/admin-project-reports.jade +++ b/app/partials/admin/admin-project-reports.jade @@ -18,6 +18,7 @@ div.wrapper( p(translate="ADMIN.REPORTS.DESCRIPTION") + div.admin-attributes-section(tg-csv-epic) div.admin-attributes-section(tg-csv-us) div.admin-attributes-section(tg-csv-task) div.admin-attributes-section(tg-csv-issue) diff --git a/app/partials/admin/admin-project-request-ownership.jade b/app/partials/admin/admin-project-request-ownership.jade index 713bd6b6..7e17b3e7 100644 --- a/app/partials/admin/admin-project-request-ownership.jade +++ b/app/partials/admin/admin-project-request-ownership.jade @@ -1,5 +1,5 @@ .owner-avatar - img(ng-src="{{owner.photo || '/#{v}/images/user-noimage.png'}}", alt="{{::owner.full_name_display}}") + img(tg-avatar="owner", alt="{{::owner.full_name_display}}") .owner-info .title {{ 'ADMIN.PROJECT_PROFILE.PROJECT_OWNER' | translate }} diff --git a/app/partials/admin/admin-project-values-custom-fields.jade b/app/partials/admin/admin-project-values-custom-fields.jade index 691079b5..19cb292b 100644 --- a/app/partials/admin/admin-project-values-custom-fields.jade +++ b/app/partials/admin/admin-project-values-custom-fields.jade @@ -17,6 +17,13 @@ div.wrapper( include ../includes/components/mainTitle p.admin-subtitle(translate="ADMIN.CUSTOM_FIELDS.SUBTITLE") + div.admin-attributes-section( + tg-project-custom-attributes, + ng-controller="ProjectCustomAttributesController as ctrl", + ng-init="type='epic'; customFieldSectionTitle='ADMIN.CUSTOM_FIELDS.EPIC_DESCRIPTION'; customFieldButtonTitle='ADMIN.CUSTOM_FIELDS.EPIC_ADD'" + ) + include ../includes/modules/admin/admin-custom-attributes + div.admin-attributes-section( tg-project-custom-attributes, ng-controller="ProjectCustomAttributesController as ctrl", diff --git a/app/partials/admin/admin-project-values-status.jade b/app/partials/admin/admin-project-values-status.jade index 8eab5f9b..64540f3a 100644 --- a/app/partials/admin/admin-project-values-status.jade +++ b/app/partials/admin/admin-project-values-status.jade @@ -15,6 +15,12 @@ div.wrapper(ng-controller="ProjectValuesSectionController", include ../includes/components/mainTitle p.admin-subtitle(translate="ADMIN.PROJECT_VALUES_STATUS.SUBTITLE") + div.admin-attributes-section(tg-project-values, type="epic-statuses", + ng-controller="ProjectValuesController as ctrl", + ng-init="section='admin'; resource='epics'; type='epic-statuses'; sectionName='ADMIN.PROJECT_VALUES_STATUS.EPIC_TITLE'" + objName="status") + include ../includes/modules/admin/project-status + div.admin-attributes-section(tg-project-values, type="userstory-statuses", ng-controller="ProjectValuesController as ctrl", ng-init="section='admin'; resource='userstories'; type='userstory-statuses'; sectionName='ADMIN.PROJECT_VALUES_STATUS.US_TITLE'", diff --git a/app/partials/admin/admin-project-values-tags.jade b/app/partials/admin/admin-project-values-tags.jade new file mode 100644 index 00000000..ff8830d9 --- /dev/null +++ b/app/partials/admin/admin-project-values-tags.jade @@ -0,0 +1,32 @@ +doctype html + +div.wrapper( + ng-controller="ProjectValuesSectionController", + ng-init="sectionName='ADMIN.PROJECT_VALUES_TAGS.TITLE'" +) + + tg-project-menu + + sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="project-values") + include ../includes/modules/admin-menu + + sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-tags") + include ../includes/modules/admin-submenu-project-values + + section.main.admin-common.admin-attributes.colors-table.tags-table( + tg-project-tags, + ng-controller="ProjectTagsController as ctrl" + ) + header.header-with-actions + .title + include ../includes/components/mainTitle + p.admin-subtitle(translate="ADMIN.PROJECT_VALUES_TAGS.SUBTITLE") + .action-buttons + a.button.button-green.show-add-new( + href="" + title="{{ 'ADMIN.PROJECT_VALUES_TAGS.NEW_TAG'|translate }}" + translate="ADMIN.PROJECT_VALUES_TAGS.NEW_TAG" + ) + + .admin-attributes-section + include ../includes/modules/admin/project-tags diff --git a/app/partials/admin/admin-third-parties-gitlab.jade b/app/partials/admin/admin-third-parties-gitlab.jade index a8950fd1..2f2fbf71 100644 --- a/app/partials/admin/admin-third-parties-gitlab.jade +++ b/app/partials/admin/admin-third-parties-gitlab.jade @@ -60,7 +60,7 @@ div.wrapper.roles( ) a.help-button( - href="https://tree.taiga.io/support/integrations/gitlab-integration/" + href="https://tree.taiga.io/support/integrations/gogs-integration/" target="_blank" ) tg-svg(svg-icon="icon-question") diff --git a/app/partials/admin/admin-third-parties-gogs.jade b/app/partials/admin/admin-third-parties-gogs.jade new file mode 100644 index 00000000..b5763d6c --- /dev/null +++ b/app/partials/admin/admin-third-parties-gogs.jade @@ -0,0 +1,56 @@ +doctype html + +div.wrapper.roles( + tg-gogs-webhooks + ng-controller="GogsController as ctrl", + ng-init="section='admin'" +) + tg-project-menu + sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="third-parties") + include ../includes/modules/admin-menu + sidebar.menu-tertiary.sidebar(tg-admin-navigation="third-parties-gogs") + include ../includes/modules/admin-submenu-third-parties + + section.main.admin-common.admin-third-parties + include ../includes/components/mainTitle + + form + fieldset + label(for="secret-key", translate="ADMIN.THIRD_PARTIES.SECRET_KEY") + input( + id="secret-key" + type="text" + name="secret-key" + ng-model="gogs.secret" + placeholder="{{'ADMIN.THIRD_PARTIES.SECRET_KEY' | translate}}" + ) + + fieldset + .select-input-text(tg-select-input-text) + div + label(for="payload-url", translate="ADMIN.THIRD_PARTIES.PAYLOAD_URL") + .field-with-option + input( + id="payload-url" + type="text" + name="payload-url" + readonly="readonly" + ng-model="gogs.webhooks_url" + placeholder="{{'ADMIN.THIRD_PARTIES.PAYLOAD_URL' | translate}}" + ) + .option-wrapper.select-input-content + tg-svg(svg-icon="icon-clipboard") + .help-copy(translate="COMMON.COPY_TO_CLIPBOARD") + + button.button-green.submit-button( + type="submit" + title="{{'COMMON.SAVE' | translate}}" + translate="COMMON.SAVE" + ) + + a.help-button( + href="https://tree.taiga.io/support/integrations/gogs-integration/" + target="_blank" + ) + tg-svg(svg-icon="icon-question") + span(translate="ADMIN.HELP") diff --git a/app/partials/admin/admin-third-parties-webhooks.jade b/app/partials/admin/admin-third-parties-webhooks.jade index 0246727f..ee52e09e 100644 --- a/app/partials/admin/admin-third-parties-webhooks.jade +++ b/app/partials/admin/admin-third-parties-webhooks.jade @@ -1,7 +1,9 @@ doctype html -div.wrapper.roles(ng-controller="WebhooksController as ctrl", - ng-init="section='admin'") +div.wrapper.roles( + ng-controller="WebhooksController as ctrl", + ng-init="section='admin'" +) tg-project-menu sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="third-parties") @@ -10,25 +12,27 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", include ../includes/modules/admin-submenu-third-parties section.main.admin-common.admin-webhooks(tg-new-webhook) - include ../includes/components/mainTitle - - p.admin-subtitle(translate="ADMIN.WEBHOOKS.SUBTITLE") - div.webhooks-options - a.button-green.hidden.add-webhook( - href="" - title="{{'ADMIN.WEBHOOKS.ADD_NEW' | translate}}" - translate="ADMIN.WEBHOOKS.ADD_NEW" - ) + header.header-with-actions + include ../includes/components/mainTitle + .action-buttons + a.button-green.hidden.add-webhook( + href="" + title="{{'ADMIN.WEBHOOKS.ADD_NEW' | translate}}" + translate="ADMIN.WEBHOOKS.ADD_NEW" + ) section.webhooks-table.basic-table - div.table-header - div.row - div.webhook-service(translate="COMMON.FIELDS.NAME") - div.webhook-url(translate="COMMON.FIELDS.URL") - div.webhook-options - div.table-body - div.single-webhook-wrapper(tg-webhook="webhook", ng-repeat="webhook in webhooks") - div.edition-mode.hidden + .table-header + .row + .webhook-service(translate="COMMON.FIELDS.NAME") + .webhook-url(translate="COMMON.FIELDS.URL") + .webhook-options + .table-body + .single-webhook-wrapper( + tg-webhook="webhook" + ng-repeat="webhook in webhooks" + ) + .edition-mode.hidden form.row fieldset.webhook-service input( @@ -37,9 +41,9 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", data-required="true" ng-model="webhook.name" placeholder="{{'ADMIN.WEBHOOKS.TYPE_NAME' | translate}}" - ) - div.webhook-url - div.webhook-url-inputs + ) + .webhook-url + .webhook-url-inputs fieldset input( type="text" @@ -57,7 +61,7 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", data-required="true" ng-model="webhook.key" ) - div.webhook-options + .webhook-options a.edit-existing( href="" title="{{'ADMIN.WEBHOOKS.SAVE' | translate}}" @@ -69,11 +73,10 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", ) tg-svg(svg-icon="icon-close") - div.visualization-mode - div.row - div.webhook-service - span(ng-bind="webhook.name") - div.webhook-url + .visualization-mode + .row + .webhook-service(ng-bind="webhook.name") + .webhook-url span(ng-bind="webhook.url") a.show-history.toggle-history( href="" @@ -82,18 +85,27 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", translate="ADMIN.WEBHOOKS.SHOW_HISTORY" ) - div.webhook-options - div.webhook-options-wrapper - a.test-webhook(href="", title="{{'ADMIN.WEBHOOKS.TEST' | translate}}") + .webhook-options + .webhook-options-wrapper + a.test-webhook( + href="" + title="{{'ADMIN.WEBHOOKS.TEST' | translate}}" + ) tg-svg(svg-icon="icon-check-empty") - a.edit-webhook(href="", title="{{'ADMIN.WEBHOOKS.EDIT' | translate}}") + a.edit-webhook( + href="" + title="{{'ADMIN.WEBHOOKS.EDIT' | translate}}" + ) tg-svg(svg-icon="icon-edit") - a.delete-webhook(href="", title="{{'ADMIN.WEBHOOKS.DELETE' | translate}}") + a.delete-webhook( + href="" + title="{{'ADMIN.WEBHOOKS.DELETE' | translate}}" + ) tg-svg(svg-icon="icon-trash") - div.webhooks-history(ng-show="webhook.logs") - div.history-single-wrapper(ng-repeat="log in webhook.logs") - div.history-single + .webhooks-history + .history-single-wrapper(ng-repeat="log in webhook.logs") + .history-single div span.history-response-icon( ng-class="log.validStatus ? 'history-success' : 'history-error'" @@ -103,8 +115,8 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", a.toggle-log(href="") tg-svg(svg-icon="icon-arrow-down") - div.history-single-response - div.history-single-request-header + .history-single-response + .history-single-request-header span(translate="ADMIN.WEBHOOKS.REQUEST") a.resend-request( href="" @@ -113,20 +125,29 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", ) tg-svg(svg-icon="icon-reload") span(translate="ADMIN.WEBHOOKS.RESEND_REQUEST") - div.history-single-request-body - div.response-container + .history-single-request-body + .response-container span.response-title(translate="ADMIN.WEBHOOKS.HEADERS") - textarea(name="headers", ng-bind="log.prettySentHeaders") + textarea( + name="headers" + ng-bind="log.prettySentHeaders" + ) - div.response-container + .response-container span.response-title(translate="ADMIN.WEBHOOKS.PAYLOAD") - textarea(name="payload", ng-bind="log.prettySentData") + textarea( + name="payload" + ng-bind="log.prettySentData" + ) - div.history-single-response-header + .history-single-response-header span(translate="ADMIN.WEBHOOKS.RESPONSE") - div.history-single-response-body - div.response-container - textarea(name="response-data", ng-bind="log.response_data") + .history-single-response-body + .response-container + textarea( + name="response-data" + ng-bind="log.response_data" + ) form.new-webhook-form.row.hidden fieldset.webhook-service @@ -137,8 +158,8 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", ng-model="newValue.name" placeholder="{{'ADMIN.WEBHOOKS.TYPE_NAME' | translate}}" ) - div.webhook-url - div.webhook-url-inputs + .webhook-url + .webhook-url-inputs fieldset input( type="text" @@ -156,10 +177,16 @@ div.wrapper.roles(ng-controller="WebhooksController as ctrl", data-required="true" ng-model="newValue.key" ) - div.webhook-options - a.add-new(href="", title="{{'ADMIN.WEBHOOKS.SAVE' | translate}}") + .webhook-options + a.add-new( + href="" + title="{{'ADMIN.WEBHOOKS.SAVE' | translate}}" + ) tg-svg(svg-icon="icon-save") - a.cancel-new(href="", title="{{'ADMIN.WEBHOOKS.CANCEL' | translate}}") + a.cancel-new( + href="" + title="{{'ADMIN.WEBHOOKS.CANCEL' | translate}}" + ) tg-svg(svg-icon="icon-close") a.help-button( diff --git a/app/partials/admin/lightbox-add-members.jade b/app/partials/admin/lightbox-add-members.jade index dbdc66b4..9b8caa73 100644 --- a/app/partials/admin/lightbox-add-members.jade +++ b/app/partials/admin/lightbox-add-members.jade @@ -10,6 +10,7 @@ tg-lightbox-close required placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}" data-required="true" + name="email-{{$index}}" data-type="email" ng-model="member.email" ) @@ -17,12 +18,14 @@ tg-lightbox-close ng-if="!$first" type="email" placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}" + name="email-{{$index}}" data-type="email" ng-model="member.email" ) fieldset select( ng-if="vm.project" + name="role-{{$index}}" ng-model="member.role_id" ng-options="role.id as role.name for role in vm.project.roles" ) diff --git a/app/partials/admin/memberships-row-avatar.jade b/app/partials/admin/memberships-row-avatar.jade index a15f3b45..89411cb6 100644 --- a/app/partials/admin/memberships-row-avatar.jade +++ b/app/partials/admin/memberships-row-avatar.jade @@ -1,9 +1,13 @@ figure.avatar - img(src!="<%- imgurl %>", alt!="<%- full_name %>") + img( + style!="background-color: <%- bg %>" + src!="<%- imgurl %>", alt!="<%- full_name %>" + ) figcaption - span.name(ng-non-bindable) <%- full_name %> + div.name + span(ng-non-bindable) <%- full_name %> <% if (isOwner) { %> - tg-svg( + tg-svg.owner-badge( svg-icon="icon-badge", svg-title-translate="COMMON.OWNER" ) diff --git a/app/partials/auth/invitation.jade b/app/partials/auth/invitation.jade index caf9041c..ececd510 100644 --- a/app/partials/auth/invitation.jade +++ b/app/partials/auth/invitation.jade @@ -2,14 +2,16 @@ div.wrapper div.invitation-main div.centered.invitation-container(tg-invitation) a.avatar(href="", tg-bo-title="invitation.invited_by.full_name_display") - img(tg-bo-src="invitation.invited_by.photo", - tg-bo-alt="invitation.invited_by.full_name_display") + img( + tg-avatar="invitation.invited_by" + tg-bo-alt="invitation.invited_by.full_name_display" + ) span.person-name(tg-bo-bind="invitation.invited_by.full_name_display") span.invitation-text p(translate="AUTH.INVITED_YOU") p.project-name(tg-bo-bind="invitation.project_name") - div.invitation-form + div.invitation-form(ng-class="{'public-register-disabled': !publicRegisterEnabled}") include ../includes/modules/invitation-login-form include ../includes/modules/invitation-register-form diff --git a/app/partials/backlog/backlog.jade b/app/partials/backlog/backlog.jade index def38f1d..721d5cb0 100644 --- a/app/partials/backlog/backlog.jade +++ b/app/partials/backlog/backlog.jade @@ -3,8 +3,21 @@ doctype html div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", ng-init="section='backlog'") tg-project-menu - sidebar.menu-secondary.extrabar.filters-bar(tg-backlog-filters) - include ../includes/modules/backlog-filters + + sidebar.backlog-filter + tg-filter( + q="ctrl.filterQ" + filters="ctrl.filters" + custom-filters="ctrl.customFilters" + selected-filters="ctrl.selectedFilters" + customFilters="ctl.customFilters" + on-save-custom-filter="ctrl.saveCustomFilter(name)" + on-add-filter="ctrl.addFilter(filter)" + on-select-custom-filter="ctrl.selectCustomFilter(filter)" + on-remove-custom-filter="ctrl.removeCustomFilter(filter)" + on-remove-filter="ctrl.removeFilter(filter)" + on-change-q="ctrl.changeQ(q)" + ) section.main.backlog include ../includes/components/mainTitle @@ -23,7 +36,7 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", div.backlog-menu div.backlog-table-options - a.trans-button.move-to-current-sprint.move-to-sprint( + a.trans-button.menu-button.move-to-current-sprint.move-to-sprint.e2e-move-to-sprint( ng-if="currentSprint" href="" title="{{'BACKLOG.MOVE_US_TO_CURRENT_SPRINT' | translate}}" @@ -31,7 +44,7 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", ) tg-svg(svg-icon="icon-move") span.text(translate="BACKLOG.MOVE_US_TO_CURRENT_SPRINT") - a.trans-button.move-to-latest-sprint.move-to-sprint( + a.trans-button.menu-button.move-to-latest-sprint.move-to-sprint.e2e-move-to-sprint( ng-if="!currentSprint" href="" title="{{'BACKLOG.MOVE_US_TO_LATEST_SPRINT' | translate}}" @@ -39,14 +52,21 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", ) tg-svg(svg-icon="icon-move") span.text(translate="BACKLOG.MOVE_US_TO_LATEST_SPRINT") - a.trans-button( - ng-if="userstories.length" + a.trans-button.menu-button.e2e-open-filter.ng-animate-disabled( + ng-if="!ctrl.activeFilters" href="" title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}" id="show-filters-button" translate="BACKLOG.FILTERS.SHOW" ) - a.trans-button( + a.trans-button.menu-button.active.e2e-open-filter.ng-animate-disabled( + ng-if="ctrl.activeFilters" + href="" + title="{{'BACKLOG.FILTERS.HIDE' | translate}}" + id="show-filters-button" + translate="BACKLOG.FILTERS.HIDE" + ) + a.trans-button.menu-button( ng-if="userstories.length" href="" title="{{'BACKLOG.TAGS.TOGGLE' | translate}}" @@ -58,12 +78,9 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", section.backlog-table(ng-class="{'hidden': !userstories.length}") include ../includes/modules/backlog-table - div.empty-backlog( - ng-class="{'hidden': userstories === undefined || userstories.length}" - tg-backlog-empty-sortable - ) + div.empty-large(ng-class="{'hidden': userstories === undefined || userstories.length}") img( - src="/#{v}/images/backlog-empty.png" + src="/#{v}/images/empty/empty_mex.png" alt="{{'BACKLOG.EMPTY' | translate}}" ) p.title(translate="BACKLOG.EMPTY") diff --git a/app/partials/backlog/filter-selected.jade b/app/partials/backlog/filter-selected.jade deleted file mode 100644 index 29e0a12e..00000000 --- a/app/partials/backlog/filter-selected.jade +++ /dev/null @@ -1,9 +0,0 @@ -<% _.each(filters, function(f) { %> -.single-filter.selected( - data-type!="<%- f.type %>" - data-id!="<%- f.id %>" -) - span.name(style!="<%- f.style %>") <%- f.name %> - a.remove-filter(href="") - tg-svg(svg-icon="icon-close") -<% }) %> diff --git a/app/partials/backlog/filters.jade b/app/partials/backlog/filters.jade deleted file mode 100644 index 1d504108..00000000 --- a/app/partials/backlog/filters.jade +++ /dev/null @@ -1,17 +0,0 @@ -<% _.each(filters, function(f) { %> -<% if (f.selected) { %> -a.single-filter.active(data-type!="<%- f.type %>", data-id!="<%- f.id %>") - span.name(style!="<%- f.style %>") - | <%- f.name %> - <% if (f.count){ %> - span.number <%- f.count %> - <% } %> -<% } else { %> -a.single-filter(data-type!="<%- f.type %>", data-id!="<%- f.id %>") - span.name(style!="<%- f.style %>") - | <%- f.name %> - <% if (f.count){ %> - span.number <%- f.count %> - <% } %> -<% } %> -<% }) %> \ No newline at end of file diff --git a/app/partials/common/components/assigned-to.jade b/app/partials/common/components/assigned-to.jade index 9157fc4d..3a8f8eb1 100644 --- a/app/partials/common/components/assigned-to.jade +++ b/app/partials/common/components/assigned-to.jade @@ -1,5 +1,9 @@ .user-avatar(class!="<% if (isIocaine) { %> is-iocaine <% }; %>") - img(src!="<%- photo %>", alt!="<%- fullName %>") + img( + style!="background-color: <%- bg %>" + src!="<%- avatar %>" + alt!="<%- fullName %>" + ) <% if (isIocaine) { %> .iocaine-symbol(title="{{ 'TASK.TITLE_ACTION_IOCAINE' | translate }}") tg-svg(svg-icon="icon-iocaine") @@ -14,7 +18,7 @@ .assigned-to-options <% if (!isEditable && fullNameVisible) { %> - span.assigned-name + span.assigned-name(ng-non-bindable) <%- fullName %> <% }; %> @@ -24,7 +28,7 @@ title="{{ 'COMMON.ASSIGNED_TO.TITLE_ACTION_EDIT_ASSIGNMENT'|translate }}" class!="user-assigned <% if (isEditable) { %>editable<% }; %>" ) - span.assigned-name + span.assigned-name(ng-non-bindable) <% if (fullNameVisible) { %> <%- fullName %> <% }; %> diff --git a/app/partials/common/components/created-by.jade b/app/partials/common/components/created-by.jade index 8486d0fa..606a014d 100644 --- a/app/partials/common/components/created-by.jade +++ b/app/partials/common/components/created-by.jade @@ -12,6 +12,7 @@ title="{{owner.full_name_display}}" ) img( - src="{{owner.photo}}" + ng-style="{'background': owner.bg}" + ng-src="{{owner.avatar}}" alt="{{owner.full_name_display}}" ) diff --git a/app/partials/common/components/editable-subject.jade b/app/partials/common/components/editable-subject.jade index 0a294cba..bc1738c5 100644 --- a/app/partials/common/components/editable-subject.jade +++ b/app/partials/common/components/editable-subject.jade @@ -1,11 +1,16 @@ -.view-subject - | {{ item.subject }} +.view-subject {{ item.subject }} tg-svg.edit( svg-icon="icon-edit", title="{{'COMMON.EDIT' | translate}}" ) .edit-subject - input(type="text", ng-model="item.subject", data-required="true", data-maxlength="500", ng-model-options="{ debounce: 200 }") + input( + type="text" + ng-model="item.subject" + data-required="true" + data-maxlength="500" + ng-model-options="{ debounce: 200 }" + ) span.save-container a.save(href="") tg-svg( diff --git a/app/partials/common/components/list-item-assigned-to-avatar.jade b/app/partials/common/components/list-item-assigned-to-avatar.jade index bb2ead9c..e8535d23 100644 --- a/app/partials/common/components/list-item-assigned-to-avatar.jade +++ b/app/partials/common/components/list-item-assigned-to-avatar.jade @@ -1,3 +1,7 @@ div.avatar - img(src!="<%- imgurl %>", alt!="<%- name %>") + img( + style!="background-color: <%- bg %>" + src!="<%- imgurl %>" + alt!="<%- name %>" + ) span.avatar-caption <%- name %> diff --git a/app/partials/common/components/user-display.jade b/app/partials/common/components/user-display.jade new file mode 100644 index 00000000..3ac35373 --- /dev/null +++ b/app/partials/common/components/user-display.jade @@ -0,0 +1,23 @@ +.user-avatar(ng-if="url") + a( + href="{{url}}" + title="{{user.full_name_display}}" + ) + img( + ng-style="{'background-color': user.bg}" + ng-src="{{user.avatar}}" + alt="{{user.full_name_display}}" + ) +a.user-full-name( + ng-if="url" + href="{{url}}" + title="{{user.full_name_display}}" +) {{user.full_name_display}} + +.user-avatar(ng-if="!url") + img( + ng-style="{'background-color': user.bg}" + ng-src="{{user.avatar}}" + alt="{{user.full_name_display}}" + ) +span.user-full-name(ng-if="!url") {{user.full_name_display}} diff --git a/app/partials/common/components/watchers.jade b/app/partials/common/components/watchers.jade index 7959982f..b899353e 100644 --- a/app/partials/common/components/watchers.jade +++ b/app/partials/common/components/watchers.jade @@ -1,20 +1,15 @@ -<% _.each(watchers, function(watcher) { %> -<% if(watcher) { %> -.user-list-single +.user-list-single(ng-repeat="watcher in watchers") .user-list-avatar img( - src!="<%- watcher.photo %>" - alt!="<%- watcher.full_name_display %>" + tg-avatar="watcher" + alt="{{watcher.full_name_display}}" ) .user-list-name - span <%- watcher.full_name_display %> + span {{watcher.full_name_display}} - <% if(isEditable){ %> tg-svg.js-delete-watcher.delete-watcher( + ng-if="isEditable" svg-icon="icon-trash", svg-title-translate="COMMON.WATCHERS.DELETE", - data-watcher-id!="<%- watcher.id %>" + data-watcher-id="{{watcher.id}}" ) - <% }; %> -<% } %> -<% }); %> diff --git a/app/partials/common/components/wysiwyg.jade b/app/partials/common/components/wysiwyg.jade index 3ed85550..f68d4467 100644 --- a/app/partials/common/components/wysiwyg.jade +++ b/app/partials/common/components/wysiwyg.jade @@ -1,6 +1,7 @@ mixin wysihelp - div.wysiwyg-help - span.drag-drop-help(translate="COMMON.WYSIWYG.ATTACH_FILE_HELP") + .wysiwyg-help + span.drag-drop-help(ng-if="wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP") + span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST") a.help-markdown( href="https://tree.taiga.io/support/misc/taiga-markdown-syntax/" target="_blank" diff --git a/app/partials/common/estimation/us-estimation-points-per-role.jade b/app/partials/common/estimation/us-estimation-points-per-role.jade index e7a2fbb0..94544c0f 100644 --- a/app/partials/common/estimation/us-estimation-points-per-role.jade +++ b/app/partials/common/estimation/us-estimation-points-per-role.jade @@ -1,11 +1,14 @@ ul.points-per-role <% _.each(roles, function(role) { %> - li.ticket-role-points.total(class!="<% if(editable){ %>clickable<% } %>", data-role-id!="<%- role.id %>", title!="<%- role.name %>") + li.ticket-role-points.total( + class!="<% if(editable){ %>clickable<% } %>" + data-role-id!="<%- role.id %>" + title!="<%- role.name %>" + ) span.points <%- role.points %> tg-svg(svg-icon="icon-arrow-down") - span.role - <%- role.name %> + span.role(tg-loading!="<%- loading == role.id %>") <%- role.name %> <% }); %> li.ticket-role-points.total span.points <%- totalPoints %> diff --git a/app/partials/common/history/history-activity.jade b/app/partials/common/history/history-activity.jade deleted file mode 100644 index 3b58f956..00000000 --- a/app/partials/common/history/history-activity.jade +++ /dev/null @@ -1,41 +0,0 @@ -.activity-single(class!="<%- mode %>") - .activity-user - a.avatar(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") - img(src!="<%- avatar %>", alt!="<%- userFullName %>") - .activity-content - .activity-username - a.username(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") - | <%- userFullName %> - span.date - | <%- creationDate %> - - <% if (comment.length > 0) { %> - <% if ((deleteCommentDate || deleteCommentUser)) { %> - .deleted-comment - span(translate="COMMENTS.DELETED_INFO", - translate-values!="{ user: '<%- deleteCommentUser %>', date: '<%- deleteCommentDate %>'}") - <% } %> - .comment.wysiwyg - div(ng-non-bindable) - | <%= comment %> - <% if (!deleteCommentDate && mode !== "activity" && canDeleteComment) { %> - a.comment-delete( - href="", - title!="<%- deleteCommentActionTitle %>", - data-activity-id!="<%- activityId %>" - ) - tg-svg(svg-icon="icon-trash") - <% } %> - <% } %> - - <% if(changes.length > 0) { %> - .changes - <% if (mode != "activity") { %> - a.changes-title(href="", title="{{'ACTIVITY.SHOW_ACTIVITY' | translate}}") - span <%- changesText %> - tg-svg(svg-icon="icon-arrow-right") - <% } %> - <% _.each(changes, function(change) { %> - | <%= change %> - <% }) %> - <% } %> diff --git a/app/partials/common/history/history-base-entries.jade b/app/partials/common/history/history-base-entries.jade deleted file mode 100644 index fe662b98..00000000 --- a/app/partials/common/history/history-base-entries.jade +++ /dev/null @@ -1,6 +0,0 @@ -<% if (showMore > 0) { %> -a(href="" title="{{ 'ACTIVITY.SHOW_MORE' | translate}}" class="show-more show-more-comments", translate="ACTIVITY.SHOW_MORE", translate-values!="{showMore: '<%- showMore %>'}") -<% } %> -<% _.each(entries, function(entry) { %> -<%= entry %> -<% }) %> \ No newline at end of file diff --git a/app/partials/common/history/history-base.jade b/app/partials/common/history/history-base.jade deleted file mode 100644 index 1fddd9c4..00000000 --- a/app/partials/common/history/history-base.jade +++ /dev/null @@ -1,35 +0,0 @@ -include ../components/wysiwyg.jade - -section.history - <% if (commentsVisible || historyVisible) { %> - ul.history-tabs - <% if (commentsVisible) { %> - li.active - a( - href="", - data-section-class="history-comments" - ) - tg-svg(svg-icon="icon-writer") - span.tab-title(translate="COMMENTS.TITLE") - <% } %> - <% if (historyVisible) { %> - li - a( - href="", - data-section-class="history-activity" - ) - tg-svg(svg-icon="icon-timeline") - span.tab-title(translate="ACTIVITY.TITLE") - <% } %> - <% } %> - section.history-comments - .comments-list - div(tg-editable-wysiwyg, ng-model!="<%- ngmodel %>") - div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment") - textarea(ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}", ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup") - <% if (mode !== "edit") { %> - +wysihelp - button(type="button", ng-disabled!="!<%- ngmodel %>.comment.length" title="{{'COMMENTS.COMMENT' | translate}}", translate="COMMENTS.COMMENT", class="button button-green save-comment") - <% } %> - section.history-activity.hidden - .changes-list diff --git a/app/partials/common/history/history-change-attachment.jade b/app/partials/common/history/history-change-attachment.jade deleted file mode 100644 index 6b12816f..00000000 --- a/app/partials/common/history/history-change-attachment.jade +++ /dev/null @@ -1,16 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - <% _.each(diff, function(change) { %> - p - strong <%- change.name %>  - strong(translate="COMMON.FROM") - br - span <%- change.from %> - p - strong <%- change.name %>  - strong(translate="COMMON.TO") - br - span <%- change.to %> - <% }) %> diff --git a/app/partials/common/history/history-change-diff.jade b/app/partials/common/history/history-change-diff.jade deleted file mode 100644 index 13e137bd..00000000 --- a/app/partials/common/history/history-change-diff.jade +++ /dev/null @@ -1,6 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - p - span <%= diff %> diff --git a/app/partials/common/history/history-change-generic.jade b/app/partials/common/history/history-change-generic.jade deleted file mode 100644 index 8c29b5c1..00000000 --- a/app/partials/common/history/history-change-generic.jade +++ /dev/null @@ -1,12 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - p - strong(translate="COMMON.FROM") - br - span <%- from %> - p - strong(translate="COMMON.TO") - br - span <%- to %> diff --git a/app/partials/common/history/history-change-list.jade b/app/partials/common/history/history-change-list.jade deleted file mode 100644 index 038cfc88..00000000 --- a/app/partials/common/history/history-change-list.jade +++ /dev/null @@ -1,17 +0,0 @@ -.change-entry - .activity-changed - span <%- name %> - .activity-fromto - <% if (removed.length > 0) { %> - p - strong(translate="ACTIVITY.REMOVED") - br - span <%- removed %> - <% } %> - - <% if (added.length > 0) { %> - p - strong(translate="ACTIVITY.ADDED") - br - span <%- added %> - <% } %> diff --git a/app/partials/common/history/history-change-points.jade b/app/partials/common/history/history-change-points.jade deleted file mode 100644 index 89429a93..00000000 --- a/app/partials/common/history/history-change-points.jade +++ /dev/null @@ -1,14 +0,0 @@ -<% _.each(points, function(point, name) { %> -.change-entry - .activity-changed - span(translate="ACTIVITY.US_POINTS", translate-values!="{name: '<%- name %>'}") - .activity-fromto - p - strong(translate="COMMON.FROM") - br - span <%- point[0] %> - p - strong(translate="COMMON.TO") - br - span <%- point[1] %> -<% }); %> diff --git a/app/partials/common/history/history-deleted-comment.jade b/app/partials/common/history/history-deleted-comment.jade deleted file mode 100644 index 73a2f2e2..00000000 --- a/app/partials/common/history/history-deleted-comment.jade +++ /dev/null @@ -1,18 +0,0 @@ -.activity-single.comment.deleted-comment - div - span(translate="COMMENTS.DELETED_INFO", - translate-values!="{user: '<%- deleteCommentUser %>', date: '<%- deleteCommentDate %>'}") - a(href="", title="{{'COMMENTS.SHOW_DELETED' | translate}}", - class="show-deleted-comment", translate="COMMENTS.SHOW_DELETED") - a(href="", title="{{'COMMENTS.HIDE_DELETED' | translate}}", - class="hide-deleted-comment hidden", translate="COMMENTS.HIDE_DELETED") - .comment-body.wysiwyg <%= deleteComment %> - <% if (canRestoreComment) { %> - a.comment-restore( - href="" - data-activity-id!="<%- activityId %>" - title="{{ 'COMMENTS.RESTORE' | translate }}" - ) - tg-svg(svg-icon="icon-reload") - span(translate="COMMENTS.RESTORE") - <% } %> diff --git a/app/partials/common/lightbox/lightbox-assigned-to-users.jade b/app/partials/common/lightbox/lightbox-assigned-to-users.jade index d25096e0..16c06377 100644 --- a/app/partials/common/lightbox/lightbox-assigned-to-users.jade +++ b/app/partials/common/lightbox/lightbox-assigned-to-users.jade @@ -5,10 +5,14 @@ href="" title="{{'COMMON.ASSIGNED_TO' | translate}}" ) - img(src!="<%- selected.photo %>") + img( + style!="background: <%- selected.avatar.bg %>" + src!="<%- selected.avatar.url %>" + ) a.user-list-name( href="" title!="<%- selected.full_name_display %>" + ng-non-bindable ) | <%-selected.full_name_display %> tg-svg.remove-assigned-to( @@ -24,10 +28,14 @@ href="#" title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}" ) - img(src!="<%- user.photo %>") + img( + style!="background: <%- user.avatar.bg %>" + src!="<%- user.avatar.url %>" + ) a.user-list-name( href="" title!="<%- user.full_name_display %>" + ng-non-bindable ) | <%- user.full_name_display %> <% }) %> diff --git a/app/partials/common/lightbox/lightbox-attachment-preview.jade b/app/partials/common/lightbox/lightbox-attachment-preview.jade deleted file mode 100644 index 84223a72..00000000 --- a/app/partials/common/lightbox/lightbox-attachment-preview.jade +++ /dev/null @@ -1,5 +0,0 @@ -.attachment-preview - tg-lightbox-close - - a(href="{{::file.get('url')}}", title="{{::file.get('description')}}", target="_blank", download="{{::file.get('name')}}") - img(src="{{::file.get('url')}}") diff --git a/app/partials/common/lightbox/lightbox-change-owner.jade b/app/partials/common/lightbox/lightbox-change-owner.jade index a1264699..b6c966b6 100644 --- a/app/partials/common/lightbox/lightbox-change-owner.jade +++ b/app/partials/common/lightbox/lightbox-change-owner.jade @@ -18,7 +18,7 @@ tg-lightbox-close href="#" title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}" ) - img(ng-src="{{vm.selected.photo}}") + img(tg-avatar="vm.selected") a.user-list-name( href="" title="{{vm.selected.full_name_display}}" @@ -33,7 +33,8 @@ tg-lightbox-close href="#" title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}" ) - img(ng-src="{{user.photo}}") + img(tg-avatar="user") + a.user-list-name( href="" title="{{user.full_name_display}}" diff --git a/app/partials/common/tag/lb-tag-line-tags.jade b/app/partials/common/tag/lb-tag-line-tags.jade index cfe107d7..af7568f9 100644 --- a/app/partials/common/tag/lb-tag-line-tags.jade +++ b/app/partials/common/tag/lb-tag-line-tags.jade @@ -1,5 +1,8 @@ <% _.each(tags, function(tag) { %> -span(class="tag", style!="<%- tag.style %>") +span( + class="tag" + style!="<%- tag.style %>" +) span.tag-name <%- tag.name %> a.remove-tag(href="", title="{{'COMMON.TAGS.DELETE' | translate}}") tg-svg(svg-icon="icon-close") diff --git a/app/partials/common/tag/tag-line.jade b/app/partials/common/tag/tag-line.jade index 336b3731..7457060a 100644 --- a/app/partials/common/tag/tag-line.jade +++ b/app/partials/common/tag/tag-line.jade @@ -1,9 +1,15 @@ .tags-container -a(href="#", class="add-tag hidden", title="{{'COMMON.TAGS.ADD' | translate}}") +a.add-tag.hidden( + href="#" + title="{{'COMMON.TAGS.ADD' | translate}}" +) tg-svg(svg-icon="icon-add") span.add-tag-text(translate="COMMON.TAGS.ADD") span.add-tag-input - input(type="text", placeholder="{{'COMMON.TAGS.PLACEHOLDER' | translate}}", class="tag-input hidden") + input.tag-input.hidden( + type="text" + placeholder="{{'COMMON.TAGS.PLACEHOLDER' | translate}}" + ) span.save.hidden(title="{{'COMMON.SAVE' | translate}}") - tg-svg(svg-icon="icon-save") \ No newline at end of file + tg-svg(svg-icon="icon-save") diff --git a/app/partials/common/tag/tags-line-tags.jade b/app/partials/common/tag/tags-line-tags.jade index 90b934e4..07f3a981 100644 --- a/app/partials/common/tag/tags-line-tags.jade +++ b/app/partials/common/tag/tags-line-tags.jade @@ -1,8 +1,16 @@ <% _.each(tags, function(tag) { %> -span(class="tag", style!="border-left: 5px solid <%- tag.color %>;") +<% if (tag.name == deleteTagLoading) { %> +div(tg-loading="true") +<% } else { %> +span.tag(style!="border-left: 5px solid <%- tag.style %>;") span.tag-name <%- tag.name %> <% if (isEditable) { %> - a.remove-tag(href="", title="{{'COMMON.TAGS.DELETE' | translate}}") + a.remove-tag( + href="" + title="{{'COMMON.TAGS.DELETE' | translate}}" + ) tg-svg(svg-icon="icon-close") <% } %> +<% } %> <% }); %> +div(tg-loading!="<%- loading %>") diff --git a/app/partials/contrib/main.jade b/app/partials/contrib/main.jade index 5ed1fd83..b1ffdf7b 100644 --- a/app/partials/contrib/main.jade +++ b/app/partials/contrib/main.jade @@ -1,6 +1,9 @@ doctype html -div.wrapper.roles(ng-init="section='admin'", ng-controller="ContribController as ctrl") +div.wrapper.roles( + ng-init="section='admin'" + ng-controller="ContribController as ctrl" +) tg-project-menu sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="contrib") diff --git a/app/partials/custom-attributes/custom-attributes-values.jade b/app/partials/custom-attributes/custom-attributes-values.jade index e2dbdcd3..2a5ec5eb 100644 --- a/app/partials/custom-attributes/custom-attributes-values.jade +++ b/app/partials/custom-attributes/custom-attributes-values.jade @@ -1,8 +1,17 @@ section.duty-custom-fields(ng-show="ctrl.customAttributes.length") - div.custom-fields-header + .custom-fields-header span(translate="COMMON.CUSTOM_ATTRIBUTES.CUSTOM_FIELDS") - // Remove .open class on click on this button in both .icon and .custom-fields-body to close - a.collapse(href="", class!="<% if (!collapsed) { %>open<% } %>") + a.collapse( + href="" + ng-class="{'open': !collapsed}" + ng-click="toggleCollapse()" + ) tg-svg(svg-icon="icon-arrow-down") - div.custom-fields-body(class!="<% if (!collapsed) { %>open<% } %>") - div(ng-repeat="att in ctrl.customAttributes", tg-custom-attribute-value="ctrl.getAttributeValue(att)", required-edition-perm!="<%- requiredEditionPerm %>") + .custom-fields-body( + ng-show="!collapsed" + ) + .custom-attribute( + ng-repeat="attr in ctrl.customAttributes" + tg-custom-attribute-value="ctrl.getAttributeValue(attr)" + required-edition-perm!="<%- requiredEditionPerm %>" + ) diff --git a/app/partials/epic/epic-detail.jade b/app/partials/epic/epic-detail.jade new file mode 100644 index 00000000..52bb688c --- /dev/null +++ b/app/partials/epic/epic-detail.jade @@ -0,0 +1,128 @@ +doctype html + +div.wrapper( + ng-controller="EpicDetailController as ctrl", + ng-init="section='epics'" +) + tg-project-menu + + div.main.us-detail + div.us-detail-header.header-with-actions + include ../includes/components/mainTitle + + section.us-story-main-data + header + tg-vote-button.upvote-btn( + item="epic" + on-upvote="ctrl.onUpvote" + on-downvote="ctrl.onDownvote" + ) + + .detail-header-container.epic-header-container(ng-class="{blocked: epic.is_blocked}") + tg-color-selector.color-selector( + is-color-required="true" + init-color="epic.color" + on-select-color="ctrl.onSelectColor(color)" + required-perm="modify_epic" + ) + tg-detail-header( + item="epic" + project="project" + required-perm="modify_epic" + ng-if="project && epic" + format="text" + ) + .subheader + tg-tag-line.tags-block( + ng-if="epic && project" + project="project" + item="epic" + permissions="modify_epic" + ) + tg-created-by-display.ticket-created-by(ng-model="epic") + + section.duty-content( + tg-editable-description + tg-editable-wysiwyg + ng-model="epic" + required-perm="modify_epic" + ) + + // Custom Fields + tg-custom-attributes-values( + ng-model="epic" + type="epic" + project="project" + required-edition-perm="modify_epic" + ) + + tg-related-userstories( + project="immutableProject" + userstories="userstories" + epic="immutableEpic" + ) + + tg-attachments-full( + obj-id="epic.id" + type="epic", + project-id="projectId" + edit-permission = "modify_epic" + ) + + tg-history-section( + ng-if="epic" + type="epic" + name="epic" + id="epic.id" + project-id="projectId" + ) + + sidebar.menu-secondary.sidebar.ticket-data + + .ticket-header + span.ticket-title( + tg-epic-status-display + ng-model="epic" + ) + span.detail-status( + tg-epic-status-button + ng-model="epic" + ) + + section.ticket-assigned-to( + tg-assigned-to + ng-model="epic" + required-perm="modify_epic" + ) + + section.ticket-watch-buttons + div.ticket-watch( + tg-watch-button + item="epic" + data-environment="ticket" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) + div.ticket-watchers( + tg-watchers + ng-model="epic" + required-perm="modify_epic" + ) + + section.ticket-detail-settings + tg-us-team-requirement-button(ng-model="epic") + tg-us-client-requirement-button(ng-model="epic") + tg-block-button( + tg-check-permission="modify_epic", + ng-model="epic" + ) + tg-delete-button( + tg-check-permission="delete_epic", + on-delete-title="{{'EPIC.ACTION_DELETE' | translate}}", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="epic" + ) + + div.lightbox.lightbox-block(tg-lb-block, ng-model="epic", title="EPIC.LIGHTBOX_TITLE_BLOKING_EPIC") + div.lightbox.lightbox-select-user(tg-lb-assignedto) + div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/includes/components/backlog-row.jade b/app/partials/includes/components/backlog-row.jade index 0ed659b9..4a3a6143 100644 --- a/app/partials/includes/components/backlog-row.jade +++ b/app/partials/includes/components/backlog-row.jade @@ -1,23 +1,23 @@ -div.row.us-item-row( +.row.us-item-row( ng-repeat="us in userstories track by us.id" tg-bind-scope ng-class="{blocked: us.is_blocked}" tg-class-permission="{'readonly': '!modify_us'}" ) - div.input(tg-check-permission="modify_us") + .input(tg-check-permission="modify_us") input( type="checkbox" name="" ) - div.votes( + .votes( ng-class="{'inactive': !us.total_voters, 'is-voted': us.is_voter}" title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:us.total_voters||0}:'messageformat' }}" ) tg-svg(svg-icon="icon-upvote") span {{ ::us.total_voters }} - div.user-stories - div.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog") - div.user-story-name + .user-stories + .tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog") + .user-story-name a.clickable( href="" tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" @@ -26,7 +26,12 @@ div.row.us-item-row( ) span(tg-bo-ref="us.ref") span(ng-bind="us.subject") - div.us-settings + tg-belong-to-epics( + format="pill" + ng-if="us.epics" + epics="us.epics" + ) + .us-settings a.e2e-edit.edit-story( href="" tg-check-permission="modify_us" diff --git a/app/partials/includes/components/empty-search-results.jade b/app/partials/includes/components/empty-search-results.jade index f3248bd3..47634e21 100644 --- a/app/partials/includes/components/empty-search-results.jade +++ b/app/partials/includes/components/empty-search-results.jade @@ -1,5 +1,5 @@ img( - src="/#{v}/images/search-empty.png" + src="/#{v}/images/empty/empty_tex.png" alt="{{ 'SEARCH.EMPTY_TITLE' | translate }}" ) p.title {{ 'SEARCH.EMPTY_TITLE' | translate }} diff --git a/app/partials/includes/components/select-color.jade b/app/partials/includes/components/select-color.jade index 3f9f4306..16e3e777 100644 --- a/app/partials/includes/components/select-color.jade +++ b/app/partials/includes/components/select-color.jade @@ -1,26 +1,8 @@ div.popover.select-color ul - li.color(style="background: #fce94f", data-color="#fce94f") - li.color(style="background: #edd400", data-color="#edd400") - li.color(style="background: #c4a000", data-color="#c4a000") - li.color(style="background: #8ae234", data-color="#8ae234") - li.color(style="background: #73d216", data-color="#73d216") - li.color(style="background: #4e9a06", data-color="#4e9a06") - li.color(style="background: #d3d7cf", data-color="#d3d7cf") - li.color(style="background: #fcaf3e", data-color="#fcaf3e") - li.color(style="background: #f57900", data-color="#f57900") - li.color(style="background: #ce5c00", data-color="#ce5c00") - li.color(style="background: #729fcf", data-color="#729fcf") - li.color(style="background: #3465a4", data-color="#3465a4") - li.color(style="background: #204a87", data-color="#204a87") - li.color(style="background: #888a85", data-color="#888a85") - li.color(style="background: #ad7fa8", data-color="#ad7fa8") - li.color(style="background: #75507b", data-color="#75507b") - li.color(style="background: #5c3566", data-color="#5c3566") - li.color(style="background: #ef2929", data-color="#ef2929") - li.color(style="background: #cc0000", data-color="#cc0000") - li.color(style="background: #a40000", data-color="#a40000") - li.color(style="background: #2e3436", data-color="#2e3436") + li.color(ng-repeat="c in colorList" ng-style="::{background: c}", data-color="{{::c }}") + li.color.empty-color(ng-if="allowEmpty", data-color="") input(type="text", placeholder="personalized colors", ng-model="color") - div.selected-color(ng-style="{'background-color': color}") + div.selected-color(ng-style="{'background-color': color}", ng-if="color !== null") + div.selected-color(ng-style="{'background-color': none}", ng-if="color === null") diff --git a/app/partials/includes/components/sprint-summary.jade b/app/partials/includes/components/sprint-summary.jade index 364a336a..b4f5f6ac 100644 --- a/app/partials/includes/components/sprint-summary.jade +++ b/app/partials/includes/components/sprint-summary.jade @@ -1,29 +1,44 @@ div.summary.large-summary div.large-summary-wrapper - div.summary-progress-wrapper + .summary-progress-wrapper div.summary-progress-bar(tg-progress-bar="stats.completedPercentage") div.data - span.number(ng-bind="stats.completedPercentage + '%'") + span.number(ng-bind="stats.completedPercentage + '%'") - div.summary-stats - span.number(ng-bind="stats.totalPointsSum|default:'--'") - span.description(translate="BACKLOG.SPRINT_SUMMARY.TOTAL_POINTS") - div.summary-stats - span.number(ng-bind="stats.completedPointsSum|default:'--'") - span.description(translate="BACKLOG.SPRINT_SUMMARY.COMPLETED_POINTS") + .stats-wrapper(ng-class="{'show-role-points': showRolePoints}") + .main-summary-stats + span.summary-stats.toggle-points-per-role(ng-click="showRolePoints = true") + tg-svg(svg-icon="icon-arrow-down") + span.number(ng-bind="stats.totalPointsSum|default:'--'") + span.description(translate="BACKLOG.SPRINT_SUMMARY.TOTAL_POINTS") + div.summary-stats.summary-completed-points + span.number(ng-bind="stats.completedPointsSum|default:'--'") + span.description(translate="BACKLOG.SPRINT_SUMMARY.COMPLETED_POINTS") - div.summary-stats - tg-svg(svg-icon="icon-bulk") - span.number(ng-bind="stats.openTasks|default:'--'") - span.description(translate="BACKLOG.SPRINT_SUMMARY.OPEN_TASKS") - div.summary-stats - span.number(ng-bind="stats.completed_tasks|default:'--'") - span.description(translate="BACKLOG.SPRINT_SUMMARY.CLOSED_TASKS") + div.summary-stats.summary-open-tasks + tg-svg(svg-icon="icon-bulk") + span.number(ng-bind="stats.openTasks|default:'--'") + span.description(translate="BACKLOG.SPRINT_SUMMARY.OPEN_TASKS") + div.summary-stats.summary-closed-tasks + span.number(ng-bind="stats.completed_tasks|default:'--'") + span.description(translate="BACKLOG.SPRINT_SUMMARY.CLOSED_TASKS") - div.summary-stats(title="{{'COMMON.IOCAINE_TEXT' | translate}}") - tg-svg(svg-icon="icon-iocaine") - span.number(ng-bind="stats.iocaine_doses|default:'--'") - span.description(translate="BACKLOG.SPRINT_SUMMARY.IOCAINE_DOSES") + div.summary-stats.summary-iocaine(title="{{'COMMON.IOCAINE_TEXT' | translate}}") + tg-svg(svg-icon="icon-iocaine") + span.number(ng-bind="stats.iocaine_doses|default:'--'") + span.description(translate="BACKLOG.SPRINT_SUMMARY.IOCAINE_DOSES") + + .points-per-role-stats.toggle-points-per-role( + ng-click="showRolePoints = false" + ) + span.points-per-role-stats-title + tg-svg(svg-icon="icon-arrow-up") + span(translate="BACKLOG.SPRINT_SUMMARY.POINTS_PER_ROLE") + + .points-per-role-stats-content + .summary-stats(ng-repeat="rolePoint in pointsByRole") + span.number {{rolePoint.points}} + span.role {{rolePoint.name}} div.stats.toggle-analytics-visibility(title="{{'BACKLOG.SPRINT_SUMMARY.SHOW_STATISTICS_TITLE' | translate}}") tg-svg(svg-icon="icon-graph") diff --git a/app/partials/includes/components/taskboard-task.jade b/app/partials/includes/components/taskboard-task.jade deleted file mode 100644 index 07808869..00000000 --- a/app/partials/includes/components/taskboard-task.jade +++ /dev/null @@ -1,18 +0,0 @@ -div.taskboard-tagline(tg-colorize-tags="task.tags", tg-colorize-tags-type="taskboard") -div.taskboard-task-inner - div.taskboard-user-avatar(tg-taskboard-user-avatar, users="usersById", task="task", project="project") - tg-svg.iocaine( - ng-if="task.is_iocaine" - svg-icon="icon-iocaine", - svg-title="{{'COMMON.IOCAINE_TEXT' | translate}}" - ) - p.taskboard-text - a.task-assigned(href="", title="{{'TASKBOARD.TITLE_ACTION_ASSIGN' | translate}}") - span.task-num(tg-bo-ref="task.ref") - a.task-name(href="", title="#{{ ::task.ref }} {{ ::task.subject }}", ng-bind="task.subject", - tg-nav="project-tasks-detail:project=project.slug,ref=task.ref") - tg-svg.edit-task( - tg-check-permission="modify_task" - svg-icon="icon-edit", - svg-title-translate="TASKBOARD.TITLE_ACTION_EDIT" - ) diff --git a/app/partials/includes/modules/admin-submenu-project-values.jade b/app/partials/includes/modules/admin-submenu-project-values.jade index 00533831..bd0c71c3 100644 --- a/app/partials/includes/modules/admin-submenu-project-values.jade +++ b/app/partials/includes/modules/admin-submenu-project-values.jade @@ -24,3 +24,7 @@ section.admin-submenu li#adminmenu-values-custom-fields a(href="", tg-nav="project-admin-project-values-custom-fields:project=project.slug") span.title(translate="ADMIN.SUBMENU_PROJECT_VALUES.CUSTOM_FIELDS") + + li#adminmenu-values-tags + a(href="", tg-nav="project-admin-project-values-tags:project=project.slug") + span.title(translate="ADMIN.SUBMENU_PROJECT_VALUES.TAGS") diff --git a/app/partials/includes/modules/admin-submenu-third-parties.jade b/app/partials/includes/modules/admin-submenu-third-parties.jade index a0666cb1..ab39cb4c 100644 --- a/app/partials/includes/modules/admin-submenu-third-parties.jade +++ b/app/partials/includes/modules/admin-submenu-third-parties.jade @@ -13,3 +13,6 @@ section.admin-submenu li#adminmenu-third-parties-bitbucket a(href="", tg-nav="project-admin-third-parties-bitbucket:project=project.slug") span.title Bitbucket + li#adminmenu-third-parties-gogs + a(href="", tg-nav="project-admin-third-parties-gogs:project=project.slug") + span.title Gogs diff --git a/app/partials/includes/modules/admin/admin-custom-attributes.jade b/app/partials/includes/modules/admin/admin-custom-attributes.jade index ddbb11b0..847a4133 100644 --- a/app/partials/includes/modules/admin/admin-custom-attributes.jade +++ b/app/partials/includes/modules/admin/admin-custom-attributes.jade @@ -16,13 +16,13 @@ section.custom-fields-table.basic-table .table-body .js-sortable - div( + .e2e-item( ng-repeat="attr in customAttributes track by attr.id" tg-bind-scope ) form.js-form(tg-bind-scope) div.row.single-custom-field.js-view-custom-field - tg-svg(svg-icon="icon-drag") + tg-svg.e2e-drag(svg-icon="icon-drag") div.custom-name {{ attr.name }} div.custom-description {{ attr.description }} div.custom-field-type(ng-switch on="attr.type") diff --git a/app/partials/includes/modules/admin/default-values.jade b/app/partials/includes/modules/admin/default-values.jade index a8ab9e6e..cd3b3745 100644 --- a/app/partials/includes/modules/admin/default-values.jade +++ b/app/partials/includes/modules/admin/default-values.jade @@ -1,20 +1,40 @@ section.default-values form + + //- Epics + fieldset + label(for="default-value-epic", translate="ADMIN.DEFAULT_VALUES.LABEL_EPIC_STATUS") + select(id="default-value-epic", ng-model="project.default_epic_status", + ng-options="s.id as s.name for s in epicStatusList") + + //- User stories + fieldset + label(for="default-value-us", translate="ADMIN.DEFAULT_VALUES.LABEL_US_STATUS") + select(id="default-value-us", ng-model="project.default_us_status", + ng-options="s.id as s.name for s in usStatusList") + fieldset label(for="default-points", translate="ADMIN.DEFAULT_VALUES.LABEL_POINTS") select(id="default-points", ng-model="project.default_points", ng-options="s.id as s.name for s in pointsList") - fieldset - label(for="default-value-us", translate="ADMIN.DEFAULT_VALUES.LABEL_US") - select(id="default-value-us", ng-model="project.default_us_status", - ng-options="s.id as s.name for s in usStatusList") - + //- Tasks fieldset label(for="default-value-task", translate="ADMIN.DEFAULT_VALUES.LABEL_TASK_STATUS") select(id="default-value-task", ng-model="project.default_task_status", ng-options="s.id as s.name for s in taskStatusList") + //- Issues + fieldset + label(for="default-value-issue-type", translate="ADMIN.DEFAULT_VALUES.LABEL_ISSUE_TYPE") + select(id="default-value-issue-type", ng-model="project.default_issue_type", + ng-options="s.id as s.name for s in issueTypesList") + + fieldset + label(for="default-value-issue-status", translate="ADMIN.DEFAULT_VALUES.LABEL_ISSUE_STATUS") + select(id="default-value-issue-status", ng-model="project.default_issue_status", + ng-options="s.id as s.name for s in issueStatusList") + fieldset label(for="default-value-priority", translate="ADMIN.DEFAULT_VALUES.LABEL_PRIORITY") select(id="default-value-priority", ng-model="project.default_priority", @@ -25,16 +45,6 @@ section.default-values select(id="default-value-severity", ng-model="project.default_severity", ng-options="s.id as s.name for s in severitiesList") - fieldset - label(for="default-value-issue-type", translate="ADMIN.DEFAULT_VALUES.LABEL_ISSUE_TYPE") - select(id="default-value-issue-type", ng-model="project.default_issue_type", - ng-options="s.id as s.name for s in issueTypesList") - - fieldset - label(for="default-value-issue-status", translate="ADMIN.DEFAULT_VALUES.LABEL_ISSUE_STATUS") - select(id="default-value-issue-status", ng-model="project.default_issue_status", - ng-options="s.id as s.name for s in issueStatusList") - fieldset button.button-green.submit-button(type="submit", title="{{'COMMON.SAVE' | translate}}") span(translate="COMMON.SAVE") diff --git a/app/partials/includes/modules/admin/project-tags.jade b/app/partials/includes/modules/admin/project-tags.jade new file mode 100644 index 00000000..2c31ea1b --- /dev/null +++ b/app/partials/includes/modules/admin/project-tags.jade @@ -0,0 +1,186 @@ +section + .admin-tags-section-wrapper-empty( + ng-if="!projectTagsAll.length && ctrl.loading" + tg-loading="ctrl.loading" + ) + + .admin-tags-section-wrapper + form.add-tag-container.new-value.hidden + tg-color-selector.color-column( + is-color-required="false" + init-color="newValue.color" + on-select-color="newValue.color = color" + ) + + .tag-name + input( + name="tag" + type="text" + placeholder="{{'ADMIN.TYPES.PLACEHOLDER_WRITE_NAME' | translate}}", + ng-model="newValue.tag" + data-required="true" + data-maxlength="255" + ) + + .options-column(tg-loading="loadingCreate") + a.add-new.e2e-save(href="") + tg-svg( + title="{{'COMMON.ADD' | translate}}", + svg-icon="icon-save" + ) + a.delete-new(href="") + tg-svg( + title="{{'COMMON.CANCEL' | translate}}", + svg-icon="icon-close" + ) + + .empty-large.tags-empty(ng-if="!projectTagsAll.length && !ctrl.loading") + img( + src="/#{v}/images/empty/empty_field.png" + alt="{{'BACKLOG.EMPTY' | translate}}" + ) + p.title(translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY") + + .table-header.table-tags-editor( + ng-if="projectTagsAll.length" + ) + div.row.header-tag-row + .color-column(translate="COMMON.FIELDS.COLOR") + .status-name(translate="COMMON.FIELDS.NAME") + .color-filter + input.e2e-tags-filter( + id="filter-tags-input" + type="text" + name="name" + ng-model="tagsFilter.name" + ng-model-options="{debounce: 200}" + ) + label(for="filter-tags-input") + tg-svg(svg-icon="icon-search") + + .table-main.table-admin-tags( + ng-if="projectTagsAll.length" + ) + div(ng-show="!mixingTags.toTag") + .empty-large.admin-attributes-section-wrapper-empty( + ng-show="!projectTags.length" + tg-loading="ctrl.loading" + ) + img( + src="/#{v}/images/empty/empty_moon.png" + alt="{{'BACKLOG.EMPTY' | translate}}" + ) + p(translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY_SEARCH") + + div.e2e-tag-row( + ng-repeat="tag in projectTags" + tg-bind-scope + ) + form(tg-bind-scope) + .row.tag-row.table-main.visualization(ng-class="{{ ctrl.mixingClass(tag) }}") + .color-column + .current-color( + ng-style="{background: tag.color}" + ng-if="tag.color" + ) + .current-color.empty-color(ng-if="!tag.color") + + .status-name + span(tg-bo-html="tag.name") + + .options-column + a.mix-tags(href="") + tg-svg( + title="{{'ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE' | translate}}" + svg-icon="icon-merge" + ) + div.popover.merge-explanation + span(translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE") + + a.edit-value(href="") + tg-svg( + svg-icon="icon-edit" + title="{{'ADMIN.COMMON.TITLE_ACTION_EDIT_VALUE' | translate}}" + ) + + a.delete-tag(href="", tg-loading="loadingDelete") + tg-svg( + svg-icon="icon-trash" + title="{{'ADMIN.COMMON.TITLE_ACTION_DELETE_VALUE' | translate}}" + ) + + .row.tag-row.table-main.edition.hidden + tg-color-selector.color-column( + is-color-required="false" + ng-model="tag" + init-color="tag.color" + on-select-color="tag.color = color" + ) + + .status-name + input( + name="to_tag" + type="text" + placeholder="{{'ADMIN.TYPES.PLACEHOLDER_WRITE_NAME' | translate}}", + ng-model="tag.name" + data-required="true" + data-maxlength="255" + ) + + .options-column(tg-loading="loadingEdit") + a.save.e2e-save(href="") + tg-svg( + title="{{'COMMON.SAVE' | translate}}" + svg-icon="icon-save" + ) + a.cancel(href="") + tg-svg( + title="{{'COMMON.CANCEL' | translate}}" + svg-icon="icon-close" + ) + + div(ng-show="mixingTags.toTag") + div( + ng-repeat="tag in projectTags" + tg-bind-scope + ) + form(tg-bind-scope) + .row.mixing-row.table-main.visualization( + ng-class="ctrl.mixingClass(tag)" + ) + .color-column + .current-color( + ng-if="tag.color" + ng-style="{background: tag.color}" + ) + .current-color.empty-color( + ng-if="!tag.color" + ) + + .status-name + span(tg-bo-html="tag.name") + + .mixing-options-column( + ng-if="mixingTags.toTag === tag.name" + tg-loading="loadingMixing" + ) + .mixing-help-text( + translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_HELP_TEXT" + ) + a.mixing-confirm.button-green( + href="" + ng-if="mixingTags.fromTags.length" + translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE" + ) + a.mixing-cancel.button-gray( + href="" + translate="COMMON.CANCEL" + ) + + .mixing-options-column( + ng-if="mixingTags.fromTags.indexOf(tag.name) !== -1" + ) + tg-svg.mixing-selected( + title="{{'ADMIN.PROJECT_VALUES_TAGS.SELECTED' | translate}}" + svg-icon="icon-merge" + ) diff --git a/app/partials/includes/modules/backlog-filters.jade b/app/partials/includes/modules/backlog-filters.jade deleted file mode 100644 index 6198bfaa..00000000 --- a/app/partials/includes/modules/backlog-filters.jade +++ /dev/null @@ -1,36 +0,0 @@ -section.filters - div.filters-inner - h1 - span.title(translate="COMMON.FILTERS.TITLE") - - form - fieldset - input(type="text", placeholder="{{'COMMON.FILTERS.INPUT_PLACEHOLDER' | translate}}", ng-model="filtersQ") - tg-svg.search-action( - svg-icon="icon-search", - title="{{'COMMON.FILTERS.TITLE_ACTION_FILTER_BUTTON' | translate}}" - ) - - div.filters-step-cat - div.filters-applied - h2.hidden.breadcrumb - a.back( - href="" - title="{{'COMMON.FILTERS.BREADCRUMB_TITLE' | translate}}" - translate="BACKLOG.FILTERS.TITLE" - ) - tg-svg(svg-icon="icon-arrow-right") - a.subfilter(href="") - span.title(translate="COMMON.FILTERS.BREADCRUMB_STATUS") - div.filters-cats - ul - li - a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_STATUS' | translate}}", data-type="status") - span.title(translate="BACKLOG.FILTERS.FILTER_CATEGORY_STATUS") - tg-svg(svg-icon="icon-arrow-right") - li - a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_TAGS' | translate}}", data-type="tags") - span.title(translate="BACKLOG.FILTERS.FILTER_CATEGORY_TAGS") - tg-svg(svg-icon="icon-arrow-right") - - div.filter-list.hidden diff --git a/app/partials/includes/modules/invitation-register-form.jade b/app/partials/includes/modules/invitation-register-form.jade index b5921ffa..1afc1d31 100644 --- a/app/partials/includes/modules/invitation-register-form.jade +++ b/app/partials/includes/modules/invitation-register-form.jade @@ -1,9 +1,9 @@ -form.register-form +form.register-form(ng-show="publicRegisterEnabled") p.form-header(translate="REGISTER_FORM.TITLE") fieldset input( type="text" - autocorrect="off" + autocorrect="off" autocapitalize="none" name="username" ng-model="dataRegister.username" diff --git a/app/partials/includes/modules/issues-filters.jade b/app/partials/includes/modules/issues-filters.jade deleted file mode 100644 index 1dd12fa8..00000000 --- a/app/partials/includes/modules/issues-filters.jade +++ /dev/null @@ -1,84 +0,0 @@ -section.filters - div.filters-inner - h1 - span.title(translate="ISSUES.FILTERS.TITLE") - form - fieldset - input(type="text", placeholder="{{'ISSUES.FILTERS.INPUT_SEARCH_PLACEHOLDER' | translate}}", - ng-model="filtersQ") - tg-svg.search-action(svg-icon="icon-search", title="{{'ISSUES.FILTERS.TITLE_ACTION_SEARCH' | translate}}") - div.filters-step-cat - div.filters-applied - a.hide.button.button-gray.save-filters(href="", title="{{'COMMON.SAVE' | translate}}", ng-class="{hide: filters.length}", translate="ISSUES.FILTERS.ACTION_SAVE_CUSTOM_FILTER") - h2.hidden.breadcrumb - a.back(href="", title="{{'ISSUES.FILTERS.TITLE_BREADCRUMB' | translate}}", translate="ISSUES.FILTERS.BREADCRUMB") - tg-svg(svg-icon="icon-arrow-right") - a.subfilter(href="", title="cat-name") - span.title(translate="COMMON.FILTERS.BREADCRUMB_STATUS") - div.filters-cats - ul - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.TYPE' | translate}}" - data-type="types" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.TYPE") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.STATUS' | translate}}" - data-type="status" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.STATUS") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.SEVERITY' | translate}}" - data-type="severities" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.SEVERITY") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.PRIORITIES' | translate}}" - data-type="priorities" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.PRIORITIES") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.TAGS' | translate}}" - data-type="tags" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.TAGS") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single(href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.ASSIGNED_TO' | translate}}" - data-type="assignedTo" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.ASSIGNED_TO") - tg-svg(svg-icon="icon-arrow-right") - li - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.CREATED_BY' | translate}}" - data-type="createdBy" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.CREATED_BY") - tg-svg(svg-icon="icon-arrow-right") - li.custom-filters(ng-if="filters.myFilters.length") - a.filters-cat-single( - href="" - title="{{ 'ISSUES.FILTERS.CATEGORIES.CUSTOM_FILTERS' | translate}}" - data-type="myFilters" - ) - span.title(translate="ISSUES.FILTERS.CATEGORIES.CUSTOM_FILTERS") - tg-svg(svg-icon="icon-arrow-right") - - div.filter-list.hidden diff --git a/app/partials/includes/modules/issues-table.jade b/app/partials/includes/modules/issues-table.jade index 10f4473e..c1a5fcfe 100644 --- a/app/partials/includes/modules/issues-table.jade +++ b/app/partials/includes/modules/issues-table.jade @@ -1,13 +1,21 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") div.row.title - div.level-field(data-fieldname="type", translate="ISSUES.TABLE.COLUMNS.TYPE") - div.level-field(data-fieldname="severity", translate="ISSUES.TABLE.COLUMNS.SEVERITY") - div.level-field(data-fieldname="priority", translate="ISSUES.TABLE.COLUMNS.PRIORITY") - div.votes(data-fieldname="total_voters", translate="ISSUES.TABLE.COLUMNS.VOTES") - div.subject(data-fieldname="subject", translate="ISSUES.TABLE.COLUMNS.SUBJECT") - div.issue-field(data-fieldname="status", translate="ISSUES.TABLE.COLUMNS.STATUS") - div.created-field(data-fieldname="created_date", translate="ISSUES.TABLE.COLUMNS.CREATED") - div.assigned-field(data-fieldname="assigned_to", translate="ISSUES.TABLE.COLUMNS.ASSIGNED_TO") + div.level-field(data-fieldname="type") + | {{"ISSUES.TABLE.COLUMNS.TYPE" | translate}} + div.level-field(data-fieldname="severity") + | {{"ISSUES.TABLE.COLUMNS.SEVERITY" | translate}} + div.level-field(data-fieldname="priority") + | {{"ISSUES.TABLE.COLUMNS.PRIORITY" | translate}} + div.votes(data-fieldname="total_voters") + | {{"ISSUES.TABLE.COLUMNS.VOTES" | translate}} + div.subject(data-fieldname="subject") + | {{"ISSUES.TABLE.COLUMNS.SUBJECT" | translate}} + div.issue-field(data-fieldname="status") + | {{"ISSUES.TABLE.COLUMNS.STATUS" | translate}} + div.created-field(data-fieldname="created_date") + | {{"ISSUES.TABLE.COLUMNS.CREATED" | translate}} + div.assigned-field(data-fieldname="assigned_to") + | {{"ISSUES.TABLE.COLUMNS.ASSIGNED_TO" | translate}} div.row.table-main( ng-repeat="issue in issues track by issue.id" @@ -16,12 +24,24 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") div.level-field(tg-listitem-type="issue") div.level-field(tg-listitem-severity="issue") div.level-field(tg-listitem-priority="issue") - div.votes( - ng-class="{'inactive': !issue.total_voters, 'is-voted': issue.is_voter}" + div.votes.ng-animate-disabled( + ng-class="{'inactive': !issue.total_voters}" + ng-if="!issue.is_voter" title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:issue.total_voters||0}:'messageformat' }}" + ng-click="ctrl.upVoteIssue(issue.id)" + tg-loading="ctrl.voting == issue.id" ) tg-svg(svg-icon="icon-upvote") - span {{ ::issue.total_voters }} + span {{ issue.total_voters }} + div.votes.ng-animate-disabled( + ng-class="{'is-voted': issue.is_voter}" + ng-if="issue.is_voter" + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:issue.total_voters||0}:'messageformat' }}" + ng-click="ctrl.downVoteIssue(issue.id)" + tg-loading="ctrl.voting == issue.id" + ) + tg-svg(svg-icon="icon-upvote") + span {{ issue.total_voters }} div.subject a( href="" @@ -47,7 +67,10 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") svg-icon="icon-arrow-down" ) - div.created-field(tg-bo-bind="issue.created_date|momentFormat:'DD MMM YYYY HH:mm'") + div.created-field( + tg-bo-bind="issue.created_date|momentFormat:'DD MMM YYYY'" + tg-bo-title="issue.created_date|momentFormat:'DD MMM YYYY HH:mm'" + ) div.assigned-field(tg-issue-assigned-to-inline-edition="issue") div.issue-assignedto(title="{{'ISSUES.TABLE.TITLE_ACTION_ASSIGNED_TO' | translate}}") @@ -57,9 +80,9 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") svg-icon="icon-arrow-down" ) -section.empty-issues(ng-if="issues != undefined && issues.length == 0") +section.empty-large(ng-if="issues != undefined && issues.length == 0") img( - src="/#{v}/images/issues-empty.png", + src="/#{v}/images/empty/empty_moon.png", alt="{{ISSUES.TABLE.EMPTY.TITLE | translate }}" ) p.title(translate="ISSUES.TABLE.EMPTY.TITLE") diff --git a/app/partials/includes/modules/kanban-table.jade b/app/partials/includes/modules/kanban-table.jade index 328e52d8..12c396c1 100644 --- a/app/partials/includes/modules/kanban-table.jade +++ b/app/partials/includes/modules/kanban-table.jade @@ -1,8 +1,13 @@ -div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable) +div.kanban-table( + tg-kanban-squish-column, + tg-kanban-sortable, + ng-class="{'zoom-0': ctrl.zoomLevel == 0}" +) div.kanban-table-header div.kanban-table-inner h2.task-colum-name(ng-repeat="s in usStatusList track by s.id", - ng-style="{'border-top-color':s.color}", tg-bo-title="s.name", + ng-style="{'border-top-color':s.color}", + tg-bo-title="s.name", ng-class='{vfold:folds[s.id]}', tg-class-permission="{'readonly': '!modify_task'}") span(tg-bo-bind="s.name") @@ -21,21 +26,6 @@ div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable) ng-class='{hidden:!folds[s.id]}' ) tg-svg(svg-icon="icon-unfold-column") - a.option( - href="" - title="{{'KANBAN.TITLE_ACTION_FOLD_CARDS' | translate}}" - ng-class="{hidden:statusViewModes[s.id] == 'minimized'}" - ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')" - ) - tg-svg.fold-action(svg-icon="icon-fold-row") - a.option( - href="" - title="{{'KANBAN.TITLE_ACTION_UNFOLD_CARDS' | translate}}" - ng-class="{hidden:statusViewModes[s.id] == 'maximized'}" - ng-click="ctrl.updateStatusViewMode(s.id, 'maximized')" - ) - tg-svg.fold-action(svg-icon="icon-unfold-row") - a.option( href="" title="{{'KANBAN.TITLE_ACTION_ADD_US' | translate}}" @@ -54,7 +44,7 @@ div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable) ) tg-svg.bulk-action(svg-icon="icon-bulk") - a.option( + a.option.e2e-archived( href="" ng-attr-title="{{title}}" ng-class="class" @@ -65,18 +55,29 @@ div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable) div.kanban-table-body div.kanban-table-inner div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}', - ng-repeat="s in usStatusList track by s.id", + ng-repeat="s in ::usStatusList track by s.id", tg-kanban-wip-limit="s", tg-kanban-column-height-fixer, tg-bind-scope ) - div.kanban-task( - ng-repeat="us in usByStatus[s.id] track by us.id", - tg-kanban-userstory, - ng-model="us", - tg-bind-scope, - tg-class-permission="{'readonly': '!modify_task'}" - ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id), 'card-placeholder': us.isPlaceholder}" - placeholder="{{us.isPlaceholder}}" + .card-placeholder( + ng-if="ctrl.showPlaceHolder(s.id)" + ng-include="'common/components/kanban-placeholder.html'" ) + + tg-card.card.ng-animate-disabled( + tg-repeat="us in usByStatus.get(s.id.toString()) track by us.getIn(['model', 'id'])", + ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" + tg-class-permission="{'readonly': '!modify_task'}" + tg-bind-scope, + on-toggle-fold="ctrl.toggleFold(id)" + on-click-edit="ctrl.editUs(id)" + on-click-assigned-to="ctrl.changeUsAssignedTo(id)" + project="project" + item="us" + zoom="ctrl.zoom" + zoom-level="ctrl.zoomLevel" + archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))" + ) + div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s") diff --git a/app/partials/includes/modules/lightbox-create-issue.jade b/app/partials/includes/modules/lightbox-create-issue.jade index d1f4f37b..ec245f0a 100644 --- a/app/partials/includes/modules/lightbox-create-issue.jade +++ b/app/partials/includes/modules/lightbox-create-issue.jade @@ -29,18 +29,21 @@ form ) fieldset - .tags-block( - tg-lb-tag-line - ng-model="issue.tags" + tg-tag-line-common.tags-block( + ng-if="project && createIssueOpen" + project="project" + tags="issue.tags" + permissions="add_issue" + on-add-tag="addTag(name, color)" + on-delete-tag="deleteTag(tag)" ) fieldset section - tg-attachments-simple( - attachments="attachments", - on-add="addAttachment(attachment)" - on-delete="deleteAttachment(attachment)" - ) + tg-attachments-simple( + attachments="attachments", + on-add="addAttachment(attachment)" + ) fieldset textarea.description( diff --git a/app/partials/includes/modules/lightbox-task-create-edit.jade b/app/partials/includes/modules/lightbox-task-create-edit.jade index 41fcf957..4e61d4f6 100644 --- a/app/partials/includes/modules/lightbox-task-create-edit.jade +++ b/app/partials/includes/modules/lightbox-task-create-edit.jade @@ -30,9 +30,13 @@ form ) fieldset - div.tags-block( - tg-lb-tag-line - ng-model="task.tags" + tg-tag-line-common.tags-block( + ng-if="project && createEditTaskOpen" + project="project" + tags="task.tags" + permissions="add_task" + on-add-tag="addTag(name, color)" + on-delete-tag="deleteTag(tag)" ) fieldset diff --git a/app/partials/includes/modules/lightbox-us-create-edit.jade b/app/partials/includes/modules/lightbox-us-create-edit.jade index 7345e627..2c9d7ccb 100644 --- a/app/partials/includes/modules/lightbox-us-create-edit.jade +++ b/app/partials/includes/modules/lightbox-us-create-edit.jade @@ -24,9 +24,13 @@ form ) fieldset - div.tags-block( - tg-lb-tag-line - ng-model="us.tags" + tg-tag-line-common.tags-block( + ng-if="project && createEditUsOpen" + project="project" + tags="us.tags" + permissions="add_us" + on-add-tag="addTag(name, color)" + on-delete-tag="deleteTag(tag)" ) fieldset diff --git a/app/partials/includes/modules/search-filter.jade b/app/partials/includes/modules/search-filter.jade index 2cf10b90..9f7834a5 100644 --- a/app/partials/includes/modules/search-filter.jade +++ b/app/partials/includes/modules/search-filter.jade @@ -1,4 +1,13 @@ ul.search-filter + li.epics(data-name="epics") + a( + href="#" + title="{{ 'SEARCH.FILTER_EPICS' | translate }}" + ) + tg-svg(svg-icon="icon-epics") + span.num + span.name(translate="SEARCH.FILTER_EPICS") + li.userstories(data-name="userstories") a.active( href="#" diff --git a/app/partials/includes/modules/search-result-table.jade b/app/partials/includes/modules/search-result-table.jade index f6550aa5..f794f480 100644 --- a/app/partials/includes/modules/search-result-table.jade +++ b/app/partials/includes/modules/search-result-table.jade @@ -18,7 +18,26 @@ script(type="text/ng-template", id="search-issues") div.status(tg-listitem-issue-status="issue") div.assigned-to(tg-listitem-assignedto="issue") - div.empty-search-results(ng-class="{'hidden': issues.length}") + div.empty-large(ng-class="{'hidden': issues.length}") + include ../components/empty-search-results + +script(type="text/ng-template", id="search-epics") + div.search-result-table-container(ng-class="{'hidden': !epics.length}", tg-bind-scope) + div.search-result-table-header + div.row.title + div.ref(translate="COMMON.FIELDS.REF") + div.user-stories(translate="SEARCH.FILTER_EPICS") + div.status(translate="COMMON.FIELDS.STATUS") + div.search-result-table-body + div.row.table-main(ng-repeat="epic in epics track by epic.id") + div.ref(tg-bo-ref="epic.ref") + div.user-stories + div.user-story-name + a(href="", tg-nav="project-epics-detail:project=project.slug,ref=epic.ref", + tg-bo-bind="epic.subject") + div.status(tg-listitem-epic-status="epic") + + div.empty-search-results(ng-class="{'hidden': epics.length}") include ../components/empty-search-results @@ -45,7 +64,7 @@ script(type="text/ng-template", id="search-userstories") div.status(tg-listitem-us-status="us") div.points(tg-bo-bind="us.total_points") - div.empty-search-results(ng-class="{'hidden': userstories.length}") + div.empty-large(ng-class="{'hidden': userstories.length}") include ../components/empty-search-results script(type="text/ng-template", id="search-tasks") @@ -66,7 +85,7 @@ script(type="text/ng-template", id="search-tasks") div.status(tg-listitem-task-status="task") div.assigned-to(tg-listitem-assignedto="task") - div.empty-search-results(ng-class="{'hidden': tasks.length}") + div.empty-large(ng-class="{'hidden': tasks.length}") include ../components/empty-search-results script(type="text/ng-template", id="search-wikipages") @@ -81,5 +100,5 @@ script(type="text/ng-template", id="search-wikipages") a(href="", tg-nav="project-wiki-page:project=project.slug,slug=wikipage.slug", tg-bo-bind="wikipage.slug") - div.empty-search-results(ng-class="{'hidden': wikipages.length}") + div.empty-large(ng-class="{'hidden': wikipages.length}") include ../components/empty-search-results diff --git a/app/partials/includes/modules/sprint.jade b/app/partials/includes/modules/sprint.jade index f56ca58b..f85bc3d6 100644 --- a/app/partials/includes/modules/sprint.jade +++ b/app/partials/includes/modules/sprint.jade @@ -1,6 +1,7 @@ header(tg-backlog-sprint-header, ng-model="sprint") -div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points") +.summary-progress-wrapper + div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points") div.sprint-table(tg-bind-scope, ng-class="{'sprint-empty-wrapper': !sprint.user_stories.length}") div.sprint-empty(ng-if="!sprint.user_stories.length") @@ -18,6 +19,11 @@ div.sprint-table(tg-bind-scope, ng-class="{'sprint-empty-wrapper': !sprint.user_ ng-class="{closed: us.is_closed, blocked: us.is_blocked}") span(tg-bo-ref="us.ref") span(tg-bo-bind="us.subject") + tg-belong-to-epics( + format="pill" + ng-if="us.epics" + epics="us.epics" + ) div.column-points.width-1(tg-bo-bind="us.total_points", ng-class="{closed: us.is_closed, blocked: us.is_blocked}") diff --git a/app/partials/includes/modules/sprints.jade b/app/partials/includes/modules/sprints.jade index 0b52dd40..8c0b856b 100644 --- a/app/partials/includes/modules/sprints.jade +++ b/app/partials/includes/modules/sprints.jade @@ -15,9 +15,9 @@ section.sprints ) tg-svg(svg-icon="icon-add") - div.sprints-empty(ng-if="totalMilestones === 0") + div.empty-small(ng-if="totalMilestones === 0") img( - src="/#{v}/images/sprint-empty.png" + src="/#{v}/images/empty/empty_sprint.png" alt="{{'BACKLOG.SPRINTS.EMPTY' | translate}}" ) p.title(translate="BACKLOG.SPRINTS.EMPTY") diff --git a/app/partials/includes/modules/taskboard-table.jade b/app/partials/includes/modules/taskboard-table.jade index 16eff4f9..1b7bad37 100644 --- a/app/partials/includes/modules/taskboard-table.jade +++ b/app/partials/includes/modules/taskboard-table.jade @@ -1,8 +1,18 @@ -div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable) +div.taskboard-table( + tg-taskboard-squish-column, + tg-taskboard-sortable, + ng-class="{'zoom-0': ctrl.zoomLevel == 0}" +) div.taskboard-table-header div.taskboard-table-inner h2.task-colum-name(translate="TASKBOARD.TABLE.COLUMN") - h2.task-colum-name(ng-repeat="s in taskStatusList track by s.id", ng-style="{'border-top-color':s.color}", ng-class="{'column-fold':statusesFolded[s.id]}", class="squish-status-{{s.id}}", tg-bo-title="s.name") + h2.task-colum-name( + ng-repeat="s in ::taskStatusList track by s.id" + ng-style="{'border-top-color':s.color}" + ng-class="{'column-fold':statusesFolded[s.id]}" + class="squish-status-{{s.id}}" + tg-bo-title="s.name" + ) span(tg-bo-bind="s.name") tg-svg.hfold.fold-action( @@ -44,25 +54,41 @@ div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable) tg-bo-title="'#' + us.ref + ' ' + us.subject") span.us-ref(tg-bo-ref="us.ref") span(ng-bind="us.subject") + tg-belong-to-epics( + format="pill" + ng-if="us.epics" + epics="us.epics" + ) p.points-value span(ng-bind="us.total_points") span(translate="TASKBOARD.TABLE.FIELD_POINTS") include ../components/addnewtask - div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) - div.taskboard-task( - ng-repeat="task in usTasks[us.id][st.id] track by task.id" - tg-bind-scope - tg-class-permission="{'readonly': '!modify_task'}" - ng-class="{'card-placeholder': task.isPlaceholder}" + + div.taskboard-tasks-box.task-column( + ng-repeat="st in ::taskStatusList track by st.id", + class="squish-status-{{st.id}}", + ng-class="{'column-fold':statusesFolded[st.id]}", + tg-bind-scope + ) + .card-placeholder( + ng-if="ctrl.showPlaceHolder(st.id, us.id)" + ng-include="'common/components/taskboard-placeholder.html'" + ) + tg-card.card.ng-animate-disabled( + tg-repeat="task in usTasks.getIn([us.id.toString(), st.id.toString()]) track by task.get('id')" + ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" + tg-class-permission="{'readonly': '!modify_task'}" + tg-bind-scope, + on-toggle-fold="ctrl.toggleFold(id)" + on-click-edit="ctrl.editTask(id)" + on-click-assigned-to="ctrl.changeTaskAssignedTo(id)" + project="project" + item="task" + zoom="ctrl.zoom" + zoom-level="ctrl.zoomLevel" + type="task" ) - div(ng-if="!task.isPlaceholder", tg-taskboard-task) - include ../components/taskboard-task - - div(ng-if="task.isPlaceholder") - - var card = 'task' - include ../../common/components/taskboard-placeholder - div.task-row(ng-init="us = null", ng-class="{'row-fold':usFolded[null]}") div.taskboard-userstory-box.task-column a.vfold( @@ -82,15 +108,29 @@ div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable) h3.us-title span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE") include ../components/addnewtask.jade - div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) - div.taskboard-task( - ng-repeat="task in usTasks[null][st.id] track by task.id" - tg-bind-scope - tg-class-permission="{'readonly': '!modify_task'}" - ng-class="{'card-placeholder': task.isPlaceholder}" - ) - div(ng-if="!task.isPlaceholder", tg-taskboard-task) - include ../components/taskboard-task - div(ng-if="task.isPlaceholder") - include ../../common/components/taskboard-placeholder + div.taskboard-tasks-box.task-column( + ng-repeat="st in ::taskStatusList track by st.id", + class="squish-status-{{st.id}}", + ng-class="{'column-fold':statusesFolded[st.id]}", + tg-bind-scope + ) + .card-placeholder( + ng-if="ctrl.showPlaceHolder(st.id, us.id)" + ng-include="'common/components/taskboard-placeholder.html'" + ) + + tg-card.card.ng-animate-disabled( + tg-bind-scope, + tg-repeat="task in usTasks.getIn(['null', st.id.toString()]) track by task.get('id')" + ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" + tg-class-permission="{'readonly': '!modify_task'}" + on-toggle-fold="ctrl.toggleFold(id)" + on-click-edit="ctrl.editTask(id)" + on-click-assigned-to="ctrl.changeTaskAssignedTo(id)" + project="project" + item="task" + zoom="ctrl.zoom" + zoom-level="ctrl.zoomLevel" + type="task" + ) diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade index aa8dfefa..e85c9a3d 100644 --- a/app/partials/issue/issues-detail.jade +++ b/app/partials/issue/issues-detail.jade @@ -17,57 +17,20 @@ div.wrapper( on-upvote="ctrl.onUpvote" on-downvote="ctrl.onDownvote" ) - .us-title(ng-class="{blocked: issue.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="issue.ref") - span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") - - p.us-related-task(ng-if="issue.generated_user_stories.length") {{ 'ISSUES.PROMOTED'|translate }} - a( - href="" - ng-repeat="us in issue.generated_user_stories" - tg-check-permission="view_us" - tg-bo-title="'#' + us.ref + ' ' + us.subject" - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" - ) - span(tg-bo-ref="us.ref") - - p.external-reference(ng-if="issue.external_reference") - | {{ 'ISSUES.EXTERNAL_REFERENCE'|translate }} - a( - target="_blank" - tg-bo-href="issue.external_reference[1]" - title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}" - ) - span {{ issue.external_reference[1] }} - - p.block-desc-container(ng-show="issue.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="issue.blocked_note || ('ISSUES.BLOCKED' | translate)") - - .issue-nav - a( - ng-show="previousUrl" - tg-bo-href="previousUrl" - title="{{'ISSUES.TITLE_PREVIOUS_ISSUE' | translate}}" - ) - tg-svg( - svg-icon="icon-arrow-left" - ) - a( - ng-show="nextUrl" - tg-bo-href="nextUrl" - title="{{'ISSUES.TITLE_NEXT_ISSUE' | translate}}" - - ) - tg-svg( - svg-icon="icon-arrow-right" - ) - .subheader - .tags-block( - tg-tag-line - ng-model="issue" + tg-detail-header.detail-header-container( + item="issue" + project="project" required-perm="modify_issue" + ng-class="{blocked: issue.is_blocked}" + ng-if="project && issue" + format="text" + ) + .subheader + tg-tag-line.tags-block( + ng-if="issue && project" + project="project" + item="issue" + permissions="modify_issue" ) tg-created-by-display.ticket-created-by(ng-model="issue") @@ -93,9 +56,11 @@ div.wrapper( edit-permission = "modify_issue" ) - tg-history( - ng-model="issue" + tg-history-section( + ng-if="issue" type="issue" + name="issue" + id="issue.id" ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/partials/issue/issues-filters-selected.jade b/app/partials/issue/issues-filters-selected.jade deleted file mode 100644 index 29e0a12e..00000000 --- a/app/partials/issue/issues-filters-selected.jade +++ /dev/null @@ -1,9 +0,0 @@ -<% _.each(filters, function(f) { %> -.single-filter.selected( - data-type!="<%- f.type %>" - data-id!="<%- f.id %>" -) - span.name(style!="<%- f.style %>") <%- f.name %> - a.remove-filter(href="") - tg-svg(svg-icon="icon-close") -<% }) %> diff --git a/app/partials/issue/issues-filters.jade b/app/partials/issue/issues-filters.jade deleted file mode 100644 index 95763e72..00000000 --- a/app/partials/issue/issues-filters.jade +++ /dev/null @@ -1,22 +0,0 @@ -<% _.each(filters, function(f) { %> -<% if (!f.selected) { %> -.single-filter( - data-type!="<%- f.type %>" - data-id!="<%- f.id %>" -) - span.name(style!="<%- f.style %>") <%- f.name %> - <% if (f.count){ %> - span.number <%- f.count %> - <% } %> - <% if (f.type == "myFilters"){ %> - a.remove-filter(href="") - tg-svg(svg-icon="icon-trash") - <% } %> -<% } %> -<% }) %> -span(class="new") -input( - class="hidden my-filter-name" - type="text" - placeholder="{{'ISSUES.PLACEHOLDER_FILTER_NAME' | translate}}" -) diff --git a/app/partials/issue/issues.jade b/app/partials/issue/issues.jade index 70f2f701..97df1b10 100644 --- a/app/partials/issue/issues.jade +++ b/app/partials/issue/issues.jade @@ -1,9 +1,25 @@ doctype html -div.wrapper.issues.lightbox-generic-form(tg-issues, ng-controller="IssuesController as ctrl", ng-init="section='issues'") +div.wrapper.issues.lightbox-generic-form( + tg-issues + ng-controller="IssuesController as ctrl" + ng-init="section='issues'" +) tg-project-menu - sidebar.menu-secondary.extrabar.filters-bar(tg-issues-filters) - include ../includes/modules/issues-filters + sidebar.filters-bar + tg-filter( + q="ctrl.filterQ" + filters="ctrl.filters" + custom-filters="ctrl.customFilters" + selected-filters="ctrl.selectedFilters" + customFilters="ctl.customFilters" + on-save-custom-filter="ctrl.saveCustomFilter(name)" + on-add-filter="ctrl.addFilter(filter)" + on-select-custom-filter="ctrl.selectCustomFilter(filter)" + on-remove-custom-filter="ctrl.removeCustomFilter(filter)" + on-remove-filter="ctrl.removeFilter(filter)" + on-change-q="ctrl.changeQ(q)" + ) section.main.issues-page header diff --git a/app/partials/issue/promote-issue-to-us-button.jade b/app/partials/issue/promote-issue-to-us-button.jade index fe2bd499..bd523f5f 100644 --- a/app/partials/issue/promote-issue-to-us-button.jade +++ b/app/partials/issue/promote-issue-to-us-button.jade @@ -1,4 +1,4 @@ -a.promote-button.is-editable( +a.promote-button.button-gray.is-editable( href="" tg-check-permission="add_us" title="{{ 'ISSUES.ACTION_PROMOTE_TO_US' | translate }}" diff --git a/app/partials/kanban/kanban-task.jade b/app/partials/kanban/kanban-task.jade deleted file mode 100644 index 46fed783..00000000 --- a/app/partials/kanban/kanban-task.jade +++ /dev/null @@ -1,33 +0,0 @@ -div.kanban-tagline( - tg-colorize-tags="us.tags" - tg-colorize-tags-type="kanban" - ng-hide="us.isArchived" -) -div.kanban-task-inner(ng-class="{'task-archived': us.isArchived}") - div.avatar-wrapper(tg-kanban-user-avatar="us.assigned_to", ng-model="us", ng-hide="us.isArchived") - div.task-text(ng-hide="us.isArchived") - a.task-assigned(href="", title="{{'US.ASSIGN' | translate}}") - span.task-num(tg-bo-ref="us.ref") - a.task-name(href="", title="#{{ ::us.ref }} {{ us.subject }}", ng-bind="us.subject", - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", - tg-nav-get-params="{\"kanban-status\": {{us.status}}}") - - p.task-points(href="", title="{{'US.TOTAL_US_POINTS' | translate}}") - span(ng-if="us.total_points !== null", ng-bind="us.total_points") - span.points-text(ng-if="us.total_points !== null", translate="COMMON.FIELDS.POINTS") - span(ng-if="us.total_points === null", translate="US.NOT_ESTIMATED") - - div.task-archived-text(ng-show="us.isArchived") - p(translate="KANBAN.ARCHIVED") - p - span.task-num(tg-bo-ref="us.ref") - span.task-name(ng-bind="us.subject") - p(translate="KANBAN.UNDO_ARCHIVED") - - a.edit-us( - href="", - title="{{'COMMON.EDIT' | translate}}", - tg-check-permission="modify_us", - ng-hide="us.isArchived" - ) - tg-svg(svg-icon="icon-edit") diff --git a/app/partials/kanban/kanban.jade b/app/partials/kanban/kanban.jade index 99c05aa1..2e65ebe2 100644 --- a/app/partials/kanban/kanban.jade +++ b/app/partials/kanban/kanban.jade @@ -5,7 +5,36 @@ div.wrapper(tg-kanban, ng-controller="KanbanController as ctrl" tg-project-menu section.main.kanban - include ../includes/components/mainTitle + tg-filter( + ng-show="ctrl.openFilter" + q="ctrl.filterQ" + filters="ctrl.filters" + custom-filters="ctrl.customFilters" + selected-filters="ctrl.selectedFilters" + customFilters="ctl.customFilters" + on-save-custom-filter="ctrl.saveCustomFilter(name)" + on-add-filter="ctrl.addFilter(filter)" + on-select-custom-filter="ctrl.selectCustomFilter(filter)" + on-remove-custom-filter="ctrl.removeCustomFilter(filter)" + on-remove-filter="ctrl.removeFilter(filter)" + on-change-q="ctrl.changeQ(q)" + ) + + .kanban-header + include ../includes/components/mainTitle + .taskboard-actions + tg-kanban-board-zoom( + ng-if="usByStatus.size", + on-zoom-change="ctrl.setZoom(zoomLevel, zoom)" + ) + + button.button-filter.e2e-open-filter( + ng-click="ctrl.openFilter = !ctrl.openFilter" + title="{{ctrl.selectedFilters.length}} {{'COMMON.FILTERS.APPLIED_FILTERS_NUM' | translate}}" + ) + span.filter-num(ng-if="ctrl.selectedFilters.length") {{ctrl.selectedFilters.length}} + tg-svg(svg-icon="icon-filters") + include ../includes/modules/kanban-table div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory) diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade index a0d00f8d..2a331a5b 100644 --- a/app/partials/task/task-detail.jade +++ b/app/partials/task/task-detail.jade @@ -26,56 +26,21 @@ div.wrapper( on-upvote="ctrl.onUpvote", on-downvote="ctrl.onDownvote" ) - div.us-title(ng-class="{blocked: task.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="task.ref") - span.us-name( - tg-editable-subject - ng-model="task" - required-perm="modify_task" - ) - - h3.us-related-task(ng-if="us") - | {{ 'TASK.OWNER_US'|translate }} - a( - href="" - tg-check-permission="view_us" - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" - title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}" - ) - span(tg-bo-ref="us.ref") - span(tg-bo-bind="us.subject") - - p.external-reference(ng-if="task.external_reference") - a( - tg-bo-href="task.external_reference[1]", - target="_blank" - title="{{'TASK.TITLE_LINK_GO_ORIGIN' | translate}}" - ) - | {{ "TASK.ORIGIN_US"| translate }} - span {{ task.external_reference[1] }} - - p.block-desc-container(ng-show="task.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description( - ng-bind="task.blocked_note || ('TASK.BLOCKED_DESCRIPTION' | translate)" - ) - - div.issue-nav - a( - ng-show="previousUrl" - tg-bo-href="previousUrl" - title="{{'TASK.PREVIOUS' | translate}}" - ) - tg-svg(svg-icon="icon-arrow-left") - a( - ng-show="nextUrl" - tg-bo-href="nextUrl" - title="{{'TASK.NEXT' | translate}}" - ) - tg-svg(svg-icon="icon-arrow-right") + tg-detail-header.detail-header-container( + item="task" + project="project" + required-perm="modify_task" + ng-class="{blocked: task.is_blocked}" + ng-if="project && task" + type="text" + ) .subheader - div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task") + tg-tag-line.tags-block( + ng-if="task && project" + project="project" + item="task" + permissions="modify_task" + ) tg-created-by-display.ticket-created-by(ng-model="task") section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="task", required-perm="modify_task") @@ -95,7 +60,12 @@ div.wrapper( edit-permission = "modify_task" ) - tg-history(ng-model="task", type="task") + tg-history-section( + ng-if="task" + type="task" + name="task" + id="task.id" + ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/partials/taskboard/taskboard-user.jade b/app/partials/taskboard/taskboard-user.jade index ca05113f..70f25205 100644 --- a/app/partials/taskboard/taskboard-user.jade +++ b/app/partials/taskboard/taskboard-user.jade @@ -1,6 +1,15 @@ figure.avatar.avatar-assigned-to a(href='#', title="{{'TASKBOARD.TITLE_ACTION_ASSIGN' | translate}}", ng-class="{'not-clickable': !clickable}") - img(ng-src='{{imgurl}}') + img( + ng-style="{'background-color': avatar.bg}" + ng-src='{{avatar.url}}' + ) figure.avatar.avatar-task-link - a(tg-nav='project-tasks-detail:project=project.slug,ref=task.ref', ng-attr-title='{{task.subject}}') - img(ng-src='{{imgurl}}') + a( + tg-nav='project-tasks-detail:project=project.slug,ref=task.ref' + ng-attr-title='{{task.subject}}' + ) + img( + ng-style="{'background-color': avatar.bg}" + ng-src='{{avatar.url}}' + ) diff --git a/app/partials/taskboard/taskboard.jade b/app/partials/taskboard/taskboard.jade index 8a12aaf1..d9e85720 100644 --- a/app/partials/taskboard/taskboard.jade +++ b/app/partials/taskboard/taskboard.jade @@ -4,11 +4,38 @@ div.wrapper(tg-taskboard, ng-controller="TaskboardController as ctrl", ng-init="section='backlog'") tg-project-menu section.main.taskboard - .taskboard-inner + tg-filter( + ng-show="ctrl.openFilter" + q="ctrl.filterQ" + filters="ctrl.filters" + custom-filters="ctrl.customFilters" + selected-filters="ctrl.selectedFilters" + customFilters="ctl.customFilters" + on-save-custom-filter="ctrl.saveCustomFilter(name)" + on-add-filter="ctrl.addFilter(filter)" + on-select-custom-filter="ctrl.selectCustomFilter(filter)" + on-remove-custom-filter="ctrl.removeCustomFilter(filter)" + on-remove-filter="ctrl.removeFilter(filter)" + on-change-q="ctrl.changeQ(q)" + ) + .taskboard-header h1 span(tg-bo-bind="project.name", class="project-name-short") span.green(tg-bo-bind="sprint.name") span.date(tg-date-range="sprint.estimated_start,sprint.estimated_finish") + .taskboard-actions + tg-taskboard-zoom( + ng-if="usTasks.size", + on-zoom-change="ctrl.setZoom(zoomLevel, zoom)" + ) + button.button-filter.e2e-open-filter( + ng-click="ctrl.openFilter = !ctrl.openFilter" + ) + span.filter-num(ng-if="ctrl.selectedFilters.length") {{ctrl.selectedFilters.length}} + tg-svg(svg-icon="icon-filters") + + .taskboard-inner + include ../includes/components/sprint-summary div.graphics-container diff --git a/app/partials/team/team-member-current-user.jade b/app/partials/team/team-member-current-user.jade index caefc7e5..8f800638 100644 --- a/app/partials/team/team-member-current-user.jade +++ b/app/partials/team/team-member-current-user.jade @@ -1,7 +1,7 @@ .row .username .avatar - img(tg-bo-src="currentUser.photo", tg-bo-alt="currentUser.full_name_display") + img(tg-avatar="currentUser", tg-bo-alt="currentUser.full_name_display") .avatar-data .name @@ -23,7 +23,7 @@ .member-stats( tg-team-member-stats stats="stats" - user="currentUser.user" + user="currentUser.id" issuesEnabled="issuesEnabled" tasksenabled="tasksEnabled" wikienabled="wikiEnabled" diff --git a/app/partials/team/team-members.jade b/app/partials/team/team-members.jade index 2155d134..247b42be 100644 --- a/app/partials/team/team-members.jade +++ b/app/partials/team/team-members.jade @@ -1,7 +1,7 @@ .row.member(ng-repeat="user in memberships | membersFilter:filtersQ:filtersRole") .username .avatar - img(tg-bo-src="user.photo", tg-bo-alt="user.full_name_display") + img(tg-avatar="user", tg-bo-alt="user.full_name_display") .avatar-data a.name( @@ -19,7 +19,7 @@ .member-stats( tg-team-member-stats stats="stats" - user="user.user" + user="user.id" issuesEnabled="issuesEnabled" tasksenabled="tasksEnabled" wikienabled="wikiEnabled" diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade index 0896a232..6c04fb9f 100644 --- a/app/partials/us/us-detail.jade +++ b/app/partials/us/us-detail.jade @@ -26,49 +26,21 @@ div.wrapper( on-upvote="ctrl.onUpvote" on-downvote="ctrl.onDownvote" ) - div.us-title(ng-class="{blocked: us.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="us.ref") - span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") - - p.us-related-task(ng-if="us.origin_issue") - | {{ 'US.PROMOTED'|translate }} - a( - href="" - tg-check-permission="view_us" - tg-nav="project-issues-detail:project=project.slug,ref=us.origin_issue.ref" - tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject" - title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}" - ) - span(tg-bo-ref="us.origin_issue.ref") - - p.external-reference(ng-if="us.external_reference") - | {{ 'US.EXTERNAL_REFERENCE'|translate }} - a( - tg-bo-href="us.external_reference[1]", - title="{{'US.GO_TO_EXTERNAL_REFERENCE' | translate}}" - target="_blank" - ) - span {{ us.external_reference[1] }} - - p.block-desc-container(ng-show="us.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="us.blocked_note || ('US.BLOCKED' | translate)") - div.issue-nav - a( - ng-show="previousUrl" - tg-bo-href="previousUrl" - title="{{'US.PREVIOUS' | translate}}" - ) - tg-svg(svg-icon="icon-arrow-left") - a( - ng-show="nextUrl" - tg-bo-href="nextUrl" - title="{{'US.NEXT' | translate}}" - ) - tg-svg(svg-icon="icon-arrow-right") + tg-detail-header.detail-header-container( + item="us" + project="project" + required-perm="modify_us" + ng-class="{blocked: us.is_blocked}" + ng-if="project && us" + type="text" + ) .subheader - .tags-block(tg-tag-line, ng-model="us", required-perm="modify_us") + tg-tag-line.tags-block( + ng-if="us && project" + project="project" + item="us" + permissions="modify_us" + ) tg-created-by-display.ticket-created-by(ng-model="us") section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="us", required-perm="modify_us") @@ -90,9 +62,12 @@ div.wrapper( edit-permission = "modify_us" ) - tg-history( - ng-model="us" + tg-history-section( + ng-if="us" type="us" + name="us" + id="us.id" + project-id="projectId" ) sidebar.menu-secondary.sidebar.ticket-data diff --git a/app/partials/user/user-change-password.jade b/app/partials/user/user-change-password.jade index 60d7eaf1..11e6b70c 100644 --- a/app/partials/user/user-change-password.jade +++ b/app/partials/user/user-change-password.jade @@ -17,7 +17,6 @@ div.wrapper( fieldset label(for="current-password", translate="CHANGE_PASSWORD.FIELD_CURRENT_PASSWORD") input( - data-required="true" type="password" name="password" id="current-password" diff --git a/app/partials/user/user-profile.jade b/app/partials/user/user-profile.jade index 2eb62e65..fc3518bd 100644 --- a/app/partials/user/user-profile.jade +++ b/app/partials/user/user-profile.jade @@ -16,7 +16,7 @@ div.wrapper( form .project-details-image(tg-user-avatar) fieldset.image-container - img.image(ng-src="{{user.big_photo}}" alt="avatar") + img.image(tg-avatar="user" alt="avatar") .loading-overlay img.loading-spinner( src="/#{v}/svg/spinner-circle.svg", diff --git a/app/partials/wiki/wiki-list.jade b/app/partials/wiki/wiki-list.jade new file mode 100644 index 00000000..97b4025c --- /dev/null +++ b/app/partials/wiki/wiki-list.jade @@ -0,0 +1,57 @@ +doctype html + +div.wrapper( + ng-controller="WikiPagesListController as ctrl" + ng-init="section='wiki'" +) + tg-project-menu + sidebar.menu-secondary.extrabar.wiki-nav( + ng-if="linksVisible" + tg-wiki-nav + ng-model="wikiLinks" + ) + section.main + header + h1 + span(tg-bo-bind="project.name") + span.green(translate="PROJECT.SECTION.WIKI") + span.date(translate="WIKI.SECTION_PAGES_LIST") + + section.wiki-pages-table.basic-table + .row.title + .title-field( + translate="WIKI.PAGES_LIST_COLUMNS.TITLE" + ) + .editions-field( + translate="WIKI.PAGES_LIST_COLUMNS.EDITIONS" + ) + .creator-field( + translate="WIKI.PAGES_LIST_COLUMNS.CREATOR" + ) + .created-field( + translate="WIKI.PAGES_LIST_COLUMNS.CREATED" + ) + .last-modifier-field( + translate="WIKI.PAGES_LIST_COLUMNS.LAST_MODIFIER" + ) + .modified-field( + translate="WIKI.PAGES_LIST_COLUMNS.MODIFIED" + ) + + .row.table-main(ng-repeat="wikipage in wikipages track by wikipage.slug") + .title-field + a( + href="" + tg-nav="project-wiki-page:project=project.slug,slug=wikipage.slug" + ) {{wikipage.slug}} + .editions-field {{wikipage.editions}} + .creator-field( + tg-user-display + tg-user-id="{{wikipage.owner}}" + ) + .created-field(tg-bo-bind="wikipage.created_date|momentFormat:'DD MMM YYYY HH:mm'") + .last-modifier-field( + tg-user-display + tg-user-id="{{wikipage.last_modifier}}" + ) + .modified-field(tg-bo-bind="wikipage.modified_date|momentFormat:'DD MMM YYYY HH:mm'") diff --git a/app/partials/wiki/wiki-nav.jade b/app/partials/wiki/wiki-nav.jade index f1686f03..30564de7 100644 --- a/app/partials/wiki/wiki-nav.jade +++ b/app/partials/wiki/wiki-nav.jade @@ -1,11 +1,25 @@ header - h1(translate="WIKI.NAVIGATION.SECTION_NAME") + h1.title(translate="WIKI.NAVIGATION.SECTION_NAME") -nav - ul - <% _.each(wikiLinks, function(link, index) { %> - li.wiki-link(data-id!="<%- index %>") - a.link-title(title!="<%- link.title %>", href!="<%- link.url %>")<%- link.title %> +ul.wiki-link-container + li.wiki-link.fixed-link + a.link-title( + href="" + tg-nav="project-wiki:project=project.slug" + translate="WIKI.NAVIGATION.HOME" + ) + +ul.sortable.wiki-link-container + li.wiki-link.e2e-wiki-page-link( + ng-repeat="link in wikiLinks" + data-id!="{{ $index }}" + tg-bind-scope + tg-class-permission="{'is-sortable': 'add_wiki_link'}" + ) + <% if (addWikiLinkPermission) { %> + tg-svg.dragger(svg-icon="icon-drag") + <% } %> + a.link-title(title!="{{ link.title }}", href!="{{ link.url }}") {{ link.title }} <% if (deleteWikiLinkPermission) { %> a.js-delete-link.remove-wiki-page(title!="{{'WIKI.DELETE_LINK_TITLE' | translate}}") @@ -15,10 +29,10 @@ nav input.hidden( type="text" placeholder="{{'COMMON.FIELDS.NAME' | translate}}" - value!="<%- link.title %>" + value!="{{ link.title }}" ) - <% }) %> +ul.sortable.wiki-link-container li.new.hidden input( type="text" @@ -26,10 +40,18 @@ nav ) <% if (addWikiLinkPermission) { %> -a( +a.add-button( href="" title="{{'WIKI.NAVIGATION.ACTION_ADD_LINK' | translate}}" - class="add-button button-gray" ) + tg-svg(svg-icon="icon-add") span(translate="WIKI.NAVIGATION.ACTION_ADD_LINK") <% } %> + +ul.wiki-link-container.wiki-all-links(ng-if="wikiLinks.length") + li.wiki-link.fixed-link + a.link-title( + href="" + tg-nav="project-wiki-list:project=project.slug" + translate="WIKI.NAVIGATION.ALL_PAGES" + ) diff --git a/app/partials/wiki/wiki-summary.jade b/app/partials/wiki/wiki-summary.jade index f2855cba..8acc644e 100644 --- a/app/partials/wiki/wiki-summary.jade +++ b/app/partials/wiki/wiki-summary.jade @@ -1,14 +1,27 @@ -div.wiki-times-edited - span.number <%- totalEditions %> - span.description(translate="WIKI.SUMMARY.TIMES_EDITED") +.wiki-username-edition + .avatar + img( + style!="background-color: <%- user.avatar.bg %>" + src!="<%- user.avatar.url %>" + alt!="<%- user.name %>" + ) + .wiki-user-modification + span.description(translate="WIKI.SUMMARY.LAST_MODIFICATION") + span.username <%- user.name %> -div.wiki-last-modified +.wiki-last-modified span.number <%- lastModifiedDate %> span.description(translate="WIKI.SUMMARY.LAST_EDIT") -div.wiki-username-edition - figure.avatar - img(src!="<%- user.imgUrl %>" alt!="<%- user.name %>") - div.wiki-user-modification - span.description(translate="WIKI.SUMMARY.LAST_MODIFICATION") - span.username <%- user.name %> +.wiki-times-edited + span.number <%- totalEditions %> + span.description(translate="WIKI.SUMMARY.TIMES_EDITED") + +tg-svg.remove( + tg-check-permission="delete_wiki_page" + title="{{'WIKI.REMOVE' | translate}}" + ng-click="ctrl.delete()" + svg-icon="icon-trash" + ng-if="wiki.id" + +) diff --git a/app/partials/wiki/wiki.jade b/app/partials/wiki/wiki.jade index a7e59bd5..cde221a6 100644 --- a/app/partials/wiki/wiki.jade +++ b/app/partials/wiki/wiki.jade @@ -1,18 +1,21 @@ doctype html -div.wrapper(ng-controller="WikiDetailController as ctrl", - ng-init="section='wiki'") +div.wrapper( + ng-controller="WikiDetailController as ctrl" + ng-init="section='wiki'" +) tg-project-menu - sidebar.menu-secondary.extrabar(ng-if="linksVisible") - section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks") + sidebar.menu-secondary.extrabar.wiki-nav( + ng-if="linksVisible" + tg-wiki-nav + ng-model="wikiLinks" + ) section.main.wiki header h1 span(tg-bo-bind="project.name") span.green(translate="PROJECT.SECTION.WIKI") - - div.summary.wiki-summary(tg-wiki-summary, ng-model="wiki", ng-if="wiki.id") h2.wiki-title(ng-bind='wikiTitle') section.wiki-content( tg-editable-wysiwyg, @@ -20,6 +23,12 @@ div.wrapper(ng-controller="WikiDetailController as ctrl", ng-model="wiki" ) + .summary.wiki-summary( + tg-wiki-summary + ng-model="wiki" + ng-if="wiki.id" + ) + tg-attachments-full( ng-if="wiki.id" obj-id="wiki.id" @@ -28,12 +37,7 @@ div.wrapper(ng-controller="WikiDetailController as ctrl", edit-permission = "modify_wiki_page" ) - a.remove( - href="" - ng-click="ctrl.delete()" + tg-wiki-history( ng-if="wiki.id" - title="{{'WIKI.REMOVE' | translate}}" - tg-check-permission="delete_wiki_page" + wiki-id="wiki.id" ) - tg-svg(svg-icon="icon-trash") - span(translate="WIKI.REMOVE") diff --git a/app/styles/components/basic-table.scss b/app/styles/components/basic-table.scss index 3e710ed1..09df2464 100644 --- a/app/styles/components/basic-table.scss +++ b/app/styles/components/basic-table.scss @@ -11,6 +11,9 @@ padding: .3rem 0; text-align: left; width: 100%; + @include breakpoint(tablet) { + flex-direction: column; + } @for $i from 1 through 8 { .width-#{$i} { flex-basis: 50px; diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index f6f95f46..96dbb3b5 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -33,13 +33,13 @@ } &.disabled, &[disabled] { - background: $whitish; + background: $mass-white; box-shadow: none; color: $gray-light; cursor: not-allowed; opacity: .65; &:hover { - background: $whitish; + background: $mass-white; color: $gray-light; } } @@ -155,3 +155,26 @@ a.button-gray { display: inline-block; margin-top: .5rem; } + +.button-filter { + @extend %button; + background: $whitish; + margin-left: 1rem; + padding: .4rem .5rem; + position: relative; + &:hover { + background: $gray-light; + fill: $whitish; + } + .filter-num { + @include font-size(small); + @include font-type(medium); + background: $red; + border-radius: 50%; + height: 1rem; + left: -.5rem; + position: absolute; + top: -.5rem; + width: 1rem; + } +} diff --git a/app/styles/components/editor-help.scss b/app/styles/components/editor-help.scss index 5336a078..c03689e8 100644 --- a/app/styles/components/editor-help.scss +++ b/app/styles/components/editor-help.scss @@ -1,9 +1,9 @@ .wysiwyg-help { - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; margin-top: -.5rem; - padding: .25rem .5rem; + padding: .45rem .5rem; a { display: inline-block; } @@ -16,6 +16,7 @@ .help-markdown, .help-button { + @include font-size(xsmall); &:hover { span { transition: color .2s linear; @@ -29,6 +30,7 @@ vertical-align: text-top; } .icon { + @include svg-size(.9rem); fill: $gray-light; margin-right: .2rem; } diff --git a/app/styles/components/empty.scss b/app/styles/components/empty.scss new file mode 100644 index 00000000..23fe77c7 --- /dev/null +++ b/app/styles/components/empty.scss @@ -0,0 +1,34 @@ +%empty { + margin-top: 4rem; + text-align: center; + img { + margin-bottom: 1rem; + width: 100%; + } + .title { + @include font-size(large); + text-transform: uppercase; + } + p { + @include font-type(light); + margin: 0; + } + a { + @include font-type(light); + color: $primary; + } +} + +.empty-small { + @extend %empty; + img { + max-width: 175px; + } +} + +.empty-large { + @extend %empty; + img { + max-width: 800px; + } +} diff --git a/app/styles/components/filter.scss b/app/styles/components/filter.scss deleted file mode 100644 index c681c70d..00000000 --- a/app/styles/components/filter.scss +++ /dev/null @@ -1,50 +0,0 @@ -.single-filter { - @include font-type(text); - @include clearfix; - align-items: center; - background: darken($whitish, 10%); // Fallback - display: flex; - justify-content: space-between; - margin-bottom: .5rem; - opacity: .5; - padding-right: .5rem; - position: relative; - &:hover { - color: $grayer; - opacity: 1; - transition: opacity .2s linear; - } - &.selected, - &.active { - color: $grayer; - opacity: 1; - transition: opacity .2s linear; - } - .name, - .number { - padding: 8px 10px; - } - .name { - @include ellipsis(100%); - display: block; - width: 100%; - } - .number { - background: darken($whitish, 20%); // Fallback - position: absolute; - right: 0; - top: 0; - } - .remove-filter { - display: block; - svg { - fill: $gray; - transition: fill .2s linear; - } - &:hover { - svg { - fill: $red; - } - } - } -} diff --git a/app/styles/components/history.scss b/app/styles/components/history.scss new file mode 100644 index 00000000..d949df35 --- /dev/null +++ b/app/styles/components/history.scss @@ -0,0 +1,32 @@ +.history-tabs { + background: $whitish; + display: flex; + flex-direction: row; + a { + display: inline-block; + padding: .75rem 1rem; + &:hover { + color: $primary; + } + } + .history-tab { + @include font-type(bold); + border-bottom: 3px solid transparent; + color: $gray-light; + transition: all .1s linear; + &.active { + border-bottom: 3px solid $grayer; + color: $grayer; + } + } + .order-comments { + @include font-type(light); + color: $grayer; + margin-left: auto; + transition: none; + } + .icon-arrow-up, + .icon-arrow-down { + @include svg-size(.75rem); + } +} diff --git a/app/styles/components/kanban-task.scss b/app/styles/components/kanban-task.scss deleted file mode 100644 index 121bcfb2..00000000 --- a/app/styles/components/kanban-task.scss +++ /dev/null @@ -1,220 +0,0 @@ -.kanban-task { - background: $card; - border: 1px solid $card-hover; - box-shadow: none; - cursor: move; - margin: .2rem; - position: relative; - &:last-child { - margin-bottom: 0; - } - &:hover { - .edit-us { - display: block; - fill: $card-dark; - opacity: 1; - transition: color .3s linear, opacity .3s linear; - } - } - &.gu-mirror { - box-shadow: 1px 1px 15px rgba($black, .4); - opacity: 1; - transition: box-shadow .3s linear; - } - &.blocked { - background: $red; - border: 1px solid darken($red, 10%); - color: $white; - a, - span { - color: $white; - } - } - &.card-placeholder { - background: darken($whitish, 2%); - border: 3px dashed darken($whitish, 8%); - cursor: default; - } - .kanban-tagline { - border-color: $card-hover; - display: flex; - height: .6rem; - } - .kanban-tag { - border-top: .3rem solid $card-hover; - flex-basis: 0; - flex-grow: 1; - height: .6rem; - z-index: 90; - } - .kanban-task-inner { - display: flex; - padding: .5rem; - } - .avatar-wrapper { - flex-basis: 55px; - flex-grow: 0; - flex-shrink: 0; - width: 55px; - img { - width: 100%; - } - } - .avatar { - a { - @include font-size(small); - text-align: center; - } - img { - margin: 0 auto; - &:hover { - border: 2px solid $primary; - transition: border .3s linear; - } - } - } - .task-text { - @include font-size(small); - flex-grow: 1; - padding: 0 .5rem 0 .8rem; - } - .task-assigned { - color: $card-dark; - display: block; - } - .task-num { - color: $grayer; - margin-right: .3rem; - } - .task-name { - @include font-type(bold); - } - .loading { - bottom: .5rem; - position: absolute; - } - .edit-us { - display: block; - opacity: 0; - position: absolute; - svg { - @include svg-size(1.1rem); - fill: $card-hover; - } - &:hover { - cursor: pointer; - svg { - fill: darken($card-hover, 15%); - transition: color .3s linear; - } - } - } -} - - -.kanban-task-maximized { - .task-archived { - background: darken($whitish, 5%); - padding: .5rem; - text-align: left; - transition: background .3s linear; - &:hover { - background: darken($whitish, 8%); - transition: background .3s linear; - } - .task-archived-text { - flex: 1; - } - span { - color: $gray-light; - } - p { - @include font-size(small); - color: $gray-light; - margin: 0; - &:last-child { - color: $gray; - margin: .5rem 0; - text-align: center; - } - } - } - .task-name { - word-wrap: break-word; - } - .loading, - .edit-us { - bottom: .2rem; - right: .5rem; - } - .task-points { - @include font-size(small); - color: darken($card-hover, 15%); - margin: 0; - span { - display: inline-block; - &:first-child { - padding-right: .2rem; - } - } - .points-text { - text-transform: lowercase; - } - } - .kanban-tag { - border-top: .3rem solid; - } -} - -.kanban-task-minimized { - .kanban-task-inner { - padding: 0 .3rem; - } - .task-archived { - @include font-size(small); - background: darken($whitish, 5%); - padding: .3rem; - text-align: left; - .task-archived-text { - flex: 1; - } - span { - color: $gray-light; - } - .task-name { - display: inline-block; - max-width: 70%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - p { - color: $gray-light; - margin: 0; - &:last-child { - display: none; - } - } - } - .task-num { - vertical-align: top; - } - .task-name { - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 135px; - } - .task-points { - display: none; - } - .icon-edit { - bottom: .2rem; - right: 1rem; - top: 1.4rem; - } - .kanban-tag { - border-top: .2rem solid; - } -} diff --git a/app/styles/components/list-items.scss b/app/styles/components/list-items.scss index 06cdef94..fb72b3a8 100644 --- a/app/styles/components/list-items.scss +++ b/app/styles/components/list-items.scss @@ -110,11 +110,10 @@ } .ticket-id { color: $gray-light; - margin-right: .3rem; } .ticket-blocked { color: $red; - margin-left: .3rem; + margin-right: .25rem; } } diff --git a/app/styles/components/markitup.scss b/app/styles/components/markitup.scss index 93beffec..59dc99a2 100644 --- a/app/styles/components/markitup.scss +++ b/app/styles/components/markitup.scss @@ -1,6 +1,6 @@ .markItUpHeader { ul { - background: $whitish; + background: $mass-white; padding: .3rem; li { display: inline-block; @@ -30,7 +30,7 @@ .preview { .actions { - background: $whitish; + background: $mass-white; margin-top: .5rem; min-height: 2rem; padding: .3rem; diff --git a/app/styles/components/select-color.scss b/app/styles/components/select-color.scss index dc329393..92941c0c 100644 --- a/app/styles/components/select-color.scss +++ b/app/styles/components/select-color.scss @@ -19,6 +19,9 @@ height: 35px; width: 35px; } + .empty-color { + @include empty-color(33); + } ul { float: left; margin-bottom: 1rem; diff --git a/app/styles/components/summary.scss b/app/styles/components/summary.scss index 2b9a8cda..52e419fd 100644 --- a/app/styles/components/summary.scss +++ b/app/styles/components/summary.scss @@ -2,14 +2,18 @@ $summary-background: $grayer; .summary { align-content: center; + align-items: center; background: $summary-background; color: $white; display: flex; flex-wrap: wrap; + height: 65px; justify-content: flex-start; margin-bottom: 2rem; - padding: 1em; + overflow: hidden; + padding: 1rem; .summary-stats { + align-items: center; display: flex; margin: 0 .5rem; } @@ -20,7 +24,7 @@ $summary-background: $grayer; } .number { @include font-size(xlarge); - @include font-type(bold); + @include font-type(light); line-height: .9; margin-right: .3rem; } @@ -69,6 +73,21 @@ $summary-background: $grayer; transition: fill .2s; } } + .main-summary-stats { + display: flex; + transform: translateY(0); + transition: all .2s ease-in-out; + } + + + .show-role-points { + .points-per-role-stats { + transform: translateY(-35px); + } + .main-summary-stats { + transform: translateY(-65px); + } + } } .summary-progress-bar { @@ -102,7 +121,12 @@ $summary-background: $grayer; } .large-summary { + align-items: stretch; justify-content: space-between; + padding: .75rem 1rem; + .stats-wrapper { + padding-top: .35rem; + } .large-summary-wrapper { align-content: center; display: flex; @@ -110,6 +134,7 @@ $summary-background: $grayer; justify-content: flex-start; } .summary-progress-wrapper { + align-items: center; display: flex; } .summary-progress-bar { @@ -122,13 +147,23 @@ $summary-background: $grayer; border: 0; margin: 0; } + &.summary-completed-points, + &.summary-closed-tasks { + border-right: 1px solid $blackish; + margin-right: 0; + padding-right: 1rem; + +.summary-stats { + border-left: 1px solid $gray; + margin-left: 0; + padding-left: 1rem; + } + } } .icon { + @include svg-size(1.3rem); fill: currentColor; - height: 1.5rem; margin-right: .4rem; vertical-align: middle; - width: 1.5rem; &.icon-stats { color: $primary; float: right; @@ -146,6 +181,33 @@ $summary-background: $grayer; } } } + .points-per-role-stats-content { + display: flex; + padding-left: 1rem; + .summary-stats { + padding: 0; + } + } + .toggle-points-per-role { + color: $white; + cursor: pointer; + svg { + @include svg-size(); + } + } + .points-per-role-stats { + margin-left: .5rem; + transform: translateY(35px); + transition: all .2s ease-in-out; + .number { + @include font-size(large); + @include font-type(normal); + } + .role { + @include font-size(xsmall); + @include font-type(light); + } + } } .empty-burndown { diff --git a/app/styles/components/tag.scss b/app/styles/components/tag.scss deleted file mode 100644 index ff2ed64c..00000000 --- a/app/styles/components/tag.scss +++ /dev/null @@ -1,88 +0,0 @@ -.tag { - @include font-type(light); - @include font-size(small); - background: $whitish; // Fallback - border-radius: 0 5px 5px 0; - color: $grayer; - display: inline-block; - margin: 0 .5rem .5rem 0; - padding: .5rem; - text-align: center; - .icon-delete { - color: $gray-light; - margin-left: 1rem; - &:hover { - color: $red; - } - } -} - -.ui-autocomplete { - background: $white; - border: 1px solid $gray-light; - z-index: 99910; - .ui-state-focus { - background: $primary-light; - } - li { - cursor: pointer; - } -} - -.ui-helper-hidden-accessible { - display: none; -} - -.tags-block { - .tags-container { - display: inline-block; - } - input { - margin-right: .25rem; - padding: .4rem; - width: 14rem; - } - .save { - cursor: pointer; - display: inline-block; - margin-left: .5rem; - } - .icon-save { - @include svg-size(); - fill: $grayer; - &:hover { - fill: $primary; - transition: .2s linear; - } - } - .tag { - @include font-size(small); - margin: 0 .5rem .5rem 0; - padding: .5rem; - } - .icon-close { - @include svg-size(.7rem); - fill: $gray-light; - margin-left: .25rem; - &:hover { - cursor: pointer; - fill: $red; - } - } - .add-tag { - color: $gray-light; - display: inline-block; - &:hover { - color: $primary-light; - } - } - .icon-add { - @include svg-size(.9rem); - } - .add-tag-text { - @include font-size(small); - } - .remove-tag { - display: inline-block; - } -} diff --git a/app/styles/components/taskboard-task.scss b/app/styles/components/taskboard-task.scss deleted file mode 100644 index 7a9e6516..00000000 --- a/app/styles/components/taskboard-task.scss +++ /dev/null @@ -1,140 +0,0 @@ -.taskboard-task { - background: $card; - border: 1px solid $card-hover; - box-shadow: none; - cursor: move; - margin: .2rem; - position: relative; - &:hover { - .icon-edit { - display: block; - fill: $card-dark; - opacity: 1; - transition: color .3s linear, opacity .3s linear; - } - } - &.gu-mirror { - box-shadow: 1px 1px 15px rgba($black, .4); - transition: box-shadow .3s linear; - } - .blocked { - background: $red; - border: 1px solid darken($red, 10%); - color: $white; - svg, - span { - color: $white; - fill: $white; - } - &:hover { - .icon-edit { - fill: currentColor; - } - } - } - &.card-placeholder { - background: darken($whitish, 2%); - border: 3px dashed darken($whitish, 8%); - cursor: default; - } - .taskboard-tagline { - border-color: $card-hover; - display: flex; - height: .6rem; - } - .taskboard-tag { - border-top: .3rem solid $card-hover; - flex-basis: 0; - flex-grow: 1; - height: .6rem; - z-index: 90; - } - .taskboard-task-inner { - display: flex; - padding: .5rem; - } - .taskboard-user-avatar { - flex-basis: 50px; - flex-grow: 1; - max-width: 55px; - a { - @include font-size(small); - display: block; - text-align: center; - } - img { - margin: 0 auto; - &:hover { - border: 2px solid $primary; - transition: border .3s linear; - } - } - } - .iocaine { - left: .2rem; - position: absolute; - top: 1rem; - img { - filter: hue-rotate(150deg) saturate(200%); - } - } - .icon-iocaine { - background: $black; - border-radius: 5px; - fill: $white; - height: 1.75rem; - padding: .25rem; - width: 1.75rem; - } - .task-assigned { - @include font-size(small); - color: $card-dark; - display: block; - &:hover { - color: $primary; - } - } - .task-num { - color: $grayer; - margin-right: .5em; - } - .task-name { - @include font-type(bold); - } - .taskboard-text { - @include font-size(small); - flex-basis: 50px; - flex-grow: 10; - padding: 0 .5rem 0 1rem; - word-wrap: break-word; - } - .icon { - transition: color .3s linear, opacity .3s linear; - } - .loading { - bottom: .5rem; - position: absolute; - } - .edit-task { - bottom: .5rem; - position: absolute; - top: auto; - } - .icon-edit { - @include svg-size(1.1rem); - cursor: pointer; - fill: $card-hover; - opacity: 0; - &:hover { - fill: $card-dark; - } - } - .icon-edit, - .loading { - right: 1rem; - } -} - -.task-drag { - @include box-shadow(); -} diff --git a/app/styles/components/track-btn.scss b/app/styles/components/track-btn.scss index 3ed50c71..221d893a 100644 --- a/app/styles/components/track-btn.scss +++ b/app/styles/components/track-btn.scss @@ -13,7 +13,7 @@ position: relative; .track-inner { align-items: center; - background: $whitish; + background: $mass-white; border-radius: 4px 0 0 4px; display: flex; flex: 1; @@ -22,7 +22,7 @@ margin-right: .1rem; min-width: 140px; &:hover { - background: darken($whitish, 5%); + background: darken($mass-white, 5%); transition: background .3s; } } @@ -131,7 +131,7 @@ justify-content: center; margin-right: .3rem; .vote-inner { - background: $whitish; + background: $mass-white; color: $gray-light; display: block; padding: 1rem; @@ -139,7 +139,7 @@ } a { &:hover { - background: darken($whitish, 5%); + background: darken($mass-white, 5%); color: $primary-dark; transition: background .3s; path { diff --git a/app/styles/components/wysiwyg.scss b/app/styles/components/wysiwyg.scss index ad26118e..dbf1220d 100644 --- a/app/styles/components/wysiwyg.scss +++ b/app/styles/components/wysiwyg.scss @@ -6,23 +6,44 @@ h1 { @include font-size(xlarge); @include font-type(text); - line-height: 2.5rem; + font-size: 2.25em; + line-height: 1.2; + margin-bottom: 1rem; + margin-top: 1rem; + padding-bottom: .5rem; text-transform: uppercase; } h2 { - @include font-size(large); + @include font-size(larger); @include font-type(bold); - margin-bottom: .5rem; - text-transform: uppercase; + line-height: 1.225; + margin-bottom: 1rem; + margin-top: 1rem; + padding-bottom: .5rem; } h3 { + @include font-size(large); @include font-type(bold); - text-transform: uppercase; + margin-bottom: 1rem; + margin-top: 1rem; + padding-bottom: .5rem; + } + h4 { + @include font-type(bold); + margin-bottom: 1rem; + margin-top: 1rem; } ul, ol { + line-height: 1.5; list-style-position: outside; - margin-left: 1rem; + margin-bottom: 0; + margin-top: 0; + padding-left: 2em; + ul, + ol { + padding-left: 1rem; + } } ul { list-style-type: disc; @@ -53,6 +74,11 @@ .codehilite { overflow: auto; } + blockquote { + p { + margin: 0; + } + } pre, code { @include font-size(small); @@ -64,10 +90,11 @@ overflow: auto; unicode-bidi: embed; white-space: pre-wrap; + } pre { line-height: 1.4rem; - padding: .5rem; + padding: 1rem; } table { border: $gray-light 1px solid; @@ -103,4 +130,7 @@ background: $white; max-height: none; } + hr { + border: 1px solid $whitish; + } } diff --git a/app/styles/core/base.scss b/app/styles/core/base.scss index b42e403f..7b4af1d2 100644 --- a/app/styles/core/base.scss +++ b/app/styles/core/base.scss @@ -56,21 +56,6 @@ body { min-width: 0; padding: 1rem; width: 320px; - &.filters-bar { - flex: 0 0 auto; - padding: 0; - transition: all .2s linear; - width: 0; - &.active { - padding: 2em 1em; - transition: all .2s linear; - width: 260px; - .filters-inner { - opacity: 1; - transition: all .4s ease-in; - } - } - } .search-in { margin-top: .5rem; } diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index ed6cb729..4ea9378f 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -1,2 +1,2 @@ $navbar: 40px; -$main-height: calc(100vh - 40px); +$main-height: calc(100vh - #{$navbar}); diff --git a/app/styles/dependencies/mixins/empty-color.scss b/app/styles/dependencies/mixins/empty-color.scss new file mode 100644 index 00000000..9c49729b --- /dev/null +++ b/app/styles/dependencies/mixins/empty-color.scss @@ -0,0 +1,39 @@ +@function sqrt($r) { + $x0: 1; + $x1: $x0; + + @for $i from 1 through 10 { + $x1: $x0 - ($x0 * $x0 - abs($r)) / (2 * $x0); + $x0: $x1; + } + + @return round($x1); +} + +@mixin empty-color($width) { + background: $mass-white; + border: 1px solid $whitish; + position: relative; + &:after { + content: ""; + width: 2px; + height: #{sqrt(2*$width*$width)}px; + background: #ff8282; + transform: rotate(-45deg); + position: absolute; + top: 0; + left: 0; + transform-origin: top; + } + &:before { + content: ""; + width: 2px; + height: #{sqrt(2*$width*$width)}px; + background: #ff8282; + transform: rotate(45deg); + position: absolute; + top: 0; + right: 0; + transform-origin: top; + } +} diff --git a/app/styles/dependencies/mixins/epics-dashboard.scss b/app/styles/dependencies/mixins/epics-dashboard.scss new file mode 100644 index 00000000..75781d58 --- /dev/null +++ b/app/styles/dependencies/mixins/epics-dashboard.scss @@ -0,0 +1,55 @@ +@mixin epics-table { + .project, + .assigned { + padding: .5rem; + } + .vote, + .status, + .sprint, + .name, + .progress { + padding: 1rem .5rem; + } + .vote { + flex-basis: 60px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + text-align: center; + } + .assigned, + .project { + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + text-align: center; + } + .status, + .sprint { + flex-basis: 150px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + max-width: 150px; + text-align: center; + } + .name, + .progress { + flex-basis: 20vw; + flex-grow: 1; + flex-shrink: 1; + max-width: 40vw; + } + .progress { + flex-shrink: 3; + margin-right: 1rem; + position: relative; + } + .sprint { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 90%; + } +} diff --git a/app/styles/dependencies/mixins/popover.scss b/app/styles/dependencies/mixins/popover.scss index 19fd9038..595896a7 100644 --- a/app/styles/dependencies/mixins/popover.scss +++ b/app/styles/dependencies/mixins/popover.scss @@ -1,4 +1,15 @@ -@mixin popover($width, $top: '', $left: '', $bottom: '', $right: '', $arrow-width: 0, $arrow-top: '', $arrow-left: '', $arrow-bottom: '', $arrow-height: 15px) { +@mixin popover( + $width, + $top: '', + $left: '', + $bottom: '', + $right: '', + $arrow-width: 0, + $arrow-top: '', + $arrow-left: '', + $arrow-bottom: '', + $arrow-height: 15px +) { @include font-type(light); @include font-size(small); background: $blackish; @@ -14,6 +25,7 @@ top: #{$top}; width: $width; z-index: 99; + text-align: center; a { @include font-size(small); border-bottom: 1px solid $grayer; diff --git a/app/styles/extras/dependencies.scss b/app/styles/extras/dependencies.scss index a7fff44b..2d94ceff 100644 --- a/app/styles/extras/dependencies.scss +++ b/app/styles/extras/dependencies.scss @@ -21,3 +21,4 @@ @import '../dependencies/mixins/slide'; @import '../dependencies/mixins/svg'; @import '../dependencies/mixins/track-buttons'; +@import '../dependencies/mixins/empty-color'; diff --git a/app/styles/layout/admin-project-tags.scss b/app/styles/layout/admin-project-tags.scss new file mode 100644 index 00000000..982b554c --- /dev/null +++ b/app/styles/layout/admin-project-tags.scss @@ -0,0 +1,133 @@ +.add-tag-container { + align-items: center; + background: $mass-white; + display: flex; + margin: .5rem 0; + padding: 1rem; + .color-column { + cursor: pointer; + flex-basis: 60px; + flex-grow: 0; + flex-shrink: 0; + position: relative; + } + .tag-name { + flex-basis: 80%; + margin-right: 1rem; + } + .options-column { + display: flex; + .loading-spinner { + margin-right: 1.2rem; + width: 1.2rem; + } + } + .current-color { + &.empty-color { + @include empty-color(38); + } + } + input[type="text"] { + background: $white; + } + .icon { + &.icon-close, + &.icon-save { + opacity: 1; + } + } +} + +.tags-table { + .table-tags-editor { + input[type="text"] { + background-color: transparent; + border: 0; + border-bottom: 1px solid transparent; + box-shadow: none; + transition: border-bottom .2s linear; + &:focus { + border-bottom: 1px solid $gray; + outline: none; + } + } + .row { + &.header-tag-row { + cursor: default; + padding-left: 1rem; + } + } + } + .color-filter { + align-items: center; + display: flex; + flex-grow: 1; + padding: 0 10px; + position: relative; + &:hover { + input { + border-bottom: 1px solid $whitish; + } + } + input { + padding: 0; + } + label { + cursor: pointer; + } + } + .row { + &.tag-row { + margin: .3rem 0; + padding: .7rem; + &:hover { + cursor: default; + } + } + .loading-spinner { + margin-right: 1.2rem; + width: 1.2rem; + } + } + .mix-tags { + position: relative; + .popover { + @include popover(120px, '', '', 2rem, -85%, 1rem, '', 50%, -5px); + } + &:hover { + .popover { + display: block; + } + } + } + .mixing-options-column { + text-align: right; + .loading-spinner { + margin-right: 1.2rem; + width: 1.2rem; + } + } + .mixing-tags-from, + .mixing-tags-to { + background: lighten(rgba($primary-light, .2), 30%); + } + .mixing-confirm { + margin: 0 .5rem; + } + .mixing-help-text { + @include font-size(xsmall); + color: $primary-dark; + display: inline; + padding-right: .5rem; + text-align: center; + @include breakpoint(laptop) { + display: block; + padding: .5rem; + } + } + .current-color { + &.empty-color { + @include empty-color(38); + } + } +} diff --git a/app/styles/layout/admin-project-values.scss b/app/styles/layout/admin-project-values.scss index 7474d016..2d17fb9e 100644 --- a/app/styles/layout/admin-project-values.scss +++ b/app/styles/layout/admin-project-values.scss @@ -11,6 +11,15 @@ width: 100%; } } + .admin-tags-section-wrapper-empty { + color: $gray-light; + padding: 10vh 0 0; + text-align: center; + } + .loading-spinner { + max-height: 3rem; + max-width: 3rem; + } } } @@ -31,7 +40,7 @@ .project-values-title { align-content: center; align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; padding: .8em 1rem; diff --git a/app/styles/layout/backlog.scss b/app/styles/layout/backlog.scss index 340aa1af..f59fda27 100644 --- a/app/styles/layout/backlog.scss +++ b/app/styles/layout/backlog.scss @@ -1,21 +1,43 @@ +.backlog-filter { + align-items: stretch; + display: flex; + opacity: 0; + overflow: hidden; + position: relative; + transition: all .2s linear; + width: 0; + tg-filter { + transform: translateX(-260px); + transition: all .2s linear; + } + &.active { + opacity: 1; + transition: all .2s linear; + width: 260px; + tg-filter { + transform: translateX(0); + } + } +} .backlog-menu { - background: $whitish; + background: $mass-white; color: $blackish; display: flex; justify-content: space-between; margin-bottom: 1rem; - .trans-button { + .menu-button { + border-radius: 0; color: $blackish; display: inline-block; padding: .4rem 1.5rem; &.active, &:hover { - background: darken($whitish, 10%); - color: $grayer; + background: $whitish; + color: $gray; } &.active { &:hover { - background: lighten($gray, 30%); + background: darken($whitish, 10%); } } &.move-to-sprint { diff --git a/app/styles/layout/invitation.scss b/app/styles/layout/invitation.scss index f8d1ea37..cbdf801c 100644 --- a/app/styles/layout/invitation.scss +++ b/app/styles/layout/invitation.scss @@ -101,4 +101,10 @@ .login-form { border-right: 1px solid rgba($white, .3); } + .public-register-disabled { + width: 400px; + .login-form { + border-right: 0; + } + } } diff --git a/app/styles/layout/issues.scss b/app/styles/layout/issues.scss index 1c122c2f..52e9deff 100644 --- a/app/styles/layout/issues.scss +++ b/app/styles/layout/issues.scss @@ -1,10 +1,6 @@ .issues { .filters-bar { - flex: 0 0 auto; + position: relative; width: 260px; } - .filters-inner { - opacity: 1; - padding: 1rem; - } } diff --git a/app/styles/layout/kanban.scss b/app/styles/layout/kanban.scss index ce1cb08d..11a7ea77 100644 --- a/app/styles/layout/kanban.scss +++ b/app/styles/layout/kanban.scss @@ -4,6 +4,7 @@ height: $main-height; max-height: $main-height; max-width: calc(100vw - 50px); + position: relative; header { min-height: 70px; } @@ -14,3 +15,12 @@ display: none; } } + +.kanban-header { + align-items: center; + display: flex; + justify-content: space-between; + .options { + display: flex; + } +} diff --git a/app/styles/layout/taskboard.scss b/app/styles/layout/taskboard.scss index 7c64a5d5..e213b56f 100644 --- a/app/styles/layout/taskboard.scss +++ b/app/styles/layout/taskboard.scss @@ -1,6 +1,7 @@ .taskboard { height: $main-height; overflow: hidden; + position: relative; h1, .graphics-container, .summary { @@ -11,6 +12,12 @@ } } +.taskboard-header { + align-items: center; + display: flex; + justify-content: space-between; +} + .taskboard-inner { display: flex; flex-direction: column; diff --git a/app/styles/layout/ticket-detail.scss b/app/styles/layout/ticket-detail.scss index 93017208..30af3d0a 100644 --- a/app/styles/layout/ticket-detail.scss +++ b/app/styles/layout/ticket-detail.scss @@ -7,178 +7,6 @@ justify-content: center; margin-bottom: .5rem; } - .us-title { - @include font-size(large); - @include font-type(text); - align-items: flex-start; - background: $whitish; - display: flex; - flex: 1; - flex-direction: column; - padding: .5rem; - position: relative; - transition: all .2s linear; - &.blocked { - background: $red; - transition: all .2s linear; - vertical-align: middle; - .us-title-text, - input { - margin-bottom: .5rem; - } - .us-number, - .us-name, - .us-related-task { - color: $white; - } - a { - color: $white; - transition: color .3s linear; - } - a:hover { - color: $red-light; - } - .unblock { - @include font-type(bold); - color: $white; - float: right; - } - .unblock:hover { - color: $red-light; - transition: color .3s linear; - } - } - p { - margin-bottom: 0; - } - .us-edit-name-inner { - display: flex; - } - .edit-subject { - align-content: center; - align-items: center; - display: flex; - width: 100%; - } - input { - background: $white; - flex-grow: 1; - } - .save-container { - flex-grow: 1; - .save { - display: block; - } - } - .us-title-text { - @include font-size(larger); - @include font-type(text); - align-content: center; - align-items: center; - display: flex; - flex: 1; - margin-bottom: 0; - max-width: 92%; - width: 100%; - } - .us-title-text:hover { - .edit { - opacity: 1; - transition: opacity .3s linear; - } - } - .us-number { - @include font-type(text); - color: $gray-light; - flex-shrink: 0; - line-height: 2.2rem; - margin-right: .5rem; - } - .us-name { - color: $gray; - display: inline-block; - flex-grow: 1; - line-height: 2.2rem; - padding-right: 1rem; - width: 100%; - } - .save, - .edit { - cursor: pointer; - margin-left: .5rem; - svg { - fill: $gray-light; - } - } - .edit { - opacity: 0; - } - .us-related-task { - @include font-size(small); - color: $gray-light; - margin-top: .5rem; - a { - border-left: 1px solid $gray-light; - padding: 0 .2rem; - } - a:hover { - color: $primary; - } - a:first-child { - border: 0; - } - } - .block-desc-container { - @include font-size(small); - } - .block-description-title { - @include font-type(bold); - color: $white; - margin-right: .5rem; - } - .block-description { - color: $white; - display: inline-block; - margin-right: 5rem; - } - } - .loading-spinner { - @include loading-spinner; - max-height: 1.5rem; - max-width: 1.5rem; - } -} - -.blocked-warning { - margin-bottom: 1rem; - .blocked { - @include font-type(text); - @include font-size(xlarge); - color: $red; - line-height: 2.5rem; - margin-bottom: .5rem; - } - .icon { - @include font-size(xlarge); - vertical-align: middle; - } - .block-description { - color: $grayer; - margin: 0; - } -} - -.issue-nav { - position: absolute; - right: 1rem; - top: 1rem; - a { - display: inline-block; - } - svg { - @include svg-size(1.2rem); - fill: currentColor; - } } .subheader { @@ -217,7 +45,7 @@ transition: all .2s linear; } .editable { - background: $whitish; + background: $mass-white; cursor: pointer; } .no-description { @@ -255,7 +83,7 @@ } .view-description { .edit { - background: $whitish; + background: $mass-white; height: 2rem; left: 0; opacity: 0; diff --git a/app/styles/layout/wiki.scss b/app/styles/layout/wiki.scss index 696267da..3930ca41 100644 --- a/app/styles/layout/wiki.scss +++ b/app/styles/layout/wiki.scss @@ -1,34 +1,23 @@ .wiki { + max-width: 1024px; + .wysiwyg { + margin-bottom: 0; + } .wiki-title { @include font-type(light); - @include font-size(larger); - } - .remove { - @include font-size(small); - color: $gray-light; - &:hover { - color: $red-light; - transition: color .1s linear; - .icon { - fill: $red-light; - transition: fill .1s linear; - } - } - .icon { - color: $gray-light; - margin-right: .2rem; - } + @include font-size(xxlarge); + margin-bottom: 0; + padding: 1rem; } } .wiki-content { - @include cursor-progress; - margin-bottom: 2rem; + @include font-size(large); position: relative; &.editable { &:hover { .wysiwyg { - background: $whitish; + background: $mass-white; cursor: pointer; } } @@ -44,7 +33,7 @@ } .edit { @include svg-size(2rem); - background: $whitish; + background: $mass-white; left: 0; opacity: 0; padding: .2rem .5rem; diff --git a/app/styles/modules/admin/admin-project-export.scss b/app/styles/modules/admin/admin-project-export.scss index 1a02135c..3508321e 100644 --- a/app/styles/modules/admin/admin-project-export.scss +++ b/app/styles/modules/admin/admin-project-export.scss @@ -15,7 +15,7 @@ h3 { @include font-type(bold); @include font-size(large); - background: $whitish; + background: $mass-white; color: $gray; margin: .5rem; padding: .5rem; diff --git a/app/styles/modules/admin/admin-project-profile.scss b/app/styles/modules/admin/admin-project-profile.scss index ed86342f..31060629 100644 --- a/app/styles/modules/admin/admin-project-profile.scss +++ b/app/styles/modules/admin/admin-project-profile.scss @@ -68,7 +68,7 @@ display: none; } label { - background: $whitish; + background: $mass-white; color: $grayer; text-align: center; transition: all .2s linear; @@ -101,13 +101,13 @@ } .privacy-project[disabled] { + label { - background: $whitish; + background: $mass-white; box-shadow: none; color: $gray-light; cursor: not-allowed; opacity: .65; &:hover { - background: $whitish; + background: $mass-white; color: $gray-light; } } diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index 1cca310e..cb87cc1d 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -31,20 +31,24 @@ } .webhook-service { flex-basis: 20%; - flex-grow: 0; + flex-shrink: 0; } .webhook-url { - flex-basis: 400px; - flex-grow: 8; + flex-basis: 60%; + flex-grow: 0; + flex-shrink: 0; + overflow: hidden; span { - @include ellipsis($width: 65%); + @include ellipsis(85%); color: $gray-light; display: inline-block; vertical-align: middle; } a { color: $primary; + cursor: pointer; margin-left: .5rem; + white-space: nowrap; &:hover { color: $primary-light; } @@ -54,7 +58,9 @@ flex-basis: 100px; flex-grow: 0; flex-shrink: 0; + margin-left: auto; a { + cursor: pointer; display: inline-block; margin-right: .5rem; } @@ -82,7 +88,7 @@ } .webhooks-history { - @include slide(1000px, hidden, $min: 0); + display: none; } .history-single-wrapper { diff --git a/app/styles/modules/admin/contrib.scss b/app/styles/modules/admin/contrib.scss index 2db94d29..ec951f1d 100644 --- a/app/styles/modules/admin/contrib.scss +++ b/app/styles/modules/admin/contrib.scss @@ -51,3 +51,23 @@ } } } + +.contrib-form-wrapper { + align-items: center; + display: flex; + margin-bottom: 1rem; + input { + margin: 0; + } + .contrib-input { + border: 0; + flex: 5; + margin: 0; + } + .contrib-test { + border: 0; + flex: 1; + margin: 0; + margin-left: 1rem; + } +} diff --git a/app/styles/modules/backlog/backlog-table.scss b/app/styles/modules/backlog/backlog-table.scss index 81288a34..a7124d28 100644 --- a/app/styles/modules/backlog/backlog-table.scss +++ b/app/styles/modules/backlog/backlog-table.scss @@ -28,7 +28,7 @@ flex-shrink: 0; } .user-stories { - overflow: hidden; + // overflow: hidden; width: 100%; } .status { @@ -183,6 +183,9 @@ } .gu-transit { background: $whitish; + } + .sortable-placeholder { + background: $mass-white; height: 40px; width: 100%; * { @@ -281,23 +284,3 @@ } } } - -.empty-backlog { - @include font-type(light); - padding: 2rem; - text-align: center; - .row { - display: none; - } - img { - margin-bottom: 1rem; - } - .title { - @include font-size(large); - margin-bottom: .5rem; - text-transform: uppercase; - } - a { - color: $primary; - } -} diff --git a/app/styles/modules/backlog/sprints.scss b/app/styles/modules/backlog/sprints.scss index ae95465b..040cadba 100644 --- a/app/styles/modules/backlog/sprints.scss +++ b/app/styles/modules/backlog/sprints.scss @@ -47,14 +47,12 @@ a { @include font-size(normal); @include font-type(text); - @include ellipsis($width: 90%); display: inline-block; margin-right: .5rem; } } .sprint { margin-bottom: 2rem; - overflow: hidden; header { position: relative; } @@ -80,10 +78,11 @@ } } .number { - @include font-size(small); + @include font-size(xsmall); + margin-right: .2rem; } .description { - @include font-size(x-small); + @include font-size(xsmall); line-height: .6rem; margin-top: 5px; } @@ -182,7 +181,6 @@ padding: 0 4px; } .us-name { - @include ellipsis(230px); display: block; &.closed { color: lighten($gray-light, 5%); diff --git a/app/styles/modules/backlog/taskboard-table.scss b/app/styles/modules/backlog/taskboard-table.scss index 16b5f1f2..45c5abe6 100644 --- a/app/styles/modules/backlog/taskboard-table.scss +++ b/app/styles/modules/backlog/taskboard-table.scss @@ -3,27 +3,29 @@ $column-width: 300px; $column-flex: 1; $column-shrink: 0; -$column-margin: 0 10px 0 0; +$column-margin: 0 5px 0 0; +$column-padding: .5rem 1rem; @mixin fold { - .taskboard-task { - background: none; - border: 0; - margin: 0; - min-height: 0; - .taskboard-task-inner { - padding: .1rem; - } - .taskboard-tagline, - .taskboard-text { + .card { + align-self: flex-start; + margin-top: .5rem; + tg-card-slideshow, + .card-unfold, + .card-tag, + .card-title, + .card-owner-actions, + .card-data, + .card-statistics, + .card-owner-name { display: none; } - .avatar { - height: 35px; - width: 35px; - } - .icon { - display: none; + .card-owner { + img { + height: 1.3rem; + margin-right: 0; + width: 1.3rem; + } } } &.task-column, @@ -47,22 +49,18 @@ $column-margin: 0 10px 0 0; height: 100%; overflow: hidden; width: 100%; - .taskboard-task { - &.readonly { - cursor: auto; - } - &.gu-mirror { - opacity: 1; - .avatar-task-link { - display: none; - } + &.zoom-0 { + .task-colum-name span { + padding-right: 1rem; } } } .taskboard-table-header { - margin-bottom: .5rem; - min-height: 40px; + flex-basis: 38px; + flex-grow: 0; + flex-shrink: 0; + min-height: 38px; position: relative; width: 100%; .taskboard-table-inner { @@ -73,7 +71,7 @@ $column-margin: 0 10px 0 0; .task-colum-name { @include font-size(medium); align-items: center; - background: $whitish; + background: $mass-white; border-top: 3px solid $gray-light; color: $gray; display: flex; @@ -83,7 +81,7 @@ $column-margin: 0 10px 0 0; justify-content: space-between; margin: $column-margin; max-width: $column-width; - padding: .5rem 1rem; + padding: $column-padding; position: relative; text-transform: uppercase; width: $column-width; @@ -102,6 +100,9 @@ $column-margin: 0 10px 0 0; margin: 0; } } + span { + @include ellipsis(65%); + } } tg-svg { display: block; @@ -128,7 +129,8 @@ $column-margin: 0 10px 0 0; } .taskboard-table-body { - height: 100%; + flex: 1; + margin-bottom: 5rem; overflow: auto; width: 100%; .task-column { @@ -147,14 +149,10 @@ $column-margin: 0 10px 0 0; } .column-fold { @include fold; - .taskboard-task { - max-width: 40px; - width: 40px; - } } .task-row { display: flex; - margin-bottom: .5rem; + margin-bottom: .25rem; min-height: 10rem; width: 100%; &.blocked { @@ -167,6 +165,7 @@ $column-margin: 0 10px 0 0; .points-value, .points-value:hover { color: $white; + fill: $white; transition: color .3s linear; } .taskboard-tasks-box { @@ -185,18 +184,26 @@ $column-margin: 0 10px 0 0; } } } - .taskboard-userstory-box { padding: .5rem .5rem .5rem 1.5rem; } - .avatar-task-link { - display: none; + +} + +.taskboard-userstory-box { + position: relative; + .us-title { + @include font-size(normal); + @include font-type(text); + margin-bottom: 0; + margin-right: 3rem; } - .avatar-assigned-to { - display: block; - } - .icon { - transition: fill .2s linear; + .points-value { + @include font-size(small); + color: $gray-light; + span { + margin-right: .1rem; + } } tg-svg { cursor: pointer; @@ -219,20 +226,3 @@ $column-margin: 0 10px 0 0; } } } - -.taskboard-userstory-box { - position: relative; - .us-title { - @include font-size(normal); - @include font-type(text); - margin-bottom: 0; - margin-right: 3rem; - } - .points-value { - @include font-size(small); - color: $gray-light; - span { - margin-right: .1rem; - } - } -} diff --git a/app/styles/modules/common/colors-table.scss b/app/styles/modules/common/colors-table.scss index 872bbbeb..fabf7df4 100644 --- a/app/styles/modules/common/colors-table.scss +++ b/app/styles/modules/common/colors-table.scss @@ -8,11 +8,30 @@ } .row { padding-left: 50px; + &:hover { + background: transparent; + } } } - .table-main { - .row:hover { - background: lighten($primary, 60%); + .row { + align-items: center; + display: flex; + justify-content: center; + padding: 1rem; + &:last-child { + border: 0; + } + &.edition { + padding-left: 3rem; + .current-color { + cursor: pointer; + } + } + &.hidden { + display: none; + } + &:hover { + background: lighten(rgba($primary-light, .2), 30%); cursor: move; transition: background .2s ease-in; .icon { @@ -24,39 +43,11 @@ transition: opacity .3s linear; } } - .options-column { - a { - display: inline-block; - } - } - } - form { - &:last-child { - .row { - border: 0; - } - } - } - .row { - align-items: center; - border-bottom: 1px solid $whitish; - display: flex; - justify-content: center; - padding: 1rem; - &:last-child { - border: 0; - } - &.edition { - .current-color { - cursor: pointer; - } - } - &.edition, - &.new-value { + &.no-draggable { padding-left: 50px; - } - &.hidden { - display: none; + &:hover { + cursor: auto; + } } .color-column { flex-basis: 60px; @@ -70,10 +61,13 @@ .status-wip-limit { flex-basis: 100px; flex-grow: 1; + flex-shrink: 0; } - .status-name { + .status-name, + .color-name { flex-basis: 150px; - flex-grow: 6; + flex-grow: 1; + flex-shrink: 0; padding: 0 10px; position: relative; span { @@ -81,6 +75,9 @@ display: block; } } + .color-name { + flex-basis: 100px; + } .status-slug { flex-basis: 150px; flex-grow: 6; @@ -105,7 +102,15 @@ padding: 0 0 0 10px; text-align: center; } + } + .options-column { + a { + cursor: pointer; + display: inline-block; + } + } + .row-edit { .options-column { opacity: 1; @@ -113,18 +118,19 @@ } .current-color { - background-color: $gray-light; + background-color: $whitish; border-radius: 2px; height: 40px; width: 40px; } + .icon { cursor: pointer; fill: $gray-light; margin-right: 1rem; opacity: 0; &:hover { - fill: $primary; + fill: $primary-light; transition: all .2s ease-in; } &.icon-check { @@ -132,13 +138,20 @@ fill: $primary; opacity: 1; } + &.icon-merge { + cursor: default; + opacity: 1; + } + &.icon-search { + cursor: none; + fill: $primary; + opacity: 1; + } &.icon-drag { cursor: move; } &.icon-trash { - &:hover { - fill: $red-light; - } + fill: $red-light; } } .gu-mirror { diff --git a/app/styles/modules/common/custom-fields.scss b/app/styles/modules/common/custom-fields.scss index d0efb714..d2aed368 100644 --- a/app/styles/modules/common/custom-fields.scss +++ b/app/styles/modules/common/custom-fields.scss @@ -4,7 +4,7 @@ @include font-type(bold); align-content: space-between; align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; padding: .5rem 1rem; @@ -13,10 +13,12 @@ } .collapse { display: block; + transform: rotate(-90deg); + transition: .1s ease-out; + } + .open { + transform: rotate(0); } - } - .custom-fields-body { - @include slide(1000px, hidden, $min: 0); } .custom-field-single { border-bottom: 1px solid $whitish; diff --git a/app/styles/modules/common/history.scss b/app/styles/modules/common/history.scss deleted file mode 100644 index 554454a2..00000000 --- a/app/styles/modules/common/history.scss +++ /dev/null @@ -1,278 +0,0 @@ -.history { - margin-bottom: 1rem; -} -.changes-title { - display: block; - padding: .5rem; - &:hover { - .icon { - color: $primary; - transform: rotate(90deg); - transition: all .2s linear; - } - } - .icon { - color: $grayer; - float: right; - transform: rotate(0); - transition: all .2s linear; - } -} -.change-entry { - border-bottom: 1px solid $gray-light; - display: flex; - padding: .5rem; - &:last-child { - border-bottom: 0; - } - .activity-changed, - .activity-fromto { - flex-basis: 50px; - flex-grow: 1; - } - .activity-changed { - @include font-type(bold); - } - .activity-fromto { - @include font-size(small); - word-wrap: break-word; - } -} -.history-tabs { - @include font-type(light); - border-bottom: 1px solid $whitish; - border-top: 1px solid $whitish; - margin-bottom: 0; - li { - background: $white; - display: inline-block; - position: relative; - &.active { - border-left: 1px solid $whitish; - border-right: 1px solid $whitish; - color: $primary; - top: 1px; - } - &:hover { - color: $grayer; - transition: color .2s ease-in; - } - } - a { - color: $gray-light; - display: block; - padding: .5rem 2rem; - transition: color .2s ease-in; - } - .icon { - fill: currentColor; - height: .75rem; - margin-right: .5rem; - width: .75rem; - } -} -.add-comment { - @include cursor-progress; - @include clearfix; - margin-top: 1rem; - &.active { - .button-green { - display: block; - margin-top: .5rem; - } - textarea { - height: 6rem; - transition: height .3s ease-in; - } - .help-markdown { - opacity: 1; - transition: opacity .3s linear; - } - .preview-icon { - opacity: 1; - position: absolute; - right: 1rem; - } - } - textarea { - background: $white; - height: 5rem; - min-height: 41px; - } - .help-markdown { - opacity: 0; - } - .save-comment { - color: $white; - float: right; - } - .button-green { - display: none; - } - .edit, - .preview-icon { - position: absolute; - right: 1rem; - top: .5rem; - } - .edit { - fill: $gray-light; - &:hover { - cursor: pointer; - fill: $primary; - } - } - .preview-icon { - opacity: 0; - } -} -.show-more-comments { - @include font-size(small); - border-bottom: 1px solid $gray-light; - border-top: 1px solid $gray-light; - color: $gray-light; - display: block; - padding: 1rem 0 1rem 1rem; - &:hover { - background: lighten($primary, 60%); - transition: background .2s ease-in; - } -} -.comment-list { - &.activeanimation { - .comment-single.ng-enter:last-child, - .comment-single.ng-leave:last-child { - transition: all .3s ease-in; - } - .comment-single.ng-enter:last-child, - .comment-single.ng-leave.ng-leave-active:last-child { - opacity: 0; - } - .comment-single.ng-leave:last-child, - .comment-single.ng-enter.ng-enter-active:last-child { - opacity: 1; - } - } -} -.activity-single { - border-bottom: 1px solid $gray-light; - display: flex; - padding: 2rem 0; - position: relative; - &:hover { - .comment-delete { - opacity: 1; - transition: opacity .2s linear; - } - .comment-restore { - opacity: 1; - transition: opacity .2s linear; - } - } - &:first-child { - margin-top: 0; - } - &:last-child { - border-bottom: 0; - } - &.deleted-comment, - .deleted-comment { - @include font-size(small); - color: $gray-light; - padding: .5rem; - a { - color: $gray-light; - margin-left: .3rem; - &:hover { - color: $primary; - transition: color .2s linear; - } - } - img { - filter: grayscale(100%); - opacity: .5; - } - .comment-body { - display: none; - margin: .2rem 0 .5rem; - p { - @include font-size(medium); - } - } - } - .comment-restore { - @include font-size(small); - color: $gray-light; - display: block; - position: absolute; - right: 0; - top: .4rem; - .icon { - vertical-align: baseline; - } - &:hover { - color: $primary; - transition: color .2s linear; - } - } - .username { - color: $primary; - margin-bottom: .5rem; - } - .activity-user { - flex-basis: 60px; - flex-shrink: 0; - margin-right: 1rem; - img { - width: 100%; - } - } - .activity-username { - color: $primary; - margin-bottom: .5rem; - } - .activity-content { - flex-shrink: 0; - width: calc(100% - 80px); - } - .changes { - background: $whitish; - .change-entry { - display: none; - &.active { - display: flex; - } - } - } - .date { - @include font-size(small); - color: $gray-light; - margin-left: 1rem; - } - .wysiwyg { - margin-bottom: 0; - } - .comment-delete { - cursor: pointer; - display: block; - opacity: 0; - position: absolute; - right: .5rem; - top: 2rem; - svg { - fill: $red-light; - transition: all .2s linear; - } - &:hover { - svg { - fill: $red; - transition: color .2s linear; - } - } - } - &.activity { - .change-entry { - display: flex; - } - } -} diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index 77e9644e..9cfb907a 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -23,7 +23,7 @@ } label { @include font-size(xsmall); - background: $whitish; + background: $mass-white; border: 1px solid $gray-light; color: $grayer; cursor: pointer; @@ -192,8 +192,8 @@ } .member-limit-warning { @include font-size(small); - background: $red-light; - color: $white; + background: $mass-white; + color: $grayer; margin: 1rem 0; padding: 1rem 2rem; text-align: center; diff --git a/app/styles/modules/common/nav.scss b/app/styles/modules/common/nav.scss index 0da555f0..6aca2ffe 100644 --- a/app/styles/modules/common/nav.scss +++ b/app/styles/modules/common/nav.scss @@ -27,6 +27,9 @@ tg-project-menu { padding: 1.1rem .8rem; position: relative; } + li { + position: relative; + } a:hover { background: rgba($black, .2); transition: color .3s linear; @@ -100,3 +103,45 @@ tg-project-menu { opacity: 1; } } + +.backlog-sprints-menu { + @include font-size(small); + animation: slideLeft 200ms ease-in-out both; + background: linear-gradient(to right, rgba($black, 1) 0%, rgba($black, .8) 100%); + color: $white; + display: block; + left: 50px; + opacity: 1; + padding: .4rem 1rem; + position: absolute; + top: 1rem; + transition: all .2s; + white-space: nowrap; + z-index: 99; + a { + color: $white; + padding: .6rem .8rem; + text-align: left; + text-transform: none; + &:nth-child(2) { + padding: 1rem .8rem .6rem; + } + &:last-child { + padding: .6rem .8rem .4rem; + } + &:hover { + background: none; + } + } + &::after { + background: rgba($blackish, 1); + content: ''; + height: $label-arrow-wh; + left: calc(-#{$label-arrow-wh}/2); + position: absolute; + top: calc(1rem - #{$label-arrow-wh}/2); + transform: rotate(45deg); + width: $label-arrow-wh; + z-index: 98; + } +} diff --git a/app/styles/modules/common/related-tasks.scss b/app/styles/modules/common/related-tasks.scss index d34e27db..a2762c97 100644 --- a/app/styles/modules/common/related-tasks.scss +++ b/app/styles/modules/common/related-tasks.scss @@ -6,7 +6,7 @@ .related-tasks-header { align-content: center; align-items: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; min-height: 36px; diff --git a/app/styles/modules/common/ticket-data.scss b/app/styles/modules/common/ticket-data.scss index 1f18c223..edcdeea1 100644 --- a/app/styles/modules/common/ticket-data.scss +++ b/app/styles/modules/common/ticket-data.scss @@ -37,6 +37,7 @@ a { @include font-type(text); padding: .5rem 1rem; + text-align: left; } a:hover { background: rgba($primary-light, .2); diff --git a/app/styles/modules/common/wizard.scss b/app/styles/modules/common/wizard.scss index f0482026..5bcae75f 100644 --- a/app/styles/modules/common/wizard.scss +++ b/app/styles/modules/common/wizard.scss @@ -57,7 +57,6 @@ .icon { @include svg-size(1.5rem); fill: currentColor; - margin-right: 1rem; vertical-align: text-top; } .template-name { diff --git a/app/styles/modules/epics/epic-detail.scss b/app/styles/modules/epics/epic-detail.scss new file mode 100644 index 00000000..fe7a80b5 --- /dev/null +++ b/app/styles/modules/epics/epic-detail.scss @@ -0,0 +1,10 @@ +.epic-header-container { + display: flex; + .color-selector { + margin-right: .5rem; + } + tg-detail-header { + flex: 1; + width: 100%; + } +} diff --git a/app/styles/modules/filters/filters.scss b/app/styles/modules/filters/filters.scss deleted file mode 100644 index bc938262..00000000 --- a/app/styles/modules/filters/filters.scss +++ /dev/null @@ -1,114 +0,0 @@ -.filters { - h1 { - vertical-align: baseline; - .icon { - margin: 0; - } - a { - vertical-align: baseline; - } - } - .breadcrumb { - @include font-size(large); - margin-top: 1rem; - .icon-arrow-right { - @include svg-size(.7rem); - margin: 0 .25rem; - vertical-align: middle; - } - .back { - color: $gray-light; - } - } - input { - background: $grayer; - color: $white; - @include placeholder { - color: $gray-light; - } - } - .search-action { - position: absolute; - right: .7rem; - top: .7rem; - } -} - -.filters-inner { - opacity: 0; - transition: all .1s ease-in; - .loading { - margin: 0; - padding: 8px; - text-align: center; - width: 100%; - .loading-spinner { - @include loading-spinner; - max-height: 1rem; - max-width: 1rem; - } - } -} - -.filters-applied { - margin-top: .5rem; -} - -.filters-step-cat { - .save-filters { - color: $white; - display: block; - text-align: center; - } - .my-filter-name { - background: $grayer; - color: $whitish; - width: 100%; - @include placeholder { - color: $gray-light; - } - } -} - -.filter-list { - .single-filter { - cursor: pointer; - } -} - -.filters-cats { - margin-top: 2rem; - li { - border-bottom: 1px solid $gray-light; - text-transform: uppercase; - } - .custom-filters { - .title { - color: $primary; - } - } - a { - align-items: center; - color: $grayer; - display: flex; - justify-content: space-between; - padding: .5rem 0 .5rem .5rem; - transition: color .2s ease-in; - &:hover { - color: $primary; - transition: color .2s ease-in; - .icon { - opacity: 1; - transition: opacity .2s ease-in; - } - } - } - .icon { - fill: currentColor; - float: right; - height: .9rem; - opacity: 0; - transition: opacity .2s ease-in; - width: .9rem; - } -} diff --git a/app/styles/modules/help/joyride.scss b/app/styles/modules/help/joyride.scss index 945a61cd..7a617736 100644 --- a/app/styles/modules/help/joyride.scss +++ b/app/styles/modules/help/joyride.scss @@ -51,7 +51,7 @@ color: $white; } &.introjs-disabled { - background: $whitish; + background: $mass-white; background-color: none; color: $white; } diff --git a/app/styles/modules/home-project.scss b/app/styles/modules/home-project.scss index 13e869f7..3da70541 100644 --- a/app/styles/modules/home-project.scss +++ b/app/styles/modules/home-project.scss @@ -55,7 +55,7 @@ @include font-type(text); @include font-type(bold); align-content: center; - background: $whitish; + background: $mass-white; display: flex; justify-content: space-between; margin-bottom: .5rem; diff --git a/app/styles/modules/issues/issues-table.scss b/app/styles/modules/issues/issues-table.scss index d641bbd9..1b0ad746 100644 --- a/app/styles/modules/issues/issues-table.scss +++ b/app/styles/modules/issues/issues-table.scss @@ -51,29 +51,58 @@ } } .level-field { - flex-basis: 75px; + flex-basis: 85px; flex-grow: 0; flex-shrink: 0; text-align: center; - width: 75px; + width: 85px; } .votes { color: $gray; + cursor: pointer; flex-basis: 75px; flex-shrink: 0; text-align: center; width: 75px; + &:hover { + color: $primary-light; + transition: all .2s linear; + svg { + fill: $primary-light; + transition: all .2s linear; + } + } &.inactive { color: $gray-light; } - &.is-voted { - color: $primary-light; - } + } + .icon-upvote { + @include svg-size(.75rem); + fill: $gray; + margin-right: .25rem; + vertical-align: middle; + } + .icon-arrow-up, + .icon-arrow-down { + @include svg-size(.7rem); + fill: $gray-light; + margin-left: .25rem; + vertical-align: middle; + } + .is-voted { + color: $primary-light; + transition: all .2s linear; svg { - @include svg-size(.75rem); - fill: $gray; - margin-right: .25rem; - vertical-align: middle; + fill: $primary-light; + transition: all .2s linear; + } + &:hover { + color: $red-light; + svg { + fill: $red-light; + transform: rotate(180deg); + } + } } .subject { @@ -153,19 +182,3 @@ display: inline-block; } } - -.empty-issues { - margin-top: 4rem; - text-align: center; - img { - margin-bottom: 1rem; - } - .title { - @include font-size(large); - text-transform: uppercase; - } - p { - @include font-type(light); - margin: 0; - } -} diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss index 1ea5bf03..2dd442ce 100644 --- a/app/styles/modules/kanban/kanban-table.scss +++ b/app/styles/modules/kanban/kanban-table.scss @@ -1,10 +1,11 @@ //Table basic shared vars -$column-width: 300px; +$column-width: 296px; $column-folded-width: 30px; $column-flex: 0; $column-shrink: 0; -$column-margin: 0 10px 0 0; +$column-margin: 0 5px 0 0; +$column-padding: .5rem 1rem; .kanban-table { display: flex; @@ -12,7 +13,19 @@ $column-margin: 0 10px 0 0; height: 100%; overflow: hidden; width: 100%; + &.zoom-0 { + .task-column, + .task-colum-name { + max-width: $column-width / 2; + } + .task-colum-name span { + padding-right: 1rem; + } + } .vfold { + tg-card { + display: none; + } &.task-colum-name { align-items: center; display: flex; @@ -36,9 +49,6 @@ $column-margin: 0 10px 0 0; min-width: $column-folded-width; width: $column-folded-width; } - .kanban-task { - display: none; - } .kanban-column-intro { display: none; } @@ -46,11 +56,11 @@ $column-margin: 0 10px 0 0; .readonly { cursor: auto; } + } .kanban-table-header { - margin-bottom: .5rem; - min-height: 40px; + min-height: 38px; position: relative; width: 100%; .kanban-table-inner { @@ -58,10 +68,13 @@ $column-margin: 0 10px 0 0; overflow: hidden; position: absolute; } + .options { + display: flex; + } .task-colum-name { @include font-size(medium); align-items: center; - background: $whitish; + background: $mass-white; border-top: 3px solid $gray-light; color: $gray; display: flex; @@ -71,7 +84,7 @@ $column-margin: 0 10px 0 0; justify-content: space-between; margin: $column-margin; max-width: $column-width; - padding: .5rem .5rem .5rem 1rem; + padding: $column-padding; position: relative; text-transform: uppercase; &:last-child { @@ -110,6 +123,7 @@ $column-margin: 0 10px 0 0; max-width: $column-width; overflow-y: auto; widows: $column-width; + width: $column-width; &:last-child { margin-right: 0; } @@ -131,7 +145,7 @@ $column-margin: 0 10px 0 0; } } .kanban-uses-box { - background: $whitish; + background: $mass-white; } } diff --git a/app/styles/modules/search/search-result-table.scss b/app/styles/modules/search/search-result-table.scss index 79571ccd..0f9109e5 100644 --- a/app/styles/modules/search/search-result-table.scss +++ b/app/styles/modules/search/search-result-table.scss @@ -85,19 +85,3 @@ .search-result-table-header { @include font-type(bold); } - -.empty-search-results { - margin-top: 4rem; - text-align: center; - img { - margin-bottom: 1rem; - } - .title { - @include font-size(large); - text-transform: uppercase; - } - p { - @include font-type(light); - margin: 0; - } -} diff --git a/app/styles/modules/wiki/wiki-nav.scss b/app/styles/modules/wiki/wiki-nav.scss index ef09edb9..ca0a521d 100644 --- a/app/styles/modules/wiki/wiki-nav.scss +++ b/app/styles/modules/wiki/wiki-nav.scss @@ -1,17 +1,97 @@ +.wiki-nav { + padding: 0; + width: 240px; + .title { + @include font-size(larger); + padding: 2rem 1rem 0 2rem; + } + .add-button { + align-items: center; + display: flex; + padding: 1rem 1rem 1rem 2rem; + text-transform: uppercase; + vertical-align: middle; + &:hover { + svg { + background: $primary-light; + } + } + svg { + @include svg-size(1.25rem); + background: $gray-light; + border-radius: 2px; + fill: $white; + margin-right: .5rem; + padding: .25rem; + transition: background .2s linear; + } + } + .wiki-link-container { + margin: 0; + &.wiki-all-links { + border-top: 1px solid $gray-light; + } + } + input[type="text"] { + background: $whitish; + color: $grayer; + margin: 1rem 1rem 1rem 2rem; + width: 80%; + @include placeholder { + color: $gray-light; + } + } + .loading { + padding: 1rem; + text-align: center; + } +} .wiki-link { - @include font-type(text); align-items: center; - border-bottom: 1px solid $gray-light; + border-bottom: 1px solid $whitish; display: flex; justify-content: space-between; - padding: 1rem 0 1rem 1rem; - text-transform: uppercase; + margin-left: 2rem; + padding-right: 1rem; + position: relative; &:hover { .remove-wiki-page { cursor: pointer; opacity: 1; transition: opacity .2s linear; - transition-delay: .2s; + transition-delay: .1s; + } + .dragger { + cursor: move; + opacity: 1; + transition: opacity .2s linear; + transition-delay: .1s; + } + } + &.gu-mirror { + border-bottom: 0; + } + &.fixed-link { + @include font-size(large); + text-transform: uppercase; + } + &.is-sortable { + cursor: move; + } + .link-title { + cursor: pointer; + display: block; + flex-grow: 1; + padding: 1rem 0; + } + .dragger { + fill: $gray-light; + left: -1rem; + opacity: 0; + position: absolute; + top: 1rem; + svg { + @include svg-size(.7rem); } } .remove-wiki-page { @@ -22,37 +102,4 @@ } } } - .link-title { - cursor: pointer; - } - .icon-trash { - fill: $gray-light; - } -} - -.wiki-nav { - ul { - border-top: 1px solid $gray-light; - } - .add-button { - color: $white; - display: block; - margin-bottom: .5rem; - text-align: center; - } - input[type="text"] { - @include font-type(text); - @include font-size(medium); - background: $grayer; - color: $whitish; - @include placeholder { - color: $gray-light; - } - } - .loading { - margin: 0; - padding: 8px; - text-align: center; - width: 100%; - } } diff --git a/app/styles/modules/wiki/wiki-pages-table.scss b/app/styles/modules/wiki/wiki-pages-table.scss new file mode 100644 index 00000000..130cc9a0 --- /dev/null +++ b/app/styles/modules/wiki/wiki-pages-table.scss @@ -0,0 +1,49 @@ +.wiki-pages-table { + display: flex; + .row { + padding: .5rem; + } + .title { + @include font-size(medium); + @include font-type(bold); + } + .table-main { + @include font-size(small); + } + .title-field { + flex-basis: 180px; + flex-grow: 1; + flex-shrink: 0; + } + .created-field, + .created-field, + .modified-field { + flex-basis: 10vw; + flex-grow: 0; + flex-shrink: 0; + margin-right: .5rem; + } + .editions-field { + flex-basis: 80px; + flex-grow: 0; + flex-shrink: 0; + margin-right: .5rem; + text-align: center; + } + .creator-field, + .last-modifier-field { + align-items: center; + display: flex; + flex-basis: 200px; + .user-avatar { + flex-grow: 0; + img { + height: 2rem; + } + } + .user-full-name { + flex-grow: 1; + padding: .5rem; + } + } +} diff --git a/app/styles/modules/wiki/wiki-summary.scss b/app/styles/modules/wiki/wiki-summary.scss index 21189166..8abcf6bf 100644 --- a/app/styles/modules/wiki/wiki-summary.scss +++ b/app/styles/modules/wiki/wiki-summary.scss @@ -1,28 +1,42 @@ .wiki-summary { align-items: center; - flex-wrap: wrap; justify-content: flex-start; + margin-top: 1rem; + &.summary { + background: $mass-white; + color: $gray; + } div { display: flex; - justify-content: space-between; - margin-right: 1rem; - } - .number { - line-height: 2rem; - top: 0; + margin-right: 1.25rem; } .wiki-user-modification { display: flex; flex-direction: column; justify-content: flex-start; } - figure { - margin-right: .3rem; - width: 32px; + .avatar { + margin-right: .5rem; + width: 2.25rem; + } + img { + height: 100%; + width: 100%; } .username { @include font-size(large); - color: $primary-light; white-space: nowrap; } + .remove { + fill: $gray-light; + margin-left: auto; + transition: fill .1s linear; + &:hover { + cursor: pointer; + fill: $red-light; + } + svg { + @include svg-size(1.5rem); + } + } } diff --git a/app/styles/shame/shame.scss b/app/styles/shame/shame.scss index 35b86fc0..5a238d13 100644 --- a/app/styles/shame/shame.scss +++ b/app/styles/shame/shame.scss @@ -26,3 +26,10 @@ svg { a[ng-click] svg { pointer-events: auto; } + +// chrome url break +tg-card { + .card-title span:last-child { + word-break: break-word; + } +} diff --git a/app/svg/sprite.svg b/app/svg/sprite.svg index 0a5d3ec6..869e7c00 100644 --- a/app/svg/sprite.svg +++ b/app/svg/sprite.svg @@ -429,5 +429,30 @@ fill="#fff" d="M511.998 107.939c-222.856 0-404.061 181.204-404.061 404.061s181.205 404.061 404.061 404.061c222.856 0 404.061-181.203 404.061-404.061s-181.205-404.061-404.061-404.061zM511.998 158.447c88.671 0 169.621 32.484 231.616 86.222l-498.947 498.948c-53.74-61.998-86.223-142.945-86.223-231.617 0-195.561 157.992-353.553 353.553-353.553zM779.328 280.383c53.74 61.998 86.223 142.945 86.223 231.617 0 195.561-157.992 353.553-353.553 353.553-88.671 0-169.617-32.484-231.616-86.222l498.947-498.948z"> + + Add user + + + + View more + + + + Merge + + + + Fill + + + + Epics + + + + Broken Link + + diff --git a/app/themes/high-contrast/variables.scss b/app/themes/high-contrast/variables.scss index cd5ef6e6..1ecd40e2 100755 --- a/app/themes/high-contrast/variables.scss +++ b/app/themes/high-contrast/variables.scss @@ -16,6 +16,9 @@ $primary-light: #212121; $primary: #000; $primary-dark: #000; +// Mass white +$mass-white: #f5f5f5; + //Warning colors $red-light: #ff0062; $red: #ff2400; diff --git a/app/themes/material-design/custom.scss b/app/themes/material-design/custom.scss index 25252a9d..877e2884 100644 --- a/app/themes/material-design/custom.scss +++ b/app/themes/material-design/custom.scss @@ -79,7 +79,7 @@ input[type="date"], input[type="password"], select, textarea { - background: $whitish; + background: $mass-white; border-color: $primary; color: $grayer; @include placeholder { @@ -152,7 +152,7 @@ tg-project-menu { //Taskboard table .taskboard-table-header { .task-colum-name { - background: lighten($primary-light, 20%); + background: $mass-white; border-top: 3px solid $primary; .icon { fill: $primary; @@ -161,7 +161,7 @@ tg-project-menu { } .taskboard-table-body { .taskboard-tasks-box { - background: $whitish; + background: $mass-white; } } diff --git a/app/themes/material-design/variables.scss b/app/themes/material-design/variables.scss index c76c2e0b..f975e2c3 100755 --- a/app/themes/material-design/variables.scss +++ b/app/themes/material-design/variables.scss @@ -11,6 +11,9 @@ $gray-light: #BDBDBD; $whitish: #EEEEEE; $white: #fff; +// Mass white +$mass-white: #f5f5f5; + // Primary colors $primary-light: #8c9eff; $primary: #3f51b5; diff --git a/app/themes/taiga/custom.scss b/app/themes/taiga/custom.scss index 40fca1f4..c57561e0 100644 --- a/app/themes/taiga/custom.scss +++ b/app/themes/taiga/custom.scss @@ -15,7 +15,7 @@ body { // Secondary panel .menu-secondary { - background: $whitish; + background: $mass-white; } // Tertiary panel @@ -25,7 +25,7 @@ body { // Extra bar panel .extrabar { - background: $whitish; + background: $mass-white; } @@ -61,7 +61,7 @@ input[type="date"], input[type="password"], select, textarea { - background: lighten($whitish, 6%); + background: $mass-white; border-color: $gray-light; color: $grayer; @include placeholder { @@ -82,7 +82,7 @@ textarea { // Blockquote blockquote { - border-left: 5px solid $whitish; + border-left: 5px solid $mass-white; } blockquote, @@ -120,7 +120,7 @@ tg-project-menu { } .main-nav { - svg path { + svg { fill: $white; } } @@ -132,7 +132,7 @@ tg-project-menu { //Taskboard table .taskboard-table-header { .task-colum-name { - background: lighten($whitish, 5%); + background: $mass-white; border-top: 3px solid $gray-light; .icon { fill: $gray-light; @@ -141,7 +141,7 @@ tg-project-menu { } .taskboard-table-body { .taskboard-tasks-box { - background: lighten($whitish, 5%); + background: $mass-white; } } @@ -152,7 +152,7 @@ tg-project-menu { //Kanban table .kanban-table-header { .task-colum-name { - background: lighten($whitish, 5%); + background: $mass-white; border-top: 3px solid $gray-light; .icon { color: $gray-light; @@ -162,6 +162,6 @@ tg-project-menu { .kanban-table-body { .kanban-uses-box { - background: lighten($whitish, 5%); + background: $mass-white; } } diff --git a/app/themes/taiga/variables.scss b/app/themes/taiga/variables.scss index e8d00c10..ebc23064 100755 --- a/app/themes/taiga/variables.scss +++ b/app/themes/taiga/variables.scss @@ -11,6 +11,9 @@ $gray-light: #767676; $whitish: #e4e3e3; $white: #fff; +// Mass white +$mass-white: #f5f5f5; + // Primary colors $primary-light: #9dce0a; $primary: #5b8200; diff --git a/bower.json b/bower.json index 63bd0389..d8c928f0 100644 --- a/bower.json +++ b/bower.json @@ -49,42 +49,42 @@ "dependencies": { "emoticons": "~0.1.7", "jquery-flot": "~0.8.2", - "angular": "1.4.7", - "angular-route": "1.4.7", - "angular-animate": "1.4.7", - "angular-aria": "1.4.7", - "angular-sanitize": "1.4.7", + "angular": "1.5.5", + "angular-route": "1.5.5", + "angular-animate": "1.5.5", + "angular-aria": "1.5.5", + "angular-sanitize": "1.5.5", "checksley": "~0.6.0", - "jquery": "~2.1.1", + "jquery": "~2.2.3", "markitup-1x": "~1.1.14", "jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.7", "flot-axislabels": "markrcote/flot-axislabels", "flot-orderBars": "emmerich/flot-orderBars", "flot.tooltip": "~0.8.4", - "Sortable": "~0.1.8", - "moment": "~2.10.6", + "moment": "~2.13.0", "pikaday": "~1.4.0", - "raven-js": "~1.1.16", + "raven-js": "~3.0.0", "l.js": "~0.1.0", - "angular-translate": "~2.8.1", - "angular-translate-loader-partial": "~2.8.1", - "angular-translate-loader-static-files": "~2.8.1", - "angular-translate-interpolation-messageformat": "~2.8.1", - "ngInfiniteScroll": "1.2.1", - "immutable": "~3.7.6", - "bluebird": "~2.10.2", - "intro.js": "~1.1.1", - "lodash": "~4.0.0", - "messageformat": "^0.1.8", + "angular-translate": "~2.10.0", + "angular-translate-loader-partial": "~2.10.0", + "angular-translate-loader-static-files": "~2.10.0", + "angular-translate-interpolation-messageformat": "~2.10.0", + "ngInfiniteScroll": "^1.3.0", + "immutable": "~3.8.1", + "bluebird": "~3.3.5", + "intro.js": "~2.1.0", + "lodash": "~4.11.2", + "messageformat": "^0.3.1", "dragula.js": "dragula#^3.6.6", "bourbon": "^4.2.7" }, "resolutions": { - "lodash": "~4.0.0", + "lodash": "~4.11.2", "moment": "~2.10.6", - "jquery": "~2.1.1", - "angular": "1.4.7", - "messageformat": "0.1.8" + "jquery": "~2.2.3", + "angular": "1.5.5", + "messageformat": "0.3.1", + "angular-translate": "2.10.0" }, "private": true } diff --git a/conf.e2e.js b/conf.e2e.js index c3151a09..d7ca0a01 100644 --- a/conf.e2e.js +++ b/conf.e2e.js @@ -2,8 +2,9 @@ require("babel-register"); require("babel-polyfill"); var utils = require('./e2e/utils'); +var argv = require('minimist')(process.argv.slice(2)); -exports.config = { +var config = { seleniumAddress: 'http://localhost:4444/wd/hub', framework: 'mocha', params: { @@ -37,6 +38,7 @@ exports.config = { issues: "e2e/suites/issues/*.e2e.js", tasks: "e2e/suites/tasks/*.e2e.js", userProfile: "e2e/suites/user-profile/*.e2e.js", + epics: "e2e/suites/epics/*.e2e.js", userStories: "e2e/suites/user-stories/*.e2e.js", backlog: "e2e/suites/backlog.e2e.js", home: "e2e/suites/home.e2e.js", @@ -101,8 +103,6 @@ exports.config = { // }; // browser.addMockModule('trackMouse', trackMouse); - var argv = require('minimist')(process.argv.slice(2)); - browser.params.glob.back = argv.back; require('./e2e/capabilities.js'); @@ -140,4 +140,53 @@ exports.config = { return browser.get(browser.params.glob.host); }); } +}; + + +if (argv.json) { + var fs = require('fs'); + var dir = './e2e/reports'; + + if (!fs.existsSync(dir)){ + fs.mkdirSync(dir); + } + + var suites = argv.suite.split(',').join('-'); + + var reportFileName = 'report-' + suites + '-chrome.json'; + + if (argv.firefox) { + reportFileName = 'report-' + suites + '-firefox.json'; + } else if (argv.ie) { + reportFileName = 'report-' + suites + '-ie.json'; + } + + process.env['MOCHA_REPORTER'] = 'JSON'; + process.env['MOCHA_REPORTER_FILE'] = 'e2e/reports/' + reportFileName; + + config.mochaOpts.reporter = 'reporter-file'; } + +if (argv.firefox) { + config.capabilities = { + browserName: 'firefox' + }; +} + +if (argv.ie) { + config.capabilities = { + browserName: 'internet explorer', + version: '11' + }; +} + +if (argv.seleniumAddress) { + config.seleniumAddress = argv.seleniumAddress; +} + + +if (argv.host) { + config.params.glob.host = argv.host; +} + +exports.config = config; diff --git a/conf/conf.example.json b/conf/conf.example.json index 7868fbf1..325001cb 100644 --- a/conf/conf.example.json +++ b/conf/conf.example.json @@ -3,6 +3,7 @@ "eventsUrl": null, "eventsMaxMissedHeartbeats": 5, "eventsHeartbeatIntervalTime": 60000, + "eventsReconnectTryInterval": 10000, "debug": true, "debugInfo": false, "defaultLanguage": "en", diff --git a/e2e/helpers/admin-attributes-helper.js b/e2e/helpers/admin-attributes-helper.js index dd742b5f..b3bedf2d 100644 --- a/e2e/helpers/admin-attributes-helper.js +++ b/e2e/helpers/admin-attributes-helper.js @@ -34,6 +34,30 @@ helper.getSection = function(item) { }; }; +helper.getTagsSection = function(item) { + let section = $$('.admin-attributes-section').get(item); + + return { + el: section, + rows: function() { + return section.$$('.e2e-tag-row'); + }, + edit: async function(row) { + let editButton = row.$('.edit-value'); + + return browser.actions() + .mouseMove(editButton) + .click() + .perform(); + } + }; +}; + + +helper.getTagsFilter = function() { + return $('.table-header .e2e-tags-filter'); +}; + helper.getStatusNames = function(section) { return section.$$('.status-name span').getText(); }; @@ -94,6 +118,14 @@ helper.getGenericForm = function(form) { return form.$('.status-name input'); }; + obj.colorBox = function() { + return form.$('.edition .e2e-open-color-selector'); + }; + + obj.colorText = function() { + return form.$('.color-selector-dropdown input'); + }; + return obj; }; diff --git a/e2e/helpers/admin-memberships.js b/e2e/helpers/admin-memberships.js index 89cdb4d5..e22bc3f5 100644 --- a/e2e/helpers/admin-memberships.js +++ b/e2e/helpers/admin-memberships.js @@ -28,7 +28,7 @@ helper.getNewMemberLightbox = function() { el.$$('.remove-fieldset').get(index).click(); }, submit: function() { - el.$('.submit-button').click(); + return el.$('.submit-button').click(); } }; @@ -49,7 +49,7 @@ helper.getMembers = function() { helper.getOwner = function() { return helper.getMembers().filter(async (member) => { - return !!await member.$$('.icon-badge').count(); + return !!await member.$$('.owner-badge').count(); }).first(); }; diff --git a/e2e/helpers/backlog-helper.js b/e2e/helpers/backlog-helper.js index aec19df6..932e1c34 100644 --- a/e2e/helpers/backlog-helper.js +++ b/e2e/helpers/backlog-helper.js @@ -19,8 +19,21 @@ helper.getCreateEditUsLightbox = function() { subject: function() { return el.$('input[name="subject"]'); }, - tags: function() { - return el.$('.tag-input'); + tags: async function() { + $('.e2e-show-tag-input').click(); + $('.e2e-open-color-selector').click(); + + $$('.e2e-color-dropdown li').get(1).click(); + $('.e2e-add-tag-input') + .sendKeys('xxxyy') + .sendKeys(protractor.Key.ENTER); + + $$('.e2e-delete-tag').last().click(); + + $('.e2e-add-tag-input') + .sendKeys('a') + .sendKeys(protractor.Key.ARROW_DOWN) + .sendKeys(protractor.Key.ENTER); }, description: function() { return el.$('textarea[name="description"]'); @@ -130,10 +143,20 @@ helper.openNewMilestone = function(item) { $('.add-sprint').click(); }; +helper.getClosedSprintTable = function() { + return $$('.sprint-empty').last(); +}; + helper.toggleClosedSprints = function() { $('.filter-closed-sprints').click(); }; +helper.toggleSprint = async function(el) { + el.$('.compact-sprint').click(); + + await utils.common.waitTransitionTime(el.$('.sprint-table')); +}; + helper.closedSprints = function() { return $$('.sprint-closed'); }; @@ -164,6 +187,18 @@ helper.getUsRef = function(elm) { return elm.$('span[tg-bo-ref]').getText(); }; +helper.loadFullBacklog = async function() { + do { + var uss = helper.userStories(); + var count = await uss.count(); + var last = uss.last(); + + await browser.executeScript("arguments[0].scrollIntoView();", last.getWebElement()); + + var newcount = await uss.count(); + } while(count < newcount); +}; + // get ref with the larger length helper.getTestingFilterRef = async function() { let userstories = helper.userStories(); diff --git a/e2e/helpers/common-helper.js b/e2e/helpers/common-helper.js index d2e24891..83762161 100644 --- a/e2e/helpers/common-helper.js +++ b/e2e/helpers/common-helper.js @@ -63,3 +63,20 @@ helper.lightboxAttachment = async function() { expect(countAttachments + 1).to.be.equal(newCountAttachments); }; + +helper.tags = function() { + $('.e2e-show-tag-input').click(); + $('.e2e-open-color-selector').click(); + + $$('.e2e-color-dropdown li').get(1).click(); + $('.e2e-add-tag-input') + .sendKeys('xxxyy') + .sendKeys(protractor.Key.ENTER); + + $$('.e2e-delete-tag').last().click(); + + $('.e2e-add-tag-input') + .sendKeys('a') + .sendKeys(protractor.Key.ARROW_DOWN) + .sendKeys(protractor.Key.ENTER); +} diff --git a/e2e/helpers/custom-fields-helper.js b/e2e/helpers/custom-fields-helper.js index 2ac05c33..837ac956 100644 --- a/e2e/helpers/custom-fields-helper.js +++ b/e2e/helpers/custom-fields-helper.js @@ -44,14 +44,14 @@ helper.edit = async function(indexType, indexCustomField, name, desc, option) { }; helper.drag = function(indexType, indexCustomField, indexNewPosition) { - let customField = helper.getCustomFiledsByType(indexType).get(indexCustomField); - let newPosition = helper.getCustomFiledsByType(indexType).get(indexNewPosition).getLocation(); + let customField = helper.getCustomFiledsByType(indexType).get(indexCustomField).$('.e2e-drag'); + let newPosition = helper.getCustomFiledsByType(indexType).get(indexNewPosition); - return utils.common.drag(customField, newPosition, {y: 30}); + return utils.common.drag(customField, newPosition, 5, 25); }; helper.getCustomFiledsByType = function(indexType) { - return $$('div[tg-project-custom-attributes]').get(indexType).$$('.js-sortable > div'); + return $$('div[tg-project-custom-attributes]').get(indexType).$$('.e2e-item'); }; helper.delete = async function(indexType, indexCustomField) { @@ -66,7 +66,7 @@ helper.delete = async function(indexType, indexCustomField) { }; helper.getName = function(indexType, indexCustomField) { - return helper.getCustomFiledsByType(indexType).get(indexCustomField).$('.custom-name span').getText(); + return helper.getCustomFiledsByType(indexType).get(indexCustomField).$('.js-view-custom-field .custom-name').getText(); }; helper.getDetailFields = function() { diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js index 37425948..a315edbb 100644 --- a/e2e/helpers/detail-helper.js +++ b/e2e/helpers/detail-helper.js @@ -2,22 +2,23 @@ var utils = require('../utils'); var helper = module.exports; helper.title = function() { - let el = $('span[tg-editable-subject]'); + let el = $('.e2e-story-header'); let obj = { el: el, getTitle: function() { - return el.$('.view-subject').getText(); + return el.$('.e2e-title-subject').getText(); }, setTitle: function(title) { - el.$('.view-subject').click(); - el.$('.edit-subject input').clear().sendKeys(title); + el.$('.e2e-detail-edit').click(); + el.$('.e2e-title-input').clear().sendKeys(title); }, - save: function() { - el.$('.save').click(); + save: async function() { + el.$('.e2e-title-button').click(); + await browser.waitForAngular(); } }; @@ -29,7 +30,9 @@ helper.description = function(){ let obj = { el: el, - + focus: function() { + el.$('textarea').click(); + }, enabledEditionMode: async function(){ await el.$(".view-description").click(); }, @@ -54,35 +57,36 @@ helper.description = function(){ helper.tags = function() { - let el = $('div[tg-tag-line]'); + let el = $('tg-tag-line-common'); let obj = { el:el, clearTags: async function() { - let tags = await el.$$('.icon-delete'); + let tags = await el.$$('.e2e-delete-tag'); let totalTags = tags.length; let htmlChanges = null; while (totalTags > 0) { htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container")); - await el.$$('.icon-delete').first().click(); + await el.$$('.e2e-delete-tag').first().click(); totalTags --; await htmlChanges(); } }, getTagsText: function() { - return el.$$('.tag-name').getText(); + return el.$$('tg-tag span').getText(); }, addTags: async function(tags) { let htmlChanges = null - el.$('.add-tag').click(); + $('.e2e-show-tag-input').click(); + for (let tag of tags){ htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container")); - el.$('.tag-input').sendKeys(tag); - await browser.actions().sendKeys(protractor.Key.ENTER).perform(); + el.$('.e2e-add-tag-input').sendKeys(tag); + el.$('.save').click(); await htmlChanges(); } } @@ -142,17 +146,37 @@ helper.assignedTo = function() { return obj; }; +helper.editComment = function() { + let el = $('.comment-editor'); + let obj = { + el:el, + + updateText: function (text) { + el.$('textarea').sendKeys(text); + }, + + saveComment: async function () { + el.$('.save-comment').click(); + await browser.waitForAngular(); + } + } + return obj; + +}; + helper.history = function() { let el = $('section.history'); let obj = { el:el, - selectCommentsTab: function() { - el.$$('.history-tabs li a').first().click(); + selectCommentsTab: async function() { + el.$('.e2e-comments-tab').click(); + await browser.waitForAngular(); }, - selectActivityTab: function() { - el.$$('.history-tabs li a').last().click(); + selectActivityTab: async function() { + el.$('.e2e-activity-tab').click(); + await browser.waitForAngular(); }, addComment: async function(comment) { @@ -166,46 +190,66 @@ helper.history = function() { }, countComments: async function() { - let moreComments = el.$('.comments-list .show-more-comments'); - let moreCommentsIsPresent = await moreComments.isPresent(); - if (moreCommentsIsPresent){ - moreComments.click(); - } - await browser.waitForAngular(); - let comments = await el.$$(".activity-single.comment"); + let comments = await el.$$(".comment-wrapper"); return comments.length; }, countActivities: async function() { - let moreActivities = el.$('.changes-list .show-more-comments'); - let selectActivityTabIsPresent = await moreActivities.isPresent(); - if (selectActivityTabIsPresent){ - utils.common.link(moreActivities); - // moreActivities.click(); - } - await browser.waitForAngular(); - let activities = await el.$$(".activity-single.activity"); + let activities = await el.$$(".activity"); return activities.length; }, countDeletedComments: async function() { - let moreComments = el.$('.comments-list .show-more-comments'); - let moreCommentsIsPresent = await moreComments.isPresent(); - if (moreCommentsIsPresent){ - moreComments.click(); - } - await browser.waitForAngular(); - let comments = await el.$$(".activity-single.comment.deleted-comment"); + let comments = await el.$$(".deleted-comment-wrapper"); return comments.length; }, + editLastComment: async function() { + let lastComment = el.$$(".comment-wrapper").last(); + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").first().click(); + await browser.waitForAngular(); + }, + deleteLastComment: async function() { - el.$$(".activity-single.comment .comment-delete").last().click(); + let lastComment = el.$$(".comment-wrapper").last(); + + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").last().click(); + await browser.waitForAngular(); + }, + + showVersionsLastComment: async function() { + el.$$(".comment-edited a").last().click(); + await browser.waitForAngular(); + }, + + closeVersionsLastComment: async function() { + $(".lightbox-display-historic .close").click(); + await browser.waitForAngular(); + }, + + enableEditModeLastComment: async function() { + let lastComment = el.$$(".comment-wrapper").last(); + browser + .actions() + .mouseMove(lastComment) + .perform(); + + lastComment.$$(".comment-option").last().click(); await browser.waitForAngular(); }, restoreLastComment: async function() { - el.$$(".activity-single.comment.deleted-comment .comment-restore").last().click(); + el.$$(".deleted-comment-wrapper .restore-comment").last().click(); await browser.waitForAngular(); } } @@ -281,9 +325,7 @@ helper.attachment = function() { }, upload: async function(filePath, name) { let addAttach = el.$('#add-attach'); - let countAttachments = await $$('tg-attachment').count(); - let toggleInput = function() { $('#add-attach').toggle(); }; @@ -298,8 +340,8 @@ helper.attachment = function() { return !!count; }, 5000); - await el.$('tg-attachment .editable-attachment-comment input').sendKeys(name); + await browser.sleep(500); await browser.actions().sendKeys(protractor.Key.ENTER).perform(); await browser.executeScript(toggleInput); await browser.waitForAngular(); @@ -415,6 +457,18 @@ helper.attachment = function() { list: function() { $('.view-list').click(); }, + previewLightbox: function() { + return utils.lightbox.open($('tg-attachments-preview')); + }, + getPreviewSrc: function() { + return $('tg-attachments-preview img').getAttribute('src'); + }, + nextPreview: function() { + return $('tg-attachments-preview .next').click(); + }, + attachmentLinks: function() { + return $$('.e2e-attachment-link'); + } }; return obj; @@ -488,3 +542,43 @@ helper.watchersLightbox = function() { return obj; }; + +helper.teamRequirement = function() { + let el = $('tg-us-team-requirement-button'); + + let obj = { + el: el, + + toggleStatus: async function(){ + await el.$("label").click(); + await browser.waitForAngular(); + }, + + isRequired: async function() { + let classes = await el.$("label").getAttribute('class'); + return classes.includes("active"); + } + }; + + return obj; +}; + +helper.clientRequirement = function() { + let el = $('tg-us-client-requirement-button'); + + let obj = { + el: el, + + toggleStatus: async function(){ + await el.$("label").click(); + await browser.waitForAngular(); + }, + + isRequired: async function() { + let classes = await el.$("label").getAttribute('class'); + return classes.includes("active"); + } + }; + + return obj; +}; diff --git a/e2e/helpers/epic-detail-helper.js b/e2e/helpers/epic-detail-helper.js new file mode 100644 index 00000000..4db091e9 --- /dev/null +++ b/e2e/helpers/epic-detail-helper.js @@ -0,0 +1,76 @@ +var utils = require('../utils'); +var commonHelper = require('./common-helper'); + +var helper = module.exports; + + +helper.colorEditor = function() { + let el = $('tg-color-selector'); + + let obj = { + el: el, + + open: async function(){ + await el.$(".e2e-open-color-selector").click(); + }, + + selectFirstColor: async function() { + let color = el.$$(".color-selector-option").first(); + color.click(); + await browser.waitForAngular(); + }, + + selectLastColor: async function() { + let color = el.$$(".color-selector-option").last(); + color.click(); + await browser.waitForAngular(); + } + }; + + return obj; +}; + +helper.relatedUserstories = function() { + let el = $('tg-related-userstories'); + let lightboxCreateRelatedUserStories = el.$(".lightbox-create-related-user-stories"); + + let obj = { + el: el, + + createNewUserStory: async function(subject) { + el.$(".e2e-add-userstory-button").click(); + el.$(".e2e-new-userstory-label").click(); + el.$(".e2e-single-creation-label").click(); + el.$(".e2e-new-userstory-input-text").sendKeys(subject); + el.$(".e2e-create-userstory-button").click(); + await utils.lightbox.close(lightboxCreateRelatedUserStories); + }, + + createNewUserStories: async function(subject) { + el.$(".e2e-add-userstory-button").click(); + el.$(".e2e-new-userstory-label").click(); + el.$(".e2e-bulk-creation-label").click(); + el.$(".e2e-new-userstories-input-textarea").sendKeys(subject); + el.$(".e2e-create-userstory-button").click(); + await utils.lightbox.close(lightboxCreateRelatedUserStories); + }, + + selectFirstRelatedUserstory: async function() { + el.$(".e2e-add-userstory-button").click(); + el.$(".e2e-existing-user-story-label").click(); + el.$(".e2e-filter-userstories-input").click().sendKeys("#1"); + el.$$(".e2e-userstories-select option").get(1).click() + el.$(".e2e-select-related-userstory-button").click(); + await utils.lightbox.close(lightboxCreateRelatedUserStories); + }, + + deleteFirstRelatedUserstory: async function() { + let relatedUSRow = el.$$("tg-related-userstory-row").first(); + browser.actions().mouseMove(relatedUSRow).perform(); + relatedUSRow.$(".e2e-delete-userstory").click(); + await utils.lightbox.confirm.ok(); + } + }; + + return obj; +} diff --git a/e2e/helpers/epics-dashboard-helper.js b/e2e/helpers/epics-dashboard-helper.js new file mode 100644 index 00000000..787acd1c --- /dev/null +++ b/e2e/helpers/epics-dashboard-helper.js @@ -0,0 +1,200 @@ +var utils = require('../utils'); + +var helper = module.exports; + +helper.epic = function() { + let el = $$('.e2e-epic'); + + let obj = { + el: el, + getEpics: async function() { + return el.count(); + }, + createEpic: async function(date, description) { + $('.e2e-create-epic').click(); + utils.common.takeScreenshot("epics", "epics-create-epic"); + $('.e2e-create-epic-subject').clear().sendKeys(date + description); + $('.e2e-create-epic-status').click(); + $$('.e2e-create-epic-status > option').get(0).click(); + $('.e2e-create-epic-description').clear().sendKeys(date + description); + $('.e2e-create-epic-client-requirement').click(); + $('.e2e-create-epic-team-requirement').click(); + $('.e2e-create-epic-blocked').click(); + $('.e2e-create-epic-blocked-note').clear().sendKeys(date + description); + $('.e2e-create-epic-button').click(); + }, + displayUserStoriesinEpic: async function() { + utils.common.takeScreenshot("epics", "epics-child-closed"); + let storiesCount = await el.count(); + let epicChildren; + for (var i = 0; i < storiesCount; i++) { + let story = await el.get(i); + story.click(); + epicChildren = await story.$$('.e2e-story').count(); + if (epicChildren > 0) { + await utils.common.takeScreenshot("epics", "epics-child-open"); + break; + } + } + return epicChildren; + }, + getAssignedTo: async function() { + return await el.get(0).$('.e2e-assigned-to-image').getAttribute("title"); + }, + resetAssignedTo: async function() { + el.get(0).$('.e2e-assigned-to-image').click(); + $$('.e2e-assigned-to-selector').get(0).click(); + await browser.waitForAngular(); + }, + editAssignedTo: async function() { + el.get(0).$('.e2e-assigned-to-image').click(); + utils.common.takeScreenshot("epics", "epics-edit-assigned"); + $$('.e2e-assigned-to-selector').last().click(); + await browser.waitForAngular(); + }, + removeAssignedTo: async function() { + el.get(0).$('.e2e-assigned-to-image').click(); + $('.e2e-unassign').click(); + await browser.waitForAngular(); + return el.get(0).$('.e2e-assigned-to-image').getAttribute("alt"); + }, + resetStatus: async function() { + el.get(0).$('.e2e-epic-status').click(); + el.get(0).$$('.e2e-edit-epic-status').get(0).click(); + await browser.waitForAngular(); + }, + getStatus: function() { + return el.get(0).$('.e2e-epic-status').getText(); + }, + editStatus: async function() { + el.get(0).$('.e2e-epic-status').click(); + utils.common.takeScreenshot("epics", "epics-edit-status"); + el.get(0).$$('.e2e-edit-epic-status').last().click(); + await browser.waitForAngular(); + }, + getColumns: function() { + return $$('.e2e-epics-table-header > div').count(); + }, + removeColumns: async function() { + $('.e2e-epics-column-button').click(); + utils.common.takeScreenshot("epics", "epics-edit-columns"); + $$('.e2e-epics-column-dropdown .check').first().click(); + } + } + + return obj; +} + +// helper.title = function() { +// let el = $('.e2e-story-header'); +// +// let obj = { +// el: el, +// +// getTitle: function() { +// return el.$('.e2e-title-subject').getText(); +// }, +// +// setTitle: function(title) { +// el.$('.e2e-detail-edit').click(); +// el.$('.e2e-title-input').clear().sendKeys(title); +// }, +// +// save: async function() { +// el.$('.e2e-title-button').click(); +// await browser.waitForAngular(); +// } +// }; +// +// return obj; +// }; + +// +// helper.getCreateIssueLightbox = function() { +// let el = $('div[tg-lb-create-issue]'); +// +// let obj = { +// el: el, +// waitOpen: function() { +// return utils.lightbox.open(el); +// }, +// waitClose: function() { +// return utils.lightbox.close(el); +// }, +// subject: function() { +// return el.$$('input').first(); +// }, +// tags: function() { +// return el.$('.tag-input'); +// }, +// submit: function() { +// el.$('button[type="submit"]').click(); +// } +// }; +// +// return obj; +// }; +// +// helper.getBulkCreateLightbox = function() { +// let el = $('div[tg-lb-create-bulk-issues]'); +// +// let obj = { +// el: el, +// waitOpen: function() { +// return utils.lightbox.open(el); +// }, +// textarea: function() { +// return el.$('textarea'); +// }, +// submit: function() { +// el.$('button[type="submit"]').click(); +// }, +// waitClose: function() { +// return utils.lightbox.close(el); +// } +// }; +// +// return obj; +// }; +// +// helper.openNewIssueLb = function() { +// $('.new-issue .button-green').click(); +// }; +// +// helper.openBulk = function() { +// $('.new-issue .button-bulk').click(); +// }; +// +// helper.clickColumn = function(index) { +// $$('.row.title > div').get(index).click(); +// }; +// +// helper.getTable = function() { +// return $('.basic-table'); +// }; +// +// helper.openAssignTo = function(index) { +// $$('.issue-assignedto').get(index).click(); +// }; +// +// helper.getAssignTo = function(index) { +// return $$('.assigned-field figcaption').get(index).getText(); +// }; +// +// helper.clickPagination = function(index) { +// $$('.paginator li').get(index).click(); +// }; +// +// helper.getIssues = function() { +// return $$('.row.table-main'); +// }; +// +// helper.parseIssue = async function(elm) { +// let obj = {}; +// +// obj.ref = await elm.$$('.subject span').get(0).getText(); +// obj.ref = obj.ref.replace('#', ''); +// obj.subject = await elm.$$('.subject span').get(1).getText(); +// +// return obj; +// }; diff --git a/e2e/helpers/filters-helper.js b/e2e/helpers/filters-helper.js new file mode 100644 index 00000000..f317b3ea --- /dev/null +++ b/e2e/helpers/filters-helper.js @@ -0,0 +1,81 @@ +var utils = require('../utils'); + +var helper = module.exports; + +helper.getFilter = function() { + return $('tg-filter'); +}; + +helper.open = async function() { + let isPresent = await $('.e2e-open-filter').isPresent(); + + if(isPresent) { + $('.e2e-open-filter').click(); + } else { + return; + } + + var filter = helper.getFilter(); + + return utils.common.transitionend('tg-filter'); +}; + +helper.byText = function(text) { + return $('.e2e-filter-q').sendKeys(text); +}; + +helper.clearByTextInput = function() { + return utils.common.clear($('.e2e-filter-q')); +}; + +helper.clearFilters = async function() { + let filters = $$('.e2e-remove-filter'); + let filtersSize = await filters.count() + + for(var i = 0; i < filtersSize; i++) { + filters.get(i).click(); + } + + await helper.clearByTextInput(); + let isPresent = await $('.e2e-category.selected').isPresent(); + + if(isPresent) { + $('.e2e-category.selected').click(); + } +}; + +helper.getFiltersCounters = function() { + return $$('.e2e-filter-count'); +}; + +helper.getCustomFilters = function() { + return $$('.e2e-custom-filter'); +}; + +helper.firterByLastCustomFilter = function() { + helper.openCustomFiltersCategory(); + helper.getCustomFilters().last().click(); +}; + +helper.openCustomFiltersCategory = function() { + $('.e2e-custom-filters').click(); +}; + +helper.removeLastCustomFilter = function() { + $$('.e2e-remove-custom-filter').last().click(); +} + +helper.firterByCategoryWithContent = function() { + $$('.e2e-category').first().click(); + + let filter = helper.getFiltersCounters().first().element(by.xpath('..')); + + return filter.click(); +}; + +helper.saveFilter = async function(name) { + $('.e2e-open-custom-filter-form').click(); + + await $('.e2e-filter-name-input').sendKeys(name); + await $('.e2e-filter-name-input').sendKeys(protractor.Key.ENTER); +}; diff --git a/e2e/helpers/index.js b/e2e/helpers/index.js index bc497ffc..307b9daf 100644 --- a/e2e/helpers/index.js +++ b/e2e/helpers/index.js @@ -13,3 +13,5 @@ module.exports.adminPermissions = require("./admin-permissions"); module.exports.adminIntegrations = require("./admin-integrations"); module.exports.issues = require("./issues-helper"); module.exports.createProject = require("./create-project-helper"); +module.exports.epicsDashboard = require("./epics-dashboard-helper"); +module.exports.epicDetail = require("./epic-detail-helper"); diff --git a/e2e/helpers/issues-helper.js b/e2e/helpers/issues-helper.js index 2308fa26..85022ebe 100644 --- a/e2e/helpers/issues-helper.js +++ b/e2e/helpers/issues-helper.js @@ -90,57 +90,3 @@ helper.parseIssue = async function(elm) { return obj; }; - -helper.getFilterInput = function() { - return $$('sidebar[tg-issues-filters] input').get(0); -}; - -helper.filtersCats = function() { - return $$('.filters-cats li'); -}; - -helper.filtersList = function() { - return $$('.filter-list .single-filter'); -}; - -helper.selectFilter = async function(index) { - helper.filtersList().get(index).click(); -}; - -helper.saveFilter = async function(name) { - $('.filters-step-cat .save-filters').click(); - - await $('.filter-list input').sendKeys(name); - - return browser.actions().sendKeys(protractor.Key.ENTER).perform(); -}; - -helper.backToFilters = function() { - $$('.breadcrumb a').get(0).click(); -}; - -helper.removeFilters = async function() { - let count = await $$('.filters-applied .single-filter.selected').count(); - - while(count) { - $$('.single-filter.selected').get(0).$('.remove-filter').click(); - - count = await $$('.single-filter.selected').count(); - } -}; - -helper.getCustomFilters = function() { - return $$('.filter-list div[data-type="myFilters"]'); -}; - -helper.removeCustomFilters = async function() { - let count = await $$('.filter-list .remove-filter').count(); - - while(count) { - $$('.filter-list .remove-filter').get(0).click(); - - await utils.lightbox.confirm.ok(); - - count = await $$('.filter-list .remove-filter').count(); - } -}; diff --git a/e2e/helpers/kanban-helper.js b/e2e/helpers/kanban-helper.js index a40703fb..86ad9047 100644 --- a/e2e/helpers/kanban-helper.js +++ b/e2e/helpers/kanban-helper.js @@ -7,7 +7,7 @@ helper.getHeaderColumns = function() { }; helper.openNewUsLb = function(column) { - helper.getHeaderColumns().get(column).$$('.option').get(4).click(); + helper.getHeaderColumns().get(column).$$('.option').get(2).click(); }; helper.getColumns = function() { @@ -15,15 +15,31 @@ helper.getColumns = function() { }; helper.getColumnUssTitles = function(column) { - return helper.getColumns().$$('.task-name').getText(); + return helper.getColumns().$$('.e2e-title').getText(); }; helper.getBoxUss = function(column) { - return helper.getColumns().get(column).$$('.kanban-task'); + return helper.getColumns().get(column).$$('tg-card'); }; -helper.editUs = function(column, us) { - helper.getColumns().get(column).$$('.edit-us').get(us).click(); +helper.getUss = function() { + return $$('tg-card'); +}; + +helper.editUs = async function(column, us) { + let editionZone = helper.getColumns().get(column).$$('.card-owner-actions').get(us); + + await browser + .actions() + .mouseMove(editionZone) + .perform(); + + return browser + .actions() + .mouseMove(editionZone) + .mouseMove(editionZone.$('.e2e-edit')) + .click() + .perform(); }; helper.openBulkUsLb = function(column) { @@ -42,22 +58,18 @@ helper.unFoldColumn = function(column) { columnNode.$$('.options a').get(1).click(); }; -helper.foldCards = function(column) { - let columnNode = helper.getHeaderColumns().get(column); - - columnNode.$$('.options a').get(2).click(); -}; - -helper.unFoldCards = function(column) { - let columnNode = helper.getHeaderColumns().get(column); - - columnNode.$$('.options a').get(3).click(); -}; - helper.scrollRight = function() { return browser.executeScript('$(".kanban-table-body:last").scrollLeft(10000);'); }; helper.watchersLinks = function() { - return $$('.task-assigned'); + return $$('.e2e-assign'); +}; + +helper.zoom = async function(level) { + return browser + .actions() + .mouseMove($('tg-board-zoom'), {y: 14, x: level * 49}) + .click() + .perform(); }; diff --git a/e2e/helpers/project-detail-helper.js b/e2e/helpers/project-detail-helper.js index efc676f5..88f2c81a 100644 --- a/e2e/helpers/project-detail-helper.js +++ b/e2e/helpers/project-detail-helper.js @@ -58,11 +58,14 @@ helper.getChangeOwnerLb = function() { return utils.lightbox.close(el); }, search: function(q) { - el.$$('input').get(0).sendKeys(q); + return el.$$('input').get(0).sendKeys(q); }, select: function(index) { el.$$('.user-list-single').get(index).click(); }, + getUserName: function(index) { + return el.$$('.user-list-single').get(index).$('.user-list-name').getText(); + }, addComment: function(text) { el.$('.add-comment a').click(); el.$('textarea').sendKeys(text); @@ -74,3 +77,7 @@ helper.getChangeOwnerLb = function() { return obj; }; + +helper.enableAddTags = function() { + $('.add-tag-button').click(); +}; diff --git a/e2e/helpers/taskboard-helper.js b/e2e/helpers/taskboard-helper.js index 084d4f6f..7132c4fe 100644 --- a/e2e/helpers/taskboard-helper.js +++ b/e2e/helpers/taskboard-helper.js @@ -13,7 +13,11 @@ helper.getBox = function(row, column) { helper.getBoxTasks = function(row, column) { let box = helper.getBox(row, column); - return box.$$('.taskboard-task'); + return box.$$('tg-card'); +}; + +helper.getTasks = function() { + return $$('tg-card'); }; helper.openNewTaskLb = function(row) { @@ -52,8 +56,20 @@ helper.unFoldColumn = function(row) { icon.click(); }; -helper.editTask = function(row, column, task) { - helper.getBoxTasks(row, column).get(task).$('.edit-task').click(); +helper.editTask = async function(row, column, task) { + let editionZone = helper.getBoxTasks(row, column).$$('.card-owner-actions').get(task); + + await browser + .actions() + .mouseMove(editionZone) + .perform(); + + return browser + .actions() + .mouseMove(editionZone) + .mouseMove(editionZone.$('.e2e-edit')) + .click() + .perform(); }; helper.toggleGraph = function() { @@ -114,5 +130,13 @@ helper.getBulkCreateTask = function() { }; helper.watchersLinks = function() { - return $$('.task-assigned'); + return $$('.e2e-assign'); +}; + +helper.zoom = async function(level) { + return browser + .actions() + .mouseMove($('tg-board-zoom'), {y: 14, x: level * 66}) + .click() + .perform(); }; diff --git a/e2e/helpers/us-detail-helper.js b/e2e/helpers/us-detail-helper.js index c41f53af..b419d433 100644 --- a/e2e/helpers/us-detail-helper.js +++ b/e2e/helpers/us-detail-helper.js @@ -3,45 +3,6 @@ var commonHelper = require('./common-helper'); var helper = module.exports; -helper.teamRequirement = function() { - let el = $('tg-us-team-requirement-button'); - - let obj = { - el: el, - - toggleStatus: async function(){ - await el.$("label").click(); - await browser.waitForAngular(); - }, - - isRequired: async function() { - let classes = await el.$("label").getAttribute('class'); - return classes.includes("active"); - } - }; - - return obj; -}; - -helper.clientRequirement = function() { - let el = $('tg-us-client-requirement-button'); - - let obj = { - el: el, - - toggleStatus: async function(){ - await el.$("label").click(); - await browser.waitForAngular(); - }, - - isRequired: async function() { - let classes = await el.$("label").getAttribute('class'); - return classes.includes("active"); - } - }; - - return obj; -}; helper.relatedTaskForm = async function(form, name, status, assigned_to) { await form.$('input').sendKeys(name); diff --git a/e2e/helpers/wiki-helper.js b/e2e/helpers/wiki-helper.js index f883e63d..c648d0c5 100644 --- a/e2e/helpers/wiki-helper.js +++ b/e2e/helpers/wiki-helper.js @@ -3,7 +3,7 @@ var utils = require('../utils'); var helper = module.exports; helper.links = function() { - let el = $('section[tg-wiki-nav]'); + let el = $('sidebar[tg-wiki-nav]'); let obj = { el: el, @@ -13,12 +13,25 @@ helper.links = function() { el.$(".new input").sendKeys(pageTitle); browser.actions().sendKeys(protractor.Key.ENTER).perform(); await browser.waitForAngular(); - let newLink = await el.$$(".wiki-link a").last(); + let newLink = await el.$$(".e2e-wiki-page-link a").last(); return newLink; }, - get: function() { - return el.$$(".wiki-link a.link-title"); + get: function(index) { + if(index !== null && index !== undefined) { + return el.$$(".e2e-wiki-page-link a.link-title").get(index); + } + + return el.$$(".e2e-wiki-page-link a.link-title"); + }, + + row: function(index) { + return el.$$(".e2e-wiki-page-link").get(index); + }, + + getNameOf: async function(index) { + let item = await obj.get(index); + return item.getText(); }, deleteLink: async function(link){ @@ -32,12 +45,23 @@ helper.links = function() { return obj; }; +helper.dragAndDropLinks = async function(indexFrom, indexTo) { + let selectedLink = helper.links().row(indexFrom).$('.dragger'); + + let newPosition = helper.links().get(indexTo).getLocation(); + return utils.common.drag(selectedLink, newPosition); +}; + helper.editor = function(){ let el = $('.main.wiki'); let obj = { el: el, + focus: function() { + el.$("textarea").click(); + }, + enabledEditionMode: async function(){ await el.$("section[tg-editable-wiki-content] .view-wiki-content").click(); }, @@ -58,7 +82,7 @@ helper.editor = function(){ }, getInnerHtml: async function(text){ - let wikiText = await el.$(".content").getInnerHtml(); + let wikiText = await el.$(".view-wiki-content .wysiwyg").getInnerHtml(); return wikiText; }, @@ -75,7 +99,10 @@ helper.editor = function(){ await el.$(".preview-icon a").click(); await browser.waitForAngular(); }, - + closePreview: async function(){ + await el.$(".actions .wysiwyg").click(); + await browser.waitForAngular(); + }, save: async function(){ await el.$(".save").click(); await browser.waitForAngular(); diff --git a/e2e/shared/detail.js b/e2e/shared/detail.js index 898dcb24..364813c5 100644 --- a/e2e/shared/detail.js +++ b/e2e/shared/detail.js @@ -1,8 +1,10 @@ var path = require('path'); +var utils = require('../utils'); var detailHelper = require('../helpers').detail; var commonHelper = require('../helpers').common; var customFieldsHelper = require('../helpers/custom-fields-helper'); var commonUtil = require('../utils/common'); +var lightbox = require('../utils/lightbox'); var notifications = require('../utils/notifications'); var chai = require('chai'); @@ -46,21 +48,47 @@ shared.tagsTesting = async function() { expect(newtagsText).to.be.not.eql(tagsText); } -shared.descriptionTesting = async function() { - let descriptionHelper = detailHelper.description(); - let description = await descriptionHelper.getInnerHtml(); - let date = Date.now(); - descriptionHelper.enabledEditionMode(); - descriptionHelper.setText("New description " + date); - descriptionHelper.save(); +shared.descriptionTesting = function() { + it('confirm close with ESC', async function() { + let descriptionHelper = detailHelper.description(); - let newDescription = await descriptionHelper.getInnerHtml(); - let notificationOpen = await notifications.success.open(); + descriptionHelper.enabledEditionMode(); - expect(notificationOpen).to.be.equal.true; - expect(newDescription).to.be.not.equal(description); + browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); - await notifications.success.close(); + await lightbox.confirm.cancel(); + + let descriptionVisibility = await $('.edit-description').isDisplayed(); + + expect(descriptionVisibility).to.be.true; + + descriptionHelper.focus(); + + browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); + + await lightbox.confirm.ok(); + + descriptionVisibility = await $('.edit-description').isDisplayed(); + + expect(descriptionVisibility).to.be.false; + }); + + it('edit', async function() { + let descriptionHelper = detailHelper.description(); + let description = await descriptionHelper.getInnerHtml(); + let date = Date.now(); + descriptionHelper.enabledEditionMode(); + descriptionHelper.setText("New description " + date); + descriptionHelper.save(); + + let newDescription = await descriptionHelper.getInnerHtml(); + let notificationOpen = await notifications.success.open(); + + expect(notificationOpen).to.be.equal.true; + expect(newDescription).to.be.not.equal(description); + + await notifications.success.close(); + }); } shared.statusTesting = async function(status1 , status2) { @@ -164,29 +192,50 @@ shared.assignedToTesting = function() { }); } -shared.historyTesting = async function() { +shared.historyTesting = async function(screenshotsFolder) { let historyHelper = detailHelper.history(); + + //Adding a comment historyHelper.selectCommentsTab(); + await utils.common.takeScreenshot(screenshotsFolder, "show comments tab"); let commentsCounter = await historyHelper.countComments(); let date = Date.now(); - await historyHelper.addComment("New comment " + date); - let newCommentsCounter = await historyHelper.countComments(); + await historyHelper.addComment("New comment " + date); + await utils.common.takeScreenshot(screenshotsFolder, "new coment"); + + let newCommentsCounter = await historyHelper.countComments(); expect(newCommentsCounter).to.be.equal(commentsCounter+1); + //Edit last comment + historyHelper.editLastComment(); + let editComment = detailHelper.editComment(); + editComment.updateText("This is the new and updated text"); + editComment.saveComment(); + await utils.common.takeScreenshot(screenshotsFolder, "edit comment"); + + //Show versions from last comment edited + historyHelper.showVersionsLastComment(); + await utils.common.takeScreenshot(screenshotsFolder, "show comment versions"); + + historyHelper.closeVersionsLastComment(); + //Deleting last comment let deletedCommentsCounter = await historyHelper.countDeletedComments(); await historyHelper.deleteLastComment(); + let newDeletedCommentsCounter = await historyHelper.countDeletedComments(); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1); + await utils.common.takeScreenshot(screenshotsFolder, "deleted comment"); //Restore last comment deletedCommentsCounter = await historyHelper.countDeletedComments(); await historyHelper.restoreLastComment(); newDeletedCommentsCounter = await historyHelper.countDeletedComments(); expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1); + await utils.common.takeScreenshot(screenshotsFolder, "restored comment"); //Store comment with a modification commentsCounter = await historyHelper.countComments(); @@ -194,7 +243,8 @@ shared.historyTesting = async function() { historyHelper.writeComment("New comment " + date); let title = detailHelper.title(); title.setTitle('changed'); - title.save(); + await title.save(); + await utils.notifications.success.close(); newCommentsCounter = await historyHelper.countComments(); @@ -202,10 +252,11 @@ shared.historyTesting = async function() { //Check activity await historyHelper.selectActivityTab(); + await utils.common.takeScreenshot(screenshotsFolder, "show activity tab"); let activitiesCounter = await historyHelper.countActivities(); - expect(activitiesCounter).to.be.least(newCommentsCounter); + expect(newCommentsCounter).to.be.least(1); } shared.blockTesting = async function() { @@ -223,12 +274,12 @@ shared.blockTesting = async function() { let descriptionText = await $('.block-description').getText(); expect(descriptionText).to.be.equal('This is a testing block reason'); - let isDisplayed = $('.block-description').isDisplayed(); + let isDisplayed = $('.block-desc-container').isDisplayed(); expect(isDisplayed).to.be.equal.true; blockHelper.unblock(); - isDisplayed = $('.block-description').isDisplayed(); + isDisplayed = $('.block-desc-container').isDisplayed(); expect(isDisplayed).to.be.equal.false; await notifications.success.close(); @@ -240,6 +291,7 @@ shared.attachmentTesting = async function() { // Uploading attachment let attachmentsLength = await attachmentHelper.countAttachments(); + var fileToUpload = commonUtil.uploadFilePath(); await attachmentHelper.upload(fileToUpload, 'This is the testing name ' + date); @@ -262,7 +314,6 @@ shared.attachmentTesting = async function() { await attachmentHelper.renameLastAttchment('This is the new testing name ' + date); name = await attachmentHelper.getLastAttachmentName(); expect(name).to.be.equal('This is the new testing name ' + date); - // Deprecating let deprecatedAttachmentsLength = await attachmentHelper.countDeprecatedAttachments(); await attachmentHelper.deprecateLastAttachment(); @@ -288,6 +339,28 @@ shared.attachmentTesting = async function() { attachmentHelper.list(); + // Gallery images + var fileToUploadImage = commonUtil.uploadImagePath(); + + await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date); + + await attachmentHelper.upload(fileToUpload, 'testing image ' + date); + + await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date); + + attachmentHelper.attachmentLinks().last().click(); + + await attachmentHelper.previewLightbox(); + let previewSrc = await attachmentHelper.getPreviewSrc(); + + await attachmentHelper.nextPreview(); + + let previewSrc2 = await attachmentHelper.getPreviewSrc(); + + await lightbox.exit(); + + expect(previewSrc).not.to.be.equal(previewSrc2); + // Deleting attachmentsLength = await attachmentHelper.countAttachments(); await attachmentHelper.deleteLastAttachment(); @@ -476,3 +549,37 @@ shared.customFields = function(typeIndex) { expect(fieldText).to.be.equal('test text2 edit'); }); }; + +shared.teamRequirementTesting = function() { + it('team requirement edition', async function() { + let requirementHelper = detailHelper.teamRequirement(); + let isRequired = await requirementHelper.isRequired(); + + // Toggle + requirementHelper.toggleStatus(); + let newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.not.equal(newIsRequired); + + // Toggle again + requirementHelper.toggleStatus(); + newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.equal(newIsRequired); + }); +} + +shared.clientRequirementTesting = function () { + it('client requirement edition', async function() { + let requirementHelper = detailHelper.clientRequirement(); + let isRequired = await requirementHelper.isRequired(); + + // Toggle + requirementHelper.toggleStatus(); + let newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.not.equal(newIsRequired); + + // Toggle again + requirementHelper.toggleStatus(); + newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.equal(newIsRequired); + }); +} diff --git a/e2e/shared/filters.js b/e2e/shared/filters.js new file mode 100644 index 00000000..3cc193ad --- /dev/null +++ b/e2e/shared/filters.js @@ -0,0 +1,78 @@ +var filterHelper = require('../helpers/filters-helper'); +var utils = require('../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +module.exports = function(name, counter) { + before(async () => { + await filterHelper.open(); + await browser.sleep(4000); + + utils.common.takeScreenshot(name, 'filters'); + }); + + it('filter by ref', async () => { + await filterHelper.byText('xxxxyy123123123'); + + let len = await counter(); + len = await counter(); + + await filterHelper.clearFilters(); + + expect(len).to.be.equal(0); + }); + + it('filter by category', async () => { + let len = await counter(); + + await filterHelper.firterByCategoryWithContent(); + + let newLength = await counter(); + + expect(len).to.be.above(newLength); + + await filterHelper.clearFilters(); + + newLength = await counter(); + + expect(len).to.be.equal(newLength); + }); + + it('save custom filters', async () => { + let len = await counter(); + let customFiltersSize = await filterHelper.getCustomFilters().count(); + + await filterHelper.firterByCategoryWithContent(); + await filterHelper.saveFilter("custom-filter"); + await filterHelper.clearFilters(); + await filterHelper.firterByLastCustomFilter(); + + let newLength = await counter(); + let newCustomFiltersSize = await filterHelper.getCustomFilters().count(); + + expect(newLength).to.be.below(len); + expect(newCustomFiltersSize).to.be.equal(customFiltersSize + 1); + + await filterHelper.clearFilters(); + }); + + it('remove custom filters', async () => { + filterHelper.openCustomFiltersCategory(); + + let customFiltersSize = await filterHelper.getCustomFilters().count(); + + filterHelper.removeLastCustomFilter(); + + let newCustomFiltersSize = await filterHelper.getCustomFilters().count(); + + expect(newCustomFiltersSize).to.be.equal(customFiltersSize - 1); + }); + + after(async function() { + await filterHelper.clearFilters(); + }); +}; diff --git a/e2e/suites/admin/attributes/custom-fields.e2e.js b/e2e/suites/admin/attributes/custom-fields.e2e.js index fd435b8a..aef42ac7 100644 --- a/e2e/suites/admin/attributes/custom-fields.e2e.js +++ b/e2e/suites/admin/attributes/custom-fields.e2e.js @@ -16,8 +16,64 @@ describe('custom-fields', function() { }); describe('create custom fields', function() { + describe('epics', function() { + let typeIndex = 0; + + it('create', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + await customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + await customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + expect(countCustomFields).to.be.equal(oldCountCustomFields + 2); + }); + + it('edit', async function() { + customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); + + let open = await utils.notifications.success.open(); + + expect(open).to.be.true; + + await utils.notifications.success.close(); + }); + + it('drag', async function() { + let nameOld = await customFieldsHelper.getName(typeIndex, 0); + + await customFieldsHelper.drag(typeIndex, 0, 1); + + let nameNew = await customFieldsHelper.getName(typeIndex, 1); + + expect(nameNew).to.be.equal(nameOld); + }); + + it('delete', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + await customFieldsHelper.delete(typeIndex, 0); + + await browser.wait(async function() { + let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + return countCustomFields === oldCountCustomFields - 1; + }, 4000); + }); + }); + describe('userstories', function() { - let typeIndex = 0; + let typeIndex = 1; it('create', async function() { let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); @@ -42,19 +98,21 @@ describe('custom-fields', function() { it('edit', async function() { await customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 1); - let notification = await utils.notifications.success.open(); + let open = await utils.notifications.success.open(); - expect(notification).to.be.true; + expect(open).to.be.true; + + await utils.notifications.success.close(); }); - it.skip('drag', async function() { + it('drag', async function() { let nameOld = await customFieldsHelper.getName(typeIndex, 0); await customFieldsHelper.drag(typeIndex, 0, 1); - let nameNew = awcustomFieldsHelper.getName(typeIndex, 1); + let nameNew = await customFieldsHelper.getName(typeIndex, 1); - expect(nameNew).to.be.eventually.equal(nameOld); + expect(nameNew).to.be.equal(nameOld); }); it('delete', async function() { @@ -71,19 +129,16 @@ describe('custom-fields', function() { }); describe('tasks', function() { - let typeIndex = 1; + let typeIndex = 2; it('create', async function() { let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); - await customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1); - // debounce :( await utils.notifications.success.open(); await browser.sleep(2500); await customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3); - // debounce :( await utils.notifications.success.open(); await browser.sleep(2500); @@ -96,17 +151,21 @@ describe('custom-fields', function() { it('edit', async function() { customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); - expect(utils.notifications.success.open()).to.be.eventually.true; + let open = await utils.notifications.success.open(); + + expect(open).to.be.true; + + await utils.notifications.success.close(); }); - it.skip('drag', async function() { + it('drag', async function() { let nameOld = await customFieldsHelper.getName(typeIndex, 0); await customFieldsHelper.drag(typeIndex, 0, 1); - let nameNew = customFieldsHelper.getName(typeIndex, 1); + let nameNew = await customFieldsHelper.getName(typeIndex, 1); - expect(nameNew).to.be.eventually.equal(nameOld); + expect(nameNew).to.be.equal(nameOld); }); it('delete', async function() { @@ -123,7 +182,7 @@ describe('custom-fields', function() { }); describe('issues', function() { - let typeIndex = 2; + let typeIndex = 3; it('create', async function() { let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); @@ -148,17 +207,21 @@ describe('custom-fields', function() { it('edit', async function() { customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); - expect(utils.notifications.success.open()).to.be.eventually.true; + let open = await utils.notifications.success.open(); + + expect(open).to.be.true; + + await utils.notifications.success.close(); }); - it.skip('drag', async function() { + it('drag', async function() { let nameOld = await customFieldsHelper.getName(typeIndex, 0); await customFieldsHelper.drag(typeIndex, 0, 1); - let nameNew = customFieldsHelper.getName(typeIndex, 1); + let nameNew = await customFieldsHelper.getName(typeIndex, 1); - expect(nameNew).to.be.eventually.equal(nameOld); + expect(nameNew).to.be.equal(nameOld); }); it('delete', async function() { @@ -173,5 +236,6 @@ describe('custom-fields', function() { }, 4000); }); }); + }); }); diff --git a/e2e/suites/admin/attributes/points.e2e.js b/e2e/suites/admin/attributes/points.e2e.js index 1c9a5148..afe390bc 100644 --- a/e2e/suites/admin/attributes/points.e2e.js +++ b/e2e/suites/admin/attributes/points.e2e.js @@ -86,7 +86,7 @@ describe('attributes - points', function() { expect(newStatuses.indexOf(newStatusName)).to.be.not.equal(-1); }); - it.skip('drag', async function() { + it('drag', async function() { let section = adminAttributesHelper.getSection(0); let rows = section.rows(); let points = await adminAttributesHelper.getPointsNames(section.el); diff --git a/e2e/suites/admin/attributes/priorities.e2e.js b/e2e/suites/admin/attributes/priorities.e2e.js index fcbe4129..677ab263 100644 --- a/e2e/suites/admin/attributes/priorities.e2e.js +++ b/e2e/suites/admin/attributes/priorities.e2e.js @@ -84,7 +84,7 @@ describe('attributes - priorities', function() { expect(newPriorities.indexOf(newPriorityName)).to.be.not.equal(-1); }); - it.skip('drag', async function() { + it('drag', async function() { let section = adminAttributesHelper.getSection(0); let rows = section.rows(); let priorities = await adminAttributesHelper.getGenericNames(section.el); diff --git a/e2e/suites/admin/attributes/severities.e2e.js b/e2e/suites/admin/attributes/severities.e2e.js index 2078db4a..a50bfb24 100644 --- a/e2e/suites/admin/attributes/severities.e2e.js +++ b/e2e/suites/admin/attributes/severities.e2e.js @@ -84,7 +84,7 @@ describe('attributes - severities', function() { expect(newObjs.indexOf(newName)).to.be.not.equal(-1); }); - it.skip('drag', async function() { + it('drag', async function() { let section = adminAttributesHelper.getSection(0); let rows = section.rows(); let objs = await adminAttributesHelper.getGenericNames(section.el); diff --git a/e2e/suites/admin/attributes/status.e2e.js b/e2e/suites/admin/attributes/status.e2e.js index bb37dd23..d212fc9d 100644 --- a/e2e/suites/admin/attributes/status.e2e.js +++ b/e2e/suites/admin/attributes/status.e2e.js @@ -110,7 +110,7 @@ describe('attributes - status', function() { expect(newStatuses.indexOf(newStatusName)).to.be.not.equal(-1); }); - it.skip('drag', async function() { + it('drag', async function() { let section = adminAttributesHelper.getSection(0); let rows = section.rows(); let statuses = await adminAttributesHelper.getStatusNames(section.el); diff --git a/e2e/suites/admin/attributes/tags.e2e.js b/e2e/suites/admin/attributes/tags.e2e.js new file mode 100644 index 00000000..403591fd --- /dev/null +++ b/e2e/suites/admin/attributes/tags.e2e.js @@ -0,0 +1,56 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - tags', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/tags'); + + await adminAttributesHelper.waitLoad(); + + utils.common.takeScreenshot('attributes', 'tags'); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getTagsSection(0); + let rows = section.rows(); + let row = rows.get(0); + + section.edit(row); + + let form = adminAttributesHelper.getGenericForm(row.$$('form').first()); + + var colorBox = form.colorBox(); + await colorBox.click(); + await form.colorText().clear(); + await form.colorText().sendKeys('#000000'); + await browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + await browser.waitForAngular(); + + section = adminAttributesHelper.getTagsSection(0); + rows = section.rows(); + row = rows.get(0); + let backgroundColor = await row.$$('.e2e-open-color-selector').get(0).getCssValue('background-color'); + expect(backgroundColor).to.be.equal('rgba(0, 0, 0, 1)'); + utils.common.takeScreenshot('attributes', 'tag edited is black'); + }); + + it('filter', async function() { + let tagsFilter = adminAttributesHelper.getTagsFilter(); + await tagsFilter.clear(); + await tagsFilter.sendKeys('ad'); + await browser.sleep(5000); + + let section = adminAttributesHelper.getTagsSection(0); + let rows = section.rows(); + let count = await rows.count(); + expect(count).to.be.equal(2); + }); +}); diff --git a/e2e/suites/admin/attributes/types.e2e.js b/e2e/suites/admin/attributes/types.e2e.js index f16c763d..d646b6c5 100644 --- a/e2e/suites/admin/attributes/types.e2e.js +++ b/e2e/suites/admin/attributes/types.e2e.js @@ -84,7 +84,7 @@ describe('attributes - types', function() { expect(newObjs.indexOf(newName)).to.be.not.equal(-1); }); - it.skip('drag', async function() { + it('drag', async function() { let section = adminAttributesHelper.getSection(0); let rows = section.rows(); let objs = await adminAttributesHelper.getGenericNames(section.el); diff --git a/e2e/suites/admin/members.e2e.js b/e2e/suites/admin/members.e2e.js index 419e6a66..5178b139 100644 --- a/e2e/suites/admin/members.e2e.js +++ b/e2e/suites/admin/members.e2e.js @@ -50,7 +50,8 @@ describe('admin - members', function() { }); it('submit', async function() { - newMemberLightbox.submit(); + await browser.sleep(1000); + await newMemberLightbox.submit(); await newMemberLightbox.waitClose(); @@ -101,11 +102,9 @@ describe('admin - members', function() { utils.common.takeScreenshot('memberships', 'delete-owner-lb'); let isLeaveProjectWarningOpen = await adminMembershipsHelper.isLeaveProjectWarningOpen(); - expect(isLeaveProjectWarningOpen).to.be.equal(true); let lb = adminMembershipsHelper.leavingProjectWarningLb(); - await utils.lightbox.open(lb); utils.lightbox.exit(lb); diff --git a/e2e/suites/admin/project/modules.e2e.js b/e2e/suites/admin/project/modules.e2e.js index 7ad0e361..5c85e3a8 100644 --- a/e2e/suites/admin/project/modules.e2e.js +++ b/e2e/suites/admin/project/modules.e2e.js @@ -78,7 +78,7 @@ describe('modules', function() { }); it('enable videoconference', async function() { - let functionality = $$('.module').get(4); + let functionality = $$('.module').get(5); let input = functionality.$('.check input'); diff --git a/e2e/suites/admin/project/project-detail.e2e.js b/e2e/suites/admin/project/project-detail.e2e.js index e3c15366..033015a4 100644 --- a/e2e/suites/admin/project/project-detail.e2e.js +++ b/e2e/suites/admin/project/project-detail.e2e.js @@ -18,7 +18,9 @@ describe('project detail', function() { }); it('edit tag, description and project settings', async function() { - let tag = $('.tag-input'); + adminHelper.enableAddTags(); + + let tag = $('.e2e-add-tag-input'); tag.sendKeys('aaa'); browser.actions().sendKeys(protractor.Key.ENTER).perform(); @@ -105,7 +107,10 @@ describe('project detail', function() { await lb.waitOpen(); - lb.search('Alicia Flores'); + let username = lb.getUserName(0); + + await lb.search(username); + lb.select(0); lb.addComment('text'); diff --git a/e2e/suites/auth/auth.e2e.js b/e2e/suites/auth/auth.e2e.js index 0e9dbc5f..ff4557c4 100644 --- a/e2e/suites/auth/auth.e2e.js +++ b/e2e/suites/auth/auth.e2e.js @@ -43,6 +43,8 @@ describe('auth', function() { it("redirect to login", async function() { browser.get(browser.params.glob.host + path); + await utils.common.waitLoader(); + let url = await browser.getCurrentUrl(); expect(url).to.be.equal(browser.params.glob.host + 'login?next=' + encodeURIComponent('/' + path)); @@ -53,6 +55,8 @@ describe('auth', function() { $('input[name="password"]').sendKeys('123123'); $('.submit-button').click(); + await utils.common.waitLoader(); + let url = await browser.getCurrentUrl(); expect(url).to.be.equal(browser.params.glob.host + path); diff --git a/e2e/suites/backlog.e2e.js b/e2e/suites/backlog.e2e.js index 23ba0a3c..f847a4f2 100644 --- a/e2e/suites/backlog.e2e.js +++ b/e2e/suites/backlog.e2e.js @@ -5,9 +5,12 @@ var commonHelper = require('../helpers').common; var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); +var sharedFilters = require('../shared/filters'); + chai.use(chaiAsPromised); var expect = chai.expect; + describe('backlog', function() { before(async function() { browser.get(browser.params.glob.host + 'project/project-3/backlog'); @@ -47,11 +50,7 @@ describe('backlog', function() { createUSLightbox.status(2).click(); // tags - createUSLightbox.tags().sendKeys('aaa'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - createUSLightbox.tags().sendKeys('bbb'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + commonHelper.tags(); // description createUSLightbox.description().sendKeys('test test'); @@ -144,11 +143,7 @@ describe('backlog', function() { editUSLightbox.status(3).click(); // tags - editUSLightbox.tags().sendKeys('www'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - editUSLightbox.tags().sendKeys('xxx'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + editUSLightbox.tags(); // description editUSLightbox.description().sendKeys('test test test test'); @@ -166,6 +161,7 @@ describe('backlog', function() { }); }); + it('edit status inline', async function() { await backlogHelper.setUsStatus(0, 1); @@ -199,13 +195,14 @@ describe('backlog', function() { expect(newUsCount).to.be.equal(usCount - 1); }); - it.skip('drag backlog us', async function() { + it('drag backlog us', async function() { let dragableElements = backlogHelper.userStories(); - let dragElement = dragableElements.get(1); + let dragElement = dragableElements.get(4); let dragElementHandler = dragElement.$('.icon-drag'); let draggedElementRef = await backlogHelper.getUsRef(dragElement); + await utils.common.drag(dragElementHandler, dragableElements.get(0)); await browser.waitForAngular(); @@ -214,7 +211,7 @@ describe('backlog', function() { expect(firstElementTextRef).to.be.equal(draggedElementRef); }); - it.skip('reorder multiple us', async function() { + it('reorder multiple us', async function() { let dragableElements = backlogHelper.userStories(); let count = await dragableElements.count(); @@ -233,8 +230,7 @@ describe('backlog', function() { let ref2 = await backlogHelper.getUsRef(dragElement); draggedRefs.push(await backlogHelper.getUsRef(dragElement)); - await utils.common.drag(dragElement, dragableElements.get(0)); - await browser.sleep(200); + await utils.common.drag(dragElement.$('.icon-drag'), dragableElements.get(0)); let elementRef1 = await backlogHelper.getUsRef(dragableElements.get(0)); let elementRef2 = await backlogHelper.getUsRef(dragableElements.get(1)); @@ -243,7 +239,7 @@ describe('backlog', function() { expect(elementRef1).to.be.equal(draggedRefs[1]); }); - it.skip('drag multiple us to milestone', async function() { + it('drag multiple us to milestone', async function() { let sprint = backlogHelper.sprints().get(0); let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); @@ -256,7 +252,7 @@ describe('backlog', function() { let dragElement = dragableElements.get(0); let dragElementHandler = dragElement.$('.icon-drag'); - await utils.common.drag(dragElementHandler, sprint); + await utils.common.drag(dragElementHandler, sprint.$('.sprint-table')); await browser.waitForAngular(); let ussSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); @@ -264,8 +260,8 @@ describe('backlog', function() { expect(ussSprintCount).to.be.equal(initUssSprintCount + 2); }); - it.skip('drag us to milestone', async function() { - let sprint = backlogHelper.sprints().get(0); + it('drag us to milestone', async function() { + let sprint = backlogHelper.sprints().get(0).$('.sprint-table'); let dragableElements = backlogHelper.userStories(); let dragElement = dragableElements.get(0); @@ -283,7 +279,7 @@ describe('backlog', function() { expect(ussSprintCount).to.be.equal(initUssSprintCount + 1); }); - it('move to current sprint button', async function() { + it('move to lastest sprint button', async function() { let dragElement = backlogHelper.userStories().first(); dragElement.$('input[type="checkbox"]').click(); @@ -292,7 +288,7 @@ describe('backlog', function() { let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - $('#move-to-current-sprint').click(); + $('.e2e-move-to-sprint').click(); await htmlChanges(); @@ -303,7 +299,7 @@ describe('backlog', function() { expect(sprintRefs.indexOf(draggedRef)).to.be.not.equal(-1); }); - it.skip('reorder milestone us', async function() { + it('reorder milestone us', async function() { let sprint = backlogHelper.sprints().get(0); let dragableElements = backlogHelper.getSprintUsertories(sprint); @@ -318,7 +314,7 @@ describe('backlog', function() { expect(firstElementRef).to.be.equal(firstElementRef); }); - it.skip('drag us from milestone to milestone', async function() { + it('drag us from milestone to milestone', async function() { let sprint1 = backlogHelper.sprints().get(0); let sprint2 = backlogHelper.sprints().get(1); @@ -326,7 +322,7 @@ describe('backlog', function() { let dragElement = backlogHelper.getSprintUsertories(sprint1).get(0); - await utils.common.drag(dragElement, sprint2); + await utils.common.drag(dragElement, sprint2.$('.sprint-table')); await browser.waitForAngular(); let firstElement = backlogHelper.getSprintUsertories(sprint2).get(0); @@ -453,143 +449,9 @@ describe('backlog', function() { }); }); - describe('filters', function() { - it('show filters', async function() { - let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'opacity'); - - $('#show-filters-button').click(); - - await transition(); - - utils.common.takeScreenshot('backlog', 'backlog-filters'); - }); - - it('filter by subject', async function() { - let usCount = await backlogHelper.userStories().count(); - let filterQ = element(by.model('filtersQ')); - - let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - await filterQ.sendKeys('add'); - - await htmlChanges(); - - let newUsCount = await backlogHelper.userStories().count(); - - expect(newUsCount).to.be.below(usCount); - - htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - // clear status - await filterQ.clear(); - - await htmlChanges(); - }); - - it('filter by ref', async function() { - let userstories = backlogHelper.userStories(); - let filterQ = element(by.model('filtersQ')); - let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - let ref = await backlogHelper.getTestingFilterRef(); - - ref = ref.replace('#', ''); - - await filterQ.sendKeys(ref); - await htmlChanges(); - - let newUsCount = await userstories.count(); - expect(newUsCount).to.be.equal(1); - - htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - // clear status - await filterQ.clear(); - - await htmlChanges(); - }); - - it('filter by status', async function() { - let usCount = await backlogHelper.userStories().count(); - - let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - $$('.filters-cats a').first().click(); - $$('.filter-list a').first().click(); - - await htmlChanges(); - - let newUsCount = await backlogHelper.userStories().count(); - - expect(newUsCount).to.be.below(usCount); - - //remove status - htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - $$('.filters-applied a').first().click(); - - await htmlChanges(); - - newUsCount = await backlogHelper.userStories().count(); - - expect(newUsCount).to.be.equal(usCount); - - backlogHelper.goBackFilters(); - }); - - it('filter by tags', async function() { - let usCount = await backlogHelper.userStories().count(); - let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - $$('.filters-cats a').get(1).click(); - await browser.waitForAngular(); - - $$('.filter-list a').first().click(); - - await htmlChanges(); - - let newUsCount = await backlogHelper.userStories().count(); - - expect(newUsCount).to.be.below(usCount); - - //remove tags - htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); - - $$('.filters-applied a').first().click(); - - await htmlChanges(); - - newUsCount = await backlogHelper.userStories().count(); - - expect(newUsCount).to.be.equal(usCount); - }); - - it('trying drag with filters open', async function() { - let dragableElements = backlogHelper.userStories(); - let dragElement = dragableElements.get(5); - - await utils.common.drag(dragElement, dragableElements.get(0)); - - let waitErrorOpen = await utils.notifications.error.open(); - - expect(waitErrorOpen).to.be.true; - - await utils.notifications.error.close(); - }); - - it('hide filters', async function() { - let menu = $('.menu-secondary.filters-bar'); - let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'width'); - - $('#show-filters-button').click(); - - await transition(); - - let waitWidth = await menu.getCssValue('width'); - - expect(waitWidth).to.be.equal('0px'); - }); - }); + describe('backlog filters', sharedFilters.bind(this, 'backlog', () => { + return backlogHelper.userStories().count(); + })); describe('closed sprints', function() { async function createEmptyMilestone() { @@ -606,12 +468,29 @@ describe('backlog', function() { } async function dragClosedUsToMilestone() { - await backlogHelper.setUsStatus(2, 5); + //create us + backlogHelper.openNewUs(); - let dragElement = backlogHelper.userStories().get(2); + let createUSLightbox = backlogHelper.getCreateEditUsLightbox(); + + await createUSLightbox.waitOpen(); + + createUSLightbox.subject().sendKeys('subject'); + + //closed status + createUSLightbox.status(5).click(); + + createUSLightbox.submit(); + + await utils.lightbox.close(createUSLightbox.el); + + await backlogHelper.loadFullBacklog(); + + // drag us to milestone + let dragElement = backlogHelper.userStories().last(); let dragElementHandler = dragElement.$('.icon-drag'); - let sprint = backlogHelper.sprints().last(); + let sprint = backlogHelper.getClosedSprintTable(); await utils.common.drag(dragElementHandler, sprint); return browser.waitForAngular(); @@ -638,21 +517,24 @@ describe('backlog', function() { expect(closedSprints).to.be.equal(0); }); - it.skip('open sprint by drag open US to closed sprint', async function() { + it('open sprint by drag open US to closed sprint', async function() { backlogHelper.toggleClosedSprints(); - await backlogHelper.setUsStatus(1, 0); + await backlogHelper.setUsStatus(1, 1); - let dragElement = backlogHelper.userStories().get(0); + let dragElement = backlogHelper.userStories().get(1); let dragElementHandler = dragElement.$('.icon-drag'); let sprint = backlogHelper.sprints().last(); - await utils.common.drag(dragElementHandler, sprint); + + await backlogHelper.toggleSprint(sprint); + + await utils.common.drag(dragElementHandler, sprint.$('.sprint-table')); await browser.waitForAngular(); - let closedSprints = await backlogHelper.closedSprints().count(); + let closedSprints = await $('.filter-closed-sprints').isPresent(); - expect(closedSprints).to.be.equal(0); + expect(closedSprints).to.be.false; }); }); }); diff --git a/e2e/suites/discover/discover-search.e2e.js b/e2e/suites/discover/discover-search.e2e.js index 9fa5bd02..2cbb2ddf 100644 --- a/e2e/suites/discover/discover-search.e2e.js +++ b/e2e/suites/discover/discover-search.e2e.js @@ -29,8 +29,6 @@ describe('discover search', () => { discoverHelper.searchFilter(3); - await htmlChanges(); - let url = await browser.getCurrentUrl(); let projects = discoverHelper.searchProjects(); @@ -40,11 +38,14 @@ describe('discover search', () => { }); it('search by text', async () => { - discoverHelper.searchInput().sendKeys('Project Example 0'); + let projects = discoverHelper.searchProjects(); + let projectTitle = projects.get(0).$('h2 a').getText(); + + discoverHelper.searchInput().sendKeys(projectTitle); discoverHelper.sendSearch(); - let projects = discoverHelper.searchProjects(); + projects = discoverHelper.searchProjects(); expect(await projects.count()).to.be.equal(1); }); }); diff --git a/e2e/suites/epics/epic-dashboard.e2e.js b/e2e/suites/epics/epic-dashboard.e2e.js new file mode 100644 index 00000000..9eb5d606 --- /dev/null +++ b/e2e/suites/epics/epic-dashboard.e2e.js @@ -0,0 +1,76 @@ +var utils = require('../../utils'); +var epicsDashboardHelper = require('../../helpers').epicsDashboard; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('Epics Dashboard', function(){ + let epicsUrl = ''; + + before(async function(){ + await utils.nav + .init() + .project('Project Example 0') + .epics() + .go(); + + epicsUrl = await browser.getCurrentUrl(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("epics", "dashboard"); + }); + + it('display child stories', async function() { + let epic = epicsDashboardHelper.epic(); + let childStoriesNum = await epic.displayUserStoriesinEpic(); + expect(childStoriesNum).to.be.above(0); + }); + + it('create Epic', async function() { + let date = Date.now(); + let description = Math.random().toString(36).substring(7); + let epic = epicsDashboardHelper.epic(); + let currentEpicsNum = await epic.getEpics(); + await epic.createEpic(date, description); + let newEpicsNum = await epic.getEpics(); + expect(newEpicsNum).to.be.above(currentEpicsNum); + }); + + it('change epic assigned from dashboard', async function() { + let epic = epicsDashboardHelper.epic(); + await epic.resetAssignedTo(); + let currentAssigned = await epic.getAssignedTo(); + await epic.editAssignedTo(); + let newAssigned = await epic.getAssignedTo(); + expect(currentAssigned).to.be.not.equal(newAssigned); + }); + + it('remove assigned from dashboard', async function() { + let epic = epicsDashboardHelper.epic(); + await epic.resetAssignedTo(); + let unAssigned = await epic.removeAssignedTo(); + expect(unAssigned).to.be.equal('Unassigned'); + }); + + it('change status from dashboard', async function() { + let epic = epicsDashboardHelper.epic(); + await epic.resetStatus(); + let currentStatus = await epic.getStatus(); + await epic.editStatus(); + let newStatus = await epic.getStatus(); + expect(currentStatus).to.be.not.equal(newStatus); + }); + + it('remove columns from dashboard', async function() { + let epic = epicsDashboardHelper.epic(); + let currentColumns = await epic.getColumns(); + await epic.removeColumns(); + let newColumns = await epic.getColumns(); + expect(currentColumns).to.be.above(newColumns); + }); + +}) diff --git a/e2e/suites/epics/epic-detail.e2e.js b/e2e/suites/epics/epic-detail.e2e.js new file mode 100644 index 00000000..66c5e34a --- /dev/null +++ b/e2e/suites/epics/epic-detail.e2e.js @@ -0,0 +1,100 @@ +var utils = require('../../utils'); +var sharedDetail = require('../../shared/detail'); +var epicDetailHelper = require('../../helpers').epicDetail; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('Epic detail', async function(){ + let epicUrl = ''; + + before(async function(){ + await utils.nav + .init() + .project('Project Example 0') + .epics() + .epic(0) + .go(); + + epicUrl = await browser.getCurrentUrl(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("epics", "detail"); + }); + + it('color edition', async function() { + let colorEditor = epicDetailHelper.colorEditor(); + await colorEditor.open(); + await colorEditor.selectFirstColor(); + await colorEditor.open(); + await colorEditor.selectLastColor(); + await utils.common.takeScreenshot("epics", "detail color updated"); + }); + + it('title edition', sharedDetail.titleTesting); + + it('tags edition', sharedDetail.tagsTesting); + + describe('description', sharedDetail.descriptionTesting); + + describe('related userstories', function() { + let relatedUserstories = epicDetailHelper.relatedUserstories(); + it('create new user story', async function(){ + await relatedUserstories.createNewUserStory("Testing subject"); + }); + + it('create new user stories in bulk', async function(){ + await relatedUserstories.createNewUserStories("Testing subject1\nTesting subject 2"); + }); + + it('add related userstory', async function(){ + await relatedUserstories.selectFirstRelatedUserstory(); + }); + + it('delete related userstory', async function(){ + await relatedUserstories.deleteFirstRelatedUserstory(); + }) + }); + + it('status edition', sharedDetail.statusTesting.bind(this, 'Ready', 'In progress')); + + describe('assigned to edition', sharedDetail.assignedToTesting); + + describe('watchers edition', sharedDetail.watchersTesting); + + it('history', sharedDetail.historyTesting.bind(this, "epics")); + + it('block', sharedDetail.blockTesting); + + describe('team requirement edition', sharedDetail.teamRequirementTesting); + + describe('client requirement edition', sharedDetail.clientRequirementTesting); + + it('attachments', sharedDetail.attachmentTesting); + + describe('custom-fields', sharedDetail.customFields.bind(this, 0)); + + it('screenshot', async function() { + await utils.common.takeScreenshot("epics", "detail updated"); + }); + + describe('delete & redirect', function() { + it('delete', sharedDetail.deleteTesting); + + it('redirected', async function (){ + let url = await browser.getCurrentUrl(); + expect(url).not.to.be.equal(epicUrl); + }); + }); + +}); + + +/* +TODO: +# Related user stories +*/ diff --git a/e2e/suites/home.e2e.js b/e2e/suites/home.e2e.js index 43b4d352..2f7a19ea 100644 --- a/e2e/suites/home.e2e.js +++ b/e2e/suites/home.e2e.js @@ -47,7 +47,7 @@ describe('home', function() { }); }); - describe.skip("project drag and drop", function() { + describe("project drag and drop", function() { var draggedElementText; before(async function() { @@ -72,7 +72,7 @@ describe('home', function() { }); it('projects menu has the new order', async function() { - var firstElementText = await $$('div[tg-dropdown-project-list] ul a').first().getInnerHtml(); + var firstElementText = await $$('div[tg-dropdown-project-list] ul a span').first().getInnerHtml(); expect(firstElementText).to.be.equal(draggedElementText); }); diff --git a/e2e/suites/issues/issue-detail.e2e.js b/e2e/suites/issues/issue-detail.e2e.js index b713dbaa..352d6615 100644 --- a/e2e/suites/issues/issue-detail.e2e.js +++ b/e2e/suites/issues/issue-detail.e2e.js @@ -29,7 +29,7 @@ describe('Issue detail', async function(){ it('tags edition', sharedDetail.tagsTesting); - it('description edition', sharedDetail.descriptionTesting); + describe('description', sharedDetail.descriptionTesting); it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test')); @@ -37,13 +37,13 @@ describe('Issue detail', async function(){ describe('watchers edition', sharedDetail.watchersTesting); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "issues")); it('block', sharedDetail.blockTesting); it('attachments', sharedDetail.attachmentTesting); - describe('custom-fields', sharedDetail.customFields.bind(this, 2)); + describe('custom-fields', sharedDetail.customFields.bind(this, 3)); it('screenshot', async function() { await utils.common.takeScreenshot("issues", "detail updated"); @@ -57,4 +57,5 @@ describe('Issue detail', async function(){ expect(url).not.to.be.equal(issueUrl); }); }); + }); diff --git a/e2e/suites/issues/issues.e2e.js b/e2e/suites/issues/issues.e2e.js index 809d2003..264b4300 100644 --- a/e2e/suites/issues/issues.e2e.js +++ b/e2e/suites/issues/issues.e2e.js @@ -1,6 +1,7 @@ var utils = require('../../utils'); var issuesHelper = require('../../helpers').issues; var commonHelper = require('../../helpers').common; +var sharedFilters = require('../../shared/filters'); var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); @@ -37,11 +38,7 @@ describe('issues list', function() { createIssueLightbox.subject().sendKeys('subject'); // tags - await createIssueLightbox.tags().sendKeys('aaa'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - await createIssueLightbox.tags().sendKeys('bbb'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + commonHelper.tags(); }); it('upload attachments', commonHelper.lightboxAttachment); @@ -126,195 +123,7 @@ describe('issues list', function() { expect(issueUserName).to.be.equal(newUserName); }); - describe('filters', function() { - it('by ref', async function() { - let table = issuesHelper.getTable(); - let issues = issuesHelper.getIssues(); - let issue = issues.get(0); - issue = await issuesHelper.parseIssue(issue); - let filterInput = issuesHelper.getFilterInput(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - await filterInput.sendKeys(issue.ref); - await htmlChanges(); - - let newIssuesCount = await issues.count(); - - expect(newIssuesCount).to.be.equal(1); - - htmlChanges = await utils.common.outerHtmlChanges(table); - await utils.common.clear(filterInput); - await htmlChanges(); - }); - - it('by subject', async function() { - let table = issuesHelper.getTable(); - let issues = issuesHelper.getIssues(); - let issue = issues.get(0); - issue = await issuesHelper.parseIssue(issue); - let filterInput = issuesHelper.getFilterInput(); - - let oldIssuesCount = await $$('.row.table-main').count(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - await filterInput.sendKeys(issue.subject); - await htmlChanges(); - - let newIssuesCount = await issues.count(); - - expect(newIssuesCount).not.to.be.equal(oldIssuesCount); - expect(newIssuesCount).to.be.above(0); - - htmlChanges = await utils.common.outerHtmlChanges(table); - await utils.common.clear(filterInput); - await htmlChanges(); - }); - - it('by type', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(0).$('a').click(); - issuesHelper.selectFilter(0); - - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by status', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(1).$('a').click(); - issuesHelper.selectFilter(0); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by severity', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(2).$('a').click(); - issuesHelper.selectFilter(0); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by priorities', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(3).$('a').click(); - issuesHelper.selectFilter(0); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by tags', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(4).$('a').click(); - issuesHelper.selectFilter(1); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by assigned to', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(5).$('a').click(); - issuesHelper.selectFilter(0); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('by created by', async function() { - let table = issuesHelper.getTable(); - - let htmlChanges = await utils.common.outerHtmlChanges(table); - issuesHelper.filtersCats().get(6).$('a').click(); - issuesHelper.selectFilter(0); - await htmlChanges(); - - issuesHelper.backToFilters(); - - await issuesHelper.removeFilters(); - }); - - it('empty', async function() { - let table = issuesHelper.getTable(); - let htmlChanges = await utils.common.outerHtmlChanges(table); - - let filterInput = issuesHelper.getFilterInput(); - - await filterInput.sendKeys(new Date().getTime()); - - await htmlChanges(); - - let newIssuesCount = await issuesHelper.getIssues().count(); - - expect(newIssuesCount).to.be.equal(0); - - await utils.common.takeScreenshot('issues', 'empty-issues'); - await utils.common.clear(filterInput); - }); - - it('save custom filter', async function() { - issuesHelper.filtersCats().get(1).$('a').click(); - issuesHelper.selectFilter(0); - - await browser.waitForAngular(); - - await issuesHelper.saveFilter('custom'); - - let customFilters = await issuesHelper.getCustomFilters().count(); - - expect(customFilters).to.be.equal(1); - - await issuesHelper.removeFilters(); - issuesHelper.backToFilters(); - }); - - it('apply custom filter', async function() { - let table = issuesHelper.getTable(); - let htmlChanges = await utils.common.outerHtmlChanges(table); - - issuesHelper.filtersCats().get(7).$('a').click(); - - issuesHelper.selectFilter(0); - - await htmlChanges(); - - await issuesHelper.removeFilters(); - }); - - it('remove custom filter', async function() { - await issuesHelper.removeCustomFilters(); - - let customFilterCount = await issuesHelper.getCustomFilters().count(); - - expect(customFilterCount).to.be.equal(0); - }); - }); + describe('issues filters', sharedFilters.bind(this, 'issues', () => { + return issuesHelper.getIssues().count(); + })); }); diff --git a/e2e/suites/kanban.e2e.js b/e2e/suites/kanban.e2e.js index 2b2a477a..481dcf77 100644 --- a/e2e/suites/kanban.e2e.js +++ b/e2e/suites/kanban.e2e.js @@ -2,6 +2,8 @@ var utils = require('../utils'); var kanbanHelper = require('../helpers').kanban; var backlogHelper = require('../helpers').backlog; var commonHelper = require('../helpers').common; +var filterHelper = require('../helpers/filters-helper'); +var sharedFilters = require('../shared/filters'); var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); @@ -18,6 +20,24 @@ describe('kanban', function() { utils.common.takeScreenshot('kanban', 'kanban'); }); + it('zoom', async function() { + kanbanHelper.zoom(1); + await browser.sleep(1000); + utils.common.takeScreenshot('kanban', 'zoom1'); + + kanbanHelper.zoom(2); + await browser.sleep(1000); + utils.common.takeScreenshot('kanban', 'zoom2'); + + kanbanHelper.zoom(3); + await browser.sleep(1000); + utils.common.takeScreenshot('kanban', 'zoom3'); + + kanbanHelper.zoom(4); + await browser.sleep(1000); + utils.common.takeScreenshot('kanban', 'zoom4'); + }); + describe('create us', function() { let createUSLightbox = null; let formFields = {}; @@ -54,11 +74,7 @@ describe('kanban', function() { expect(totalPoints).to.be.equal('4'); // tags - createUSLightbox.tags().sendKeys('www'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - createUSLightbox.tags().sendKeys('xxx'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + commonHelper.tags(); // description createUSLightbox.description().sendKeys(formFields.description); @@ -127,11 +143,7 @@ describe('kanban', function() { expect(totalPoints).to.be.equal('4'); // tags - createUSLightbox.tags().sendKeys('www'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - createUSLightbox.tags().sendKeys('xxx'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + createUSLightbox.tags(); // description createUSLightbox.description().sendKeys(formFields.description); @@ -148,7 +160,6 @@ describe('kanban', function() { await utils.lightbox.close(createUSLightbox.el); let ussTitles = await kanbanHelper.getColumnUssTitles(0); - let findSubject = ussTitles.indexOf(formFields.subject) !== -1; expect(findSubject).to.be.true; @@ -205,34 +216,16 @@ describe('kanban', function() { expect(foldedColumns).to.be.equal(0); }); - - it('fold cars', async function() { - kanbanHelper.foldCards(0); - - utils.common.takeScreenshot('kanban', 'fold-cards'); - - let minimized = await $$('.kanban-task-minimized').count(); - - expect(minimized).to.be.above(1); - }); - - it('unfold cars', async function() { - kanbanHelper.unFoldCards(0); - - let minimized = await $$('.kanban-task-minimized').count(); - - expect(minimized).to.be.equal(0); - }); }); - it.skip('move us between columns', async function() { + it('move us between columns', async function() { let initOriginUsCount = await kanbanHelper.getBoxUss(0).count(); let initDestinationUsCount = await kanbanHelper.getBoxUss(1).count(); let usOrigin = kanbanHelper.getBoxUss(0).first(); let destination = kanbanHelper.getColumns().get(1); - await utils.common.drag(usOrigin, destination); + await utils.common.drag(usOrigin, destination, 0, 10); browser.waitForAngular(); @@ -243,7 +236,7 @@ describe('kanban', function() { expect(destinationUsCount).to.be.equal(initDestinationUsCount + 1); }); - describe.skip('archive', function() { + describe('archive', function() { it('move to archive', async function() { let initOriginUsCount = await kanbanHelper.getBoxUss(3).count(); @@ -252,7 +245,7 @@ describe('kanban', function() { await kanbanHelper.scrollRight(); - await utils.common.drag(usOrigin, destination); + await utils.common.drag(usOrigin, destination, 0, 10); browser.waitForAngular(); @@ -264,7 +257,7 @@ describe('kanban', function() { }); it('show archive', async function() { - $('.icon-open-eye').click(); + $('.e2e-archived').click(); await kanbanHelper.scrollRight(); @@ -276,7 +269,7 @@ describe('kanban', function() { }); it('close archive', async function() { - $('.icon-closed-eye').click(); + $('.e2e-archived').click(); let usCount = await kanbanHelper.getBoxUss(5).count(); @@ -297,8 +290,12 @@ describe('kanban', function() { await lightbox.waitClose(); - let usAssignedTo = await kanbanHelper.getBoxUss(0).get(0).$('.task-assigned').getText(); + let usAssignedTo = await kanbanHelper.getBoxUss(0).get(0).$('.card-owner-name').getText(); expect(assgnedToName).to.be.equal(usAssignedTo); }); + + describe('kanban filters', sharedFilters.bind(this, 'kanban', () => { + return kanbanHelper.getUss().count(); + })); }); diff --git a/e2e/suites/search.e2e.js b/e2e/suites/search.e2e.js index 7e3b86fb..84f30372 100644 --- a/e2e/suites/search.e2e.js +++ b/e2e/suites/search.e2e.js @@ -84,7 +84,7 @@ describe('search page', function() { let searchTerm = element(by.model('searchTerm')); await searchTerm.clear(); - let text = await $$('.table-main').get(0).$('a').getText(); + let text = await $$('.table-main').get(0).$$('a').first().getText(); let htmlChanges = await utils.common.outerHtmlChanges('.search-result-table-body'); diff --git a/e2e/suites/tasks/task-detail.e2e.js b/e2e/suites/tasks/task-detail.e2e.js index 850cd3e1..10389c00 100644 --- a/e2e/suites/tasks/task-detail.e2e.js +++ b/e2e/suites/tasks/task-detail.e2e.js @@ -31,7 +31,7 @@ describe('Task detail', function(){ it('tags edition', sharedDetail.tagsTesting); - it('description edition', sharedDetail.descriptionTesting); + describe('description', sharedDetail.descriptionTesting); it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test')); @@ -53,13 +53,13 @@ describe('Task detail', function(){ expect(newIsIocaine).to.be.equal(isIocaine); }); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "tasks")); it('block', sharedDetail.blockTesting); it('attachments', sharedDetail.attachmentTesting); - describe('custom-fields', sharedDetail.customFields.bind(this, 1)); + describe('custom-fields', sharedDetail.customFields.bind(this, 2)); it('screenshot', async function() { await utils.common.takeScreenshot("tasks", "detail updated"); diff --git a/e2e/suites/tasks/taskboard.e2e.js b/e2e/suites/tasks/taskboard.e2e.js index de04df4d..61a66b08 100644 --- a/e2e/suites/tasks/taskboard.e2e.js +++ b/e2e/suites/tasks/taskboard.e2e.js @@ -2,6 +2,8 @@ var utils = require('../../utils'); var backlogHelper = require('../../helpers').backlog; var taskboardHelper = require('../../helpers').taskboard; var commonHelper = require('../../helpers').common; +var filterHelper = require('../../helpers/filters-helper'); +var sharedFilters = require('../../shared/filters'); var chai = require('chai'); var chaiAsPromised = require('chai-as-promised'); @@ -21,6 +23,24 @@ describe('taskboard', function() { utils.common.takeScreenshot('taskboard', 'taskboard'); }); + it('zoom', async function() { + taskboardHelper.zoom(0); + await browser.sleep(1000); + utils.common.takeScreenshot('taskboard', 'zoom1'); + + taskboardHelper.zoom(1); + await browser.sleep(1000); + utils.common.takeScreenshot('taskboard', 'zoom1'); + + taskboardHelper.zoom(2); + await browser.sleep(1000); + utils.common.takeScreenshot('taskboard', 'zoom2'); + + taskboardHelper.zoom(3); + await browser.sleep(1000); + utils.common.takeScreenshot('taskboard', 'zoom3'); + }); + describe('create task', function() { let createTaskLightbox = null; let formFields = {}; @@ -46,11 +66,7 @@ describe('taskboard', function() { createTaskLightbox.subject().sendKeys(formFields.subject); createTaskLightbox.description().sendKeys(formFields.description); - createTaskLightbox.tags().sendKeys('aaa'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - - createTaskLightbox.tags().sendKeys('bbb'); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); + commonHelper.tags(); await createTaskLightbox.blocked().click(); await createTaskLightbox.blockedNote().sendKeys(formFields.blockedNote); @@ -65,7 +81,7 @@ describe('taskboard', function() { let tasks = taskboardHelper.getBoxTasks(0, 0); - let tasksSubject = await $$('.task-name').getText(); + let tasksSubject = await $$('.e2e-title').getText(); let findSubject = tasksSubject.indexOf(formFields.subject) !== -1; @@ -111,7 +127,7 @@ describe('taskboard', function() { let tasks = taskboardHelper.getBoxTasks(0, 0); - let tasksSubject = await $$('.task-name').getText(); + let tasksSubject = await $$('.e2e-title').getText(); let findSubject = tasksSubject.indexOf(formFields.subject) !== 1; @@ -213,7 +229,7 @@ describe('taskboard', function() { }); }); - describe.skip('move tasks', function() { + describe('move tasks', function() { it('move task between statuses', async function() { let initOriginTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); let initDestinationTaskCount = await taskboardHelper.getBoxTasks(0, 1).count(); @@ -221,9 +237,9 @@ describe('taskboard', function() { let taskOrigin = taskboardHelper.getBoxTasks(0, 0).first(); let destination = taskboardHelper.getBox(0, 1); - await utils.common.drag(taskOrigin, destination); + await utils.common.drag(taskOrigin, destination, 0, 10); - browser.waitForAngular(); + await browser.waitForAngular(); let originTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); let destinationTaskCount = await taskboardHelper.getBoxTasks(0, 1).count(); @@ -232,20 +248,19 @@ describe('taskboard', function() { expect(destinationTaskCount).to.be.equal(initDestinationTaskCount + 1); }); - // jquery ui drag bug - it.skip('move task between US\s', async function() { + it('move task between US\s', async function() { let initOriginTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); - let initDestinationTaskCount = await taskboardHelper.getBoxTasks(1, 1).count(); + let initDestinationTaskCount = await taskboardHelper.getBoxTasks(1, 0).count(); let taskOrigin = taskboardHelper.getBoxTasks(0, 0).first(); let destination = taskboardHelper.getBox(1, 0); - await utils.common.drag(taskOrigin, destination); + await utils.common.drag(taskOrigin, destination, 0, 10); - browser.waitForAngular(); + await browser.waitForAngular(); let originTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); - let destinationTaskCount = await taskboardHelper.getBoxTasks(1, 1).count(); + let destinationTaskCount = await taskboardHelper.getBoxTasks(1, 0).count(); expect(originTaskCount).to.be.equal(initOriginTaskCount - 1); expect(destinationTaskCount).to.be.equal(initDestinationTaskCount + 1); @@ -266,7 +281,7 @@ describe('taskboard', function() { await lightbox.waitClose(); - let usAssignedTo = await taskboardHelper.getBoxTasks(0, 0).get(0).$('.task-assigned').getText(); + let usAssignedTo = await taskboardHelper.getBoxTasks(0, 0).get(0).$('.card-owner-name').getText(); expect(assgnedToName).to.be.equal(usAssignedTo); }); @@ -297,4 +312,8 @@ describe('taskboard', function() { expect(open).to.be.false; }); }); + + describe('taskboard filters', sharedFilters.bind(this, 'taskboard', () => { + return taskboardHelper.getTasks().count(); + })); }); diff --git a/e2e/suites/team.e2e.js b/e2e/suites/team.e2e.js index 6da84e2e..d147cac3 100644 --- a/e2e/suites/team.e2e.js +++ b/e2e/suites/team.e2e.js @@ -9,7 +9,7 @@ var expect = chai.expect; describe('leaving project', function(){ before(async function(){ - browser.get(browser.params.glob.host + 'project/project-4/team'); + browser.get(browser.params.glob.host + 'project/project-3/team'); await utils.common.waitLoader(); }); diff --git a/e2e/suites/user-profile/user-profile-votes.e2e.js b/e2e/suites/user-profile/user-profile-votes.e2e.js index 44ede43d..f1402e8b 100644 --- a/e2e/suites/user-profile/user-profile-votes.e2e.js +++ b/e2e/suites/user-profile/user-profile-votes.e2e.js @@ -40,7 +40,7 @@ describe('user profile - votes', function() { expect(hasMoreItems).to.be.equal(true); }); - it('votes tab - filter user stories', async function() { + it('votes tab - filter epics', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(1).click(); @@ -52,7 +52,7 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('votes tab - filter tasks', async function() { + it('votes tab - filter user stories', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(2).click(); @@ -64,7 +64,7 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('votes tab - filter issues', async function() { + it('votes tab - filter tasks', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(3).click(); @@ -76,6 +76,18 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); + it('votes tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(4).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + it('votes tab - filter by query', async function() { let allItems = await $$('div[infinite-scroll] > div').count(); @@ -130,7 +142,7 @@ describe('user profile - votes', function() { expect(hasMoreItems).to.be.equal(true); }); - it('votes tab - filter user stories', async function() { + it('votes tab - filter epics', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(1).click(); @@ -142,7 +154,7 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('votes tab - filter tasks', async function() { + it('votes tab - filter user stories', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(2).click(); @@ -154,7 +166,7 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('votes tab - filter issues', async function() { + it('votes tab - filter tasks', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(3).click(); @@ -166,6 +178,18 @@ describe('user profile - votes', function() { expect(allItems).to.be.not.equal(filteredItems); }); + it('votes tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(4).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + it('votes tab - filter by query', async function() { let allItems = await $$('div[infinite-scroll] > div').count(); diff --git a/e2e/suites/user-profile/user-profile-watched.e2e.js b/e2e/suites/user-profile/user-profile-watched.e2e.js index 2b33b45a..df85a428 100644 --- a/e2e/suites/user-profile/user-profile-watched.e2e.js +++ b/e2e/suites/user-profile/user-profile-watched.e2e.js @@ -53,7 +53,8 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter user stories', async function() { + + it('watched tab - filter epics', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(2).click(); @@ -65,7 +66,7 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter tasks', async function() { + it('watched tab - filter user stories', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(3).click(); @@ -77,7 +78,7 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter issues', async function() { + it('watched tab - filter tasks', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(4).click(); @@ -89,6 +90,18 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); + it('watched tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(5).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + it('watched tab - filter by query', async function() { let allItems = await $$('div[infinite-scroll] > div').count(); @@ -155,7 +168,7 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter user stories', async function() { + it('watched tab - filter epics', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(2).click(); @@ -167,7 +180,7 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter tasks', async function() { + it('watched tab - filter user stories', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(3).click(); @@ -179,7 +192,7 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); - it('watched tab - filter issues', async function() { + it('watched tab - filter tasks', async function() { let allItems = await $('div[infinite-scroll]').getInnerHtml(); await $$('div.filters > a').get(4).click(); @@ -191,6 +204,18 @@ describe('user profile - watched', function() { expect(allItems).to.be.not.equal(filteredItems); }); + it('watched tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(5).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + it('watched tab - filter by query', async function() { let allItems = await $$('div[infinite-scroll] > div').count(); diff --git a/e2e/suites/user-stories/user-story-detail.e2e.js b/e2e/suites/user-stories/user-story-detail.e2e.js index e63d3294..1a7d28af 100644 --- a/e2e/suites/user-stories/user-story-detail.e2e.js +++ b/e2e/suites/user-stories/user-story-detail.e2e.js @@ -30,51 +30,25 @@ describe('User story detail', function(){ it('tags edition', sharedDetail.tagsTesting); - it('description edition', sharedDetail.descriptionTesting); + describe('description', sharedDetail.descriptionTesting); it('status edition', sharedDetail.statusTesting.bind(this, 'Ready', 'In progress')); describe('assigned to edition', sharedDetail.assignedToTesting); - it('team requirement edition', async function() { - let requirementHelper = usDetailHelper.teamRequirement(); - let isRequired = await requirementHelper.isRequired(); + describe('team requirement edition', sharedDetail.teamRequirementTesting); - // Toggle - requirementHelper.toggleStatus(); - let newIsRequired = await requirementHelper.isRequired(); - expect(isRequired).to.be.not.equal(newIsRequired); - - // Toggle again - requirementHelper.toggleStatus(); - newIsRequired = await requirementHelper.isRequired(); - expect(isRequired).to.be.equal(newIsRequired); - }); - - it('client requirement edition', async function() { - let requirementHelper = usDetailHelper.clientRequirement(); - let isRequired = await requirementHelper.isRequired(); - - // Toggle - requirementHelper.toggleStatus(); - let newIsRequired = await requirementHelper.isRequired(); - expect(isRequired).to.be.not.equal(newIsRequired); - - // Toggle again - requirementHelper.toggleStatus(); - newIsRequired = await requirementHelper.isRequired(); - expect(isRequired).to.be.equal(newIsRequired); - }); + describe('client requirement edition', sharedDetail.clientRequirementTesting); describe('watchers edition', sharedDetail.watchersTesting); - it('history', sharedDetail.historyTesting); + it('history', sharedDetail.historyTesting.bind(this, "user-stories")); it('block', sharedDetail.blockTesting); it('attachments', sharedDetail.attachmentTesting); - describe('custom-fields', sharedDetail.customFields.bind(this, 0)); + describe('custom-fields', sharedDetail.customFields.bind(this, 1)); describe('related tasks', function() { it('create', async function() { diff --git a/e2e/suites/wiki.e2e.js b/e2e/suites/wiki.e2e.js index f0c37d72..4f9b6e48 100644 --- a/e2e/suites/wiki.e2e.js +++ b/e2e/suites/wiki.e2e.js @@ -21,10 +21,13 @@ describe('wiki', function() { }); it('add link', async function(){ + let linkText = "Test link" + new Date().getTime(); + await wikiHelper.links().addLink(linkText); + let timestamp = new Date().getTime(); currentWiki.slug = "test-link" + timestamp; - let linkText = "Test link" + timestamp; + linkText = "Test link" + timestamp; currentWiki.link = await wikiHelper.links().addLink(linkText); }); @@ -45,6 +48,17 @@ describe('wiki', function() { expect(url).to.be.equal(browser.params.glob.host + 'project/project-0/wiki/' + currentWiki.slug); }); + utils.common.browserSkip('internet explorer', "drag & drop links", async function() { + let nameOld = await wikiHelper.links().getNameOf(0); + + await wikiHelper.dragAndDropLinks(0, 1); + + let nameNew = await wikiHelper.links().getNameOf(0); + + expect(nameNew).to.be.equal(nameOld); + + }); + it('remove link', async function() { wikiHelper.links().deleteLink(currentWiki.link); await utils.common.takeScreenshot("wiki", "deleting-the-created-link"); @@ -60,6 +74,7 @@ describe('wiki', function() { //preview wikiHelper.editor().preview(); await utils.common.takeScreenshot("wiki", "home-edition-preview"); + wikiHelper.editor().closePreview(); //save wikiHelper.editor().save(); @@ -74,6 +89,28 @@ describe('wiki', function() { await utils.common.takeScreenshot("wiki", "home-edition"); }); + it('confirm close with ESC in lightbox', async function() { + wikiHelper.editor().enabledEditionMode(); + + browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); + + await utils.lightbox.confirm.cancel(); + + let descriptionVisibility = await $('.view-wiki-content').isDisplayed(); + + expect(descriptionVisibility).to.be.false; + + wikiHelper.editor().focus(); + + browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); + + await utils.lightbox.confirm.ok(); + + descriptionVisibility = await $('.view-wiki-content').isDisplayed(); + + expect(descriptionVisibility).to.be.true; + }); + it('attachments', sharedDetail.attachmentTesting); it('delete', async function() { diff --git a/e2e/utils/common.js b/e2e/utils/common.js index 39a6bdbb..d3a3c4dc 100644 --- a/e2e/utils/common.js +++ b/e2e/utils/common.js @@ -176,41 +176,82 @@ common.prepare = function() { common.dragEnd = function(elm) { return browser.wait(async function() { - let count = await $$('.ui-sortable-helper').count(); + let count = await $$('.gu-mirror').count(); return count === 0; - }, 1000); + }, 5000); }; -common.drag = async function(elm, elm2, offset) { - // this code doesn't have sense (jquery ui + scroll drag + selenium = :( ) - await browser.actions() - .mouseMove(elm) - .mouseDown() - .perform(); +common.drag = async function(elm, elm2, extrax = 0, extray = 0) { + var drag = ` + var drag = arguments[0].origin; + var dest = arguments[0].dest; + var extrax = arguments[0].extrax; + var extray = arguments[0].extray; - await browser.actions() - .mouseMove(elm2, offset) - .perform(); + function isScrolledIntoView(el) { + var elemTop = el.getBoundingClientRect().top; + var elemBottom = el.getBoundingClientRect().bottom; - await browser.sleep(60); + var isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight); + return isVisible; + } - await browser.actions() - .mouseMove({x: 10, y: -10}) // fire jqueryui mousemove event always - .perform(); + function triggerMouseEvent (node, eventType, opts) { + var event = new CustomEvent(eventType); + event.initEvent (eventType, true, true); - await browser.sleep(60); + if(opts && opts.cords) { + event.pageX = opts.cords.x; + event.clientX = opts.cords.x; + event.pageY = opts.cords.y; + event.clientY = opts.cords.y - window.pageYOffset; + dest.scrollIntoView(); + } - await browser.actions() - .mouseMove({x: -10, y: 10}) - .perform(); + event.which = 1; - await browser.sleep(60); + node.dispatchEvent(event); + } - return browser.actions() - .mouseUp() - .perform() - .then(common.dragEnd); + if (!isScrolledIntoView(drag)) { + drag.scrollIntoView(); + } + + triggerMouseEvent(drag, "mousedown"); + + triggerMouseEvent(document.documentElement, "mousemove", { + cords: { + x: $(dest).offset().left + extrax, + y: $(dest).offset().top + extray + } + }); + + if (!isScrolledIntoView(dest)) { + dest.scrollIntoView(); + } + + triggerMouseEvent(document.documentElement, "mousemove", { + cords: { + x: $(dest).offset().left + extrax, + y: $(dest).offset().top + extray + } + }); + + triggerMouseEvent(document.documentElement, "mouseup", { + cords: { + x: $(dest).offset().left + extrax, + y: $(dest).offset().top + extray + } + }); + `; + + return browser.executeScript(drag, { + origin: elm.getWebElement(), + dest: elm2.getWebElement(), + extrax: extrax, + extray: extray + }).then(common.dragEnd); }; common.transitionend = function(selector, property) { diff --git a/e2e/utils/lightbox.js b/e2e/utils/lightbox.js index 471bb098..d88b50cd 100644 --- a/e2e/utils/lightbox.js +++ b/e2e/utils/lightbox.js @@ -4,11 +4,17 @@ var lightbox = module.exports; var transition = 300; lightbox.exit = function(el) { + if (!el) { + el = $('.lightbox.open'); + } + if (typeof el === 'string' || el instanceof String) { el = $(el); } el.$('.close').click(); + + return lightbox.close(el); }; lightbox.open = async function(el) { @@ -22,7 +28,7 @@ lightbox.open = async function(el) { return common.hasClass(el, 'open'); }, 4000); - await browser.sleep(transition); + await browser.sleep(transition + 100); if (open) { deferred.fulfill(true); @@ -34,7 +40,6 @@ lightbox.open = async function(el) { }; lightbox.close = async function(el) { - var deferred = protractor.promise.defer(); var present = true; if (typeof el == 'string' || el instanceof String) { @@ -43,22 +48,19 @@ lightbox.close = async function(el) { present = await el.isPresent(); - if (!present) { - deferred.fulfill(true); - } else { - return browser.wait(function() { - return common.hasClass(el, 'open').then(function(open) { + if (present) { + try { + await browser.wait(async function() { + let open = await common.hasClass(el, 'open'); return !open; - }); - }, 4000) - .then(function() { - return deferred.fulfill(true); - }, function() { - deferred.reject(new Error('Lightbox doesn\'t close')); - }); + }, 4000); + } catch (e) { + new Error('Lightbox doesn\'t close') + return false; + } } - return deferred.promise; + return true; }; lightbox.confirm = {}; @@ -71,3 +73,13 @@ lightbox.confirm.ok = async function() { await lightbox.close(lb); }; + + +lightbox.confirm.cancel = async function() { + let lb = $('.lightbox-generic-ask'); + await lightbox.open(lb); + + lb.$('.button-red').click(); + + await lightbox.close(lb); +}; diff --git a/e2e/utils/nav.js b/e2e/utils/nav.js index 14bce981..b3c32daa 100644 --- a/e2e/utils/nav.js +++ b/e2e/utils/nav.js @@ -46,8 +46,23 @@ var actions = { return common.waitLoader(); }, + + epics: async function() { + await common.link($('#nav-epics a')); + + return common.waitLoader(); + }, + + epic: async function(index) { + let epic = $$('.e2e-epic-row .name a').get(index); + + await common.link(epic); + + return common.waitLoader(); + }, + backlog: async function() { - await common.link($('#nav-backlog a')); + await common.link($$('#nav-backlog a').first()); return common.waitLoader(); }, @@ -75,7 +90,7 @@ var actions = { return common.waitLoader(); }, task: async function(index) { - let task = $$('div[tg-taskboard-task] a.task-name').get(index); + let task = $$('tg-card .card-title a').get(index); await common.link(task); @@ -101,6 +116,14 @@ var nav = { this.actions.push(actions.issue.bind(null, index)); return this; }, + epics: function(index) { + this.actions.push(actions.epics.bind(null, index)); + return this; + }, + epic: function(index) { + this.actions.push(actions.epic.bind(null, index)); + return this; + }, backlog: function(index) { this.actions.push(actions.backlog.bind(null, index)); return this; diff --git a/e2e/utils/notifications.js b/e2e/utils/notifications.js index 57f3af4e..9e14a857 100644 --- a/e2e/utils/notifications.js +++ b/e2e/utils/notifications.js @@ -11,7 +11,7 @@ notifications.success.open = function() { return browser .wait(function() { return common.hasClass(el, 'active'); - }, 6000) + }, 6000, "notification success open") .then(function(active) { return browser.sleep(transition).then(function() { return active; @@ -25,7 +25,7 @@ notifications.success.close = function() { return browser .wait(function() { return common.hasClass(el, 'inactive'); - }, 6000) + }, 6000, "notification success close") .then(function(active) { return browser.sleep(transition).then(function() { return active; diff --git a/gulpfile.js b/gulpfile.js index 350a09a4..3fbc02a5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -78,6 +78,7 @@ paths.modulesLocales = paths.app + "modules/**/locales/*.json"; paths.sass = [ paths.app + "**/*.scss", + "!" + paths.app + "**/*.mixin.scss", "!" + paths.app + "styles/bourbon/**/*.scss", "!" + paths.app + "styles/dependencies/**/*.scss", "!" + paths.app + "styles/extras/**/*.scss", @@ -129,6 +130,7 @@ paths.coffee_order = [ paths.app + "coffee/modules/backlog/*.coffee", paths.app + "coffee/modules/taskboard/*.coffee", paths.app + "coffee/modules/kanban/*.coffee", + paths.app + "coffee/modules/epics/*.coffee", paths.app + "coffee/modules/issues/*.coffee", paths.app + "coffee/modules/userstories/*.coffee", paths.app + "coffee/modules/tasks/*.coffee", @@ -290,7 +292,11 @@ gulp.task("css-lint-app", function() { return gulp.src(cssFiles) .pipe(gulpif(!isDeploy, cache(csslint("csslintrc.json"), { success: function(csslintFile) { - return csslintFile.csslint.success; + if (csslintFile.csslint) { + return csslintFile.csslint.success; + } else { + return false; + } }, value: function(csslintFile) { return { @@ -353,6 +359,14 @@ gulp.task("compile-themes", function(cb) { }); gulp.task("styles", function(cb) { + return runSequence("scss-lint", + "sass-compile", + ["app-css", "vendor-css"], + "main-css", + cb); +}); + +gulp.task("styles-lint", function(cb) { return runSequence("scss-lint", "sass-compile", "css-lint-app", @@ -588,7 +602,7 @@ gulp.task("watch", function() { livereload.listen(); gulp.watch(paths.jade, ["jade-watch"]); - gulp.watch(paths.sass_watch, ["styles"]); + gulp.watch(paths.sass_watch, ["styles-lint"]); gulp.watch(paths.styles_dependencies, ["styles-dependencies"]); gulp.watch(paths.svg, ["copy-svg"]); gulp.watch(paths.coffee, ["app-watch"]); diff --git a/package.json b/package.json index dc1abef9..2849caf4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "e2e": "./node_modules/.bin/babel-node run-e2e.js" }, "devDependencies": { - "angular-mocks": "1.4.7", + "angular-mocks": "1.5.5", "babel-cli": "^6.6.5", "babel-polyfill": "^6.7.4", "babel-preset-es2015": "^6.6.0", @@ -38,57 +38,58 @@ "css": "^2.2.1", "del": "^2.0.2", "express": "^4.12.0", - "glob": "^5.0.15", + "glob": "^7.0.3", "gulp": "^3.8.11", "gulp-add-src": "^0.2.0", "gulp-angular-templatecache": "^1.5.0", "gulp-autoprefixer": "^3.0.1", - "gulp-cache": "^0.3.0", + "gulp-cache": "^0.4.4", "gulp-cached": "1.1.0", "gulp-coffee": "^2.3.1", - "gulp-coffeelint": "^0.5.0", + "gulp-coffeelint": "^0.6.0", "gulp-concat": "^2.5.2", - "gulp-csslint": "^0.2.0", - "gulp-filter": "^3.0.1", + "gulp-csslint": "^0.3.0", + "gulp-filter": "^4.0.0", "gulp-flatten": "0.2.0", "gulp-if": "^2.0.0", - "gulp-imagemin": "^2.2.1", + "gulp-imagemin": "^3.0.1", "gulp-insert": "^0.5.0", "gulp-jade": "^1.0.0", - "gulp-jade-inheritance": "0.5.3", + "gulp-jade-inheritance": "0.5.5", "gulp-jsonminify": "^1.0.0", "gulp-livereload": "^3.8.1", - "gulp-minify-css": "^0.4.6", + "gulp-minify-css": "^1.2.4", "gulp-order": "^1.1.1", "gulp-plumber": "^1.0.1", "gulp-print": "^2.0.1", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.3", "gulp-sass": "^2.3.1", - "gulp-scss-lint": "0.3.6", + "gulp-scss-lint": "0.4.0", "gulp-size": "^2.0.0", - "gulp-sourcemaps": "^1.5.0", - "gulp-template": "^3.0.0", - "gulp-uglify": "~1.4.1", + "gulp-sourcemaps": "^2.0.0-alpha", + "gulp-template": "^4.0.0", + "gulp-uglify": "~1.5.3", "gulp-util": "^3.0.7", - "gulp-wrap": "^0.11.0", - "image-size": "^0.3.5", - "inquirer": "^0.10.0", + "gulp-wrap": "^0.12.0", + "image-size": "^0.5.0", + "inquirer": "^1.0.2", "jade": "^1.11.0", "karma": "^0.13.10", - "karma-chai-plugins": "^0.6.0", - "karma-chrome-launcher": "^0.2.0", - "karma-coffee-preprocessor": "^0.3.0", - "karma-mocha": "^0.2.0", + "karma-chai-plugins": "^0.7.0", + "karma-chrome-launcher": "^1.0.1", + "karma-coffee-preprocessor": "^1.0.0", + "karma-mocha": "^1.0.1", "karma-sourcemap-loader": "^0.3.4", "merge-stream": "^1.0.0", "minimist": "^1.1.1", "mocha": "^2.2.4", + "node-sass": "3.7.0", "node-uuid": "^1.4.3", - "node-sass": "3.4.2", "photoswipe": "^4.1.0", "pre-commit": "^1.0.5", - "readable-stream": "~2.0.2", + "readable-stream": "~2.1.2", + "reporter-file": "^1.0.0", "run-sequence": "^1.0.2", "sinon": "^1.14.1", "through2": "^2.0.1", @@ -99,6 +100,6 @@ ], "dependencies": { "awesomplete": "^1.0.0", - "dom-autoscroller": "^1.2.3" + "dom-autoscroller": "^1.3.1" } } diff --git a/run-e2e.js b/run-e2e.js index ffb1b9c8..56d78341 100644 --- a/run-e2e.js +++ b/run-e2e.js @@ -12,6 +12,7 @@ var suites = [ 'wiki', 'admin', 'issues', + 'epics', 'tasks', 'userProfile', 'userStories', @@ -35,7 +36,26 @@ function backup() { } function launchProtractor(suit) { - child_process.spawnSync('protractor', ['conf.e2e.js', '--suite=' + suit, '--back=' + taigaBackPath], {stdio: "inherit"}); + let protractorParams = ['conf.e2e.js', '--suite=' + suit, '--back=' + taigaBackPath]; + + var discard = [ + "_", + "s", + "a", + "b" + ]; + + for(var arg in argv) { + if (discard.indexOf(arg) === -1) { + if(typeof argv[arg] === 'boolean') { + protractorParams.push('--' + arg); + } else { + protractorParams.push('--' + arg + "=" + argv[arg]); + } + } + } + + child_process.spawnSync('protractor', protractorParams, {stdio: "inherit"}); } function restoreBackup() {