diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index 14ae9316..29957a51 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -567,10 +567,10 @@ $translate, $compile, $currentUserService, avatarService) -> 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 + assigned_to_extra_info = $scope.usersById[$scope.selected] + $scope.fullName = assigned_to_extra_info?.full_name_display $scope.isUnassigned = false - $scope.avatar = avatarService.getAvatar(assignedObject.assigned_to_extra_info) + $scope.avatar = avatarService.getAvatar(assigned_to_extra_info) $scope.bg = $scope.avatar.bg $scope.isIocaine = assignedObject?.is_iocaine else diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index fbf305f6..ee1aff50 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -779,7 +779,13 @@ CreateEditDirective = ( $log, $repo, $model, $rs, $rootScope, lightboxService, $loading, $translate, $confirm, $q, attachmentsService, $template, $compile) -> link = ($scope, $el, attrs) -> + schema = null + objType = null form = null + + attachmentsToAdd = Immutable.List() + attachmentsToDelete = Immutable.List() + schemas = { us: { objName: 'User Story', @@ -851,67 +857,50 @@ $confirm, $q, attachmentsService, $template, $compile) -> } } - attachmentsToAdd = Immutable.List() - attachmentsToDelete = Immutable.List() - $scope.$on "genericform:new", (ctx, data) -> - if beforeMount('new', data, ['project']) - mountCreateForm(data) - afterMount() + getSchema(data) + $scope.mode = 'new' + $scope.getOrCreate = false + mount(data) + + $scope.$on "genericform:new-or-existing", (ctx, data) -> + getSchema(data) + $scope.mode = 'add-existing' + $scope.getOrCreate = true + $scope.existingFilterText = '' + $scope.existingItems = {} + $scope.existingOptions = data.existingOptions + mount(data) $scope.$on "genericform:edit", (ctx, data) -> - if beforeMount('edit', data, ['project', 'obj', 'attachments']) - mountUpdateForm(data) - afterMount() + getSchema(data) + $scope.mode = 'edit' + $scope.getOrCreate = false + mount(data) - beforeMount = (mode, data, requiredAttrs) -> - form.reset() if form - - # Get form schema - if !data.objType || !schemas[data.objType] - return $log.error( - "Invalid objType `#{data.objType}` for `genericform:#{mode}` event") + getSchema = (data) -> $scope.objType = data.objType - $scope.schema = schemas[data.objType] + if !$scope.objType || !schemas[$scope.objType] + return $log.error("Invalid objType `#{$scope.objType}` for `genericform` event") + schema = schemas[$scope.objType] - # Get required attrs of the directive - getAttrs(mode, data, requiredAttrs) + mount = (data) -> + $scope.objName = schema.objName + if $scope.mode == 'edit' + $scope.obj = data.obj + $scope.attachments = Immutable.fromJS(data.attachments) + else + $scope.obj = $model.make_model(schema.model, schema.initialData(data)) + $scope.attachments = Immutable.List() - return true + _.map schema.data($scope.project), (value, key) -> + $scope[key] = value - mountCreateForm = (data) -> - $scope.obj = $model.make_model($scope.schema.model, $scope.schema.initialData(data)) - $scope.isNew = true - $scope.attachments = Immutable.List() - $scope.text = { - title: $translate.instant("LIGHTBOX.CREATE_EDIT.NEW", { objName: $scope.schema.objName }) - action: $translate.instant("COMMON.CREATE") - } - render() - - mountUpdateForm = (data) -> - $scope.isNew = false - $scope.attachments = Immutable.fromJS($scope.attachments) - $scope.text = { - title: $translate.instant("LIGHTBOX.CREATE_EDIT.EDIT", { objName: $scope.schema.objName }) - action: $translate.instant("COMMON.SAVE") - } - render() - - afterMount = () -> + form.reset() if form 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] + setStatus($scope.obj.status) + $scope.lightboxOpen = true + lightboxService.open($el) resetAttachments = () -> attachmentsToAdd = Immutable.List() @@ -929,7 +918,6 @@ $confirm, $q, attachmentsService, $template, $compile) -> $scope.addTag = (tag, color) -> value = trim(tag.toLowerCase()) - tags = $scope.project.tags projectTags = $scope.project.tags_colors @@ -940,11 +928,9 @@ $confirm, $q, attachmentsService, $template, $compile) -> 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 @@ -953,98 +939,117 @@ $confirm, $q, attachmentsService, $template, $compile) -> $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) - + 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() + addExisting = (ref) -> + currentLoading = $loading().target($el.find(".add-existing-button")).start() + selectedItem = $scope.existingItems[parseInt(ref)] + selectedItem.setAttr($scope.existingOptions.targetField, $scope.existingOptions.targetValue) + $repo.save(selectedItem, true).then (data) -> + currentLoading.finish() + lightboxService.close($el) + $rootScope.$broadcast("#{$scope.objType}form:add:success", selectedItem) + $scope.getTargetTitle = (item) -> + index = item[$scope.existingOptions.targetField] + return $scope.existingOptions.targetsById[index]?.name + + $scope.existingFilterChanged = (value) -> + if value? + $rs[schema.model].listInAllProjects( + { project: $scope.project.id, q: value }, true + ).then (data) -> + $scope.existingItems = {} + _.map(data, (itemModel) -> + itemModel.html = itemModel.subject + + targetTitle = $scope.getTargetTitle(itemModel) + if targetTitle + itemModel.html = "#{itemModel.html} (#{targetTitle})" + itemModel.class = 'strong' + + $scope.existingItems[itemModel.ref] = itemModel + ) + + $scope.addExisting = (selectedItem) -> + event.preventDefault() + addExisting(selectedItem) + + submit = debounce 2000, (event) -> form = $el.find("form").checksley() if not form.validate() return - currentLoading = $loading().target(submitButton).start() + currentLoading = $loading().target($el.find(".submit-button")).start() - if $scope.isNew - promise = $repo.create($scope.schema.model, $scope.obj) + if $scope.mode == 'new' + promise = $repo.create(schema.model, $scope.obj) broadcastEvent = "#{$scope.objType}form:new:success" else + if ($scope.obj.due_date instanceof moment) + prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") + $scope.obj.due_date = $scope.obj.due_date.format("YYYY-MM-DD") + promise = $repo.save($scope.obj, true) broadcastEvent = "#{$scope.objType}form:edit:success" promise.then (data) -> - deleteAttachments(data) - .then () -> createAttachments(data) - .then () -> + 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) - + close() + $rs[schema.model].getByRef(data.project, data.ref, 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 = () -> + checkClose = () -> if !$scope.obj.isModified() - lightboxService.close($el) + close() $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() + close() + + close = () -> + delete $scope.objType + delete $scope.mode + $scope.lightboxOpen = false + lightboxService.close($el) $el.on "submit", "form", submit $el.find('.close').on "click", (event) -> event.preventDefault() event.stopPropagation() - close() + checkClose() $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) + checkClose() $el.on "click", ".status-dropdown", (event) -> event.preventDefault() @@ -1080,28 +1085,30 @@ $confirm, $q, attachmentsService, $template, $compile) -> $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 - render = () -> - templatePath = "common/lightbox/lightbox-create-edit/lb-create-edit-#{$scope.objType}.html" - template = $template.get(templatePath, true) + setStatus = (id) -> + $scope.obj.status = id + $scope.selectedStatus = _.find $scope.statusList, (item) -> item.id == id + $scope.obj.is_closed = $scope.selectedStatus.is_closed - _.map $scope.schema.data($scope.project), (value, key) -> + render = () -> + # templatePath = "common/lightbox/lightbox-create-edit/lb-create-edit-#{$scope.objType}.html" + # template = $template.get(templatePath, true) + + _.map schema.data($scope.project), (value, key) -> $scope[key] = value - html = $compile(template($scope))($scope) - $el.html(html) + # html = $compile(template($scope))($scope) + # $el.html(html) return { link: link + templateUrl: "common/lightbox/lightbox-create-edit/lb-create-edit.html" } module.directive("tgLbCreateEdit", [ diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index 60177860..b77ae0f4 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -107,7 +107,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga else if @.zoomLevel > 1 && previousZoomLevel <= 1 @.zoomLoading = true - @.loadTasks().then () => + @q.all([@.loadTasks(), @.loadIssues()]).then () => @.zoomLoading = false @taskboardTasksService.resetFolds() @@ -320,6 +320,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @analytics.trackEvent("issue", "create", "create issue on taskboard", 1) + @scope.$on "issueform:add:success", (event, issue) => + @.refreshTagsColors().then () => + @taskboardIssuesService.add(issue) + @scope.$on "issueform:edit:success", (event, issue) => @.refreshTagsColors().then () => @taskboardIssuesService.replaceModel(issue) @@ -376,6 +380,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @scope.taskStatusList = _.sortBy(project.task_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order") @scope.usStatusById = groupBy(project.us_statuses, (e) -> e.id) + @scope.issueStatusById = groupBy(project.issue_statuses, (e) -> e.id) + @scope.milestonesById = groupBy(project.milestones, (e) -> e.id) @scope.$emit('project:loaded', project) @@ -417,10 +423,14 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga loadIssues: -> params = {} + + if @.zoomLevel > 1 + params.include_attachments = 1 + params = _.merge params, @location.search() return @rs.issues.listInProject(@scope.projectId, @scope.sprintId, params).then (issues) => - @taskboardIssuesService.init(@scope.project, @scope.usersById) + @taskboardIssuesService.init(@scope.project, @scope.usersById, @scope.issueStatusById) @taskboardIssuesService.set(issues) loadTasks: -> @@ -544,6 +554,26 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga askResponse.finish(false) @confirm.notify("error") + removeIssueFromSprint: (id) -> + issue = @.taskboardIssuesService.getIssue(id) + issue = issue.set('loading-delete', true) + + @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") + message = removingIssue.subject + @confirm.askOnDelete(title, message, subtitle).then (askResponse) => + removingIssue.milestone = null + promise = @repo.save(removingIssue) + promise.then => + @.taskboardIssuesService.remove(removingIssue) + askResponse.finish() + promise.then null, -> + askResponse.finish(false) + @confirm.notify("error") + taskMove: (ctx, task, oldStatusId, usId, statusId, order) -> task = @taskboardTasksService.getTaskModel(task.get('id')) @@ -587,17 +617,26 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga addNewIssue: (type, us) -> switch type - when "standard" then @rootscope.$broadcast("genericform:new", + when "standard" then @rootscope.$broadcast("genericform:new-or-existing", { 'objType': 'issue', 'project': @scope.project, - 'sprintId': @scope.sprintId + 'sprintId': @scope.sprintId, + 'existingOptions': { + targetField: 'milestone', + targetValue: @scope.sprintId, + targetsById: @scope.milestonesById, + title: "#{@translate.instant("COMMON.FIELDS.SPRINT")} #{@scope.sprint.name}", + } }) when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id) when "bulk" then @rootscope.$broadcast("issueform:bulk", @scope.projectId, @scope.sprintId) - toggleFold: (id) -> - @taskboardTasksService.toggleFold(id) + toggleFold: (id, modelName) -> + if modelName == 'issues' + @taskboardIssuesService.toggleFold(id) + else if modelName == 'tasks' + @taskboardTasksService.toggleFold(id) changeTaskAssignedTo: (id) -> task = @taskboardTasksService.getTaskModel(id) diff --git a/app/coffee/modules/taskboard/taskboard-issues.coffee b/app/coffee/modules/taskboard/taskboard-issues.coffee index 4ebc87fa..dc121da0 100644 --- a/app/coffee/modules/taskboard/taskboard-issues.coffee +++ b/app/coffee/modules/taskboard/taskboard-issues.coffee @@ -25,16 +25,33 @@ class TaskboardIssuesService extends taiga.Service @.reset() reset: () -> + @.foldStatusChanged = {} @.issuesRaw = [] - init: (project, usersById) -> + init: (project, usersById, issueStatusById) -> + @.issueStatusById = issueStatusById @.project = project @.usersById = usersById + resetFolds: () -> + @.foldStatusChanged = {} + @.refresh() + + toggleFold: (issueId) -> + @.foldStatusChanged[issueId] = !@.foldStatusChanged[issueId] + @.refresh() + add: (issue) -> @.issuesRaw = @.issuesRaw.concat(issue) @.refresh() + remove: (issue) -> + for key, item of @.issuesRaw + if issue.id == item.id + @.issuesRaw.splice(key, 1) + @.refresh() + return + set: (issues) -> @.issuesRaw = issues @.refresh() @@ -58,14 +75,14 @@ class TaskboardIssuesService extends taiga.Service issues = [] for issueModel in @.issuesRaw issue = {} - - model = issueModel.getAttrs() - - issue.model = model - issue.images = _.filter model.attachments, (it) -> return !!it.thumbnail_card_url + issue.foldStatusChanged = @.foldStatusChanged[issueModel.id] + issue.model = issueModel.getAttrs() + issue.modelName = issueModel.getName() issue.id = issueModel.id + issue.status = @.issueStatusById[issueModel.status] + issue.images = _.filter issue.model.attachments, (it) -> return !!it.thumbnail_card_url issue.assigned_to = @.usersById[issueModel.assigned_to] - issue.colorized_tags = _.map issue.model.tags, (tag) => + issue.colorized_tags = _.map issue.model.tags, (tag) -> return {name: tag[0], color: tag[1]} issues.push(issue) diff --git a/app/coffee/modules/taskboard/taskboard-tasks.coffee b/app/coffee/modules/taskboard/taskboard-tasks.coffee index 276f338d..024072d5 100644 --- a/app/coffee/modules/taskboard/taskboard-tasks.coffee +++ b/app/coffee/modules/taskboard/taskboard-tasks.coffee @@ -140,6 +140,9 @@ class TaskboardTasksService extends taiga.Service return {"task_id": task.id, "order": @.order[task.id], "set_orders": setOrders} refresh: -> + if !@.project + return + @.tasksRaw = _.sortBy @.tasksRaw, (it) => @.order[it.id] tasks = @.tasksRaw diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index af1697e8..877ca370 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -48,7 +48,8 @@ "CARD": { "ASSIGN_TO": "Assign To", "EDIT": "Edit card", - "DELETE": "Delete card" + "DELETE": "Delete card", + "REMOVE_ISSUE_FROM_SPRINT": "Remove issue from sprint" }, "FORM_ERRORS": { "DEFAULT_MESSAGE": "This value seems to be invalid.", @@ -1097,7 +1098,12 @@ "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?" + "ADD_EXISTING": "Add {{ objName }} to {{ targetName }}", + "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 }}?", + "NO_ITEMS_FOUND": "It looks like nothing was found with your search criteria" }, "DELETE_DUE_DATE": { "TITLE": "Delete due date", @@ -1351,6 +1357,8 @@ "TITLE_ACTION_ASSIGN": "Assign task", "PLACEHOLDER_CARD_TITLE": "This could be a task", "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", + "NEW_ISSUE": "New issue", + "EXISTING_ISSUE": "Existing issue", "TABLE": { "COLUMN": "User story", "TITLE_ACTION_FOLD": "Fold column", @@ -1426,6 +1434,10 @@ "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?" + }, "CONFIRM_PROMOTE": { "TITLE": "Promote this issue to a new user story", "MESSAGE": "Are you sure you want to create a new US from this Issue?" diff --git a/app/modules/components/card/card-templates/card-data.jade b/app/modules/components/card/card-templates/card-data.jade index 06567b93..1995f614 100644 --- a/app/modules/components/card/card-templates/card-data.jade +++ b/app/modules/components/card/card-templates/card-data.jade @@ -2,13 +2,21 @@ ng-if="vm.visible('extra_info')" ng-class="{'empty-tasks': !vm.item.getIn(['model', 'tasks']).size}" ) - span.card-estimation.not-estimated( - ng-if="vm.item.getIn(['model', 'total_points']) === null && vm.visible('empty_extra_info')", - translate="US.NOT_ESTIMATED" - ) - span.card-estimation( - ng-if="vm.item.getIn(['model', 'total_points'])" - ) {{"COMMON.FIELDS.POINTS" | translate}} {{vm.item.getIn(['model', 'total_points'])}} + span(ng-switch="vm.item.get('modelName') == 'issues'") + span(ng-switch-when="true") + span.card-status-tag( + ng-if="vm.item.get('status')" + ng-style="{color: vm.item.getIn(['status', 'color'])}" + ) {{ vm.item.getIn(['status', 'name']) }} + span(ng-switch-when="false") + span.card-estimation.not-estimated( + ng-if="vm.item.getIn(['model', 'total_points']) === null && vm.visible('empty_extra_info')", + translate="US.NOT_ESTIMATED" + ) + span.card-estimation( + ng-if="vm.item.getIn(['model', 'total_points'])" + ) {{"COMMON.FIELDS.POINTS" | translate}} {{vm.item.getIn(['model', 'total_points'])}} + .card-statistics tg-due-date.statistic.card-due-date( due-date="vm.item.getIn(['model', 'due_date'])" diff --git a/app/modules/components/card/card-templates/card-owner.jade b/app/modules/components/card/card-templates/card-owner.jade index 48e76d3a..5714ede4 100644 --- a/app/modules/components/card/card-templates/card-owner.jade +++ b/app/modules/components/card/card-templates/card-owner.jade @@ -46,7 +46,14 @@ title="{{ 'COMMON.CARD.EDIT' | translate }}" ) tg-svg(svg-icon="icon-edit") - + a.e2e-edit.card-edit( + href="" + ng-if="vm.item.get('modelName') == 'issues'" + ng-click="!$event.ctrlKey && !$event.metaKey && vm.onClickRemove({id: vm.item.get('id')})" + tg-loading="vm.item.get('loading-remove-from-sprint')" + title="{{ 'COMMON.CARD.REMOVE_ISSUE_FROM_SPRINT' | translate }}" + ) + tg-svg(svg-icon="icon-close") a.e2e-edit.card-delete( href="" ng-click="!$event.ctrlKey && !$event.metaKey && vm.onClickDelete({id: vm.item.get('id')})" diff --git a/app/modules/components/card/card.controller.coffee b/app/modules/components/card/card.controller.coffee index b42b171f..b1636299 100644 --- a/app/modules/components/card/card.controller.coffee +++ b/app/modules/components/card/card.controller.coffee @@ -87,6 +87,8 @@ class CardController getNavKey: () -> if @.type == 'task' return 'project-tasks-detail' + else if @.type == 'issue' + return 'project-issues-detail' else return 'project-userstories-detail' diff --git a/app/modules/components/card/card.directive.coffee b/app/modules/components/card/card.directive.coffee index 87338ca0..8d6ba291 100644 --- a/app/modules/components/card/card.directive.coffee +++ b/app/modules/components/card/card.directive.coffee @@ -31,6 +31,7 @@ cardDirective = () -> onToggleFold: "&", onClickAssignedTo: "&", onClickEdit: "&", + onClickRemove: "&", onClickDelete: "&", project: "=", item: "=", diff --git a/app/modules/components/card/card.scss b/app/modules/components/card/card.scss index 5ec762f4..aff3d6fe 100644 --- a/app/modules/components/card/card.scss +++ b/app/modules/components/card/card.scss @@ -125,7 +125,7 @@ .card-actions { display: flex; justify-content: space-between; - padding: 0 .5rem; + padding: 0 0 0 .5rem; } .card-delete:hover { color: $red-light; @@ -160,6 +160,12 @@ font-size: 14px; justify-content: space-between; padding: 0 1rem .5rem; + .card-status-tag { + font-size: .75rem; + height: .1rem; + line-height: .1rem; + padding: 0 .5em 0 0; + } .card-estimation.not-estimated { font-size: .8125rem; } 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 7f0a19d3..629a260e 100644 --- a/app/modules/components/due-date/due-date-popover.directive.coffee +++ b/app/modules/components/due-date/due-date-popover.directive.coffee @@ -22,24 +22,6 @@ 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] @@ -50,9 +32,28 @@ dueDatePopoverDirective = ($translate, datePickerConfigService) -> el.find(".date-picker-popover").popover().close() scope.$apply() }) - el.picker = new Pikaday(datePickerConfig) + el.on "click", ".date-picker-popover-trigger", (event) -> + if ctrl.disabled() + return + event.preventDefault() + event.stopPropagation() + if !el.picker.getDate() + el.picker.setDate(moment(ctrl.dueDate).format('YYYY-MM-DD')) + el.find(".date-picker-popover").popover().open() + + el.on "click", ".date-picker-clean", (event) -> + event.preventDefault() + event.stopPropagation() + ctrl.dueDate = null + el.picker.setDate(ctrl.dueDate) + el.find(".date-picker-popover").popover().close() + scope.$apply() + + scope.$on "status:changed", (ctx, status) -> + ctrl.isClosed = ctrl.item.is_closed + controller: "DueDateCtrl", controllerAs: "vm", bindToController: true, diff --git a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-issue.jade b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-issue.jade index 43b9ae21..0edcc1f5 100644 --- a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-issue.jade +++ b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-issue.jade @@ -1,40 +1,32 @@ -extends lb-create-edit - -block options - section.ticket-assigned-to( - tg-assigned-to-inline +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( + not-auto-save="true" ng-model="obj" - required-perm="modify_{{ objType }}" ) - div.ticket-data-container - tg-issue-type-button.ticket-status( - not-auto-save="true" - ng-model="obj" - ) - tg-issue-severity-button.ticket-status( - not-auto-save="true" - ng-model="obj" - ) - tg-issue-priority-button.ticket-status( - not-auto-save="true" - 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" + tg-issue-severity-button.ticket-status( + not-auto-save="true" + ng-model="obj" ) + tg-issue-priority-button.ticket-status( + not-auto-save="true" + 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") diff --git a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-task.jade b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-task.jade index 13c9994d..713c825d 100644 --- a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-task.jade +++ b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-task.jade @@ -1,40 +1,32 @@ -extends lb-create-edit - -block options - section.ticket-assigned-to( - tg-assigned-to-inline - ng-model="obj" - required-perm="modify_{{ objType }}" +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.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 }" + 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-lock") - - tg-blocking-message-input( - watch="obj.is_blocked" - ng-model="obj.blocked_note" - ) + 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") diff --git a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-us.jade b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-us.jade index 94173646..af33d1fa 100644 --- a/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-us.jade +++ b/app/partials/common/lightbox/lightbox-create-edit/lb-create-edit-us.jade @@ -1,44 +1,36 @@ -extends lb-create-edit +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") -block options - section.ticket-assigned-to.multiple-assign( - tg-assigned-users-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.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() }" ) - 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") + 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" - ) + 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") 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 4d4d12bd..6ee654da 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 @@ -1,62 +1,139 @@ tg-lightbox-close -form - h2.title {{ text.title }} - div.form-wrapper - main - fieldset - input( - type="text" - name="subject" - ng-model-options="{ debounce: 200 }" - ng-model="obj.subject" - placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}" +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 } }} + + .existing-or-new-selector(ng-show="getOrCreate == true") + .existing-or-new-selector-single + input( + type="radio" + name="related-with-selector" + id="add-existing" + value="add-existing" + ng-model="mode" + ) + label.e2e-existing-user-story-label(for="add-existing") + span.name {{ 'LIGHTBOX.CREATE_EDIT.EXISTING_OBJECT' | translate: { objName: objName } }} + + .existing-or-new-selector-single + input( + type="radio" + name="related-with-selector" + id="new" + value="new" + ng-model="mode" + ) + label.e2e-new-userstory-label(for="new") + span.name {{ 'LIGHTBOX.CREATE_EDIT.NEW_OBJECT' | translate: { objName: objName } }} + + div(ng-if="mode == 'add-existing'") + .existing-item-wrapper + label(for="existing-filter") {{ 'LIGHTBOX.CREATE_EDIT.CHOOSE_EXISTING' | translate: { objName: objName } }} + input.filter( + id="existing-filter" + name="existing-filter" + type="text" + ng-model="existingFilterText" + ng-model-options="{ debounce: 200 }" + ng-change="existingFilterChanged(existingFilterText)" + ) + + .existing-item(ng-show="existingItems") + select.userstory.e2e-userstories-select( + size="5" + ng-model="selectedItem" data-required="true" - data-maxlength="500" + ) + - var hash = "#"; + option.hidden(value="") + option( + ng-repeat="(ref, obj) in existingItems" + ng-class="obj.class" + value="{{ ::ref }}" + ) #{hash}{{ ref }} {{ obj.html }} + + p.no-stories-found( + ng-show="existingFilterText && !existingItems" + translate="EPIC.NO_USERSTORIES_FOUND" + ) {{ 'LIGHTBOX.CREATE_EDIT.NO_ITEMS_FOUND' | translate }} + + button.button-green.add-existing-button( + ng-click="addExisting(selectedItem)" + ng-disabled="!selectedItem" + ) {{ 'COMMON.ADD' | translate }} {{ objName }} + + div(ng-if="mode != 'add-existing'") + .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" + 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 }} + + div(ng-switch="objType") + div(ng-switch-when="issue") + include lb-create-edit-issue + div(ng-switch-when="task") + include lb-create-edit-task + div(ng-switch-when="us") + include lb-create-edit-us + + tg-blocking-message-input( + watch="obj.is_blocked" + ng-model="obj.blocked_note" ) - 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") {{ text.action }} + button.button-green.submit-button(type="submit", ng-switch="mode") + span(ng-switch-when="new") {{ 'COMMON.CREATE' | translate }} + span(ng-switch-when="edit") {{ 'COMMON.SAVE' | translate }} div.lightbox.lightbox-select-user(tg-lb-assignedto) \ No newline at end of file diff --git a/app/partials/includes/modules/taskboard-table.jade b/app/partials/includes/modules/taskboard-table.jade index 8a72d18a..6b749d5e 100644 --- a/app/partials/includes/modules/taskboard-table.jade +++ b/app/partials/includes/modules/taskboard-table.jade @@ -83,7 +83,7 @@ div.taskboard-table( ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" tg-class-permission="{'readonly': '!modify_task'}" tg-bind-scope, - on-toggle-fold="ctrl.toggleFold(id)" + on-toggle-fold="ctrl.toggleFold(id, 'tasks')" on-click-edit="ctrl.editTask(id)" on-click-delete="ctrl.deleteTask(id)" on-click-assigned-to="ctrl.changeTaskAssignedTo(id)" @@ -129,7 +129,7 @@ div.taskboard-table( tg-repeat="task in usTasks.getIn(['null', st.id.toString()]) track by task.get('id')" ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" tg-class-permission="{'readonly': '!modify_task'}" - on-toggle-fold="ctrl.toggleFold(id)" + on-toggle-fold="ctrl.toggleFold(id, 'tasks')" on-click-edit="ctrl.editTask(id)" on-click-delete="ctrl.deleteTask(id)" on-click-assigned-to="ctrl.changeTaskAssignedTo(id)" @@ -141,22 +141,23 @@ div.taskboard-table( ) div.taskboard-row.issues-row(ng-class="{'row-fold':usFolded[0]}") div.taskboard-row-title-box.taskboard-column - a.vfold( - href="" - title="{{'TASKBOARD.TABLE.TITLE_ACTION_FOLD_ROW' | translate}}" - ng-click='foldUs(0)' - ng-class="{hidden:usFolded[0]}" - ) - tg-svg.fold-action(svg-icon="icon-fold-row") - a.vunfold( - href="" - title="{{'TASKBOARD.TABLE.TITLE_ACTION_UNFOLD_ROW' | translate}}" - ng-click='foldUs(0)' - ng-class="{hidden:!usFolded[0]}" - ) - tg-svg.fold-action(svg-icon="icon-unfold-row") - h3.task-colum-name(translate="TASKBOARD.TABLE.ROW_ISSUES_TITLE") - include ../components/addnewissue.jade + div.task-colum-name + a.toggle-fold.vfold( + href="" + title="{{'TASKBOARD.TABLE.TITLE_ACTION_FOLD_ROW' | translate}}" + ng-click='foldUs(0)' + ng-class="{hidden:usFolded[0]}" + ) + tg-svg.fold-action(svg-icon="icon-fold-row") + a.toggle-fold.vunfold( + href="" + title="{{'TASKBOARD.TABLE.TITLE_ACTION_UNFOLD_ROW' | translate}}" + ng-click='foldUs(0)' + ng-class="{hidden:!usFolded[0]}" + ) + tg-svg.fold-action(svg-icon="icon-unfold-row") + span.row-title(translate="TASKBOARD.TABLE.ROW_ISSUES_TITLE") + include ../components/addnewissue.jade div.taskboard-cards-box tg-card.card.ng-animate-disabled( @@ -164,8 +165,9 @@ div.taskboard-table( class="kanban-task-minimized" tg-class-permission="{'readonly': '!modify_issue'}" tg-bind-scope, - on-toggle-fold="ctrl.toggleFold(id)" + on-toggle-fold="ctrl.toggleFold(id, 'issues')" on-click-edit="ctrl.editIssue(id)" + on-click-remove="ctrl.removeIssueFromSprint(id)" on-click-delete="ctrl.deleteIssue(id)" on-click-assigned-to="ctrl.changeIssueAssignedTo(id)" project="project" diff --git a/app/styles/modules/backlog/taskboard-table.scss b/app/styles/modules/backlog/taskboard-table.scss index 124e8e2f..a37d4ab8 100644 --- a/app/styles/modules/backlog/taskboard-table.scss +++ b/app/styles/modules/backlog/taskboard-table.scss @@ -54,20 +54,7 @@ $column-padding: .5rem 1rem; padding-right: 1rem; } } -} -.taskboard-table-header { - flex-basis: 2.4rem; - flex-grow: 0; - flex-shrink: 0; - min-height: 2.4rem; - position: relative; - width: 100%; - .taskboard-table-inner { - display: flex; - overflow: hidden; - position: absolute; - } .task-colum-name { @include font-size(medium); align-items: center; @@ -104,6 +91,20 @@ $column-padding: .5rem 1rem; @include ellipsis(65%); } } +} + +.taskboard-table-header { + flex-basis: 2.4rem; + flex-grow: 0; + flex-shrink: 0; + min-height: 2.4rem; + position: relative; + width: 100%; + .taskboard-table-inner { + display: flex; + overflow: hidden; + position: absolute; + } tg-svg { display: block; margin-right: .3rem; @@ -184,6 +185,22 @@ $column-padding: .5rem 1rem; max-height: 400px; width: 100%; } + .taskboard-row-title-box { + padding: 0; + } + .task-colum-name { + justify-content: flex-start; + padding: .5rem .5rem .5rem 2em; + } + .row-title { + flex-grow: 1; + } + .toggle-fold { + display: block; + left: .5rem; + position: absolute; + top: -.4rem; + } .card { cursor: default; height: auto; diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index b4ccc8e1..102bb447 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -21,23 +21,40 @@ min-height: 4.5rem; resize: vertical; } - label { - @include font-size(xsmall); - background: $mass-white; - border: 1px solid $gray-light; - color: $grayer; - cursor: pointer; - display: block; - padding: 7px 30px; - transition: all .2s ease-in; - &:hover { - span { + + .existing-or-new-selector { + display: flex; + margin-bottom: 2rem; + input { + display: none; + &:checked+label { + background: $primary-light; color: $white; + transition: background .2s ease-in; + } + &:checked+label:hover { + background: $primary-light; + } + +label { + background: rgba($whitish, .7); + cursor: pointer; + display: block; + font-size: 1em; + padding: 2rem 1rem; + text-align: center; + text-transform: uppercase; + transition: background .2s ease-in; + } + +label:hover { + background: rgba($primary-light, .3); + transition: background .2s ease-in; } } - span { - color: $grayer; - vertical-align: middle; + .existing-or-new-selector-single { + flex: 1; + &:first-child { + margin-right: .5rem; + } } } @@ -622,14 +639,25 @@ margin-bottom: $spacing * 2; main { flex-grow: 1; - margin-right: $spacing; + max-width: $width - $sidebar-width; } .sidebar { border-left: 2px solid $whitish; + margin-left: $spacing; padding-left: $spacing; + $min-width: $sidebar-width; width: $sidebar-width; } } + .existing-item-wrapper { + margin-bottom: $spacing * 2; + select .strong { + @include font-type(bold); + } + } + .add-existing-button { + width: 100%; + } .status-button { display: flex; position: relative;