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-10-20 11:43:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 11:43:02 +0300
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/vue_shared/components
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue81
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue138
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue87
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue137
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue (renamed from app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue)37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue10
37 files changed, 779 insertions, 318 deletions
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 0c1d55ae707..4cab5e964de 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
+ hideLineNumbers: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
mounted() {
eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT);
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 84770dbac6f..40044e518c3 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
@@ -8,8 +8,6 @@ export default {
name: 'SimpleViewer',
components: {
GlIcon,
- SourceEditor: () =>
- import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
},
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
@@ -22,9 +20,6 @@ export default {
lineNumbers() {
return this.content.split('\n').length;
},
- refactorBlobViewerEnabled() {
- return this.glFeatures.refactorBlobViewer;
- },
},
mounted() {
const { hash } = window.location;
@@ -52,14 +47,8 @@ export default {
</script>
<template>
<div>
- <source-editor
- v-if="isRawContent && refactorBlobViewerEnabled"
- :value="content"
- :file-name="fileName"
- :editor-options="{ readOnly: true }"
- />
- <div v-else class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
- <div class="line-numbers">
+ <div class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
+ <div v-if="!hideLineNumbers" class="line-numbers">
<a
v-for="line in lineNumbers"
:id="`L${line}`"
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 3c21b14894b..7563c35dfc8 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -81,8 +81,8 @@ export default {
},
},
i18n: {
- fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
- shortDescription: __('Choose any color'),
+ fullDescription: __('Enter any color or choose one of the suggested colors below.'),
+ shortDescription: __('Enter any color.'),
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 7b88b36aa0f..ea507017caa 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -97,7 +97,7 @@ export default {
});
})
.catch(() => {
- this.previewContent = __('An error occurred while fetching markdown preview');
+ this.previewContent = __('An error occurred while fetching Markdown preview');
this.isLoading = false;
});
}
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
index c4dfcf93a18..014276c7e36 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
@@ -1,13 +1,11 @@
<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import { slugifyWithUnderscore } from '~/lib/utils/text_utility';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
GlAlert,
- GlSprintf,
- GlLink,
LocalStorageSync,
},
props: {
@@ -15,10 +13,6 @@ export default {
type: String,
required: true,
},
- feedbackLink: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -44,19 +38,8 @@ export default {
<template>
<div v-show="showAlert">
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json />
- <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissFeedbackAlert">
- <gl-sprintf
- :message="
- __(
- 'Please share your feedback about %{featureName} %{linkStart}in this issue%{linkEnd} to help us improve the experience.',
- )
- "
- >
- <template #featureName>{{ featureName }}</template>
- <template #link="{ content }">
- <gl-link :href="feedbackLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
+ <slot></slot>
</gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue b/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue
new file mode 100644
index 00000000000..5d0ed8b0821
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue
@@ -0,0 +1,81 @@
+<script>
+import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes';
+
+export default {
+ model: {
+ prop: 'index',
+ event: 'change',
+ },
+ props: {
+ /* v-model property to manage location in list */
+ index: {
+ type: Number,
+ required: true,
+ },
+ /* Highest index that can be navigated to */
+ max: {
+ type: Number,
+ required: true,
+ },
+ /* Lowest index that can be navigated to */
+ min: {
+ type: Number,
+ required: true,
+ },
+ /* Which index to set v-model to on init */
+ defaultIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ watch: {
+ max() {
+ // If the max index (list length) changes, reset the index
+ this.$emit('change', this.defaultIndex);
+ },
+ },
+ created() {
+ this.$emit('change', this.defaultIndex);
+ document.addEventListener('keydown', this.handleKeydown);
+ },
+ beforeDestroy() {
+ document.removeEventListener('keydown', this.handleKeydown);
+ },
+ methods: {
+ handleKeydown(event) {
+ if (event.keyCode === DOWN_KEY_CODE) {
+ // Prevents moving scrollbar
+ event.preventDefault();
+ event.stopPropagation();
+ // Moves to next index
+ this.increment(1);
+ } else if (event.keyCode === UP_KEY_CODE) {
+ // Prevents moving scrollbar
+ event.preventDefault();
+ event.stopPropagation();
+ // Moves to previous index
+ this.increment(-1);
+ } else if (event.keyCode === TAB_KEY_CODE) {
+ this.$emit('tab');
+ }
+ },
+ increment(val) {
+ if (this.max === 0) {
+ return;
+ }
+
+ const nextIndex = Math.max(this.min, Math.min(this.index + val, this.max));
+
+ // Return if the index didn't change
+ if (nextIndex === this.index) {
+ return;
+ }
+
+ this.$emit('change', nextIndex);
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
new file mode 100644
index 00000000000..9e9bda8ad3e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/epic.fragment.graphql
@@ -0,0 +1,15 @@
+fragment EpicNode on Epic {
+ id
+ iid
+ group {
+ fullPath
+ }
+ title
+ state
+ reference
+ referencePath: reference(full: true)
+ webPath
+ webUrl
+ createdAt
+ closedAt
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
new file mode 100644
index 00000000000..4bb4b586fc9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql
@@ -0,0 +1,16 @@
+#import "./epic.fragment.graphql"
+
+query searchEpics($fullPath: ID!, $search: String, $state: EpicState) {
+ group(fullPath: $fullPath) {
+ epics(
+ search: $search
+ state: $state
+ includeAncestorGroups: true
+ includeDescendantGroups: false
+ ) {
+ nodes {
+ ...EpicNode
+ }
+ }
+ }
+}
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 d1326e96794..cee7c40aa83 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
@@ -67,6 +67,11 @@ export default {
required: false,
default: 'id',
},
+ searchBy: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -112,16 +117,18 @@ export default {
);
},
showDefaultSuggestions() {
- return this.availableDefaultSuggestions.length;
+ return this.availableDefaultSuggestions.length > 0;
},
showRecentSuggestions() {
- return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
+ return (
+ this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
+ );
},
showPreloadedSuggestions() {
- return this.preloadedSuggestions.length && !this.searchKey;
+ return this.preloadedSuggestions.length > 0 && !this.searchKey;
},
showAvailableSuggestions() {
- return this.availableSuggestions.length;
+ return this.availableSuggestions.length > 0;
},
showSuggestions() {
// These conditions must match the template under `#suggestions` slot
@@ -134,13 +141,19 @@ export default {
this.showAvailableSuggestions
);
},
+ searchTerm() {
+ return this.searchBy && this.activeTokenValue
+ ? this.activeTokenValue[this.searchBy]
+ : undefined;
+ },
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.suggestions.length) {
- this.$emit('fetch-suggestions', this.value.data);
+ const search = this.searchTerm ? this.searchTerm : this.value.data;
+ this.$emit('fetch-suggestions', search);
}
},
},
@@ -148,8 +161,10 @@ export default {
methods: {
handleInput: debounce(function debouncedSearch({ data }) {
this.searchKey = data;
- if (!this.suggestionsLoading) {
- this.$emit('fetch-suggestions', data);
+
+ if (!this.suggestionsLoading && !this.activeTokenValue) {
+ const search = this.searchTerm ? this.searchTerm : data;
+ this.$emit('fetch-suggestions', search);
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(activeTokenValue) {
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 9f68308808e..9c2f5306654 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
@@ -1,22 +1,19 @@
<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_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
+import { DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
+import searchEpicsQuery from '../queries/search_epics.query.graphql';
+
+import BaseToken from './base_token.vue';
export default {
- separator: '::&',
+ prefix: '&',
+ separator: '::',
components: {
- GlDropdownDivider,
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlLoadingIcon,
},
props: {
config: {
@@ -27,11 +24,15 @@ export default {
type: Object,
required: true,
},
+ active: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
epics: this.config.initialEpics || [],
- loading: true,
+ loading: false,
};
},
computed: {
@@ -56,98 +57,73 @@ export default {
}
return this.defaultEpics;
},
- activeEpic() {
- if (this.currentValue && this.epics.length) {
- // Check if current value is an epic ID.
- if (typeof this.currentValue === 'number') {
- return this.epics.find((epic) => epic[this.idProperty] === this.currentValue);
- }
-
- // Current value is a string.
- const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator);
- return this.epics.find(
- (epic) =>
- epic.group_full_path === groupPath &&
- epic[this.idProperty] === parseInt(idProperty, 10),
- );
- }
- return null;
- },
- displayText() {
- return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`;
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.epics.length) {
- this.searchEpics({ data: this.currentValue });
- }
- },
- },
},
methods: {
- fetchEpicsBySearchTerm({ epicPath = '', search = '' }) {
+ fetchEpics(search = '') {
+ return this.$apollo
+ .query({
+ query: searchEpicsQuery,
+ variables: { fullPath: this.config.fullPath, search },
+ })
+ .then(({ data }) => data.group?.epics.nodes);
+ },
+ fetchEpicsBySearchTerm(search) {
this.loading = true;
- this.config
- .fetchEpics({ epicPath, search })
+ this.fetchEpics(search)
.then((response) => {
- this.epics = Array.isArray(response) ? response : response.data;
+ this.epics = Array.isArray(response) ? response : response?.data;
})
.catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
.finally(() => {
this.loading = false;
});
},
- searchEpics: debounce(function debouncedSearch({ data }) {
- let epicPath = this.activeEpic?.web_url;
-
- // When user visits the page with token value already included in filters
- // 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)) {
- const [groupPath, epicIid] = data.split(this.$options.separator);
- epicPath = `/groups/${groupPath}/-/epics/${epicIid}`;
+ getActiveEpic(epics, data) {
+ if (data && epics.length) {
+ return epics.find((epic) => this.getValue(epic) === data);
}
- this.fetchEpicsBySearchTerm({ epicPath, search: data });
- }, DEBOUNCE_DELAY),
-
+ return undefined;
+ },
getValue(epic) {
- return this.config.useIdValue
- ? String(epic[this.idProperty])
- : `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`;
+ return this.getEpicIdProperty(epic).toString();
+ },
+ displayValue(epic) {
+ return `${this.$options.prefix}${this.getEpicIdProperty(epic)}${this.$options.separator}${
+ epic?.title
+ }`;
+ },
+ getEpicIdProperty(epic) {
+ return getIdFromGraphQLId(epic[this.idProperty]);
},
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :active="active"
+ :suggestions-loading="loading"
+ :suggestions="epics"
+ :get-active-token-value="getActiveEpic"
+ :default-suggestions="availableDefaultEpics"
+ :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
+ search-by="title"
+ @fetch-suggestions="fetchEpicsBySearchTerm"
v-on="$listeners"
- @input="searchEpics"
>
- <template #view="{ inputValue }">
- {{ activeEpic ? displayText : inputValue }}
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="epic in availableDefaultEpics"
- :key="epic.value"
- :value="epic.value"
+ v-for="epic in suggestions"
+ :key="epic.id"
+ :value="getValue(epic)"
>
- {{ epic.text }}
+ {{ epic.title }}
</gl-filtered-search-suggestion>
- <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)">
- {{ epic.title }}
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
index b2f077f5329..5955f31fc70 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -77,7 +77,7 @@ export default {
};
</script>
<template>
- <div class="issue-assignees">
+ <div>
<user-avatar-link
v-for="assignee in assigneesToShow"
:key="assignee.id"
@@ -97,10 +97,9 @@ export default {
</user-avatar-link>
<span
v-if="numHiddenAssignees > 0"
- v-gl-tooltip
+ v-gl-tooltip.bottom
:title="assigneesCounterTooltip"
class="avatar-counter"
- data-placement="bottom"
data-qa-selector="avatar_counter_content"
>{{ assigneeCounterLabel }}</span
>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 095d1854c8b..8aeff9257a5 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,6 +1,12 @@
<script>
import '~/commons/bootstrap';
-import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlTooltip,
+ GlTooltipDirective,
+ GlButton,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { sprintf } from '~/locale';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
@@ -22,6 +28,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [relatedIssuableMixin],
props: {
@@ -84,7 +91,7 @@ export default {
/>
</div>
<gl-tooltip :target="() => $refs.iconElementXL">
- <span v-html="stateTitle /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="stateTitle"></span>
</gl-tooltip>
<gl-icon
v-if="confidential"
@@ -110,7 +117,7 @@ export default {
class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
>
<gl-tooltip :target="() => this.$refs.iconElement">
- <span v-html="stateTitle /* eslint-disable-line vue/no-v-html */"></span>
+ <span v-safe-html="stateTitle"></span>
</gl-tooltip>
<span v-gl-tooltip :title="itemPath" class="path-id-text d-inline-block">{{
itemPath
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index d6a20984ad1..ce7cbafb97d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -1,5 +1,6 @@
<script>
import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton } from '@gitlab/ui';
+import { __, n__ } from '~/locale';
export default {
components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton },
@@ -13,12 +14,26 @@ export default {
type: String,
required: true,
},
+ batchSuggestionsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
message: null,
};
},
+ computed: {
+ dropdownText() {
+ if (this.batchSuggestionsCount <= 1) {
+ return __('Apply suggestion');
+ }
+
+ return n__('Apply %d suggestion', 'Apply %d suggestions', this.batchSuggestionsCount);
+ },
+ },
methods: {
onApply() {
this.$emit('apply', this.message);
@@ -29,10 +44,11 @@ export default {
<template>
<gl-dropdown
- :text="__('Apply suggestion')"
+ :text="dropdownText"
:disabled="disabled"
boundary="window"
right
+ lazy
menu-class="gl-w-full!"
data-qa-selector="apply_suggestion_dropdown"
@shown="$refs.commitMessage.$el.focus()"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 77730ada9bb..86f04c78ebe 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -254,7 +254,7 @@ export default {
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
createFlash({
- message: __('Error rendering markdown preview'),
+ message: __('Error rendering Markdown preview'),
}),
);
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index 9c954fce322..7d8d8c0b90e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -54,8 +54,8 @@ export default {
applySuggestion(callback, message) {
this.$emit('apply', { suggestionId: this.suggestion.id, callback, message });
},
- applySuggestionBatch() {
- this.$emit('applyBatch');
+ applySuggestionBatch(message) {
+ this.$emit('applyBatch', message);
},
addSuggestionToBatch() {
this.$emit('addToBatch', this.suggestion.id);
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 5fdef0b1a23..f9ae59567b2 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
@@ -58,12 +58,19 @@ export default {
isApplyingSingle: false,
};
},
+
computed: {
isApplying() {
return this.isApplyingSingle || this.isApplyingBatch;
},
tooltipMessage() {
- return this.canApply ? __('This also resolves this thread') : this.inapplicableReason;
+ if (!this.canApply) {
+ return this.inapplicableReason;
+ }
+
+ return this.batchSuggestionsCount > 1
+ ? __('This also resolves all related threads')
+ : __('This also resolves this thread');
},
isDisableButton() {
return this.isApplying || !this.canApply;
@@ -72,13 +79,30 @@ export default {
if (this.isApplyingSingle || this.batchSuggestionsCount < 2) {
return __('Applying suggestion...');
}
+
return __('Applying suggestions...');
},
isLoggedIn() {
return isLoggedIn();
},
+ showApplySuggestion() {
+ if (!this.isLoggedIn) return false;
+
+ if (this.batchSuggestionsCount >= 1 && !this.isBatched) {
+ return false;
+ }
+
+ return true;
+ },
},
methods: {
+ apply(message) {
+ if (this.batchSuggestionsCount > 1) {
+ this.applySuggestionBatch(message);
+ } else {
+ this.applySuggestion(message);
+ }
+ },
applySuggestion(message) {
if (!this.canApply) return;
this.isApplyingSingle = true;
@@ -88,9 +112,9 @@ export default {
applySuggestionCallback() {
this.isApplyingSingle = false;
},
- applySuggestionBatch() {
+ applySuggestionBatch(message) {
if (!this.canApply) return;
- this.$emit('applyBatch');
+ this.$emit('applyBatch', message);
},
addSuggestionToBatch() {
this.$emit('addToBatch');
@@ -115,45 +139,34 @@ export default {
<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">
- <gl-button
- class="btn-inverted js-remove-from-batch-btn btn-grouped"
- :disabled="isApplying"
- @click="removeSuggestionFromBatch"
- >
- {{ __('Remove from batch') }}
- </gl-button>
- <gl-button
- v-gl-tooltip.viewport="__('This also resolves all related threads')"
- class="btn-inverted js-apply-batch-btn btn-grouped"
- data-qa-selector="apply_suggestions_batch_button"
- :disabled="isApplying"
- variant="success"
- @click="applySuggestionBatch"
- >
- {{ __('Apply suggestions') }}
- <span class="badge badge-pill badge-pill-success">
- {{ batchSuggestionsCount }}
- </span>
- </gl-button>
- </div>
- <div v-else class="d-flex align-items-center">
- <gl-button
- v-if="suggestionsCount > 1 && !isDisableButton"
- class="btn-inverted js-add-to-batch-btn btn-grouped"
- data-qa-selector="add_suggestion_batch_button"
- :disabled="isDisableButton"
- @click="addSuggestionToBatch"
- >
- {{ __('Add suggestion to batch') }}
- </gl-button>
+ <div v-else-if="isLoggedIn" class="d-flex align-items-center">
+ <div v-if="isBatched">
+ <gl-button
+ class="btn-inverted js-remove-from-batch-btn btn-grouped"
+ :disabled="isApplying"
+ @click="removeSuggestionFromBatch"
+ >
+ {{ __('Remove from batch') }}
+ </gl-button>
+ </div>
+ <div v-else-if="!isDisableButton && suggestionsCount > 1">
+ <gl-button
+ class="btn-inverted js-add-to-batch-btn btn-grouped"
+ data-qa-selector="add_suggestion_batch_button"
+ :disabled="isDisableButton"
+ @click="addSuggestionToBatch"
+ >
+ {{ __('Add suggestion to batch') }}
+ </gl-button>
+ </div>
<apply-suggestion
- v-if="isLoggedIn"
+ v-if="showApplySuggestion"
v-gl-tooltip.viewport="tooltipMessage"
:disabled="isDisableButton"
:default-commit-message="defaultCommitMessage"
+ :batch-suggestions-count="batchSuggestionsCount"
class="gl-ml-3"
- @apply="applySuggestion"
+ @apply="apply"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 63774c6c498..e36cfb3b275 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -68,6 +68,10 @@ export default {
if (this.suggestionsWatch) {
this.suggestionsWatch();
}
+
+ if (this.defaultCommitMessageWatch) {
+ this.defaultCommitMessageWatch();
+ }
},
methods: {
renderSuggestions() {
@@ -123,12 +127,16 @@ export default {
suggestionDiff.suggestionsCount = this.suggestionsCount;
});
+ this.defaultCommitMessageWatch = this.$watch('defaultCommitMessage', () => {
+ suggestionDiff.defaultCommitMessage = this.defaultCommitMessage;
+ });
+
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
});
- suggestionDiff.$on('applyBatch', () => {
- this.$emit('applyBatch', { flashContainer: this.$el });
+ suggestionDiff.$on('applyBatch', (message) => {
+ this.$emit('applyBatch', { message, flashContainer: this.$el });
});
suggestionDiff.$on('addToBatch', (suggestionId) => {
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 d6501a37a35..9ea14ed506c 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -34,12 +34,23 @@ export default {
type: Object,
required: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
computed: {
...mapGetters(['getUserData']),
renderedNote() {
return renderMarkdown(this.note.body);
},
+ avatarSize() {
+ if (this.line) {
+ return 16;
+ }
+ return 40;
+ },
},
};
</script>
@@ -50,7 +61,7 @@ export default {
<user-avatar-link
:link-href="getUserData.path"
:img-src="getUserData.avatar_url"
- :img-size="40"
+ :img-size="avatarSize"
/>
</div>
<div ref="note" :class="{ discussion: !note.individual_note }" class="timeline-content">
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js
new file mode 100644
index 00000000000..9700117a3da
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js
@@ -0,0 +1,34 @@
+import ProjectListItem from './project_list_item.vue';
+
+export default {
+ component: ProjectListItem,
+ title: 'vue_shared/components/project_selector/project_list_item',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ProjectListItem },
+ props: Object.keys(argTypes),
+ template: '<project-list-item v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ project: {
+ id: '1',
+ name: 'MyProject',
+ name_with_namespace: 'path / to / MyProject',
+ },
+ selected: false,
+};
+
+export const SelectedProject = Template.bind({});
+SelectedProject.args = {
+ ...Default.args,
+ selected: true,
+};
+
+export const MatchedProject = Template.bind({});
+MatchedProject.args = {
+ ...Default.args,
+ matcher: 'proj',
+};
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 36d3696ec36..0bd57c84018 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
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { isString } from 'lodash';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -8,6 +8,7 @@ import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/def
export default {
name: 'ProjectListItem',
components: { GlIcon, ProjectAvatar, GlButton },
+ directives: { SafeHtml },
props: {
project: {
type: Object,
@@ -58,9 +59,9 @@ export default {
<span v-if="truncatedNamespace" class="text-secondary">/&nbsp;</span>
</div>
<div
+ v-safe-html="highlightedProjectName"
:title="project.name"
class="js-project-name text-truncate"
- v-html="highlightedProjectName /* eslint-disable-line vue/no-v-html */"
></div>
</div>
</gl-button>
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 5c3a6852219..6538de085b0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
@@ -62,7 +62,7 @@ export default {
<div>
<clipboard-button
v-if="!isLoading"
- css-class="sidebar-collapsed-icon dont-change-state gl-rounded-0! gl-hover-bg-transparent"
+ css-class="sidebar-collapsed-icon js-dont-change-state gl-rounded-0! gl-hover-bg-transparent"
v-bind="clipboardProps"
/>
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 8853dc8b9e3..0ea22eb7aea 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,4 +1,5 @@
import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
@@ -66,10 +67,10 @@ export default {
}
if (isScopedLabel(candidateLabel)) {
- const scopedBase = scopedLabelKey(candidateLabel);
- const currentActiveScopedLabel = state.labels.find(({ title }) => {
- return title.startsWith(scopedBase) && title !== '' && title !== candidateLabel.title;
- });
+ const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
+ const currentActiveScopedLabel = state.labels.find(
+ ({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title,
+ );
if (currentActiveScopedLabel) {
currentActiveScopedLabel.set = false;
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
index 00c54313292..389eb174c0e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
@@ -1,3 +1,5 @@
+export const SCOPED_LABEL_DELIMITER = '::';
+
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 0fcc67c0ffa..3ee0baf8812 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,9 +1,9 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
-
+import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
-import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
+import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils';
export default {
components: {
@@ -48,10 +48,30 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ isVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
showDropdownContentsCreateView: false,
+ localSelectedLabels: [...this.selectedLabels],
+ isDirty: false,
};
},
computed: {
@@ -64,28 +84,66 @@ export default {
dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
},
+ buttonText() {
+ if (!this.localSelectedLabels.length) {
+ return this.dropdownButtonText || __('Label');
+ } else if (this.localSelectedLabels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.localSelectedLabels[0].title,
+ remainingLabelCount: this.localSelectedLabels.length - 1,
+ });
+ }
+ return this.localSelectedLabels[0].title;
+ },
showDropdownFooter() {
- return (
- !this.showDropdownContentsCreateView &&
- (this.isDropdownVariantSidebar(this.variant) ||
- this.isDropdownVariantEmbedded(this.variant))
- );
+ return !this.showDropdownContentsCreateView && !this.isStandalone;
+ },
+ isStandalone() {
+ return isDropdownVariantStandalone(this.variant);
},
},
- methods: {
- showDropdown() {
- this.$refs.dropdown.show();
+ watch: {
+ localSelectedLabels: {
+ handler() {
+ this.isDirty = true;
+ },
+ deep: true,
+ },
+ isVisible(newVal) {
+ if (newVal) {
+ this.$refs.dropdown.show();
+ this.isDirty = false;
+ } else {
+ this.$refs.dropdown.hide();
+ this.setLabels();
+ }
},
+ selectedLabels(newVal) {
+ this.localSelectedLabels = newVal;
+ },
+ },
+ methods: {
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
},
toggleDropdownContent() {
this.toggleDropdownContentsCreateView();
// Required to recalculate dropdown position as its size changes
- this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
+ if (this.$refs.dropdown?.$refs.dropdown) {
+ this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
+ }
+ },
+ setLabels() {
+ if (!this.isDirty) {
+ return;
+ }
+ this.$emit('setLabels', this.localSelectedLabels);
+ },
+ handleDropdownHide() {
+ if (!isDropdownVariantSidebar(this.variant)) {
+ this.setLabels();
+ }
},
- isDropdownVariantSidebar,
- isDropdownVariantEmbedded,
},
};
</script>
@@ -93,14 +151,16 @@ export default {
<template>
<gl-dropdown
ref="dropdown"
- :text="dropdownButtonText"
+ :text="buttonText"
class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content"
+ @hide="handleDropdownHide"
>
<template #header>
<div
- v-if="isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)"
+ v-if="!isStandalone"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-header"
>
<gl-button
v-if="showDropdownContentsCreateView"
@@ -119,27 +179,33 @@ export default {
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
+ data-testid="close-button"
@click="$emit('closeDropdown')"
/>
</div>
</template>
- <component
- :is="dropdownContentsView"
- :selected-labels="selectedLabels"
- :allow-multiselect="allowMultiselect"
- @hideCreateView="toggleDropdownContentsCreateView"
- @setLabels="$emit('setLabels', $event)"
- />
+ <template #default>
+ <component
+ :is="dropdownContentsView"
+ v-model="localSelectedLabels"
+ :selected-labels="selectedLabels"
+ :allow-multiselect="allowMultiselect"
+ :issuable-type="issuableType"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
+ @hideCreateView="toggleDropdownContentsCreateView"
+ />
+ </template>
<template #footer>
<div v-if="showDropdownFooter" data-testid="dropdown-footer">
<gl-dropdown-item
v-if="allowLabelCreate"
data-testid="create-label-button"
- @click.native.capture.stop="toggleDropdownContent"
+ @click.capture.native.stop="toggleDropdownContent"
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.native.capture.stop>
+ <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 2e31b386fdd..a2ed08e6b28 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -2,9 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
+import { labelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
-import projectLabelsQuery from './graphql/project_labels.query.graphql';
const errorMessage = __('Error creating label.');
@@ -18,9 +19,19 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- projectPath: {
- default: '',
+ props: {
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
},
},
data() {
@@ -38,6 +49,27 @@ export default {
const colorsMap = gon.suggested_label_colors;
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
+ mutationVariables() {
+ if (this.issuableType === IssuableType.Epic) {
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ groupPath: this.fullPath,
+ };
+ }
+
+ return this.attrWorkspacePath !== undefined
+ ? {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ groupPath: this.attrWorkspacePath,
+ }
+ : {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ projectPath: this.fullPath,
+ };
+ },
},
methods: {
getColorCode(color) {
@@ -51,8 +83,8 @@ export default {
},
updateLabelsInCache(store, label) {
const sourceData = store.readQuery({
- query: projectLabelsQuery,
- variables: { fullPath: this.projectPath, searchTerm: '' },
+ query: labelsQueries[this.issuableType].workspaceQuery,
+ variables: { fullPath: this.fullPath, searchTerm: '' },
});
const collator = new Intl.Collator('en');
@@ -63,8 +95,8 @@ export default {
});
store.writeQuery({
- query: projectLabelsQuery,
- variables: { fullPath: this.projectPath, searchTerm: '' },
+ query: labelsQueries[this.issuableType].workspaceQuery,
+ variables: { fullPath: this.fullPath, searchTerm: '' },
data,
});
},
@@ -75,11 +107,7 @@ export default {
data: { labelCreate },
} = await this.$apollo.mutate({
mutation: createLabelMutation,
- variables: {
- title: this.labelTitle,
- color: this.selectedColor,
- projectPath: this.projectPath,
- },
+ variables: this.mutationVariables,
update: (
store,
{
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 857367a0721..e6a25362ff0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,12 +1,18 @@
<script>
-import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdownForm,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
-import projectLabelsQuery from './graphql/project_labels.query.graphql';
+import { labelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue';
export default {
@@ -15,9 +21,12 @@ export default {
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
+ GlIntersectionObserver,
LabelItem,
},
- inject: ['projectPath'],
+ model: {
+ prop: 'localSelectedLabels',
+ },
props: {
selectedLabels: {
type: Array,
@@ -27,30 +36,44 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ localSelectedLabels: {
+ type: Array,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
searchKey: '',
labels: [],
- localSelectedLabels: [...this.selectedLabels],
+ isVisible: false,
};
},
apollo: {
labels: {
- query: projectLabelsQuery,
+ query() {
+ return labelsQueries[this.issuableType].workspaceQuery;
+ },
variables() {
return {
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
searchTerm: this.searchKey,
};
},
skip() {
- return this.searchKey.length === 1;
+ return this.searchKey.length === 1 || !this.isVisible;
},
update: (data) => data.workspace?.labels?.nodes || [],
async result() {
if (this.$refs.searchInput) {
- await this.$nextTick();
+ await this.$nextTick;
this.$refs.searchInput.focusInput();
}
},
@@ -64,7 +87,7 @@ export default {
return this.$apollo.queries.labels.loading;
},
localSelectedLabelsIds() {
- return this.localSelectedLabels.map((label) => label.id);
+ return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id));
},
visibleLabels() {
if (this.searchKey) {
@@ -82,7 +105,6 @@ export default {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
- this.$emit('setLabels', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
@@ -109,16 +131,21 @@ export default {
}
},
updateSelectedLabels(label) {
+ let labels;
if (this.isLabelSelected(label)) {
- this.localSelectedLabels = this.localSelectedLabels.filter(
- ({ id }) => id !== getIdFromGraphQLId(label.id),
+ labels = this.localSelectedLabels.filter(
+ ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
);
} else {
- this.localSelectedLabels.push({
- ...label,
- id: getIdFromGraphQLId(label.id),
- });
+ labels = [
+ ...this.localSelectedLabels,
+ {
+ ...label,
+ id: getIdFromGraphQLId(label.id),
+ },
+ ];
}
+ this.$emit('input', labels);
},
handleLabelClick(label) {
this.updateSelectedLabels(label);
@@ -129,46 +156,52 @@ export default {
setSearchKey(value) {
this.searchKey = value;
},
+ onDropdownAppear() {
+ this.isVisible = true;
+ this.$refs.searchInput.focusInput();
+ },
},
};
</script>
<template>
- <gl-dropdown-form class="labels-select-contents-list js-labels-list">
- <gl-search-box-by-type
- ref="searchInput"
- :value="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- data-testid="dropdown-input-field"
- @input="debouncedSearchKeyUpdate"
- />
- <div ref="labelsListContainer" data-testid="dropdown-content">
- <gl-loading-icon
- v-if="labelsFetchInProgress"
- class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
- size="md"
+ <gl-intersection-observer @appear="onDropdownAppear">
+ <gl-dropdown-form class="labels-select-contents-list js-labels-list">
+ <gl-search-box-by-type
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="debouncedSearchKeyUpdate"
/>
- <template v-else>
- <gl-dropdown-item
- v-for="label in visibleLabels"
- :key="label.id"
- :is-checked="isLabelSelected(label)"
- :is-check-centered="true"
- :is-check-item="true"
- data-testid="labels-list"
- @click.native.capture.stop="handleLabelClick(label)"
- >
- <label-item :label="label" />
- </gl-dropdown-item>
- <gl-dropdown-item
- v-show="showNoMatchingResultsMessage"
- class="gl-p-3 gl-text-center"
- data-testid="no-results"
- >
- {{ __('No matching results') }}
- </gl-dropdown-item>
- </template>
- </div>
- </gl-dropdown-form>
+ <div ref="labelsListContainer" data-testid="dropdown-content">
+ <gl-loading-icon
+ v-if="labelsFetchInProgress"
+ class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full gl-mb-3"
+ size="md"
+ />
+ <template v-else>
+ <gl-dropdown-item
+ v-for="label in visibleLabels"
+ :key="label.id"
+ :is-checked="isLabelSelected(label)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ data-testid="labels-list"
+ @click.native.capture.stop="handleLabelClick(label)"
+ >
+ <label-item :label="label" />
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-show="showNoMatchingResultsMessage"
+ class="gl-p-3 gl-text-center"
+ data-testid="no-results"
+ >
+ {{ __('No matching results') }}
+ </gl-dropdown-item>
+ </template>
+ </div>
+ </gl-dropdown-form>
+ </gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
new file mode 100644
index 00000000000..a2e8579486f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
@@ -0,0 +1,15 @@
+query epicLabels($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ issuable: epic(iid: $iid) {
+ id
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
new file mode 100644
index 00000000000..acc9bcd2015
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
@@ -0,0 +1,12 @@
+query groupLabels($fullPath: ID!, $searchTerm: String) {
+ workspace: group(fullPath: $fullPath) {
+ labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 3c834770563..6bd43da2203 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -1,21 +1,18 @@
<script>
-import Vue from 'vue';
-import Vuex from 'vuex';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { labelsQueries } from '~/sidebar/constants';
import { DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
-import issueLabelsQuery from './graphql/issue_labels.query.graphql';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
} from './utils';
-Vue.use(Vuex);
-
export default {
components: {
DropdownValue,
@@ -23,8 +20,21 @@ export default {
DropdownValueCollapsed,
SidebarEditableItem,
},
- inject: ['iid', 'projectPath', 'allowLabelEdit'],
+ inject: {
+ allowLabelEdit: {
+ default: false,
+ },
+ },
props: {
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
allowLabelRemove: {
type: Boolean,
required: false,
@@ -90,43 +100,60 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
contentIsOnViewport: true,
- issueLabels: [],
+ issuableLabels: [],
};
},
+ computed: {
+ isLoading() {
+ return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
+ },
+ },
apollo: {
- issueLabels: {
- query: issueLabelsQuery,
+ issuableLabels: {
+ query() {
+ return labelsQueries[this.issuableType].issuableQuery;
+ },
+ skip() {
+ return !isDropdownVariantSidebar(this.variant);
+ },
variables() {
return {
iid: this.iid,
- fullPath: this.projectPath,
+ fullPath: this.fullPath,
};
},
update(data) {
return data.workspace?.issuable?.labels.nodes || [];
},
+ error() {
+ createFlash({ message: __('Error fetching labels.') });
+ },
},
},
methods: {
handleDropdownClose(labels) {
- if (labels.length) this.$emit('updateSelectedLabels', labels);
- this.$emit('onDropdownClose');
+ this.$emit('updateSelectedLabels', labels);
+ this.collapseEditableItem();
},
- collapseDropdown() {
- this.$refs.editable.collapse();
+ collapseEditableItem() {
+ this.$refs.editable?.collapse();
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
- showDropdown() {
- this.$nextTick(() => {
- this.$refs.dropdownContents.showDropdown();
- });
- },
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
@@ -145,20 +172,19 @@ export default {
<template v-if="isDropdownVariantSidebar(variant)">
<dropdown-value-collapsed
ref="dropdownButtonCollapsed"
- :labels="issueLabels"
+ :labels="issuableLabels"
@onValueClick="handleCollapsedValueClick"
/>
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
- :loading="labelsSelectInProgress"
+ :loading="isLoading"
:can-edit="allowLabelEdit"
- @open="showDropdown"
>
<template #collapsed>
<dropdown-value
:disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
+ :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@@ -170,7 +196,7 @@ export default {
<template #default="{ edit }">
<dropdown-value
:disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
+ :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@@ -180,8 +206,6 @@ export default {
<slot></slot>
</dropdown-value>
<dropdown-contents
- v-if="edit"
- ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle"
@@ -190,11 +214,30 @@ export default {
:labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
:variant="variant"
- @closeDropdown="collapseDropdown"
+ :issuable-type="issuableType"
+ :is-visible="edit"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
@setLabels="handleDropdownClose"
+ @closeDropdown="collapseEditableItem"
/>
</template>
</sidebar-editable-item>
</template>
+ <dropdown-contents
+ v-else
+ ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :dropdown-button-text="dropdownButtonText"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ :variant="variant"
+ :issuable-type="issuableType"
+ :full-path="fullPath"
+ @setLabels="handleDropdownClose"
+ />
</div>
</template>
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
index d2afc02233e..294e5bd9f90 100644
--- 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
@@ -4,7 +4,7 @@ import TodoButton from './todo_button.vue';
export default {
component: TodoButton,
- title: 'vue_shared/components/todo_toggle/todo_button',
+ title: 'vue_shared/components/sidebar/todo_toggle/todo_button',
};
const Template = (args, { argTypes }) => ({
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index afb1ea702fa..0a7a22ed3a8 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -45,7 +45,7 @@ export default {
data() {
return {
dragCounter: 0,
- isDragDataValid: false,
+ isDragDataValid: true,
};
},
computed: {
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js
new file mode 100644
index 00000000000..256db2ea1ce
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js
@@ -0,0 +1,5 @@
+// Types of obstacles to user deletion
+export const OBSTACLE_TYPES = Object.freeze({
+ oncallSchedules: 'ONCALL_SCHEDULE',
+ escalationPolicies: 'ESCALATION_POLICY',
+});
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
new file mode 100644
index 00000000000..d2030c14029
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
@@ -0,0 +1,37 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import { OBSTACLE_TYPES } from './constants';
+import UserDeletionObstaclesList from './user_deletion_obstacles_list.vue';
+
+export default {
+ component: UserDeletionObstaclesList,
+ title: 'vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { UserDeletionObstaclesList },
+ props: Object.keys(argTypes),
+ template: '<user-deletion-obstacles-list v-bind="$props" v-on="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ obstacles: [
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'APAC',
+ url: 'https://domain.com/group/main-application/oncall_schedules',
+ projectName: 'main-application',
+ projectUrl: 'https://domain.com/group/main-application',
+ },
+ {
+ type: OBSTACLE_TYPES.escalationPolicies,
+ name: 'Engineering On-call',
+ url: 'https://domain.com/group/microservice-backend/escalation_policies',
+ projectName: 'Microservice Backend',
+ projectUrl: 'https://domain.com/group/microservice-backend',
+ },
+ ],
+ userName: 'Thomspon Smith',
+ isCurrentUser: false,
+};
diff --git a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
index e37a663ace3..1eea660d527 100644
--- a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
@@ -1,6 +1,16 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
+import { OBSTACLE_TYPES } from './constants';
+
+const OBSTACLE_TEXT = {
+ [OBSTACLE_TYPES.oncallSchedules]: s__(
+ 'OnCallSchedules|On-call schedule %{obstacle} in Project %{project}',
+ ),
+ [OBSTACLE_TYPES.escalationPolicies]: s__(
+ 'EscalationPolicies|Escalation policy %{obstacle} in Project %{project}',
+ ),
+};
export default {
components: {
@@ -8,7 +18,7 @@ export default {
GlLink,
},
props: {
- schedules: {
+ obstacles: {
type: Array,
required: true,
},
@@ -45,6 +55,15 @@ export default {
);
},
},
+ methods: {
+ textForObstacle(obstacle) {
+ return OBSTACLE_TEXT[obstacle.type];
+ },
+ urlForObstacle(obstacle) {
+ // Fallback to scheduleUrl for backwards compatibility
+ return obstacle.url || obstacle.scheduleUrl;
+ },
+ },
};
</script>
@@ -52,17 +71,15 @@ export default {
<div>
<p data-testid="title">{{ title }}</p>
- <ul data-testid="schedules-list">
- <li v-for="(schedule, index) in schedules" :key="`${schedule.name}-${index}`">
- <gl-sprintf
- :message="s__('OnCallSchedules|On-call schedule %{schedule} in Project %{project}')"
- >
- <template #schedule>
- <gl-link :href="schedule.scheduleUrl" target="_blank">{{ schedule.name }}</gl-link>
+ <ul data-testid="obstacles-list">
+ <li v-for="(obstacle, index) in obstacles" :key="`${obstacle.name}-${index}`">
+ <gl-sprintf :message="textForObstacle(obstacle)">
+ <template #obstacle>
+ <gl-link :href="urlForObstacle(obstacle)" target="_blank">{{ obstacle.name }}</gl-link>
</template>
<template #project>
- <gl-link :href="schedule.projectUrl" target="_blank">{{
- schedule.projectName
+ <gl-link :href="obstacle.projectUrl" target="_blank">{{
+ obstacle.projectName
}}</gl-link>
</template>
</gl-sprintf>
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js
new file mode 100644
index 00000000000..502302a1ef2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js
@@ -0,0 +1,19 @@
+import { OBSTACLE_TYPES } from './constants';
+
+const addTypeToObstacles = (obstacles, type) => {
+ if (!obstacles) return [];
+
+ return obstacles?.map((obstacle) => ({ type, ...obstacle }));
+};
+
+// For use with user objects formatted via internal REST API.
+// If the removal/deletion of a user could cause critical
+// problems, return a single array containing all affected
+// associations including their type.
+export const parseUserDeletionObstacles = (user) => {
+ if (!user) return [];
+
+ return Object.keys(OBSTACLE_TYPES).flatMap((type) => {
+ return addTypeToObstacles(user[type], OBSTACLE_TYPES[type]);
+ });
+};
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 74616763f8f..05e0c3b0be3 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
@@ -93,19 +93,27 @@ export default {
</div>
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
- <gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="profile" class="gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2 gl-overflow-hidden">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <gl-icon name="work" class="gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
+ <div v-if="user.location" class="gl-display-flex gl-mb-2">
+ <gl-icon name="location" class="gl-flex-shrink-0" />
+ <span class="gl-ml-2">{{ user.location }}</span>
+ </div>
+ <div
+ v-if="user.localTime && !user.bot"
+ class="gl-display-flex gl-mb-2"
+ data-testid="user-popover-local-time"
+ >
+ <gl-icon name="clock" class="gl-flex-shrink-0" />
+ <span class="gl-ml-2">{{ user.localTime }}</span>
+ </div>
</div>
- <div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
- <gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" />
- <span class="gl-ml-2">{{ user.location }}</span>
- </div>
- <div v-if="statusHtml" class="js-user-status gl-mt-3">
+ <div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
<span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
</div>
<div v-if="user.bot" class="gl-text-blue-500">
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 df0981aea7a..6da2d39a95a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -92,7 +92,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-edit',
- handle: () => this.showModal('#modal-confirm-fork-edit'),
+ handle: () => {
+ this.$emit('edit', 'simple');
+ this.showModal('#modal-confirm-fork-edit');
+ },
}
: { href: this.editUrl };
@@ -128,7 +131,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-webide',
- handle: () => this.showModal('#modal-confirm-fork-webide'),
+ handle: () => {
+ this.$emit('edit', 'ide');
+ this.showModal('#modal-confirm-fork-webide');
+ },
}
: { href: this.webIdeUrl };