Commit 6df79435 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 5605efec
......@@ -17,6 +17,11 @@ export default {
type: Boolean,
required: true,
},
isDraggingDesign: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -121,7 +126,7 @@ export default {
</slot>
<transition name="design-dropzone-fade">
<div
v-show="dragging"
v-show="dragging && !isDraggingDesign"
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
>
<div v-show="!isDragDataValid" class="mw-50 text-center">
......
#import "../fragments/design_list.fragment.graphql"
mutation DesignManagementMove(
$id: DesignManagementDesignID!
$previous: DesignManagementDesignID
$next: DesignManagementDesignID
) {
designManagementMove(input: { id: $id, previous: $previous, next: $next }) {
designCollection {
designs {
nodes {
...DesignListItem
}
}
}
errors
}
}
......@@ -2,6 +2,7 @@
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import VueDraggable from 'vuedraggable';
import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue';
import Design from '../components/list/item.vue';
......@@ -9,6 +10,7 @@ import DesignDestroyer from '../components/design_destroyer.vue';
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
import DesignDropzone from '../components/upload/design_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import allDesignsMixin from '../mixins/all_designs';
......@@ -16,13 +18,18 @@ import {
UPLOAD_DESIGN_ERROR,
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
MOVE_DESIGN_ERROR,
designUploadSkippedWarning,
designDeletionError,
} from '../utils/error_messages';
import { updateStoreAfterUploadDesign } from '../utils/cache_update';
import {
updateStoreAfterUploadDesign,
updateDesignsOnStoreAfterReorder,
} from '../utils/cache_update';
import {
designUploadOptimisticResponse,
isValidDesignFile,
moveDesignOptimisticResponse,
} from '../utils/design_management_utils';
import { getFilename } from '~/lib/utils/file_upload';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
......@@ -40,6 +47,7 @@ export default {
DesignVersionDropdown,
DeleteButton,
DesignDropzone,
VueDraggable,
},
mixins: [allDesignsMixin],
apollo: {
......@@ -61,6 +69,8 @@ export default {
},
filesToBeSaved: [],
selectedDesigns: [],
isDraggingDesign: false,
reorderedDesigns: null,
};
},
computed: {
......@@ -254,11 +264,48 @@ export default {
toggleOffPasteListener() {
document.removeEventListener('paste', this.onDesignPaste);
},
designMoveVariables(newIndex, element) {
const variables = {
id: element.id,
};
if (newIndex > 0) {
variables.previous = this.reorderedDesigns[newIndex - 1].id;
}
if (newIndex < this.reorderedDesigns.length - 1) {
variables.next = this.reorderedDesigns[newIndex + 1].id;
}
return variables;
},
reorderDesigns({ moved: { newIndex, element } }) {
this.$apollo
.mutate({
mutation: moveDesignMutation,
variables: this.designMoveVariables(newIndex, element),
update: (store, { data: { designManagementMove } }) => {
return updateDesignsOnStoreAfterReorder(
store,
designManagementMove,
this.projectQueryBody,
);
},
optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
})
.catch(() => {
createFlash(MOVE_DESIGN_ERROR);
});
},
onDesignMove(designs) {
this.reorderedDesigns = designs;
},
},
beforeRouteUpdate(to, from, next) {
this.selectedDesigns = [];
next();
},
dragOptions: {
animation: 200,
ghostClass: 'gl-visibility-hidden',
},
};
</script>
......@@ -312,20 +359,35 @@ export default {
<gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert>
<ol v-else class="list-unstyled row">
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
<design-dropzone
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
:has-designs="hasDesigns"
@change="onUploadDesign"
/>
</li>
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-3 gl-mb-3">
<vue-draggable
v-else
:value="designs"
:disabled="!isLatestVersion"
v-bind="$options.dragOptions"
tag="ol"
draggable=".js-design-tile"
class="list-unstyled row"
@start="isDraggingDesign = true"
@end="isDraggingDesign = false"
@change="reorderDesigns"
@input="onDesignMove"
>
<li
v-for="design in designs"
:key="design.id"
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone
:has-designs="hasDesigns"
:is-dragging-design="isDraggingDesign"
@change="onExistingDesignDropzoneChange($event, design.filename)"
><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
/></design-dropzone>
>
<design
v-bind="design"
:is-uploading="isDesignToBeSaved(design.filename)"
class="gl-bg-white"
/>
</design-dropzone>
<input
v-if="canSelectDesign(design.filename)"
......@@ -335,7 +397,17 @@ export default {
@change="changeSelectedDesigns(design.filename)"
/>
</li>
</ol>
<template #header>
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
<design-dropzone
:is-dragging-design="isDraggingDesign"
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
:has-designs="hasDesigns"
@change="onUploadDesign"
/>
</li>
</template>
</vue-draggable>
</div>
<router-view :key="$route.fullPath" />
</div>
......
......@@ -203,6 +203,15 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
});
};
const moveDesignInStore = (store, designManagementMove, query) => {
const data = store.readQuery(query);
data.project.issue.designCollection.designs = designManagementMove.designCollection.designs;
store.writeQuery({
...query,
data,
});
};
const onError = (data, message) => {
createFlash(message);
throw new Error(data.errors);
......@@ -264,3 +273,11 @@ export const updateStoreAfterUploadDesign = (store, data, query) => {
addNewDesignToStore(store, data, query);
}
};
export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
if (hasErrors(data)) {
createFlash(data.errors[0]);
} else {
moveDesignInStore(store, data, query);
}
};
......@@ -85,7 +85,8 @@ export const designUploadOptimisticResponse = files => {
/**
* Generates optimistic response for a design upload mutation
* @param {Array<File>} files
* @param {Object} note
* @param {Object} position
*/
export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
......@@ -104,6 +105,27 @@ export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
},
});
/**
* Generates optimistic response for a design upload mutation
* @param {Array} designs
*/
export const moveDesignOptimisticResponse = designs => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
designManagementMove: {
__typename: 'DesignManagementMovePayload',
designCollection: {
__typename: 'DesignCollection',
designs: {
__typename: 'DesignConnection',
nodes: designs,
},
},
errors: [],
},
});
const normalizeAuthor = author => ({
...author,
web_url: author.webUrl,
......
......@@ -40,6 +40,10 @@ export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
'You must upload a file with the same file name when dropping onto an existing design.',
);
export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename =>
......
......@@ -242,6 +242,7 @@ export default {
<gl-button
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
:loading="redirecting"
:disabled="redirecting"
category="primary"
......
......@@ -23,6 +23,8 @@ import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToCamelCase } from '~/lib/utils/text_utility';
export default {
i18n: {
openedAgo: __('opened %{timeAgoString} by %{user}'),
......@@ -34,6 +36,8 @@ export default {
GlLabel,
GlIcon,
GlSprintf,
IssueHealthStatus: () =>
import('ee_component/related_items_tree/components/issue_health_status.vue'),
},
directives: {
GlTooltip,
......@@ -195,6 +199,9 @@ export default {
},
];
},
healthStatus() {
return convertToCamelCase(this.issuable.health_status);
},
},
mounted() {
// TODO: Refactor user popover to use its own component instead of
......@@ -288,7 +295,7 @@ export default {
</div>
<div class="issuable-info">
<span class="js-ref-path">
<span class="js-ref-path gl-mr-4 mr-sm-0">
<span
v-if="isJiraIssue"
class="svg-container jira-logo-container"
......@@ -298,7 +305,7 @@ export default {
{{ referencePath }}
</span>
<span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-2">
<span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-4">
&middot;
<gl-sprintf
:message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo"
......@@ -321,7 +328,7 @@ export default {
<gl-link
v-if="issuable.milestone"
v-gl-tooltip
class="gl-display-none d-sm-inline-block gl-mr-2 js-milestone"
class="gl-display-none d-sm-inline-block gl-mr-4 js-milestone milestone"
:href="milestoneLink"
:title="milestoneTooltipText"
>
......@@ -332,7 +339,7 @@ export default {
<span
v-if="dueDate"
v-gl-tooltip
class="gl-display-none d-sm-inline-block gl-mr-2 js-due-date"
class="gl-display-none d-sm-inline-block gl-mr-4 js-due-date"
:class="{ cred: isOverdue }"
:title="__('Due date')"
>
......@@ -340,6 +347,24 @@ export default {
{{ dueDateWords }}
</span>
<span
v-if="hasWeight"
v-gl-tooltip
:title="__('Weight')"
class="gl-display-none d-sm-inline-block gl-mr-4"
data-testid="weight"
data-qa-selector="issuable_weight_content"
>
<gl-icon name="weight" class="align-text-bottom" />
{{ issuable.weight }}
</span>
<issue-health-status
v-if="issuable.health_status"
:health-status="healthStatus"
class="gl-mr-4 issuable-tag-valign"
/>
<gl-label
v-for="label in issuable.labels"
:key="label.id"
......@@ -351,21 +376,9 @@ export default {
:title="label.name"
:scoped="isScoped(label)"
size="sm"
class="gl-mr-2"
class="gl-mr-2 issuable-tag-valign"
>{{ label.name }}</gl-label
>
<span
v-if="hasWeight"
v-gl-tooltip
:title="__('Weight')"
class="gl-display-none d-sm-inline-block"
data-testid="weight"
data-qa-selector="issuable_weight_content"
>
<gl-icon name="weight" class="align-text-bottom" />
{{ issuable.weight }}
</span>
</div>
</div>
......
......@@ -45,7 +45,7 @@ export default {
};
</script>
<template>
<gl-new-dropdown :text="$options.labels.defaultLabel" category="primary" variant="info">
<gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
......
......@@ -81,27 +81,6 @@ $item-remove-button-space: 42px;
max-width: 0;
}
.status {
&-at-risk {
color: $red-500;
background-color: $red-100;
}
&-needs-attention {
color: $orange-700;
background-color: $orange-100;
}
&-on-track {
color: $green-600;
background-color: $green-100;
}
}
.gl-label-text {
font-weight: $gl-font-weight-bold;
}
.bullet-separator {
font-size: 9px;
color: $gray-200;
......
......@@ -804,6 +804,10 @@
}
}
}
.milestone {
color: $gray-700;
}
}
@media(max-width: map-get($grid-breakpoints, lg)-1) {
......
......@@ -37,8 +37,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, @project)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
end
before_action do
......
# frozen_string_literal: true
module Mutations
module Boards
module Lists
class Base < BaseMutation
include Mutations::ResolvesIssuable
argument :board_id, ::Types::GlobalIDType[::Board],
required: true,
description: 'The Global ID of the issue board to mutate'
field :list,
Types::BoardListType,
null: true,
description: 'List of the issue board'
authorize :admin_list
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Board)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Boards
module Lists
class Create < Base
graphql_name 'BoardListCreate'
argument :backlog, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Create the backlog list'
argument :label_id, ::Types::GlobalIDType[::Label],
required: false,
description: 'ID of an existing label'
def ready?(**args)
if args.slice(*mutually_exclusive_args).size != 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
def resolve(**args)
board = authorized_find!(id: args[:board_id])
params = create_list_params(args)
authorize_list_type_resource!(board, params)
list = create_list(board, params)
{
list: list.valid? ? list : nil,
errors: errors_on_object(list)
}
end
private
def authorize_list_type_resource!(board, params)
return unless params[:label_id]
labels = ::Labels::AvailableLabelsService.new(current_user, board.resource_parent, params)
.filter_labels_ids_in_param(:label_id)
unless labels.present?
raise Gitlab::Graphql::Errors::ArgumentError, 'Label not found!'
end
end
def create_list(board, params)
create_list_service =
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
create_list_service.execute(board)
end
def create_list_params(args)
params = args.slice(*mutually_exclusive_args).with_indifferent_access
params[:label_id] = GitlabSchema.parse_gid(params[:label_id]).model_id if params[:label_id]
params
end
def mutually_exclusive_args
[:backlog, :label_id]
end