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

Commit be928829 authored by Tim Zallmann's avatar Tim Zallmann

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 '~/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');
}
}
......@@ -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));
......
......@@ -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');
}
}
......@@ -808,3 +808,5 @@ $modal-body-height: 134px;
Prometheus
*/
$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;
}
.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;
padding: $gl-padding-4 $grid-size;
line-height: 16px;
border-radius: $label-border-radius;
color: $white-light;
......@@ -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: $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 {
......@@ -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 {
font-size: 0;
padding: 12px 16px;
......@@ -284,10 +189,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] {
......@@ -324,3 +227,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;
}
......@@ -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 @@ def index
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 @@ def fallback_path
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
......@@ -211,6 +211,14 @@ def view_labels_title(subject)
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
......@@ -137,6 +137,10 @@ def priority(project)
priority.try(:priority)
end
def priority?
priorities.present?
end
def template?
template
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']
.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 }
.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: false }
= 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')
- @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 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')
- 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.present? : 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