diff --git a/app/modules/feedback/feedback.service.coffee b/app/modules/feedback/feedback.service.coffee new file mode 100644 index 00000000..86b8d6f9 --- /dev/null +++ b/app/modules/feedback/feedback.service.coffee @@ -0,0 +1,12 @@ +class FeedbackService extends taiga.Service + @.$inject = ["tgLightboxFactory"] + + constructor: (@lightboxFactory) -> + + + sendFeedback: -> + @lightboxFactory.create("tg-lb-feedback", { + "class": "lightbox lightbox-feedback lightbox-generic-form" + }) + +angular.module("taigaFeedback").service("tgFeedbackService", FeedbackService) diff --git a/app/modules/home/duties/duty.directive.coffee b/app/modules/home/duties/duty.directive.coffee new file mode 100644 index 00000000..539fafd4 --- /dev/null +++ b/app/modules/home/duties/duty.directive.coffee @@ -0,0 +1,37 @@ +DutyDirective = (navurls, projectsService, $translate) -> + link = (scope, el, attrs, ctrl) -> + scope.vm = {} + scope.vm.duty = scope.duty + + scope.vm.getDutyType = () -> + if scope.vm.duty + if scope.vm.duty._name == "userstories" + return $translate.instant("COMMON.USER_STORY") + if scope.vm.duty._name == "tasks" + return $translate.instant("COMMON.TASK") + if scope.vm.duty._name == "issues" + return $translate.instant("COMMON.ISSUE") + + scope.vm.getUrl = () -> + if scope.vm.duty + ctx = { + project: projectsService.projectsById.get(String(scope.vm.duty.project)).slug + ref: scope.vm.duty.ref + } + return navurls.resolve("project-#{scope.vm.duty._name}-detail", ctx) + + scope.vm.getProjectName = () -> + if scope.vm.duty + return projectsService.projectsById.get(String(scope.vm.duty.project)).name + + directive = { + templateUrl: "home/duties/duty.html" + scope: { + "duty": "=tgDuty" + } + link: link + } + + return directive + +angular.module("taigaHome").directive("tgDuty", ["$tgNavUrls", "tgProjectsService", "$translate", DutyDirective]) diff --git a/app/modules/home/duties/duty.scss b/app/modules/home/duties/duty.scss new file mode 100644 index 00000000..20725659 --- /dev/null +++ b/app/modules/home/duties/duty.scss @@ -0,0 +1,73 @@ +.working-on, +.watching { + margin-bottom: 2rem; + .duty-single { + align-items: center; + border-bottom: 1px solid $whitish; + cursor: pointer; + display: flex; + flex-direction: row; + padding: .5rem; + &:last-child { + border: 0; + } + &.blocked { + background: rgba($red-light, .2); + .duty-type, + .duty-status { + color: $red; + } + } + } + .avatar { + flex-basis: 47px; + height: 47px; + margin-right: .5rem; + width: 47px; + } + .duty-data { + flex: 1; + margin-right: .5rem; + } + .duty-type, + .duty-status { + @extend %small; + color: $gray; + margin-right: .3rem; + } + .duty-title { + display: block; + margin-top: .25rem; + } + .duty-id { + color: $gray-light; + margin-right: .3rem; + } + .duty-project { + @extend %small; + align-self: flex-start; + color: $gray-light; + margin-left: auto; + text-align: right; + width: 120px; + } + .see-more { + display: block; + margin: 2rem 30%; + } +} +.watching-empty { + padding: 5vh; + text-align: center; + svg { + margin: 2rem auto; + max-width: 160px; + text-align: center; + path { + fill: $whitish; + } + } + p { + @extend %small; + } +} diff --git a/app/modules/home/home.directive.coffee b/app/modules/home/home.directive.coffee new file mode 100644 index 00000000..a929d370 --- /dev/null +++ b/app/modules/home/home.directive.coffee @@ -0,0 +1,26 @@ +HomeDirective = (homeService) -> + link = (scope, el, attrs, ctrl) -> + scope.vm = {} + taiga.defineImmutableProperty(scope.vm, "workInProgress", () -> homeService.workInProgress) + + scope.$watch "vm.workInProgress", (workInProgress) -> + if workInProgress.size > 0 + userStories = workInProgress.get("assignedTo").get("userStories") + tasks = workInProgress.get("assignedTo").get("tasks") + issues = workInProgress.get("assignedTo").get("issues") + scope.vm.assignedTo = userStories.concat(tasks).concat(issues) + + userStories = workInProgress.get("watching").get("userStories") + tasks = workInProgress.get("watching").get("tasks") + issues = workInProgress.get("watching").get("issues") + scope.vm.watching = userStories.concat(tasks).concat(issues) + + directive = { + templateUrl: "home/home.html" + scope: {} + link: link + } + + return directive + +angular.module("taigaHome").directive("tgHome", ["tgHomeService", HomeDirective]) diff --git a/app/modules/home/home.scss b/app/modules/home/home.scss new file mode 100644 index 00000000..a6aad27b --- /dev/null +++ b/app/modules/home/home.scss @@ -0,0 +1,24 @@ +.home-wrapper { + display: flex; + padding-top: 2rem; + .duty-summary { + flex: 1; + margin-right: 2rem; + } + .project-list { + width: 250px; + } + .see-more-projects-btn { + display: block; + margin-top: 2rem; + } + .title-bar { + @extend %title; + @extend %larger; + align-content: center; + background: $whitish; + display: flex; + margin: 0 0 .5rem; + padding: .9rem 1rem; + } +} diff --git a/app/modules/home/home.service.coffee b/app/modules/home/home.service.coffee new file mode 100644 index 00000000..a3b97fce --- /dev/null +++ b/app/modules/home/home.service.coffee @@ -0,0 +1,59 @@ +class HomeService extends taiga.Service + @.$inject = ["$q", "$tgResources", "$rootScope", "$projectUrl"] + + constructor: (@q, @rs, @rootScope, @projectUrl) -> + @.workInProgress = Immutable.Map() + @.inProgress = false + + fetchWorkInProgress: (userId) -> + if not @.inProgress + @.inProgress = true + params = { + status__is_closed: false + assigned_to: userId + } + assignedUserStoriesPromise = @rs.userstories.listInAllProjects(params).then (userstories) => + @.assignedToUserStories = userstories + + assignedTasksPromise = @rs.tasks.listInAllProjects(params).then (tasks) => + @.assignedToTasks = tasks + + assignedIssuesPromise = @rs.issues.listInAllProjects(params).then (issues) => + @.assignedToIssues = issues + + params = { + status__is_closed: false + watchers: userId + } + watchingUserStoriesPromise = @rs.userstories.listInAllProjects(params).then (userstories) => + @.watchingUserStories = userstories + + watchingTasksPromise = @rs.tasks.listInAllProjects(params).then (tasks) => + @.watchingTasks = tasks + + watchingIssuesPromise = @rs.issues.listInAllProjects(params).then (issues) => + @.watchingIssues = issues + + workPromise = @q.all([assignedUserStoriesPromise, assignedTasksPromise, + assignedIssuesPromise, watchingUserStoriesPromise, + watchingUserStoriesPromise, watchingIssuesPromise]) + + workPromise.then => + @.workInProgress = Immutable.fromJS({ + assignedTo: { + userStories: @.assignedToUserStories + tasks: @.assignedToTasks + issues: @.assignedToIssues + } + watching: { + userStories: @.watchingUserStories + tasks: @.watchingTasks + issues: @.watchingIssues + } + }) + + @.inProgress = false + + return workPromise + +angular.module("taigaHome").service("tgHomeService", HomeService) diff --git a/app/modules/home/projects/home-project-list.directive.coffee b/app/modules/home/projects/home-project-list.directive.coffee new file mode 100644 index 00000000..3bcbe7d9 --- /dev/null +++ b/app/modules/home/projects/home-project-list.directive.coffee @@ -0,0 +1,18 @@ +HomeProjectListDirective = (projectsService) -> + link = (scope, el, attrs, ctrl) -> + scope.vm = {} + + taiga.defineImmutableProperty(scope.vm, "projects", () -> projectsService.projects.get("recents")) + + scope.vm.newProject = -> + projectsService.newProject() + + directive = { + templateUrl: "home/projects/home-project-list.html" + scope: {} + link: link + } + + return directive + +angular.module("taigaHome").directive("tgHomeProjectList", ["tgProjectsService", HomeProjectListDirective]) diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade new file mode 100644 index 00000000..5fadc4d1 --- /dev/null +++ b/app/modules/home/projects/home-project-list.jade @@ -0,0 +1,18 @@ +ul.home-project-list(ng-show="vm.projects.size") + li.home-project-list-single(tg-bind-scope, tg-repeat="project in vm.projects") + a(href="#", tg-nav="project:project=project.slug") + h2.home-project-list-single-title + span.project-name(ng-bind="::project.name", title="{{ ::project.name }}") + span.private(ng-if="project.is_private", title="{{'PROJECT.PRIVATE' | translate}}") + include ../../../svg/lock.svg + p {{ ::project.description | limitTo:150 }} + span(ng-if="::project.description.length > 150") ... +a.see-more-projects-btn.button-gray(ng-show="vm.projects.size", href="#", tg-nav="projects", title="{{'PROJECT.NAVIGATION.SEE_MORE_PROJECTS' | translate}}", translate="PROJECT.NAVIGATION.SEE_MORE_PROJECTS") +section.projects-empty(ng-hide="vm.projects.size") + include ../../../svg/empty-project.svg + p You don't have any projects yet + a.create-project-btn.button-green( + href="#", + ng-click="vm.newProject()", + title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}", + translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT") diff --git a/app/modules/home/projects/home-project-list.scss b/app/modules/home/projects/home-project-list.scss new file mode 100644 index 00000000..50909a97 --- /dev/null +++ b/app/modules/home/projects/home-project-list.scss @@ -0,0 +1,71 @@ +.home-project-list { + li { + border-right: 5px solid $whitish; + cursor: pointer; + margin-bottom: .5rem; + padding: .5rem; + padding-right: 1rem; + text-overflow: ellipsis; + transition: border-color .3s linear; + &:hover { + border-color: $fresh-taiga; + p { + color: $gray; + } + .private path { + fill: $gray; + } + } + } + h2 { + @extend %text; + color: $gray; + font-size: 1.5rem; + line-height: 1.3; + margin-bottom: 0; + text-transform: none; + .project-name { + display: inline-block; + max-width: 90%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + .private { + display: inline-block; + margin-left: .3rem; + width: .5rem; + path { + fill: $gray-light; + transition: fill .3s linear; + } + } + } + p { + @extend %text; + @extend %small; + color: $gray-light; + margin: 0; + transition: color .3s linear; + } +} + +.projects-empty { + text-align: center; + svg { + height: 100px; + margin: 1rem auto; + text-align: center; + width: 100%; + path { + fill: $whitish; + } + } + p { + @extend %small; + } + .create-project-btn { + display: block; + } +} diff --git a/app/modules/projects/listing/projects-listing.directive.coffee b/app/modules/projects/listing/projects-listing.directive.coffee new file mode 100644 index 00000000..b88e72cd --- /dev/null +++ b/app/modules/projects/listing/projects-listing.directive.coffee @@ -0,0 +1,42 @@ +ProjectsListingDirective = (projectsService) -> + link = (scope, el, attrs, ctrl) -> + scope.vm = {} + itemEl = null + tdom = el.find(".js-sortable") + + tdom.sortable({ + dropOnEmpty: true + revert: 200 + axis: "y" + opacity: .95 + placeholder: 'placeholder' + }) + + tdom.on "sortstop", (event, ui) -> + itemEl = ui.item + project = itemEl.scope().project + index = itemEl.index() + + sorted_project_ids = _.map(scope.vm.projects.toArray(), (p) -> p.id) + sorted_project_ids = _.without(sorted_project_ids, project.id) + sorted_project_ids.splice(index, 0, project.id) + sortData = [] + for value, index in sorted_project_ids + sortData.push({"project_id": value, "order":index}) + + projectsService.bulkUpdateProjectsOrder(sortData) + + taiga.defineImmutableProperty(scope.vm, "projects", () -> projectsService.projects.get("all")) + + scope.vm.newProject = -> + projectsService.newProject() + + directive = { + templateUrl: "projects/listing/projects-listing.html" + scope: {} + link: link + } + + return directive + +angular.module("taigaProjects").directive("tgProjectsListing", ["tgProjectsService", ProjectsListingDirective]) diff --git a/app/modules/projects/listing/projects-listing.jade b/app/modules/projects/listing/projects-listing.jade new file mode 100644 index 00000000..452a7009 --- /dev/null +++ b/app/modules/projects/listing/projects-listing.jade @@ -0,0 +1,32 @@ +div.project-list-wrapper.centered + div.project-list-title + h1(translate="PROJECTS.MY_PROJECTS") + div.create-options + a.create-project-btn.button-green(href="#", ng-click="vm.newProject()", title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}", translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT") + span(tg-import-project-button) + a.button-blackish.import-project-button(href="", title="{{'PROJECT.NAVIGATION.TITLE_ACTION_IMPORT' | translate}}") + span.icon.icon-upload + input.import-file.hidden(type="file") + + section.project-list-section + div.project-list + ul.js-sortable + li.project-list-single(tg-bind-scope, tg-repeat="project in vm.projects") + div.project-list-single-left + div.project-list-single-title + h1 + a(href="#", tg-nav="project:project=project.slug") + h1.project-name(ng-bind="::project.name", title="{{ ::project.name }}") + span.private(ng-if="project.is_private", title="{{'PROJECT.PRIVATE' | translate}}") + include ../../../svg/lock.svg + p {{ ::project.description | limitTo:300 }} + span(ng-if="::project.description.length > 300") ... + + div.project-list-single-tags.tags-container(ng-if="::project.tags") + div.tags-block(tg-colorize-tags="project.tags", tg-colorize-tags-type="backlog") + + div.project-list-single-right + span.drag.icon.icon-drag-v + + aside.help-area + p(translate="PROJECT.HELP") diff --git a/app/modules/projects/listing/projects-listing.scss b/app/modules/projects/listing/projects-listing.scss new file mode 100644 index 00000000..3a2a5d08 --- /dev/null +++ b/app/modules/projects/listing/projects-listing.scss @@ -0,0 +1,97 @@ +.project-list-single { + border-bottom: 1px solid $whitish; + display: flex; + justify-content: center; + padding: .5rem; + position: relative; +} + +.project-list-single-left, +.project-list-single-right { + display: flex; + flex-direction: column; +} + +.project-list-single-left { + align-content: space-between; + flex: 4; + h1 { + @extend %text; + @extend %large; + display: inline-block; + margin-bottom: 0; + text-transform: none; + } + .project-name { + @extend %text; + @extend %larger; + color: $gray; + vertical-align: middle; + white-space: nowrap; + } + .private { + display: inline-block; + margin-left: .3rem; + width: .5rem; + path { + fill: $gray-light; + transition: fill .3s linear; + } + } + p { + @extend %text; + @extend %small; + color: $gray; + margin-bottom: 0; + max-width: 95%; + } + .project-list-single-tags { + align-content: flex-end; + display: flex; + flex: 3; + flex-wrap: wrap; + justify-content: flex-start; + margin-top: .5rem; + } + .tag { + align-self: flex-end; + margin-right: .5rem; + padding: .5rem; + } +} + +.project-list-single-right { + justify-content: space-between; + .project-list-single-stats { + align-self: flex-end; + display: flex; + div { + color: $gray-light; + margin-right: .5rem; + .icon { + margin-right: .2rem; + vertical-align: center; + } + } + .active { + .icon { + color: $star-fill; + } + } + } + .project-list-single-members { + align-self: flex-end; + display: flex; + flex-grow: 0; + flex-wrap: wrap; + margin-top: 1rem; + max-width: 160px; + a { + display: block; + } + img { + margin-right: .3rem; + width: 34px; + } + } +}