Merge branch '39549-label-list-page-redesign-with-draggable-labels' into 'master'
Resolve "Label list page redesign with draggable labels" Closes #39549 See merge request gitlab-org/gitlab-ce!18466
This commit is contained in:
commit
be928829cf
16 changed files with 332 additions and 285 deletions
|
@ -1,7 +1,12 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import { __ } from '~/locale';
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
import flash from './flash';
|
import flash from './flash';
|
||||||
import { __ } from './locale';
|
|
||||||
|
const tooltipTitles = {
|
||||||
|
group: __('Unsubscribe at group level'),
|
||||||
|
project: __('Unsubscribe at project level'),
|
||||||
|
};
|
||||||
|
|
||||||
export default class GroupLabelSubscription {
|
export default class GroupLabelSubscription {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
|
@ -35,6 +40,7 @@ export default class GroupLabelSubscription {
|
||||||
this.$unsubscribeButtons.attr('data-url', url);
|
this.$unsubscribeButtons.attr('data-url', url);
|
||||||
|
|
||||||
axios.post(url)
|
axios.post(url)
|
||||||
|
.then(() => GroupLabelSubscription.setNewTooltip($btn))
|
||||||
.then(() => this.toggleSubscriptionButtons())
|
.then(() => this.toggleSubscriptionButtons())
|
||||||
.catch(() => flash(__('There was an error when subscribing to this label.')));
|
.catch(() => flash(__('There was an error when subscribing to this label.')));
|
||||||
}
|
}
|
||||||
|
@ -44,4 +50,14 @@ export default class GroupLabelSubscription {
|
||||||
this.$subscribeButtons.toggleClass('hidden');
|
this.$subscribeButtons.toggleClass('hidden');
|
||||||
this.$unsubscribeButtons.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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default class LabelManager {
|
||||||
this.otherLabels = otherLabels || $('.js-other-labels');
|
this.otherLabels = otherLabels || $('.js-other-labels');
|
||||||
this.errorMessage = 'Unable to update label prioritization at this time';
|
this.errorMessage = 'Unable to update label prioritization at this time';
|
||||||
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
|
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
|
||||||
|
this.$badgeItemTemplate = $('#js-badge-item-template');
|
||||||
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
|
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
|
||||||
filter: '.empty-message',
|
filter: '.empty-message',
|
||||||
forceFallback: true,
|
forceFallback: true,
|
||||||
|
@ -63,7 +64,11 @@ export default class LabelManager {
|
||||||
$target = this.otherLabels;
|
$target = this.otherLabels;
|
||||||
$from = this.prioritizedLabels;
|
$from = this.prioritizedLabels;
|
||||||
}
|
}
|
||||||
$label.detach().appendTo($target);
|
|
||||||
|
const $detachedLabel = $label.detach();
|
||||||
|
this.toggleLabelPriorityBadge($detachedLabel, action);
|
||||||
|
$detachedLabel.appendTo($target);
|
||||||
|
|
||||||
if ($from.find('li').length) {
|
if ($from.find('li').length) {
|
||||||
$from.find('.empty-message').removeClass('hidden');
|
$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() {
|
onPrioritySortUpdate() {
|
||||||
this.savePrioritySort()
|
this.savePrioritySort()
|
||||||
.catch(() => flash(this.errorMessage));
|
.catch(() => flash(this.errorMessage));
|
||||||
|
|
|
@ -3,6 +3,17 @@ import { __ } from './locale';
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
import flash from './flash';
|
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 {
|
export default class ProjectLabelSubscription {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
this.$container = $(container);
|
this.$container = $(container);
|
||||||
|
@ -15,12 +26,10 @@ export default class ProjectLabelSubscription {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const $btn = $(event.currentTarget);
|
const $btn = $(event.currentTarget);
|
||||||
const $span = $btn.find('span');
|
|
||||||
const url = $btn.attr('data-url');
|
const url = $btn.attr('data-url');
|
||||||
const oldStatus = $btn.attr('data-status');
|
const oldStatus = $btn.attr('data-status');
|
||||||
|
|
||||||
$btn.addClass('disabled');
|
$btn.addClass('disabled');
|
||||||
$span.toggleClass('hidden');
|
|
||||||
|
|
||||||
axios.post(url).then(() => {
|
axios.post(url).then(() => {
|
||||||
let newStatus;
|
let newStatus;
|
||||||
|
@ -32,21 +41,28 @@ export default class ProjectLabelSubscription {
|
||||||
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
|
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$span.toggleClass('hidden');
|
|
||||||
$btn.removeClass('disabled');
|
$btn.removeClass('disabled');
|
||||||
|
|
||||||
this.$buttons.attr('data-status', newStatus);
|
this.$buttons.attr('data-status', newStatus);
|
||||||
this.$buttons.find('> span').text(newAction);
|
this.$buttons.find('> span').text(newAction);
|
||||||
|
|
||||||
this.$buttons.map((button) => {
|
this.$buttons.map((i, button) => {
|
||||||
const $button = $(button);
|
const $button = $(button);
|
||||||
|
const originalTitle = $button.attr('data-original-title');
|
||||||
|
|
||||||
if ($button.attr('data-original-title')) {
|
if (originalTitle) {
|
||||||
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle');
|
ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
});
|
});
|
||||||
}).catch(() => flash(__('There was an error subscribing to this label.')));
|
}).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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -808,3 +808,5 @@ $modal-body-height: 134px;
|
||||||
Prometheus
|
Prometheus
|
||||||
*/
|
*/
|
||||||
$prometheus-table-row-highlight-color: $theme-gray-100;
|
$prometheus-table-row-highlight-color: $theme-gray-100;
|
||||||
|
|
||||||
|
$priority-label-empty-state-width: 114px;
|
||||||
|
|
|
@ -57,69 +57,8 @@
|
||||||
border-bottom-left-radius: $border-radius-base;
|
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 {
|
.color-label {
|
||||||
padding: 0 $grid-size;
|
padding: $gl-padding-4 $grid-size;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
border-radius: $label-border-radius;
|
border-radius: $label-border-radius;
|
||||||
color: $white-light;
|
color: $white-light;
|
||||||
|
@ -133,26 +72,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.manage-labels-list {
|
.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) {
|
> li:not(.empty-message):not(.is-not-draggable) {
|
||||||
background-color: $white-light;
|
background-color: $white-light;
|
||||||
cursor: move;
|
margin-bottom: 5px;
|
||||||
cursor: -webkit-grab;
|
display: flex;
|
||||||
cursor: -moz-grab;
|
justify-content: space-between;
|
||||||
|
padding: $gl-padding;
|
||||||
&:active {
|
border-radius: $border-radius-default;
|
||||||
cursor: -webkit-grabbing;
|
|
||||||
cursor: -moz-grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sortable-ghost {
|
&.sortable-ghost {
|
||||||
opacity: 0.3;
|
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 {
|
.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 {
|
.prioritized-labels {
|
||||||
|
@ -215,22 +136,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 {
|
.filtered-labels {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
@ -284,10 +189,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-subscribe-button {
|
.label-subscribe-button {
|
||||||
@media(min-width: map-get($grid-breakpoints, md)) {
|
width: 105px;
|
||||||
min-width: 105px;
|
font-weight: 200;
|
||||||
margin-left: $gl-padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-subscribe-button-icon {
|
.label-subscribe-button-icon {
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
|
@ -324,3 +227,95 @@
|
||||||
font-size: $label-font-size;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController
|
||||||
include ToggleSubscriptionAction
|
include ToggleSubscriptionAction
|
||||||
|
|
||||||
before_action :label, only: [:edit, :update, :destroy]
|
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 :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
|
||||||
before_action :save_previous_label_path, only: [:edit]
|
before_action :save_previous_label_path, only: [:edit]
|
||||||
|
|
||||||
|
@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController
|
||||||
format.html do
|
format.html do
|
||||||
@labels = @group.labels.page(params[:page])
|
@labels = @group.labels.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
available_labels = LabelsFinder.new(
|
render json: LabelSerializer.new.represent_appearance(@available_labels)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController
|
||||||
def save_previous_label_path
|
def save_previous_label_path
|
||||||
session[:previous_labels_path] = URI(request.referer || '').path
|
session[:previous_labels_path] = URI(request.referer || '').path
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -211,6 +211,14 @@ module LabelsHelper
|
||||||
end
|
end
|
||||||
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
|
# Required for Banzai::Filter::LabelReferenceFilter
|
||||||
module_function :render_colored_label, :text_color_for_bg, :escape_once
|
module_function :render_colored_label, :text_color_for_bg, :escape_once
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,6 +137,10 @@ class Label < ActiveRecord::Base
|
||||||
priority.try(:priority)
|
priority.try(:priority)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def priority?
|
||||||
|
priorities.present?
|
||||||
|
end
|
||||||
|
|
||||||
def template?
|
def template?
|
||||||
template
|
template
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,21 +1,32 @@
|
||||||
- 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']
|
- issuables = ['issues', 'merge requests']
|
||||||
|
|
||||||
.top-area.adjust
|
- if can_admin_label
|
||||||
.nav-text
|
- content_for(:header_content) do
|
||||||
= _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
|
.nav-controls
|
||||||
|
= link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
|
||||||
|
|
||||||
.nav-controls
|
- if @labels.exists?
|
||||||
- if can?(current_user, :admin_label, @group)
|
#promote-label-modal
|
||||||
= link_to "New label", new_group_label_path(@group), class: "btn btn-new"
|
%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 }
|
||||||
|
|
||||||
.labels
|
.labels-container.prepend-top-5
|
||||||
.other-labels
|
.other-labels
|
||||||
- if @labels.present?
|
- if can_admin_label
|
||||||
%ul.content-list.manage-labels-list.js-other-labels
|
%h5{ class: ('hide' if hide) } Labels
|
||||||
= render partial: 'shared/label', subject: @group, collection: @labels, as: :label
|
%ul.content-list.manage-labels-list.js-other-labels
|
||||||
= paginate @labels, theme: 'gitlab'
|
= render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
|
||||||
- else
|
= paginate @labels, theme: 'gitlab'
|
||||||
.nothing-here-block
|
- else
|
||||||
= _("No labels created yet.")
|
= 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')
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
- @no_container = true
|
- @no_container = true
|
||||||
- page_title "Labels"
|
- page_title "Labels"
|
||||||
- hide_class = ''
|
|
||||||
- can_admin_label = can?(current_user, :admin_label, @project)
|
- 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?
|
- if @labels.exists? || @prioritized_labels.exists?
|
||||||
#promote-label-modal
|
#promote-label-modal
|
||||||
%div{ class: container_class }
|
%div{ class: container_class }
|
||||||
.top-area.adjust
|
.top-area.adjust
|
||||||
.nav-text
|
.nav-text
|
||||||
Labels can be applied to issues and merge requests.
|
= _('Labels can be applied to issues and merge requests.')
|
||||||
- if can_admin_label
|
- 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
|
.labels-container.prepend-top-5
|
||||||
.nav-controls
|
|
||||||
= link_to new_project_label_path(@project), class: "btn btn-new" do
|
|
||||||
New label
|
|
||||||
|
|
||||||
.labels
|
|
||||||
- if can_admin_label
|
- if can_admin_label
|
||||||
-# Only show it in the first page
|
-# Only show it in the first page
|
||||||
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
|
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
|
||||||
.prioritized-labels{ class: ('hide' if hide) }
|
.prioritized-labels{ class: ('hide' if hide) }
|
||||||
%h5 Prioritized Labels
|
%h5.prepend-top-10= _('Prioritized Labels')
|
||||||
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
|
.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?}" }
|
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
|
||||||
= render 'shared/empty_states/priority_labels'
|
= render 'shared/empty_states/priority_labels'
|
||||||
- if @prioritized_labels.present?
|
- 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?
|
- if @labels.present?
|
||||||
.other-labels
|
.other-labels
|
||||||
- if can_admin_label
|
- if can_admin_label
|
||||||
%h5{ class: ('hide' if hide) } Other Labels
|
%h5{ class: ('hide' if hide) }= _('Other Labels')
|
||||||
%ul.content-list.manage-labels-list.js-other-labels
|
.content-list.manage-labels-list.js-other-labels
|
||||||
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
|
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
|
||||||
= paginate @labels, theme: 'gitlab'
|
= paginate @labels, theme: 'gitlab'
|
||||||
- else
|
- else
|
||||||
= render 'shared/empty_states/labels'
|
= 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')
|
||||||
|
|
|
@ -1,93 +1,70 @@
|
||||||
- label_css_id = dom_id(label)
|
- label_css_id = dom_id(label)
|
||||||
- status = label_subscription_status(label, @project).inquiry if current_user
|
- status = label_subscription_status(label, @project).inquiry if current_user
|
||||||
- subject = local_assigns[:subject]
|
- 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.present? : false)
|
||||||
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
|
- 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_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
|
||||||
- show_label_issues_link = show_label_issuables_link?(label, :issues, 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 } }
|
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
|
||||||
= render "shared/label_row", label: label
|
= render "shared/label_row", label: label, subject: subject, force_priority: force_priority
|
||||||
|
%ul.label-actions-list
|
||||||
.d-inline-block.d-sm-none.dropdown
|
- if @project
|
||||||
%button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
|
%li.inline
|
||||||
Options
|
.label-badge.label-badge-gray= label.model_name.human.capitalize
|
||||||
= icon('caret-down')
|
- if can?(current_user, :admin_label, @project)
|
||||||
.dropdown-menu.dropdown-menu-right
|
%li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
|
||||||
%ul
|
dom_id: dom_id(label), type: label.type } }
|
||||||
- if show_label_merge_requests_link
|
%button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') }
|
||||||
%li
|
= sprite_icon('star-o')
|
||||||
= link_to_label(label, subject: subject, type: :merge_request) do
|
%button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') }
|
||||||
View merge requests
|
= sprite_icon('star')
|
||||||
- if show_label_issues_link
|
%li.inline
|
||||||
%li
|
= link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do
|
||||||
= link_to_label(label, subject: subject) do
|
= sprite_icon('pencil')
|
||||||
View open issues
|
%li.inline
|
||||||
- if current_user
|
.dropdown
|
||||||
%li.label-subscription
|
%button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
|
||||||
- if can_subscribe_to_label_in_different_levels?(label)
|
= sprite_icon('ellipsis_v')
|
||||||
%a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
|
.dropdown-menu.dropdown-open-left
|
||||||
%span Unsubscribe
|
%ul
|
||||||
%a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
|
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
|
||||||
%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')
|
|
||||||
- if current_user
|
|
||||||
.label-subscription.inline
|
|
||||||
- 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')
|
|
||||||
|
|
||||||
.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
|
%li
|
||||||
%a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
|
%button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
|
||||||
Project level
|
data: { url: promote_project_label_path(label.project, label),
|
||||||
%a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
|
label_title: label.title,
|
||||||
Group level
|
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
|
||||||
|
%li.inline.label-subscription
|
||||||
|
- if can_subscribe_to_label_in_different_levels?(label)
|
||||||
|
%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.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
|
- 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)
|
%span= label_subscription_toggle_button_text(label, @project)
|
||||||
= icon('spinner spin', class: 'label-subscribe-button-loading')
|
|
||||||
|
|
||||||
= render 'shared/delete_label_modal', label: label
|
= render 'shared/delete_label_modal', label: label
|
||||||
|
|
|
@ -1,30 +1,23 @@
|
||||||
- subject = local_assigns[:subject]
|
- 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_issues_link = show_label_issuables_link?(label, :issues, project: @project)
|
||||||
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
|
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
|
||||||
|
|
||||||
%span.label-row
|
.label-name
|
||||||
- if can?(current_user, :admin_label, @project)
|
= link_to_label(label, subject: @project, tooltip: false)
|
||||||
.draggable-handler
|
.label-description
|
||||||
= icon('bars')
|
.append-right-default.prepend-left-default
|
||||||
.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
|
|
||||||
- if label.description.present?
|
- if label.description.present?
|
||||||
.description-text
|
.description-text.append-bottom-10
|
||||||
= markdown_field(label, :description)
|
= markdown_field(label, :description)
|
||||||
.d-none.d-sm-none.d-md-block
|
%ul.label-links
|
||||||
- if show_label_issues_link
|
- 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
|
- 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')
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Label list page redesign
|
||||||
|
merge_request: 18466
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -36,7 +36,7 @@ feature 'Labels subscription' do
|
||||||
within "#group_label_#{feature.id}" do
|
within "#group_label_#{feature.id}" do
|
||||||
expect(page).not_to have_button 'Unsubscribe'
|
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).not_to have_selector('.dropdown-group-label')
|
||||||
expect(page).to have_button 'Unsubscribe'
|
expect(page).to have_button 'Unsubscribe'
|
||||||
|
@ -45,7 +45,7 @@ feature 'Labels subscription' do
|
||||||
|
|
||||||
expect(page).to have_selector('.dropdown-group-label')
|
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).not_to have_selector('.dropdown-group-label')
|
||||||
expect(page).to have_button 'Unsubscribe'
|
expect(page).to have_button 'Unsubscribe'
|
||||||
|
@ -68,7 +68,7 @@ feature 'Labels subscription' do
|
||||||
find('.dropdown-group-label').click
|
find('.dropdown-group-label').click
|
||||||
|
|
||||||
page.within('.dropdown-group-label') do
|
page.within('.dropdown-group-label') do
|
||||||
find('a.js-subscribe-button', text: text).click
|
find('.js-subscribe-button', text: text).click
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,16 +102,16 @@ feature 'Prioritize labels' do
|
||||||
drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
|
drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
|
||||||
|
|
||||||
page.within('.prioritized-labels') do
|
page.within('.prioritized-labels') do
|
||||||
expect(first('li')).to have_content('feature')
|
expect(first('.label-list-item')).to have_content('feature')
|
||||||
expect(page.all('li').last).to have_content('bug')
|
expect(page.all('.label-list-item').last).to have_content('bug')
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh
|
refresh
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within('.prioritized-labels') do
|
page.within('.prioritized-labels') do
|
||||||
expect(first('li')).to have_content('feature')
|
expect(first('.label-list-item')).to have_content('feature')
|
||||||
expect(page.all('li').last).to have_content('bug')
|
expect(page.all('.label-list-item').last).to have_content('bug')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,9 @@ describe "User removes labels" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "removes label" do
|
it "removes label" do
|
||||||
page.within(".labels") do
|
page.within(".other-labels") do
|
||||||
page.first(".label-list-item") do
|
page.first(".label-list-item") do
|
||||||
|
first('.js-label-options-dropdown').click
|
||||||
first(".remove-row").click
|
first(".remove-row").click
|
||||||
first(:link, "Delete label").click
|
first(:link, "Delete label").click
|
||||||
end
|
end
|
||||||
|
@ -36,17 +37,16 @@ describe "User removes labels" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "removes all labels" do
|
it "removes all labels" do
|
||||||
page.within(".labels") do
|
loop do
|
||||||
loop do
|
li = page.first(".label-list-item")
|
||||||
li = page.first(".label-list-item")
|
break unless li
|
||||||
break unless li
|
|
||||||
|
|
||||||
li.click_link("Delete")
|
li.find('.js-label-options-dropdown').click
|
||||||
click_link("Delete label")
|
li.click_button("Delete")
|
||||||
end
|
click_link("Delete label")
|
||||||
|
|
||||||
expect(page).to have_content("Generate a default set of labels").and have_content("New label")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content("Generate a default set of labels").and have_content("New label")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue