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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-29 06:12:00 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-29 06:12:00 +0300
commit616cd97932e5317a7bada6afe6813ec7111ed0c4 (patch)
tree8c4ee0282e560212ae3fcf49b8d51d1c2ba98a7d
parentbe6cab1d69ab884df73b155d8402a8d13289e066 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/Geo Replicate a new Git repository type.md2
-rw-r--r--.gitlab/issue_templates/Geo Replicate a new blob type.md2
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_manual_todo.yml1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue30
-rw-r--r--app/assets/javascripts/sidebar/constants.js15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue130
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue80
-rw-r--r--danger/database/Dangerfile4
-rw-r--r--doc/ci/jobs/ci_job_token.md4
-rw-r--r--lib/gitlab/email/message/in_product_marketing/base.rb4
-rw-r--r--lib/gitlab/email/message/in_product_marketing/helper.rb3
-rw-r--r--spec/frontend/sidebar/sidebar_labels_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js107
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js115
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js22
23 files changed, 534 insertions, 215 deletions
diff --git a/.gitlab/issue_templates/Geo Replicate a new Git repository type.md b/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
index 476ee14a632..0d822945798 100644
--- a/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
+++ b/.gitlab/issue_templates/Geo Replicate a new Git repository type.md
@@ -109,7 +109,7 @@ Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org
bin/rake geo:db:migrate
```
-- [ ] Be sure to commit the relevant changes in `ee/db/geo/schema.rb`
+- [ ] Be sure to commit the relevant changes in `ee/db/geo/structure.sql`
### Add verification state fields on the Geo primary site
diff --git a/.gitlab/issue_templates/Geo Replicate a new blob type.md b/.gitlab/issue_templates/Geo Replicate a new blob type.md
index aef983f6495..00a71fa406e 100644
--- a/.gitlab/issue_templates/Geo Replicate a new blob type.md
+++ b/.gitlab/issue_templates/Geo Replicate a new blob type.md
@@ -110,7 +110,7 @@ Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org
bin/rake geo:db:migrate
```
-- [ ] Be sure to commit the relevant changes in `ee/db/geo/schema.rb`
+- [ ] Be sure to commit the relevant changes in `ee/db/geo/structure.sql`
### Add verification state fields on the Geo primary site
diff --git a/.rubocop.yml b/.rubocop.yml
index 141ba874b21..4bf2392867d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,7 +28,6 @@ AllCops:
- 'node_modules/**/*'
- 'db/fixtures/**/*'
- 'db/schema.rb'
- - 'ee/db/geo/schema.rb'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 80421ca6bfe..0d9c95ccf96 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -2603,7 +2603,6 @@ Rails/IncludeUrlHelper:
- 'ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- 'ee/spec/lib/banzai/filter/issuable_state_filter_spec.rb'
- 'lib/gitlab/ci/badge/metadata.rb'
- - 'lib/gitlab/email/message/in_product_marketing/helper.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/helpers/nav/top_nav_helper_spec.rb'
- 'spec/helpers/notify_helper_spec.rb'
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index 9fdf941579d..af426584f4f 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -53,30 +53,32 @@ export default {
handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown');
},
- getUpdateVariables(dropdownLabels) {
- const currentLabelIds = this.selectedLabels.map((label) => label.id);
- const dropdownLabelIds = dropdownLabels.map((label) => label.id);
- const userAddedLabelIds = this.glFeatures.labelsWidget
- ? difference(dropdownLabelIds, currentLabelIds)
- : dropdownLabels.filter((label) => label.set).map((label) => label.id);
- const userRemovedLabelIds = this.glFeatures.labelsWidget
- ? difference(currentLabelIds, dropdownLabelIds)
- : dropdownLabels.filter((label) => !label.set).map((label) => label.id);
+ getUpdateVariables(labels) {
+ let labelIds = [];
- const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
+ if (this.glFeatures.labelsWidget) {
+ labelIds = labels.map(({ id }) => toLabelGid(id));
+ } else {
+ const currentLabelIds = this.selectedLabels.map((label) => label.id);
+ const userAddedLabelIds = labels.filter((label) => label.set).map((label) => label.id);
+ const userRemovedLabelIds = labels.filter((label) => !label.set).map((label) => label.id);
+
+ labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds).map(
+ toLabelGid,
+ );
+ }
switch (this.issuableType) {
case IssuableType.Issue:
return {
- addLabelIds: userAddedLabelIds,
iid: this.iid,
projectPath: this.projectPath,
- removeLabelIds: userRemovedLabelIds,
+ labelIds,
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
- labelIds: labelIds.map(toLabelGid),
+ labelIds,
operationMode: MutationOperationMode.Replace,
projectPath: this.projectPath,
};
@@ -152,8 +154,8 @@ export default {
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.variant"
+ :issuable-type="issuableType"
data-qa-selector="labels_block"
- @onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index fd43fb80b7f..e593973da82 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -31,6 +31,10 @@ import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subs
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.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';
+import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
+import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
@@ -105,6 +109,17 @@ export const referenceQueries = {
},
};
+export const labelsQueries = {
+ [IssuableType.Issue]: {
+ issuableQuery: issueLabelsQuery,
+ workspaceQuery: projectLabelsQuery,
+ },
+ [IssuableType.Epic]: {
+ issuableQuery: epicLabelsQuery,
+ workspaceQuery: groupLabelsQuery,
+ },
+};
+
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 10ab80f4ec2..9f5a2f4ebb0 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -241,6 +241,7 @@ function mountMilestoneSelect() {
export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels');
+ const { fullPath } = getSidebarOptions();
if (!el) {
return false;
@@ -251,6 +252,7 @@ export function mountSidebarLabels() {
apolloProvider,
provide: {
...el.dataset,
+ fullPath,
allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
allowLabelEdit: parseBoolean(el.dataset.canEdit),
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
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 0fcc67c0ffa..1e3d7814829 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,9 +1,9 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
-
+import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
-import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
+import { isDropdownVariantStandalone } from './utils';
export default {
components: {
@@ -48,10 +48,15 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
showDropdownContentsCreateView: false,
+ localSelectedLabels: [...this.selectedLabels],
};
},
computed: {
@@ -64,28 +69,42 @@ export default {
dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
},
+ buttonText() {
+ if (!this.localSelectedLabels.length) {
+ return this.dropdownButtonText || __('Label');
+ } else if (this.localSelectedLabels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.localSelectedLabels[0].title,
+ remainingLabelCount: this.localSelectedLabels.length - 1,
+ });
+ }
+ return this.localSelectedLabels[0].title;
+ },
showDropdownFooter() {
- return (
- !this.showDropdownContentsCreateView &&
- (this.isDropdownVariantSidebar(this.variant) ||
- this.isDropdownVariantEmbedded(this.variant))
- );
+ return !this.showDropdownContentsCreateView && !this.isStandalone;
},
+ isStandalone() {
+ return isDropdownVariantStandalone(this.variant);
+ },
+ },
+ mounted() {
+ this.$refs.dropdown.show();
},
methods: {
- showDropdown() {
- this.$refs.dropdown.show();
- },
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
},
toggleDropdownContent() {
this.toggleDropdownContentsCreateView();
// Required to recalculate dropdown position as its size changes
- this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
+ if (this.$refs.dropdown?.$refs.dropdown) {
+ this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
+ }
+ },
+ closeDropdown() {
+ this.$emit('setLabels', this.localSelectedLabels);
+ this.$refs.dropdown.hide();
},
- isDropdownVariantSidebar,
- isDropdownVariantEmbedded,
},
};
</script>
@@ -93,14 +112,16 @@ export default {
<template>
<gl-dropdown
ref="dropdown"
- :text="dropdownButtonText"
+ :text="buttonText"
class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content"
+ @hide="$emit('setLabels', localSelectedLabels)"
>
<template #header>
<div
- v-if="isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)"
+ 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"
@@ -119,27 +140,31 @@ export default {
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
- @click="$emit('closeDropdown')"
+ data-testid="close-button"
+ @click="closeDropdown"
/>
</div>
</template>
- <component
- :is="dropdownContentsView"
- :selected-labels="selectedLabels"
- :allow-multiselect="allowMultiselect"
- @hideCreateView="toggleDropdownContentsCreateView"
- @setLabels="$emit('setLabels', $event)"
- />
+ <template #default>
+ <component
+ :is="dropdownContentsView"
+ v-model="localSelectedLabels"
+ :selected-labels="selectedLabels"
+ :allow-multiselect="allowMultiselect"
+ :issuable-type="issuableType"
+ @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.native.capture.stop="toggleDropdownContent"
+ @click.capture.native.stop="toggleDropdownContent"
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.native.capture.stop>
+ <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
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 2e31b386fdd..0f660c92e7c 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,9 +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 projectLabelsQuery from './graphql/project_labels.query.graphql';
const errorMessage = __('Error creating label.');
@@ -19,10 +20,16 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: {
- projectPath: {
+ fullPath: {
default: '',
},
},
+ props: {
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
labelTitle: '',
@@ -38,6 +45,19 @@ export default {
const colorsMap = gon.suggested_label_colors;
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
+ mutationVariables() {
+ return this.issuableType === IssuableType.Epic
+ ? {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ groupPath: this.fullPath,
+ }
+ : {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ projectPath: this.fullPath,
+ };
+ },
},
methods: {
getColorCode(color) {
@@ -51,8 +71,8 @@ export default {
},
updateLabelsInCache(store, label) {
const sourceData = store.readQuery({
- query: projectLabelsQuery,
- variables: { fullPath: this.projectPath, searchTerm: '' },
+ query: labelsQueries[this.issuableType].workspaceQuery,
+ variables: { fullPath: this.fullPath, searchTerm: '' },
});
const collator = new Intl.Collator('en');
@@ -63,8 +83,8 @@ export default {
});
store.writeQuery({
- query: projectLabelsQuery,
- variables: { fullPath: this.projectPath, searchTerm: '' },
+ query: labelsQueries[this.issuableType].workspaceQuery,
+ variables: { fullPath: this.fullPath, searchTerm: '' },
data,
});
},
@@ -75,11 +95,7 @@ export default {
data: { labelCreate },
} = await this.$apollo.mutate({
mutation: createLabelMutation,
- variables: {
- title: this.labelTitle,
- color: this.selectedColor,
- projectPath: this.projectPath,
- },
+ variables: this.mutationVariables,
update: (
store,
{
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 857367a0721..e8c42226ca8 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,12 +1,18 @@
<script>
-import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdownForm,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ 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 projectLabelsQuery from './graphql/project_labels.query.graphql';
+import { labelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
export default {
@@ -15,9 +21,13 @@ export default {
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
+ GlIntersectionObserver,
LabelItem,
},
- inject: ['projectPath'],
+ inject: ['fullPath'],
+ model: {
+ prop: 'localSelectedLabels',
+ },
props: {
selectedLabels: {
type: Array,
@@ -27,20 +37,29 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ localSelectedLabels: {
+ type: Array,
+ required: true,
+ },
},
data() {
return {
searchKey: '',
labels: [],
- localSelectedLabels: [...this.selectedLabels],
};
},
apollo: {
labels: {
- query: projectLabelsQuery,
+ query() {
+ return labelsQueries[this.issuableType].workspaceQuery;
+ },
variables() {
return {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
searchTerm: this.searchKey,
};
},
@@ -50,8 +69,8 @@ export default {
update: (data) => data.workspace?.labels?.nodes || [],
async result() {
if (this.$refs.searchInput) {
- await this.$nextTick();
- this.$refs.searchInput.focusInput();
+ await this.$nextTick;
+ this.focusInputField();
}
},
error() {
@@ -82,7 +101,6 @@ export default {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
- this.$emit('setLabels', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
@@ -109,16 +127,19 @@ export default {
}
},
updateSelectedLabels(label) {
+ let labels;
if (this.isLabelSelected(label)) {
- this.localSelectedLabels = this.localSelectedLabels.filter(
- ({ id }) => id !== getIdFromGraphQLId(label.id),
- );
+ labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id));
} else {
- this.localSelectedLabels.push({
- ...label,
- id: getIdFromGraphQLId(label.id),
- });
+ labels = [
+ ...this.localSelectedLabels,
+ {
+ ...label,
+ id: getIdFromGraphQLId(label.id),
+ },
+ ];
}
+ this.$emit('input', labels);
},
handleLabelClick(label) {
this.updateSelectedLabels(label);
@@ -129,46 +150,51 @@ export default {
setSearchKey(value) {
this.searchKey = value;
},
+ focusInputField() {
+ this.$refs.searchInput.focusInput();
+ },
},
};
</script>
<template>
- <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"
- class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
- size="md"
+ <gl-intersection-observer @appear="focusInputField">
+ <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"
/>
- <template v-else>
- <gl-dropdown-item
- v-for="label in visibleLabels"
- :key="label.id"
- :is-checked="isLabelSelected(label)"
- :is-check-centered="true"
- :is-check-item="true"
- data-testid="labels-list"
- @click.native.capture.stop="handleLabelClick(label)"
- >
- <label-item :label="label" />
- </gl-dropdown-item>
- <gl-dropdown-item
- v-show="showNoMatchingResultsMessage"
- class="gl-p-3 gl-text-center"
- data-testid="no-results"
- >
- {{ __('No matching results') }}
- </gl-dropdown-item>
- </template>
- </div>
- </gl-dropdown-form>
+ <div ref="labelsListContainer" data-testid="dropdown-content">
+ <gl-loading-icon
+ v-if="labelsFetchInProgress"
+ class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full gl-mb-3"
+ size="md"
+ />
+ <template v-else>
+ <gl-dropdown-item
+ v-for="label in visibleLabels"
+ :key="label.id"
+ :is-checked="isLabelSelected(label)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ data-testid="labels-list"
+ @click.native.capture.stop="handleLabelClick(label)"
+ >
+ <label-item :label="label" />
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-show="showNoMatchingResultsMessage"
+ class="gl-p-3 gl-text-center"
+ data-testid="no-results"
+ >
+ {{ __('No matching results') }}
+ </gl-dropdown-item>
+ </template>
+ </div>
+ </gl-dropdown-form>
+ </gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
new file mode 100644
index 00000000000..a2e8579486f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -0,0 +1,15 @@
+query epicLabels($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ issuable: epic(iid: $iid) {
+ id
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
new file mode 100644
index 00000000000..acc9bcd2015
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
@@ -0,0 +1,12 @@
+query groupLabels($fullPath: ID!, $searchTerm: String) {
+ workspace: group(fullPath: $fullPath) {
+ labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+}
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 3c834770563..496bb9817f0 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,21 +1,18 @@
<script>
-import Vue from 'vue';
-import Vuex from 'vuex';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { labelsQueries } from '~/sidebar/constants';
import { DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
-import issueLabelsQuery from './graphql/issue_labels.query.graphql';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
} from './utils';
-Vue.use(Vuex);
-
export default {
components: {
DropdownValue,
@@ -23,7 +20,15 @@ export default {
DropdownValueCollapsed,
SidebarEditableItem,
},
- inject: ['iid', 'projectPath', 'allowLabelEdit'],
+ inject: {
+ iid: {
+ default: '',
+ },
+ allowLabelEdit: {
+ default: false,
+ },
+ fullPath: {},
+ },
props: {
allowLabelRemove: {
type: Boolean,
@@ -90,43 +95,52 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
contentIsOnViewport: true,
- issueLabels: [],
+ issuableLabels: [],
};
},
+ computed: {
+ isLoading() {
+ return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
+ },
+ },
apollo: {
- issueLabels: {
- query: issueLabelsQuery,
+ issuableLabels: {
+ query() {
+ return labelsQueries[this.issuableType].issuableQuery;
+ },
+ skip() {
+ return !isDropdownVariantSidebar(this.variant);
+ },
variables() {
return {
iid: this.iid,
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
};
},
update(data) {
return data.workspace?.issuable?.labels.nodes || [];
},
+ error() {
+ createFlash({ message: __('Error fetching labels.') });
+ },
},
},
methods: {
handleDropdownClose(labels) {
- if (labels.length) this.$emit('updateSelectedLabels', labels);
- this.$emit('onDropdownClose');
- },
- collapseDropdown() {
- this.$refs.editable.collapse();
+ this.$emit('updateSelectedLabels', labels);
+ this.$refs.editable?.collapse();
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
- showDropdown() {
- this.$nextTick(() => {
- this.$refs.dropdownContents.showDropdown();
- });
- },
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
@@ -145,20 +159,19 @@ export default {
<template v-if="isDropdownVariantSidebar(variant)">
<dropdown-value-collapsed
ref="dropdownButtonCollapsed"
- :labels="issueLabels"
+ :labels="issuableLabels"
@onValueClick="handleCollapsedValueClick"
/>
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
- :loading="labelsSelectInProgress"
+ :loading="isLoading"
:can-edit="allowLabelEdit"
- @open="showDropdown"
>
<template #collapsed>
<dropdown-value
:disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
+ :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@@ -170,7 +183,7 @@ export default {
<template #default="{ edit }">
<dropdown-value
:disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
+ :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@@ -181,7 +194,6 @@ export default {
</dropdown-value>
<dropdown-contents
v-if="edit"
- ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
@@ -190,11 +202,25 @@ export default {
:labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
:variant="variant"
- @closeDropdown="collapseDropdown"
+ :issuable-type="issuableType"
@setLabels="handleDropdownClose"
/>
</template>
</sidebar-editable-item>
</template>
+ <dropdown-contents
+ v-else
+ ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :dropdown-button-text="dropdownButtonText"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ :variant="variant"
+ :issuable-type="issuableType"
+ @setLabels="handleDropdownClose"
+ />
</div>
</template>
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
index 3018196ddbc..693c03b9dad 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -33,7 +33,7 @@ MSG
DATABASE_APPROVED_LABEL = 'database::approved'
non_geo_db_schema_updated = !git.modified_files.grep(%r{\Adb/structure\.sql}).empty?
-geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).empty?
+geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/structure\.sql}).empty?
non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
@@ -45,7 +45,7 @@ if non_geo_migration_created && !non_geo_db_schema_updated
end
if geo_migration_created && !geo_db_schema_updated
- warn format(format_str, migrations: 'Geo migrations', schema: helper.html_link("ee/db/geo/schema.rb"))
+ warn format(format_str, migrations: 'Geo migrations', schema: helper.html_link("ee/db/geo/structure.sql"))
end
return unless helper.ci?
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 70c22d566e5..6b4505ad1b3 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -12,9 +12,9 @@ When a pipeline job is about to run, GitLab generates a unique token and injects
You can use a GitLab CI/CD job token to authenticate with specific API endpoints:
- Packages:
- - [Package Registry](../../user/packages/package_registry/index.md). To push to the
+ - [Package Registry](../../user/packages/package_registry/index.md#use-gitlab-cicd-to-build-packages). To push to the
Package Registry, you can use [deploy tokens](../../user/project/deploy_tokens/index.md).
- - [Container Registry](../../user/packages/container_registry/index.md)
+ - [Container Registry](../../user/packages/container_registry/index.md#build-and-push-by-using-gitlab-cicd)
(the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
- [Container Registry API](../../api/container_registry.md)
(scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
diff --git a/lib/gitlab/email/message/in_product_marketing/base.rb b/lib/gitlab/email/message/in_product_marketing/base.rb
index 96551c89837..c4895d35a14 100644
--- a/lib/gitlab/email/message/in_product_marketing/base.rb
+++ b/lib/gitlab/email/message/in_product_marketing/base.rb
@@ -50,7 +50,7 @@ module Gitlab
def cta_link
case format
when :html
- link_to cta_text, group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
+ ActionController::Base.helpers.link_to cta_text, group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
else
[cta_text, group_email_campaigns_url(group, track: track, series: series)].join(' >> ')
end
@@ -89,7 +89,7 @@ module Gitlab
case format
when :html
links.map do |text, link|
- link_to(text, link)
+ ActionController::Base.helpers.link_to(text, link)
end
else
'| ' + links.map do |text, link|
diff --git a/lib/gitlab/email/message/in_product_marketing/helper.rb b/lib/gitlab/email/message/in_product_marketing/helper.rb
index 4780e08322a..cec0aad44a6 100644
--- a/lib/gitlab/email/message/in_product_marketing/helper.rb
+++ b/lib/gitlab/email/message/in_product_marketing/helper.rb
@@ -7,7 +7,6 @@ module Gitlab
module Helper
include ActionView::Context
include ActionView::Helpers::TagHelper
- include ActionView::Helpers::UrlHelper
private
@@ -32,7 +31,7 @@ module Gitlab
def link(text, link)
case format
when :html
- link_to text, link
+ ActionController::Base.helpers.link_to text, link
else
"#{text} (#{link})"
end
diff --git a/spec/frontend/sidebar/sidebar_labels_spec.js b/spec/frontend/sidebar/sidebar_labels_spec.js
index 7455f684380..1141db5b812 100644
--- a/spec/frontend/sidebar/sidebar_labels_spec.js
+++ b/spec/frontend/sidebar/sidebar_labels_spec.js
@@ -110,10 +110,9 @@ describe('sidebar labels', () => {
mutation: updateIssueLabelsMutation,
variables: {
input: {
- addLabelIds: [40],
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
- removeLabelIds: [26, 55],
+ labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
},
},
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 843298a1406..2e43ebe3f80 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
+import { labelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
-import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import {
mockSuggestedColors,
createLabelSuccessfulResponse,
- labelsQueryResponse,
+ workspaceLabelsQueryResponse,
} from './mock_data';
jest.mock('~/flash');
@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => {
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
};
- const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
+ const createComponent = ({
+ mutationHandler = createLabelSuccessHandler,
+ issuableType = IssuableType.Issue,
+ } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: projectLabelsQuery,
- data: labelsQueryResponse.data,
+ query: labelsQueries[issuableType].workspaceQuery,
+ data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
searchTerm: '',
@@ -61,6 +65,9 @@ describe('DropdownContentsCreateView', () => {
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
apolloProvider: mockApollo,
+ propsData: {
+ issuableType,
+ },
});
};
@@ -135,15 +142,6 @@ describe('DropdownContentsCreateView', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
- it('calls a mutation with correct parameters on Create button click', () => {
- findCreateButton().vm.$emit('click');
- expect(createLabelSuccessHandler).toHaveBeenCalledWith({
- color: '#009966',
- projectPath: '',
- title: 'Test title',
- });
- });
-
it('renders a loader spinner after Create button click', async () => {
findCreateButton().vm.$emit('click');
await nextTick();
@@ -162,6 +160,30 @@ describe('DropdownContentsCreateView', () => {
});
});
+ it('calls a mutation with `projectPath` variable on the issue', () => {
+ createComponent();
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ projectPath: '',
+ title: 'Test title',
+ });
+ });
+
+ it('calls a mutation with `groupPath` variable on the epic', () => {
+ createComponent({ issuableType: IssuableType.Epic });
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ groupPath: '',
+ title: 'Test title',
+ });
+ });
+
it('calls createFlash is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 537bbc8e71e..effb365d67e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -1,36 +1,38 @@
-import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
-import { mockConfig, labelsQueryResponse } from './mock_data';
+import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
-const selectedLabels = [
+const localSelectedLabels = [
{
- id: 28,
- title: 'Bug',
- description: 'Label for bugs',
- color: '#FF0000',
- textColor: '#FFFFFF',
+ color: '#2f7b2e',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/2',
+ title: 'Label2',
},
];
describe('DropdownContentsLabelsView', () => {
let wrapper;
- const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse);
+ const successfulQueryHandler = jest.fn().mockResolvedValue(workspaceLabelsQueryResponse);
+
+ const findFirstLabel = () => wrapper.findAllComponents(GlDropdownItem).at(0);
const createComponent = ({
initialState = mockConfig,
@@ -43,14 +45,15 @@ describe('DropdownContentsLabelsView', () => {
localVue,
apolloProvider: mockApollo,
provide: {
- projectPath: 'test',
+ fullPath: 'test',
iid: 1,
variant: DropdownVariant.Sidebar,
...injected,
},
propsData: {
...initialState,
- selectedLabels,
+ localSelectedLabels,
+ issuableType: IssuableType.Issue,
},
stubs: {
GlSearchBoxByType,
@@ -129,6 +132,15 @@ describe('DropdownContentsLabelsView', () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
+
expect(createFlash).toHaveBeenCalled();
});
+
+ it('emits an `input` event on label click', async () => {
+ createComponent();
+ await waitForPromises();
+ findFirstLabel().trigger('click');
+
+ expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels));
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index a1b40a891ec..38fd1cc0a3d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -1,6 +1,5 @@
-import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-
+import { nextTick } from 'vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
@@ -8,10 +7,25 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s
import { mockLabels } from './mock_data';
+const showDropdown = jest.fn();
+
+const GlDropdownStub = {
+ template: `
+ <div data-testid="dropdown">
+ <slot name="header"></slot>
+ <slot></slot>
+ <slot name="footer"></slot>
+ </div>
+ `,
+ methods: {
+ show: showDropdown,
+ },
+};
+
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {} } = {}) => {
+ const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -22,38 +36,76 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
+ issuableType: 'issue',
...props,
},
+ data() {
+ return {
+ ...data,
+ };
+ },
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
stubs: {
- GlDropdown,
+ GlDropdown: GlDropdownStub,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
+ const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
+ const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ const findDropdown = () => wrapper.findComponent(GlDropdownStub);
+
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
+ const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
+ it('calls dropdown `show` method on component mount', () => {
+ createComponent();
+
+ expect(showDropdown).toHaveBeenCalled();
+ });
+
+ it('emits `setLabels` event on dropdown hide', () => {
+ createComponent();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setLabels')).toEqual([[mockLabels]]);
+ });
+
+ it('does not render header on standalone variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+
+ expect(findDropdownHeader().exists()).toBe(false);
+ });
+
+ it('renders header on embedded variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Embedded } });
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
+ it('renders header on sidebar variant', () => {
+ createComponent();
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
describe('Create view', () => {
beforeEach(() => {
- wrapper.vm.toggleDropdownContentsCreateView();
+ createComponent({ data: { showDropdownContentsCreateView: true } });
});
it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
- expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true);
+ expect(findCreateView().exists()).toBe(true);
});
it('does not render footer', () => {
@@ -67,11 +119,31 @@ describe('DropdownContent', () => {
it('renders go back button', () => {
expect(findGoBackButton().exists()).toBe(true);
});
+
+ it('changes the view to Labels view on back button click', async () => {
+ findGoBackButton().vm.$emit('click', new MouseEvent('click'));
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
+
+ it('changes the view to Labels view on `hideCreateView` event', async () => {
+ findCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
});
describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
- expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true);
+ expect(findLabelsView().exists()).toBe(true);
});
it('renders footer on sidebar dropdown', () => {
@@ -109,19 +181,12 @@ describe('DropdownContent', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
- it('triggers `toggleDropdownContent` method on create label button click', () => {
- jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
+ it('changes the view to Create on create label button click', async () => {
findCreateLabelButton().trigger('click');
- expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled();
+ await nextTick();
+ expect(findLabelsView().exists()).toBe(false);
});
});
});
-
- describe('template', () => {
- it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
- expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
- expect(wrapper.attributes('style')).toBeUndefined();
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index a18511fa21d..0c0319376b7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -1,28 +1,59 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
+import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { mockConfig, issuableLabelsQueryResponse } from './mock_data';
-import { mockConfig } from './mock_data';
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
+const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('LabelsSelectRoot', () => {
let wrapper;
- const createComponent = (config = mockConfig, slots = {}) => {
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+
+ const expandDropdown = () => wrapper.vm.$refs.editable.expand();
+
+ const createComponent = ({
+ config = mockConfig,
+ slots = {},
+ queryHandler = successfulQueryHandler,
+ } = {}) => {
+ const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]);
+
wrapper = shallowMount(LabelsSelectRoot, {
slots,
- propsData: config,
+ apolloProvider: mockApollo,
+ localVue,
+ propsData: {
+ ...config,
+ issuableType: IssuableType.Issue,
+ },
stubs: {
- DropdownContents,
SidebarEditableItem,
},
provide: {
iid: '1',
- projectPath: 'test',
+ fullPath: 'test',
canUpdate: true,
allowLabelEdit: true,
+ allowLabelCreate: true,
+ labelsManagePath: 'test',
},
});
};
@@ -42,33 +73,67 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
+ async ({ variant, cssClass }) => {
createComponent({
- ...mockConfig,
- variant,
+ config: { ...mockConfig, variant },
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
+ await nextTick();
+ expect(wrapper.classes()).toContain(cssClass);
},
);
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
- });
+ describe('if dropdown variant is `sidebar`', () => {
+ it('renders sidebar editable item', () => {
+ createComponent();
+ expect(findSidebarEditableItem().exists()).toBe(true);
+ });
+
+ it('passes true `loading` prop to sidebar editable item when loading labels', () => {
+ createComponent();
+ expect(findSidebarEditableItem().props('loading')).toBe(true);
+ });
- it('renders `dropdown-value` component', async () => {
- createComponent(mockConfig, {
- default: 'None',
+ describe('when labels are fetched successfully', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes true `loading` prop to sidebar editable item', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown value component when query labels is resolved', () => {
+ expect(findDropdownValue().exists()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual(
+ issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
+ );
+ });
+
+ it('emits `onLabelRemove` event on dropdown value label remove event', () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ findDropdownValue().vm.$emit('onLabelRemove', label);
+ expect(wrapper.emitted('onLabelRemove')).toEqual([[label]]);
+ });
+ });
+
+ it('creates flash with error message when query is rejected', async () => {
+ createComponent({ queryHandler: errorQueryHandler });
+ await waitForPromises();
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
- await wrapper.vm.$nextTick;
+ });
+
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if there are labels to update', async () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ createComponent();
+ await waitForPromises();
- const valueComp = wrapper.find(DropdownValue);
+ expandDropdown();
+ await nextTick();
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
+ findDropdownContents().vm.$emit('setLabels', [label]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index fceaabec2d0..4ff33f578e1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -86,7 +86,7 @@ export const createLabelSuccessfulResponse = {
},
};
-export const labelsQueryResponse = {
+export const workspaceLabelsQueryResponse = {
data: {
workspace: {
labels: {
@@ -108,3 +108,23 @@ export const labelsQueryResponse = {
},
},
};
+
+export const issuableLabelsQueryResponse = {
+ data: {
+ workspace: {
+ issuable: {
+ id: '1',
+ labels: {
+ nodes: [
+ {
+ color: '#330066',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/1',
+ title: 'Label1',
+ },
+ ],
+ },
+ },
+ },
+ },
+};