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>2022-06-20 14:10:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 14:10:13 +0300
commit0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch)
tree7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app/assets/javascripts/vue_shared
parent72123183a20411a36d607d70b12d57c484394c8e (diff)
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue10
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js9
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue214
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js39
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue125
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue59
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue17
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue7
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue28
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue6
66 files changed, 1036 insertions, 334 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index c93f620995f..f2ea55df63d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -18,7 +18,6 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import initUserPopovers from '~/user_popovers';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
@@ -83,9 +82,6 @@ export default {
alertId: {
default: '',
},
- isThreatMonitoringPage: {
- default: false,
- },
projectId: {
default: '',
},
@@ -175,7 +171,6 @@ export default {
updated() {
this.$nextTick(() => {
highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'));
- initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
methods: {
@@ -225,9 +220,7 @@ export default {
});
},
incidentPath(issueId) {
- return this.isThreatMonitoringPage
- ? joinPaths(this.projectIssuesPath, issueId)
- : joinPaths(this.projectIssuesPath, 'incident', issueId);
+ return joinPaths(this.projectIssuesPath, 'incident', issueId);
},
trackPageViews() {
const { category, action } = this.trackAlertsDetailsViewsOptions;
@@ -374,7 +367,6 @@ export default {
</gl-tab>
<metric-images-tab
- v-if="!isThreatMonitoringPage"
:data-testid="$options.tabsConfig[1].id"
:title="$options.tabsConfig[1].title"
/>
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 489d4afa41f..72dcc16b57a 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
@@ -302,9 +302,11 @@ export default {
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
{{ __('None') }} -
<gl-button
- class="gl-ml-2"
+ class="gl-ml-2 gl-reset-color!"
href="#"
+ category="tertiary"
variant="link"
+ size="small"
data-testid="unassigned-users"
@click="updateAlertAssignees(currentUser)"
>
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
index 6cc70739eaa..d106f545c61 100644
--- a/app/assets/javascripts/vue_shared/alert_details/constants.js
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -30,13 +30,4 @@ export const PAGE_CONFIG = {
label: 'Status',
},
},
- THREAT_MONITORING: {
- TITLE: 'THREAT_MONITORING',
- STATUSES: {
- TRIGGERED: s__('ThreatMonitoring|Unreviewed'),
- ACKNOWLEDGED: s__('ThreatMonitoring|In review'),
- RESOLVED: s__('ThreatMonitoring|Resolved'),
- IGNORED: s__('ThreatMonitoring|Dismissed'),
- },
- },
};
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 614748fa80d..5793069440c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -65,16 +65,12 @@ export default (selector) => {
const opsProperties = {};
- if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
- const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
- page
- ];
- provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
- provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
- opsProperties.store = createStore({}, service);
- } else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
- provide.isThreatMonitoringPage = true;
- }
+ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
+ page
+ ];
+ provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
+ provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ opsProperties.store = createStore({}, service);
// eslint-disable-next-line no-new
new Vue({
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 9bccc49e894..8bffc2479a1 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -45,7 +45,12 @@ export default {
return validSizes.includes(value);
},
},
- borderless: {
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isBorderless: {
type: Boolean,
required: false,
default: false,
@@ -67,15 +72,19 @@ export default {
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`;
},
icon() {
- return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
+ return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
},
},
};
</script>
<template>
<span
- :class="[wrapperStyleClasses, { interactive: isInteractive }]"
+ :class="[
+ wrapperStyleClasses,
+ { interactive: isInteractive, active: isActive, borderless: isBorderless },
+ ]"
:style="{ height: `${size}px`, width: `${size}px` }"
+ data-testid="ci-icon-wrapper"
>
<gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index f14e1992901..dd6923d9fcd 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -45,7 +45,7 @@ export default {
};
</script>
<template>
- <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
+ <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="confirm">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
new file mode 100644
index 00000000000..92817d5fa70
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
@@ -0,0 +1,25 @@
+<script>
+export default {
+ props: {
+ color: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <span
+ class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0"
+ data-testid="color-item"
+ :style="{ backgroundColor: color }"
+ ></span>
+ <span class="hide-collapsed">{{ title }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
new file mode 100644
index 00000000000..6b79883d76b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
@@ -0,0 +1,214 @@
+<script>
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants';
+import DropdownContents from './dropdown_contents.vue';
+import DropdownValue from './dropdown_value.vue';
+import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
+import epicColorQuery from './graphql/epic_color.query.graphql';
+import updateEpicColorMutation from './graphql/epic_update_color.mutation.graphql';
+
+export default {
+ i18n: {
+ assignColor: s__('ColorWidget|Assign epic color'),
+ dropdownButtonText: COLOR_WIDGET_COLOR,
+ fetchingError: s__('ColorWidget|Error fetching epic color.'),
+ updatingError: s__('ColorWidget|An error occurred while updating color.'),
+ widgetTitle: COLOR_WIDGET_COLOR,
+ },
+ components: {
+ DropdownValue,
+ DropdownContents,
+ SidebarEditableItem,
+ },
+ props: {
+ allowEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ variant: {
+ type: String,
+ required: false,
+ default: DROPDOWN_VARIANT.Sidebar,
+ },
+ dropdownButtonText: {
+ type: String,
+ required: false,
+ default: COLOR_WIDGET_COLOR,
+ },
+ dropdownTitle: {
+ type: String,
+ required: false,
+ default: s__('ColorWidget|Assign epic color'),
+ },
+ },
+ data() {
+ return {
+ issuableColor: {
+ color: '',
+ title: '',
+ },
+ colorUpdateInProgress: false,
+ oldIid: null,
+ sidebarExpandedOnClick: false,
+ };
+ },
+ apollo: {
+ issuableColor: {
+ query: epicColorQuery,
+ skip() {
+ return !isDropdownVariantSidebar(this.variant);
+ },
+ variables() {
+ return {
+ iid: this.iid,
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ const issuableColor = data.workspace?.issuable?.color;
+
+ if (issuableColor) {
+ return ISSUABLE_COLORS.find((color) => color.color === issuableColor) ?? DEFAULT_COLOR;
+ }
+
+ return DEFAULT_COLOR;
+ },
+ error() {
+ createFlash({
+ message: this.$options.i18n.fetchingError,
+ captureError: true,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.colorUpdateInProgress || this.$apollo.queries.issuableColor.loading;
+ },
+ },
+ watch: {
+ iid(_, oldVal) {
+ this.oldIid = oldVal;
+ },
+ },
+ methods: {
+ handleDropdownClose(color) {
+ if (this.iid !== '') {
+ this.updateSelectedColor(this.getUpdateVariables(color));
+ } else {
+ this.$emit('updateSelectedColor', color);
+ }
+
+ this.collapseEditableItem();
+ },
+ collapseEditableItem() {
+ this.$refs.editable?.collapse();
+ if (this.sidebarExpandedOnClick) {
+ this.sidebarExpandedOnClick = false;
+ this.$emit('toggleCollapse');
+ }
+ },
+ getUpdateVariables(color) {
+ const currentIid = this.oldIid || this.iid;
+
+ return {
+ iid: currentIid,
+ groupPath: this.fullPath,
+ color: color.color,
+ };
+ },
+ updateSelectedColor(inputVariables) {
+ this.colorUpdateInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateEpicColorMutation,
+ variables: { input: inputVariables },
+ })
+ .then(({ data }) => {
+ if (data.updateIssuableColor?.errors?.length) {
+ throw new Error();
+ }
+
+ this.$emit('updateSelectedColor', {
+ id: data.updateIssuableColor?.issuable?.id,
+ color: data.updateIssuableColor?.issuable?.color,
+ });
+ })
+ .catch((error) =>
+ createFlash({
+ message: this.$options.i18n.updatingError,
+ captureError: true,
+ error,
+ }),
+ )
+ .finally(() => {
+ this.colorUpdateInProgress = false;
+ });
+ },
+ isDropdownVariantSidebar,
+ isDropdownVariantEmbedded,
+ },
+};
+</script>
+
+<template>
+ <div
+ class="labels-select-wrapper gl-relative"
+ :class="{
+ 'is-embedded': isDropdownVariantEmbedded(variant),
+ }"
+ >
+ <template v-if="isDropdownVariantSidebar(variant)">
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.widgetTitle"
+ :loading="isLoading"
+ :can-edit="allowEdit"
+ @open="oldIid = null"
+ >
+ <template #collapsed>
+ <dropdown-value :selected-color="issuableColor">
+ <slot></slot>
+ </dropdown-value>
+ </template>
+ <template #default="{ edit }">
+ <dropdown-value :selected-color="issuableColor" class="gl-mb-2">
+ <slot></slot>
+ </dropdown-value>
+ <dropdown-contents
+ ref="dropdownContents"
+ :dropdown-button-text="dropdownButtonText"
+ :dropdown-title="dropdownTitle"
+ :selected-color="issuableColor"
+ :variant="variant"
+ :is-visible="edit"
+ @setColor="handleDropdownClose"
+ @closeDropdown="collapseEditableItem"
+ />
+ </template>
+ </sidebar-editable-item>
+ </template>
+ <dropdown-contents
+ v-else
+ ref="dropdownContents"
+ :dropdown-button-text="dropdownButtonText"
+ :dropdown-title="dropdownTitle"
+ :selected-color="issuableColor"
+ :variant="variant"
+ @setColor="handleDropdownClose"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
new file mode 100644
index 00000000000..c70785abd1e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
@@ -0,0 +1,30 @@
+import { __, s__ } from '~/locale';
+
+export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color');
+
+export const DROPDOWN_VARIANT = {
+ Sidebar: 'sidebar',
+ Embedded: 'embedded',
+};
+
+export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' };
+
+export const ISSUABLE_COLORS = [
+ DEFAULT_COLOR,
+ {
+ title: s__('SuggestedColors|Green'),
+ color: '#217645',
+ },
+ {
+ title: s__('SuggestedColors|Red'),
+ color: '#c91c00',
+ },
+ {
+ title: s__('SuggestedColors|Orange'),
+ color: '#9e5400',
+ },
+ {
+ title: s__('SuggestedColors|Purple'),
+ color: '#694cc0',
+ },
+];
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
new file mode 100644
index 00000000000..4eb1d3d08ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlDropdown } from '@gitlab/ui';
+import DropdownContentsColorView from './dropdown_contents_color_view.vue';
+import DropdownHeader from './dropdown_header.vue';
+import { isDropdownVariantSidebar } from './utils';
+
+export default {
+ components: {
+ DropdownContentsColorView,
+ DropdownHeader,
+ GlDropdown,
+ },
+ props: {
+ dropdownTitle: {
+ type: String,
+ required: true,
+ },
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ dropdownButtonText: {
+ type: String,
+ required: true,
+ },
+ variant: {
+ type: String,
+ required: true,
+ },
+ isVisible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDropdownContentsCreateView: false,
+ localSelectedColor: this.selectedColor,
+ isDirty: false,
+ };
+ },
+ computed: {
+ buttonText() {
+ if (!this.localSelectedColor?.title) {
+ return this.dropdownButtonText;
+ }
+
+ return this.localSelectedColor.title;
+ },
+ },
+ watch: {
+ localSelectedColor: {
+ handler() {
+ this.isDirty = true;
+ },
+ deep: true,
+ },
+ isVisible(newVal) {
+ if (newVal) {
+ this.$refs.dropdown.show();
+ this.isDirty = false;
+ this.localSelectedColor = this.selectedColor;
+ } else {
+ this.$refs.dropdown.hide();
+ this.setColor();
+ }
+ },
+ selectedColor(newVal) {
+ if (!this.isDirty) {
+ this.localSelectedColor = newVal;
+ }
+ },
+ },
+ methods: {
+ setColor() {
+ if (!this.isDirty) {
+ return;
+ }
+ this.$emit('setColor', this.localSelectedColor);
+ },
+ handleDropdownHide() {
+ this.$emit('closeDropdown');
+ if (!isDropdownVariantSidebar(this.variant)) {
+ this.setColor();
+ }
+ this.$refs.dropdown.hide();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide">
+ <template #header>
+ <dropdown-header
+ ref="header"
+ :dropdown-title="dropdownTitle"
+ @closeDropdown="handleDropdownHide"
+ />
+ </template>
+ <template #default>
+ <dropdown-contents-color-view
+ v-model="localSelectedColor"
+ @closeDropdown="handleDropdownHide"
+ />
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
new file mode 100644
index 00000000000..62f4cf59c14
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlDropdownForm, GlDropdownItem } from '@gitlab/ui';
+import ColorItem from './color_item.vue';
+import { ISSUABLE_COLORS } from './constants';
+
+export default {
+ components: {
+ GlDropdownForm,
+ GlDropdownItem,
+ ColorItem,
+ },
+ model: {
+ prop: 'selectedColor',
+ },
+ props: {
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ colors: ISSUABLE_COLORS,
+ };
+ },
+ methods: {
+ isColorSelected(color) {
+ return this.selectedColor.color === color.color;
+ },
+ handleColorClick(color) {
+ this.$emit('input', color);
+ this.$emit('closeDropdown', this.selectedColor);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-form>
+ <div>
+ <gl-dropdown-item
+ v-for="color in colors"
+ :key="color.color"
+ :is-checked="isColorSelected(color)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click.native.capture.stop="handleColorClick(color)"
+ >
+ <color-item :color="color.color" :title="color.title" />
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown-form>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
new file mode 100644
index 00000000000..a32b1570f5f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ dropdownTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
new file mode 100644
index 00000000000..4cba66eefd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { COLOR_WIDGET_COLOR } from './constants';
+import ColorItem from './color_item.vue';
+
+export default {
+ i18n: {
+ dropdownTitle: COLOR_WIDGET_COLOR,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ ColorItem,
+ },
+ props: {
+ selectedColor: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="value js-value">
+ <div
+ v-gl-tooltip.left.viewport
+ :title="$options.i18n.dropdownTitle"
+ class="sidebar-collapsed-icon"
+ >
+ <gl-icon name="appearance" />
+ <color-item
+ :color="selectedColor.color"
+ :title="selectedColor.title"
+ class="gl-font-base gl-line-height-24"
+ />
+ </div>
+
+ <color-item class="hide-collapsed" :color="selectedColor.color" :title="selectedColor.title" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
new file mode 100644
index 00000000000..959e0f8c1a5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
@@ -0,0 +1,9 @@
+query epicColor($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ issuable: epic(iid: $iid) {
+ id
+ color
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
new file mode 100644
index 00000000000..2975b42253f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateEpicColor($input: UpdateEpicInput!) {
+ updateIssuableColor: updateEpic(input: $input) {
+ issuable: epic {
+ id
+ color
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
new file mode 100644
index 00000000000..46196e793b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
@@ -0,0 +1,15 @@
+import { DROPDOWN_VARIANT } from './constants';
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `sidebar`
+ * @param {string} variant
+ */
+export const isDropdownVariantSidebar = (variant) => variant === DROPDOWN_VARIANT.Sidebar;
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `embedded`
+ * @param {string} variant
+ */
+export const isDropdownVariantEmbedded = (variant) => variant === DROPDOWN_VARIANT.Embedded;
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 9cf8638f3cb..3ecfac10f9c 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
@@ -1,8 +1,5 @@
<script>
-import {
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { forEach, escape } from 'lodash';
@@ -14,7 +11,7 @@ let axiosSource;
export default {
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
},
directives: {
SafeHtml,
@@ -115,7 +112,7 @@ export default {
<template>
<div ref="markdownPreview" class="md-previewer" data-testid="md-previewer">
- <gl-skeleton-loading v-if="isLoading" />
+ <gl-skeleton-loader v-if="isLoading" />
<div
v-else
v-safe-html:[$options.safeHtmlConfig]="previewContent"
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 d7a84798e47..5d7f4ae2a01 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
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const DEBOUNCE_DELAY = 500;
export const MAX_RECENT_TOKENS_SIZE = 3;
@@ -46,11 +46,13 @@ export const SortDirection = {
export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
-export const TOKEN_TITLE_AUTHOR = __('Author');
export const TOKEN_TITLE_ASSIGNEE = __('Assignee');
-export const TOKEN_TITLE_MILESTONE = __('Milestone');
+export const TOKEN_TITLE_AUTHOR = __('Author');
+export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
+export const TOKEN_TITLE_CONTACT = s__('Crm|Contact');
export const TOKEN_TITLE_LABEL = __('Label');
-export const TOKEN_TITLE_TYPE = __('Type');
-export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
-export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
+export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
+export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_TYPE = __('Type');
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 c3a0a97a7ba..6a4ff07c999 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
@@ -168,7 +168,7 @@ export default {
if (data || operator) {
this.searchKey = data;
- if (!this.suggestionsLoading && !this.activeTokenValue) {
+ if (!this.activeTokenValue) {
let search = this.searchTerm ? this.searchTerm : data;
if (search.startsWith('"') && search.endsWith('"')) {
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 6f24955814c..178c57a5666 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
@@ -43,9 +43,7 @@ export default {
},
methods: {
getActiveLabel(labels, data) {
- return labels.find(
- (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(),
- );
+ return labels.find((label) => this.getLabelName(label) === stripQuotes(data));
},
/**
* There's an inconsistency between private and public API
@@ -128,7 +126,7 @@ export default {
<div class="gl-display-flex gl-align-items-center">
<span
:style="{ backgroundColor: label.color }"
- class="gl-display-inline-block mr-2 p-2"
+ class="gl-display-inline-block gl-mr-3 gl-p-3"
></span>
<div>{{ getLabelName(label) }}</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index 69548f0e7a8..15d858b99b9 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -1,5 +1,11 @@
<script>
-import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -12,6 +18,7 @@ export default {
},
components: {
GlFormInputGroup,
+ GlFormInput,
GlFormGroup,
GlButton,
ClipboardButton,
@@ -80,10 +87,15 @@ export default {
this.$emit('visibility-change', this.valueIsVisible);
},
+ handleClick() {
+ this.$refs.input.$el.select();
+ },
handleCopyButtonClick() {
this.$emit('copy');
},
handleFormInputCopy(event) {
+ this.handleCopyButtonClick();
+
if (this.computedValueIsVisible) {
return;
}
@@ -96,14 +108,21 @@ export default {
</script>
<template>
<gl-form-group v-bind="$attrs">
- <gl-form-input-group
- :value="displayedValue"
- input-class="gl-font-monospace! gl-cursor-default!"
- select-on-click
- readonly
- v-bind="formInputGroupProps"
- @copy="handleFormInputCopy"
- >
+ <gl-form-input-group>
+ <gl-form-input
+ ref="input"
+ readonly
+ class="gl-font-monospace! gl-cursor-default!"
+ v-bind="formInputGroupProps"
+ :value="displayedValue"
+ @copy="handleFormInputCopy"
+ @click="handleClick"
+ />
+
+ <!--
+ This v-if is necessary to avoid an issue with border radius.
+ See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88059#note_969812649
+ -->
<template v-if="showToggleVisibilityButton || showCopyButton" #append>
<gl-button
v-if="showToggleVisibilityButton"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index ba2b5eaa4f9..4fdf7f45643 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -266,7 +266,7 @@ export default {
}}
</p>
<gl-button
- variant="info"
+ variant="confirm"
category="primary"
size="small"
@click="handleSuggestDismissed"
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 7a7074da084..78a7fed6293 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -98,13 +98,15 @@ export default {
<span v-else-if="isConfidential" ref="confidential">
{{ confidentialContextText }}
{{ __('People without permission will never get a notification.') }}
- <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link>
+ <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{
+ __('Learn more.')
+ }}</gl-link>
</span>
<span v-else-if="isLocked" ref="locked">
{{ lockedContextText }}
{{ __('Only project members can comment.') }}
- <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more') }}</gl-link>
+ <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more.') }}</gl-link>
</span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 3aca068c074..2206ae98c73 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'SkeletonNote',
components: {
- GlSkeletonLoading,
+ GlSkeletonLoader,
TimelineEntryItem,
},
};
@@ -16,7 +16,7 @@ export default {
<div class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loading /></div>
+ <div class="note-body"><gl-skeleton-loader /></div>
</div>
</timeline-entry-item>
</template>
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 dd7a851b1be..3593ea16968 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -18,7 +18,7 @@
*/
import {
GlButton,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlSkeletonLoader,
GlTooltipDirective,
GlIcon,
GlSafeHtmlDirective as SafeHtml,
@@ -26,9 +26,9 @@ import {
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -46,7 +46,7 @@ export default {
noteHeader,
TimelineEntryItem,
GlButton,
- GlSkeletonLoading,
+ GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -94,7 +94,7 @@ export default {
},
},
mounted() {
- initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
+ $(this.$refs['gfm-content']).renderGFM();
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
@@ -130,7 +130,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-safe-html="actionTextHtml"></span>
+ <span ref="gfm-content" v-safe-html="actionTextHtml"></span>
<template
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
#extra-controls
@@ -172,7 +172,7 @@ export default {
</div>
<div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</pre>
<pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
<gl-button
@@ -218,7 +218,9 @@ export default {
</tr>
</table>
</div>
- <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
+ <div v-else-if="showLines" class="mt-4">
+ <gl-skeleton-loader />
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index f21092af501..67ad7769c7c 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -130,16 +130,19 @@ export default {
<span data-testid="legend-text">{{ legendText }}</span>
</template>
</gl-infinite-scroll>
- <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
+ <div v-if="showNoResultsMessage" class="gl-text-gray-600 gl-ml-3 js-no-results-message">
{{ __('Sorry, no projects matched your search') }}
</div>
<div
v-if="showMinimumSearchQueryMessage"
- class="text-muted ml-2 js-minimum-search-query-message"
+ class="gl-text-gray-600 gl-ml-3 js-minimum-search-query-message"
>
{{ __('Enter at least three characters to search') }}
</div>
- <div v-if="showSearchErrorMessage" class="text-danger ml-2 js-search-error-message">
+ <div
+ v-if="showSearchErrorMessage"
+ class="gl-text-red-500 gl-font-weight-bold gl-ml-3 js-search-error-message"
+ >
{{ __('Something went wrong, unable to search projects') }}
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index da68fe961a6..1948a6778f4 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -12,7 +12,7 @@ export default {
GlFilteredSearch,
},
props: {
- filter: {
+ filters: {
type: Array,
required: true,
},
@@ -33,7 +33,7 @@ export default {
computed: {
internalFilter: {
get() {
- return this.filter;
+ return this.filters;
},
set(value) {
this.$emit('filter:changed', value);
@@ -71,7 +71,7 @@ export default {
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
const newQueryString = this.generateQueryData({
sorting: { ...this.sorting, sort },
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('sorting:changed', { sort });
this.$emit('query:changed', newQueryString);
@@ -79,7 +79,7 @@ export default {
onSortItemClick(item) {
const newQueryString = this.generateQueryData({
sorting: { ...this.sorting, orderBy: item },
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('sorting:changed', { orderBy: item });
this.$emit('query:changed', newQueryString);
@@ -87,7 +87,7 @@ export default {
submitSearch() {
const newQueryString = this.generateQueryData({
sorting: this.sorting,
- filter: this.filter,
+ filter: this.filters,
});
this.$emit('filter:submit');
this.$emit('query:changed', newQueryString);
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index 34845e3d9e4..c97e191b630 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -1,7 +1,5 @@
import { s__ } from '~/locale';
-export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
-
export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
index 5d144c0d699..06852f511bf 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -9,35 +9,19 @@ export default {
RunnerInstructionsModal,
},
directives: {
- GlModalDirective,
+ GlModal: GlModalDirective,
},
modalId: 'runner-instructions-modal',
i18n: {
buttonText: s__('Runners|Show runner installation instructions'),
},
- data() {
- return {
- opened: false,
- };
- },
- methods: {
- onClick() {
- // lazily mount modal to prevent premature instructions requests
- this.opened = true;
- },
- },
};
</script>
<template>
<div>
- <gl-button
- v-gl-modal-directive="$options.modalId"
- class="gl-mt-4"
- data-testid="show-modal-button"
- @click="onClick"
- >
+ <gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button">
{{ $options.i18n.buttonText }}
</gl-button>
- <runner-instructions-modal v-if="opened" :modal-id="$options.modalId" />
+ <runner-instructions-modal :modal-id="$options.modalId" />
</div>
</template>
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 9eaaf7d1c18..bfaf3b92c34 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
@@ -17,7 +17,6 @@ import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
- PLATFORMS_WITHOUT_ARCHITECTURES,
REGISTRATION_TOKEN_PLACEHOLDER,
} from './constants';
import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
@@ -59,21 +58,25 @@ export default {
apollo: {
platforms: {
query: getRunnerPlatformsQuery,
+ skip() {
+ // Only load instructions once the modal is shown
+ return !this.shown;
+ },
update(data) {
- return data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
- return {
- name,
- humanReadableName,
- architectures: architectures?.nodes || [],
- };
- });
+ return (
+ data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
+ return {
+ name,
+ humanReadableName,
+ architectures: architectures?.nodes || [],
+ };
+ }) ?? []
+ );
},
result() {
- if (this.platforms.length) {
- // If it is set and available, select the defaultSelectedPlatform.
- // Otherwise, select the first available platform
- this.selectPlatform(this.defaultPlatform() || this.platforms[0]);
- }
+ // If it is set and available, select the defaultSelectedPlatform.
+ // Otherwise, select the first available platform
+ this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name);
},
error() {
this.toggleAlert(true);
@@ -82,12 +85,12 @@ export default {
instructions: {
query: getRunnerSetupInstructionsQuery,
skip() {
- return !this.selectedPlatform;
+ return !this.shown || !this.selectedPlatform;
},
variables() {
return {
- platform: this.selectedPlatformName,
- architecture: this.selectedArchitectureName || '',
+ platform: this.selectedPlatform,
+ architecture: this.selectedArchitecture || '',
};
},
update(data) {
@@ -100,6 +103,7 @@ export default {
},
data() {
return {
+ shown: false,
platforms: [],
selectedPlatform: null,
selectedArchitecture: null,
@@ -109,55 +113,63 @@ export default {
};
},
computed: {
- platformsEmpty() {
- return isEmpty(this.platforms);
- },
instructionsEmpty() {
return isEmpty(this.instructions);
},
- selectedPlatformName() {
- return this.selectedPlatform?.name;
- },
- selectedArchitectureName() {
- return this.selectedArchitecture?.name;
+ architectures() {
+ return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || [];
},
- hasArchitecureList() {
- return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatformName);
+ binaryUrl() {
+ return this.architectures.find(({ name }) => name === this.selectedArchitecture)
+ ?.downloadLocation;
},
instructionsWithoutArchitecture() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.instructions;
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions;
},
runnerInstallationLink() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link;
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link;
},
registerInstructionsWithToken() {
const { registerInstructions } = this.instructions || {};
if (this.registrationToken) {
- return registerInstructions.replace(REGISTRATION_TOKEN_PLACEHOLDER, this.registrationToken);
+ return registerInstructions?.replace(
+ REGISTRATION_TOKEN_PLACEHOLDER,
+ this.registrationToken,
+ );
}
-
return registerInstructions;
},
},
+ updated() {
+ // Refocus on dom changes, after loading data
+ this.refocusSelectedPlatformButton();
+ },
methods: {
show() {
this.$refs.modal.show();
},
- focusSelected() {
- // By default the first platform always gets the focus, but when the `defaultPlatformName`
- // property is present, any other platform might actually be selected.
- this.$refs[this.selectedPlatformName]?.[0].$el.focus();
+ onShown() {
+ this.shown = true;
+ this.refocusSelectedPlatformButton();
},
- defaultPlatform() {
- return this.platforms.find((platform) => platform.name === this.defaultPlatformName);
+ refocusSelectedPlatformButton() {
+ // On modal opening, the first focusable element is auto-focused by bootstrap-vue
+ // This can be confusing for users, because the wrong platform button can
+ // get focused when setting a `defaultPlatformName`.
+ // This method refocuses the expected button.
+ // See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
+ this.$refs[this.selectedPlatform]?.[0].$el.focus();
},
- selectPlatform(platform) {
- this.selectedPlatform = platform;
+ selectPlatform(platformName) {
+ this.selectedPlatform = platformName;
- if (!platform.architectures?.some(({ name }) => name === this.selectedArchitectureName)) {
- // Select first architecture when current value is not available
- this.selectArchitecture(platform.architectures[0]);
+ // Update architecture when platform changes
+ const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture);
+ if (arch) {
+ this.selectArchitecture(arch.name);
+ } else {
+ this.selectArchitecture(this.architectures[0]?.name);
}
},
selectArchitecture(architecture) {
@@ -175,6 +187,7 @@ export default {
},
},
i18n: {
+ environment: __('Environment'),
installARunner: s__('Runners|Install a runner'),
architecture: s__('Runners|Architecture'),
downloadInstallBinary: s__('Runners|Download and install binary'),
@@ -182,6 +195,7 @@ export default {
registerRunnerCommand: s__('Runners|Command to register runner'),
fetchError: s__('Runners|An error has occurred fetching instructions'),
copyInstructions: s__('Runners|Copy instructions'),
+ viewInstallationInstructions: s__('Runners|View installation instructions'),
},
closeButton: {
text: __('Close'),
@@ -197,17 +211,17 @@ export default {
:action-secondary="$options.closeButton"
v-bind="$attrs"
v-on="$listeners"
- @shown="focusSelected"
+ @shown="onShown"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
</gl-alert>
- <gl-skeleton-loader v-if="platformsEmpty && $apollo.loading" />
+ <gl-skeleton-loader v-if="!platforms.length && $apollo.loading" />
- <template v-if="!platformsEmpty">
+ <template v-if="platforms.length">
<h5>
- {{ __('Environment') }}
+ {{ $options.i18n.environment }}
</h5>
<div v-gl-resize-observer="onPlatformsButtonResize">
<gl-button-group
@@ -220,29 +234,29 @@ export default {
v-for="platform in platforms"
:key="platform.name"
:ref="platform.name"
- :selected="selectedPlatform && selectedPlatform.name === platform.name"
- @click="selectPlatform(platform)"
+ :selected="selectedPlatform === platform.name"
+ @click="selectPlatform(platform.name)"
>
{{ platform.humanReadableName }}
</gl-button>
</gl-button-group>
</div>
</template>
- <template v-if="hasArchitecureList">
+ <template v-if="architectures.length">
<template v-if="selectedPlatform">
<h5>
{{ $options.i18n.architecture }}
<gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
- <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName">
+ <gl-dropdown class="gl-mb-3" :text="selectedArchitecture">
<gl-dropdown-item
- v-for="architecture in selectedPlatform.architectures"
+ v-for="architecture in architectures"
:key="architecture.name"
:is-check-item="true"
- :is-checked="selectedArchitectureName === architecture.name"
+ :is-checked="selectedArchitecture === architecture.name"
data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture)"
+ @click="selectArchitecture(architecture.name)"
>
{{ architecture.name }}
</gl-dropdown-item>
@@ -250,8 +264,9 @@ export default {
<div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
+ v-if="binaryUrl"
class="gl-ml-auto"
- :href="selectedArchitecture.downloadLocation"
+ :href="binaryUrl"
download
icon="download"
data-testid="binary-download-button"
@@ -298,7 +313,7 @@ export default {
<p>{{ instructionsWithoutArchitecture }}</p>
<gl-button :href="runnerInstallationLink">
<gl-icon name="external-link" />
- {{ s__('Runners|View installation instructions') }}
+ {{ $options.i18n.viewInstallationInstructions }}
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index 60111210f5d..9388ef4ba45 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -2,6 +2,9 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index 399db978b60..1064cbc26e3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -4,6 +4,9 @@ import { mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue` instead.
export default {
components: {
DropdownContentsLabelsView,
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 2cccb8325f4..3ff3755de46 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
@@ -2,6 +2,9 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue` instead.
export default {
components: {
GlButton,
@@ -51,10 +54,10 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
- <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <div class="dropdown-title d-flex align-items-center pt-0 pb-2 gl-mb-0">
<gl-button
:aria-label="__('Go back')"
- variant="link"
+ category="tertiary"
size="small"
class="js-btn-back dropdown-header-button p-0"
icon="arrow-left"
@@ -63,7 +66,7 @@ export default {
<span class="flex-grow-1">{{ labelsCreateTitle }}</span>
<gl-button
:aria-label="__('Close')"
- variant="link"
+ category="tertiary"
size="small"
class="dropdown-header-button p-0"
icon="close"
@@ -95,7 +98,7 @@ export default {
></span>
<gl-form-input
v-model.trim="selectedColor"
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
/>
</div>
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 134575b7a27..e235bfde394 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
@@ -13,6 +13,9 @@ import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/
import LabelItem from './label_item.vue';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue` instead.
export default {
components: {
GlIntersectionObserver,
@@ -166,7 +169,7 @@ export default {
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
- variant="link"
+ category="tertiary"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@@ -193,6 +196,7 @@ export default {
:key="label.id"
:label="label"
:is-label-set="label.set"
+ :is-label-indeterminate="label.indeterminate"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
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 e91a0489ef1..e4325492334 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
@@ -2,6 +2,9 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue` instead.
export default {
components: {
GlButton,
@@ -23,7 +26,9 @@ export default {
</script>
<template>
- <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold">
+ <div
+ class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold gl-mb-0"
+ >
{{ __('Labels') }}
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index 35ac9ef8565..e59d150dd43 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -5,6 +5,9 @@ import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue` instead.
export default {
components: {
GlLabel,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
index 8a26c4a6618..5966c78aa51 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
@@ -2,6 +2,9 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
export default {
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
index dd40add6376..154e3013acd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -1,6 +1,10 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue` instead.
export default {
functional: true,
props: {
@@ -12,6 +16,11 @@ export default {
type: Boolean,
required: true,
},
+ isLabelIndeterminate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
highlight: {
type: Boolean,
required: false,
@@ -19,7 +28,7 @@ export default {
},
},
render(h, { props, listeners }) {
- const { label, highlight, isLabelSet } = props;
+ const { label, highlight, isLabelSet, isLabelIndeterminate } = props;
const labelColorBox = h('span', {
class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
@@ -33,18 +42,36 @@ export default {
const checkedIcon = h(GlIcon, {
class: {
- 'gl-mr-3 gl-flex-shrink-0': true,
+ 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true,
hidden: !isLabelSet,
},
+ attrs: {
+ title: __('Selected for all items.'),
+ 'data-testid': 'checked-icon',
+ },
props: {
name: 'mobile-issue-close',
},
});
+ const indeterminateIcon = h(GlIcon, {
+ class: {
+ 'gl-mr-3 gl-flex-shrink-0 has-tooltip': true,
+ hidden: !isLabelIndeterminate,
+ },
+ attrs: {
+ title: __('Selected for some items.'),
+ 'data-testid': 'indeterminate-icon',
+ },
+ props: {
+ name: 'dash',
+ },
+ });
+
const noIcon = h('span', {
class: {
'gl-mr-5 gl-pr-3': true,
- hidden: isLabelSet,
+ hidden: isLabelSet || isLabelIndeterminate,
},
attrs: {
'data-testid': 'no-icon',
@@ -63,7 +90,7 @@ export default {
},
},
},
- [noIcon, checkedIcon, labelColorBox, labelTitle],
+ [noIcon, checkedIcon, indeterminateIcon, labelColorBox, labelTitle],
);
return h(
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 7e259cb8b96..b61996cdcdb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -15,6 +15,9 @@ import labelsSelectModule from './store';
Vue.use(Vuex);
+// @deprecated This component should only be used when there is no GraphQL API.
+// In most cases you should use
+// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue` instead.
export default {
store: new Vuex.Store(labelsSelectModule()),
components: {
@@ -198,11 +201,12 @@ export default {
!state.showDropdownButton &&
!state.showDropdownContents
) {
- let filterFn = (label) => label.touched;
- if (this.isDropdownVariantEmbedded) {
- filterFn = (label) => label.set;
- }
- this.handleDropdownClose(state.labels.filter(filterFn));
+ const filterTouchedLabelsFn = (label) => label.touched;
+ const filterSetLabelsFn = (label) => label.set;
+ const labels = this.isDropdownVariantEmbedded
+ ? state.labels.filter(filterSetLabelsFn)
+ : state.labels.filter(filterTouchedLabelsFn);
+ this.handleDropdownClose(labels, state.labels.filter(filterTouchedLabelsFn));
}
},
/**
@@ -265,11 +269,11 @@ export default {
isInDropdownContents
);
},
- handleDropdownClose(labels) {
- // Only emit label updates if there are any labels to update
- // on UI.
+ handleDropdownClose(labels, touchedLabels) {
+ // Only emit label updates if there are any
+ // labels to update on UI.
if (labels.length) this.$emit('updateSelectedLabels', labels);
- this.$emit('onDropdownClose');
+ this.$emit('onDropdownClose', touchedLabels);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
index d14f96720b7..ef3eedd9bb2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -8,9 +8,10 @@ import { DropdownVariant } from '../constants';
* @param {object} state
*/
export const dropdownButtonText = (state, getters) => {
- const selectedLabels = getters.isDropdownVariantSidebar
- ? state.labels.filter((label) => label.set)
- : state.selectedLabels;
+ const selectedLabels =
+ getters.isDropdownVariantSidebar || getters.isDropdownVariantEmbedded
+ ? state.labels.filter((label) => label.set || label.indeterminate)
+ : state.selectedLabels;
if (!selectedLabels.length) {
return state.dropdownButtonText || __('Label');
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 9e64f03fe84..43b23994cdf 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
@@ -2,8 +2,39 @@ import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
+const transformLabels = (labels, selectedLabels) =>
+ labels.map((label) => {
+ const selectedLabel = selectedLabels.find(({ id }) => id === label.id);
+
+ return {
+ ...label,
+ set: Boolean(selectedLabel?.set),
+ indeterminate: Boolean(selectedLabel?.indeterminate),
+ };
+ });
+
export default {
[types.SET_INITIAL_STATE](state, props) {
+ // We need to ensure that selectedLabels have
+ // `set` & `indeterminate` properties defined.
+ if (props.selectedLabels?.length) {
+ props.selectedLabels.forEach((label) => {
+ /* eslint-disable no-param-reassign */
+ if (label.set === undefined && label.indeterminate === undefined) {
+ label.set = true;
+ label.indeterminate = false;
+ } else if (label.set === undefined && label.indeterminate !== undefined) {
+ label.set = false;
+ } else if (label.set !== undefined && label.indeterminate === undefined) {
+ label.indeterminate = false;
+ } else {
+ label.set = false;
+ label.indeterminate = false;
+ }
+ /* eslint-enable no-param-reassign */
+ });
+ }
+
Object.assign(state, { ...props });
},
@@ -36,10 +67,7 @@ export default {
// selectedLabels array.
state.labelsFetchInProgress = false;
state.labelsFetched = true;
- state.labels = labels.map((label) => ({
- ...label,
- set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
- }));
+ state.labels = transformLabels(labels, state.selectedLabels);
},
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
@@ -62,7 +90,8 @@ export default {
const candidateLabel = state.labels.find((label) => labelId === label.id);
if (candidateLabel) {
candidateLabel.touched = true;
- candidateLabel.set = !candidateLabel.set;
+ candidateLabel.set = candidateLabel.indeterminate ? true : !candidateLabel.set;
+ candidateLabel.indeterminate = false;
}
if (isScopedLabel(candidateLabel)) {
@@ -80,9 +109,6 @@ export default {
},
[types.UPDATE_LABELS_SET_STATE](state) {
- state.labels = state.labels.map((label) => ({
- ...label,
- set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
- }));
+ state.labels = transformLabels(state.labels, state.selectedLabels);
},
};
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 0fa64a29b3a..5471cda0cc5 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
@@ -169,6 +169,9 @@ export default {
setFocus() {
this.$refs.header.focusInput();
},
+ hideDropdown() {
+ this.$refs.dropdown.hide();
+ },
showDropdown() {
this.$refs.dropdown.show();
},
@@ -205,7 +208,7 @@ export default {
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
:is-standalone="isStandalone"
@toggleDropdownContentsCreateView="toggleDropdownContent"
- @closeDropdown="$emit('closeDropdown')"
+ @closeDropdown="hideDropdown"
@input="debouncedSearchKeyUpdate"
@searchEnter="selectFirstItem"
/>
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 090bf9493bf..5f344ae4214 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
@@ -140,18 +140,19 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
<div class="dropdown-input">
- <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-3">
+ <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mt-3">
{{ error }}
</gl-alert>
<gl-form-input
v-model.trim="labelTitle"
+ class="gl-mt-3"
:placeholder="__('Name new label')"
:autofocus="true"
data-testid="label-title-input"
/>
</div>
<div class="dropdown-content gl-px-3">
- <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3!">
+ <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3! gl-mb-0">
<gl-link
v-for="(color, index) in suggestedColors"
:key="index"
@@ -169,7 +170,7 @@ export default {
></span>
<gl-form-input
v-model.trim="selectedColor"
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
data-testid="selected-color-text"
/>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
index faad69732dd..aaddab43e2a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -51,7 +51,7 @@ export default {
<div data-testid="dropdown-header">
<div
v-if="!isStandalone"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3! gl-mb-0"
data-testid="dropdown-header-title"
>
<gl-button
@@ -72,6 +72,7 @@ export default {
class="dropdown-header-button gl-p-0!"
icon="close"
data-testid="close-button"
+ data-qa-selector="close_labels_dropdown_button"
@click="$emit('closeDropdown')"
/>
</div>
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 57ee816c4c7..57e3ee4aaa5 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
@@ -92,7 +92,9 @@ export default {
@click="handleCollapsedClick"
>
<gl-icon name="labels" />
- <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">{{
+ selectedLabels.length
+ }}</span>
</div>
<span
v-if="!selectedLabels.length"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index c30ca5369ee..7b62f0cdb7d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -72,7 +72,7 @@ export default {
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight"
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal"
:class="firstLineClass"
><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index bed6dd4d5c6..0d78530d878 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
export const BIDI_CHAR_TOOLTIP = __(
'Potentially unwanted character detected: Unicode BiDi Control',
);
+
+export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
new file mode 100644
index 00000000000..c9f7e5508be
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -0,0 +1,13 @@
+import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
+import wrapComments from './wrap_comments';
+
+/**
+ * Registers our plugins for Highlight.js
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} hljs - the Highlight.js instance.
+ */
+export const registerPlugins = (hljs) => {
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
new file mode 100644
index 00000000000..5be92af5b55
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
@@ -0,0 +1,39 @@
+import { HLJS_COMMENT_SELECTOR } from '../constants';
+
+const createWrapper = (content) => {
+ const span = document.createElement('span');
+ span.className = HLJS_COMMENT_SELECTOR;
+ span.innerHTML = content;
+ return span.outerHTML;
+};
+
+/**
+ * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
+ * This ensures that multi-line comments are rendered correctly in the GitLab UI.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+export default (result) => {
+ if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
+
+ let wrapComment = false;
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
+ .split('\n')
+ .map((lineContent) => {
+ const includesClosingTag = lineContent.includes('</span>');
+ if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) {
+ wrapComment = true;
+ return lineContent;
+ }
+ const line = wrapComment ? createWrapper(lineContent) : lineContent;
+ if (includesClosingTag) {
+ wrapComment = false;
+ }
+ return line;
+ })
+ .join('\n');
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index ed87a202b15..f819a9e5be2 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
+import { registerPlugins } from './plugins/index';
/*
* This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
@@ -111,6 +112,7 @@ export default {
let detectedLanguage = language;
let highlightedContent;
if (this.hljs) {
+ registerPlugins(this.hljs);
if (!detectedLanguage) {
const hljsHighlightAuto = this.hljs.highlightAuto(content);
highlightedContent = hljsHighlightAuto.value;
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 f62bf686f85..424cab20c7e 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
@@ -149,7 +149,7 @@ export default {
>
<slot>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
@click="openFileUpload"
>
@@ -192,7 +192,7 @@ export default {
<transition name="upload-dropzone-fade">
<div
v-show="dragging && !enableDragBehavior"
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
>
<div v-show="!isDragDataValid" class="mw-50 gl-text-center">
<slot name="invalid-drag-data-slot">
diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
index bc5e0cf10dd..20a666509a4 100644
--- a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
+++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
@@ -34,7 +34,7 @@ export default {
<div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
<div
v-if="$slots['left-primary-text']"
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-mb-4"
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
>
<slot name="left-primary-text"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/constants.js b/app/assets/javascripts/vue_shared/components/user_popover/constants.js
new file mode 100644
index 00000000000..1d49aefd297
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_popover/constants.js
@@ -0,0 +1 @@
+export const USER_POPOVER_DELAY = 200;
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 ec7a7cd72ae..768cd005727 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
@@ -14,12 +14,14 @@ import { glEmojiTag } from '~/emoji';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import { USER_POPOVER_DELAY } from './constants';
const MAX_SKELETON_LINES = 4;
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
+ USER_POPOVER_DELAY,
components: {
GlIcon,
GlLink,
@@ -48,6 +50,11 @@ export default {
required: false,
default: 'top',
},
+ show: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -133,25 +140,21 @@ export default {
</script>
<template>
- <!-- 200ms delay so not every mouseover triggers Popover -->
- <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport">
- <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
- <div
- class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p"
- >
+ <!-- Delayed so not every mouseover triggers Popover -->
+ <gl-popover
+ :css-classes="['gl-max-w-48']"
+ :show="show"
+ :target="target"
+ :delay="$options.USER_POPOVER_DELAY"
+ :placement="placement"
+ boundary="viewport"
+ triggers="hover focus manual"
+ >
+ <div class="gl-py-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
+ <div class="gl-mr-4 gl-flex-shrink-0">
<user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-m-0!" />
- <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3">
- <gl-button
- :variant="toggleFollowButtonVariant"
- :loading="toggleFollowLoading"
- size="small"
- data-testid="toggle-follow-button"
- @click="toggleFollow"
- >{{ toggleFollowButtonText }}</gl-button
- >
- </div>
</div>
- <div class="gl-w-full gl-min-w-0 gl-word-break-word">
+ <div class="gl-w-full gl-word-break-word gl-display-flex gl-align-items-center">
<template v-if="userIsLoading">
<gl-skeleton-loader
:lines="$options.maxSkeletonLines"
@@ -161,7 +164,7 @@ export default {
/>
</template>
<template v-else>
- <div class="gl-mb-3">
+ <div>
<h5 class="gl-m-0">
<user-name-with-status
:name="user.name"
@@ -170,42 +173,64 @@ export default {
/>
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
- </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-flex-shrink-0" />
- <span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
+ <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3">
+ <gl-button
+ :variant="toggleFollowButtonVariant"
+ :loading="toggleFollowLoading"
+ size="small"
+ data-testid="toggle-follow-button"
+ @click="toggleFollow"
+ >{{ toggleFollowButtonText }}</gl-button
+ >
</div>
- <div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <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="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
- <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
- </div>
- <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500">
- <gl-icon name="question" />
- <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
- <gl-sprintf :message="__('Learn more about %{username}')">
- <template #username>{{ user.name }}</template>
- </gl-sprintf>
- </gl-link>
</div>
</template>
</div>
</div>
+ <div class="gl-mt-2 gl-w-full gl-word-break-word">
+ <template v-if="userIsLoading">
+ <gl-skeleton-loader
+ :lines="$options.maxSkeletonLines"
+ preserve-aspect-ratio="none"
+ equal-width-lines
+ :height="24"
+ />
+ </template>
+ <template v-else>
+ <div class="gl-text-gray-500">
+ <div v-if="user.bio" class="gl-display-flex gl-mb-2">
+ <gl-icon name="profile" class="gl-flex-shrink-0" />
+ <span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
+ </div>
+ <div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
+ <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="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
+ <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
+ </div>
+ <div v-if="user.bot && user.websiteUrl" class="gl-text-blue-500">
+ <gl-icon name="question" />
+ <gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
+ <gl-sprintf :message="__('Learn more about %{username}')">
+ <template #username>{{ user.name }}</template>
+ </gl-sprintf>
+ </gl-link>
+ </div>
+ </template>
+ </div>
</gl-popover>
</template>
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 15f84e48179..cac0d5a45c9 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -307,7 +307,7 @@ export default {
<actions-button
:actions="actions"
:selected-key="selection"
- :variant="isBlob ? 'info' : 'default'"
+ :variant="isBlob ? 'confirm' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
@select="select"
/>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 3ebeec4a50b..14328b1f25f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -71,10 +71,14 @@ export const AVATAR_SHAPE_OPTION_RECT = 'rect';
export const confidentialityInfoText = (workspaceType, issuableType) =>
sprintf(
__(
- 'Only %{workspaceType} members with at least Reporter role can view or be notified about this %{issuableType}.',
+ 'Only %{workspaceType} members with %{permissions} can view or be notified about this %{issuableType}.',
),
{
workspaceType: workspaceType === WorkspaceType.project ? __('project') : __('group'),
issuableType: issuableType === IssuableType.Issue ? __('issue') : __('epic'),
+ permissions:
+ issuableType === IssuableType.Issue
+ ? __('at least the Reporter role, the author, and assignees')
+ : __('at least the Reporter role'),
},
);
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
index f4cbaba9313..033bb8c3885 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
@@ -29,7 +29,6 @@ export default {
<template>
<div class="issuable-create-container">
<slot name="title"></slot>
- <hr class="gl-mt-0" />
<issuable-form
:description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath"
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 0758cb507e9..89eecea5239 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -51,9 +51,9 @@ export default {
<template>
<gl-form class="common-note-form gfm-form" @submit.stop.prevent>
- <div data-testid="issuable-title" class="form-group row">
- <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
- <div class="col-sm-10">
+ <div data-testid="issuable-title" class="row">
+ <label for="issuable-title" class="col-12 gl-mb-0">{{ __('Title') }}</label>
+ <div class="col-12">
<gl-form-group :description="__('Maximum of 255 characters')">
<gl-form-input
id="issuable-title"
@@ -66,10 +66,8 @@ export default {
</div>
</div>
<div data-testid="issuable-description" class="form-group row">
- <label for="issuable-description" class="col-form-label col-sm-2">{{
- __('Description')
- }}</label>
- <div class="col-sm-10">
+ <label for="issuable-description" class="col-12">{{ __('Description') }}</label>
+ <div class="col-12">
<markdown-field
:markdown-preview-path="descriptionPreviewPath"
:markdown-docs-path="descriptionHelpPath"
@@ -91,37 +89,28 @@ export default {
</markdown-field>
</div>
</div>
- <div class="row">
- <div class="col-lg-6">
- <div data-testid="issuable-labels" class="form-group row">
- <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{
- __('Labels')
- }}</label>
- <div class="col-md-8 col-sm-10">
- <div class="issuable-form-select-holder">
- <labels-select
- :allow-label-edit="true"
- :allow-label-create="true"
- :allow-multiselect="true"
- :allow-scoped-labels="true"
- :labels-fetch-path="labelsFetchPath"
- :labels-manage-path="labelsManagePath"
- :selected-labels="selectedLabels"
- :labels-list-title="__('Select label')"
- :footer-create-label-title="__('Create project label')"
- :footer-manage-label-title="__('Manage project labels')"
- :variant="$options.LabelSelectVariant.Embedded"
- @updateSelectedLabels="handleUpdateSelectedLabels"
- />
- </div>
- </div>
+ <div data-testid="issuable-labels" class="form-group row">
+ <label for="issuable-labels" class="col-12">{{ __('Labels') }}</label>
+ <div class="col-12">
+ <div class="issuable-form-select-holder">
+ <labels-select
+ :allow-label-edit="true"
+ :allow-label-create="true"
+ :allow-multiselect="true"
+ :allow-scoped-labels="true"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ :selected-labels="selectedLabels"
+ :labels-list-title="__('Select label')"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :variant="$options.LabelSelectVariant.Embedded"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ />
</div>
</div>
</div>
- <div
- data-testid="issuable-create-actions"
- class="footer-block row-content-block gl-display-flex"
- >
+ <div data-testid="issuable-create-actions" class="footer-block gl-display-flex gl-mt-6">
<slot
name="actions"
:issuable-title="issuableTitle"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 6453290f6ea..a9f8caa3e1f 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -23,6 +23,11 @@ export default {
},
mixins: [timeagoMixin],
props: {
+ hasScopedLabelsFeature: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
issuableSymbol: {
type: String,
required: true,
@@ -132,7 +137,7 @@ export default {
return Boolean(this.$slots[slotName]);
},
scopedLabel(label) {
- return isScopedLabel(label);
+ return this.hasScopedLabelsFeature && isScopedLabel(label);
},
labelTitle(label) {
return label.title || label.name;
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 8b293b2e9f6..8fbf0bb10a0 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -1,10 +1,5 @@
<script>
-import {
- GlAlert,
- GlKeysetPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlPagination,
-} from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlSkeletonLoader, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@@ -27,7 +22,7 @@ export default {
components: {
GlAlert,
GlKeysetPagination,
- GlSkeletonLoading,
+ GlSkeletonLoader,
IssuableTabs,
FilteredSearchBar,
IssuableItem,
@@ -138,6 +133,11 @@ export default {
required: false,
default: 2,
},
+ hasScopedLabelsFeature: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
labelFilterParam: {
type: String,
required: false,
@@ -307,7 +307,7 @@ export default {
</issuable-bulk-edit-sidebar>
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
- <gl-skeleton-loading />
+ <gl-skeleton-loader />
</li>
</ul>
<template v-else>
@@ -325,6 +325,7 @@ export default {
:class="{ 'gl-cursor-grab': isManualOrdering }"
data-qa-selector="issuable_container"
:data-qa-issuable-title="issuable.title"
+ :has-scoped-labels-feature="hasScopedLabelsFeature"
:issuable-symbol="issuableSymbol"
:issuable="issuable"
:label-filter-param="labelFilterParam"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index c6dce6a51c2..be9afc0610d 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
@@ -46,6 +46,13 @@ export const AvailableSortOptions = [
},
];
+export const IssuableTypes = {
+ Issue: 'ISSUE',
+ Incident: 'INCIDENT',
+ TestCase: 'TEST_CASE',
+ Requirement: 'REQUIREMENT',
+};
+
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_SKELETON_COUNT = 5;
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 05dc1650379..5eb3da3c62e 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: true,
- },
statusIcon: {
type: String,
required: true,
@@ -162,7 +158,6 @@ export default {
<template v-else>
<issuable-title
:issuable="issuable"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:enable-edit="enableEdit"
@edit-issuable="$emit('edit-issuable', $event)"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index 649dbd6576b..f035795a045 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -40,11 +40,6 @@ export default {
required: false,
default: '',
},
- statusBadgeClass: {
- type: String,
- required: false,
- default: '',
- },
statusIcon: {
type: String,
required: false,
@@ -113,12 +108,7 @@ export default {
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
- <gl-badge
- data-testid="status"
- class="issuable-status-badge gl-mr-3"
- :class="statusBadgeClass"
- :variant="badgeVariant"
- >
+ <gl-badge class="issuable-status-badge gl-mr-3" :variant="badgeVariant">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span>
</gl-badge>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index c165ee91c59..7ed93c042f8 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -17,11 +17,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: false,
- default: '',
- },
statusIcon: {
type: String,
required: false,
@@ -108,7 +103,6 @@ export default {
<div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
:issuable-state="issuable.state"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:blocked="issuable.blocked"
@@ -127,7 +121,6 @@ export default {
<issuable-body
:issuable="issuable"
- :status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:enable-edit="enableEdit"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 47f05a2cee2..3d7c71ce974 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,12 +1,14 @@
<script>
import {
GlIcon,
+ GlBadge,
GlButton,
GlIntersectionObserver,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { __ } from '~/locale';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
i18n: {
@@ -14,6 +16,7 @@ export default {
},
components: {
GlIcon,
+ GlBadge,
GlButton,
GlIntersectionObserver,
},
@@ -26,10 +29,6 @@ export default {
type: Object,
required: true,
},
- statusBadgeClass: {
- type: String,
- required: true,
- },
statusIcon: {
type: String,
required: true,
@@ -44,6 +43,11 @@ export default {
stickyTitleVisible: false,
};
},
+ computed: {
+ badgeVariant() {
+ return this.issuable.state === IssuableStates.Opened ? 'success' : 'info';
+ },
+ },
methods: {
handleTitleAppear() {
this.stickyTitleVisible = false;
@@ -60,7 +64,7 @@ export default {
<div class="title-container">
<h1
v-safe-html="issuable.titleHtml || issuable.title"
- class="title qa-title"
+ class="title qa-title gl-font-size-h-display"
dir="auto"
data-testid="title"
></h1>
@@ -84,14 +88,12 @@ export default {
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
- <p
- data-testid="status"
- class="issuable-status-box status-box gl-white-space-nowrap gl-my-0"
- :class="statusBadgeClass"
- >
- <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
- <span class="gl-display-none d-sm-block"><slot name="status-badge"></slot></span>
- </p>
+ <gl-badge class="gl-white-space-nowrap gl-mr-3" :variant="badgeVariant">
+ <gl-icon v-if="statusIcon" class="gl-sm-display-none" :name="statusIcon" />
+ <span class="gl-display-none gl-sm-display-block">
+ <slot name="status-badge"></slot>
+ </span>
+ </gl-badge>
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
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 1f3cc663848..8e9b8ef3e6f 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
@@ -130,11 +130,7 @@ export default {
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
- <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
- <template #separator>
- <gl-icon name="chevron-right" :size="8" />
- </template>
- </gl-breadcrumb>
+ <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs" />
<legacy-container :key="activePanel.name" :selector="activePanel.selector" />
</div>
</div>