summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/group_label_subscription.js18
-rw-r--r--app/assets/javascripts/label_manager.js15
-rw-r--r--app/assets/javascripts/project_label_subscription.js28
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/labels.scss227
-rw-r--r--app/controllers/groups/labels_controller.rb23
-rw-r--r--app/helpers/labels_helper.rb8
-rw-r--r--app/models/label.rb4
-rw-r--r--app/views/groups/labels/index.html.haml47
-rw-r--r--app/views/projects/labels/index.html.haml34
-rw-r--r--app/views/shared/_label.html.haml133
-rw-r--r--app/views/shared/_label_row.html.haml35
-rw-r--r--changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml5
-rw-r--r--spec/features/projects/labels/subscription_spec.rb6
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb8
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb20
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
&middot;
- = 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