From 90fbaaee0e72caa8bcfd86f725de62c86fcde656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 8 May 2018 16:42:41 +0200 Subject: [PATCH] Generic lightbox form to create/edit us, tasks and issues --- app/coffee/modules/backlog/main.coffee | 25 +- app/coffee/modules/common/components.coffee | 260 ++++++++ app/coffee/modules/common/lightboxes.coffee | 573 +++++++++++------- app/coffee/modules/issues/detail.coffee | 27 +- app/coffee/modules/issues/lightboxes.coffee | 125 ---- app/coffee/modules/issues/list.coffee | 11 +- app/coffee/modules/kanban/main.coffee | 20 +- .../modules/taskboard/lightboxes.coffee | 205 ------- app/coffee/modules/taskboard/main.coffee | 24 +- app/locales/taiga/locale-en.json | 10 +- .../card/card-templates/card-data.jade | 1 - .../detail/header/detail-header.jade | 3 +- .../due-date/due-date-controller.coffee | 40 +- .../due-date-popover.directive.coffee | 69 +++ .../components/due-date/due-date-popover.jade | 16 + .../due-date/due-date.directive.coffee | 47 +- app/modules/components/due-date/due-date.scss | 63 +- app/partials/backlog/backlog.jade | 4 +- .../common/components/assigned-to-inline.jade | 65 ++ .../components/assigned-users-inline.jade | 117 ++++ .../common/lightbox/lightbox-due-date.jade | 1 + .../includes/components/backlog-row.jade | 2 +- .../includes/modules/issues-table.jade | 2 +- .../lb-create-edit-issue.jade | 40 ++ .../lb-create-edit-task.jade | 40 ++ .../lb-create-edit-us.jade | 44 ++ .../lightbox-create-edit/lb-create-edit.jade | 66 ++ .../modules/lightbox-create-issue.jade | 60 -- .../modules/lightbox-task-create-edit.jade | 92 --- .../modules/lightbox-us-create-edit.jade | 100 --- app/partials/issue/issues-detail.jade | 5 +- app/partials/issue/issues.jade | 4 +- app/partials/kanban/kanban.jade | 4 +- app/partials/task/related-task-row.jade | 1 - app/partials/task/task-detail.jade | 5 +- app/partials/taskboard/taskboard.jade | 4 +- app/partials/us/us-detail.jade | 5 +- app/styles/modules/common/lightbox.scss | 226 +++++++ 38 files changed, 1527 insertions(+), 879 deletions(-) create mode 100644 app/modules/components/due-date/due-date-popover.directive.coffee create mode 100644 app/modules/components/due-date/due-date-popover.jade create mode 100644 app/partials/common/components/assigned-to-inline.jade create mode 100644 app/partials/common/components/assigned-users-inline.jade create mode 100644 app/partials/includes/modules/lightbox-create-edit/lb-create-edit-issue.jade create mode 100644 app/partials/includes/modules/lightbox-create-edit/lb-create-edit-task.jade create mode 100644 app/partials/includes/modules/lightbox-create-edit/lb-create-edit-us.jade create mode 100644 app/partials/includes/modules/lightbox-create-edit/lb-create-edit.jade delete mode 100644 app/partials/includes/modules/lightbox-create-issue.jade delete mode 100644 app/partials/includes/modules/lightbox-task-create-edit.jade delete mode 100644 app/partials/includes/modules/lightbox-us-create-edit.jade diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index 7340cde1..8866a18f 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -541,7 +541,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return @rs.userstories.getByRef(projectId, ref).then (us) => @rs2.attachments.list("us", us.id, projectId).then (attachments) => - @rootscope.$broadcast("usform:edit", us, attachments.toJS()) + @rootscope.$broadcast("genericform:edit", { + 'objType': 'us', + 'statusList': @scope.usStatusList, + 'obj': us, + 'attachments': attachments.toJS() + }) currentLoading.finish() deleteUserStory: (us) -> @@ -566,8 +571,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F addNewUs: (type) -> switch type - when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, - @scope.project.default_us_status, @scope.usStatusList) + when "standard" then @rootscope.$broadcast("genericform:new", + { + 'objType': 'us', + 'project': @scope.project, + 'statusList': @scope.usStatusList + }) when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, @scope.project.default_us_status) @@ -575,13 +584,13 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @rootscope.$broadcast("sprintform:create", @scope.projectId) findCurrentSprint: () -> - currentDate = new Date().getTime() + 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') + 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') - return currentDate >= start && currentDate <= end + return currentDate >= start && currentDate <= end module.controller("BacklogController", BacklogController) diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index 238a31d7..14ae9316 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -525,6 +525,266 @@ module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoa AssignedToDirective]) + +############################################################################# +## Assigned to (inline) directive +############################################################################# + +AssignedToInlineDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $template +$translate, $compile, $currentUserService, avatarService) -> + link = ($scope, $el, $attrs, $model) -> + isEditable = -> + return $scope.project?.my_permissions?.indexOf($attrs.requiredPerm) != -1 + + normalizeString = (string) -> + normalizedString = string + normalizedString = normalizedString.replace("Á", "A").replace("Ä", "A").replace("À", "A") + normalizedString = normalizedString.replace("É", "E").replace("Ë", "E").replace("È", "E") + normalizedString = normalizedString.replace("Í", "I").replace("Ï", "I").replace("Ì", "I") + normalizedString = normalizedString.replace("Ó", "O").replace("Ö", "O").replace("Ò", "O") + normalizedString = normalizedString.replace("Ú", "U").replace("Ü", "U").replace("Ù", "U") + return normalizedString + + filterUsers = (text, user) -> + username = user.full_name_display.toUpperCase() + username = normalizeString(username) + text = text.toUpperCase() + text = normalizeString(text) + return _.includes(username, text) + + renderUserlist = (text) -> + users = _.clone($scope.activeUsers, true) + users = _.reject(users, {"id": $scope.selected.id}) if $scope.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) + + $scope.users = _.slice(users, 0, 5) + $scope.showMore = users.length > 5 + + renderUser = (assignedObject) -> + if assignedObject?.assigned_to + $scope.selected = assignedObject.assigned_to + assignedObject.assigned_to_extra_info = $scope.usersById[$scope.selected] + $scope.fullName = assignedObject.assigned_to_extra_info?.full_name_display + $scope.isUnassigned = false + $scope.avatar = avatarService.getAvatar(assignedObject.assigned_to_extra_info) + $scope.bg = $scope.avatar.bg + $scope.isIocaine = assignedObject?.is_iocaine + else + $scope.fullName = $translate.instant("COMMON.ASSIGNED_TO.ASSIGN") + $scope.isUnassigned = true + $scope.avatar = avatarService.getAvatar(null) + $scope.bg = null + $scope.isIocaine = false + + $scope.fullNameVisible = !($scope.isUnassigned && !$currentUserService.isAuthenticated()) + $scope.isEditable = isEditable() + + $el.on "click", ".users-dropdown", (event) -> + event.preventDefault() + event.stopPropagation() + renderUserlist() + $scope.$apply() + $el.find(".pop-users").popover().open() + + $el.on "click", ".users-search", (event) -> + event.stopPropagation() + + $el.on "click", ".assign-to-me", (event) -> + event.preventDefault() + return if not isEditable() + $model.$modelValue.assigned_to = $currentUserService.getUser().get('id') + renderUser($model.$modelValue) + $scope.$apply() + + $el.on "click", ".remove-user", (event) -> + event.preventDefault() + return if not isEditable() + $model.$modelValue.assigned_to = null + renderUser() + $scope.$apply() + + $scope.$watch "usersSearch", (searchingText) -> + if searchingText? + renderUserlist(searchingText) + $el.find('input').focus() + + $el.on "click", ".user-list-single", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + $model.$modelValue.assigned_to = target.data("user-id") + renderUser($model.$modelValue) + $scope.$apply() + + $scope.$watch $attrs.ngModel, (instance) -> + renderUser(instance) + + $scope.$on "isiocaine:changed", (ctx, instance) -> + renderUser(instance) + + $scope.$on "$destroy", -> + $el.off() + + return { + link:link, + require:"ngModel", + templateUrl: "common/components/assigned-to-inline.html" + } + +module.directive("tgAssignedToInline", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading" +"$tgQueueModelTransformation", "$tgTemplate", "$translate", "$compile","tgCurrentUserService" +"tgAvatarService", AssignedToInlineDirective]) + + +############################################################################# +## Assigned users (inline) directive +############################################################################# + +AssignedUsersInlineDirective = ($rootscope, $confirm, $repo, $loading, $modelTransform, $template +$translate, $compile, $currentUserService, avatarService) -> + link = ($scope, $el, $attrs, $model) -> + currentAssignedIds = [] + currentAssignedTo = null + + isAssigned = -> + return currentAssignedIds.length > 0 + + normalizeString = (string) -> + normalizedString = string + normalizedString = normalizedString.replace("Á", "A").replace("Ä", "A").replace("À", "A") + normalizedString = normalizedString.replace("É", "E").replace("Ë", "E").replace("È", "E") + normalizedString = normalizedString.replace("Í", "I").replace("Ï", "I").replace("Ì", "I") + normalizedString = normalizedString.replace("Ó", "O").replace("Ö", "O").replace("Ò", "O") + normalizedString = normalizedString.replace("Ú", "U").replace("Ü", "U").replace("Ù", "U") + return normalizedString + + filterUsers = (text, user) -> + username = user.full_name_display.toUpperCase() + username = normalizeString(username) + text = text.toUpperCase() + text = normalizeString(text) + return _.includes(username, text) + + renderUsersList = (text) -> + users = _.clone($scope.activeUsers, true) + users = _.sortBy(users, (o) -> if o.id is $scope.user.id then 0 else o.id) + users = _.filter(users, _.partial(filterUsers, text)) if text? + + # Add selected users + selected = [] + _.map users, (user) -> + if user.id in currentAssignedIds + user.avatar = avatarService.getAvatar(user) + selected.push(user) + + # Filter users in searchs + visible = [] + _.map users, (user) -> + if user.id not in currentAssignedIds + user.avatar = avatarService.getAvatar(user) + visible.push(user) + + $scope.selected = _.slice(selected, 0, 5) + if $scope.selected.length < 5 + $scope.users = _.slice(visible, 0, 5 - $scope.selected.length) + else + $scope.users = [] + $scope.showMore = users.length > 5 + + renderUsers = () -> + assignedUsers = _.map(currentAssignedIds, (assignedUserId) -> $scope.usersById[assignedUserId]) + assignedUsers = _.filter assignedUsers, (it) -> return !!it + + $scope.hiddenUsers = if currentAssignedIds.length > 3 then currentAssignedIds.length - 3 else 0 + $scope.assignedUsers = _.slice(assignedUsers, 0, 3) + + $scope.isAssigned = isAssigned() + + applyToModel = () -> + _.map currentAssignedIds, (userId) -> + if !$scope.usersById[userId] + currentAssignedIds.splice(currentAssignedIds.indexOf(userId), 1) + if currentAssignedIds.length == 0 + currentAssignedTo = null + else if currentAssignedIds.indexOf(currentAssignedTo) == -1 || !currentAssignedTo + currentAssignedTo = currentAssignedIds[0] + $model.$modelValue.setAttr('assigned_users', currentAssignedIds) + $model.$modelValue.assigned_to = currentAssignedTo + + $el.on "click", ".users-dropdown", (event) -> + event.preventDefault() + event.stopPropagation() + renderUsersList() + $scope.$apply() + $el.find(".pop-users").popover().open() + + $el.on "click", ".users-search", (event) -> + event.stopPropagation() + + $el.on "click", ".assign-to-me", (event) -> + event.preventDefault() + currentAssignedIds.push($currentUserService.getUser().get('id')) + renderUsers() + applyToModel() + $scope.usersSearch = null + $scope.$apply() + + $scope.$watch "usersSearch", (searchingText) -> + if searchingText? + renderUsersList(searchingText) + $el.find('input').focus() + + $el.on "click", ".user-list-single", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + index = currentAssignedIds.indexOf(target.data("user-id")) + if index == -1 + currentAssignedIds.push(target.data("user-id")) + else + currentAssignedIds.splice(index, 1) + renderUsers() + applyToModel() + $el.find(".pop-users").popover().close() + $scope.usersSearch = null + $scope.$apply() + + $el.on "click", ".remove-user", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + index = currentAssignedIds.indexOf(target.data("user-id")) + if index > -1 + currentAssignedIds.splice(index, 1) + renderUsers() + applyToModel() + $scope.$apply() + + $scope.$watch $attrs.ngModel, (item) -> + return if not item? + currentAssignedIds = [] + assigned_to = null + + if item.assigned_users? + currentAssignedIds = item.assigned_users + assigned_to = item.assigned_to + renderUsers() + + $scope.$on "$destroy", -> + $el.off() + + return { + link:link, + require: "ngModel", + templateUrl: "common/components/assigned-users-inline.html" + } + +module.directive("tgAssignedUsersInline", ["$rootScope", "$tgConfirm", "$tgRepo", +"$tgLoading", "$tgQueueModelTransformation", "$tgTemplate", "$translate", "$compile", +"tgCurrentUserService", "tgAvatarService", AssignedUsersInlineDirective]) + + ############################################################################# ## Block Button directive ############################################################################# diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index 8b203d32..15df7813 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -294,237 +294,6 @@ BlockingMessageInputDirective = ($log, $template, $compile) -> module.directive("tgBlockingMessageInput", ["$log", "$tgTemplate", "$compile", BlockingMessageInputDirective]) -############################################################################# -## Create/Edit Userstory Lightbox Directive -############################################################################# - -CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading, $translate, $confirm, $q, attachmentsService) -> - link = ($scope, $el, attrs) -> - form = null - $scope.createEditUs = {} - $scope.isNew = true - - attachmentsToAdd = Immutable.List() - attachmentsToDelete = Immutable.List() - - resetAttachments = () -> - attachmentsToAdd = Immutable.List() - attachmentsToDelete = Immutable.List() - - $scope.addAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.push(attachment) - - $scope.deleteAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.filter (it) -> - return it.get('name') != attachment.get('name') - - 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([value , 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 - $scope.isNew = true - $scope.usStatusList = statusList - $scope.attachments = Immutable.List() - - resetAttachments() - - $scope.us = $model.make_model("userstories", { - project: projectId - points : {} - status: status - is_archived: false - tags: [] - subject: "" - description: "" - }) - - # Update texts for creation - $el.find(".button-green").html($translate.instant("COMMON.CREATE")) - $el.find(".title").html($translate.instant("LIGHTBOX.CREATE_EDIT_US.NEW_US")) - $el.find(".tag-input").val("") - - $el.find(".blocked-note").addClass("hidden") - $el.find("label.blocked").removeClass("selected") - $el.find("label.team-requirement").removeClass("selected") - $el.find("label.client-requirement").removeClass("selected") - - $scope.createEditUsOpen = true - - lightboxService.open $el, () -> - $scope.createEditUsOpen = false - - $scope.$on "usform:edit", (ctx, us, attachments) -> - form.reset() if form - - $scope.us = us - $scope.attachments = Immutable.fromJS(attachments) - $scope.isNew = false - - resetAttachments() - - # Update texts for edition - $el.find(".button-green").html($translate.instant("COMMON.SAVE")) - $el.find(".title").html($translate.instant("LIGHTBOX.CREATE_EDIT_US.EDIT_US")) - $el.find(".tag-input").val("") - - # Update requirement info (team, client or blocked) - if us.is_blocked - $el.find(".blocked-note").removeClass("hidden") - $el.find("label.blocked").addClass("selected") - else - $el.find(".blocked-note").addClass("hidden") - $el.find("label.blocked").removeClass("selected") - - if us.team_requirement - $el.find("label.team-requirement").addClass("selected") - else - $el.find("label.team-requirement").removeClass("selected") - if us.client_requirement - $el.find("label.client-requirement").addClass("selected") - else - $el.find("label.client-requirement").removeClass("selected") - - $scope.createEditUsOpen = true - - lightboxService.open $el, () -> - $scope.createEditUsOpen = false - - createAttachments = (obj) -> - promises = _.map attachmentsToAdd.toJS(), (attachment) -> - attachmentsService.upload(attachment.file, obj.id, $scope.us.project, 'us') - - return $q.all(promises) - - deleteAttachments = (obj) -> - promises = _.map attachmentsToDelete.toJS(), (attachment) -> - return attachmentsService.delete("us", attachment.id) - - return $q.all(promises) - - submit = debounce 2000, (event) => - event.preventDefault() - - form = $el.find("form").checksley() - if not form.validate() - return - - currentLoading = $loading() - .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, true) - broadcastEvent = "usform:edit:success" - - promise.then (data) -> - deleteAttachments(data) - .then () => createAttachments(data) - .then () => - currentLoading.finish() - lightboxService.close($el) - - $rs.userstories.getByRef(data.project, data.ref, params).then (us) -> - $rootScope.$broadcast(broadcastEvent, us) - - - promise.then null, (data) -> - currentLoading.finish() - form.setErrors(data) - if data._error_message - $confirm.notify("error", data._error_message) - - submitButton = $el.find(".submit-button") - - close = () => - if !$scope.us.isModified() - lightboxService.close($el) - $scope.$apply -> - $scope.us.revert() - else - $confirm.ask($translate.instant("LIGHTBOX.CREATE_EDIT_US.CONFIRM_CLOSE")).then (result) -> - lightboxService.close($el) - $scope.us.revert() - result.finish() - - $el.on "submit", "form", submit - - $el.find('.close').on "click", (event) -> - event.preventDefault() - event.stopPropagation() - close() - - $el.keydown (event) -> - event.stopPropagation() - code = if event.keyCode then event.keyCode else event.which - if code == 27 - close() - - $scope.$on "$destroy", -> - $el.find('.close').off() - $el.off() - - return {link: link} - -module.directive("tgLbCreateEditUserstory", [ - "$tgRepo", - "$tgModel", - "$tgResources", - "$rootScope", - "lightboxService", - "$tgLoading", - "$translate", - "$tgConfirm", - "$q", - "tgAttachmentsService" - CreateEditUserstoryDirective -]) - - ############################################################################# ## Creare Bulk Userstories Lightbox Directive ############################################################################# @@ -938,6 +707,17 @@ SetDueDateDirective = (lightboxService, $loading, $translate, $confirm, $modelTr .target($el.find(".submit-button")) .start() + if $scope.notAutoSave + new_due_date = $('.due-date').val() + $scope.object.due_date = if (new_due_date) \ + then moment(new_due_date, prettyDate).format("YYYY-MM-DD") \ + else null + + $scope.$apply() + currentLoading.finish() + lightboxService.close($el) + return + transform = $modelTransform.save (object) -> new_due_date = $('.due-date').val() object.due_date = if (new_due_date) \ @@ -968,7 +748,11 @@ SetDueDateDirective = (lightboxService, $loading, $translate, $confirm, $modelTr askResponse.finish() $('.due-date').val(null) $scope.object.due_date_reason = null - save() + if $scope.notAutoSave + $scope.object.due_date = null + lightboxService.close($el) + else + save() $el.on "click", ".delete-due-date", (event) -> event.preventDefault() @@ -982,3 +766,328 @@ SetDueDateDirective = (lightboxService, $loading, $translate, $confirm, $modelTr module.directive("tgLbSetDueDate", ["lightboxService", "$tgLoading", "$translate", "$tgConfirm" "$tgQueueModelTransformation", SetDueDateDirective]) + + + +############################################################################# +## Create/Edit Lightbox Directive +############################################################################# + +CreateEditDirective = ( +$log, $repo, $model, $rs, $rootScope, lightboxService, $loading, $translate, +$confirm, $q, attachmentsService) -> + link = ($scope, $el, attrs) -> + form = null + schemas = { + us: { + objName: 'User Story', + model: 'userstories', + params: { include_attachments: true, include_tasks: true }, + requiredAttrs: ['project'], + initialData: (data) -> + return { + project: data.project.id + points : {} + status: data.project.default_us_status + is_archived: false + tags: [] + subject: "" + description: "" + } + } + task: { + objName: 'Task', + model: 'tasks', + params: { include_attachments: true }, + requiredAttrs: ['project', 'sprintId', 'usId'], + initialData: (data) -> + return { + project: data.project.id + milestone: data.sprintId + user_story: data.usId + is_archived: false + status: data.project.default_task_status + assigned_to: null + tags: [], + subject: "", + description: "", + } + }, + issue: { + objName: 'Issue', + model: 'issues', + params: { include_attachments: true }, + requiredAttrs: ['project', 'typeList', 'typeById', 'severityList', 'priorityList'], + initialData: (data) -> + return { + project: data.project.id + subject: "" + status: data.project.default_issue_status + type: data.project.default_issue_type + priority: data.project.default_priority + severity: data.project.default_severity + tags: [] + } + } + } + + attachmentsToAdd = Immutable.List() + attachmentsToDelete = Immutable.List() + + $scope.$on "genericform:new", (ctx, data) -> + if beforeMount('new', data, ['objType', 'statusList', ]) + mountCreateForm(data) + afterMount() + + $scope.$on "genericform:edit", (ctx, data) -> + if beforeMount('edit', data, ['objType', 'statusList', 'obj', 'attachments']) + mountUpdateForm(data) + afterMount() + + beforeMount = (mode, data, requiredAttrs) -> + form.reset() if form + $el.find(".tag-input").val("") + + # Get form schema + if !data.objType || !schemas[data.objType] + return $log.error( + "Invalid objType `#{data.objType}` for `genericform:#{mode}` event") + $scope.schema = schemas[data.objType] + + # Get required attrs for creation from objType schema + if mode == 'new' + requiredAttrs = $scope.schema.requiredAttrs.concat(requiredAttrs) + + # Check if required attrs for creating are present + getAttrs(mode, data, requiredAttrs) + + return true + + mountCreateForm = (data) -> + $scope.obj = $model.make_model($scope.schema.model, $scope.schema.initialData(data)) + $scope.isNew = true + $scope.attachments = Immutable.List() + + # Update texts for creation + $el.find(".button-green").html($translate.instant("COMMON.CREATE")) + $el.find(".title").html($translate.instant( + "LIGHTBOX.CREATE_EDIT.NEW", { objName: $scope.schema.objName })) + $el.find(".blocked-note").addClass("hidden") + + mountUpdateForm = (data) -> + $scope.isNew = false + $scope.attachments = Immutable.fromJS($scope.attachments) + + # Update texts for edition + $el.find(".button-green").html($translate.instant("COMMON.SAVE")) + $el.find(".title").html($translate.instant( + "LIGHTBOX.CREATE_EDIT.EDIT", { objName: $scope.schema.objName })) + + afterMount = () -> + resetAttachments() + setStatus($scope.obj.status) + $scope.createEditOpen = true + lightboxService.open $el, () -> + $scope.createEditOpen = false + + getAttrs = (mode, data, attrs) -> + for attr in attrs + if !data[attr] + return $log.error "`#{attr}` attribute required in `genericform:#{mode}` event" + $scope[attr] = data[attr] + + resetAttachments = () -> + attachmentsToAdd = Immutable.List() + attachmentsToDelete = Immutable.List() + + $scope.addAttachment = (attachment) -> + attachmentsToAdd = attachmentsToAdd.push(attachment) + + $scope.deleteAttachment = (attachment) -> + attachmentsToAdd = attachmentsToAdd.filter (it) -> + return it.get('name') != attachment.get('name') + + 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.obj.tags) + + inserted = _.find itemtags, (it) -> it[0] == value + + if !inserted + itemtags.push([value , color]) + $scope.obj.tags = itemtags + + $scope.deleteTag = (tag) -> + value = trim(tag[0].toLowerCase()) + + tags = $scope.project.tags + itemtags = _.clone($scope.obj.tags) + + _.remove itemtags, (tag) -> tag[0] == value + + $scope.obj.tags = itemtags + + _.pull($scope.obj.tags, value) + + + createAttachments = (obj) -> + promises = _.map attachmentsToAdd.toJS(), (attachment) -> + attachmentsService.upload( + attachment.file, obj.id, $scope.obj.project, $scope.objType) + + return $q.all(promises) + + deleteAttachments = (obj) -> + promises = _.map attachmentsToDelete.toJS(), (attachment) -> + return attachmentsService.delete($scope.objType, attachment.id) + + return $q.all(promises) + + submit = debounce 2000, (event) -> + event.preventDefault() + + form = $el.find("form").checksley() + if not form.validate() + return + + currentLoading = $loading().target(submitButton).start() + + if $scope.isNew + promise = $repo.create($scope.schema.model, $scope.obj) + broadcastEvent = "#{$scope.objType}form:new:success" + else + promise = $repo.save($scope.obj, true) + broadcastEvent = "#{$scope.objType}form:edit:success" + + promise.then (data) -> + deleteAttachments(data) + .then () -> createAttachments(data) + .then () -> + currentLoading.finish() + lightboxService.close($el) + + $rs[$scope.schema.model].getByRef( + data.project, data.ref, $scope.schema.params).then (obj) -> + $rootScope.$broadcast(broadcastEvent, obj) + + promise.then null, (data) -> + currentLoading.finish() + form.setErrors(data) + if data._error_message + $confirm.notify("error", data._error_message) + + submitButton = $el.find(".submit-button") + + close = () -> + if !$scope.obj.isModified() + lightboxService.close($el) + $scope.$apply -> + $scope.obj.revert() + else + $confirm.ask( + $translate.instant("LIGHTBOX.CREATE_EDIT.CONFIRM_CLOSE")).then (result) -> + lightboxService.close($el) + $scope.obj.revert() + result.finish() + + $el.on "submit", "form", submit + + $el.find('.close').on "click", (event) -> + event.preventDefault() + event.stopPropagation() + close() + + $el.keydown (event) -> + event.stopPropagation() + code = if event.keyCode then event.keyCode else event.which + if code == 27 + close() + + $scope.$on "$destroy", -> + $el.find('.close').off() + $el.off() + + $scope.$watch "obj", -> + if !$scope.obj + return + setStatus($scope.obj.status) + + $el.on "click", ".status-dropdown", (event) -> + event.preventDefault() + event.stopPropagation() + $el.find(".pop-status").popover().open() + + $el.on "click", ".status", (event) -> + event.preventDefault() + event.stopPropagation() + setStatus(angular.element(event.currentTarget).data("status-id")) + $scope.$apply() + $scope.$broadcast("status:changed", $scope.obj.status) + $el.find(".pop-status").popover().close() + + $el.on "click", ".users-dropdown", (event) -> + event.preventDefault() + event.stopPropagation() + $el.find(".pop-users").popover().open() + + $el.on "click", ".team-requirement", (event) -> + $scope.obj.team_requirement = not $scope.obj.team_requirement + $scope.$apply() + + $el.on "click", ".client-requirement", (event) -> + $scope.obj.client_requirement = not $scope.obj.client_requirement + $scope.$apply() + + $el.on "click", ".is-blocked", (event) -> + $scope.obj.is_blocked = not $scope.obj.is_blocked + $scope.$apply() + + $el.on "click", ".iocaine", (event) -> + $scope.obj.is_iocaine = not $scope.obj.is_iocaine + $scope.$broadcast("isiocaine:changed", $scope.obj) + + setStatus = (id) -> + $scope.obj.status = id + $scope.selectedStatus = _.find $scope.statusList, (item) -> item.id == id + + $scope.isTeamRequirement = () -> + return $scope.obj?.team_requirement + + $scope.isClientRequirement = () -> + return $scope.obj?.client_requirement + + return { + link: link + } + +module.directive("tgLbCreateEdit", [ + "$log", + "$tgRepo", + "$tgModel", + "$tgResources", + "$rootScope", + "lightboxService", + "$tgLoading", + "$translate", + "$tgConfirm", + "$q", + "tgAttachmentsService" + CreateEditDirective +]) \ No newline at end of file diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index 8137ef04..7687e8cd 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -355,10 +355,12 @@ IssueTypeButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTransfo template = $template.get("issue/issue-type-button.html", true) link = ($scope, $el, $attrs, $model) -> + notAutoSave = $scope.$eval($attrs.notAutoSave) + isEditable = -> return $scope.project.my_permissions.indexOf("modify_issue") != -1 - render = (issue) => + render = (issue) -> type = $scope.typeById[issue.type] html = template({ @@ -374,6 +376,11 @@ IssueTypeButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTransfo save = (type) -> $.fn.popover().closeAll() + if notAutoSave + $model.$modelValue.type = type + $scope.$apply() + return + currentLoading = $loading() .target($el.find(".level-name")) .start() @@ -445,10 +452,12 @@ IssueSeverityButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTra template = $template.get("issue/issue-severity-button.html", true) link = ($scope, $el, $attrs, $model) -> + notAutoSave = $scope.$eval($attrs.notAutoSave) + isEditable = -> return $scope.project.my_permissions.indexOf("modify_issue") != -1 - render = (issue) => + render = (issue) -> severity = $scope.severityById[issue.severity] html = template({ @@ -464,6 +473,11 @@ IssueSeverityButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTra save = (severity) -> $.fn.popover().closeAll() + if notAutoSave + $model.$modelValue.severity = severity + $scope.$apply() + return + currentLoading = $loading() .target($el.find(".level-name")) .start() @@ -536,10 +550,12 @@ IssuePriorityButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTra template = $template.get("issue/issue-priority-button.html", true) link = ($scope, $el, $attrs, $model) -> + notAutoSave = $scope.$eval($attrs.notAutoSave) + isEditable = -> return $scope.project.my_permissions.indexOf("modify_issue") != -1 - render = (issue) => + render = (issue) -> priority = $scope.priorityById[issue.priority] html = template({ @@ -555,6 +571,11 @@ IssuePriorityButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTra save = (priority) -> $.fn.popover().closeAll() + if notAutoSave + $model.$modelValue.priority = priority + $scope.$apply() + return + currentLoading = $loading() .target($el.find(".level-name")) .start() diff --git a/app/coffee/modules/issues/lightboxes.coffee b/app/coffee/modules/issues/lightboxes.coffee index b8d57df3..3861c463 100644 --- a/app/coffee/modules/issues/lightboxes.coffee +++ b/app/coffee/modules/issues/lightboxes.coffee @@ -29,131 +29,6 @@ trim = @.taiga.trim module = angular.module("taigaIssues") -############################################################################# -## Issue Create Lightbox Directive -############################################################################# - -CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, $q, attachmentsService) -> - link = ($scope, $el, $attrs) -> - form = $el.find("form").checksley() - $scope.issue = {} - $scope.attachments = Immutable.List() - - $scope.$on "issueform:new", (ctx, project)-> - form.reset() - - resetAttachments() - - $el.find(".tag-input").val("") - lightboxService.open $el, () -> - $scope.createIssueOpen = false - - $scope.issue = { - project: project.id - subject: "" - status: project.default_issue_status - type: project.default_issue_type - priority: project.default_priority - severity: project.default_severity - 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') - - return $q.all(promises) - - attachmentsToAdd = Immutable.List() - - resetAttachments = () -> - attachmentsToAdd = Immutable.List() - $scope.attachments = Immutable.List() - - $scope.addAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.push(attachment) - - $scope.deleteAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.filter (it) -> - return it.get('name') != attachment.get('name') - - $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.issue.tags) - - _.remove itemtags, (tag) -> tag[0] == value - - $scope.issue.tags = itemtags - - _.pull($scope.issue.tags, value) - - submit = debounce 2000, (event) => - event.preventDefault() - - if not form.validate() - return - - currentLoading = $loading() - .target(submitButton) - .start() - - promise = $repo.create("issues", $scope.issue) - - promise.then (data) -> - return createAttachments(data) - - promise.then (data) -> - currentLoading.finish() - $rootscope.$broadcast("issueform:new:success", data) - lightboxService.close($el) - $confirm.notify("success") - - promise.then null, -> - currentLoading.finish() - $confirm.notify("error") - - submitButton = $el.find(".submit-button") - - $el.on "submit", "form", submit - - - return {link:link} - -module.directive("tgLbCreateIssue", ["$tgRepo", "$tgConfirm", "$rootScope", "lightboxService", "$tgLoading", - "$q", "tgAttachmentsService", CreateIssueDirective]) - - ############################################################################# ## Issue Bulk Create Lightbox Directive ############################################################################# diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee index ce8b8c99..41f5a59a 100644 --- a/app/coffee/modules/issues/list.coffee +++ b/app/coffee/modules/issues/list.coffee @@ -367,7 +367,16 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi # Functions used from templates addNewIssue: -> - @rootscope.$broadcast("issueform:new", @scope.project) + project = @projectService.project.toJS() + @rootscope.$broadcast("genericform:new", { + 'objType': 'issue', + 'statusList': @scope.issueStatusList, + 'project': project, + 'severityList': @scope.severityList, + 'priorityList': @scope.priorityList, + 'typeById': groupBy(project.issue_types, (x) -> x.id), + 'typeList': _.sortBy(project.issue_types, "order") + }) addIssuesInBulk: -> @rootscope.$broadcast("issueform:bulk", @scope.projectId) diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index eface736..f5c18ead 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -164,8 +164,12 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi addNewUs: (type, statusId) -> switch type - when "standard" then @rootscope.$broadcast("usform:new", - @scope.projectId, statusId, @scope.usStatusList) + when "standard" then @rootscope.$broadcast("genericform:new", + { + 'objType': 'us', + 'project': @scope.project, + 'statusList': @scope.usStatusList + }) when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, statusId) @@ -175,9 +179,15 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @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()) + .then (editingUserStory) => + @rs2.attachments.list( + "us", us.get('id'), us.getIn(['model', 'project'])).then (attachments) => + @rootscope.$broadcast("genericform:edit", { + 'objType': 'us', + 'obj': editingUserStory, + 'statusList': @scope.usStatusList, + 'attachments': attachments.toJS() + }) us = us.set('loading-edit', false) @kanbanUserstoriesService.replace(us) diff --git a/app/coffee/modules/taskboard/lightboxes.coffee b/app/coffee/modules/taskboard/lightboxes.coffee index 2c23966c..63a6a982 100644 --- a/app/coffee/modules/taskboard/lightboxes.coffee +++ b/app/coffee/modules/taskboard/lightboxes.coffee @@ -27,196 +27,6 @@ bindOnce = @.taiga.bindOnce debounce = @.taiga.debounce trim = @.taiga.trim -CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, $confirm, attachmentsService) -> - link = ($scope, $el, attrs) -> - $scope.isNew = true - - attachmentsToAdd = Immutable.List() - attachmentsToDelete = Immutable.List() - - resetAttachments = () -> - attachmentsToAdd = Immutable.List() - attachmentsToDelete = Immutable.List() - - $scope.addAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.push(attachment) - - $scope.deleteAttachment = (attachment) -> - attachmentsToAdd = attachmentsToAdd.filter (it) -> - return it.get('name') != attachment.get('name') - - if attachment.get("id") - attachmentsToDelete = attachmentsToDelete.push(attachment) - - createAttachments = (obj) -> - promises = _.map attachmentsToAdd.toJS(), (attachment) -> - attachmentsService.upload(attachment.file, obj.id, $scope.task.project, 'task') - - return $q.all(promises) - - deleteAttachments = (obj) -> - promises = _.map attachmentsToDelete.toJS(), (attachment) -> - return attachmentsService.delete("task", attachment.id) - - 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 = $model.make_model('tasks', { - project: $scope.projectId - milestone: sprintId - user_story: usId - is_archived: false - status: $scope.project.default_task_status - assigned_to: null - tags: [], - subject: "", - description: "", - }) - $scope.isNew = true - $scope.attachments = Immutable.List() - - resetAttachments() - - # Update texts for creation - create = $translate.instant("COMMON.CREATE") - $el.find(".button-green").html(create) - - newTask = $translate.instant("LIGHTBOX.CREATE_EDIT_TASK.TITLE") - $el.find(".title").html(newTask + " ") - - $el.find(".tag-input").val("") - lightboxService.open $el, () -> - $scope.createEditTaskOpen = false - - $scope.createEditTaskOpen = true - - $scope.$on "taskform:edit", (ctx, task, attachments) -> - $scope.task = task - $scope.isNew = false - - $scope.attachments = Immutable.fromJS(attachments) - - resetAttachments() - - # Update texts for edition - save = $translate.instant("COMMON.SAVE") - edit = $translate.instant("LIGHTBOX.CREATE_EDIT_TASK.ACTION_EDIT") - - $el.find(".button-green").html(save) - $el.find(".title").html(edit + " ") - - $el.find(".tag-input").val("") - lightboxService.open $el, () -> - $scope.createEditTaskOpen = false - - $scope.createEditTaskOpen = true - - - submitButton = $el.find(".submit-button") - - submit = debounce 2000, (event) => - event.preventDefault() - - form = $el.find("form").checksley() - 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" - else - promise = $repo.save($scope.task) - broadcastEvent = "taskform:edit:success" - - promise.then (data) -> - deleteAttachments(data) - .then () => createAttachments(data) - .then () => - $rs.tasks.getByRef(data.project, data.ref, params).then (task) -> - $rootscope.$broadcast(broadcastEvent, task) - - currentLoading = $loading() - .target(submitButton) - .start() - - promise.then (data) -> - currentLoading.finish() - lightboxService.close($el) - - $el.on "submit", "form", submit - - close = () => - if !$scope.task.isModified() - lightboxService.close($el) - $scope.$apply -> - $scope.task.revert() - else - $confirm.ask($translate.instant("LIGHTBOX.CREATE_EDIT_TASK.CONFIRM_CLOSE")).then (result) -> - lightboxService.close($el) - $scope.task.revert() - result.finish() - - $el.find('.close').on "click", (event) -> - event.preventDefault() - event.stopPropagation() - close() - - $el.keydown (event) -> - event.stopPropagation() - code = if event.keyCode then event.keyCode else event.which - if code == 27 - close() - - $scope.$on "$destroy", -> - $el.find('.close').off() - $el.off() - - return {link: link} - CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $model) -> link = ($scope, $el, attrs) -> @@ -248,7 +58,6 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $ # TODO: error handling promise.then null, -> currentLoading.finish() - console.log "FAIL" $scope.$on "taskform:bulk", (ctx, sprintId, usId)-> lightboxService.open($el) @@ -266,20 +75,6 @@ CreateBulkTasksDirective = ($repo, $rs, $rootscope, $loading, lightboxService, $ module = angular.module("taigaTaskboard") -module.directive("tgLbCreateEditTask", [ - "$tgRepo", - "$tgModel", - "$tgResources", - "$rootScope", - "$tgLoading", - "lightboxService", - "$translate", - "$q", - "$tgConfirm", - "tgAttachmentsService", - CreateEditTaskDirective -]) - module.directive("tgLbCreateBulkTasks", [ "$tgRepo", "$tgResources", diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index 660a90c3..e677ada6 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -439,9 +439,17 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga task = task.set('loading-edit', true) @taskboardTasksService.replace(task) - @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()) + @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("genericform:edit", { + 'objType': 'task', + 'obj': editingTask, + 'statusList': @scope.taskStatusList, + 'attachments': attachments.toJS() + }) + task = task.set('loading', false) @taskboardTasksService.replace(task) @@ -496,7 +504,15 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga ## Template actions addNewTask: (type, us) -> switch type - when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id) + when "standard" then @rootscope.$broadcast("genericform:new", + { + 'objType': 'task', + 'project': @scope.project, + 'sprintId': @scope.sprintId, + 'usId': us?.id, + 'status': @scope.project.default_task_status, + 'statusList': @scope.taskStatusList + }) when "bulk" then @rootscope.$broadcast("taskform:bulk", @scope.sprintId, us?.id) toggleFold: (id) -> diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 82c97e4a..bed74bed 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -150,7 +150,9 @@ "VOTES": "Votes", "SPRINT": "Sprint", "DUE_DATE": "Due date", - "DUE_DATE_REASON": "Due date reason" + "DUE_DATE_REASON": "Due date reason", + "CLIENT_REQUIREMENT": "Client requirement", + "TEAM_REQUIREMENT": "Team requirement" }, "ROLES": { "ALL": "All" @@ -1091,6 +1093,12 @@ "EDIT_US": "Edit user story", "CONFIRM_CLOSE": "You have not saved changes.\nAre you sure you want to close the form?" }, + "CREATE_EDIT": { + "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this {{ objName }}", + "NEW": "New {{ objName }}", + "EDIT": "Edit {{ objName }}", + "CONFIRM_CLOSE": "You have not saved changes.\nAre you sure you want to close the form?" + }, "DELETE_DUE_DATE": { "TITLE": "Delete due date", "SUBTITLE": "Are you sure you want to delete this due date?" diff --git a/app/modules/components/card/card-templates/card-data.jade b/app/modules/components/card/card-templates/card-data.jade index e04bc53a..06567b93 100644 --- a/app/modules/components/card/card-templates/card-data.jade +++ b/app/modules/components/card/card-templates/card-data.jade @@ -12,7 +12,6 @@ .card-statistics tg-due-date.statistic.card-due-date( due-date="vm.item.getIn(['model', 'due_date'])" - due-date-status="vm.item.getIn(['model', 'due_date_status'])" is-closed="vm.item.getIn(['model', 'is_closed'])" ) .statistic.card-iocaine( diff --git a/app/modules/components/detail/header/detail-header.jade b/app/modules/components/detail/header/detail-header.jade index a01088a1..55e001d5 100644 --- a/app/modules/components/detail/header/detail-header.jade +++ b/app/modules/components/detail/header/detail-header.jade @@ -21,9 +21,8 @@ tg-svg.detail-edit.e2e-detail-edit(svg-icon="icon-edit") tg-due-date( due-date="vm.item.due_date" - due-date-status="vm.item.due_date_status" - ng-if="vm.item.due_date" is-closed="vm.item.is_closed" + ng-if="vm.item.due_date" ) .edit-title-wrapper(ng-if="vm.editMode") input.edit-title-input.e2e-title-input( diff --git a/app/modules/components/due-date/due-date-controller.coffee b/app/modules/components/due-date/due-date-controller.coffee index 83740926..8e344706 100644 --- a/app/modules/components/due-date/due-date-controller.coffee +++ b/app/modules/components/due-date/due-date-controller.coffee @@ -21,9 +21,10 @@ class DueDateController @.$inject = [ "$translate" "tgLightboxFactory" + "tgProjectService" ] - constructor: (@translate, @tgLightboxFactory) -> + constructor: (@translate, @tgLightboxFactory, @projectService) -> visible: () -> return @.format == 'button' or @.dueDate? @@ -37,17 +38,35 @@ class DueDateController 'due_soon': 'due-soon', 'past_due': 'past-due', 'set': 'due-set', + 'not_set': 'not-set', } - return colors[@.dueDateStatus] or '' + return colors[@status()] or '' title: () -> - if @.format == 'button' - return if @.dueDate then @._formatTitle() else 'Edit due date' + if @.dueDate + return @._formatTitle() + else if @.format == 'button' + return @translate.instant('COMMON.DUE_DATE.TITLE_ACTION_SET_DUE_DATE') + return '' - return @._formatTitle() + status: () -> + if !@.dueDate + return 'not_set' + + project = @projectService.project.toJS() + project.due_soon_threshold = 14 # TODO get value from taiga-back + dueDate = moment(@.dueDate) + now = moment() + + if @.isClosed + return 'no_longer_applicable' + else if now > dueDate + return 'past_due' + else if now.add(moment.duration(project.due_soon_threshold, "days")) >= dueDate + return 'due_soon' + return 'set' _formatTitle: () -> - dueDateStatus = 'closed' titles = { 'no_longer_applicable': 'COMMON.DUE_DATE.NO_LONGER_APPLICABLE', 'due_soon': 'COMMON.DUE_DATE.DUE_SOON', @@ -56,16 +75,17 @@ class DueDateController prettyDate = @translate.instant("COMMON.PICKERDATE.FORMAT") formatedDate = moment(@.dueDate).format(prettyDate) - if not titles[@.dueDateStatus] + status = @status() + if not titles[status] return formatedDate - return "#{formatedDate} (#{@translate.instant(titles[@.dueDateStatus])})" + return "#{formatedDate} (#{@translate.instant(titles[status])})" setDueDate: () -> return if @.disabled() @tgLightboxFactory.create( "tg-lb-set-due-date", {"class": "lightbox lightbox-set-due-date"}, - {"object": @.item} + {"object": @.item, "notAutoSave": @.notAutoSave} ) -angular.module('taigaComponents').controller('DueDate', DueDateController) +angular.module('taigaComponents').controller('DueDateCtrl', DueDateController) diff --git a/app/modules/components/due-date/due-date-popover.directive.coffee b/app/modules/components/due-date/due-date-popover.directive.coffee new file mode 100644 index 00000000..7f0a19d3 --- /dev/null +++ b/app/modules/components/due-date/due-date-popover.directive.coffee @@ -0,0 +1,69 @@ +### +# Copyright (C) 2014-2018 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: due-date.directive.coffee +### + +module = angular.module("taigaComponents") + +dueDatePopoverDirective = ($translate, datePickerConfigService) -> + return { + link: (scope, el, attrs, ctrl) -> + prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") + if ctrl.dueDate + ctrl.dueDate = moment(ctrl.dueDate, prettyDate) + + el.on "click", ".date-picker-popover-trigger", (event) -> + if ctrl.disabled() + return + event.preventDefault() + event.stopPropagation() + el.find(".date-picker-popover").popover().open() + + el.on "click", ".date-picker-clean", (event) -> + event.preventDefault() + event.stopPropagation() + ctrl.dueDate = null + scope.$apply() + el.find(".date-picker-popover").popover().close() + + datePickerConfig = datePickerConfigService.get() + _.merge(datePickerConfig, { + field: el.find('input.due-date')[0] + container: el.find('.date-picker-container')[0] + bound: false + onSelect: () -> + ctrl.dueDate = this.getMoment().format('YYYY-MM-DD') + el.find(".date-picker-popover").popover().close() + scope.$apply() + }) + + el.picker = new Pikaday(datePickerConfig) + + controller: "DueDateCtrl", + controllerAs: "vm", + bindToController: true, + templateUrl: "components/due-date/due-date-popover.html", + scope: { + dueDate: '=', + isClosed: '=', + item: '=', + format: '@', + notAutoSave: '=' + } + } + +module.directive('tgDueDatePopover', ['$translate', 'tgDatePickerConfigService', dueDatePopoverDirective]) \ No newline at end of file diff --git a/app/modules/components/due-date/due-date-popover.jade b/app/modules/components/due-date/due-date-popover.jade new file mode 100644 index 00000000..e6cd616f --- /dev/null +++ b/app/modules/components/due-date/due-date-popover.jade @@ -0,0 +1,16 @@ +.due-date-button-wrapper + label.due-date-button.button-gray.is-editable.date-picker-popover-trigger( + ng-disabled="vm.disabled()" + ng-class="vm.color()" + ng-attr-title="{{ vm.title() }}" + ) + tg-svg(svg-icon="icon-clock") + input.due-date.no-focus( + type="hidden" + picker-value="{{ vm.dueDate }}" + ) + div.date-picker-popover + div.date-picker-container + div.date-picker-popover-footer(ng-if="vm.dueDate") + a.date-picker-clean(href="", title="{{'LIGHTBOX.SET_DUE_DATE.TITLE_ACTION_DELETE_DUE_DATE' | translate}}") + tg-svg(svg-icon="icon-trash") \ No newline at end of file diff --git a/app/modules/components/due-date/due-date.directive.coffee b/app/modules/components/due-date/due-date.directive.coffee index 29a9a1eb..600f7664 100644 --- a/app/modules/components/due-date/due-date.directive.coffee +++ b/app/modules/components/due-date/due-date.directive.coffee @@ -19,25 +19,60 @@ module = angular.module("taigaComponents") -dueDateDirective = () -> +dueDateDirective = ($translate, datePickerConfigService) -> templateUrl = (el, attrs) -> if attrs.format return "components/due-date/due-date-" + attrs.format + ".html" return "components/due-date/due-date-icon.html" return { - link: (scope) -> - controller: "DueDate", + link: (scope, el, attrs, ctrl) -> + renderDatePicker = () -> + prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") + if ctrl.dueDate + ctrl.dueDate = moment(ctrl.dueDate, prettyDate) + + el.on "click", ".date-picker-popover-trigger", (event) -> + if ctrl.disabled() + return + event.preventDefault() + event.stopPropagation() + el.find(".date-picker-popover").popover().open() + + el.on "click", ".date-picker-clean", (event) -> + event.preventDefault() + event.stopPropagation() + ctrl.dueDate = null + scope.$apply() + el.find(".date-picker-popover").popover().close() + + datePickerConfig = datePickerConfigService.get() + _.merge(datePickerConfig, { + field: el.find('input.due-date')[0] + container: el.find('.date-picker-container')[0] + bound: false + onSelect: () -> + ctrl.dueDate = this.getMoment().format('YYYY-MM-DD') + el.find(".date-picker-popover").popover().close() + scope.$apply() + }) + + el.picker = new Pikaday(datePickerConfig) + + if attrs.format == 'button-popover' + renderDatePicker() + + controller: "DueDateCtrl", controllerAs: "vm", bindToController: true, templateUrl: templateUrl, scope: { dueDate: '=', - dueDateStatus: '=', isClosed: '=', item: '=', - format: '@' + format: '@', + notAutoSave: '=' } } -module.directive('tgDueDate', dueDateDirective) \ No newline at end of file +module.directive('tgDueDate', ['$translate', 'tgDatePickerConfigService', dueDateDirective]) \ No newline at end of file diff --git a/app/modules/components/due-date/due-date.scss b/app/modules/components/due-date/due-date.scss index e3209bc3..5efd37dd 100644 --- a/app/modules/components/due-date/due-date.scss +++ b/app/modules/components/due-date/due-date.scss @@ -6,17 +6,25 @@ padding: 1rem; transition: background .2s linear; transition-delay: .1s; - &.closed { + &.closed, + &.text-button.closed:hover { background: $gray-lighter; + border-color: $gray-lighter; } - &.due-set { + &.due-set, + &.text-button.due-set:hover { background: $yellow-green; + border-color: $yellow-green; } - &.due-soon { + &.due-soon, + &.text-button.due-soon:hover { background: $my-sin; + border-color: $my-sin; } - &.past-due { + &.past-due, + &.text-button.past-due:hover { background: $red-light; + border-color: $red-light; } &:hover { background: $gray; @@ -24,6 +32,17 @@ &.editable { cursor: pointer; } + &.text-button { + color: $white; + margin: 0; + padding: .5rem; + &.not-set:hover { + color: $white; + } + &.not-set { + color: $gray; + } + } } .due-date-icon { @@ -66,3 +85,39 @@ width: .9rem; } } + +.due-date-button-wrapper { + display: flex; + position: relative; +} + +.date-picker-container { + overflow: visible; +} + +.date-picker-popover { + background: $white; + border: 1px solid $gray; + display: none; + left: 0; + overflow: visible; + position: absolute; + top: 56px; + width: auto; + .pika-single { + border: 0; + } +} + +.date-picker-popover-footer { + padding: .2rem .5rem; + text-align: right; + svg { + fill: $gray; + height: 1rem; + width: 1rem; + } + a:hover svg { + fill: $red; + } +} diff --git a/app/partials/backlog/backlog.jade b/app/partials/backlog/backlog.jade index 941f3ce4..713702b4 100644 --- a/app/partials/backlog/backlog.jade +++ b/app/partials/backlog/backlog.jade @@ -123,8 +123,8 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", sidebar.menu-secondary.sidebar include ../includes/modules/sprints - div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory) - include ../includes/modules/lightbox-us-create-edit + div.lightbox.lightbox-generic-form.lightbox-create-edit(tg-lb-create-edit) + include ../includes/modules/lightbox-create-edit/lb-create-edit-us div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) include ../includes/modules/lightbox-us-bulk diff --git a/app/partials/common/components/assigned-to-inline.jade b/app/partials/common/components/assigned-to-inline.jade new file mode 100644 index 00000000..1df3c902 --- /dev/null +++ b/app/partials/common/components/assigned-to-inline.jade @@ -0,0 +1,65 @@ +.user-avatar(ng-class!="{ 'is-iocaine': isIocaine }") + img( + style="background-color: {{ bg }}" + src="{{ avatar.url }}" + alt="{{ fullName }}" + ) + .iocaine-symbol( + ng-if="isIocaine" + title="{{ 'TASK.TITLE_ACTION_IOCAINE' | translate }}" + ) + tg-svg(svg-icon="icon-iocaine") + + +.assigned-to + .assigned-to-options + span.assigned-name( + ng-if="!isEditable && fullNameVisible" + ) {{ fullName }} + + span.users-dropdown.user-assigned( + title="{{ 'COMMON.ASSIGNED_TO.TITLE_ACTION_EDIT_ASSIGNMENT'|translate }}" + ng-class="{ 'editable': isEditable }" + ng-if="isEditable" + ) + span.assigned-name + span(ng-if="fullNameVisible") {{ fullName }} + + div.pop-users.popover + input.users-search( + type="text" + data-maxlength="500" + placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}" + ng-model="usersSearch" + ) + a.user-list-single( + href="" + data-user-id="{{ user.id }}" + title="{{ user.full_name_display }}" + ng-repeat="user in users" + ) + img.user-list-avatar( + style="background: {{user.avatar.bg }}" + src="{{ user.avatar.url }}" + ) + span.user-list-name( + href="" + title="{{ user.full_name_display }}" + ) {{ user.full_name_display }} + .show-more(ng-if="showMore") + span(translate="COMMON.ASSIGNED_TO.TOO_MANY") + + span(translate="COMMON.OR", ng-if="isUnassigned") + div(ng-if="isUnassigned") + a.assign-to-me( + href="#" + title="{{'COMMON.ASSIGNED_TO.SELF' | translate}}" + ) + span {{ "COMMON.ASSIGNED_TO.SELF" | translate }} + + tg-svg.remove-user( + ng-if="isEditable && !isUnassigned" + svg-icon="icon-close", + title="{{'COMMON.ASSIGNED_TO.DELETE_ASSIGNMENT' | translate}}" + ) + diff --git a/app/partials/common/components/assigned-users-inline.jade b/app/partials/common/components/assigned-users-inline.jade new file mode 100644 index 00000000..02f9cf2c --- /dev/null +++ b/app/partials/common/components/assigned-users-inline.jade @@ -0,0 +1,117 @@ +div(ng-if="isAssigned") + .user-list(ng-if="assignedUsers.length > 1") + .user-list-item(ng-repeat="assignedUser in assignedUsers") + img( + tg-avatar="assignedUser" + title="{{assignedUser.full_name_display}}" + alt="{{assignedUser.full_name_display}}" + ) + .user-list-item.counter(ng-if="hiddenUsers") + span +{{ hiddenUsers }} + + .ticket-assigned-to.single-assign(ng-if="assignedUsers.length == 1") + .user-avatar + img( + tg-avatar="assignedUsers[0]" + title="{{assignedUsers[0].full_name_display}}" + alt="{{assignedUsers[0].full_name_display}}" + ) + .assigned-to + .assigned-users-options + span.user-assigned {{ assignedUsers[0].full_name_display }} + tg-svg.remove-user( + svg-icon="icon-close", + title="{{'COMMON.ASSIGNED_TO.DELETE_ASSIGNMENT' | translate}}" + data-user-id="{{ assignedUsers[0].id }}" + ) + + div.add-assigned + a.users-dropdown.tg-add-assigned( + href="" + ng-click="openAssignedUsers()" + ) + tg-svg.add-assigned( + data-assigned-user-id="{{assignedUser.id}}", + svg-icon="icon-add", + title="{{'COMMON.ASSIGNED_USERS.ADD_ASSIGNED' | translate}}" + ) + span {{ "COMMON.ASSIGNED_USERS.ADD_ASSIGNED" | translate }} + + +.ticket-assigned-to(ng-if="!isAssigned") + .user-avatar + img( + tg-avatar="" + alt="{{ 'COMMON.ASSIGNED_TO.ASSIGN' | translate }}" + ) + .assigned-to + .assigned-users-options + a.users-dropdown.user-assigned( + href="" + title="{{ 'COMMON.ASSIGNED_TO.TITLE_ACTION_EDIT_ASSIGNMENT'|translate }}" + class="user-assigned" + ) + span.assigned-name {{ "COMMON.ASSIGNED_TO.ASSIGN" | translate }} + |   + span(translate="COMMON.OR") + |   + a.assign-to-me( + href="#" + title="{{'COMMON.ASSIGNED_TO.SELF' | translate}}" + ) + span {{ "COMMON.ASSIGNED_TO.SELF" | translate }} + +div.pop-users.popover(ng-class="{'multiple': assignedUsers.length > 0}") + input.users-search( + type="text" + data-maxlength="500" + placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}" + ng-model="usersSearch" + ) + a.user-list-single.selected( + href="" + data-user-id="{{ user.id }}" + title="{{ user.full_name_display }}" + ng-repeat="user in selected" + ng-class="{'selected': selected.indexOf(user) == -1 }" + ) + img.user-list-avatar( + style="background: {{user.avatar.bg }}" + src="{{ user.avatar.url }}" + ) + span.user-list-name( + href="" + title="{{ user.full_name_display }}" + ) {{ user.full_name_display }} + tg-svg.remove( + href="" + data-user-id="{{ user.id }}" + ng-if="selected.indexOf(user) > -1" + svg-icon="icon-close", + title="{{'COMMON.ASSIGNED_TO.DELETE_ASSIGNMENT' | translate}}" + ) + + a.user-list-single( + href="" + data-user-id="{{ user.id }}" + title="{{ user.full_name_display }}" + ng-repeat="user in users" + ) + img.user-list-avatar( + style="background: {{user.avatar.bg }}" + src="{{ user.avatar.url }}" + ) + span.user-list-name( + href="" + title="{{ user.full_name_display }}" + ) {{ user.full_name_display }} + tg-svg.remove( + href="" + data-user-id="{{ user.id }}" + ng-if="selected.indexOf(user) > -1" + svg-icon="icon-close", + title="{{'COMMON.ASSIGNED_TO.DELETE_ASSIGNMENT' | translate}}" + ) + + .show-more(ng-if="showMore") + span(translate="COMMON.ASSIGNED_TO.TOO_MANY") \ No newline at end of file diff --git a/app/partials/common/lightbox/lightbox-due-date.jade b/app/partials/common/lightbox/lightbox-due-date.jade index 8529338f..a6522f19 100644 --- a/app/partials/common/lightbox/lightbox-due-date.jade +++ b/app/partials/common/lightbox/lightbox-due-date.jade @@ -40,5 +40,6 @@ form a.delete-due-date( href="" title="{{'LIGHTBOX.SET_DUE_DATE.TITLE_ACTION_DELETE_DUE_DATE' | translate}}" + v-if="new_due_date" ) tg-svg(svg-icon="icon-trash") diff --git a/app/partials/includes/components/backlog-row.jade b/app/partials/includes/components/backlog-row.jade index 42764666..817d0ca7 100644 --- a/app/partials/includes/components/backlog-row.jade +++ b/app/partials/includes/components/backlog-row.jade @@ -29,7 +29,7 @@ span(ng-bind-html="us.subject | emojify") tg-due-date( due-date="us.due_date" - due-date-status="us.due_date_status" + is-closed="us.is_closed" ng-if="us.due_date" ) tg-belong-to-epics( diff --git a/app/partials/includes/modules/issues-table.jade b/app/partials/includes/modules/issues-table.jade index d004eba4..161b9c4c 100644 --- a/app/partials/includes/modules/issues-table.jade +++ b/app/partials/includes/modules/issues-table.jade @@ -56,7 +56,7 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") span(ng-bind-html="issue.subject | emojify") tg-due-date( due-date="issue.due_date" - due-date-status="issue.due_date_status" + is-closed="us.is_closed" ng-if="issue.due_date" ) diff --git a/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-issue.jade b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-issue.jade new file mode 100644 index 00000000..e59fab80 --- /dev/null +++ b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-issue.jade @@ -0,0 +1,40 @@ +extends lb-create-edit + +block options + section.ticket-assigned-to( + tg-assigned-to-inline + ng-model="obj" + required-perm="modify_{{ objType }}" + ) + div.ticket-data-container + tg-issue-type-button.ticket-status( + autosave="false" + ng-model="obj" + ) + tg-issue-severity-button.ticket-status( + autosave="false" + ng-model="obj" + ) + tg-issue-priority-button.ticket-status( + autosave="false" + ng-model="obj" + ) + + div.ticket-detail-settings + tg-due-date-popover( + due-date="obj.due_date" + is-closed="obj.is_closed" + item="obj" + not-auto-save="true" + ) + div + label.button-gray.is-blocked( + title="{{ 'COMMON.BLOCK_TITLE' | translate }}" + ng-class="{ 'button-red item-unblock': obj.is_blocked, 'item-block': !obj.is_blocked }" + ) + tg-svg(svg-icon="icon-lock") + + tg-blocking-message-input( + watch="obj.is_blocked" + ng-model="obj.blocked_note" + ) diff --git a/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-task.jade b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-task.jade new file mode 100644 index 00000000..13c9994d --- /dev/null +++ b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-task.jade @@ -0,0 +1,40 @@ +extends lb-create-edit + +block options + section.ticket-assigned-to( + tg-assigned-to-inline + ng-model="obj" + required-perm="modify_{{ objType }}" + ) + div.ticket-detail-settings + tg-due-date-popover( + due-date="obj.due_date" + is-closed="obj.is_closed" + item="obj" + not-auto-save="true" + ) + div + fieldset.iocaine-flag(title="{{ 'TASK.TITLE_ACTION_IOCAINE' | translate }}") + label.button-gray.iocaine( + for="is-iocaine" + ng-class="{'active': obj.is_iocaine}" + ) + tg-svg(svg-icon="icon-iocaine") + input( + type="checkbox" + id="is-iocaine" + name="is-iocaine" + ng-model="obj.is_iocaine" + ng-value="true" + ) + div + label.button-gray.is-blocked( + title="{{ 'COMMON.BLOCK_TITLE' | translate }}" + ng-class="{ 'button-red item-unblock': obj.is_blocked, 'item-block': !obj.is_blocked }" + ) + tg-svg(svg-icon="icon-lock") + + tg-blocking-message-input( + watch="obj.is_blocked" + ng-model="obj.blocked_note" + ) diff --git a/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-us.jade b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-us.jade new file mode 100644 index 00000000..94173646 --- /dev/null +++ b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit-us.jade @@ -0,0 +1,44 @@ +extends lb-create-edit + +block options + section.ticket-assigned-to.multiple-assign( + tg-assigned-users-inline + ng-model="obj" + required-perm="modify_{{ objType }}" + ) + div.ticket-estimation + tg-lb-us-estimation(ng-model="obj") + + div.ticket-detail-settings + tg-due-date-popover( + due-date="obj.due_date" + is-closed="obj.is_closed" + item="obj" + not-auto-save="true" + ) + div + label.button-gray.team-requirement( + for="team-requirement" + title="{{ 'COMMON.TEAM_REQUIREMENT' | translate }}" + ng-class="{ 'active': isTeamRequirement() }" + ) + tg-svg(svg-icon="icon-team-requirement") + div + label.button-gray.client-requirement( + for="client-requirement" + title="{{ 'COMMON.CLIENT_REQUIREMENT' | translate }}" + ng-class="{ 'active': isClientRequirement() }" + ) + tg-svg(svg-icon="icon-client-requirement") + + div + label.button-gray.is-blocked( + title="{{ 'COMMON.BLOCK_TITLE' | translate }}" + ng-class="{ 'button-red item-unblock': obj.is_blocked, 'item-block': !obj.is_blocked }" + ) + tg-svg(svg-icon="icon-lock") + + tg-blocking-message-input( + watch="obj.is_blocked" + ng-model="obj.blocked_note" + ) diff --git a/app/partials/includes/modules/lightbox-create-edit/lb-create-edit.jade b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit.jade new file mode 100644 index 00000000..d7c905ac --- /dev/null +++ b/app/partials/includes/modules/lightbox-create-edit/lb-create-edit.jade @@ -0,0 +1,66 @@ +tg-lightbox-close + +form + h2.title(translate="LIGHTBOX.CREATE_EDIT.TITLE_NEW") + div.form-wrapper + main + fieldset + input( + type="text" + name="subject" + ng-model-options="{ debounce: 200 }" + ng-model="obj.subject" + placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}" + data-required="true" + data-maxlength="500" + ) + + fieldset + tg-tag-line-common.tags-block( + ng-if="project && createEditOpen" + project="project" + tags="obj.tags" + permissions="add_{{objType}}" + on-add-tag="addTag(name, color)" + on-delete-tag="deleteTag(tag)" + ) + + fieldset + textarea.description( + rows=7 + name="description" + ng-model="obj.description" + ng-model-options="{ debounce: 200 }" + ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT.PLACEHOLDER_DESCRIPTION' | translate}}" + ) + fieldset + section + tg-attachments-simple( + attachments="attachments", + on-add="addAttachment(attachment)" + on-delete="deleteAttachment(attachment)" + ) + + sidebar.sidebar.ticket-data + fieldset.status-button + div.status-dropdown.editable(style="background-color:{{ selectedStatus.color }}") + span.status-text {{ selectedStatus.name }} + tg-svg(svg-icon="icon-arrow-down") + + ul.pop-status.popover + li(ng-repeat="s in statusList") + a.status( + href="" + title="{{ s.name }}" + data-status-id="{{ s.id }}" + ) {{ s.name }} + + block options + + button.button-green.submit-button( + type="submit" + title="{{'COMMON.CREATE' | translate}}" + translate="COMMON.CREATE" + ) + + div.lightbox.lightbox-select-user(tg-lb-assignedto) \ No newline at end of file diff --git a/app/partials/includes/modules/lightbox-create-issue.jade b/app/partials/includes/modules/lightbox-create-issue.jade deleted file mode 100644 index 8cf35fa7..00000000 --- a/app/partials/includes/modules/lightbox-create-issue.jade +++ /dev/null @@ -1,60 +0,0 @@ -tg-lightbox-close - -form - h2.title(translate="LIGHTBOX.CREATE_ISSUE.TITLE") - fieldset - input( - type="text" - ng-model="issue.subject" - ng-attr-placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}" - ng-model-options="{ debounce: 200 }" - data-required="true" - data-maxlength="500" - ) - .fieldset-row - fieldset - select.type( - ng-model="issue.type" - ng-options="t.id as t.name for t in issueTypes" - ) - fieldset - select.priority( - ng-model="issue.priority" - ng-options="p.id as p.name for p in priorityList" - ) - fieldset - select.severity( - ng-model="issue.severity" - ng-options="s.id as s.name for s in severityList" - ) - - fieldset - 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)" - ) - - fieldset - textarea.description( - ng-attr-placeholder="{{'COMMON.FIELDS.DESCRIPTION' | translate}}" - ng-model="issue.description" - ) - - // include lightbox-attachments - button.button-green.submit-button( - type="submit" - title="{{'COMMON.CREATE' | translate}}" - translate="COMMON.CREATE" - ) diff --git a/app/partials/includes/modules/lightbox-task-create-edit.jade b/app/partials/includes/modules/lightbox-task-create-edit.jade deleted file mode 100644 index 4e61d4f6..00000000 --- a/app/partials/includes/modules/lightbox-task-create-edit.jade +++ /dev/null @@ -1,92 +0,0 @@ -tg-lightbox-close -form - h2.title(translate="LIGHTBOX.CREATE_EDIT_TASK.TITLE") - fieldset - input( - type="text" - ng-model="task.subject" - ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT_TASK.PLACEHOLDER_SUBJECT' | translate}}" - ng-model-options="{ debounce: 200 }" - data-required="true" - data-maxlength="500" - ) - - fieldset - select( - ng-model="task.status" - ng-options="s.id as s.name for s in taskStatusList" - placeholder="{{'LIGHTBOX.CREATE_EDIT_TASK.PLACEHOLDER_STATUS' | translate}}" - ) - - fieldset - select( - ng-model="task.assigned_to" - ng-options="s.id as s.full_name_display for s in users" - placeholder="{{'Assigned to'}}" - ) - option( - value="" - translate="LIGHTBOX.CREATE_EDIT_TASK.OPTION_UNASSIGNED" - ) - - fieldset - 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 - section - tg-attachments-simple( - attachments="attachments", - on-add="addAttachment(attachment)" - on-delete="deleteAttachment(attachment)" - ) - - fieldset - textarea.description( - ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT_TASK.PLACEHOLDER_SHORT_DESCRIPTION' | translate}}" - ng-model="task.description" - ng-model-options="{ debounce: 200 }" - ) - - div.settings - fieldset.iocaine-flag(title="{{'COMMON.IOCAINE_TEXT' | translate}}") - input( - type="checkbox" - ng-model="task.is_iocaine" - name="iocaine-task" - id="iocaine-task" - ng-value="true" - ) - label.iocaine.trans-button(for="iocaine-task") - tg-svg(svg-icon="icon-iocaine") - span Iocaine - - fieldset.blocking-flag - input( - type="checkbox" - ng-model="task.is_blocked" - name="blocked-task" - id="blocked-task" - ng-value="true" - ) - label.blocked.trans-button( - for="blocked-task" - translate="COMMON.BLOCKED" - ) - - tg-blocking-message-input( - watch="task.is_blocked" - ng-model="task.blocked_note" - ) - - button.button-green.submit-button( - type="submit" - title="{{'COMMON.CREATE' | translate}}" - translate="COMMON.CREATE" - ) diff --git a/app/partials/includes/modules/lightbox-us-create-edit.jade b/app/partials/includes/modules/lightbox-us-create-edit.jade deleted file mode 100644 index 2c9d7ccb..00000000 --- a/app/partials/includes/modules/lightbox-us-create-edit.jade +++ /dev/null @@ -1,100 +0,0 @@ -tg-lightbox-close - -form - h2.title(translate="LIGHTBOX.CREATE_EDIT_US.TITLE") - fieldset - input( - type="text" - name="subject" - ng-model-options="{ debounce: 200 }" - ng-model="us.subject" - placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}" - data-required="true" - data-maxlength="500" - ) - - fieldset.ticket-estimation - tg-lb-us-estimation(ng-model="us") - - fieldset - select( - name="status" - ng-model="us.status" - ng-options="s.id as s.name for s in usStatusList" - ) - - fieldset - 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 - textarea.description( - name="description" - ng-model="us.description" - ng-model-options="{ debounce: 200 }" - ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT_US.PLACEHOLDER_DESCRIPTION' | translate}}" - ) - fieldset - section - tg-attachments-simple( - attachments="attachments", - on-add="addAttachment(attachment)" - on-delete="deleteAttachment(attachment)" - ) - - div.settings - fieldset.team-requirement - input( - type="checkbox" - name="team_requirement" - ng-model="us.team_requirement" - id="team-requirement" - ng-value="true" - ) - label.requirement.trans-button( - for="team-requirement" - translate="US.FIELDS.TEAM_REQUIREMENT" - ) - - fieldset.client-requirement - input( - type="checkbox" - name="client_requirement" - ng-model="us.client_requirement", - id="client-requirement" - ng-value="true" - ) - label.requirement.trans-button( - for="client-requirement" - translate="US.FIELDS.CLIENT_REQUIREMENT" - ) - - fieldset.blocking-flag - input( - type="checkbox" - name="is_blocked" - ng-model="us.is_blocked" - id="blocked-us" - ng-value="true" - ) - label.blocked.trans-button( - for="blocked-us" - translate="COMMON.BLOCKED" - ) - - tg-blocking-message-input( - watch="us.is_blocked" - ng-model="us.blocked_note" - ) - - button.button-green.submit-button( - type="submit" - title="{{'COMMON.CREATE' | translate}}" - translate="COMMON.CREATE" - ) diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade index cc292a2d..b594c3c7 100644 --- a/app/partials/issue/issues-detail.jade +++ b/app/partials/issue/issues-detail.jade @@ -114,12 +114,11 @@ div.wrapper( section.ticket-detail-settings tg-due-date( - tg-check-permission="modify_issue" due-date="issue.due_date" - due-date-status="issue.due_date_status" + format="button" is-closed="issue.is_closed" item="issue" - format="button" + tg-check-permission="modify_issue" ) tg-promote-issue-to-us-button( tg-check-permission="add_us", diff --git a/app/partials/issue/issues.jade b/app/partials/issue/issues.jade index 97df1b10..6ad5afcd 100644 --- a/app/partials/issue/issues.jade +++ b/app/partials/issue/issues.jade @@ -33,8 +33,8 @@ div.wrapper.issues.lightbox-generic-form( div.lightbox.lightbox-select-user(tg-lb-assignedto) - div.lightbox.lightbox-create-issue(tg-lb-create-issue) - include ../includes/modules/lightbox-create-issue + div.lightbox.lightbox-generic-form.lightbox-create-edit(tg-lb-create-edit) + include ../includes/modules/lightbox-create-edit/lb-create-edit-issue div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-issues) include ../includes/modules/lightbox-issue-bulk diff --git a/app/partials/kanban/kanban.jade b/app/partials/kanban/kanban.jade index 28eb7134..4ebc35c1 100644 --- a/app/partials/kanban/kanban.jade +++ b/app/partials/kanban/kanban.jade @@ -40,8 +40,8 @@ div.wrapper( include ../includes/modules/kanban-table - div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory) - include ../includes/modules/lightbox-us-create-edit + div.lightbox.lightbox-generic-form.lightbox-create-edit(tg-lb-create-edit) + include ../includes/modules/lightbox-create-edit/lb-create-edit-us div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) include ../includes/modules/lightbox-us-bulk diff --git a/app/partials/task/related-task-row.jade b/app/partials/task/related-task-row.jade index 32ee05b9..11051648 100644 --- a/app/partials/task/related-task-row.jade +++ b/app/partials/task/related-task-row.jade @@ -5,7 +5,6 @@ span(ng-non-bindable) <%= emojify(task.subject) %> tg-due-date( due-date="task.due_date" - due-date-status="task.due_date_status" ng-if="task.due_date" ) .task-settings diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade index 3d1e6357..d96a3299 100644 --- a/app/partials/task/task-detail.jade +++ b/app/partials/task/task-detail.jade @@ -103,12 +103,11 @@ div.wrapper( section.ticket-detail-settings tg-due-date( - tg-check-permission="modify_task" due-date="task.due_date" - due-date-status="task.due_date_status" + format="button" is-closed="task.is_closed" item="task" - format="button" + tg-check-permission="modify_task" ) tg-task-is-iocaine-button(ng-model="task") tg-block-button(tg-check-permission="modify_task", ng-model="task") diff --git a/app/partials/taskboard/taskboard.jade b/app/partials/taskboard/taskboard.jade index d9475033..667f50a1 100644 --- a/app/partials/taskboard/taskboard.jade +++ b/app/partials/taskboard/taskboard.jade @@ -47,8 +47,8 @@ div.wrapper( include ../includes/modules/taskboard-table - div.lightbox.lightbox-generic-form(tg-lb-create-edit-task) - include ../includes/modules/lightbox-task-create-edit + div.lightbox.lightbox-generic-form.lightbox-create-edit(tg-lb-create-edit) + include ../includes/modules/lightbox-create-edit/lb-create-edit-task div.lightbox.lightbox-generic-bulk.lightbox-task-bulk(tg-lb-create-bulk-tasks) include ../includes/modules/lightbox-task-bulk diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade index 6f5378a9..85563ba5 100644 --- a/app/partials/us/us-detail.jade +++ b/app/partials/us/us-detail.jade @@ -128,12 +128,11 @@ div.wrapper( section.ticket-detail-settings tg-due-date( - tg-check-permission="modify_us" due-date="us.due_date" - due-date-status="us.due_date_status" + format="button" is-closed="us.is_closed" item="us" - format="button" + tg-check-permission="modify_us" ) tg-us-team-requirement-button(ng-model="us") tg-us-client-requirement-button(ng-model="us") diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index a0cb4161..a55c1a12 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -600,3 +600,229 @@ } } } + + +.lightbox-create-edit { + z-index: 9998; + $control-height: 30px; + $spacing: 15px; + $width: 700px; + $pop-width: 203px; + $sidebar-width: 220px; + form { + flex-basis: $width; + max-width: $width; + width: $width; + } + .form-wrapper { + display: flex; + flex-basis: $width; + flex-direction: row; + flex-grow: 0; + margin-bottom: $spacing*2; + main { + flex-grow: 1; + margin-right: $spacing; + } + .sidebar { + border-left: 2px solid $whitish; + padding-left: $spacing; + width: $sidebar-width; + } + } + .status-button { + display: flex; + position: relative; + } + .status-dropdown { + align-content: center; + color: $white; + cursor: pointer; + display: flex; + flex-basis: 100%; + height: $control-height; + padding: .25rem .5rem; + .status-text { + flex-grow: 1; + text-transform: uppercase; + } + svg { + fill: $white; + height: .9rem; + width: .9rem; + } + } + + .popover { + a { + color: $white; + display: block; + padding: .5rem 1rem; + text-align: left; + &:hover, + &.active { + color: $primary-light; + } + } + } + + .ticket-assigned-to { + border: 0; + padding: 0; + &.single-assign { + margin: 0; + } + &.multiple-assign { + align-items: start; + flex-direction: column; + margin: 0; + } + .assigned-to-options { + display: block; + } + .remove-user { + top: 1.2rem; + } + } + + .user-list { + display: flex; + width: 100%; + $item-size: 44.75px; + .user-list-item { + margin-right: .5rem; + width: $item-size; + img { + height: $item-size; + width: $item-size; + } + &:last-child { + margin-right: 0; + } + &.counter { + background: $gray-lighter; + color: $gray-light; + font-weight: 400; + height: $item-size; + line-height: $item-size; + text-align: center; + } + } + } + + .tg-add-assigned { + align-items: center; + display: flex; + margin: .25rem 0 .75rem; + .add-assigned { + fill: $gray; + opacity: 1; + right: .5rem; + top: 2rem; + &:hover { + cursor: pointer; + fill: $red; + transition: fill .2s; + } + } + + span { + @include font-size(small); + @include font-type(light); + color: $gray; + margin: .2rem .5rem; + } + } + + .users-dropdown { + position: relative; + } + + .pop-status { + @include popover($pop-width, $control-height - 2px); + } + + .pop-users { + @include popover($pop-width, 60px, 0, '', '', 16px, -7px, ($pop-width / 3)); + &.multiple { + top: 84px; + &:after { + left: 30px; + } + } + ul { + margin-bottom: .5rem; + } + li { + border-bottom: 1px solid $gray-light; + &:last-child { + border: 0; + } + } + .user-list-single { + align-content: center; + align-items: center; + display: flex; + flex-direction: row; + flex-grow: 0; + padding: .5rem 0; + &.selected { + color: $primary; + } + } + .user-list-avatar { + height: 32px; + margin-right: .5rem; + width: 32px; + } + .user-list-name { + @include font-type(text); + flex-grow: 1; + position: relative; + } + + .remove svg { + fill: $whitish; + height: .8rem; + width: .8rem; + } + + .show-more { + border-top: 1px solid $gray-light; + padding-top: .5rem; + text-align: center; + } + } + + .ticket-data-container { + margin: 0; + padding: 0 0 .1rem; + .ticket-status .priority-data { + margin: 0; + } + } + + .ticket-estimation .points-per-role { + margin: 0; + } + + .ticket-detail-settings { + height: 52px; + justify-content: left; + margin: 1rem 0 0; + label { + border: 0; + padding: 12px 13px; + width: auto; + } + .item-block, + .item-unblock { + display: block; + margin: 0; + } + } + + .blocked-note { + margin-top: .5rem; + } +}