diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index af2948c8..0e7657df 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -61,7 +61,7 @@ class ConfirmService extends taiga.Service # Render content el.find(".title").text(title) if title el.find(".subtitle").text(subtitle) if subtitle - el.find(".message").text(message) if message + el.find(".message").html(message) if message # Assign event handlers el.on "click.confirm-dialog", ".button-green", debounce 2000, (event) => diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index 4bd1ab9a..38724a29 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -840,6 +840,9 @@ $confirm, $q, attachmentsService, $template, $compile) -> } } + $scope.setMode = (value) -> + $scope.mode = value + $scope.$on "genericform:new", (ctx, params) -> getSchema(params) $scope.mode = 'new' @@ -851,9 +854,9 @@ $confirm, $q, attachmentsService, $template, $compile) -> $scope.mode = 'add-existing' $scope.getOrCreate = true $scope.existingFilterText = '' - $scope.existingItems = [] + $rs[schema.model].listInAllProjects({ project: $scope.project.id }, true).then (data) -> - $scope.existingItems = data + $scope.existingItems = angular.copy(data) mount(params) $scope.$on "genericform:edit", (ctx, params) -> @@ -885,6 +888,7 @@ $confirm, $q, attachmentsService, $template, $compile) -> form.reset() if form resetAttachments() setStatus($scope.obj.status) + render() $scope.lightboxOpen = true lightboxService.open($el) @@ -950,6 +954,9 @@ $confirm, $q, attachmentsService, $template, $compile) -> lightboxService.close($el) $rootScope.$broadcast("#{$scope.objType}form:add:success", item) + $scope.isDisabledExisting = (item) -> + return item && item[$scope.relatedField] == $scope.relatedObjectId + $scope.addExisting = (selectedItem) -> event.preventDefault() addExisting(selectedItem) @@ -976,7 +983,7 @@ $confirm, $q, attachmentsService, $template, $compile) -> deleteAttachments(data).then () -> createAttachments(data).then () -> currentLoading.finish() - close() + lightboxService.close($el) $rs[schema.model].getByRef(data.project, data.ref, schema.params).then (obj) -> $rootScope.$broadcast(broadcastEvent, obj) promise.then null, (data) -> @@ -987,20 +994,14 @@ $confirm, $q, attachmentsService, $template, $compile) -> checkClose = () -> if !$scope.obj.isModified() - close() + lightboxService.close($el) $scope.$apply -> $scope.obj.revert() else $confirm.ask( $translate.instant("LIGHTBOX.CREATE_EDIT.CONFIRM_CLOSE")).then (result) -> result.finish() - close() - - close = () -> - delete $scope.objType - delete $scope.mode - $scope.lightboxOpen = false - lightboxService.close($el) + lightboxService.close($el) $el.on "submit", "form", submit @@ -1060,9 +1061,14 @@ $confirm, $q, attachmentsService, $template, $compile) -> $scope.selectedStatus = _.find $scope.statusList, (item) -> item.id == id $scope.obj.is_closed = $scope.selectedStatus.is_closed + render = (sprint) -> + template = $template.get("common/lightbox/lightbox-create-edit/lb-create-edit.html") + templateScope = $scope.$new() + compiledTemplate = $compile(template)(templateScope) + $el.html(compiledTemplate) + return { link: link - templateUrl: "common/lightbox/lightbox-create-edit/lb-create-edit.html" } module.directive("tgLbCreateEdit", [ diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index b76df140..7bcf2661 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -704,22 +704,47 @@ module.directive("tgPromoteIssueToUsButton", ["$rootScope", "$tgRepo", "$tgConfi ## Add Issue to Sprint button directive ############################################################################# -AssignSprintToIssueButtonDirective = ($rootScope, $rs, $repo, $loading, $translate, lightboxService) -> +AssignSprintToIssueButtonDirective = ($rootScope, $rs, $repo, $loading, $translate, +lightboxService, $confirm) -> link = ($scope, $el, $attrs, $model) -> avaliableMilestones = [] issue = null - $el.on "click", "a", (event) -> + $el.on "click", ".assign-issue-button.button-unset", (event) -> event.preventDefault() event.stopPropagation() - title = $translate.instant("ISSUES.ACTION_ASSIGN_SPRINT") + title = $translate.instant("ISSUES.ACTION_ATTACH_SPRINT") issue = $model.$modelValue $rs.sprints.list($scope.projectId, null).then (data) -> $scope.milestones = data.milestones - $scope.selectedSprintId = issue.milestone + $scope.selectedSprint = issue.milestone avaliableMilestones = angular.copy($scope.milestones) lightboxService.open($el.find(".lightbox-assign-sprint-to-issue")) + $el.on "click", ".assign-issue-button.button-set", (event) -> + event.preventDefault() + event.stopPropagation() + issue = $model.$modelValue + $rs.sprints.list($scope.projectId, null).then (data) -> + currentSprint = _.find(data.milestones, { "id": issue.milestone }) + + title = $translate.instant("ISSUES.CONFIRM_DETACH_FROM_SPRINT.TITLE") + message = $translate.instant("ISSUES.CONFIRM_DETACH_FROM_SPRINT.MESSAGE") + message += " #{currentSprint.name}" + + $confirm.ask(title, null, message).then (askResponse) -> + onSuccess = -> + $scope.$broadcast("assign-sprint-to-issue:success", null) + askResponse.finish() + lightboxService.close($el) + + onError = -> + askResponse.finish(false) + $confirm.notify("error") + + issue.setAttr('milestone', null) + $repo.save(issue, true).then(onSuccess, onError) + $scope.$on "$destroy", -> $el.off() @@ -750,5 +775,5 @@ AssignSprintToIssueButtonDirective = ($rootScope, $rs, $repo, $loading, $transla } module.directive("tgAssignSprintToIssueButton", ["$rootScope", "$tgResources", "$tgRepo", - "$tgLoading", "$translate", "lightboxService", + "$tgLoading", "$translate", "lightboxService", "$tgConfirm" AssignSprintToIssueButtonDirective] ) diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index 1e829018..b781a49e 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -564,8 +564,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @rs.issues.getByRef(issue.getIn(['model', 'project']), issue.getIn(['model', 'ref'])) .then (removingIssue) => issue = issue.set('loading-delete', false) - title = @translate.instant("ISSUES.CONFIRM_REMOVE_FROM_SPRINT.TITLE") - subtitle = @translate.instant("ISSUES.CONFIRM_REMOVE_FROM_SPRINT.MESSAGE") + title = @translate.instant("ISSUES.CONFIRM_DETACH_FROM_SPRINT.TITLE") + subtitle = @translate.instant("ISSUES.CONFIRM_DETACH_FROM_SPRINT.MESSAGE") message = removingIssue.subject @confirm.askOnDelete(title, message, subtitle).then (askResponse) => removingIssue.milestone = null diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index d9b07c77..da3e94f8 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -1102,7 +1102,7 @@ "CONFIRM_CLOSE": "You have not saved changes.\nAre you sure you want to close the form?", "EXISTING_OBJECT": "Existing {{ objName }}", "NEW_OBJECT": "New {{ objName }}", - "CHOOSE_EXISTING": "What's the {{ objName }}?", + "CHOOSE_EXISTING": "Which {{ objName }}?", "NO_ITEMS_FOUND": "It looks like nothing was found with your search criteria" }, "DELETE_DUE_DATE": { @@ -1164,11 +1164,11 @@ "CREATE_RELATED_USERSTORIES": "Create a relationship with", "NEW_USERSTORY": "New user story", "EXISTING_USERSTORY": "Existing user story", - "CHOOSE_PROJECT_FOR_CREATION": "What's the project?", + "CHOOSE_PROJECT_FOR_CREATION": "Which project?", "SUBJECT": "Subject", "SUBJECT_BULK_MODE": "Subject (bulk insert)", - "CHOOSE_PROJECT_FROM": "What's the project?", - "CHOOSE_USERSTORY": "What's the user story?", + "CHOOSE_PROJECT_FROM": "Which project?", + "CHOOSE_USERSTORY": "Which user story?", "NO_USERSTORIES": "This project has no User Stories yet. Please select another project.", "NO_USERSTORIES_FOUND": "It looks like nothing was found with your search criteria", "FILTER_USERSTORIES": "Filter user stories", @@ -1424,7 +1424,8 @@ "SECTION_NAME": "Issue", "ACTION_NEW_ISSUE": "+ NEW ISSUE", "ACTION_PROMOTE_TO_US": "Promote to User Story", - "ACTION_ASSIGN_SPRINT": "Add issue to Sprint", + "ACTION_ATTACH_SPRINT": "Attach issue to Sprint", + "ACTION_DETACH_SPRINT": "Detach issue from Sprint", "ACTION_REMOVE_FROM_SPRINT": "Remove issue from Sprint {{ sprintName }}", "PROMOTED": "This issue has been promoted to US:", "EXTERNAL_REFERENCE": "This issue has been created from", @@ -1434,15 +1435,16 @@ "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Go to the taskboard", "FILTER_SPRINTS": "Filter Sprints", - "CHOOSE_SPRINT": "What's the Sprint?", + "CHOOSE_SPRINT": "Which Sprint?", "FIELDS": { "PRIORITY": "Priority", "SEVERITY": "Severity", "TYPE": "Type" }, - "CONFIRM_REMOVE_FROM_SPRINT": { - "TITLE": "Remove issue from sprint", - "MESSAGE": "Are you sure you want to remove this issue from the sprint?" + "FILTER_ISSUES": "Filter Issues", + "CONFIRM_DETACH_FROM_SPRINT": { + "TITLE": "Detach issue from Sprint", + "MESSAGE": "You are about to detach the issue from the sprint" }, "CONFIRM_PROMOTE": { "TITLE": "Promote this issue to a new user story", diff --git a/app/modules/components/due-date/due-date-popover.directive.coffee b/app/modules/components/due-date/due-date-popover.directive.coffee index 629a260e..3088cc9e 100644 --- a/app/modules/components/due-date/due-date-popover.directive.coffee +++ b/app/modules/components/due-date/due-date-popover.directive.coffee @@ -39,7 +39,7 @@ dueDatePopoverDirective = ($translate, datePickerConfigService) -> return event.preventDefault() event.stopPropagation() - if !el.picker.getDate() + if !el.picker.getDate() && ctrl.dueDate el.picker.setDate(moment(ctrl.dueDate).format('YYYY-MM-DD')) el.find(".date-picker-popover").popover().open() diff --git a/app/modules/components/search-list/search-list.directive.coffee b/app/modules/components/search-list/search-list.directive.coffee index 21812cfa..ddc0cd42 100644 --- a/app/modules/components/search-list/search-list.directive.coffee +++ b/app/modules/components/search-list/search-list.directive.coffee @@ -33,6 +33,13 @@ searchListDirective = ($translate) -> if scope.itemType == 'issue' scope.milestonesById = groupBy(scope.project.milestones, (e) -> e.id) + if scope.filterClosed + scope.showClosed = false + + if scope.itemType == 'sprint' + scope.textShowClosed = $translate.instant("BACKLOG.SPRINTS.ACTION_SHOW_CLOSED_SPRINTS") + scope.textHideClosed = $translate.instant("BACKLOG.SPRINTS.ACTION_HIDE_CLOSED_SPRINTS") + el.on "click", ".choice", (event) -> choiceId = parseInt($(event.currentTarget).data("choice-id")) value = if attrs.ngModel?.id != choiceId then itemsById[choiceId] else null @@ -48,30 +55,45 @@ searchListDirective = ($translate) -> value = value.toString() return normalizeString(value.toUpperCase()) - el.on "blur", "#items-filter", (event) -> - filtering = false + resetSelected = () -> + scope.currentSelected = null + model.$setViewValue(null) - el.on "focus", "#items-filter", (event) -> - filtering = true + resetAll = () -> + resetSelected() + scope.searchText = '' + avaliableItems = angular.copy(scope.items) + itemsById = groupBy(avaliableItems, (x) -> x.id) + + + scope.isVisible = (item) -> + if !scope.filterClosed || scope.showClosed + return true + if (scope.itemType == 'sprint' && (item.closed || item.is_closed)) + if (scope.currentSelected?.id == item.id) + resetSelected() + return false + return true + + scope.toggleShowClosed = (item) -> + scope.showClosed = !scope.showClosed scope.filterItems = (searchText) -> - scope.items = avaliableItems.filter((item) -> + scope.itemDisabled(null) + scope.filtering = true + scope.items = _.filter(avaliableItems, (item) -> itemAttrs = item.getAttrs() if Array.isArray(scope.filterBy) _.some(scope.filterBy, (attr) -> isContainedIn(searchText, itemAttrs[attr])) else isContainedIn(searchText, itemAttrs[scope.filterBy]) ) - if scope.value - scope.value = _.find(scope.items, scope.currentSelected) + if !_.find(scope.items, scope.currentSelected) + resetSelected() scope.$watch 'items', (items) -> - if !filtering && items?.length - if scope.resetOnChange - scope.currentSelected = null - model.$setViewValue(null) - avaliableItems = angular.copy(items) - itemsById = groupBy(avaliableItems, (x) -> x.id) + if !scope.filtering && items + resetAll() return { link: link, @@ -84,7 +106,8 @@ searchListDirective = ($translate) -> filterBy: '=', items: '=', itemType: '@', - resetOnChange: "=" + filterClosed: '=', + itemDisabled: '=' } } diff --git a/app/modules/components/search-list/search-list.jade b/app/modules/components/search-list/search-list.jade index 8990adb2..97b27c0b 100644 --- a/app/modules/components/search-list/search-list.jade +++ b/app/modules/components/search-list/search-list.jade @@ -2,6 +2,18 @@ fieldset.search-list label( for="items-filter" ) {{ label }} + + a.show-closed( + href="" + ng-if="showClosedVisible" + ng-click="toggleShowClosed()" + ) + span(ng-if="!showClosed") + tg-svg(svg-icon="icon-archive") + span {{ textShowClosed }} + span(ng-if="showClosed") + tg-svg(svg-icon="icon-archive") + span {{ textHideClosed }} input.items-filter( id="items-filter" type="text" @@ -9,12 +21,13 @@ fieldset.search-list ng-model="searchText" ng-change="filterItems(searchText)" ) - ul li.choice( ng-repeat="item in items track by item.id" ng-class="{ 'selected': item.id == currentSelected.id }" data-choice-id="{{ item.id }}" + ng-if="isVisible(item)" + ng-disabled="itemDisabled(item)" ) ng-include(src="templateUrl") diff --git a/app/modules/components/search-list/search-list.scss b/app/modules/components/search-list/search-list.scss index f0ef9350..7c26786d 100644 --- a/app/modules/components/search-list/search-list.scss +++ b/app/modules/components/search-list/search-list.scss @@ -1,4 +1,16 @@ .search-list { + .show-closed { + align-content: center; + align-items: center; + display: flex; + float: right; + font-size: .9em; + svg { + height: 1em; + margin-right: .25em; + width: 1em; + } + } ul { background: $mass-white; border: 1px solid $gray-light; @@ -7,7 +19,7 @@ max-height: 200px; overflow-y: auto; } - li { + .choice { cursor: pointer; padding: .25em .5em; &.selected { @@ -17,6 +29,13 @@ color: $white; } } + &[disabled] { + color: $gray-lighter; + cursor: not-allowed; + .info { + color: $gray-lighter; + } + } ng-include { display: flex; width: 100%; @@ -25,7 +44,7 @@ text-align: left; } .info { - color: $gray-lighter; + color: $gray-light; text-align: right; } } diff --git a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit.jade b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit.jade index a6f49bc3..a685ea2a 100644 --- a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit.jade +++ b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit.jade @@ -4,10 +4,10 @@ form(ng-if="lightboxOpen") h2.title(ng-switch="mode") span(ng-switch-when="new") {{ 'LIGHTBOX.CREATE_EDIT.NEW' | translate: { objName: objName } }} span(ng-switch-when="edit") {{ 'LIGHTBOX.CREATE_EDIT.EDIT' | translate: { objName: objName } }} - span(ng-switch-when="add-existing") {{ 'LIGHTBOX.CREATE_EDIT.ADD_EXISTING' | translate: { objName: objName, targetName: existingOptions.title } }} + span(ng-switch-when="add-existing") {{ 'LIGHTBOX.CREATE_EDIT.ADD_EXISTING' | translate: { objName: objName, targetName: title } }} .existing-or-new-selector(ng-show="getOrCreate == true") - .existing-or-new-selector-single + .existing-or-new-selector-single(ng-click="setMode('add-existing')") input( type="radio" name="related-with-selector" @@ -15,10 +15,10 @@ form(ng-if="lightboxOpen") value="add-existing" ng-model="mode" ) - label.e2e-existing-user-story-label(for="add-existing") + label.e2e-existing-item-label(for="add-existing") span.name {{ 'LIGHTBOX.CREATE_EDIT.EXISTING_OBJECT' | translate: { objName: objName } }} - .existing-or-new-selector-single + .existing-or-new-selector-single(ng-click="setMode('new')") input( type="radio" name="related-with-selector" @@ -26,20 +26,20 @@ form(ng-if="lightboxOpen") value="new" ng-model="mode" ) - label.e2e-new-userstory-label(for="new") + label.e2e-new-item-label(for="new") span.name {{ 'LIGHTBOX.CREATE_EDIT.NEW_OBJECT' | translate: { objName: objName } }} div(ng-if="mode == 'add-existing'") .existing-item-wrapper tg-search-list( label="{{ 'LIGHTBOX.CREATE_EDIT.CHOOSE_EXISTING' | translate: { objName: objName } }}" - placeholder="{{ 'ISSUES.FILTER_SPRINTS' | translate }}" + placeholder="{{ 'ISSUES.FILTER_ISSUES' | translate }}" items="existingItems" ng-model="selectedItem" filter-by="['ref', 'subject']" project="project" item-type="{{ objType }}" - reset-on-change="true" + item-disabled="isDisabledExisting" ) button.button-green.add-existing-button( diff --git a/app/partials/issue/assign-sprint-to-issue-button.jade b/app/partials/issue/assign-sprint-to-issue-button.jade index d13c4b34..4c5b152c 100644 --- a/app/partials/issue/assign-sprint-to-issue-button.jade +++ b/app/partials/issue/assign-sprint-to-issue-button.jade @@ -1,17 +1,24 @@ -a.assign-issue-button.button-gray.is-editable( +a.assign-issue-button.button-gray.is-editable.button-unset( href="" - tg-check-permission="add_us" - title="{{ 'ISSUES.ACTION_ASSIGN_SPRINT' | translate }}" - ng-class="{'button-set': issue.milestone}" + ng-show="!issue.milestone" + tg-check-permission="modify_issue" + title="{{ 'ISSUES.ACTION_ATTACH_SPRINT' | translate }}" ) - tg-svg(svg-icon="icon-promote") + tg-svg(svg-icon="icon-attach") +a.assign-issue-button.button-gray.is-editable.button-set( + href="" + ng-show="issue.milestone" + tg-check-permission="modify_issue" + title="{{ 'ISSUES.ACTION_DETACH_SPRINT' | translate }}" +) + tg-svg(svg-icon="icon-detach") .lightbox.lightbox-assign-sprint-to-issue tg-lightbox-close div.lightbox-assign-related-sprint - h2.title {{ 'ISSUES.ACTION_ASSIGN_SPRINT' | translate }} + h2.title {{ 'ISSUES.ACTION_ATTACH_SPRINT' | translate }} tg-search-list( label="{{ 'ISSUES.CHOOSE_SPRINT' | translate }}" placeholder="{{ 'ISSUES.FILTER_SPRINTS' | translate }}" @@ -26,4 +33,5 @@ a.assign-issue-button.button-gray.is-editable( ng-click="saveIssueToSprint(selectedSprint, $event)" tg-loading="vm.loading" translate="COMMON.SAVE" + ng-disabled="!selectedSprint" ) diff --git a/app/styles/modules/common/ticket-data.scss b/app/styles/modules/common/ticket-data.scss index d6815fc1..8db3802b 100644 --- a/app/styles/modules/common/ticket-data.scss +++ b/app/styles/modules/common/ticket-data.scss @@ -200,6 +200,9 @@ border-color: $yellow-green; } } + .assign-issue-button.button-set:hover { + background: $red-light; + } .item-block, .item-unblock { display: none; diff --git a/app/svg/sprite.svg b/app/svg/sprite.svg index 8be33777..e925ea21 100644 --- a/app/svg/sprite.svg +++ b/app/svg/sprite.svg @@ -467,5 +467,15 @@ + + + + + + + + + +