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-07-20 18:40:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 18:40:28 +0300
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /app/assets/javascripts/vue_shared
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/deployment_instance.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/dom_element_listener.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_contact.fragment.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_organization.fragment.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql28
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql28
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue131
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue125
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/local_storage_sync.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/page_size_selector.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/slot_switch.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js46
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue33
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue3
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue20
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue53
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue3
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue5
52 files changed, 784 insertions, 155 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index 322ea64eb7e..f2c27cf611e 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -61,7 +61,7 @@ export default {
},
});
- return document.dispatchEvent(headerTodoEvent);
+ document.dispatchEvent(headerTodoEvent);
},
addToDo() {
this.isUpdating = true;
@@ -75,9 +75,10 @@ export default {
})
.then(({ data: { errors = [] } }) => {
if (errors[0]) {
- return this.throwError(errors[0]);
+ this.throwError(errors[0]);
+ return;
}
- return this.updateToDoCount(true);
+ this.updateToDoCount(true);
})
.catch(() => {
this.throwError();
@@ -98,9 +99,10 @@ export default {
})
.then(({ data: { errors = [] } }) => {
if (errors[0]) {
- return this.throwError(errors[0]);
+ this.throwError(errors[0]);
+ return;
}
- return this.updateToDoCount(false);
+ this.updateToDoCount(false);
})
.catch(() => {
this.throwError();
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
index 92817d5fa70..70cac061ca6 100644
--- 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
@@ -14,12 +14,12 @@ export default {
</script>
<template>
- <div>
+ <div class="color-item">
<span
- class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0"
+ class="dropdown-label-box color-item-color"
data-testid="color-item"
:style="{ backgroundColor: color }"
></span>
- <span class="hide-collapsed">{{ title }}</span>
+ <span class="color-item-text">{{ 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
index 6b79883d76b..a88a4ca5cb8 100644
--- 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
@@ -1,4 +1,5 @@
<script>
+import { isString } from 'lodash';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
@@ -52,13 +53,23 @@ export default {
required: false,
default: s__('ColorWidget|Assign epic color'),
},
+ defaultColor: {
+ type: Object,
+ required: false,
+ validator(value) {
+ return isString(value?.color) && isString(value?.title);
+ },
+ default() {
+ return {
+ color: '',
+ title: '',
+ };
+ },
+ },
},
data() {
return {
- issuableColor: {
- color: '',
- title: '',
- },
+ issuableColor: this.defaultColor,
colorUpdateInProgress: false,
oldIid: null,
sidebarExpandedOnClick: false,
@@ -106,9 +117,9 @@ export default {
methods: {
handleDropdownClose(color) {
if (this.iid !== '') {
- this.updateSelectedColor(this.getUpdateVariables(color));
+ this.updateSelectedColor(color);
} else {
- this.$emit('updateSelectedColor', color);
+ this.$emit('updateSelectedColor', { color });
}
this.collapseEditableItem();
@@ -129,13 +140,15 @@ export default {
color: color.color,
};
},
- updateSelectedColor(inputVariables) {
+ updateSelectedColor(color) {
this.colorUpdateInProgress = true;
+ const input = this.getUpdateVariables(color);
+
this.$apollo
.mutate({
mutation: updateEpicColorMutation,
- variables: { input: inputVariables },
+ variables: { input },
})
.then(({ data }) => {
if (data.updateIssuableColor?.errors?.length) {
@@ -144,7 +157,7 @@ export default {
this.$emit('updateSelectedColor', {
id: data.updateIssuableColor?.issuable?.id,
- color: data.updateIssuableColor?.issuable?.color,
+ color,
});
})
.catch((error) =>
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
index c70785abd1e..701ac71d755 100644
--- a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
@@ -1,4 +1,4 @@
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color');
@@ -7,7 +7,7 @@ export const DROPDOWN_VARIANT = {
Embedded: 'embedded',
};
-export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' };
+export const DEFAULT_COLOR = { title: s__('SuggestedColors|Blue'), color: '#1068bf' };
export const ISSUABLE_COLORS = [
DEFAULT_COLOR,
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
index 4eb1d3d08ca..84da6e1437e 100644
--- 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
@@ -1,11 +1,13 @@
<script>
import { GlDropdown } from '@gitlab/ui';
+import ColorItem from './color_item.vue';
import DropdownContentsColorView from './dropdown_contents_color_view.vue';
import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantSidebar } from './utils';
export default {
components: {
+ ColorItem,
DropdownContentsColorView,
DropdownHeader,
GlDropdown,
@@ -42,12 +44,15 @@ export default {
},
computed: {
buttonText() {
- if (!this.localSelectedColor?.title) {
+ if (!this.hasSelectedColor) {
return this.dropdownButtonText;
}
return this.localSelectedColor.title;
},
+ hasSelectedColor() {
+ return this.localSelectedColor?.title;
+ },
},
watch: {
localSelectedColor: {
@@ -91,7 +96,15 @@ export default {
</script>
<template>
- <gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide">
+ <gl-dropdown ref="dropdown" class="gl-w-full" @hide="handleDropdownHide">
+ <template #button-text>
+ <color-item
+ v-if="hasSelectedColor"
+ :color="localSelectedColor.color"
+ :title="localSelectedColor.title"
+ />
+ <span v-else data-testid="fallback-button-text">{{ buttonText }}</span>
+ </template>
<template #header>
<dropdown-header
ref="header"
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
index 62f4cf59c14..91906388049 100644
--- 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
@@ -36,8 +36,8 @@ export default {
</script>
<template>
- <gl-dropdown-form>
- <div>
+ <gl-dropdown-form class="js-colors-list">
+ <div data-testid="dropdown-content">
<gl-dropdown-item
v-for="color in colors"
:key="color.color"
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
index 4cba66eefd2..7ae803ebf4d 100644
--- 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
@@ -20,6 +20,11 @@ export default {
required: true,
},
},
+ computed: {
+ hasColor() {
+ return this.selectedColor.color !== '';
+ },
+ },
};
</script>
@@ -31,13 +36,18 @@ export default {
class="sidebar-collapsed-icon"
>
<gl-icon name="appearance" />
+ <color-item :color="selectedColor.color" :title="selectedColor.title" />
+ </div>
+
+ <span v-if="!hasColor" class="no-value hide-collapsed">
+ <slot></slot>
+ </span>
+ <template v-else>
<color-item
+ class="hide-collapsed"
: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" />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/deployment_instance.vue b/app/assets/javascripts/vue_shared/components/deployment_instance.vue
index 4aae86fc82b..1b907078cf9 100644
--- a/app/assets/javascripts/vue_shared/components/deployment_instance.vue
+++ b/app/assets/javascripts/vue_shared/components/deployment_instance.vue
@@ -13,8 +13,6 @@
* Mockup is https://gitlab.com/gitlab-org/gitlab/issues/35570
*/
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -23,7 +21,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
/**
* Represents the status of the pod. Each state is represented with a different
@@ -54,17 +51,11 @@ export default {
required: false,
default: '',
},
-
- logsPath: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
isLink() {
- return this.logsPath !== '' && this.podName !== '';
+ return this.podName !== '';
},
cssClass() {
@@ -74,12 +65,6 @@ export default {
link: this.isLink,
};
},
-
- computedLogPath() {
- return this.isLink && this.glFeatures.monitorLogging
- ? mergeUrlParams({ pod_name: this.podName }, this.logsPath)
- : null;
- },
},
};
</script>
@@ -88,7 +73,6 @@ export default {
v-gl-tooltip
:class="cssClass"
:title="tooltipText"
- :href="computedLogPath"
class="deployment-instance d-flex justify-content-center align-items-center"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dom_element_listener.vue b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
index ca427ed4897..b9608a26d91 100644
--- a/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
+++ b/app/assets/javascripts/vue_shared/components/dom_element_listener.vue
@@ -22,7 +22,7 @@ export default {
});
},
render() {
- return this.$slots.default;
+ return this.$scopedSlots.default?.();
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index a512eb687b7..a246eadb790 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -37,6 +37,7 @@ export default {
aria-expanded="false"
>
<gl-loading-icon v-show="isLoading" size="sm" :inline="true" />
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
<slot v-if="$slots.default"></slot>
<span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
<gl-icon
diff --git a/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue b/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue
index 5d0ed8b0821..1da84df022f 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown_keyboard_navigation.vue
@@ -75,7 +75,7 @@ export default {
},
},
render() {
- return this.$slots.default;
+ return this.$scopedSlots.default?.();
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_contact.fragment.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_contact.fragment.graphql
new file mode 100644
index 00000000000..38222e4e8c2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_contact.fragment.graphql
@@ -0,0 +1,6 @@
+fragment ContactFragment on CustomerRelationsContact {
+ id
+ firstName
+ lastName
+ email
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_organization.fragment.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_organization.fragment.graphql
new file mode 100644
index 00000000000..a7de3c7f7af
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/crm_organization.fragment.graphql
@@ -0,0 +1,4 @@
+fragment OrganizationFragment on CustomerRelationsOrganization {
+ id
+ name
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql
new file mode 100644
index 00000000000..647aaa0f7f8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql
@@ -0,0 +1,28 @@
+#import "./crm_contact.fragment.graphql"
+
+query searchCrmContacts(
+ $isProject: Boolean = false
+ $fullPath: ID!
+ $searchString: String
+ $searchIds: [CustomerRelationsContactID!]
+) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
+ contacts(search: $searchString, ids: $searchIds) {
+ nodes {
+ ...ContactFragment
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ id
+ group {
+ id
+ contacts(search: $searchString, ids: $searchIds) {
+ nodes {
+ ...ContactFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql
new file mode 100644
index 00000000000..c4f4663de45
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql
@@ -0,0 +1,28 @@
+#import "./crm_organization.fragment.graphql"
+
+query searchCrmOrganizations(
+ $isProject: Boolean = false
+ $fullPath: ID!
+ $searchString: String
+ $searchIds: [CustomerRelationsOrganizationID!]
+) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ id
+ organizations(search: $searchString, ids: $searchIds) {
+ nodes {
+ ...OrganizationFragment
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ id
+ group {
+ id
+ organizations(search: $searchString, ids: $searchIds) {
+ nodes {
+ ...OrganizationFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
new file mode 100644
index 00000000000..adfe0559b62
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+
+import { ITEM_TYPE } from '~/groups/constants';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import createFlash from '~/flash';
+import { isPositiveInteger } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql';
+
+import { DEFAULT_NONE_ANY } from '../constants';
+
+import BaseToken from './base_token.vue';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ contacts: this.config.initialContacts || [],
+ loading: false,
+ };
+ },
+ computed: {
+ defaultContacts() {
+ return this.config.defaultContacts || DEFAULT_NONE_ANY;
+ },
+ namespace() {
+ return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
+ },
+ },
+ methods: {
+ getActiveContact(contacts, data) {
+ return contacts.find((contact) => {
+ return `${this.formatContactId(contact)}` === data;
+ });
+ },
+ getContactName(contact) {
+ return `${contact.firstName} ${contact.lastName}`;
+ },
+ fetchContacts(searchTerm) {
+ let searchString = null;
+ let searchId = null;
+ if (isPositiveInteger(searchTerm)) {
+ searchId = this.formatContactGraphQLId(searchTerm);
+ } else {
+ searchString = searchTerm;
+ }
+
+ this.loading = true;
+
+ this.$apollo
+ .query({
+ query: searchCrmContactsQuery,
+ variables: {
+ fullPath: this.config.fullPath,
+ searchString,
+ searchIds: searchId ? [searchId] : null,
+ isProject: this.config.isProject,
+ },
+ })
+ .then(({ data }) => {
+ this.contacts = this.config.isProject
+ ? data[this.namespace]?.group.contacts.nodes
+ : data[this.namespace]?.contacts.nodes;
+ })
+ .catch(() =>
+ createFlash({
+ message: __('There was a problem fetching CRM contacts.'),
+ }),
+ )
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ formatContactId(contact) {
+ return `${getIdFromGraphQLId(contact.id)}`;
+ },
+ formatContactGraphQLId(id) {
+ return convertToGraphQLId('CustomerRelations::Contact', id);
+ },
+ },
+};
+</script>
+
+<template>
+ <base-token
+ :config="config"
+ :value="value"
+ :active="active"
+ :suggestions-loading="loading"
+ :suggestions="contacts"
+ :get-active-token-value="getActiveContact"
+ :default-suggestions="defaultContacts"
+ v-bind="$attrs"
+ @fetch-suggestions="fetchContacts"
+ v-on="$listeners"
+ >
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? getContactName(activeTokenValue) : inputValue }}
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion
+ v-for="contact in suggestions"
+ :key="formatContactId(contact)"
+ :value="formatContactId(contact)"
+ >
+ <div>
+ <div>{{ getContactName(contact) }}</div>
+ <div class="gl-font-sm">{{ contact.email }}</div>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
new file mode 100644
index 00000000000..e6ab944449e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -0,0 +1,125 @@
+<script>
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+
+import { ITEM_TYPE } from '~/groups/constants';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import createFlash from '~/flash';
+import { isPositiveInteger } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql';
+
+import { DEFAULT_NONE_ANY } from '../constants';
+
+import BaseToken from './base_token.vue';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ organizations: this.config.initialOrganizations || [],
+ loading: false,
+ };
+ },
+ computed: {
+ defaultOrganizations() {
+ return this.config.defaultOrganizations || DEFAULT_NONE_ANY;
+ },
+ namespace() {
+ return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
+ },
+ },
+ methods: {
+ getActiveOrganization(organizations, data) {
+ return organizations.find((organization) => {
+ return `${this.formatOrganizationId(organization)}` === data;
+ });
+ },
+ fetchOrganizations(searchTerm) {
+ let searchString = null;
+ let searchId = null;
+ if (isPositiveInteger(searchTerm)) {
+ searchId = this.formatOrganizationGraphQLId(searchTerm);
+ } else {
+ searchString = searchTerm;
+ }
+
+ this.loading = true;
+
+ this.$apollo
+ .query({
+ query: searchCrmOrganizationsQuery,
+ variables: {
+ fullPath: this.config.fullPath,
+ searchString,
+ searchIds: searchId ? [searchId] : null,
+ isProject: this.config.isProject,
+ },
+ })
+ .then(({ data }) => {
+ this.organizations = this.config.isProject
+ ? data[this.namespace]?.group.organizations.nodes
+ : data[this.namespace]?.organizations.nodes;
+ })
+ .catch(() =>
+ createFlash({
+ message: __('There was a problem fetching CRM organizations.'),
+ }),
+ )
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ formatOrganizationId(organization) {
+ return `${getIdFromGraphQLId(organization.id)}`;
+ },
+ formatOrganizationGraphQLId(id) {
+ return convertToGraphQLId('CustomerRelations::Organization', id);
+ },
+ },
+};
+</script>
+
+<template>
+ <base-token
+ :config="config"
+ :value="value"
+ :active="active"
+ :suggestions-loading="loading"
+ :suggestions="organizations"
+ :get-active-token-value="getActiveOrganization"
+ :default-suggestions="defaultOrganizations"
+ v-bind="$attrs"
+ @fetch-suggestions="fetchOrganizations"
+ v-on="$listeners"
+ >
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion
+ v-for="organization in suggestions"
+ :key="formatOrganizationId(organization)"
+ :value="formatOrganizationId(organization)"
+ >
+ {{ organization.name }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</template>
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 15d858b99b9..482a2964b4c 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
@@ -139,6 +139,7 @@ export default {
/>
</template>
</gl-form-input-group>
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
<template v-for="slot in Object.keys($slots)" #[slot]>
<slot :name="slot"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index f2abade8036..96f7427dda1 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -163,6 +163,7 @@ export default {
</template>
</section>
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
<section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
<slot></slot>
</section>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index c3f184446a8..1b89bd324c6 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -38,6 +38,7 @@ export default {
<template #default>
<div v-safe-html="options.content"></div>
</template>
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
<template v-for="slot in Object.keys($slots)" #[slot]>
<slot :name="slot"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/local_storage_sync.vue b/app/assets/javascripts/vue_shared/components/local_storage_sync.vue
index 4ece87310c7..96c779c5ce4 100644
--- a/app/assets/javascripts/vue_shared/components/local_storage_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/local_storage_sync.vue
@@ -100,7 +100,7 @@ export default {
},
},
render() {
- return this.$slots.default;
+ return this.$scopedSlots.default?.();
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 1f309a19b14..32b3a0e22c2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -248,7 +248,7 @@ export default {
labels: this.enableAutocomplete,
snippets: this.enableAutocomplete,
vulnerabilities: this.enableAutocomplete,
- contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete,
+ contacts: this.enableAutocomplete,
},
true,
);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 8a1b8363f19..7646a8718d6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -139,8 +139,8 @@ export default {
</script>
<template>
- <div class="md-suggestion-header border-bottom-0 mt-2">
- <div class="js-suggestion-diff-header font-weight-bold">
+ <div class="md-suggestion-header border-bottom-0 gl-mt-3">
+ <div class="js-suggestion-diff-header gl-font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
<gl-icon name="question-o" css-classes="link-highlight" />
@@ -151,13 +151,13 @@ export default {
</gl-badge>
<div
v-else-if="isApplying"
- class="d-flex align-items-center text-secondary"
+ class="gl-display-flex gl-align-items-center text-secondary"
data-qa-selector="applying_badge"
>
- <gl-loading-icon size="sm" class="d-flex-center mr-2" />
+ <gl-loading-icon size="sm" class="gl-align-items-center gl-justify-content-center gl-mr-3" />
<span>{{ applyingSuggestionsMessage }}</span>
</div>
- <div v-else-if="isLoggedIn" class="d-flex align-items-center">
+ <div v-else-if="isLoggedIn" class="gl-display-flex gl-align-items-center">
<div v-if="isBatched">
<gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 624dbcc6d8e..0cb4a5bc39f 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -16,17 +16,17 @@
* :note="{body: 'This is a note'}"
* />
*/
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlSafeHtmlDirective as SafeHtml, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
directives: { SafeHtml },
components: {
- userAvatarLink,
+ GlAvatarLink,
+ GlAvatar,
TimelineEntryItem,
},
props: {
@@ -55,7 +55,10 @@ export default {
return 24;
}
- return 40;
+ return {
+ default: 24,
+ md: 32,
+ };
},
},
};
@@ -64,11 +67,14 @@ export default {
<template>
<timeline-entry-item class="note note-wrapper being-posted fade-in-half">
<div class="timeline-icon">
- <user-avatar-link
- :link-href="getUserData.path"
- :img-src="getUserData.avatar_url"
- :img-size="avatarSize"
- />
+ <gl-avatar-link class="gl-mr-3" :href="getUserData.path">
+ <gl-avatar
+ :src="getUserData.avatar_url"
+ :entity-name="getUserData.username"
+ :alt="getUserData.name"
+ :size="avatarSize"
+ />
+ </gl-avatar-link>
</div>
<div ref="note" :class="{ discussion: !note.individual_note }" class="timeline-content">
<div class="note-header">
diff --git a/app/assets/javascripts/vue_shared/components/page_size_selector.vue b/app/assets/javascripts/vue_shared/components/page_size_selector.vue
new file mode 100644
index 00000000000..9783946b786
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/page_size_selector.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export const PAGE_SIZES = [20, 50, 100];
+
+export default {
+ components: { GlDropdown, GlDropdownItem },
+ props: {
+ value: {
+ type: Number,
+ required: true,
+ },
+ },
+ methods: {
+ emitInput(pageSize) {
+ this.$emit('input', pageSize);
+ },
+ getPageSizeText(pageSize) {
+ return sprintf(s__('SecurityReports|Show %{pageSize} items'), { pageSize });
+ },
+ },
+ PAGE_SIZES,
+};
+</script>
+
+<template>
+ <gl-dropdown :text="getPageSizeText(value)" right menu-class="gl-w-auto! gl-min-w-0">
+ <gl-dropdown-item
+ v-for="pageSize in $options.PAGE_SIZES"
+ :key="pageSize"
+ @click="emitInput(pageSize)"
+ >
+ <span class="gl-white-space-nowrap">{{ getPageSizeText(pageSize) }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index a8b250f2041..5516c9943b8 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -38,6 +38,7 @@ export default {
},
},
mounted() {
+ // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
this.detailsSlots = Object.keys(this.$slots).filter((k) => k.startsWith('details-'));
},
methods: {
@@ -55,7 +56,7 @@ export default {
>
<div class="gl-display-flex gl-align-items-center gl-py-3">
<div
- v-if="$slots['left-action']"
+ v-if="$slots['left-action'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */"
class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
@@ -65,7 +66,9 @@ 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']"
+ v-if="
+ $slots['left-primary'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
+ "
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"></slot>
@@ -79,7 +82,11 @@ export default {
/>
</div>
<div
- v-if="$slots['left-secondary']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'left-secondary'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
>
<slot name="left-secondary"></slot>
@@ -89,13 +96,21 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
<div
- v-if="$slots['right-primary']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'right-primary'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<slot name="right-primary"></slot>
</div>
<div
- v-if="$slots['right-secondary']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'right-secondary'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<slot name="right-secondary"></slot>
@@ -103,7 +118,9 @@ export default {
</div>
</div>
<div
- v-if="$slots['right-action']"
+ v-if="
+ $slots['right-action'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
+ "
class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
index fc0976b0792..ad979387596 100644
--- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -47,6 +47,7 @@ export default {
methods: {
recalculateMetadataSlots() {
const METADATA_PREFIX = 'metadata-';
+ // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
const metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX));
if (!isEqual(metadataSlots, this.metadataSlots)) {
@@ -76,7 +77,9 @@ export default {
</h2>
<div
- v-if="$slots['sub-header']"
+ v-if="
+ $slots['sub-header'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
+ "
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<slot name="sub-header"></slot>
@@ -107,6 +110,7 @@ export default {
</template>
</div>
</div>
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
<div v-if="$slots['right-actions']" class="gl-mt-3">
<slot name="right-actions"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
new file mode 100644
index 00000000000..1c08433ee78
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
@@ -0,0 +1 @@
+// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
new file mode 100644
index 00000000000..1c08433ee78
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
@@ -0,0 +1 @@
+// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
new file mode 100644
index 00000000000..1c08433ee78
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
@@ -0,0 +1 @@
+// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
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 5471cda0cc5..0127df730b8 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
@@ -193,7 +193,7 @@ export default {
<gl-dropdown
ref="dropdown"
:text="buttonText"
- class="gl-w-full gl-mt-2"
+ class="gl-w-full"
data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
diff --git a/app/assets/javascripts/vue_shared/components/slot_switch.vue b/app/assets/javascripts/vue_shared/components/slot_switch.vue
index 67726f01744..641b09e0982 100644
--- a/app/assets/javascripts/vue_shared/components/slot_switch.vue
+++ b/app/assets/javascripts/vue_shared/components/slot_switch.vue
@@ -20,6 +20,7 @@ export default {
computed: {
allSlotNames() {
+ // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
return Object.keys(this.$slots);
},
},
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 0d78530d878..3ac35abcf3a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -45,6 +45,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
haskell: 'haskell',
haxe: 'haxe',
http: 'http',
+ html: 'xml',
hylang: 'hy',
ini: 'ini',
isbl: 'isbl',
@@ -90,7 +91,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
scala: 'scala',
scheme: 'scheme',
scss: 'scss',
- shell: 'shell',
+ shell: 'sh',
smalltalk: 'smalltalk',
sml: 'sml',
sqf: 'sqf',
@@ -112,6 +113,12 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
yaml: 'yaml',
};
+export const EVENT_ACTION = 'view_source';
+
+export const EVENT_LABEL_VIEWER = 'source_viewer';
+
+export const EVENT_LABEL_FALLBACK = 'legacy_fallback';
+
export const LINES_PER_CHUNK = 70;
export const BIDI_CHARS = [
@@ -138,3 +145,5 @@ export const BIDI_CHAR_TOOLTIP = __(
export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
+
+export const NPM_URL = 'https://npmjs.com/package';
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
index c9f7e5508be..5d24a3d110b 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -1,5 +1,6 @@
import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
import wrapComments from './wrap_comments';
+import linkDependencies from './link_dependencies';
/**
* Registers our plugins for Highlight.js
@@ -8,6 +9,9 @@ import wrapComments from './wrap_comments';
*
* @param {Object} hljs - the Highlight.js instance.
*/
-export const registerPlugins = (hljs) => {
+export const registerPlugins = (hljs, fileType, rawContent) => {
hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ hljs.addPlugin({
+ [HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
+ });
};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
new file mode 100644
index 00000000000..5b7650c56ae
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -0,0 +1,25 @@
+import packageJsonLinker from './utils/package_json_linker';
+
+const DEPENDENCY_LINKERS = {
+ package_json: packageJsonLinker,
+};
+
+/**
+ * Highlight.js plugin for generating links to dependencies when viewing dependency files.
+ *
+ * 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
+ * @param {String} fileType - a string containing the file type
+ * @param {String} rawContent - raw (non-highlighted) file content
+ */
+export default (result, fileType, rawContent) => {
+ if (DEPENDENCY_LINKERS[fileType]) {
+ try {
+ // eslint-disable-next-line no-param-reassign
+ result.value = DEPENDENCY_LINKERS[fileType](result, rawContent);
+ } catch (e) {
+ // Shallowed (do nothing), in this case the original unlinked dependencies will be rendered.
+ }
+ }
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
new file mode 100644
index 00000000000..56ad55ef553
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -0,0 +1,15 @@
+import { escape } from 'lodash';
+import { setAttributes } from '~/lib/utils/dom_utils';
+
+export const createLink = (href, innerText) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const rel = 'nofollow noreferrer noopener';
+ const link = document.createElement('a');
+
+ setAttributes(link, { href: escape(href), rel });
+ link.innerText = escape(innerText);
+
+ return link.outerHTML;
+};
+
+export const generateHLJSOpenTag = (type) => `<span class="hljs-${escape(type)}">&quot;`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
new file mode 100644
index 00000000000..d013d077ba3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
@@ -0,0 +1,46 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+import { NPM_URL } from '../../constants';
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const attrOpenTag = generateHLJSOpenTag('attr');
+const stringOpenTag = generateHLJSOpenTag('string');
+const closeTag = '&quot;</span>';
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;@babel/core&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.18.5&quot;</span>
+ * Group 1: @babel/core
+ * Group 2: ^7.18.5
+ */
+ `${attrOpenTag}(.*)${closeTag}.*${stringOpenTag}(.*[0-9].*)(${closeTag})`,
+ 'gm',
+);
+
+const handleReplace = (original, packageName, version, dependenciesToLink) => {
+ const href = joinPaths(NPM_URL, packageName);
+ const packageLink = createLink(href, packageName);
+ const versionLink = createLink(href, version);
+ const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
+ const dependencyToLink = dependenciesToLink[packageName];
+
+ if (dependencyToLink && dependencyToLink === version) {
+ return `${attrOpenTag}${packageLink}${closeAndOpenTag}${versionLink}${closeTag}`;
+ }
+
+ return original;
+};
+
+export default (result, raw) => {
+ const { dependencies, devDependencies, peerDependencies, optionalDependencies } = JSON.parse(raw);
+
+ const dependenciesToLink = {
+ ...dependencies,
+ ...devDependencies,
+ ...peerDependencies,
+ ...optionalDependencies,
+ };
+
+ return result.value.replace(DEPENDENCY_REGEX, (original, packageName, version) =>
+ handleReplace(original, packageName, version, dependenciesToLink),
+ );
+};
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 f819a9e5be2..1bdae40332f 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
@@ -3,7 +3,14 @@ import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui';
import LineHighlighter from '~/blob/line_highlighter';
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 Tracking from '~/tracking';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+} from './constants';
import Chunk from './components/chunk.vue';
import { registerPlugins } from './plugins/index';
@@ -23,6 +30,7 @@ export default {
directives: {
SafeHtml: GlSafeHtmlDirective,
},
+ mixins: [Tracking.mixin()],
props: {
blob: {
type: Object,
@@ -49,8 +57,22 @@ export default {
lineNumbers() {
return this.splitContent.length;
},
+ unsupportedLanguage() {
+ const supportedLanguages = Object.keys(languageLoader);
+ return (
+ !supportedLanguages.includes(this.language) &&
+ !supportedLanguages.includes(this.blob.language)
+ );
+ },
},
async created() {
+ this.trackEvent(EVENT_LABEL_VIEWER);
+
+ if (this.unsupportedLanguage) {
+ this.handleUnsupportedLanguage();
+ return;
+ }
+
this.generateFirstChunk();
this.hljs = await this.loadHighlightJS();
@@ -70,6 +92,13 @@ export default {
});
},
methods: {
+ trackEvent(label) {
+ this.track(EVENT_ACTION, { label, property: this.blob.language });
+ },
+ handleUnsupportedLanguage() {
+ this.trackEvent(EVENT_LABEL_FALLBACK);
+ this.$emit('error');
+ },
generateFirstChunk() {
const lines = this.splitContent.splice(0, LINES_PER_CHUNK);
this.firstChunk = this.createChunk(lines);
@@ -112,7 +141,7 @@ export default {
let detectedLanguage = language;
let highlightedContent;
if (this.hljs) {
- registerPlugins(this.hljs);
+ registerPlugins(this.hljs, this.blob.fileType, this.content);
if (!detectedLanguage) {
const hljsHighlightAuto = this.hljs.highlightAuto(content);
highlightedContent = hljsHighlightAuto.value;
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 20a666509a4..779a2ab5461 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
@@ -1,7 +1,6 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
export default {
name: 'UsageBanner',
@@ -15,13 +14,6 @@ export default {
default: false,
},
},
- i18n: {
- dependencyProxy: s__('UsageQuota|Dependency proxy'),
- storageUsed: s__('UsageQuota|Storage used'),
- dependencyProxyMessage: s__(
- 'UsageQuota|Local proxy used for frequently-accessed upstream Docker images. %{linkStart}More information%{linkEnd}',
- ),
- },
storageUsageQuotaHelpPage: helpPagePath('user/usage_quotas'),
};
</script>
@@ -33,13 +25,21 @@ 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']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $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"
>
<slot name="left-primary-text"></slot>
</div>
<div
- v-if="$slots['left-secondary-text']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'left-secondary-text'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1 gl-w-70p gl-md-max-w-70p"
>
<slot name="left-secondary-text"></slot>
@@ -49,13 +49,21 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
<div
- v-if="$slots['right-primary-text']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'right-primary-text'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<slot name="right-primary-text"></slot>
</div>
<div
- v-if="$slots['right-secondary-text']"
+ v-if="
+ /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
+ 'right-secondary-text'
+ ]
+ "
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<slot v-if="!loading" name="right-secondary-text"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
index c58a5357883..707b0bbec67 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
@@ -96,7 +96,10 @@ export default {
/>
<gl-tooltip
- v-if="tooltipText || $slots.default"
+ v-if="
+ tooltipText ||
+ $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
+ "
:target="() => $refs.userAvatar.$el"
:placement="tooltipPlacement"
boundary="window"
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
index 15ba8e3b39b..6e8c200d5c3 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
@@ -100,7 +100,10 @@ export default {
class="avatar"
/>
<gl-tooltip
- v-if="tooltipText || $slots.default"
+ v-if="
+ tooltipText ||
+ $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
+ "
:target="() => $refs.userAvatarImage"
:placement="tooltipPlacement"
boundary="window"
diff --git a/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue b/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue
index 121c3bd94ef..ab5ddbc8af8 100644
--- a/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue
+++ b/app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue
@@ -56,7 +56,13 @@ import getUserCalloutsQuery from '~/graphql_shared/queries/get_user_callouts.que
* - shouldShowCallout: boolean
* - A combination of the above which should cover 95% of use cases: `true`
* if the query has loaded without error, and the user is logged in, and
- * the callout has not been dismissed yet; `false` otherwise.
+ * the callout has not been dismissed yet; `false` otherwise
+ *
+ * The component emits a `queryResult` event when the GraphQL query
+ * completes. The payload is a combination of the ApolloQueryResult object and
+ * this component's `slotProps` computed property. This is useful for things
+ * like cleaning up/unmounting the component if the callout shouldn't be
+ * displayed.
*/
export default {
name: 'UserCalloutDismisser',
@@ -86,6 +92,9 @@ export default {
update(data) {
return data?.currentUser;
},
+ result(data) {
+ this.$emit('queryResult', { ...data, ...this.slotProps });
+ },
error(err) {
this.queryError = err;
},
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 768cd005727..a0d8ca117a4 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
@@ -7,13 +7,13 @@ import {
GlSafeHtmlDirective,
GlSprintf,
GlButton,
+ GlAvatarLabeled,
} from '@gitlab/ui';
import { __ } from '~/locale';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '~/emoji';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
-import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import { isUserBusy } from '~/set_status_modal/utils';
import { USER_POPOVER_DELAY } from './constants';
const MAX_SKELETON_LINES = 4;
@@ -22,15 +22,17 @@ export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
USER_POPOVER_DELAY,
+ i18n: {
+ busy: __('Busy'),
+ },
components: {
GlIcon,
GlLink,
GlPopover,
GlSkeletonLoader,
- UserAvatarImage,
- UserNameWithStatus,
GlSprintf,
GlButton,
+ GlAvatarLabeled,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@@ -95,6 +97,15 @@ export default {
toggleFollowButtonVariant() {
return this.user?.isFollowed ? 'default' : 'confirm';
},
+ hasPronouns() {
+ return Boolean(this.user?.pronouns?.trim());
+ },
+ isBusy() {
+ return isUserBusy(this.availabilityStatus);
+ },
+ username() {
+ return `@${this.user?.username}`;
+ },
},
methods: {
async toggleFollow() {
@@ -149,43 +160,46 @@ export default {
:placement="placement"
boundary="viewport"
triggers="hover focus manual"
+ data-testid="user-popover"
>
- <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 class="gl-mb-3">
+ <div v-if="userIsLoading" class="gl-w-20">
+ <gl-skeleton-loader :width="160" :height="64">
+ <rect x="70" y="19" rx="3" ry="3" width="88" height="9" />
+ <rect x="70" y="36" rx="3" ry="3" width="64" height="8" />
+ <circle cx="32" cy="32" r="32" />
+ </gl-skeleton-loader>
</div>
- <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"
- preserve-aspect-ratio="none"
- equal-width-lines
- :height="52"
- />
- </template>
- <template v-else>
- <div>
- <h5 class="gl-m-0">
- <user-name-with-status
- :name="user.name"
- :availability="availabilityStatus"
- :pronouns="user.pronouns"
- />
- </h5>
- <span class="gl-text-gray-500">@{{ user.username }}</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>
+ <gl-avatar-labeled
+ v-else
+ :size="64"
+ :src="user.avatarUrl"
+ :label="user.name"
+ :sub-label="username"
+ >
+ <gl-button
+ v-if="shouldRenderToggleFollowButton"
+ class="gl-mt-3 gl-align-self-start"
+ :variant="toggleFollowButtonVariant"
+ :loading="toggleFollowLoading"
+ size="small"
+ data-testid="toggle-follow-button"
+ @click="toggleFollow"
+ >{{ toggleFollowButtonText }}</gl-button
+ >
+
+ <template #meta>
+ <span
+ v-if="hasPronouns"
+ class="gl-text-gray-500 gl-font-sm gl-font-weight-normal gl-p-1"
+ data-testid="user-popover-pronouns"
+ >({{ user.pronouns }})</span
+ >
+ <span v-if="isBusy" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal gl-p-1"
+ >({{ $options.i18n.busy }})</span
+ >
</template>
- </div>
+ </gl-avatar-labeled>
</div>
<div class="gl-mt-2 gl-w-full gl-word-break-word">
<template v-if="userIsLoading">
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
index eff39e2fb89..4ef9bc07b1c 100644
--- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
+++ b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
@@ -15,7 +15,7 @@ export default {
},
},
render() {
- return this.$slots.default;
+ return this.$scopedSlots.default?.();
},
};
</script>
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 89eecea5239..25799171905 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
@@ -81,7 +81,8 @@ export default {
ref="textarea"
v-model="issuableDescription"
dir="auto"
- class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ class="note-textarea rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ data-qa-selector="issuable_form_description_field"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
></textarea>
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 a9f8caa3e1f..b616b390032 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
@@ -86,7 +86,18 @@ export default {
createdAt() {
return getTimeago().format(this.issuable.createdAt);
},
- updatedAt() {
+ timestamp() {
+ if (this.issuable.state === 'closed' && this.issuable.closedAt) {
+ return this.issuable.closedAt;
+ }
+ return this.issuable.updatedAt;
+ },
+ formattedTimestamp() {
+ if (this.issuable.state === 'closed' && this.issuable.closedAt) {
+ return sprintf(__('closed %{timeago}'), {
+ timeago: getTimeago().format(this.issuable.closedAt),
+ });
+ }
return sprintf(__('updated %{timeAgo}'), {
timeAgo: getTimeago().format(this.issuable.updatedAt),
});
@@ -134,6 +145,7 @@ export default {
},
methods: {
hasSlotContents(slotName) {
+ // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
return Boolean(this.$slots[slotName]);
},
scopedLabel(label) {
@@ -311,10 +323,10 @@ export default {
<div
v-gl-tooltip.bottom
class="gl-text-gray-500 gl-display-none gl-sm-display-inline-block"
- :title="tooltipTitle(issuable.updatedAt)"
- data-testid="issuable-updated-at"
+ :title="tooltipTitle(timestamp)"
+ data-testid="issuable-timestamp"
>
- {{ updatedAt }}
+ {{ formattedTimestamp }}
</div>
</div>
</li>
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 8fbf0bb10a0..189bbb56432 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,11 +1,13 @@
<script>
import { GlAlert, GlKeysetPagination, GlSkeletonLoader, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { DEFAULT_SKELETON_COUNT } from '../constants';
+import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants';
import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
import IssuableItem from './issuable_item.vue';
import IssuableTabs from './issuable_tabs.vue';
@@ -29,6 +31,8 @@ export default {
IssuableBulkEditSidebar,
GlPagination,
VueDraggable,
+ PageSizeSelector,
+ LocalStorageSync,
},
props: {
namespace: {
@@ -173,6 +177,11 @@ export default {
required: false,
default: false,
},
+ showPageSizeChangeControls: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -262,7 +271,11 @@ export default {
handleVueDraggableUpdate({ newIndex, oldIndex }) {
this.$emit('reorder', { newIndex, oldIndex });
},
+ handlePageSizeChange(newPageSize) {
+ this.$emit('page-size-change', newPageSize);
+ },
},
+ PAGE_SIZE_STORAGE_KEY,
};
</script>
@@ -353,24 +366,38 @@ export default {
<slot v-else name="empty-state"></slot>
</template>
- <div v-if="showPaginationControls && useKeysetPagination" class="gl-text-center gl-mt-3">
+ <div class="gl-text-center gl-mt-6 gl-relative">
<gl-keyset-pagination
+ v-if="showPaginationControls && useKeysetPagination"
:has-next-page="hasNextPage"
:has-previous-page="hasPreviousPage"
@next="$emit('next-page')"
@prev="$emit('previous-page')"
/>
+ <gl-pagination
+ v-else-if="showPaginationControls"
+ :per-page="defaultPageSize"
+ :total-items="totalItems"
+ :value="currentPage"
+ :prev-page="previousPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="$emit('page-change', $event)"
+ />
+
+ <local-storage-sync
+ v-if="showPageSizeChangeControls"
+ :value="defaultPageSize"
+ :storage-key="$options.PAGE_SIZE_STORAGE_KEY"
+ @input="handlePageSizeChange"
+ >
+ <page-size-selector
+ :value="defaultPageSize"
+ class="gl-absolute gl-right-0"
+ @input="handlePageSizeChange"
+ />
+ </local-storage-sync>
</div>
- <gl-pagination
- v-else-if="showPaginationControls"
- :per-page="defaultPageSize"
- :total-items="totalItems"
- :value="currentPage"
- :prev-page="previousPage"
- :next-page="nextPage"
- align="center"
- class="gl-pagination gl-mt-3"
- @input="$emit('page-change', $event)"
- />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index be9afc0610d..507f333a34e 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
@@ -56,3 +56,5 @@ export const IssuableTypes = {
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_SKELETON_COUNT = 5;
+
+export const PAGE_SIZE_STORAGE_KEY = 'issuable_list_page_size';
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
index f57b5b2deb4..d4e9120ff17 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
@@ -37,7 +37,11 @@ export default {
</script>
<template>
- <div class="description" :class="{ 'js-task-list-container': canEdit && enableTaskList }">
+ <div
+ class="description"
+ :class="{ 'js-task-list-container': canEdit && enableTaskList }"
+ data-qa-selector="description_content"
+ >
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
<textarea
v-if="issuable.description && enableTaskList"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
index 33dca3e9332..2fc1f935501 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
@@ -123,7 +123,6 @@ export default {
:placeholder="__('Title')"
:aria-label="__('Title')"
:autofocus="true"
- class="qa-title-input"
@keydown="handleKeydown($event, 'title')"
/>
</gl-form-group>
@@ -149,7 +148,7 @@ export default {
:data-supports-quick-actions="enableAutocomplete"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
- class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
@keydown="handleKeydown($event, 'description')"
></textarea>
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 f035795a045..cdc5903b934 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
@@ -112,7 +112,7 @@ export default {
<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>
- <div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block">
+ <div class="issuable-meta gl-display-flex! gl-align-items-center">
<div v-if="blocked || confidential" class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
<gl-icon name="lock" :aria-label="__('Blocked')" />
@@ -139,13 +139,15 @@ export default {
:size="24"
:src="author.avatarUrl"
:label="author.name"
- class="d-none d-sm-inline-flex gl-mx-1"
+ :class="[{ 'gl-display-none': !isAuthorExternal }, 'gl-sm-display-inline-flex gl-mx-1']"
>
<template #meta>
- <gl-icon v-if="isAuthorExternal" name="external-link" />
+ <gl-icon v-if="isAuthorExternal" name="external-link" class="gl-ml-1" />
</template>
</gl-avatar-labeled>
- <strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
+ <strong v-if="author.username" class="author gl-display-inline gl-sm-display-none!"
+ >@{{ author.username }}</strong
+ >
</gl-avatar-link>
<span
v-if="taskCompletionStatus && hasTasks"
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 3d7c71ce974..35124bd15d2 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
@@ -64,8 +64,9 @@ export default {
<div class="title-container">
<h1
v-safe-html="issuable.titleHtml || issuable.title"
- class="title qa-title gl-font-size-h-display"
+ class="title gl-font-size-h-display"
dir="auto"
+ data-qa-selector="title_content"
data-testid="title"
></h1>
<gl-button
@@ -74,7 +75,7 @@ export default {
:title="$options.i18n.editTitleAndDescription"
:aria-label="$options.i18n.editTitleAndDescription"
icon="pencil"
- class="btn-edit js-issuable-edit qa-edit-button"
+ class="btn-edit js-issuable-edit"
@click="$emit('edit-issuable', $event)"
/>
</div>