diff options
-rw-r--r-- | app/assets/javascripts/group_label_subscription.js | 18 | ||||
-rw-r--r-- | app/assets/javascripts/label_manager.js | 15 | ||||
-rw-r--r-- | app/assets/javascripts/project_label_subscription.js | 28 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/variables.scss | 2 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/labels.scss | 227 | ||||
-rw-r--r-- | app/controllers/groups/labels_controller.rb | 23 | ||||
-rw-r--r-- | app/helpers/labels_helper.rb | 8 | ||||
-rw-r--r-- | app/models/label.rb | 4 | ||||
-rw-r--r-- | app/views/groups/labels/index.html.haml | 47 | ||||
-rw-r--r-- | app/views/projects/labels/index.html.haml | 34 | ||||
-rw-r--r-- | app/views/shared/_label.html.haml | 133 | ||||
-rw-r--r-- | app/views/shared/_label_row.html.haml | 35 | ||||
-rw-r--r-- | changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml | 5 | ||||
-rw-r--r-- | spec/features/projects/labels/subscription_spec.rb | 6 | ||||
-rw-r--r-- | spec/features/projects/labels/update_prioritization_spec.rb | 8 | ||||
-rw-r--r-- | spec/features/projects/labels/user_removes_labels_spec.rb | 20 |
16 files changed, 331 insertions, 282 deletions
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 5648cb9a888..d33e3a37580 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,7 +1,12 @@ import $ from 'jquery'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { __ } from './locale'; + +const tooltipTitles = { + group: __('Unsubscribe at group level'), + project: __('Unsubscribe at project level'), +}; export default class GroupLabelSubscription { constructor(container) { @@ -35,6 +40,7 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); axios.post(url) + .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); } @@ -44,4 +50,14 @@ export default class GroupLabelSubscription { this.$subscribeButtons.toggleClass('hidden'); this.$unsubscribeButtons.toggleClass('hidden'); } + + static setNewTooltip($button) { + if (!$button.hasClass('js-subscribe-button')) return; + + const type = $button.hasClass('js-group-level') ? 'group' : 'project'; + const newTitle = tooltipTitles[type]; + + $('.js-unsubscribe-button', $button.closest('.label-actions-list')) + .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 8c3de6e4045..8b01024b7d4 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -13,6 +13,7 @@ export default class LabelManager { this.otherLabels = otherLabels || $('.js-other-labels'); this.errorMessage = 'Unable to update label prioritization at this time'; this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.$badgeItemTemplate = $('#js-badge-item-template'); this.sortable = Sortable.create(this.prioritizedLabels.get(0), { filter: '.empty-message', forceFallback: true, @@ -63,7 +64,11 @@ export default class LabelManager { $target = this.otherLabels; $from = this.prioritizedLabels; } - $label.detach().appendTo($target); + + const $detachedLabel = $label.detach(); + this.toggleLabelPriorityBadge($detachedLabel, action); + $detachedLabel.appendTo($target); + if ($from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } @@ -88,6 +93,14 @@ export default class LabelManager { } } + toggleLabelPriorityBadge($label, action) { + if (action === 'remove') { + $('.js-priority-badge', $label).remove(); + } else { + $('.label-links', $label).append(this.$badgeItemTemplate.clone().html()); + } + } + onPrioritySortUpdate() { this.savePrioritySort() .catch(() => flash(this.errorMessage)); diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 6f06944ebb6..9049f87e037 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -3,6 +3,17 @@ import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +const tooltipTitles = { + group: { + subscribed: __('Unsubscribe at group level'), + unsubscribed: __('Subscribe at group level'), + }, + project: { + subscribed: __('Unsubscribe at project level'), + unsubscribed: __('Subscribe at project level'), + }, +}; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -15,12 +26,10 @@ export default class ProjectLabelSubscription { event.preventDefault(); const $btn = $(event.currentTarget); - const $span = $btn.find('span'); const url = $btn.attr('data-url'); const oldStatus = $btn.attr('data-status'); $btn.addClass('disabled'); - $span.toggleClass('hidden'); axios.post(url).then(() => { let newStatus; @@ -32,21 +41,28 @@ export default class ProjectLabelSubscription { [newStatus, newAction] = ['unsubscribed', 'Subscribe']; } - $span.toggleClass('hidden'); $btn.removeClass('disabled'); this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { + this.$buttons.map((i, button) => { const $button = $(button); + const originalTitle = $button.attr('data-original-title'); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle'); + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); } return button; }); }).catch(() => flash(__('There was an error subscribing to this label.'))); } + + static setNewTitle($button, originalTitle, newStatus) { + const type = /group/.test(originalTitle) ? 'group' : 'project'; + const newTitle = tooltipTitles[type][newStatus]; + + $button.attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 946223cfff0..0702272e3ed 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -776,3 +776,5 @@ $modal-body-height: 134px; Prometheus */ $prometheus-table-row-highlight-color: $theme-gray-100; + +$priority-label-empty-state-width: 114px;
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 25f011a534b..712e881de6f 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -57,67 +57,6 @@ border-bottom-left-radius: $border-radius-base; } -.label-row { - .label-name { - display: inline-block; - margin-bottom: 10px; - - @include media-breakpoint-up(sm) { - width: 200px; - margin-left: $gl-padding * 2; - margin-bottom: 0; - } - - .badge { - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - } - } - - .label-type { - display: block; - margin-bottom: 10px; - margin-left: 50px; - - @include media-breakpoint-up(sm) { - display: inline-block; - width: 100px; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .label-description { - display: block; - margin-bottom: 10px; - - .description-text { - margin-bottom: $gl-padding; - } - - a { - color: $blue-600; - } - - @include media-breakpoint-up(sm) { - display: inline-block; - max-width: 50%; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .badge { - padding: 4px $grid-size; - font-size: $label-font-size; - position: relative; - top: ($grid-size / 2); - } -} - .color-label { padding: 0 $grid-size; line-height: 16px; @@ -133,26 +72,29 @@ } .manage-labels-list { - @media(min-width: map-get($grid-breakpoints, md)) { - &.content-list li { - padding: $gl-padding 0; - } - } - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - - &:active { - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } + margin-bottom: 5px; + display: flex; + justify-content: space-between; + padding: 8px 8px 8px $gl-padding; + border-radius: $border-radius-default; &.sortable-ghost { opacity: 0.3; } + + .prioritized-labels & { + box-shadow: 0 1px 2px $issue-boards-card-shadow; + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + } } .btn-action { @@ -170,27 +112,6 @@ } } } - - .dropdown { - @include media-breakpoint-up(sm) { - float: right; - } - } - - @include media-breakpoint-down(xs) { - .dropdown-menu { - min-width: 100%; - } - } -} - -.draggable-handler { - display: inline-block; - vertical-align: top; - margin: 5px 0; - opacity: 0; - transition: opacity .3s; - color: $gray-darkest; } .prioritized-labels { @@ -223,22 +144,6 @@ } } -.toggle-priority { - display: inline-block; - vertical-align: top; - - button { - border-color: transparent; - padding: 5px 8px; - vertical-align: top; - font-size: 14px; - - &:hover { - border-color: transparent; - } - } -} - .filtered-labels { font-size: 0; padding: 12px 16px; @@ -292,10 +197,8 @@ } .label-subscribe-button { - @media(min-width: map-get($grid-breakpoints, md)) { - min-width: 105px; - margin-left: $gl-padding; - } + width: 105px; + font-weight: 200; .label-subscribe-button-icon { &[disabled] { @@ -332,3 +235,95 @@ font-size: $label-font-size; } } + +.labels-container { + background-color: $gray-light; + border-radius: $border-radius-default; + padding: $gl-padding $gl-padding-8; +} + +.label-actions-list { + list-style: none; + flex-shrink: 0; + padding: 0; +} + +.label-badge { + color: $theme-gray-900; + font-weight: $gl-font-weight-normal; + padding: $gl-padding-4 $gl-padding-8; + border-radius: $border-radius-default; + font-size: $label-font-size; +} + +.label-badge-blue { + background-color: $theme-blue-100; +} + +.label-badge-gray { + background-color: $theme-gray-100; +} + +.label-links { + list-style: none; + padding: 0; + white-space: nowrap; +} + +.label-link-item { + padding: 0; +} + +.label-list-item { + .content-list &::before, + .content-list &::after { + content: none; + } + + .label-name { + width: 150px; + flex-shrink: 0; + + .label { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + } + + .label-description { + flex-grow: 1; + + a { + color: $blue-600; + } + } + + .label { + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: $gl-padding-4; + } + + .label-action { + color: $theme-gray-800; + cursor: pointer; + + svg { + fill: $theme-gray-800; + } + + &:hover { + color: $blue-600; + + svg { + fill: $blue-600; + } + } + } +} + +.priority-labels-empty-state .svg-content img { + max-width: $priority-label-empty-state-width; +} diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 58be330f466..863f50e8e66 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] + before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController format.html do @labels = @group.labels.page(params[:page]) end - format.json do - available_labels = LabelsFinder.new( - current_user, - group_id: @group.id, - only_group_labels: params[:only_group_labels], - include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute - - render json: LabelSerializer.new.represent_appearance(available_labels) + render json: LabelSerializer.new.represent_appearance(@available_labels) end end end @@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController def save_previous_label_path session[:previous_labels_path] = URI(request.referer || '').path end + + def available_labels + @available_labels ||= + LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1b0e7a4a3e..c7df25cecef 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -211,6 +211,14 @@ module LabelsHelper end end + def label_status_tooltip(label, status) + type = label.is_a?(ProjectLabel) ? 'project' : 'group' + level = status.unsubscribed? ? type : status.sub('-level', '') + action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe' + + "#{action} at #{level} level" + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/models/label.rb b/app/models/label.rb index de7f1d56c64..1cf04976602 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -137,6 +137,10 @@ class Label < ActiveRecord::Base priority.try(:priority) end + def priority? + priorities.present? + end + def template? template end diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index ac7e12fcd0b..e6b20385792 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,21 +1,34 @@ -- page_title 'Labels' - +- @no_container = true +- page_title "Labels" +- can_admin_label = can?(current_user, :admin_label, @group) +- hide_class = '' +- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] -.top-area.adjust - .nav-text - = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence } +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" + +- if @labels.exists? + #promote-label-modal + %div{ class: container_class } + .top-area.adjust + .nav-text + = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } + - if can_admin_label + = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') - .nav-controls - - if can?(current_user, :admin_label, @group) - = link_to "New label", new_group_label_path(@group), class: "btn btn-new" + .labels-container.prepend-top-5 + .other-labels + - if can_admin_label + %h5{ class: ('hide' if hide) } Labels + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: true } + = paginate @labels, theme: 'gitlab' +- else + = render 'shared/empty_states/labels' -.labels - .other-labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - = _("No labels created yet.") +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 1f183c274be..fb5b0fc15c9 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,40 +1,44 @@ - @no_container = true - page_title "Labels" -- hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) +- hide_class = '' + +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" - if @labels.exists? || @prioritized_labels.exists? #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + = _('Labels can be applied to issues and merge requests.') - if can_admin_label - Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') - - if can_admin_label - .nav-controls - = link_to new_project_label_path(@project), class: "btn btn-new" do - New label - - .labels + .labels-container.prepend-top-5 - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } - %h5.prepend-top-10 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } - #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } + %h5.prepend-top-10= _('Prioritized Labels') + .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } + #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } = render 'shared/empty_states/priority_labels' - if @prioritized_labels.present? - = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } - if @labels.present? .other-labels - if can_admin_label - %h5{ class: ('hide' if hide) } Other Labels - %ul.content-list.manage-labels-list.js-other-labels + %h5{ class: ('hide' if hide) }= _('Other Labels') + .content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else = render 'shared/empty_states/labels' + +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ba5b65a209d..658d546a4a4 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,93 +1,70 @@ - label_css_id = dom_id(label) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] +- use_label_priority = local_assigns.fetch(:use_label_priority, false) +- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) +- tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .d-inline-block.d-sm-none.dropdown - %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-right - %ul - - if show_label_merge_requests_link - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - - if show_label_issues_link - %li - = link_to_label(label, subject: subject) do - View open issues - - if current_user - %li.label-subscription - - if can_subscribe_to_label_in_different_levels?(label) - %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - %span Subscribe at project level - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - %span Subscribe at group level - - else - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } } - %span= label_subscription_toggle_button_text(label, @project) - - - if can?(current_user, :admin_label, label) - %li - = link_to 'Edit', edit_label_path(label) - %li - = link_to 'Delete', - destroy_label_path(label), - title: 'Delete', - method: :delete, - data: {confirm: 'Remove this label? Are you sure?'}, - class: 'text-danger' - - .float-right.d-none.d-sm-none.d-md-block - - if can?(current_user, :admin_label, label) - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = sprite_icon('pencil') - %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } - = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do - %span.sr-only Delete - = sprite_icon('remove') + = render "shared/label_row", label: label, subject: subject, force_priority: force_priority + %ul.label-actions-list + - if @project + %li.inline + .label-badge.label-badge-gray= label.model_name.human.capitalize + - if can?(current_user, :admin_label, @project) + %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), + dom_id: dom_id(label), type: label.type } } + %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') } + = sprite_icon('star-o') + %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') } + = sprite_icon('star') + %li.inline + = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do + = sprite_icon('pencil') + %li.inline + .dropdown + %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') } + = sprite_icon('ellipsis_v') + .dropdown-menu.dropdown-open-left + %ul + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %li + %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = _('Promote to group label') + %li + %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } + %button.text-danger.remove-row{ type: 'button' }= _('Delete') - if current_user - .label-subscription.inline + %li.inline.label-subscription - if can_subscribe_to_label_in_different_levels?(label) - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - = icon('spinner spin', class: 'label-subscribe-button-loading') - + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } + %span= _('Unsubscribe') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span Subscribe - = icon('chevron-down') - %ul.dropdown-menu - %li - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - Project level - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - Group level + %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } } + %span + = _('Subscribe') + = sprite_icon('chevron-down') + .dropdown-menu.dropdown-open-left + %ul + %li + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } + %span= _('Subscribe at project level') + %li + %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } + %span= _('Subscribe at group level') - else - %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } %span= label_subscription_toggle_button_text(label, @project) - = icon('spinner spin', class: 'label-subscribe-button-loading') = render 'shared/delete_label_modal', label: label diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index f1c1ca9b2c9..0ae3ab8f090 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,30 +1,23 @@ - subject = local_assigns[:subject] +- force_priority = local_assigns.fetch(:force_priority, false) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -%span.label-row - - if can?(current_user, :admin_label, @project) - .draggable-handler - = icon('bars') - .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), - dom_id: dom_id(label), type: label.type } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' } - = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' } - = icon('star') - %span.label-name - = link_to_label(label, subject: @project, tooltip: false) - - if defined?(@project) && @project.group.present? - %span.label-type - = label.model_name.human.titleize - - %span.label-description +.label-name + = link_to_label(label, subject: @project, tooltip: false) +.label-description + .append-right-default.prepend-left-default - if label.description.present? - .description-text + .description-text.append-bottom-10 = markdown_field(label, :description) - .d-none.d-sm-none.d-md-block + %ul.label-links - if show_label_issues_link - = link_to_label(label, subject: subject) { 'Issues' } + %li.label-link-item.inline + = link_to_label(label, subject: subject) { 'Issues' } - if show_label_merge_requests_link · - = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' } + %li.label-link-item.inline + = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') } + - if force_priority + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml new file mode 100644 index 00000000000..fb4fbf80575 --- /dev/null +++ b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml @@ -0,0 +1,5 @@ +--- +title: Label list page redesign +merge_request: 18466 +author: +type: changed diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index 70e8d436dcb..fafd338e448 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -36,7 +36,7 @@ feature 'Labels subscription' do within "#group_label_#{feature.id}" do expect(page).not_to have_button 'Unsubscribe' - click_link_on_dropdown('Group level') + click_link_on_dropdown('Subscribe at group level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -45,7 +45,7 @@ feature 'Labels subscription' do expect(page).to have_selector('.dropdown-group-label') - click_link_on_dropdown('Project level') + click_link_on_dropdown('Subscribe at project level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -68,7 +68,7 @@ feature 'Labels subscription' do find('.dropdown-group-label').click page.within('.dropdown-group-label') do - find('a.js-subscribe-button', text: text).click + find('.js-subscribe-button', text: text).click end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index ae8b1364ec7..359381c391c 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -102,16 +102,16 @@ feature 'Prioritize labels' do drag_to(selector: '.label-list-item', from_index: 1, to_index: 2) page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end refresh wait_for_requests page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end end diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb index f4fda6de465..efa74015c6e 100644 --- a/spec/features/projects/labels/user_removes_labels_spec.rb +++ b/spec/features/projects/labels/user_removes_labels_spec.rb @@ -17,8 +17,9 @@ describe "User removes labels" do end it "removes label" do - page.within(".labels") do + page.within(".other-labels") do page.first(".label-list-item") do + first('.js-label-options-dropdown').click first(".remove-row").click first(:link, "Delete label").click end @@ -36,17 +37,16 @@ describe "User removes labels" do end it "removes all labels" do - page.within(".labels") do - loop do - li = page.first(".label-list-item") - break unless li + loop do + li = page.first(".label-list-item") + break unless li - li.click_link("Delete") - click_link("Delete label") - end - - expect(page).to have_content("Generate a default set of labels").and have_content("New label") + li.find('.js-label-options-dropdown').click + li.click_button("Delete") + click_link("Delete label") end + + expect(page).to have_content("Generate a default set of labels").and have_content("New label") end end end |