Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue25
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js47
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js4
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue8
-rw-r--r--app/assets/javascripts/sidebar/constants.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue33
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue82
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue131
-rw-r--r--app/controllers/projects/commits_controller.rb6
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/models/error_tracking/error_event.rb4
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/project.rb9
-rw-r--r--app/views/groups/settings/_transfer.html.haml2
21 files changed, 373 insertions, 182 deletions
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 9bbb8a1a1b2..d9d18dc0079 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -63,7 +64,7 @@ export default {
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
+ ...mapState(['sidebarType', 'issuableType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -84,7 +85,10 @@ export default {
});
},
attrWorkspacePath() {
- return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
+ return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
+ },
+ labelType() {
+ return this.isGroupBoard ? LabelType.group : LabelType.project;
},
},
methods: {
@@ -98,21 +102,19 @@ export default {
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
- handleUpdateSelectedLabels(input) {
+ handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
- iid: this.activeBoardItem.iid,
+ id,
projectPath: this.projectPathForActiveIssue,
- addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
- removeLabelIds: this.activeBoardItem.labels
- .filter((label) => !input.find((selected) => selected.id === label.id))
- .map((label) => label.id),
+ labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
+ labels,
});
},
- handleLabelRemove(input) {
+ handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue,
- removeLabelIds: [input],
+ removeLabelIds: [removeLabelId],
});
},
},
@@ -207,14 +209,13 @@ export default {
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
- :selected-labels="activeBoardItem.labels"
- :labels-select-in-progress="isSettingLabels"
:footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle"
:labels-filter-base-path="projectPathForActiveIssue"
:attr-workspace-path="attrWorkspacePath"
:issuable-type="issuableType"
+ :label-type="labelType"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index e74463825c5..ec53947fd5f 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -91,9 +91,7 @@ export default {
try {
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
- const removeLabelIds = this.selectedLabels
- .filter((label) => !payload.find((selected) => selected.id === label.id))
- .map((label) => label.id);
+ const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
const input = {
addLabelIds,
@@ -164,7 +162,7 @@ export default {
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
:is-editing="edit"
- variant="embedded"
+ variant="sidebar"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
>
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index ca993e75cf9..c81e3cb79db 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -656,30 +656,45 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetLabelsMutation,
- variables: {
- input: {
- iid: input.iid || String(activeBoardItem.iid),
- addLabelIds: input.addLabelIds ?? [],
- removeLabelIds: input.removeLabelIds ?? [],
- projectPath: input.projectPath,
+
+ if (!gon.features?.labelsWidget) {
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetLabelsMutation,
+ variables: {
+ input: {
+ iid: input.iid || String(activeBoardItem.iid),
+ labelIds: input.labelsId ?? undefined,
+ addLabelIds: input.addLabelIds ?? [],
+ removeLabelIds: input.removeLabelIds ?? [],
+ projectPath: input.projectPath,
+ },
},
- },
- });
+ });
+
+ if (data.updateIssue?.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
- commit(types.SET_LABELS_LOADING, false);
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ prop: 'labels',
+ value: data.updateIssue?.issue?.labels.nodes,
+ });
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
+ return;
}
+ let labels = input?.labels || [];
+ if (input.removeLabelIds) {
+ labels = activeBoardItem.labels.filter(
+ (label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
+ );
+ }
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
+ itemId: input.id || activeBoardItem.id,
prop: 'labels',
- value: data.updateIssue.issue.labels.nodes,
+ value: labels,
});
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 26b785932bb..928cece19f7 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
-export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index d381c076c19..ef5b84b4575 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -195,10 +195,6 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
- [mutationTypes.SET_LABELS_LOADING](state, isLoading) {
- state.isSettingLabels = isLoading;
- },
-
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 2a6605e687b..80c51c966d2 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -12,7 +12,6 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
- isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index d5647619ea3..4c08419bb88 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const mutationMap = {
@@ -48,6 +49,7 @@ export default {
return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
+ LabelType,
};
},
methods: {
@@ -154,13 +156,11 @@ export default {
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
:labels-filter-base-path="projectIssuesPath"
- :labels-select-in-progress="isLabelsSelectInProgress"
- :selected-labels="selectedLabels"
:variant="$options.variant"
:issuable-type="issuableType"
+ :attr-workspace-path="fullPath"
+ :label-type="LabelType.project"
data-qa-selector="labels_block"
- @onLabelRemove="handleLabelRemove"
- @updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index e593973da82..02843dd3c5b 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,3 +1,4 @@
+import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
@@ -29,6 +30,7 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
@@ -120,6 +122,17 @@ export const labelsQueries = {
},
};
+export const labelsMutations = {
+ [IssuableType.Issue]: {
+ mutation: updateIssueLabelsMutation,
+ mutationName: 'updateIssue',
+ },
+ [IssuableType.MergeRequest]: {
+ mutation: updateMergeRequestLabelsMutation,
+ mutationName: 'mergeRequestSetLabels',
+ },
+};
+
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
index 389eb174c0e..3e5d1d6d7d7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
@@ -5,3 +5,8 @@ export const DropdownVariant = {
Standalone: 'standalone',
Embedded: 'embedded',
};
+
+export const LabelType = {
+ group: 'GroupLabel',
+ project: 'ProjectLabel',
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 3ee0baf8812..9c57dc2a9ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,20 +1,25 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
+ DropdownHeader,
+ DropdownFooter,
GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
},
- inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
labelsCreateTitle: {
type: String,
@@ -63,8 +68,11 @@ export default {
},
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelType: {
+ type: String,
+ required: true,
},
},
data() {
@@ -72,6 +80,7 @@ export default {
showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels],
isDirty: false,
+ searchKey: '',
};
},
computed: {
@@ -113,15 +122,24 @@ export default {
if (newVal) {
this.$refs.dropdown.show();
this.isDirty = false;
+ this.localSelectedLabels = this.selectedLabels;
} else {
this.$refs.dropdown.hide();
this.setLabels();
}
},
selectedLabels(newVal) {
- this.localSelectedLabels = newVal;
+ if (!this.isDirty) {
+ this.localSelectedLabels = newVal;
+ }
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ beforeDestroy() {
+ this.debouncedSearchKeyUpdate.cancel();
+ },
methods: {
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
@@ -144,6 +162,12 @@ export default {
this.setLabels();
}
},
+ setSearchKey(value) {
+ this.searchKey = value;
+ },
+ setFocus() {
+ this.$refs.header.focusInput();
+ },
},
};
</script>
@@ -155,60 +179,41 @@ export default {
class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
+ @shown="setFocus"
>
<template #header>
- <div
+ <dropdown-header
v-if="!isStandalone"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-header"
- >
- <gl-button
- v-if="showDropdownContentsCreateView"
- :aria-label="__('Go back')"
- variant="link"
- size="small"
- class="js-btn-back dropdown-header-button gl-p-0"
- icon="arrow-left"
- data-testid="go-back-button"
- @click.stop="toggleDropdownContent"
- />
- <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- data-testid="close-button"
- @click="$emit('closeDropdown')"
- />
- </div>
+ ref="header"
+ v-model="searchKey"
+ :labels-create-title="labelsCreateTitle"
+ :labels-list-title="labelsListTitle"
+ :show-dropdown-contents-create-view="showDropdownContentsCreateView"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ @closeDropdown="$emit('closeDropdown')"
+ @input="debouncedSearchKeyUpdate"
+ />
</template>
<template #default>
<component
:is="dropdownContentsView"
v-model="localSelectedLabels"
- :selected-labels="selectedLabels"
+ :search-key="searchKey"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
+ :label-type="labelType"
@hideCreateView="toggleDropdownContentsCreateView"
/>
</template>
<template #footer>
- <div v-if="showDropdownFooter" data-testid="dropdown-footer">
- <gl-dropdown-item
- v-if="allowLabelCreate"
- data-testid="create-label-button"
- @click.capture.native.stop="toggleDropdownContent"
- >
- {{ footerCreateLabelTitle }}
- </gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
- {{ footerManageLabelTitle }}
- </gl-dropdown-item>
- </div>
+ <dropdown-footer
+ v-if="showDropdownFooter"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ @toggleDropdownContentsCreateView="toggleDropdownContent"
+ />
</template>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index a2ed08e6b28..10743d8564b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -2,10 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
+import { LabelType } from './constants';
const errorMessage = __('Error creating label.');
@@ -30,8 +30,11 @@ export default {
},
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelType: {
+ type: String,
+ required: true,
},
},
data() {
@@ -50,25 +53,13 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- if (this.issuableType === IssuableType.Epic) {
- return {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.fullPath,
- };
- }
+ const attributePath = this.labelType === LabelType.group ? 'groupPath' : 'projectPath';
- return this.attrWorkspacePath !== undefined
- ? {
- title: this.labelTitle,
- color: this.selectedColor,
- groupPath: this.attrWorkspacePath,
- }
- : {
- title: this.labelTitle,
- color: this.selectedColor,
- projectPath: this.fullPath,
- };
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ [attributePath]: this.attrWorkspacePath,
+ };
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index e6a25362ff0..2b44219a95b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,16 +1,8 @@
<script>
-import {
- GlDropdownForm,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
@@ -20,7 +12,6 @@ export default {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
- GlSearchBoxByType,
GlIntersectionObserver,
LabelItem,
},
@@ -28,10 +19,6 @@ export default {
prop: 'localSelectedLabels',
},
props: {
- selectedLabels: {
- type: Array,
- required: true,
- },
allowMultiselect: {
type: Boolean,
required: true,
@@ -48,10 +35,13 @@ export default {
type: String,
required: true,
},
+ searchKey: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- searchKey: '',
labels: [],
isVisible: false,
};
@@ -71,12 +61,6 @@ export default {
return this.searchKey.length === 1 || !this.isVisible;
},
update: (data) => data.workspace?.labels?.nodes || [],
- async result() {
- if (this.$refs.searchInput) {
- await this.$nextTick;
- this.$refs.searchInput.focusInput();
- }
- },
error() {
createFlash({ message: __('Error fetching labels.') });
},
@@ -101,12 +85,6 @@ export default {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- beforeDestroy() {
- this.debouncedSearchKeyUpdate.cancel();
- },
methods: {
isLabelSelected(label) {
return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
@@ -153,12 +131,8 @@ export default {
this.$emit('closeDropdown', this.localSelectedLabels);
}
},
- setSearchKey(value) {
- this.searchKey = value;
- },
onDropdownAppear() {
this.isVisible = true;
- this.$refs.searchInput.focusInput();
},
},
};
@@ -167,14 +141,6 @@ export default {
<template>
<gl-intersection-observer @appear="onDropdownAppear">
<gl-dropdown-form class="labels-select-contents-list js-labels-list">
- <gl-search-box-by-type
- ref="searchInput"
- :value="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- data-testid="dropdown-input-field"
- @input="debouncedSearchKeyUpdate"
- />
<div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
new file mode 100644
index 00000000000..e67e704ffb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ inject: ['allowLabelCreate', 'labelsManagePath'],
+ props: {
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-footer">
+ <gl-dropdown-item
+ v-if="allowLabelCreate"
+ data-testid="create-label-button"
+ @click.capture.native.stop="$emit('toggleDropdownContentsCreateView')"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
+ {{ footerManageLabelTitle }}
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
new file mode 100644
index 00000000000..92a4fcd4660
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlSearchBoxByType,
+ },
+ model: {
+ prop: 'searchKey',
+ },
+ props: {
+ labelsCreateTitle: {
+ type: String,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ showDropdownContentsCreateView: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFetchInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchKey: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownTitle() {
+ return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
+ },
+ },
+ methods: {
+ focusInput() {
+ this.$refs.searchInput.focusInput();
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="dropdown-header">
+ <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <gl-button
+ v-if="showDropdownContentsCreateView"
+ :aria-label="__('Go back')"
+ variant="link"
+ size="small"
+ class="js-btn-back dropdown-header-button gl-p-0"
+ icon="arrow-left"
+ data-testid="go-back-button"
+ @click.stop="$emit('toggleDropdownContentsCreateView')"
+ />
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ data-testid="close-button"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ <gl-search-box-by-type
+ v-if="!showDropdownContentsCreateView"
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="$emit('input', $event)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 6bd43da2203..6b19eda6706 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -1,8 +1,10 @@
<script>
+import { MutationOperationMode } from '~/graphql_shared/utils';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { labelsQueries } from '~/sidebar/constants';
+import { labelsQueries, labelsMutations } from '~/sidebar/constants';
import { DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
@@ -50,16 +52,6 @@ export default {
required: false,
default: DropdownVariant.Sidebar,
},
- selectedLabels: {
- type: Array,
- required: false,
- default: () => [],
- },
- labelsSelectInProgress: {
- type: Boolean,
- required: false,
- default: false,
- },
labelsFilterBasePath: {
type: String,
required: false,
@@ -95,25 +87,25 @@ export default {
required: false,
default: __('Manage group labels'),
},
- isEditing: {
- type: Boolean,
- required: false,
- default: false,
- },
issuableType: {
type: String,
required: true,
},
attrWorkspacePath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
+ },
+ labelType: {
+ type: String,
+ required: true,
},
},
data() {
return {
contentIsOnViewport: true,
issuableLabels: [],
+ labelsSelectInProgress: false,
+ oldIid: null,
};
},
computed: {
@@ -143,9 +135,19 @@ export default {
},
},
},
+ watch: {
+ iid(_, oldVal) {
+ this.oldIid = oldVal;
+ },
+ },
methods: {
handleDropdownClose(labels) {
- this.$emit('updateSelectedLabels', labels);
+ if (this.iid !== '') {
+ this.updateSelectedLabels(this.getUpdateVariables(labels));
+ } else {
+ this.$emit('updateSelectedLabels', { labels });
+ }
+
this.collapseEditableItem();
},
collapseEditableItem() {
@@ -154,6 +156,85 @@ export default {
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
+ getUpdateVariables(labels) {
+ let labelIds = [];
+
+ labelIds = labels.map(({ id }) => id);
+ const currentIid = this.oldIid || this.iid;
+
+ const updateVariables = {
+ iid: currentIid,
+ projectPath: this.fullPath,
+ labelIds,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return updateVariables;
+ case IssuableType.MergeRequest:
+ updateVariables.operationMode = MutationOperationMode.Replace;
+ return updateVariables;
+ default:
+ return {};
+ }
+ },
+ updateSelectedLabels(inputVariables) {
+ this.labelsSelectInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: labelsMutations[this.issuableType].mutation,
+ variables: { input: inputVariables },
+ })
+ .then(({ data }) => {
+ const { mutationName } = labelsMutations[this.issuableType];
+
+ if (data[mutationName]?.errors?.length) {
+ throw new Error();
+ }
+
+ this.$emit('updateSelectedLabels', {
+ id: data[mutationName]?.[this.issuableType].id,
+ labels: data[mutationName]?.[this.issuableType].labels?.nodes,
+ });
+ })
+ .catch((error) =>
+ createFlash({
+ message: __('An error occurred while updating labels.'),
+ captureError: true,
+ error,
+ }),
+ )
+ .finally(() => {
+ this.labelsSelectInProgress = false;
+ });
+ },
+ getRemoveVariables(labelId) {
+ const removeVariables = {
+ iid: this.iid,
+ projectPath: this.fullPath,
+ };
+
+ switch (this.issuableType) {
+ case IssuableType.Issue:
+ return {
+ ...removeVariables,
+ removeLabelIds: [labelId],
+ };
+ case IssuableType.MergeRequest:
+ return {
+ ...removeVariables,
+ labelIds: [labelId],
+ operationMode: MutationOperationMode.Remove,
+ };
+ default:
+ return {};
+ }
+ },
+ handleLabelRemove(labelId) {
+ this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ this.$emit('onLabelRemove', labelId);
+ },
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
@@ -180,6 +261,7 @@ export default {
:title="__('Labels')"
:loading="isLoading"
:can-edit="allowLabelEdit"
+ @open="oldIid = null"
>
<template #collapsed>
<dropdown-value
@@ -188,7 +270,7 @@ export default {
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
@@ -201,7 +283,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
class="gl-mb-2"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ @onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
@@ -212,12 +294,13 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
+ :label-type="labelType"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -233,10 +316,12 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
+ :selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
+ :label-type="labelType"
@setLabels="handleDropdownClose"
/>
</div>
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 9ca917841e9..0132306dd90 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -6,6 +6,8 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
+ COMMITS_DEFAULT_LIMIT = 40
+
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
around_action :allow_gitaly_ref_name_caching
before_action :require_non_empty_project
@@ -63,7 +65,9 @@ class Projects::CommitsController < Projects::ApplicationController
def set_commits
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
- @limit = (params[:limit] || 40).to_i
+
+ limit = params[:limit].to_i
+ @limit = limit > 0 ? limit : COMMITS_DEFAULT_LIMIT # limit can only ever be a positive number
@offset = (params[:offset] || 0).to_i
search = params[:search]
author = params[:author]
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 24c6ef8cd68..c64d9c0d4f1 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -284,9 +284,7 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- store_in_cache = parent.is_a?(Group) ? parent.cached_issues_state_count_enabled? : false
-
- Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: store_in_cache)[state]
+ Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: true)[state]
end
def close_issuable_path(issuable)
@@ -442,7 +440,7 @@ module IssuablesHelper
end
def format_count(issuable_type, count, threshold)
- if issuable_type == :issues && parent.is_a?(Group) && parent.cached_issues_state_count_enabled?
+ if issuable_type == :issues && parent.is_a?(Group)
format_cached_count(threshold, count)
else
number_with_delimiter(count)
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 686518a39fb..b4892e5c66b 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -61,9 +61,9 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
pre_context = entry['pre_context']
post_context = entry['post_context']
- context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position(pre_context, error_line_no - pre_context.size) if pre_context
context += lines_with_position([error_line], error_line_no)
- context += lines_with_position(post_context, error_line_no + 1)
+ context += lines_with_position(post_context, error_line_no + 1) if post_context
context.reject(&:blank?)
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c5e119451e3..51eeee419e0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -760,10 +760,6 @@ class Group < Namespace
Timelog.in_group(self)
end
- def cached_issues_state_count_enabled?
- Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
- end
-
def organizations
::CustomerRelations::Organization.where(group_id: self.id)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index b827f48e706..cb58fee206b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2582,18 +2582,21 @@ class Project < ApplicationRecord
config = Gitlab.config.incoming_email
wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
- config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
+ config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
end
def service_desk_custom_address
return unless Gitlab::ServiceDeskEmail.enabled?
- key = service_desk_setting&.project_key
- return unless key.present?
+ key = service_desk_setting&.project_key || default_service_desk_suffix
Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
+ def default_service_desk_suffix
+ "#{id}-issue-"
+ end
+
def root_namespace
if namespace.has_parent?
namespace.root_ancestor
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index 1472ae42152..b2379d77314 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -14,7 +14,7 @@
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- if group.paid?
- .gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-to-transfer-has-linked-subscription-alert' } }
+ .gl-alert.gl-alert-info.gl-mb-5
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= html_escape(_("This group can't be transfered because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }