From 572ba7a8b9608b1ccd6a5870a6a78d1e4161b2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Fri, 20 Jul 2018 10:58:01 +0200 Subject: [PATCH] Search List component --- app/coffee/modules/common/lightboxes.coffee | 77 +++++----------- app/coffee/modules/issues/detail.coffee | 16 ++-- app/coffee/modules/taskboard/main.coffee | 16 ++-- app/locales/taiga/locale-en.json | 2 +- .../search-list/search-list-issue-choice.jade | 3 + .../search-list-sprint-choice.jade | 2 + .../search-list/search-list.directive.coffee | 91 +++++++++++++++++++ .../components/search-list/search-list.jade | 23 +++++ .../components/search-list/search-list.scss | 36 ++++++++ .../lightbox-create-edit/lb-create-edit.jade | 36 ++------ .../issue/assign-sprint-to-issue-button.jade | 58 ++++-------- app/styles/modules/common/lightbox.scss | 4 + 12 files changed, 224 insertions(+), 140 deletions(-) create mode 100644 app/modules/components/search-list/search-list-issue-choice.jade create mode 100644 app/modules/components/search-list/search-list-sprint-choice.jade create mode 100644 app/modules/components/search-list/search-list.directive.coffee create mode 100644 app/modules/components/search-list/search-list.jade create mode 100644 app/modules/components/search-list/search-list.scss diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index eaa11263..4bd1ab9a 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -840,40 +840,43 @@ $confirm, $q, attachmentsService, $template, $compile) -> } } - $scope.$on "genericform:new", (ctx, data) -> - getSchema(data) + $scope.$on "genericform:new", (ctx, params) -> + getSchema(params) $scope.mode = 'new' $scope.getOrCreate = false - mount(data) + mount(params) - $scope.$on "genericform:new-or-existing", (ctx, data) -> - getSchema(data) + $scope.$on "genericform:new-or-existing", (ctx, params) -> + getSchema(params) $scope.mode = 'add-existing' $scope.getOrCreate = true $scope.existingFilterText = '' - $scope.existingItems = {} - $scope.existingOptions = data.existingOptions - mount(data) + $scope.existingItems = [] + $rs[schema.model].listInAllProjects({ project: $scope.project.id }, true).then (data) -> + $scope.existingItems = data + mount(params) - $scope.$on "genericform:edit", (ctx, data) -> - getSchema(data) + $scope.$on "genericform:edit", (ctx, params) -> + getSchema(params) $scope.mode = 'edit' $scope.getOrCreate = false - mount(data) + mount(params) + + getSchema = (params) -> + _.map params, (value, key) -> + $scope[key] = value - getSchema = (data) -> - $scope.objType = data.objType if !$scope.objType || !schemas[$scope.objType] return $log.error("Invalid objType `#{$scope.objType}` for `genericform` event") schema = schemas[$scope.objType] - mount = (data) -> + mount = (params) -> $scope.objName = schema.objName if $scope.mode == 'edit' - $scope.obj = data.obj - $scope.attachments = Immutable.fromJS(data.attachments) + $scope.obj = params.obj + $scope.attachments = Immutable.fromJS(params.attachments) else - $scope.obj = $model.make_model(schema.model, schema.initialData(data)) + $scope.obj = $model.make_model(schema.model, schema.initialData(params)) $scope.attachments = Immutable.List() _.map schema.data($scope.project), (value, key) -> @@ -939,35 +942,13 @@ $confirm, $q, attachmentsService, $template, $compile) -> return attachmentsService.delete($scope.objType, attachment.id) return $q.all(promises) - addExisting = (ref) -> + addExisting = (item) -> 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) -> + item.setAttr($scope.relatedField, $scope.relatedObjectId) + $repo.save(item, 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 - ) + $rootScope.$broadcast("#{$scope.objType}form:add:success", item) $scope.addExisting = (selectedItem) -> event.preventDefault() @@ -1079,16 +1060,6 @@ $confirm, $q, attachmentsService, $template, $compile) -> $scope.selectedStatus = _.find $scope.statusList, (item) -> item.id == id $scope.obj.is_closed = $scope.selectedStatus.is_closed - 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) - return { link: link templateUrl: "common/lightbox/lightbox-create-edit/lb-create-edit.html" diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index c871e0fc..b76df140 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -110,7 +110,10 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$on "assign-sprint-to-issue:success", (ctx, milestoneId) => @rootscope.$broadcast("object:updated") @scope.issue.milestone = milestoneId - @.loadSprint() + if milestoneId + @.loadSprint() + else + @scope.sprint = null initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @@ -713,6 +716,7 @@ AssignSprintToIssueButtonDirective = ($rootScope, $rs, $repo, $loading, $transla issue = $model.$modelValue $rs.sprints.list($scope.projectId, null).then (data) -> $scope.milestones = data.milestones + $scope.selectedSprintId = issue.milestone avaliableMilestones = angular.copy($scope.milestones) lightboxService.open($el.find(".lightbox-assign-sprint-to-issue")) @@ -729,15 +733,13 @@ AssignSprintToIssueButtonDirective = ($rootScope, $rs, $repo, $loading, $transla existsMilestone(filterText, milestone.name) ) - $scope.saveIssueToSprint = (selectedSprintId) -> - currentLoading = $loading().target($el.find(".e2e-select-related-sprint-button")).start() - issue.setAttr('milestone', selectedSprintId) + $scope.saveIssueToSprint = (selectedSprint, event) -> + currentLoading = $loading().target($(event.currentTarget)).start() + issue.setAttr('milestone', selectedSprint.id) $repo.save(issue, true).then (data) -> currentLoading.finish() lightboxService.close($el.find(".lightbox-assign-sprint-to-issue")) - $scope.$broadcast("assign-sprint-to-issue:success", selectedSprintId) - - + $scope.$broadcast("assign-sprint-to-issue:success", selectedSprint.id) return { link: link diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index a2460d45..1e829018 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -385,7 +385,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @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) @@ -623,15 +622,12 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga switch type when "standard" then @rootscope.$broadcast("genericform:new-or-existing", { - 'objType': 'issue', - 'project': @scope.project, - 'sprintId': @scope.sprintId, - 'existingOptions': { - targetField: 'milestone', - targetValue: @scope.sprintId, - targetsById: @scope.milestonesById, - title: "#{@translate.instant("COMMON.FIELDS.SPRINT")} #{@scope.sprint.name}", - } + objType: 'issue', + project: @scope.project, + sprintId: @scope.sprintId, + relatedField: 'milestone', + relatedObjectId: @scope.sprintId, + 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) diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index c5fd40ce..d9b07c77 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -1434,7 +1434,7 @@ "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Go to the taskboard", "FILTER_SPRINTS": "Filter Sprints", - "CHOOSE_SPRINT": "What's the user Sprint?", + "CHOOSE_SPRINT": "What's the Sprint?", "FIELDS": { "PRIORITY": "Priority", "SEVERITY": "Severity", diff --git a/app/modules/components/search-list/search-list-issue-choice.jade b/app/modules/components/search-list/search-list-issue-choice.jade new file mode 100644 index 00000000..2f82a901 --- /dev/null +++ b/app/modules/components/search-list/search-list-issue-choice.jade @@ -0,0 +1,3 @@ +- var hash = "#"; +div.title #{hash}{{ item.ref }} {{ item.subject }} +div.info(ng-if="item.milestone") {{ milestonesById[item.milestone].name }} diff --git a/app/modules/components/search-list/search-list-sprint-choice.jade b/app/modules/components/search-list/search-list-sprint-choice.jade new file mode 100644 index 00000000..0d50fb05 --- /dev/null +++ b/app/modules/components/search-list/search-list-sprint-choice.jade @@ -0,0 +1,2 @@ +div.title {{ item.name }} +div.info {{ item.estimated_start }} - {{ item.estimated_finish }} diff --git a/app/modules/components/search-list/search-list.directive.coffee b/app/modules/components/search-list/search-list.directive.coffee new file mode 100644 index 00000000..21812cfa --- /dev/null +++ b/app/modules/components/search-list/search-list.directive.coffee @@ -0,0 +1,91 @@ +### +# 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: search-list.directive.coffee +### + +module = angular.module("taigaComponents") + +normalizeString = @.taiga.normalizeString +groupBy = @.taiga.groupBy + +searchListDirective = ($translate) -> + link = (scope, el, attrs, model) -> + scope.templateUrl = "components/search-list/search-list-#{scope.itemType}-choice.html" + scope.currentSelected = null + filtering = false + avaliableItems = [] + itemsById = {} + + if scope.itemType == 'issue' + scope.milestonesById = groupBy(scope.project.milestones, (e) -> e.id) + + el.on "click", ".choice", (event) -> + choiceId = parseInt($(event.currentTarget).data("choice-id")) + value = if attrs.ngModel?.id != choiceId then itemsById[choiceId] else null + model.$setViewValue(value) + scope.currentSelected = value + scope.$apply() + + isContainedIn = (needle, haystack) -> + return _.includes(parseString(haystack), parseString(needle)) + + parseString = (value) -> + if typeof value != 'string' + value = value.toString() + return normalizeString(value.toUpperCase()) + + el.on "blur", "#items-filter", (event) -> + filtering = false + + el.on "focus", "#items-filter", (event) -> + filtering = true + + scope.filterItems = (searchText) -> + scope.items = avaliableItems.filter((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) + + 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) + + return { + link: link, + templateUrl: "components/search-list/search-list.html", + require: "ngModel", + scope: { + label: '@', + placeholder: '@', + project: '=', + filterBy: '=', + items: '=', + itemType: '@', + resetOnChange: "=" + } + } + +module.directive('tgSearchList', ['$translate', searchListDirective]) diff --git a/app/modules/components/search-list/search-list.jade b/app/modules/components/search-list/search-list.jade new file mode 100644 index 00000000..8990adb2 --- /dev/null +++ b/app/modules/components/search-list/search-list.jade @@ -0,0 +1,23 @@ +fieldset.search-list + label( + for="items-filter" + ) {{ label }} + input.items-filter( + id="items-filter" + type="text" + placeholder="{{ placeholder }}" + 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-include(src="templateUrl") + + p.no-stories-found( + ng-show="!items.length" + ) {{ noItemsText }} \ No newline at end of file diff --git a/app/modules/components/search-list/search-list.scss b/app/modules/components/search-list/search-list.scss new file mode 100644 index 00000000..f0ef9350 --- /dev/null +++ b/app/modules/components/search-list/search-list.scss @@ -0,0 +1,36 @@ +.search-list { + ul { + background: $mass-white; + border: 1px solid $gray-light; + height: 200px; + margin: .25em 0 0; + max-height: 200px; + overflow-y: auto; + } + li { + cursor: pointer; + padding: .25em .5em; + &.selected { + background: $yellow-green; + color: $white; + .info { + color: $white; + } + } + ng-include { + display: flex; + width: 100%; + .title { + flex-grow: 1; + text-align: left; + } + .info { + color: $gray-lighter; + text-align: right; + } + } + .title span { + margin-right: .5em; + } + } +} 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 6ee654da..a6f49bc3 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 @@ -31,35 +31,17 @@ form(ng-if="lightboxOpen") 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)" + tg-search-list( + label="{{ 'LIGHTBOX.CREATE_EDIT.CHOOSE_EXISTING' | translate: { objName: objName } }}" + placeholder="{{ 'ISSUES.FILTER_SPRINTS' | translate }}" + items="existingItems" + ng-model="selectedItem" + filter-by="['ref', 'subject']" + project="project" + item-type="{{ objType }}" + reset-on-change="true" ) - .existing-item(ng-show="existingItems") - select.userstory.e2e-userstories-select( - size="5" - ng-model="selectedItem" - data-required="true" - ) - - 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" diff --git a/app/partials/issue/assign-sprint-to-issue-button.jade b/app/partials/issue/assign-sprint-to-issue-button.jade index f2845557..d13c4b34 100644 --- a/app/partials/issue/assign-sprint-to-issue-button.jade +++ b/app/partials/issue/assign-sprint-to-issue-button.jade @@ -11,45 +11,19 @@ a.assign-issue-button.button-gray.is-editable( tg-lightbox-close div.lightbox-assign-related-sprint - h2 "{{ 'ISSUES.ACTION_ASSIGN_SPRINT' | translate }}" - - fieldset.existing-sprint - label( - translate="ISSUES.CHOOSE_SPRINT" - for="sprint-filter" - ) - input.sprint-filter.e2e-filter-sprints-input( - id="sprint-filter" - type="text" - placeholder="{{'ISSUES.FILTER_SPRINTS' | translate}}" - ng-model="filterText" - ng-change="filterMilestones(filterText)" - ) - - form.existing-user-story-form - select.userstory.e2e-userstories-select( - size="5" - data-required="true" - ng-model="selectedSprint" - ) - - var hash = "#"; - option.hidden( - value="" - ) - option( - ng-repeat="milestone in milestones | toMutable track by milestone.id" - value="{{ ::milestone.id }}" - ) #{hash}{{::milestone.name}} - - p.no-stories-found( - ng-show="!milestones.length" - translate="EPIC.NO_USERSTORIES_FOUND" - ) - - button.button-green.e2e-select-related-sprint-button( - href="" - ng-click="saveIssueToSprint(selectedSprint)" - ng-disabled="!selectedSprint" - tg-loading="vm.loading" - translate="COMMON.SAVE" - ) + h2.title {{ 'ISSUES.ACTION_ASSIGN_SPRINT' | translate }} + tg-search-list( + label="{{ 'ISSUES.CHOOSE_SPRINT' | translate }}" + placeholder="{{ 'ISSUES.FILTER_SPRINTS' | translate }}" + items="milestones" + ng-model="selectedSprint" + filter-by="['name', 'estimated_start', 'estimated_finish']" + project="project" + item-type="sprint" + ) + button.button-green.select-option( + href="" + ng-click="saveIssueToSprint(selectedSprint, $event)" + tg-loading="vm.loading" + translate="COMMON.SAVE" + ) diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index 1b6a8696..a0e68390 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -856,6 +856,10 @@ .ticket-detail-settings .lightbox-assign-sprint-to-issue { + .lightbox-assign-related-sprint { + width: 700px; + } + svg { fill: initial; max-height: initial;