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-08-19 12:08:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 12:08:42 +0300
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/vue_shared/components
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue165
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue100
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue99
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue235
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/papa_parse_alert.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/remove_member_modal.vue116
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js20
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue260
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_date.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue36
40 files changed, 963 insertions, 789 deletions
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
new file mode 100644
index 00000000000..eeed5e9dc3a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
@@ -0,0 +1,27 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import { __ } from '~/locale';
+import DropdownWidget from './dropdown_widget.vue';
+
+export default {
+ component: DropdownWidget,
+ title: 'vue_shared/components/dropdown/dropdown_widget/dropdown_widget',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { DropdownWidget },
+ props: Object.keys(argTypes),
+ template: '<dropdown-widget v-bind="$props" v-on="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ options: [
+ { id: 'gid://gitlab/Milestone/-1', title: __('Any Milestone') },
+ { id: 'gid://gitlab/Milestone/0', title: __('No Milestone') },
+ { id: 'gid://gitlab/Milestone/-2', title: __('Upcoming') },
+ { id: 'gid://gitlab/Milestone/-3', title: __('Started') },
+ ],
+ selectText: 'Select',
+ searchText: 'Search',
+};
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
new file mode 100644
index 00000000000..7859ef85dd8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -0,0 +1,165 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ selectText: {
+ type: String,
+ required: false,
+ default: __('Select'),
+ },
+ searchText: {
+ type: String,
+ required: false,
+ default: __('Search'),
+ },
+ presetOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selected: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ searchTerm: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isSearchEmpty() {
+ return this.searchTerm === '' && !this.isLoading;
+ },
+ noOptionsFound() {
+ return !this.isSearchEmpty && this.options.length === 0;
+ },
+ },
+ methods: {
+ selectOption(option) {
+ this.$emit('set-option', option || null);
+ },
+ isSelected(option) {
+ return (
+ this.selected &&
+ ((option.name && this.selected.name === option.name) ||
+ (option.title && this.selected.title === option.title))
+ );
+ },
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
+ setFocus() {
+ this.$refs.search.focusInput();
+ },
+ setSearchTerm(search) {
+ this.$emit('set-search', search);
+ },
+ avatarUrl(option) {
+ return option.avatar_url || option.avatarUrl || null;
+ },
+ secondaryText(option) {
+ // TODO: this has some knowledge of the context where the component is used. We could later rework it.
+ return option.username || null;
+ },
+ },
+ i18n: {
+ noMatchingResults: __('No matching results'),
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="dropdown"
+ :text="selectText"
+ lazy
+ menu-class="gl-w-full!"
+ class="gl-w-full"
+ v-on="$listeners"
+ @shown="setFocus"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ ref="search"
+ :value="searchTerm"
+ :placeholder="searchText"
+ class="js-dropdown-input-field"
+ @input="setSearchTerm"
+ />
+ </template>
+ <gl-dropdown-form class="gl-relative gl-min-h-7">
+ <gl-loading-icon
+ v-if="isLoading"
+ size="md"
+ class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <gl-dropdown-item
+ v-for="option in presetOptions"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click="selectOption(option)"
+ >
+ <slot name="preset-item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
+ <gl-dropdown-item
+ v-for="option in options"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ :avatar-url="avatarUrl(option)"
+ :secondary-text="secondaryText(option)"
+ data-testid="unselected-option"
+ @click="selectOption(option)"
+ >
+ <slot name="item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
+ {{ $options.i18n.noMatchingResults }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown-form>
+ <template #footer>
+ <slot name="footer"></slot>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 994ce6a762a..2e9634819a0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -2,10 +2,14 @@ import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200;
export const MAX_RECENT_TOKENS_SIZE = 3;
+export const WEIGHT_TOKEN_SUGGESTIONS_SIZE = 21;
export const FILTER_NONE = 'None';
export const FILTER_ANY = 'Any';
export const FILTER_CURRENT = 'Current';
+export const FILTER_UPCOMING = 'Upcoming';
+export const FILTER_STARTED = 'Started';
+export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
@@ -24,11 +28,9 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
-export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
-
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
- { value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings
- { value: 'Started', text: __('Started') }, // eslint-disable-line @gitlab/require-i18n-strings
+ { value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
+ { value: FILTER_STARTED, text: __(FILTER_STARTED) },
]);
export const SortDirection = {
@@ -36,12 +38,14 @@ export const SortDirection = {
ascending: 'ascending',
};
+export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
export const TOKEN_TITLE_AUTHOR = __('Author');
export const TOKEN_TITLE_ASSIGNEE = __('Assignee');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_LABEL = __('Label');
+export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_ITERATION = __('Iteration');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 5ab287150f2..9dc5c5db276 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -16,7 +16,7 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import { SortDirection } from './constants';
-import { stripQuotes, uniqueTokens } from './filtered_search_utils';
+import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
components: {
@@ -223,9 +223,14 @@ export default {
// Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones
- const resultantSearches = this.recentSearchesStore.setRecentSearches(
+ let resultantSearches = this.recentSearchesStore.setRecentSearches(
this.recentSearchesStore.state.recentSearches.concat(searches),
);
+ // If visited URL has search params, add them to recent search store
+ if (filterEmptySearchTerm(this.filterValue).length) {
+ resultantSearches = this.recentSearchesStore.addRecentSearch(this.filterValue);
+ }
+
this.recentSearchesService.save(resultantSearches);
this.recentSearches = resultantSearches;
});
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 571d24b50cf..6573f366b52 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -247,3 +247,12 @@ export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenVa
);
}
}
+
+/**
+ * Removes `FILTERED_SEARCH_TERM` tokens with empty data
+ *
+ * @param filterTokens array of filtered search tokens
+ * @return {Array} array of filtered search tokens
+ */
+export const filterEmptySearchTerm = (filterTokens = []) =>
+ filterTokens.filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index a25a19a006c..ae5d3965de1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -31,19 +31,25 @@ export default {
data() {
return {
authors: this.config.initialAuthors || [],
- defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
- preloadedAuthors: this.config.preloadedAuthors || [],
loading: false,
};
},
+ computed: {
+ defaultAuthors() {
+ return this.config.defaultAuthors || [DEFAULT_LABEL_ANY];
+ },
+ preloadedAuthors() {
+ return this.config.preloadedAuthors || [];
+ },
+ },
methods: {
- getActiveAuthor(authors, currentValue) {
- return authors.find((author) => author.username.toLowerCase() === currentValue);
+ getActiveAuthor(authors, data) {
+ return authors.find((author) => author.username.toLowerCase() === data.toLowerCase());
},
getAvatarUrl(author) {
return author.avatarUrl || author.avatar_url;
},
- fetchAuthorBySearchTerm(searchTerm) {
+ fetchAuthors(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
? this.config.fetchAuthors(this.config.fetchPath, searchTerm)
@@ -76,11 +82,11 @@ export default {
:active="active"
:suggestions-loading="loading"
:suggestions="authors"
- :fn-active-token-value="getActiveAuthor"
+ :get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- @fetch-suggestions="fetchAuthorBySearchTerm"
+ @fetch-suggestions="fetchAuthors"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@@ -91,7 +97,7 @@ export default {
shape="circle"
class="gl-mr-2"
/>
- <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span>
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index a4804525a53..d1326e96794 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { DEBOUNCE_DELAY } from '../constants';
+import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@@ -42,12 +42,10 @@ export default {
required: false,
default: () => [],
},
- fnActiveTokenValue: {
+ getActiveTokenValue: {
type: Function,
required: false,
- default: (suggestions, currentTokenValue) => {
- return suggestions.find(({ value }) => value === currentTokenValue);
- },
+ default: (suggestions, data) => suggestions.find(({ value }) => value === data),
},
defaultSuggestions: {
type: Array,
@@ -69,11 +67,6 @@ export default {
required: false,
default: 'id',
},
- fnCurrentTokenValue: {
- type: Function,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -81,7 +74,6 @@ export default {
recentSuggestions: this.recentSuggestionsStorageKey
? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
: [],
- loading: false,
};
},
computed: {
@@ -94,14 +86,16 @@ export default {
preloadedTokenIds() {
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
- currentTokenValue() {
- if (this.fnCurrentTokenValue) {
- return this.fnCurrentTokenValue(this.value.data);
- }
- return this.value.data.toLowerCase();
- },
activeTokenValue() {
- return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue);
+ return this.getActiveTokenValue(this.suggestions, this.value.data);
+ },
+ availableDefaultSuggestions() {
+ if (this.value.operator === OPERATOR_IS_NOT) {
+ return this.defaultSuggestions.filter(
+ (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ );
+ }
+ return this.defaultSuggestions;
},
/**
* Return all the suggestions when searchKey is present
@@ -117,6 +111,29 @@ export default {
!this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]),
);
},
+ showDefaultSuggestions() {
+ return this.availableDefaultSuggestions.length;
+ },
+ showRecentSuggestions() {
+ return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
+ },
+ showPreloadedSuggestions() {
+ return this.preloadedSuggestions.length && !this.searchKey;
+ },
+ showAvailableSuggestions() {
+ return this.availableSuggestions.length;
+ },
+ showSuggestions() {
+ // These conditions must match the template under `#suggestions` slot
+ // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65817#note_632619411
+ return (
+ this.showDefaultSuggestions ||
+ this.showRecentSuggestions ||
+ this.showPreloadedSuggestions ||
+ this.suggestionsLoading ||
+ this.showAvailableSuggestions
+ );
+ },
},
watch: {
active: {
@@ -168,10 +185,10 @@ export default {
<template #view="viewTokenProps">
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
</template>
- <template #suggestions>
- <template v-if="defaultSuggestions.length">
+ <template v-if="showSuggestions" #suggestions>
+ <template v-if="showDefaultSuggestions">
<gl-filtered-search-suggestion
- v-for="token in defaultSuggestions"
+ v-for="token in availableDefaultSuggestions"
:key="token.value"
:value="token.value"
>
@@ -179,13 +196,13 @@ export default {
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
</template>
- <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey">
+ <template v-if="showRecentSuggestions">
<gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
<slot name="suggestions-list" :suggestions="recentSuggestions"></slot>
<gl-dropdown-divider />
</template>
<slot
- v-if="preloadedSuggestions.length && !searchKey"
+ v-if="showPreloadedSuggestions"
name="suggestions-list"
:suggestions="preloadedSuggestions"
></slot>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index 5859fd10688..4ecfc1cf40c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -1,27 +1,19 @@
<script>
-import {
- GlToken,
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-
-import { DEBOUNCE_DELAY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
export default {
components: {
- GlToken,
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -34,82 +26,62 @@ export default {
data() {
return {
branches: this.config.initialBranches || [],
- defaultBranches: this.config.defaultBranches || [],
- loading: true,
+ loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeBranch() {
- return this.branches.find((branch) => branch.name.toLowerCase() === this.currentValue);
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.branches.length) {
- this.fetchBranchBySearchTerm(this.value.data);
- }
- },
+ defaultBranches() {
+ return this.config.defaultBranches || [];
},
},
methods: {
- fetchBranchBySearchTerm(searchTerm) {
+ getActiveBranch(branches, data) {
+ return branches.find((branch) => branch.name.toLowerCase() === data.toLowerCase());
+ },
+ fetchBranches(searchTerm) {
this.loading = true;
this.config
.fetchBranches(searchTerm)
.then(({ data }) => {
this.branches = data;
})
- .catch(() => createFlash({ message: __('There was a problem fetching branches.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching branches.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchBranches: debounce(function debouncedSearch({ data }) {
- this.fetchBranchBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultBranches"
+ :suggestions="branches"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveBranch"
+ @fetch-suggestions="fetchBranches"
v-on="$listeners"
- @input="searchBranches"
>
- <template #view-token="{ inputValue }">
- <gl-token variant="search-value">{{
- activeBranch ? activeBranch.name : inputValue
- }}</gl-token>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="branch in defaultBranches"
- :key="branch.value"
- :value="branch.value"
+ v-for="branch in suggestions"
+ :key="branch.id"
+ :value="branch.name"
>
- {{ branch.text }}
+ <div class="gl-display-flex">
+ <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
+ {{ branch.name }}
+ </div>
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultBranches.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="branch in branches"
- :key="branch.id"
- :value="branch.name"
- >
- <div class="gl-display-flex">
- <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
- <div>{{ branch.name }}</div>
- </div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index d186f46866c..5a69751a2cc 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -1,26 +1,21 @@
<script>
-import {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-
-import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -33,87 +28,63 @@ export default {
data() {
return {
emojis: this.config.initialEmojis || [],
- defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY,
- loading: true,
+ loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeEmoji() {
- return this.emojis.find(
- (emoji) => emoji.name.toLowerCase() === stripQuotes(this.currentValue),
- );
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.emojis.length) {
- this.fetchEmojiBySearchTerm(this.value.data);
- }
- },
+ defaultEmojis() {
+ return this.config.defaultEmojis || DEFAULT_NONE_ANY;
},
},
methods: {
- fetchEmojiBySearchTerm(searchTerm) {
+ getActiveEmoji(emojis, data) {
+ return emojis.find((emoji) => emoji.name.toLowerCase() === stripQuotes(data).toLowerCase());
+ },
+ fetchEmojis(searchTerm) {
this.loading = true;
this.config
.fetchEmojis(searchTerm)
- .then((res) => {
- this.emojis = Array.isArray(res) ? res : res.data;
+ .then((response) => {
+ this.emojis = Array.isArray(response) ? response : response.data;
+ })
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching emojis.') });
})
- .catch(() =>
- createFlash({
- message: __('There was a problem fetching emojis.'),
- }),
- )
.finally(() => {
this.loading = false;
});
},
- searchEmojis: debounce(function debouncedSearch({ data }) {
- this.fetchEmojiBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultEmojis"
+ :suggestions="emojis"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveEmoji"
+ @fetch-suggestions="fetchEmojis"
v-on="$listeners"
- @input="searchEmojis"
>
- <template #view="{ inputValue }">
- <gl-emoji v-if="activeEmoji" :data-name="activeEmoji.name" />
- <span v-else>{{ inputValue }}</span>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ <gl-emoji v-if="activeTokenValue" :data-name="activeTokenValue.name" />
+ <template v-else>{{ inputValue }}</template>
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="emoji in defaultEmojis"
- :key="emoji.value"
- :value="emoji.value"
+ v-for="emoji in suggestions"
+ :key="emoji.name"
+ :value="emoji.name"
>
- {{ emoji.value }}
+ <div class="gl-display-flex">
+ <gl-emoji class="gl-mr-3" :data-name="emoji.name" />
+ {{ emoji.name }}
+ </div>
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultEmojis.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="emoji in emojis"
- :key="emoji.name"
- :value="emoji.name"
- >
- <div class="gl-display-flex">
- <gl-emoji :data-name="emoji.name" />
- <span class="gl-ml-3">{{ emoji.name }}</span>
- </div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
index aa234cf86d9..9f68308808e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
@@ -8,7 +8,7 @@ import {
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
+import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
export default {
separator: '::&',
@@ -48,6 +48,14 @@ export default {
defaultEpics() {
return this.config.defaultEpics || DEFAULT_NONE_ANY;
},
+ availableDefaultEpics() {
+ if (this.value.operator === OPERATOR_IS_NOT) {
+ return this.defaultEpics.filter(
+ (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ );
+ }
+ return this.defaultEpics;
+ },
activeEpic() {
if (this.currentValue && this.epics.length) {
// Check if current value is an epic ID.
@@ -99,7 +107,7 @@ export default {
// We don't have any information about selected token except for its
// group path and iid joined by separator, so we need to manually
// compose epic path from it.
- if (data.includes(this.$options.separator)) {
+ if (data.includes?.(this.$options.separator)) {
const [groupPath, epicIid] = data.split(this.$options.separator);
epicPath = `/groups/${groupPath}/-/epics/${epicIid}`;
}
@@ -127,13 +135,13 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
- v-for="epic in defaultEpics"
+ v-for="epic in availableDefaultEpics"
:key="epic.value"
:value="epic.value"
>
{{ epic.text }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultEpics.length" />
+ <gl-dropdown-divider v-if="availableDefaultEpics.length" />
<gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
index ba8b2421726..c1d1bc7da91 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -1,24 +1,21 @@
<script>
-import {
- GlDropdownDivider,
- GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_ITERATIONS } from '../constants';
export default {
components: {
- GlDropdownDivider,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -35,84 +32,58 @@ export default {
};
},
computed: {
- currentValue() {
- return this.value.data;
- },
- activeIteration() {
- return this.iterations.find(
- (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue),
- );
- },
defaultIterations() {
return this.config.defaultIterations || DEFAULT_ITERATIONS;
},
},
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.iterations.length) {
- this.fetchIterationBySearchTerm(this.currentValue);
- }
- },
- },
- },
methods: {
- getValue(iteration) {
- return String(getIdFromGraphQLId(iteration.id));
+ getActiveIteration(iterations, data) {
+ return iterations.find((iteration) => this.getValue(iteration) === data);
},
- fetchIterationBySearchTerm(searchTerm) {
- const fetchPromise = this.config.fetchPath
- ? this.config.fetchIterations(this.config.fetchPath, searchTerm)
- : this.config.fetchIterations(searchTerm);
-
+ fetchIterations(searchTerm) {
this.loading = true;
-
- fetchPromise
+ this.config
+ .fetchIterations(searchTerm)
.then((response) => {
this.iterations = Array.isArray(response) ? response : response.data;
})
- .catch(() => createFlash({ message: __('There was a problem fetching iterations.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching iterations.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchIterations: debounce(function debouncedSearch({ data }) {
- this.fetchIterationBySearchTerm(data);
- }, DEBOUNCE_DELAY),
+ getValue(iteration) {
+ return String(getIdFromGraphQLId(iteration.id));
+ },
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultIterations"
+ :suggestions="iterations"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveIteration"
+ @fetch-suggestions="fetchIterations"
v-on="$listeners"
- @input="searchIterations"
>
- <template #view="{ inputValue }">
- {{ activeIteration ? activeIteration.title : inputValue }}
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="iteration in defaultIterations"
- :key="iteration.value"
- :value="iteration.value"
+ v-for="iteration in suggestions"
+ :key="iteration.id"
+ :value="getValue(iteration)"
>
- {{ iteration.text }}
+ {{ iteration.title }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultIterations.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="iteration in iterations"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 4d08f81fee9..c31f3a25fb1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -5,7 +5,7 @@ import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { DEFAULT_LABELS } from '../constants';
+import { DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
@@ -33,14 +33,18 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
- defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: false,
};
},
+ computed: {
+ defaultLabels() {
+ return this.config.defaultLabels || DEFAULT_NONE_ANY;
+ },
+ },
methods: {
- getActiveLabel(labels, currentValue) {
+ getActiveLabel(labels, data) {
return labels.find(
- (label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue),
+ (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(),
);
},
/**
@@ -68,7 +72,7 @@ export default {
}
return {};
},
- fetchLabelBySearchTerm(searchTerm) {
+ fetchLabels(searchTerm) {
this.loading = true;
this.config
.fetchLabels(searchTerm)
@@ -98,10 +102,10 @@ export default {
:active="active"
:suggestions-loading="loading"
:suggestions="labels"
- :fn-active-token-value="getActiveLabel"
+ :get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- @fetch-suggestions="fetchLabelBySearchTerm"
+ @fetch-suggestions="fetchLabels"
v-on="$listeners"
>
<template
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 66ad5ef5b4e..4b9ad6d8f91 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -1,27 +1,22 @@
<script>
-import {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
-
-import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_MILESTONES } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -34,36 +29,21 @@ export default {
data() {
return {
milestones: this.config.initialMilestones || [],
- defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeMilestone() {
- return this.milestones.find(
- (milestone) => milestone.title.toLowerCase() === stripQuotes(this.currentValue),
- );
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.milestones.length) {
- this.fetchMilestoneBySearchTerm(this.value.data);
- }
- },
+ defaultMilestones() {
+ return this.config.defaultMilestones || DEFAULT_MILESTONES;
},
},
methods: {
- fetchMilestoneBySearchTerm(searchTerm = '') {
- if (this.loading) {
- return;
- }
-
+ getActiveMilestone(milestones, data) {
+ return milestones.find(
+ (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(),
+ );
+ },
+ fetchMilestones(searchTerm) {
this.loading = true;
this.config
.fetchMilestones(searchTerm)
@@ -71,47 +51,40 @@ export default {
const data = Array.isArray(response) ? response : response.data;
this.milestones = data.slice().sort(sortMilestonesByDueDate);
})
- .catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching milestones.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchMilestones: debounce(function debouncedSearch({ data }) {
- this.fetchMilestoneBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultMilestones"
+ :suggestions="milestones"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveMilestone"
+ @fetch-suggestions="fetchMilestones"
v-on="$listeners"
- @input="searchMilestones"
>
- <template #view="{ inputValue }">
- <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ %{{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="milestone in defaultMilestones"
- :key="milestone.value"
- :value="milestone.value"
+ v-for="milestone in suggestions"
+ :key="milestone.id"
+ :value="milestone.title"
>
- {{ milestone.text }}
+ {{ milestone.title }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultMilestones.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="milestone in milestones"
- :key="milestone.id"
- :value="milestone.title"
- >
- <div>{{ milestone.title }}</div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
index 72116f0e991..280fb234576 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
@@ -1,15 +1,20 @@
<script>
-import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants';
+
+const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString());
export default {
- baseWeights: ['0', '1', '2', '3', '4', '5'],
components: {
- GlDropdownDivider,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -21,38 +26,41 @@ export default {
},
data() {
return {
- weights: this.$options.baseWeights,
- defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY,
+ weights,
};
},
+ computed: {
+ defaultWeights() {
+ return this.config.defaultWeights || DEFAULT_NONE_ANY;
+ },
+ },
methods: {
- updateWeights({ data }) {
- const weight = parseInt(data, 10);
- this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)];
+ getActiveWeight(weightSuggestions, data) {
+ return weightSuggestions.find((weight) => weight === data);
+ },
+ updateWeights(searchTerm) {
+ const weight = parseInt(searchTerm, 10);
+ this.weights = Number.isNaN(weight) ? weights : [String(weight)];
},
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultWeights"
+ :suggestions="weights"
+ :get-active-token-value="getActiveWeight"
+ @fetch-suggestions="updateWeights"
v-on="$listeners"
- @input="updateWeights"
>
- <template #suggestions>
- <gl-filtered-search-suggestion
- v-for="weight in defaultWeights"
- :key="weight.value"
- :value="weight.value"
- >
- {{ weight.text }}
- </gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultWeights.length" />
- <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight">
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight">
{{ weight }}
</gl-filtered-search-suggestion>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d343ba700ab..3ed9de6c133 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,5 +1,5 @@
<script>
-import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import $ from 'jquery';
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
@@ -10,7 +10,6 @@ import ToolbarButton from './toolbar_button.vue';
export default {
components: {
ToolbarButton,
- GlIcon,
GlPopover,
GlButton,
},
@@ -46,6 +45,7 @@ export default {
data() {
return {
tag: '> ',
+ suggestPopoverVisible: false,
};
},
computed: {
@@ -76,15 +76,27 @@ export default {
return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
},
},
+ watch: {
+ showSuggestPopover() {
+ this.updateSuggestPopoverVisibility();
+ },
+ },
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
+
+ this.updateSuggestPopoverVisibility();
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
+ async updateSuggestPopoverVisibility() {
+ await this.$nextTick();
+
+ this.suggestPopoverVisible = this.showSuggestPopover && this.canSuggest;
+ },
isValid(form) {
return (
!form ||
@@ -153,127 +165,114 @@ export default {
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
- <div class="d-inline-block">
- <toolbar-button
- tag="**"
- :button-title="
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.bold"
- icon="bold"
- />
- <toolbar-button
- tag="_"
- :button-title="
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.italic"
- icon="italic"
- />
+ <toolbar-button
+ tag="**"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.bold"
+ icon="bold"
+ />
+ <toolbar-button
+ tag="_"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.italic"
+ icon="italic"
+ />
+ <toolbar-button
+ :prepend="true"
+ :tag="tag"
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ @click="handleQuote"
+ />
+ <template v-if="canSuggest">
<toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
:prepend="true"
- :tag="tag"
- :button-title="__('Insert a quote')"
- icon="quote"
- @click="handleQuote"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ data-qa-selector="suggestion_button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
/>
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <template v-if="canSuggest">
- <toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- data-qa-selector="suggestion_button"
- class="js-suggestion-btn"
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="info"
+ category="primary"
+ size="small"
@click="handleSuggestDismissed"
- />
- <gl-popover
- v-if="showSuggestPopover && $refs.suggestButton"
- :target="$refs.suggestButton"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="showSuggestPopover"
>
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="info"
- category="primary"
- size="sm"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
- <toolbar-button
- tag="[{text}](url)"
- tag-select="url"
- :button-title="
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.link"
- icon="link"
- />
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <toolbar-button
- :prepend="true"
- tag="- "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- />
- <toolbar-button
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- />
- <toolbar-button
- :prepend="true"
- tag="- [ ] "
- :button-title="__('Add a task list')"
- icon="list-task"
- />
- <toolbar-button
- :tag="mdCollapsibleSection"
- :prepend="true"
- tag-select="Click to expand"
- :button-title="__('Add a collapsible section')"
- icon="details-block"
- />
- <toolbar-button
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- />
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <button
- v-gl-tooltip
- :aria-label="__('Go full screen')"
- class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- :title="__('Go full screen')"
- type="button"
- >
- <gl-icon name="maximize" />
- </button>
- </div>
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
+ <toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.link"
+ icon="link"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="- "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="- [ ] "
+ :button-title="__('Add a task list')"
+ icon="list-task"
+ />
+ <toolbar-button
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ />
+ <toolbar-button
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ />
+ <toolbar-button
+ class="js-zen-enter"
+ :prepend="true"
+ :button-title="__('Go full screen')"
+ icon="maximize"
+ />
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 6c35741e7e5..6a83939795c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,9 +1,9 @@
<script>
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
components: {
- GlIcon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -19,7 +19,8 @@ export default {
},
tag: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
tagBlock: {
type: String,
@@ -71,7 +72,7 @@ export default {
</script>
<template>
- <button
+ <gl-button
v-gl-tooltip
:data-md-tag="tag"
:data-md-cursor-offset="cursorOffset"
@@ -82,11 +83,11 @@ export default {
:data-md-shortcuts="shortcutsString"
:title="buttonTitle"
:aria-label="buttonTitle"
+ :icon="icon"
type="button"
- class="toolbar-btn js-md"
+ category="tertiary"
+ class="js-md"
data-container="body"
@click="() => $emit('click')"
- >
- <gl-icon :name="icon" />
- </button>
+ />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index 79a9e1fca8c..8a67754993d 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -42,12 +42,12 @@ export default {
itemsCount: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
pageInfo: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
statusTabs: {
type: Array,
diff --git a/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue b/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue
new file mode 100644
index 00000000000..fa11661255f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlAlert,
+ },
+ i18n: {
+ genericErrorMessage: s__('CsvParser|Failed to render the CSV file for the following reasons:'),
+ MissingQuotes: s__('CsvParser|Quoted field unterminated'),
+ InvalidQuotes: s__('CsvParser|Trailing quote on quoted field is malformed'),
+ UndetectableDelimiter: s__('CsvParser|Unable to auto-detect delimiter; defaulted to ","'),
+ TooManyFields: s__('CsvParser|Too many fields'),
+ TooFewFields: s__('CsvParser|Too few fields'),
+ },
+ props: {
+ papaParseErrors: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ errorMessages() {
+ const errorMessages = this.papaParseErrors.map(
+ (error) => this.$options.i18n[error.code] ?? error.message,
+ );
+ return new Set(errorMessages);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="danger" :dismissible="false">
+ {{ $options.i18n.genericErrorMessage }}
+ <ul class="gl-mb-0!">
+ <li v-for="error in errorMessages" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index a0c5a0559de..f21092af501 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -15,6 +15,11 @@ export default {
ProjectListItem,
},
props: {
+ maxListHeight: {
+ type: Number,
+ required: false,
+ default: 402,
+ },
projectSearchResults: {
type: Array,
required: true,
@@ -101,7 +106,7 @@ export default {
<div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
- :max-list-height="402"
+ :max-list-height="maxListHeight"
:fetched-items="projectSearchResults.length"
:total-items="totalResults"
@bottomReached="bottomReached"
diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
deleted file mode 100644
index 07272a5b8d6..00000000000
--- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<script>
-import { GlFormCheckbox, GlModal } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import csrf from '~/lib/utils/csrf';
-import { s__, __ } from '~/locale';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
-
-export default {
- actionCancel: {
- text: __('Cancel'),
- },
- csrf,
- components: {
- GlFormCheckbox,
- GlModal,
- OncallSchedulesList,
- },
- data() {
- return {
- modalData: {},
- };
- },
- computed: {
- isAccessRequest() {
- return parseBoolean(this.modalData.isAccessRequest);
- },
- isInvite() {
- return parseBoolean(this.modalData.isInvite);
- },
- isGroupMember() {
- return this.modalData.memberType === 'GroupMember';
- },
- actionText() {
- if (this.isAccessRequest) {
- return __('Deny access request');
- } else if (this.isInvite) {
- return s__('Member|Revoke invite');
- }
-
- return __('Remove member');
- },
- actionPrimary() {
- return {
- text: this.actionText,
- attributes: {
- variant: 'danger',
- },
- };
- },
- showUnassignIssuablesCheckbox() {
- return !this.isAccessRequest && !this.isInvite;
- },
- isPartOfOncallSchedules() {
- return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
- },
- oncallSchedules() {
- try {
- return JSON.parse(this.modalData.oncallSchedules);
- } catch (e) {
- Sentry.captureException(e);
- }
- return {};
- },
- },
- mounted() {
- document.addEventListener('click', this.handleClick);
- },
- beforeDestroy() {
- document.removeEventListener('click', this.handleClick);
- },
- methods: {
- handleClick(event) {
- const removeButton = event.target.closest('.js-remove-member-button');
- if (removeButton) {
- this.modalData = removeButton.dataset;
- this.$refs.modal.show();
- }
- },
- submitForm() {
- this.$refs.form.submit();
- },
- },
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- modal-id="remove-member-modal"
- :action-cancel="$options.actionCancel"
- :action-primary="actionPrimary"
- :title="actionText"
- data-qa-selector="remove_member_modal_content"
- @primary="submitForm"
- >
- <form ref="form" :action="modalData.memberPath" method="post">
- <p data-testid="modal-message">{{ modalData.message }}</p>
-
- <oncall-schedules-list
- v-if="isPartOfOncallSchedules"
- :schedules="oncallSchedules.schedules"
- :user-name="oncallSchedules.name"
- />
-
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
- <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
- {{ __('Also remove direct user membership from subgroups and projects') }}
- </gl-form-checkbox>
- <gl-form-checkbox v-if="showUnassignIssuablesCheckbox" name="unassign_issuables">
- {{ __('Also unassign this user from related issues and merge requests') }}
- </gl-form-checkbox>
- </form>
- </gl-modal>
-</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
index 9914bfc6026..623e7799493 100644
--- 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
@@ -132,6 +132,9 @@ export default {
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
this.searchKey = '';
+
+ // Prevent parent form submission upon hitting enter.
+ e.preventDefault();
} else if (e.keyCode === ESC_KEY_CODE) {
this.toggleDropdownContents();
}
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
index aad754e15b0..7989ad40b5a 100644
--- 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
@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
- variant="link"
- class="float-right gl-text-gray-900! gl-hover-text-blue-800! js-sidebar-dropdown-toggle"
+ category="tertiary"
+ size="small"
+ class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>
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
index 87af3ffc52c..4234bc72f3a 100644
--- 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
@@ -142,6 +142,7 @@ export default {
this.setInitialState({
selectedLabels,
});
+ setTimeout(() => this.updateLabelsSetState(), 100);
},
showDropdownContents(showDropdownContents) {
this.setContentIsOnViewport(showDropdownContents);
@@ -184,7 +185,7 @@ export default {
document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
- ...mapActions(['setInitialState', 'toggleDropdownContents']),
+ ...mapActions(['setInitialState', 'toggleDropdownContents', 'updateLabelsSetState']),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
@@ -315,7 +316,7 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
- v-show="dropdownButtonVisible && showDropdownContents"
+ v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
:render-on-top="!contentIsOnViewport"
/>
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
index 178be0f6da0..0c697e624ab 100644
--- 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
@@ -20,7 +20,11 @@ export const receiveLabelsFailure = ({ commit }) => {
message: __('Error fetching labels.'),
});
};
-export const fetchLabels = ({ state, dispatch }) => {
+export const fetchLabels = ({ state, dispatch }, options) => {
+ if (state.labelsFetched && (!options || !options.refetch)) {
+ return Promise.resolve();
+ }
+
dispatch('requestLabels');
return axios
.get(state.labelsFetchPath)
@@ -46,6 +50,7 @@ export const createLabel = ({ state, dispatch }, label) => {
})
.then(({ data }) => {
if (data.id) {
+ dispatch('fetchLabels', { refetch: true });
dispatch('receiveCreateLabelSuccess');
dispatch('toggleDropdownContentsCreateView');
} else {
@@ -60,3 +65,5 @@ export const createLabel = ({ state, dispatch }, label) => {
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
+
+export const updateLabelsSetState = ({ commit }) => commit(types.UPDATE_LABELS_SET_STATE);
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
index 2e044dc3b3c..f26e36031f4 100644
--- 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
@@ -18,3 +18,5 @@ 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';
+
+export const UPDATE_LABELS_SET_STATE = 'UPDATE_LABELS_SET_STATE';
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
index 2e0a57f15dd..8853dc8b9e3 100644
--- 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
@@ -34,15 +34,12 @@ export default {
// 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;
- }, []);
+ state.labelsFetched = true;
+ state.labels = labels.map((label) => ({
+ ...label,
+ set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
+ }));
},
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
@@ -79,4 +76,11 @@ export default {
}
}
},
+
+ [types.UPDATE_LABELS_SET_STATE](state) {
+ state.labels = state.labels.map((label) => ({
+ ...label,
+ set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
+ }));
+ },
};
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
index d66cfed4163..0185d5f88e1 100644
--- 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
@@ -1,6 +1,7 @@
export default () => ({
// Initial Data
labels: [],
+ labelsFetched: false,
selectedLabels: [],
labelsListTitle: '',
labelsCreateTitle: '',
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 1f0704f7308..6694e349b6e 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
@@ -21,9 +21,29 @@ export default {
type: String,
required: true,
},
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowMultiselect: {
+ type: Boolean,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
},
computed: {
- ...mapState(['showDropdownContentsCreateView', 'labelsListTitle']),
+ ...mapState(['showDropdownContentsCreateView']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() {
if (this.showDropdownContentsCreateView) {
@@ -75,6 +95,16 @@ export default {
@click="toggleDropdownContents"
/>
</div>
- <component :is="dropdownContentsView" @hideCreateView="toggleDropdownContentsCreateView" />
+ <component
+ :is="dropdownContentsView"
+ :selected-labels="selectedLabels"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ @hideCreateView="toggleDropdownContentsCreateView"
+ @closeDropdown="$emit('closeDropdown', $event)"
+ @toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
+ />
</div>
</template>
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 bff34743344..ffa37424c2c 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,38 +1,91 @@
<script>
-import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { mapState, mapGetters, mapActions } from 'vuex';
-
+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 { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-
+import { __ } from '~/locale';
+import { DropdownVariant } from './constants';
+import projectLabelsQuery from './graphql/project_labels.query.graphql';
import LabelItem from './label_item.vue';
export default {
components: {
- GlIntersectionObserver,
GlLoadingIcon,
GlSearchBoxByType,
GlLink,
LabelItem,
},
+ inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'],
+ props: {
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowMultiselect: {
+ type: Boolean,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
searchKey: '',
+ labels: [],
currentHighlightItem: -1,
+ localSelectedLabels: [...this.selectedLabels],
};
},
+ apollo: {
+ labels: {
+ query: projectLabelsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ searchTerm: this.searchKey,
+ };
+ },
+ skip() {
+ return this.searchKey.length === 1;
+ },
+ 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.') });
+ },
+ },
+ },
computed: {
- ...mapState([
- 'allowLabelCreate',
- 'allowMultiselect',
- 'labelsManagePath',
- 'labels',
- 'labelsFetchInProgress',
- 'labelsListTitle',
- 'footerCreateLabelTitle',
- 'footerManageLabelTitle',
- ]),
- ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
+ isDropdownVariantSidebar() {
+ return this.variant === DropdownVariant.Sidebar;
+ },
+ isDropdownVariantEmbedded() {
+ return this.variant === DropdownVariant.Embedded;
+ },
+ labelsFetchInProgress() {
+ return this.$apollo.queries.labels.loading;
+ },
+ localSelectedLabelsIds() {
+ return this.localSelectedLabels.map((label) => label.id);
+ },
visibleLabels() {
if (this.searchKey) {
return fuzzaldrinPlus.filter(this.labels, this.searchKey, {
@@ -55,17 +108,16 @@ export default {
}
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ beforeDestroy() {
+ this.$emit('closeDropdown', this.localSelectedLabels);
+ this.debouncedSearchKeyUpdate.cancel();
+ },
methods: {
- ...mapActions([
- 'toggleDropdownContents',
- 'toggleDropdownContentsCreateView',
- 'fetchLabels',
- 'receiveLabelsSuccess',
- 'updateSelectedLabels',
- 'toggleDropdownContents',
- ]),
isLabelSelected(label) {
- return this.selectedLabelsList.includes(label.id);
+ return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
},
/**
* This method scrolls item from dropdown into
@@ -86,23 +138,17 @@ export default {
}
}
},
- handleComponentAppear() {
- // We can avoid putting `catch` block here
- // as failure is handled within actions.js already.
- return this.fetchLabels().then(() => {
- this.$refs.searchInput.focusInput();
- });
- },
- /**
- * We want to remove loaded labels to ensure component
- * fetches fresh set of labels every time when shown.
- */
- handleComponentDisappear() {
- this.receiveLabelsSuccess([]);
- },
- handleCreateLabelClick() {
- this.receiveLabelsSuccess([]);
- this.toggleDropdownContentsCreateView();
+ updateSelectedLabels(label) {
+ if (this.isLabelSelected(label)) {
+ this.localSelectedLabels = this.localSelectedLabels.filter(
+ ({ id }) => id !== getIdFromGraphQLId(label.id),
+ );
+ } else {
+ this.localSelectedLabels.push({
+ ...label,
+ id: getIdFromGraphQLId(label.id),
+ });
+ }
},
/**
* This method enables keyboard navigation support for
@@ -117,10 +163,10 @@ export default {
) {
this.currentHighlightItem += 1;
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
- this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
+ this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
this.searchKey = '';
} else if (e.keyCode === ESC_KEY_CODE) {
- this.toggleDropdownContents();
+ this.$emit('closeDropdown', this.localSelectedLabels);
}
if (e.keyCode !== ESC_KEY_CODE) {
@@ -132,68 +178,82 @@ export default {
}
},
handleLabelClick(label) {
- this.updateSelectedLabels([label]);
- if (!this.allowMultiselect) this.toggleDropdownContents();
+ this.updateSelectedLabels(label);
+ if (!this.allowMultiselect) {
+ this.$emit('closeDropdown', this.localSelectedLabels);
+ }
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
},
},
};
</script>
<template>
- <gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear">
- <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
- <div class="dropdown-input" @click.stop="() => {}">
- <gl-search-box-by-type
- ref="searchInput"
- v-model="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- />
- </div>
- <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
- <gl-loading-icon
- v-if="labelsFetchInProgress"
- class="labels-fetch-loading gl-align-items-center w-100 h-100"
- size="md"
+ <div
+ class="labels-select-contents-list js-labels-list"
+ data-testid="dropdown-wrapper"
+ @keydown="handleKeyDown"
+ >
+ <div class="dropdown-input" @click.stop="() => {}">
+ <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>
+ <div ref="labelsListContainer" class="dropdown-content" 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"
+ />
+ <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list">
+ <label-item
+ v-for="(label, index) in visibleLabels"
+ :key="label.id"
+ :label="label"
+ :is-label-set="isLabelSelected(label)"
+ :highlight="index === currentHighlightItem"
+ @clickLabel="handleLabelClick(label)"
/>
- <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word">
- <label-item
- v-for="(label, index) in visibleLabels"
- :key="label.id"
- :label="label"
- :is-label-set="label.set"
- :highlight="index === currentHighlightItem"
- @clickLabel="handleLabelClick(label)"
- />
- <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
- {{ __('No matching results') }}
- </li>
- </ul>
- </div>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-footer"
- data-testid="dropdown-footer"
- >
- <ul class="list-unstyled">
- <li v-if="allowLabelCreate">
- <gl-link
- class="gl-display-flex w-100 flex-row text-break-word label-item"
- @click="handleCreateLabelClick"
- >
- {{ footerCreateLabelTitle }}
- </gl-link>
- </li>
- <li>
- <gl-link
- :href="labelsManagePath"
- class="gl-display-flex flex-row text-break-word label-item"
- >
- {{ footerManageLabelTitle }}
- </gl-link>
- </li>
- </ul>
- </div>
+ <li
+ v-show="showNoMatchingResultsMessage"
+ class="gl-p-3 gl-text-center"
+ data-testid="no-results"
+ >
+ {{ __('No matching results') }}
+ </li>
+ </ul>
+ </div>
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-footer"
+ data-testid="dropdown-footer"
+ >
+ <ul class="list-unstyled">
+ <li v-if="allowLabelCreate">
+ <gl-link
+ class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
+ data-testid="create-label-button"
+ @click="$emit('toggleDropdownContentsCreateView')"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-link>
+ </li>
+ <li>
+ <gl-link
+ :href="labelsManagePath"
+ class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
+ >
+ {{ footerManageLabelTitle }}
+ </gl-link>
+ </li>
+ </ul>
</div>
- </gl-intersection-observer>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
index b6d14965cfa..46edfa1c42a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
- variant="link"
- class="float-right js-sidebar-dropdown-toggle"
+ category="tertiary"
+ size="small"
+ class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
new file mode 100644
index 00000000000..dc39220487d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
@@ -0,0 +1,12 @@
+query projectLabels($fullPath: ID!, $searchTerm: String) {
+ workspace: project(fullPath: $fullPath) {
+ labels(searchTerm: $searchTerm, includeAncestorGroups: 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 87f36a5bb72..0499dfe468f 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
@@ -197,23 +197,6 @@ export default {
methods: {
...mapActions(['setInitialState', 'toggleDropdownContents']),
/**
- * This method differentiates between
- * dispatched actions and calls necessary method.
- */
- handleVuexActionDispatch(action, state) {
- if (
- action.type === 'toggleDropdownContents' &&
- !state.showDropdownButton &&
- !state.showDropdownContents
- ) {
- let filterFn = (label) => label.touched;
- if (this.isDropdownVariantEmbedded) {
- filterFn = (label) => label.set;
- }
- this.handleDropdownClose(state.labels.filter(filterFn));
- }
- },
- /**
* This method stores a mousedown event's target.
* Required by the click listener because the click
* event itself has no reference to this element.
@@ -276,6 +259,9 @@ export default {
handleDropdownClose(labels) {
// Only emit label updates if there are any labels to update
// on UI.
+ if (this.showDropdownContents) {
+ this.toggleDropdownContents();
+ }
if (labels.length) this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose');
},
@@ -330,10 +316,16 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
- v-show="dropdownButtonVisible && showDropdownContents"
+ v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport"
:labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ @closeDropdown="handleDropdownClose"
/>
</template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
@@ -341,7 +333,13 @@ export default {
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport"
+ :selected-labels="selectedLabels"
+ @closeDropdown="handleDropdownClose"
/>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
index 935f020f559..b3d4a204a81 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
@@ -1,6 +1,3 @@
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import * as types from './mutation_types';
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO
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);
- createFlash({
- message: __('Error fetching labels.'),
- });
-};
-export const fetchLabels = ({ state, dispatch }) => {
- dispatch('requestLabels');
- return axios
- .get(state.labelsFetchPath)
- .then(({ data }) => {
- dispatch('receiveLabelsSuccess', data);
- })
- .catch(() => dispatch('receiveLabelsFailure'));
-};
-
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
index b8da7a90b36..bd71c3b85f1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
@@ -1,13 +1,5 @@
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 TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
index 1c03d95f37b..45ec4d7ae04 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
@@ -26,27 +26,6 @@ export default {
[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.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
index e6229cf0a93..cdc7422c7df 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { todoLabel } from './utils';
+import { todoLabel, updateGlobalTodoCount } from './utils';
export default {
components: {
@@ -19,23 +19,11 @@ export default {
},
},
methods: {
- updateGlobalTodoCount(additionalTodoCount) {
- const countContainer = document.querySelector('.js-todos-count');
- if (countContainer === null) return;
- const currentCount = parseInt(countContainer.innerText, 10);
- const todoToggleEvent = new CustomEvent('todo:toggle', {
- detail: {
- count: Math.max(currentCount + additionalTodoCount, 0),
- },
- });
-
- document.dispatchEvent(todoToggleEvent);
- },
incrementGlobalTodoCount() {
- this.updateGlobalTodoCount(1);
+ updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
- this.updateGlobalTodoCount(-1);
+ updateGlobalTodoCount(-1);
},
onToggle(event) {
if (this.isTodo) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
index 59e72a2ffe3..098ab72dfb5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
@@ -3,3 +3,19 @@ import { __ } from '~/locale';
export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do');
};
+
+export const updateGlobalTodoCount = (additionalTodoCount) => {
+ const countContainer = document.querySelector('.js-todos-count');
+
+ if (countContainer === null) return;
+
+ const currentCount = parseInt(countContainer.innerText, 10);
+
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+};
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 55e2a786c8f..04423aac651 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -30,6 +30,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
linkHref: {
type: String,
required: false,
@@ -91,6 +96,7 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
>
<slot></slot> </user-avatar-image
><span
diff --git a/app/assets/javascripts/vue_shared/components/user_date.vue b/app/assets/javascripts/vue_shared/components/user_date.vue
index 38dddbf72c2..33531cc3278 100644
--- a/app/assets/javascripts/vue_shared/components/user_date.vue
+++ b/app/assets/javascripts/vue_shared/components/user_date.vue
@@ -1,7 +1,7 @@
<script>
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
-import { SHORT_DATE_FORMAT } from '../constants';
+import { SHORT_DATE_FORMAT, DATE_FORMATS } from '../constants';
export default {
props: {
@@ -10,6 +10,12 @@ export default {
required: false,
default: null,
},
+ dateFormat: {
+ type: String,
+ required: false,
+ default: SHORT_DATE_FORMAT,
+ validator: (dateFormat) => DATE_FORMATS.includes(dateFormat),
+ },
},
computed: {
formattedDate() {
@@ -17,7 +23,7 @@ export default {
if (date === null) {
return __('Never');
}
- return formatDate(new Date(date), SHORT_DATE_FORMAT);
+ return formatDate(new Date(date), this.dateFormat);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 5ba7c107c12..df0981aea7a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -59,11 +59,21 @@ export default {
required: false,
default: '',
},
+ webIdeText: {
+ type: String,
+ required: false,
+ default: '',
+ },
gitpodUrl: {
type: String,
required: false,
default: '',
},
+ gitpodText: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -99,6 +109,17 @@ export default {
...handleOptions,
};
},
+ webIdeActionText() {
+ if (this.webIdeText) {
+ return this.webIdeText;
+ } else if (this.isBlob) {
+ return __('Edit in Web IDE');
+ } else if (this.isFork) {
+ return __('Edit fork in Web IDE');
+ }
+
+ return __('Web IDE');
+ },
webIdeAction() {
if (!this.showWebIdeButton) {
return null;
@@ -111,17 +132,9 @@ export default {
}
: { href: this.webIdeUrl };
- let text = __('Web IDE');
-
- if (this.isBlob) {
- text = __('Edit in Web IDE');
- } else if (this.isFork) {
- text = __('Edit fork in Web IDE');
- }
-
return {
key: KEY_WEB_IDE,
- text,
+ text: this.webIdeActionText,
secondaryText: __('Quickly and easily edit multiple files in your project.'),
tooltip: '',
attrs: {
@@ -132,6 +145,9 @@ export default {
...handleOptions,
};
},
+ gitpodActionText() {
+ return this.gitpodText || __('Gitpod');
+ },
gitpodAction() {
if (!this.showGitpodButton) {
return null;
@@ -145,7 +161,7 @@ export default {
return {
key: KEY_GITPOD,
- text: __('Gitpod'),
+ text: this.gitpodActionText,
secondaryText,
tooltip: secondaryText,
attrs: {