GitLab wurde aktualisiert. Dank regelmäßiger Updates bleibt das THM GitLab sicher und Sie profitieren von den neuesten Funktionen. Vielen Dank für Ihre Geduld.

Commit be928829 authored by Tim Zallmann's avatar Tim Zallmann
Browse files

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
parents 7c179bf3 a97f4ec3
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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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; ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 ...@@ -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 @@ def index ...@@ -12,17 +13,8 @@ def index
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 @@ def fallback_path ...@@ -113,4 +105,15 @@ def fallback_path
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 @@ def view_labels_title(subject) ...@@ -211,6 +211,14 @@ def view_labels_title(subject)
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 @@ def priority(project) ...@@ -137,6 +137,10 @@ def priority(project)
priority.try(:priority) priority.try(:priority)
end end
def priority?
priorities.present?
end
def template? def template?
template template
end end
......
- 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