diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared')
76 files changed, 570 insertions, 340 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue index b7544a4a5d0..c24318cb9ad 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue @@ -204,7 +204,7 @@ export default { @click="$emit('toggle-sidebar')" > <gl-icon name="user" /> - <gl-loading-icon v-if="isUpdating" /> + <gl-loading-icon v-if="isUpdating" size="sm" /> </div> <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left"> <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK"> @@ -270,12 +270,12 @@ export default { <p v-else-if="userListEmpty" class="gl-mx-5 gl-my-4"> {{ __('No Matching Results') }} </p> - <gl-loading-icon v-else /> + <gl-loading-icon v-else size="sm" /> </div> </gl-dropdown> </div> - <gl-loading-icon v-if="isUpdating" :inline="true" /> + <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" /> <div v-else-if="!isDropdownShowing" class="hide-collapsed value gl-m-0" diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue index ce90a759cee..eaa5fc5af04 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue @@ -81,7 +81,7 @@ export default { <template v-if="sidebarCollapsed"> <div ref="status" class="gl-ml-6" data-testid="status-icon" @click="$emit('toggle-sidebar')"> <gl-icon name="status" /> - <gl-loading-icon v-if="isUpdating" /> + <gl-loading-icon v-if="isUpdating" size="sm" /> </div> <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left"> <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')"> @@ -120,7 +120,7 @@ export default { @handle-updating="handleUpdating" /> - <gl-loading-icon v-if="isUpdating" :inline="true" /> + <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" /> <p v-else-if="!isDropdownShowing" class="value gl-m-0" diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index 13472b48e84..bab13fe7c75 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -68,7 +68,7 @@ export default { split @click="handleClick(selectedAction, $event)" > - <template slot="button-content"> + <template #button-content> <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs"> {{ selectedAction.text }} </span> diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index e6d9a38d1fb..f4c73d12923 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -93,12 +93,12 @@ export default { return { name, list, - title: this.getAwardListTitle(list), + title: this.getAwardListTitle(list, name), classes: this.getAwardClassBindings(list), html: glEmojiTag(name), }; }, - getAwardListTitle(awardsList) { + getAwardListTitle(awardsList, name) { if (!awardsList.length) { return ''; } @@ -128,7 +128,7 @@ export default { // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { title = sprintf( - __(`%{listToShow}, and %{awardsListLength} more.`), + __(`%{listToShow}, and %{awardsListLength} more`), { listToShow: namesToShow.join(', '), awardsListLength: remainingAwardList.length, @@ -146,7 +146,7 @@ export default { title = namesToShow.join(__(' and ')); } - return title; + return title + sprintf(__(' reacted with :%{name}:'), { name }); }, handleAward(awardName) { if (!this.canAwardEmoji) { 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 9c2ed5abf04..0c1d55ae707 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js @@ -5,7 +5,13 @@ export default { props: { content: { type: String, - required: true, + required: false, + default: null, + }, + richViewer: { + type: String, + default: '', + required: false, }, type: { type: String, 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 a8a053c0d9e..dc4d1bd56e9 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 @@ -18,5 +18,5 @@ export default { }; </script> <template> - <markdown-field-view ref="content" v-safe-html="content" /> + <markdown-field-view ref="content" v-safe-html="richViewer || content" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue index f6ab3cac536..0589b47edbd 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -9,8 +9,8 @@ export default { name: 'SimpleViewer', components: { GlIcon, - EditorLite: () => - import(/* webpackChunkName: 'EditorLite' */ '~/vue_shared/components/editor_lite.vue'), + SourceEditor: () => + import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'), }, mixins: [ViewerMixin, glFeatureFlagsMixin()], inject: ['blobHash'], @@ -53,7 +53,7 @@ export default { </script> <template> <div> - <editor-lite + <source-editor v-if="isRawContent && refactorBlobViewerEnabled" :value="content" :file-name="fileName" diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 4b53f55b856..14e99977a85 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -82,13 +82,7 @@ export default { data-qa-selector="changed_file_icon_content" :data-qa-title="tooltipTitle" > - <gl-icon - v-if="showIcon" - :name="changedIcon" - :size="size" - :class="changedIconClass" - use-deprecated-sizes - /> + <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue index 2552236a073..fb7105bd416 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue @@ -28,18 +28,23 @@ export default { <slot></slot> </p> <resizable-chart-container> - <gl-area-chart - slot-scope="{ width }" - v-bind="$attrs" - :width="width" - :height="$options.chartContainerHeight" - :data="chartData" - :include-legend-avg-max="false" - :option="areaChartOptions" - > - <slot slot="tooltip-title" name="tooltip-title"></slot> - <slot slot="tooltip-content" name="tooltip-content"></slot> - </gl-area-chart> + <template #default="{ width }"> + <gl-area-chart + v-bind="$attrs" + :width="width" + :height="$options.chartContainerHeight" + :data="chartData" + :include-legend-avg-max="false" + :option="areaChartOptions" + > + <template #tooltip-title> + <slot name="tooltip-title"></slot> + </template> + <template #tooltip-content> + <slot name="tooltip-content"></slot> + </template> + </gl-area-chart> + </template> </resizable-chart-container> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue index f4fd57e4cdc..0575d7f6404 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue @@ -46,9 +46,12 @@ export default { :area-chart-options="chartOptions" > {{ dateRange }} - - <slot slot="tooltip-title" name="tooltip-title"></slot> - <slot slot="tooltip-content" name="tooltip-content"></slot> + <template #tooltip-title> + <slot name="tooltip-title"></slot> + </template> + <template #tooltip-content> + <slot name="tooltip-content"></slot> + </template> </ci-cd-analytics-area-chart> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index dbf459cb289..07bd6019b80 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -64,12 +64,6 @@ export default { </script> <template> <span :class="cssClass"> - <gl-icon - :name="icon" - :size="size" - :class="cssClasses" - :aria-label="status.icon" - use-deprecated-sizes - /> + <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/default.vue index 4bc70870767..733accdff44 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/default.vue @@ -3,6 +3,7 @@ import Identicon from '../identicon.vue'; import ProjectAvatarImage from './image.vue'; export default { + name: 'DeprecatedProjectAvatar', components: { Identicon, ProjectAvatarImage, diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/image.vue index 269736c799c..269736c799c 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/image.vue diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue index b3edd05b0ee..b786f7752df 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue @@ -89,7 +89,7 @@ export default { <template> <div class="nothing-here-block"> - <gl-loading-icon v-if="is($options.STATE_LOADING)" /> + <gl-loading-icon v-if="is($options.STATE_LOADING)" size="sm" /> <template v-else> <gl-alert v-show="is($options.STATE_ERRORED)" diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue index 8494f99fd7d..52371e42ba1 100644 --- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue +++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue @@ -1,11 +1,14 @@ <script> -/* eslint-disable vue/no-v-html */ -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; export default { + name: 'DismissibleAlert', components: { GlAlert, }, + directives: { + SafeHtml, + }, props: { html: { type: String, @@ -28,6 +31,6 @@ export default { <template> <gl-alert v-if="!isDismissed" v-bind="$attrs" @dismiss="dismiss" v-on="$listeners"> - <div v-html="html"></div> + <div v-safe-html="html"></div> </gl-alert> </template> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue index a1c7c4dd142..a512eb687b7 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue @@ -36,7 +36,7 @@ export default { data-toggle="dropdown" aria-expanded="false" > - <gl-loading-icon v-show="isLoading" :inline="true" /> + <gl-loading-icon v-show="isLoading" size="sm" :inline="true" /> <slot v-if="$slots.default"></slot> <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span> <gl-icon diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 546ee56355f..0b92c947fc7 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -7,7 +7,7 @@ import { __ } from '~/locale'; * * @example * <expand-button> - * <template slot="expanded"> + * <template #expanded> * Text goes here. * </template> * </expand-button> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index fbadb202d51..b0c1c1531aa 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -103,6 +103,9 @@ export default { focusedIndex() { if (!this.mouseOver) { this.$nextTick(() => { + if (!this.$refs.virtualScrollList?.$el) { + return; + } const el = this.$refs.virtualScrollList.$el; const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT; const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT; @@ -218,7 +221,7 @@ export default { </script> <template> - <div class="file-finder-overlay" @mousedown.self="toggle(false)"> + <div v-if="visible" class="file-finder-overlay" @mousedown.self="toggle(false)"> <div class="dropdown-menu diff-file-changes file-finder show"> <div :class="{ 'has-value': showClearInputButton }" class="dropdown-input"> <input diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 4244cab902a..276fb35b51f 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -85,7 +85,7 @@ export default { </script> <template> <span> - <gl-loading-icon v-if="loading" :inline="true" /> + <gl-loading-icon v-if="loading" size="sm" :inline="true" /> <gl-icon v-else-if="isSymlink" name="symlink" :size="size" use-deprecated-sizes /> <svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]"> <use v-bind="{ 'xlink:href': spriteHref }" /> @@ -95,7 +95,6 @@ export default { :name="folderIconName" :size="size" class="folder-icon" - use-deprecated-sizes data-qa-selector="folder_icon_content" /> </span> 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 9775a9119c6..994ce6a762a 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 @@ -10,8 +10,11 @@ export const FILTER_CURRENT = 'Current'; export const OPERATOR_IS = '='; export const OPERATOR_IS_TEXT = __('is'); export const OPERATOR_IS_NOT = '!='; +export const OPERATOR_IS_NOT_TEXT = __('is not'); export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }]; +export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }]; +export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY]; export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) }; export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) }; 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 37436de907f..571d24b50cf 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 @@ -215,35 +215,35 @@ export function urlQueryToFilter(query = '', options = {}) { /** * Returns array of token values from localStorage - * based on provided recentTokenValuesStorageKey + * based on provided recentSuggestionsStorageKey * - * @param {String} recentTokenValuesStorageKey + * @param {String} recentSuggestionsStorageKey * @returns */ -export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) { - let recentlyUsedTokenValues = []; +export function getRecentlyUsedSuggestions(recentSuggestionsStorageKey) { + let recentlyUsedSuggestions = []; if (AccessorUtilities.isLocalStorageAccessSafe()) { - recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || []; + recentlyUsedSuggestions = JSON.parse(localStorage.getItem(recentSuggestionsStorageKey)) || []; } - return recentlyUsedTokenValues; + return recentlyUsedSuggestions; } /** * Sets provided token value to recently used array - * within localStorage for provided recentTokenValuesStorageKey + * within localStorage for provided recentSuggestionsStorageKey * - * @param {String} recentTokenValuesStorageKey + * @param {String} recentSuggestionsStorageKey * @param {Object} tokenValue */ -export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) { - const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey); +export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenValue) { + const recentlyUsedSuggestions = getRecentlyUsedSuggestions(recentSuggestionsStorageKey); - recentlyUsedTokenValues.splice(0, 0, { ...tokenValue }); + recentlyUsedSuggestions.splice(0, 0, { ...tokenValue }); if (AccessorUtilities.isLocalStorageAccessSafe()) { localStorage.setItem( - recentTokenValuesStorageKey, - JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)), + recentSuggestionsStorageKey, + JSON.stringify(uniqWith(recentlyUsedSuggestions, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)), ); } } 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 3b261f5ac25..a25a19a006c 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 @@ -74,13 +74,13 @@ export default { :config="config" :value="value" :active="active" - :tokens-list-loading="loading" - :token-values="authors" + :suggestions-loading="loading" + :suggestions="authors" :fn-active-token-value="getActiveAuthor" - :default-token-values="defaultAuthors" - :preloaded-token-values="preloadedAuthors" - :recent-token-values-storage-key="config.recentTokenValuesStorageKey" - @fetch-token-values="fetchAuthorBySearchTerm" + :default-suggestions="defaultAuthors" + :preloaded-suggestions="preloadedAuthors" + :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" + @fetch-suggestions="fetchAuthorBySearchTerm" v-on="$listeners" > <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> @@ -93,9 +93,9 @@ export default { /> <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span> </template> - <template #token-values-list="{ tokenValues }"> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="author in tokenValues" + v-for="author in suggestions" :key="author.username" :value="author.username" > 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 bda6b340871..a4804525a53 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 @@ -6,9 +6,10 @@ import { GlDropdownSectionHeader, GlLoadingIcon, } from '@gitlab/ui'; +import { debounce } from 'lodash'; import { DEBOUNCE_DELAY } from '../constants'; -import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; +import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils'; export default { components: { @@ -31,12 +32,12 @@ export default { type: Boolean, required: true, }, - tokensListLoading: { + suggestionsLoading: { type: Boolean, required: false, default: false, }, - tokenValues: { + suggestions: { type: Array, required: false, default: () => [], @@ -44,21 +45,21 @@ export default { fnActiveTokenValue: { type: Function, required: false, - default: (tokenValues, currentTokenValue) => { - return tokenValues.find(({ value }) => value === currentTokenValue); + default: (suggestions, currentTokenValue) => { + return suggestions.find(({ value }) => value === currentTokenValue); }, }, - defaultTokenValues: { + defaultSuggestions: { type: Array, required: false, default: () => [], }, - preloadedTokenValues: { + preloadedSuggestions: { type: Array, required: false, default: () => [], }, - recentTokenValuesStorageKey: { + recentSuggestionsStorageKey: { type: String, required: false, default: '', @@ -77,21 +78,21 @@ export default { data() { return { searchKey: '', - recentTokenValues: this.recentTokenValuesStorageKey - ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey) + recentSuggestions: this.recentSuggestionsStorageKey + ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey) : [], loading: false, }; }, computed: { - isRecentTokenValuesEnabled() { - return Boolean(this.recentTokenValuesStorageKey); + isRecentSuggestionsEnabled() { + return Boolean(this.recentSuggestionsStorageKey); }, recentTokenIds() { - return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]); }, preloadedTokenIds() { - return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]); }, currentTokenValue() { if (this.fnCurrentTokenValue) { @@ -100,17 +101,17 @@ export default { return this.value.data.toLowerCase(); }, activeTokenValue() { - return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue); + return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue); }, /** - * Return all the tokenValues when searchKey is present - * otherwise return only the tokenValues which aren't + * Return all the suggestions when searchKey is present + * otherwise return only the suggestions which aren't * present in "Recently used" */ - availableTokenValues() { + availableSuggestions() { return this.searchKey - ? this.tokenValues - : this.tokenValues.filter( + ? this.suggestions + : this.suggestions.filter( (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) && !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), @@ -121,30 +122,30 @@ export default { active: { immediate: true, handler(newValue) { - if (!newValue && !this.tokenValues.length) { - this.$emit('fetch-token-values', this.value.data); + if (!newValue && !this.suggestions.length) { + this.$emit('fetch-suggestions', this.value.data); } }, }, }, methods: { - handleInput({ data }) { + handleInput: debounce(function debouncedSearch({ data }) { this.searchKey = data; - setTimeout(() => { - if (!this.tokensListLoading) this.$emit('fetch-token-values', data); - }, DEBOUNCE_DELAY); - }, + if (!this.suggestionsLoading) { + this.$emit('fetch-suggestions', data); + } + }, DEBOUNCE_DELAY), handleTokenValueSelected(activeTokenValue) { // Make sure that; // 1. Recently used values feature is enabled // 2. User has actually selected a value // 3. Selected value is not part of preloaded list. if ( - this.isRecentTokenValuesEnabled && + this.isRecentSuggestionsEnabled && activeTokenValue && !this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier]) ) { - setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); + setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue); } }, }, @@ -168,9 +169,9 @@ export default { <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot> </template> <template #suggestions> - <template v-if="defaultTokenValues.length"> + <template v-if="defaultSuggestions.length"> <gl-filtered-search-suggestion - v-for="token in defaultTokenValues" + v-for="token in defaultSuggestions" :key="token.value" :value="token.value" > @@ -178,19 +179,19 @@ export default { </gl-filtered-search-suggestion> <gl-dropdown-divider /> </template> - <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey"> + <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey"> <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header> - <slot name="token-values-list" :token-values="recentTokenValues"></slot> + <slot name="suggestions-list" :suggestions="recentSuggestions"></slot> <gl-dropdown-divider /> </template> <slot - v-if="preloadedTokenValues.length && !searchKey" - name="token-values-list" - :token-values="preloadedTokenValues" + v-if="preloadedSuggestions.length && !searchKey" + name="suggestions-list" + :suggestions="preloadedSuggestions" ></slot> - <gl-loading-icon v-if="tokensListLoading" /> + <gl-loading-icon v-if="suggestionsLoading" size="sm" /> <template v-else> - <slot name="token-values-list" :token-values="availableTokenValues"></slot> + <slot name="suggestions-list" :suggestions="availableSuggestions"></slot> </template> </template> </gl-filtered-search-token> 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 694dcd95b5e..5859fd10688 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 @@ -97,7 +97,7 @@ export default { {{ branch.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultBranches.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="branch in branches" 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 9ba7f3d1a1d..d186f46866c 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 @@ -101,7 +101,7 @@ export default { {{ emoji.value }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultEmojis.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="emoji in emojis" 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 d21fa9a344a..aa234cf86d9 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 @@ -56,7 +56,7 @@ export default { } // Current value is a string. - const [groupPath, idProperty] = this.currentValue?.split('::&'); + const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator); return this.epics.find( (epic) => epic.group_full_path === groupPath && @@ -65,6 +65,9 @@ export default { } return null; }, + displayText() { + return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`; + }, }, watch: { active: { @@ -103,8 +106,10 @@ export default { this.fetchEpicsBySearchTerm({ epicPath, search: data }); }, DEBOUNCE_DELAY), - getEpicDisplayText(epic) { - return `${epic.title}${this.$options.separator}${epic.iid}`; + getValue(epic) { + return this.config.useIdValue + ? String(epic[this.idProperty]) + : `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`; }, }, }; @@ -118,7 +123,7 @@ export default { @input="searchEpics" > <template #view="{ inputValue }"> - {{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }} + {{ activeEpic ? displayText : inputValue }} </template> <template #suggestions> <gl-filtered-search-suggestion @@ -129,13 +134,9 @@ export default { {{ epic.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultEpics.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> - <gl-filtered-search-suggestion - v-for="epic in epics" - :key="epic.id" - :value="`${epic.group_full_path}::&${epic[idProperty]}`" - > + <gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)"> {{ epic.title }} </gl-filtered-search-suggestion> </template> 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 7b6a590279a..ba8b2421726 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 @@ -7,6 +7,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; @@ -30,8 +31,7 @@ export default { data() { return { iterations: this.config.initialIterations || [], - defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS, - loading: true, + loading: false, }; }, computed: { @@ -39,7 +39,12 @@ export default { return this.value.data; }, activeIteration() { - return this.iterations.find((iteration) => iteration.title === this.currentValue); + return this.iterations.find( + (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue), + ); + }, + defaultIterations() { + return this.config.defaultIterations || DEFAULT_ITERATIONS; }, }, watch: { @@ -53,6 +58,9 @@ export default { }, }, methods: { + getValue(iteration) { + return String(getIdFromGraphQLId(iteration.id)); + }, fetchIterationBySearchTerm(searchTerm) { const fetchPromise = this.config.fetchPath ? this.config.fetchIterations(this.config.fetchPath, searchTerm) @@ -95,12 +103,12 @@ export default { {{ iteration.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultIterations.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="iteration in iterations" - :key="iteration.title" - :value="iteration.title" + :key="iteration.id" + :value="getValue(iteration)" > {{ iteration.title }} </gl-filtered-search-suggestion> 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 e496d099a42..4d08f81fee9 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 @@ -96,12 +96,12 @@ export default { :config="config" :value="value" :active="active" - :tokens-list-loading="loading" - :token-values="labels" + :suggestions-loading="loading" + :suggestions="labels" :fn-active-token-value="getActiveLabel" - :default-token-values="defaultLabels" - :recent-token-values-storage-key="config.recentTokenValuesStorageKey" - @fetch-token-values="fetchLabelBySearchTerm" + :default-suggestions="defaultLabels" + :recent-suggestions-storage-key="config.recentSuggestionsStorageKey" + @fetch-suggestions="fetchLabelBySearchTerm" v-on="$listeners" > <template @@ -115,9 +115,9 @@ export default { >~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token > </template> - <template #token-values-list="{ tokenValues }"> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="label in tokenValues" + v-for="label in suggestions" :key="label.id" :value="getLabelName(label)" > 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 cda6e4d6726..66ad5ef5b4e 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 @@ -9,6 +9,7 @@ import { debounce } from 'lodash'; import createFlash from '~/flash'; import { __ } from '~/locale'; +import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; @@ -34,7 +35,7 @@ export default { return { milestones: this.config.initialMilestones || [], defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, - loading: true, + loading: false, }; }, computed: { @@ -59,11 +60,16 @@ export default { }, methods: { fetchMilestoneBySearchTerm(searchTerm = '') { + if (this.loading) { + return; + } + this.loading = true; this.config .fetchMilestones(searchTerm) - .then(({ data }) => { - this.milestones = data; + .then((response) => { + const data = Array.isArray(response) ? response : response.data; + this.milestones = data.slice().sort(sortMilestonesByDueDate); }) .catch(() => createFlash({ message: __('There was a problem fetching milestones.') })) .finally(() => { @@ -96,7 +102,7 @@ export default { {{ milestone.text }} </gl-filtered-search-suggestion> <gl-dropdown-divider v-if="defaultMilestones.length" /> - <gl-loading-icon v-if="loading" /> + <gl-loading-icon v-if="loading" size="sm" /> <template v-else> <gl-filtered-search-suggestion v-for="milestone in milestones" diff --git a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue index 74f988476e3..26c50345c19 100644 --- a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue +++ b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue @@ -1,3 +1,4 @@ +<!-- eslint-disable-next-line vue/no-deprecated-functional-template --> <template functional> <footer class="form-actions d-flex justify-content-between"> <div><slot name="prepend"></slot></div> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue index 96d99faa952..dd0c0358ef6 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue @@ -74,6 +74,8 @@ export default { @hidden="syncHide" > <slot></slot> - <slot slot="modal-footer" name="modal-footer" :ok="ok" :cancel="cancel"></slot> + <template #modal-footer> + <slot name="modal-footer" :ok="ok" :cancel="cancel"></slot> + </template> </gl-modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 80b7a9b7d05..9ea48050079 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -4,7 +4,7 @@ import { GlIcon } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { unescape } from 'lodash'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import GLForm from '~/gl_form'; import axios from '~/lib/utils/axios_utils'; import { stripHtml } from '~/lib/utils/text_utility'; @@ -222,7 +222,11 @@ export default { axios .post(this.markdownPreviewPath, { text: this.textareaValue }) .then((response) => this.renderMarkdown(response.data)) - .catch(() => new Flash(__('Error loading markdown preview'))); + .catch(() => + createFlash({ + message: __('Error loading markdown preview'), + }), + ); } else { this.renderMarkdown(); } @@ -245,7 +249,11 @@ export default { this.$nextTick() .then(() => $(this.$refs['markdown-preview']).renderGFM()) - .catch(() => new Flash(__('Error rendering markdown preview'))); + .catch(() => + createFlash({ + message: __('Error rendering markdown preview'), + }), + ); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 83b8a6ae562..065d9b1b5dd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import ApplySuggestion from './apply_suggestion.vue'; @@ -73,7 +74,7 @@ export default { return __('Applying suggestions...'); }, isLoggedIn() { - return Boolean(gon.current_user_id); + return isLoggedIn(); }, }, methods: { @@ -110,7 +111,7 @@ export default { </div> <div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div> <div v-else-if="isApplying" class="d-flex align-items-center text-secondary"> - <gl-loading-icon class="d-flex-center mr-2" /> + <gl-loading-icon size="sm" class="d-flex-center mr-2" /> <span>{{ applyingSuggestionsMessage }}</span> </div> <div v-else-if="canApply && isBatched" class="d-flex align-items-center"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue index 9059f0d2a8b..a04f8616acb 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue @@ -1,7 +1,11 @@ <script> -/* eslint-disable vue/no-v-html */ +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; + export default { name: 'SuggestionDiffRow', + directives: { + SafeHtml, + }, props: { line: { type: Object, @@ -32,7 +36,7 @@ export default { :class="[{ 'd-table-cell': displayAsCell }, lineType]" data-testid="suggestion-diff-content" > - <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span> + <span v-if="line.rich_text" v-safe-html="line.rich_text" class="line"></span> <span v-else-if="line.text" class="line">{{ line.text }}</span> <span v-else class="line"></span> </td> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 53d1cca7af3..63774c6c498 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,7 +1,7 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import Vue from 'vue'; -import { deprecatedCreateFlash as Flash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; @@ -79,7 +79,10 @@ export default { const suggestionElements = container.querySelectorAll('.js-render-suggestion'); if (this.lineType === 'old') { - Flash(__('Unable to apply suggestions to a deleted line.'), 'alert', this.$el); + createFlash({ + message: __('Unable to apply suggestions to a deleted line.'), + parent: this.$el, + }); } suggestionElements.forEach((suggestionEl, i) => { diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 7393a8791b7..7112295fa57 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -82,7 +82,7 @@ export default { <span class="attaching-file-message"></span> <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> <span class="uploading-progress">0%</span> - <gl-loading-icon inline /> + <gl-loading-icon size="sm" inline /> </span> <span class="uploading-error-container hide"> <span class="uploading-error-icon"> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 69afd711797..d6501a37a35 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -16,12 +16,15 @@ * :note="{body: 'This is a note'}" * /> */ +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { mapGetters } from 'vuex'; +import { renderMarkdown } from '~/notes/utils'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { name: 'PlaceholderNote', + directives: { SafeHtml }, components: { userAvatarLink, TimelineEntryItem, @@ -34,6 +37,9 @@ export default { }, computed: { ...mapGetters(['getUserData']), + renderedNote() { + return renderMarkdown(this.note.body); + }, }, }; </script> @@ -57,9 +63,7 @@ export default { </div> </div> <div class="note-body"> - <div class="note-text md"> - <p>{{ note.body }}</p> - </div> + <div v-safe-html="renderedNote" class="note-text md"></div> </div> </div> </timeline-entry-item> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 149909d263e..c3d861d74bc 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -111,7 +111,7 @@ export default { <div class="note-header"> <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> <span v-safe-html="actionTextHtml"></span> - <template v-if="canSeeDescriptionVersion" slot="extra-controls"> + <template v-if="canSeeDescriptionVersion" #extra-controls> · <gl-button variant="link" diff --git a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue index ff2847624c5..e37a663ace3 100644 --- a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue +++ b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue @@ -27,9 +27,13 @@ export default { title() { return this.isCurrentUser ? s__('OnCallSchedules|You are currently a part of:') - : sprintf(s__('OnCallSchedules|User %{name} is currently part of:'), { - name: this.userName, - }); + : sprintf( + s__('OnCallSchedules|User %{name} is currently part of:'), + { + name: this.userName, + }, + false, + ); }, footer() { return this.isCurrentUser 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 d05e45e90b3..79a9e1fca8c 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 @@ -169,6 +169,12 @@ export default { methods: { filterItemsByStatus(tabIndex) { this.resetPagination(); + const activeStatusTab = this.statusTabs[tabIndex]; + + if (activeStatusTab == null) { + return; + } + const { filters, status } = this.statusTabs[tabIndex]; this.statusFilter = filters; this.filteredByStatus = status; diff --git a/app/assets/javascripts/vue_shared/components/project_avatar.stories.js b/app/assets/javascripts/vue_shared/components/project_avatar.stories.js new file mode 100644 index 00000000000..110c6c73bad --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/project_avatar.stories.js @@ -0,0 +1,30 @@ +import ProjectAvatar from './project_avatar.vue'; + +export default { + component: ProjectAvatar, + title: 'vue_shared/components/project_avatar', +}; + +const Template = (args, { argTypes }) => ({ + components: { ProjectAvatar }, + props: Object.keys(argTypes), + template: '<project-avatar v-bind="$props" />', +}); + +export const Default = Template.bind({}); +Default.args = { + projectAvatarUrl: + 'https://gitlab.com/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png?width=64', + projectName: 'GitLab', +}; + +export const FallbackAvatar = Template.bind({}); +FallbackAvatar.args = { + projectName: 'GitLab', +}; + +export const EmptyAltTag = Template.bind({}); +EmptyAltTag.args = { + ...Default.args, + alt: '', +}; diff --git a/app/assets/javascripts/vue_shared/components/project_avatar.vue b/app/assets/javascripts/vue_shared/components/project_avatar.vue new file mode 100644 index 00000000000..f16187022a5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/project_avatar.vue @@ -0,0 +1,45 @@ +<script> +import { GlAvatar } from '@gitlab/ui'; + +export default { + components: { + GlAvatar, + }, + props: { + projectName: { + type: String, + required: true, + }, + projectAvatarUrl: { + type: String, + required: false, + default: '', + }, + size: { + type: Number, + default: 32, + required: false, + }, + alt: { + type: String, + required: false, + default: undefined, + }, + }, + computed: { + avatarAlt() { + return this.alt ?? this.projectName; + }, + }, +}; +</script> + +<template> + <gl-avatar + shape="rect" + :entity-name="projectName" + :src="projectAvatarUrl" + :alt="avatarAlt" + :size="size" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue index ddc8bbf9b27..69f43c9e464 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue @@ -4,7 +4,7 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { isString } from 'lodash'; import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; -import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; +import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; export default { name: 'ProjectListItem', diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue index 580e1668f41..d55c93fd146 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -194,7 +194,7 @@ export default { <template v-if="selectedPlatform"> <h5> {{ $options.i18n.architecture }} - <gl-loading-icon v-if="$apollo.loading" inline /> + <gl-loading-icon v-if="$apollo.loading" size="sm" inline /> </h5> <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName"> diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue deleted file mode 100644 index bb1a8fae7b0..00000000000 --- a/app/assets/javascripts/vue_shared/components/select2_select.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import $ from 'jquery'; -import 'select2'; -import { loadCSSFile } from '~/lib/utils/css_utils'; - -export default { - // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 - // eslint-disable-next-line @gitlab/require-i18n-strings - name: 'Select2Select', - props: { - options: { - type: Object, - required: false, - default: () => ({}), - }, - value: { - type: String, - required: false, - default: '', - }, - }, - - watch: { - value() { - $(this.$refs.dropdownInput).val(this.value).trigger('change'); - }, - }, - - mounted() { - loadCSSFile(gon.select2_css_path) - .then(() => { - $(this.$refs.dropdownInput) - .val(this.value) - .select2(this.options) - .on('change', (event) => this.$emit('input', event.target.value)); - }) - .catch(() => {}); - }, - - beforeDestroy() { - $(this.$refs.dropdownInput).select2('destroy'); - }, -}; -</script> - -<template> - <input ref="dropdownInput" type="hidden" /> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue index bbc7e6e7a6e..5c3a6852219 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue @@ -1,5 +1,5 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { s__, __, sprintf } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -10,8 +10,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; export default { name: 'CopyableField', components: { - GlLoadingIcon, ClipboardButton, + GlLoadingIcon, + GlSprintf, }, props: { value: { @@ -48,12 +49,6 @@ export default { loadingIconLabel() { return sprintf(this.$options.i18n.loadingIconLabel, { name: this.name }); }, - templateText() { - return sprintf(this.$options.i18n.templateText, { - name: this.name, - value: this.value, - }); - }, }, i18n: { loadingIconLabel: __('Loading %{name}'), @@ -78,10 +73,13 @@ export default { class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap" :title="value" > - {{ templateText }} + <gl-sprintf :message="$options.i18n.templateText"> + <template #name>{{ name }}</template> + <template #value>{{ value }}</template> + </gl-sprintf> </span> - <gl-loading-icon v-if="isLoading" inline :label="loadingIconLabel" /> + <gl-loading-icon v-if="isLoading" size="sm" inline :label="loadingIconLabel" /> <clipboard-button v-else size="small" v-bind="clipboardProps" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 075681de320..4531fafbf72 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -104,7 +104,7 @@ export default { <collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" /> <div class="title"> {{ label }} - <gl-loading-icon v-if="isLoading" :inline="true" /> + <gl-loading-icon v-if="isLoading" size="sm" :inline="true" /> <div class="float-right"> <button v-if="editable && !editing" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index 320e2048f1c..12daaea8758 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -148,7 +148,7 @@ export default { @hide="handleDropdownHide" > <template #button-content - ><gl-loading-icon v-if="moveInProgress" class="gl-mr-3" />{{ + ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{ dropdownButtonTitle }}</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 index f8cc981ba3d..3ec33a653b8 100644 --- 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 @@ -108,7 +108,7 @@ export default { class="float-left d-flex align-items-center" @click="handleCreateClick" > - <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" /> + <gl-loading-icon v-show="labelCreateInProgress" size="sm" :inline="true" class="mr-1" /> {{ __('Create') }} </gl-button> <gl-button class="float-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView"> 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 86788a84260..9914bfc6026 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 @@ -48,6 +48,12 @@ export default { } return this.labels; }, + showDropdownFooter() { + return ( + (this.isDropdownVariantSidebar || this.isDropdownVariantEmbedded) && + (this.allowLabelCreate || this.labelsManagePath) + ); + }, showNoMatchingResultsMessage() { return Boolean(this.searchKey) && this.visibleLabels.length === 0; }, @@ -192,11 +198,7 @@ export default { </li> </ul> </div> - <div - v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" - class="dropdown-footer" - data-testid="dropdown-footer" - > + <div v-if="showDropdownFooter" class="dropdown-footer" data-testid="dropdown-footer"> <ul class="list-unstyled"> <li v-if="allowLabelCreate"> <gl-link @@ -206,7 +208,7 @@ export default { {{ footerCreateLabelTitle }} </gl-link> </li> - <li> + <li v-if="labelsManagePath"> <gl-link :href="labelsManagePath" class="gl-display-flex flex-row text-break-word label-item" 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 813de528c0b..aad754e15b0 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 @@ -26,7 +26,7 @@ export default { <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> {{ __('Labels') }} <template v-if="allowLabelEdit"> - <gl-loading-icon v-show="labelsSelectInProgress" inline /> + <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" 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 89f96ab916b..178be0f6da0 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 @@ -1,4 +1,4 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -16,7 +16,9 @@ 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.')); + createFlash({ + message: __('Error fetching labels.'), + }); }; export const fetchLabels = ({ state, dispatch }) => { dispatch('requestLabels'); @@ -32,7 +34,9 @@ export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LA export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS); export const receiveCreateLabelFailure = ({ commit }) => { commit(types.RECEIVE_CREATE_LABEL_FAILURE); - flash(__('Error creating label.')); + createFlash({ + message: __('Error creating label.'), + }); }; export const createLabel = ({ state, dispatch }, label) => { dispatch('requestCreateLabel'); 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 55716e1105e..2e0a57f15dd 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 @@ -1,3 +1,4 @@ +import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; import { DropdownVariant } from '../constants'; import * as types from './mutation_types'; @@ -66,5 +67,16 @@ export default { candidateLabel.touched = true; candidateLabel.set = !candidateLabel.set; } + + if (isScopedLabel(candidateLabel)) { + const scopedBase = scopedLabelKey(candidateLabel); + const currentActiveScopedLabel = state.labels.find(({ title }) => { + return title.startsWith(scopedBase) && title !== '' && title !== candidateLabel.title; + }); + + if (currentActiveScopedLabel) { + currentActiveScopedLabel.set = false; + } + } }, }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index a7f20fbe851..4651e7a1576 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -117,7 +117,7 @@ export default { data-testid="create-button" @click="createLabel" > - <gl-loading-icon v-if="labelCreateInProgress" :inline="true" class="mr-1" /> + <gl-loading-icon v-if="labelCreateInProgress" size="sm" :inline="true" class="mr-1" /> {{ __('Create') }} </gl-button> <gl-button 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 5d1663bc1fd..b6d14965cfa 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 @@ -26,7 +26,7 @@ export default { <div class="title hide-collapsed gl-mb-3"> {{ __('Labels') }} <template v-if="allowLabelEdit"> - <gl-loading-icon v-show="labelsSelectInProgress" inline /> + <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline /> <gl-button variant="link" class="float-right js-sidebar-dropdown-toggle" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 46ccb9470e5..58a940bca3b 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -1,7 +1,6 @@ <script> import { GlLabel } from '@gitlab/ui'; -import { mapState } from 'vuex'; - +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { @@ -14,15 +13,26 @@ export default { required: false, default: false, }, - }, - computed: { - ...mapState([ - 'selectedLabels', - 'allowLabelRemove', - 'allowScopedLabels', - 'labelsFilterBasePath', - 'labelsFilterParam', - ]), + selectedLabels: { + type: Array, + required: true, + }, + allowLabelRemove: { + type: Boolean, + required: true, + }, + allowScopedLabels: { + type: Boolean, + required: true, + }, + labelsFilterBasePath: { + type: String, + required: true, + }, + labelsFilterParam: { + type: String, + required: true, + }, }, methods: { labelFilterUrl(label) { @@ -33,6 +43,9 @@ export default { scopedLabel(label) { return this.allowScopedLabels && isScopedLabel(label); }, + removeLabel(labelId) { + this.$emit('onLabelRemove', getIdFromGraphQLId(labelId)); + }, }, }; </script> @@ -43,12 +56,14 @@ export default { 'has-labels': selectedLabels.length, }" class="hide-collapsed value issuable-show-labels js-value" + data-testid="value-wrapper" > - <span v-if="!selectedLabels.length" class="text-secondary"> + <span v-if="!selectedLabels.length" class="text-secondary" data-testid="empty-placeholder"> <slot></slot> </span> - <template v-for="label in selectedLabels" v-else> + <template v-else> <gl-label + v-for="label in selectedLabels" :key="label.id" data-qa-selector="selected_label_content" :data-qa-label-name="label.title" @@ -60,7 +75,7 @@ export default { :show-close-button="allowLabelRemove" :disabled="disableLabels" tooltip-placement="top" - @close="$emit('onLabelRemove', label.id)" + @close="removeLabel(label.id)" /> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql new file mode 100644 index 00000000000..1c2fd3bb7c0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql @@ -0,0 +1,15 @@ +query issueLabels($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + issuable: issue(iid: $iid) { + id + labels { + 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 7728c758e18..87f36a5bb72 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 @@ -11,6 +11,7 @@ import DropdownContents from './dropdown_contents.vue'; import DropdownTitle from './dropdown_title.vue'; import DropdownValue from './dropdown_value.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; +import issueLabelsQuery from './graphql/issue_labels.query.graphql'; import labelsSelectModule from './store'; Vue.use(Vuex); @@ -24,6 +25,7 @@ export default { DropdownContents, DropdownValueCollapsed, }, + inject: ['iid', 'projectPath'], props: { allowLabelRemove: { type: Boolean, @@ -119,8 +121,23 @@ export default { data() { return { contentIsOnViewport: true, + issueLabels: [], }; }, + apollo: { + issueLabels: { + query: issueLabelsQuery, + variables() { + return { + iid: this.iid, + fullPath: this.projectPath, + }; + }, + update(data) { + return data.workspace?.issuable?.labels.nodes || []; + }, + }, + }, computed: { ...mapState(['showDropdownButton', 'showDropdownContents']), ...mapGetters([ @@ -293,7 +310,7 @@ export default { <template v-if="isDropdownVariantSidebar"> <dropdown-value-collapsed ref="dropdownButtonCollapsed" - :labels="selectedLabels" + :labels="issueLabels" @onValueClick="handleCollapsedValueClick" /> <dropdown-title @@ -302,6 +319,11 @@ export default { /> <dropdown-value :disable-labels="labelsSelectInProgress" + :selected-labels="issueLabels" + :allow-label-remove="allowLabelRemove" + :allow-scoped-labels="allowScopedLabels" + :labels-filter-base-path="labelsFilterBasePath" + :labels-filter-param="labelsFilterParam" @onLabelRemove="$emit('onLabelRemove', $event)" > <slot></slot> 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 2b96b159ca3..935f020f559 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,4 +1,4 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -16,7 +16,9 @@ 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.')); + createFlash({ + message: __('Error fetching labels.'), + }); }; export const fetchLabels = ({ state, dispatch }) => { dispatch('requestLabels'); 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 131c6e6fb57..1c03d95f37b 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 @@ -1,3 +1,4 @@ +import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; import { DropdownVariant } from '../constants'; import * as types from './mutation_types'; @@ -55,5 +56,16 @@ export default { candidateLabel.touched = true; candidateLabel.set = !candidateLabel.set; } + + if (isScopedLabel(candidateLabel)) { + const scopedBase = scopedLabelKey(candidateLabel); + const currentActiveScopedLabel = state.labels.find( + ({ title }) => title.indexOf(scopedBase) === 0 && title !== candidateLabel.title, + ); + + if (currentActiveScopedLabel) { + currentActiveScopedLabel.set = false; + } + } }, }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js new file mode 100644 index 00000000000..d2afc02233e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js @@ -0,0 +1,23 @@ +/* eslint-disable @gitlab/require-i18n-strings */ + +import TodoButton from './todo_button.vue'; + +export default { + component: TodoButton, + title: 'vue_shared/components/todo_toggle/todo_button', +}; + +const Template = (args, { argTypes }) => ({ + components: { TodoButton }, + props: Object.keys(argTypes), + template: '<todo-button v-bind="$props" v-on="$props" />', +}); + +export const Default = Template.bind({}); +Default.argTypes = { + isTodo: { + description: 'True if to-do is unresolved (i.e. not "done")', + control: { type: 'boolean' }, + }, + click: { action: 'clicked' }, +}; 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 new file mode 100644 index 00000000000..e6229cf0a93 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue @@ -0,0 +1,56 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { todoLabel } from './utils'; + +export default { + components: { + GlButton, + }, + props: { + isTodo: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + buttonLabel() { + return todoLabel(this.isTodo); + }, + }, + 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); + }, + decrementGlobalTodoCount() { + this.updateGlobalTodoCount(-1); + }, + onToggle(event) { + if (this.isTodo) { + this.decrementGlobalTodoCount(); + } else { + this.incrementGlobalTodoCount(); + } + this.$emit('click', event); + }, + }, +}; +</script> + +<template> + <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)"> + {{ buttonLabel }} + </gl-button> +</template> 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 new file mode 100644 index 00000000000..59e72a2ffe3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js @@ -0,0 +1,5 @@ +import { __ } from '~/locale'; + +export const todoLabel = (hasTodo) => { + return hasTodo ? __('Mark as done') : __('Add a to do'); +}; diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue index c3bddabea21..fdf0c9baee3 100644 --- a/app/assets/javascripts/vue_shared/components/editor_lite.vue +++ b/app/assets/javascripts/vue_shared/components/source_editor.vue @@ -1,9 +1,9 @@ <script> import { debounce } from 'lodash'; import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants'; -import Editor from '~/editor/editor_lite'; +import Editor from '~/editor/source_editor'; -function initEditorLite({ el, ...args }) { +function initSourceEditor({ el, ...args }) { const editor = new Editor({ scrollbar: { alwaysConsumeMouseWheel: false, @@ -64,7 +64,7 @@ export default { }, }, mounted() { - this.editor = initEditorLite({ + this.editor = initSourceEditor({ el: this.$refs.editor, blobPath: this.fileName, blobContent: this.value, @@ -93,7 +93,7 @@ export default { </script> <template> <div - :id="`editor-lite-${fileGlobalId}`" + :id="`source-editor-${fileGlobalId}`" ref="editor" data-editor-loading @[$options.readyEvent]="$emit($options.readyEvent)" diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue deleted file mode 100644 index 935d222a1a9..00000000000 --- a/app/assets/javascripts/vue_shared/components/todo_button.vue +++ /dev/null @@ -1,28 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default { - components: { - GlButton, - }, - props: { - isTodo: { - type: Boolean, - required: false, - default: true, - }, - }, - computed: { - buttonLabel() { - return this.isTodo ? __('Mark as done') : __('Add a to do'); - }, - }, -}; -</script> - -<template> - <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)"> - {{ buttonLabel }} - </gl-button> -</template> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index deac24d2270..f387f8ca128 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -72,7 +72,11 @@ export default { <template v-else> <div class="gl-mb-3"> <h5 class="gl-m-0"> - <user-name-with-status :name="user.name" :availability="availabilityStatus" /> + <user-name-with-status + :name="user.name" + :availability="availabilityStatus" + :pronouns="user.pronouns" + /> </h5> <span class="gl-text-gray-500">@{{ user.username }}</span> </div> diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue index 04e44aa2ed1..b85cae0c64f 100644 --- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue +++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue @@ -96,9 +96,6 @@ export default { }, }, searchUsers: { - // TODO Remove error policy - // https://gitlab.com/gitlab-org/gitlab/-/issues/329750 - errorPolicy: 'all', query: searchUsers, variables() { return { @@ -111,28 +108,10 @@ export default { return !this.isEditing; }, update(data) { - // TODO Remove null filter (BE fix required) - // https://gitlab.com/gitlab-org/gitlab/-/issues/329750 return data.workspace?.users?.nodes.filter((x) => x?.user).map(({ user }) => user) || []; }, debounce: ASSIGNEES_DEBOUNCE_DELAY, - error({ graphQLErrors }) { - // TODO This error suppression is temporary (BE fix required) - // https://gitlab.com/gitlab-org/gitlab/-/issues/329750 - const isNullError = ({ message }) => { - return message === 'Cannot return null for non-nullable field GroupMember.user'; - }; - - if (graphQLErrors?.length > 0 && graphQLErrors.every(isNullError)) { - // only null-related errors exist, suppress them. - // eslint-disable-next-line no-console - console.error( - "Suppressing the error 'Cannot return null for non-nullable field GroupMember.user'. Please see https://gitlab.com/gitlab-org/gitlab/-/issues/329750", - ); - this.isSearching = false; - return; - } - + error() { this.$emit('error'); this.isSearching = false; }, 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 4bd3e352fd2..5ba7c107c12 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -93,9 +93,8 @@ export default { tooltip: '', attrs: { 'data-qa-selector': 'edit_button', - 'data-track-event': 'click_edit', - // eslint-disable-next-line @gitlab/require-i18n-strings - 'data-track-label': 'Edit', + 'data-track-action': 'click_consolidated_edit', + 'data-track-label': 'edit', }, ...handleOptions, }; @@ -127,9 +126,8 @@ export default { tooltip: '', attrs: { 'data-qa-selector': 'web_ide_button', - 'data-track-event': 'click_edit_ide', - // eslint-disable-next-line @gitlab/require-i18n-strings - 'data-track-label': 'Web IDE', + 'data-track-action': 'click_consolidated_edit_ide', + 'data-track-label': 'web_ide', }, ...handleOptions, }; diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue index e9983af5401..1b20ae57563 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue @@ -16,14 +16,9 @@ export default { type: Array, required: true, }, - experiment: { - type: String, - required: false, - default: null, - }, }, created() { - const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment }); + const trackingMixin = Tracking.mixin(); const trackingInstance = new Vue({ ...trackingMixin, render() { @@ -35,7 +30,7 @@ export default { }; </script> <template> - <div class="container"> + <div class="container gl-display-flex gl-flex-direction-column"> <h2 class="gl-my-7 gl-font-size-h1 gl-text-center"> {{ title }} </h2> @@ -43,11 +38,12 @@ export default { <div v-for="panel in panels" :key="panel.name" - class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5" + class="new-namespace-panel-wrapper gl-display-inline-block gl-float-left gl-px-3 gl-mb-5" > <a :href="`#${panel.name}`" - :data-qa-selector="`${panel.name}_link`" + data-qa-selector="panel_link" + :data-qa-panel-name="panel.name" class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!" @click="track('click_tab', { label: panel.name })" > diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue index a2b432d11f4..c1e8376d656 100644 --- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -36,11 +36,6 @@ export default { type: String, required: true, }, - experiment: { - type: String, - required: false, - default: null, - }, }, data() { @@ -103,12 +98,7 @@ export default { </script> <template> - <welcome-page - v-if="activePanelName === null" - :panels="panels" - :title="title" - :experiment="experiment" - > + <welcome-page v-if="!activePanelName" :panels="panels" :title="title"> <template #footer> <slot name="welcome-footer"> </slot> </template> diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js index bfea2bedd40..fb52b31c2c8 100644 --- a/app/assets/javascripts/vue_shared/plugins/global_toast.js +++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js @@ -2,7 +2,7 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; Vue.use(GlToast); -const instance = new Vue(); +export const instance = new Vue(); export default function showGlobalToast(...args) { return instance.$toast.show(...args); diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue index 12e5f634a08..0ff858e6afc 100644 --- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue +++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue @@ -5,6 +5,10 @@ import { redirectTo } from '~/lib/utils/url_utility'; import { sprintf, s__ } from '~/locale'; import apolloProvider from '../provider'; +function mutationSettingsForFeatureType(type) { + return featureToMutationMap[type]; +} + export default { apolloProvider, components: { @@ -19,7 +23,7 @@ export default { variant: { type: String, required: false, - default: 'success', + default: 'confirm', }, category: { type: String, @@ -33,17 +37,19 @@ export default { }; }, computed: { - featureSettings() { - return featureToMutationMap[this.feature.type]; + mutationSettings() { + return mutationSettingsForFeatureType(this.feature.type); }, }, methods: { async mutate() { this.isLoading = true; try { - const mutation = this.featureSettings; - const { data } = await this.$apollo.mutate(mutation.getMutationPayload(this.projectPath)); - const { errors, successPath } = data[mutation.mutationId]; + const { mutationSettings } = this; + const { data } = await this.$apollo.mutate( + mutationSettings.getMutationPayload(this.projectPath), + ); + const { errors, successPath } = data[mutationSettings.mutationId]; if (errors.length > 0) { throw new Error(errors[0]); @@ -62,6 +68,22 @@ export default { } }, }, + /** + * Returns a boolean representing whether this component can be rendered for + * the given feature. Useful for parent components to determine whether or + * not to render this component. + * @param {Object} feature The feature to check. + * @returns {boolean} + */ + canRender(feature) { + const { available, configured, canEnableByMergeRequest, type } = feature; + return ( + canEnableByMergeRequest && + available && + !configured && + Boolean(mutationSettingsForFeatureType(type)) + ); + }, i18n: { buttonLabel: s__('SecurityConfiguration|Configure via Merge Request'), noSuccessPathError: s__( @@ -74,6 +96,7 @@ export default { <template> <gl-button v-if="!feature.configured" + data-testid="configure-via-mr-button" :loading="isLoading" :variant="variant" :category="category" diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue index 8fdc5ca78db..f3dd26b02cb 100644 --- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue +++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue @@ -76,6 +76,7 @@ export default { <template> <security-report-download-dropdown + :title="s__('SecurityReports|Download results')" :artifacts="reportArtifacts" :loading="isLoadingReportArtifacts" /> diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue index 5d39d740c07..4178c5d1170 100644 --- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue +++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue @@ -21,6 +21,16 @@ export default { required: false, default: false, }, + text: { + type: String, + required: false, + default: '', + }, + title: { + type: String, + required: false, + default: '', + }, }, methods: { artifactText({ name }) { @@ -35,7 +45,8 @@ export default { <template> <gl-dropdown v-gl-tooltip - :text="s__('SecurityReports|Download results')" + :text="text" + :title="title" :loading="loading" icon="download" size="small" diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index 1cdcf87097f..4a50dfbd82f 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -22,6 +22,7 @@ export const REPORT_TYPE_DAST_PROFILES = 'dast_profiles'; export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection'; export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning'; export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning'; +export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning'; export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index d7a3d4e611e..3e0310e173e 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -200,6 +200,7 @@ export default { <template #action-buttons> <security-report-download-dropdown + :text="s__('SecurityReports|Download results')" :artifacts="reportArtifacts" :loading="isLoadingReportArtifacts" /> @@ -228,6 +229,7 @@ export default { <template #action-buttons> <security-report-download-dropdown + :text="s__('SecurityReports|Download results')" :artifacts="reportArtifacts" :loading="isLoadingReportArtifacts" /> |