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>2020-03-04 18:08:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-04 18:08:09 +0300
commitd3fc3be040a4fed2328e23ef28696dd8bd8238b4 (patch)
treef1874ea5e6e3c50c6a3c2ca2900af4ae73a53119 /app/assets/javascripts/vue_shared
parentc6c7437861bff9572747674095c4dfbdfbea4988 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue124
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue178
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue173
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js61
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js20
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js76
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js27
17 files changed, 865 insertions, 3 deletions
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 582213ee8d3..27f1a4f75d5 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -4,5 +4,9 @@ export default {
type: String,
required: true,
},
+ type: {
+ type: String,
+ required: true,
+ },
},
};
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index b3a1df8f303..afbfb1e0ee2 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,10 +1,14 @@
<script>
import ViewerMixin from './mixins';
+import { handleBlobRichViewer } from '~/blob/viewer';
export default {
mixins: [ViewerMixin],
+ mounted() {
+ handleBlobRichViewer(this.$refs.content, this.type);
+ },
};
</script>
<template>
- <div v-html="content"></div>
+ <div ref="content" v-html="content"></div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
index f519f90445e..839117becd9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
@@ -27,7 +27,12 @@ export default {
<span :style="labelStyle" class="badge color-label">
{{ label.title }}
</span>
- <gl-tooltip :target="() => $refs.regularLabelRef" placement="top" boundary="viewport">
+ <gl-tooltip
+ v-if="label.description"
+ :target="() => $refs.regularLabelRef"
+ placement="top"
+ boundary="viewport"
+ >
{{ label.description }}
</gl-tooltip>
</a>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
index ad5a86de166..94587e1cbab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
@@ -33,7 +33,12 @@ export default {
<span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label">
{{ label.title }}
</span>
- <gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
+ <gl-tooltip
+ v-if="label.description"
+ :target="() => $refs.labelTitleRef"
+ placement="top"
+ boundary="viewport"
+ >
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
><br />
{{ label.description }}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
new file mode 100644
index 00000000000..b9c611d2764
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -0,0 +1,21 @@
+<script>
+import { mapGetters } from 'vuex';
+import { GlButton, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ },
+ computed: {
+ ...mapGetters(['dropdownButtonText']),
+ },
+};
+</script>
+
+<template>
+ <gl-button class="labels-select-dropdown-button w-100 text-left">
+ <span class="dropdown-toggle-text">{{ dropdownButtonText }}</span>
+ <gl-icon name="chevron-down" class="pull-right" />
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
new file mode 100644
index 00000000000..ef8218b5135
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -0,0 +1,30 @@
+<script>
+import { mapState } from 'vuex';
+
+import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
+
+export default {
+ components: {
+ DropdownContentsLabelsView,
+ DropdownContentsCreateView,
+ },
+ computed: {
+ ...mapState(['showDropdownContentsCreateView']),
+ dropdownContentsView() {
+ if (this.showDropdownContentsCreateView) {
+ return 'dropdown-contents-create-view';
+ }
+ return 'dropdown-contents-labels-view';
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
+ >
+ <component :is="dropdownContentsView" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
new file mode 100644
index 00000000000..285a0fe9ffb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -0,0 +1,124 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import {
+ GlTooltipDirective,
+ GlButton,
+ GlIcon,
+ GlFormInput,
+ GlLink,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ GlFormInput,
+ GlLink,
+ GlLoadingIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ data() {
+ return {
+ labelTitle: '',
+ selectedColor: '',
+ };
+ },
+ computed: {
+ ...mapState(['labelsCreateTitle', 'labelCreateInProgress']),
+ disableCreate() {
+ return !this.labelTitle.length || !this.selectedColor.length || this.labelCreateInProgress;
+ },
+ suggestedColors() {
+ const colorsMap = gon.suggested_label_colors;
+ return Object.keys(colorsMap).map(color => ({ [color]: colorsMap[color] }));
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDropdownContents', 'toggleDropdownContentsCreateView', 'createLabel']),
+ getColorCode(color) {
+ return Object.keys(color).pop();
+ },
+ getColorName(color) {
+ return Object.values(color).pop();
+ },
+ handleColorClick(color) {
+ this.selectedColor = this.getColorCode(color);
+ },
+ handleCreateClick() {
+ this.createLabel({
+ title: this.labelTitle,
+ color: this.selectedColor,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="labels-select-contents-create">
+ <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <gl-button
+ :aria-label="__('Go back')"
+ variant="link"
+ size="sm"
+ class="dropdown-header-button p-0"
+ @click="toggleDropdownContentsCreateView"
+ >
+ <gl-icon name="arrow-left" />
+ </gl-button>
+ <span class="flex-grow-1">{{ labelsCreateTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="sm"
+ class="dropdown-header-button p-0"
+ @click="toggleDropdownContents"
+ >
+ <gl-icon name="close" />
+ </gl-button>
+ </div>
+ <div class="dropdown-input">
+ <gl-form-input
+ v-model.trim="labelTitle"
+ :placeholder="__('Name new label')"
+ :autofocus="true"
+ />
+ </div>
+ <div class="dropdown-content px-2">
+ <div class="suggest-colors suggest-colors-dropdown mt-0 mb-2">
+ <gl-link
+ v-for="(color, index) in suggestedColors"
+ :key="index"
+ v-gl-tooltip:tooltipcontainer
+ :style="{ backgroundColor: getColorCode(color) }"
+ :title="getColorName(color)"
+ @click.prevent="handleColorClick(color)"
+ />
+ </div>
+ <div class="color-input-container d-flex">
+ <span
+ class="dropdown-label-color-preview position-relative position-relative d-inline-block"
+ :style="{ backgroundColor: selectedColor }"
+ ></span>
+ <gl-form-input v-model.trim="selectedColor" :placeholder="__('Use custom color #FF0000')" />
+ </div>
+ </div>
+ <div class="dropdown-actions clearfix pt-2 px-2">
+ <gl-button
+ :disabled="disableCreate"
+ variant="primary"
+ class="pull-left d-flex align-items-center"
+ @click="handleCreateClick"
+ >
+ <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
+ {{ __('Create') }}
+ </gl-button>
+ <gl-button class="pull-right" @click="toggleDropdownContentsCreateView">
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
new file mode 100644
index 00000000000..7ec420fa908
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -0,0 +1,178 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon, GlButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlButton,
+ GlIcon,
+ GlSearchBoxByType,
+ GlLink,
+ },
+ data() {
+ return {
+ searchKey: '',
+ currentHighlightItem: -1,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'labelsManagePath',
+ 'labels',
+ 'labelsFetchInProgress',
+ 'labelsListTitle',
+ 'footerCreateLabelTitle',
+ 'footerManageLabelTitle',
+ ]),
+ ...mapGetters(['selectedLabelsList']),
+ visibleLabels() {
+ if (this.searchKey) {
+ return this.labels.filter(label =>
+ label.title.toLowerCase().includes(this.searchKey.toLowerCase()),
+ );
+ }
+ return this.labels;
+ },
+ },
+ watch: {
+ searchKey(value) {
+ // When there is search string present
+ // and there are matching results,
+ // highlight first item by default.
+ if (value && this.visibleLabels.length) {
+ this.currentHighlightItem = 0;
+ }
+ },
+ },
+ mounted() {
+ this.fetchLabels();
+ },
+ methods: {
+ ...mapActions([
+ 'toggleDropdownContents',
+ 'toggleDropdownContentsCreateView',
+ 'fetchLabels',
+ 'updateSelectedLabels',
+ ]),
+ getDropdownLabelBoxStyle(label) {
+ return {
+ backgroundColor: label.color,
+ };
+ },
+ isLabelSelected(label) {
+ return this.selectedLabelsList.includes(label.id);
+ },
+ /**
+ * This method scrolls item from dropdown into
+ * the view if it is off the viewable area of the
+ * container.
+ */
+ scrollIntoViewIfNeeded() {
+ const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused');
+
+ if (highlightedLabel) {
+ const rect = highlightedLabel.getBoundingClientRect();
+ if (rect.bottom > this.$refs.labelsListContainer.clientHeight) {
+ highlightedLabel.scrollIntoView(false);
+ }
+ if (rect.top < 0) {
+ highlightedLabel.scrollIntoView();
+ }
+ }
+ },
+ /**
+ * This method enables keyboard navigation support for
+ * the dropdown.
+ */
+ handleKeyDown(e) {
+ if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) {
+ this.currentHighlightItem -= 1;
+ } else if (
+ e.keyCode === DOWN_KEY_CODE &&
+ this.currentHighlightItem < this.visibleLabels.length - 1
+ ) {
+ this.currentHighlightItem += 1;
+ } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
+ this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
+ } else if (e.keyCode === ESC_KEY_CODE) {
+ this.toggleDropdownContents();
+ }
+
+ if (e.keyCode !== ESC_KEY_CODE) {
+ // Scroll the list only after highlighting
+ // styles are rendered completely.
+ this.$nextTick(() => {
+ this.scrollIntoViewIfNeeded();
+ });
+ }
+ },
+ handleLabelClick(label) {
+ this.updateSelectedLabels([label]);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="labels-select-contents-list" @keydown="handleKeyDown">
+ <gl-loading-icon
+ v-if="labelsFetchInProgress"
+ class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
+ size="md"
+ />
+ <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <span class="flex-grow-1">{{ labelsListTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="sm"
+ class="dropdown-header-button p-0"
+ @click="toggleDropdownContents"
+ >
+ <gl-icon name="close" />
+ </gl-button>
+ </div>
+ <div class="dropdown-input">
+ <gl-search-box-by-type v-model="searchKey" :autofocus="true" />
+ </div>
+ <div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
+ <ul class="list-unstyled mb-0">
+ <li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
+ <gl-link
+ class="d-flex align-items-baseline text-break-word label-item"
+ :class="{ 'is-focused': index === currentHighlightItem }"
+ @click="handleLabelClick(label)"
+ >
+ <gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
+ <span v-show="!label.set" class="mr-3 pr-2"></span>
+ <span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
+ <span>{{ label.title }}</span>
+ </gl-link>
+ </li>
+ <li v-if="!visibleLabels.length" class="p-2 text-center">
+ {{ __('No matching results') }}
+ </li>
+ </ul>
+ </div>
+ <div class="dropdown-footer">
+ <ul class="list-unstyled">
+ <li>
+ <gl-button
+ variant="link"
+ class="d-flex w-100 flex-row text-break-word label-item"
+ @click="toggleDropdownContentsCreateView"
+ >{{ footerCreateLabelTitle }}</gl-button
+ >
+ </li>
+ <li>
+ <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
+ {{ footerManageLabelTitle }}
+ </gl-link>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
new file mode 100644
index 00000000000..57f7962dfe1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -0,0 +1,39 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlLoadingIcon,
+ },
+ props: {
+ labelsSelectInProgress: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['allowLabelEdit', 'labelsFetchInProgress']),
+ },
+ methods: {
+ ...mapActions(['toggleDropdownContents']),
+ },
+};
+</script>
+
+<template>
+ <div class="title hide-collapsed append-bottom-10">
+ {{ __('Labels') }}
+ <template v-if="allowLabelEdit">
+ <gl-loading-icon v-show="labelsSelectInProgress" inline />
+ <gl-button
+ variant="link"
+ class="pull-right js-sidebar-dropdown-toggle"
+ data-qa-selector="labels_edit_button"
+ @click="toggleDropdownContents"
+ >{{ __('Edit') }}</gl-button
+ >
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
new file mode 100644
index 00000000000..695af775750
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -0,0 +1,53 @@
+<script>
+import { mapState } from 'vuex';
+import { GlLabel } from '@gitlab/ui';
+
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ GlLabel,
+ },
+ computed: {
+ ...mapState([
+ 'selectedLabels',
+ 'allowScopedLabels',
+ 'labelsFilterBasePath',
+ 'scopedLabelsDocumentationPath',
+ ]),
+ },
+ methods: {
+ labelFilterUrl(label) {
+ return `${this.labelsFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
+ },
+ scopedLabel(label) {
+ return this.allowScopedLabels && isScopedLabel(label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'has-labels': selectedLabels.length,
+ }"
+ class="hide-collapsed value issuable-show-labels js-value"
+ >
+ <span v-if="!selectedLabels.length" class="text-secondary">
+ <slot></slot>
+ </span>
+ <template v-for="label in selectedLabels" v-else>
+ <gl-label
+ :key="label.id"
+ :title="label.title"
+ :description="label.description"
+ :background-color="label.color"
+ :target="labelFilterUrl(label)"
+ :scoped="scopedLabel(label)"
+ :scoped-labels-documentation-link="scopedLabelsDocumentationPath"
+ tooltip-placement="top"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
new file mode 100644
index 00000000000..b90f441b8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -0,0 +1,173 @@
+<script>
+import Vue from 'vue';
+import Vuex, { mapState, mapActions } from 'vuex';
+import { __ } from '~/locale';
+
+import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+
+import labelsSelectModule from './store';
+
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownButton from './dropdown_button.vue';
+import DropdownContents from './dropdown_contents.vue';
+
+Vue.use(Vuex);
+
+export default {
+ store: new Vuex.Store(labelsSelectModule()),
+ components: {
+ DropdownTitle,
+ DropdownValue,
+ DropdownButton,
+ DropdownContents,
+ DropdownValueCollapsed,
+ },
+ props: {
+ allowLabelEdit: {
+ type: Boolean,
+ required: true,
+ },
+ allowLabelCreate: {
+ type: Boolean,
+ required: true,
+ },
+ allowScopedLabels: {
+ type: Boolean,
+ required: true,
+ },
+ dropdownOnly: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedLabels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ labelsSelectInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsManagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsFilterBasePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ scopedLabelsDocumentationPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsListTitle: {
+ type: String,
+ required: false,
+ default: __('Assign labels'),
+ },
+ labelsCreateTitle: {
+ type: String,
+ required: false,
+ default: __('Create group label'),
+ },
+ footerCreateLabelTitle: {
+ type: String,
+ required: false,
+ default: __('Create group label'),
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: false,
+ default: __('Manage group labels'),
+ },
+ },
+ computed: {
+ ...mapState(['showDropdownButton', 'showDropdownContents']),
+ },
+ watch: {
+ selectedLabels(selectedLabels) {
+ this.setInitialState({
+ selectedLabels,
+ });
+ },
+ },
+ mounted() {
+ this.setInitialState({
+ dropdownOnly: this.dropdownOnly,
+ allowLabelEdit: this.allowLabelEdit,
+ allowLabelCreate: this.allowLabelCreate,
+ allowScopedLabels: this.allowScopedLabels,
+ selectedLabels: this.selectedLabels,
+ labelsFetchPath: this.labelsFetchPath,
+ labelsManagePath: this.labelsManagePath,
+ labelsFilterBasePath: this.labelsFilterBasePath,
+ scopedLabelsDocumentationPath: this.scopedLabelsDocumentationPath,
+ labelsListTitle: this.labelsListTitle,
+ labelsCreateTitle: this.labelsCreateTitle,
+ footerCreateLabelTitle: this.footerCreateLabelTitle,
+ footerManageLabelTitle: this.footerManageLabelTitle,
+ });
+
+ this.$store.subscribeAction({
+ after: this.handleVuexActionDispatch,
+ });
+ },
+ methods: {
+ ...mapActions(['setInitialState']),
+ /**
+ * This method differentiates between
+ * dispatched actions and calls necessary method.
+ */
+ handleVuexActionDispatch(action, state) {
+ if (
+ action.type === 'toggleDropdownContents' &&
+ !state.showDropdownButton &&
+ !state.showDropdownContents
+ ) {
+ this.handleDropdownClose(state.labels.filter(label => label.touched));
+ }
+ },
+ handleDropdownClose(labels) {
+ // Only emit label updates if there are any labels to update
+ // on UI.
+ if (labels.length) this.$emit('updateSelectedLabels', labels);
+ this.$emit('onDropdownClose');
+ },
+ handleCollapsedValueClick() {
+ this.$emit('toggleCollapse');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="labels-select-wrapper position-relative">
+ <div v-if="!dropdownOnly">
+ <dropdown-value-collapsed
+ v-if="allowLabelCreate"
+ :labels="selectedLabels"
+ @onValueClick="handleCollapsedValueClick"
+ />
+ <dropdown-title
+ :allow-label-edit="allowLabelEdit"
+ :labels-select-in-progress="labelsSelectInProgress"
+ />
+ <dropdown-value v-show="!showDropdownButton">
+ <slot></slot>
+ </dropdown-value>
+ <dropdown-button v-show="showDropdownButton" />
+ <dropdown-contents v-if="showDropdownButton && showDropdownContents" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
new file mode 100644
index 00000000000..145ec7dc566
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -0,0 +1,61 @@
+import flash from '~/flash';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+
+export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
+
+export const toggleDropdownButton = ({ commit }) => commit(types.TOGGLE_DROPDOWN_BUTTON);
+export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDOWN_CONTENTS);
+
+export const toggleDropdownContentsCreateView = ({ commit }) =>
+ commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW);
+
+export const requestLabels = ({ commit }) => commit(types.REQUEST_LABELS);
+export const receiveLabelsSuccess = ({ commit }, labels) =>
+ commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
+export const receiveLabelsFailure = ({ commit }) => {
+ commit(types.RECEIVE_SET_LABELS_FAILURE);
+ flash(__('Error fetching labels.'));
+};
+export const fetchLabels = ({ state, dispatch }) => {
+ dispatch('requestLabels');
+ axios
+ .get(state.labelsFetchPath)
+ .then(({ data }) => {
+ dispatch('receiveLabelsSuccess', data);
+ })
+ .catch(() => dispatch('receiveLabelsFailure'));
+};
+
+export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LABEL);
+export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
+export const receiveCreateLabelFailure = ({ commit }) => {
+ commit(types.RECEIVE_CREATE_LABEL_FAILURE);
+ flash(__('Error creating label.'));
+};
+export const createLabel = ({ state, dispatch }, label) => {
+ dispatch('requestCreateLabel');
+ axios
+ .post(state.labelsManagePath, {
+ label,
+ })
+ .then(({ data }) => {
+ if (data.id) {
+ dispatch('receiveCreateLabelSuccess');
+ dispatch('toggleDropdownContentsCreateView');
+ } else {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ throw new Error('Error Creating Label');
+ }
+ })
+ .catch(() => {
+ dispatch('receiveCreateLabelFailure');
+ });
+};
+
+export const updateSelectedLabels = ({ commit }, labels) =>
+ commit(types.UPDATE_SELECTED_LABELS, { labels });
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
new file mode 100644
index 00000000000..c08a8a8ea58
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -0,0 +1,30 @@
+import { __, s__, sprintf } from '~/locale';
+
+/**
+ * Returns string representing current labels
+ * selection on dropdown button.
+ *
+ * @param {object} state
+ */
+export const dropdownButtonText = state => {
+ const selectedLabels = state.labels.filter(label => label.set);
+ if (!selectedLabels.length) {
+ return __('Label');
+ } else if (selectedLabels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: selectedLabels[0].title,
+ remainingLabelCount: selectedLabels.length - 1,
+ });
+ }
+ return selectedLabels[0].title;
+};
+
+/**
+ * Returns array containing only label IDs from
+ * selectedLabels array.
+ * @param {object} state
+ */
+export const selectedLabelsList = state => state.selectedLabels.map(label => label.id);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
new file mode 100644
index 00000000000..5f61cb732c8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
@@ -0,0 +1,12 @@
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+export default () => ({
+ namespaced: true,
+ state: state(),
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
new file mode 100644
index 00000000000..2e044dc3b3c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
@@ -0,0 +1,20 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+export const REQUEST_LABELS = 'REQUEST_LABELS';
+export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
+export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
+
+export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
+export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
+export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
+
+export const REQUEST_CREATE_LABEL = 'REQUEST_CREATE_LABEL';
+export const RECEIVE_CREATE_LABEL_SUCCESS = 'RECEIVE_CREATE_LABEL_SUCCESS';
+export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
+
+export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
+export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
+
+export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
+
+export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
new file mode 100644
index 00000000000..32a78507e88
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -0,0 +1,76 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, props) {
+ Object.assign(state, { ...props });
+ },
+
+ [types.TOGGLE_DROPDOWN_BUTTON](state) {
+ state.showDropdownButton = !state.showDropdownButton;
+ },
+
+ [types.TOGGLE_DROPDOWN_CONTENTS](state) {
+ if (!state.dropdownOnly) {
+ state.showDropdownButton = !state.showDropdownButton;
+ }
+ state.showDropdownContents = !state.showDropdownContents;
+ // Ensure that Create View is hidden by default
+ // when dropdown contents are revealed.
+ if (state.showDropdownContents) {
+ state.showDropdownContentsCreateView = false;
+ }
+ },
+
+ [types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) {
+ state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView;
+ },
+
+ [types.REQUEST_LABELS](state) {
+ state.labelsFetchInProgress = true;
+ },
+ [types.RECEIVE_SET_LABELS_SUCCESS](state, labels) {
+ // Iterate over every label and add a `set` prop
+ // to determine whether it is already a part of
+ // selectedLabels array.
+ const selectedLabelIds = state.selectedLabels.map(label => label.id);
+ state.labelsFetchInProgress = false;
+ state.labels = labels.reduce((allLabels, label) => {
+ allLabels.push({
+ ...label,
+ set: selectedLabelIds.includes(label.id),
+ });
+ return allLabels;
+ }, []);
+ },
+ [types.RECEIVE_SET_LABELS_FAILURE](state) {
+ state.labelsFetchInProgress = false;
+ },
+
+ [types.REQUEST_CREATE_LABEL](state) {
+ state.labelCreateInProgress = true;
+ },
+ [types.RECEIVE_CREATE_LABEL_SUCCESS](state) {
+ state.labelCreateInProgress = false;
+ },
+ [types.RECEIVE_CREATE_LABEL_FAILURE](state) {
+ state.labelCreateInProgress = false;
+ },
+
+ [types.UPDATE_SELECTED_LABELS](state, { labels }) {
+ // Iterate over all the labels and update
+ // `set` prop value to represent their current state.
+ const labelIds = labels.map(label => label.id);
+ state.labels = state.labels.reduce((allLabels, label) => {
+ if (labelIds.includes(label.id)) {
+ allLabels.push({
+ ...label,
+ touched: true,
+ set: !label.set,
+ });
+ } else {
+ allLabels.push(label);
+ }
+ return allLabels;
+ }, []);
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
new file mode 100644
index 00000000000..ceabc696693
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
@@ -0,0 +1,27 @@
+export default () => ({
+ // Initial Data
+ labels: [],
+ selectedLabels: [],
+ labelsListTitle: '',
+ labelsCreateTitle: '',
+ footerCreateLabelTitle: '',
+ footerManageLabelTitle: '',
+
+ // Paths
+ namespace: '',
+ labelsFetchPath: '',
+ labelsFilterBasePath: '',
+ scopedLabelsDocumentationPath: '#',
+
+ // UI Flags
+ allowLabelCreate: false,
+ allowLabelEdit: false,
+ allowScopedLabels: false,
+ dropdownOnly: false,
+ showDropdownButton: false,
+ showDropdownContents: false,
+ showDropdownContentsCreateView: false,
+ labelsFetchInProgress: false,
+ labelCreateInProgress: false,
+ selectedLabelsUpdated: false,
+});