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:
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js31
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue (renamed from app/assets/javascripts/vue_shared/components/ci_icon.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/keep_alive_slots.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue513
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header_divider.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/details_row.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js36
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_access_role_badge.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue7
-rw-r--r--app/assets/javascripts/vue_shared/global_search/constants.js13
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue55
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js4
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue8
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue3
59 files changed, 826 insertions, 600 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 93581dbbd40..655a16dea01 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -36,7 +36,7 @@ export default {
<li
:id="noteAnchorId"
class="timeline-entry note system-note note-wrapper gl-p-0!"
- data-qa-selector="alert_system_note_container"
+ data-testid="alert-system-note-container"
>
<div class="gl-display-inline-flex gl-align-items-center gl-relative">
<div
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 59f03b41144..3c19df9c196 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -94,14 +94,12 @@ export default {
return awardList.some((award) => award.user.id === this.currentUserId);
},
createAwardList(name, list) {
- const url = list.length ? list[0].url : null;
-
return {
name,
list,
title: this.getAwardListTitle(list, name),
classes: this.getAwardClassBindings(list),
- html: glEmojiTag(name, { url }),
+ html: glEmojiTag(name),
};
},
getAwardListTitle(awardsList, name) {
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 2a47e96b2e2..5a807d10f24 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -82,8 +82,6 @@ export default {
:title="tooltipTitle"
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon d-inline-block"
- data-qa-selector="changed_file_icon_content"
- :data-qa-title="tooltipTitle"
>
<gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js
new file mode 100644
index 00000000000..66012cefeaf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js
@@ -0,0 +1,31 @@
+import CiIcon from './ci_icon.vue';
+
+export default {
+ component: CiIcon,
+ title: 'vue_shared/ci_icon',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { CiIcon },
+ props: Object.keys(argTypes),
+ template: '<ci-icon v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ status: {
+ icon: 'status_success',
+ text: 'Success',
+ detailsPath: 'https://gitab.com/',
+ },
+};
+
+export const WithText = Template.bind({});
+WithText.args = {
+ status: {
+ icon: 'status_success',
+ text: 'Success',
+ detailsPath: 'https://gitab.com/',
+ },
+ showStatusText: true,
+};
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue
index a2b6b4642c9..a2b6b4642c9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index 2bdc8a174d0..e12e06a2454 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -36,11 +36,6 @@ export default {
required: false,
default: 'confirm-danger-button',
},
- buttonQaSelector: {
- type: String,
- required: false,
- default: null,
- },
buttonVariant: {
type: String,
required: false,
@@ -58,7 +53,6 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
- :data-qa-selector="buttonQaSelector"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index a1ef1f30ebb..5019ab901fd 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -72,7 +72,7 @@ export default {
attributes: {
variant: 'danger',
disabled: !this.isValid,
- 'data-qa-selector': 'confirm_danger_modal_button',
+ 'data-testid': 'confirm-danger-modal-button',
},
};
},
@@ -133,8 +133,7 @@ export default {
id="confirm_name_input"
v-model="confirmationPhrase"
class="form-control"
- data-qa-selector="confirm_danger_field"
- data-testid="confirm-danger-input"
+ data-testid="confirm-danger-field"
type="text"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index 1370f7b2a8c..7b9ecc18ce1 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -54,7 +54,7 @@ export default {
</script>
<template>
- <div class="preview-container" data-qa-selector="preview_container">
+ <div class="preview-container">
<image-viewer v-if="type === 'image'" :path="path" :file-size="fileSize" />
<markdown-viewer
v-if="type === 'markdown'"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index f28a2801bc0..332424c70ac 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -41,14 +41,7 @@ export default {
{{ fileName }}
<template v-if="fileSize > 0"> ({{ fileSizeReadable }}) </template>
</p>
- <a
- :href="path"
- class="btn btn-default"
- rel="nofollow"
- :download="fileName"
- target="_blank"
- data-qa-selector="download_button"
- >
+ <a :href="path" class="btn btn-default" rel="nofollow" :download="fileName" target="_blank">
<gl-icon :size="16" name="download" class="float-left gl-mr-3" />
{{ __('Download') }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 04ab0fd00aa..9742118cd5f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -88,7 +88,7 @@ export default {
</script>
<template>
- <div data-testid="image-viewer" data-qa-selector="image_viewer_container">
+ <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="safePath" @load="onImgLoad" />
<slot
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 1a215454ab6..ea787bfe63e 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -57,7 +57,7 @@ export default {
type: Function,
required: true,
},
- fetchInitialSelectionText: {
+ fetchInitialSelection: {
type: Function,
required: false,
default: null,
@@ -77,35 +77,23 @@ export default {
searchString: '',
items: [],
page: 1,
- selectedValue: null,
- selectedText: null,
+ selected: this.initialSelection || '',
+ initialSelectedItem: {},
errorMessage: '',
};
},
computed: {
- selected: {
- set(value) {
- this.selectedValue = value;
- this.selectedText =
- value === null ? null : this.items.find((item) => item.value === value).text;
- this.$emit('input', {
- value: this.selectedValue,
- text: this.selectedText,
- });
- },
- get() {
- return this.selectedValue;
- },
+ selectedItem() {
+ const item = this.items.find(({ value }) => value === this.selected);
+
+ return item || this.initialSelectedItem;
},
toggleText() {
- return this.selectedText ?? this.defaultToggleText;
+ return this.selectedItem?.text ?? this.defaultToggleText;
},
resetButtonLabel() {
return this.clearable ? RESET_LABEL : '';
},
- inputValue() {
- return this.selectedValue ? this.selectedValue : '';
- },
isSearchQueryTooShort() {
return this.searchString && this.searchString.length < MINIMUM_QUERY_LENGTH;
},
@@ -115,8 +103,13 @@ export default {
: this.$options.i18n.noResultsText;
},
},
+ watch: {
+ selected() {
+ this.$emit('input', this.selectedItem);
+ },
+ },
created() {
- this.fetchInitialSelection();
+ this.getInitialSelection();
},
methods: {
search: debounce(function debouncedSearch(searchString) {
@@ -148,23 +141,20 @@ export default {
this.searching = false;
this.infiniteScrollLoading = false;
},
- async fetchInitialSelection() {
+ async getInitialSelection() {
if (!this.initialSelection) {
this.pristine = false;
return;
}
- if (!this.fetchInitialSelectionText) {
+ if (!this.fetchInitialSelection) {
throw new Error(
'`initialSelection` is provided but lacks `fetchInitialSelectionText` to retrieve the corresponding text',
);
}
this.searching = true;
- const name = await this.fetchInitialSelectionText(this.initialSelection);
-
- this.selectedValue = this.initialSelection;
- this.selectedText = name;
+ this.initialSelectedItem = await this.fetchInitialSelection(this.initialSelection);
this.pristine = false;
this.searching = false;
},
@@ -218,6 +208,6 @@ export default {
<slot name="list-item" :item="item"></slot>
</template>
</gl-collapsible-listbox>
- <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
+ <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="selected" />
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index 8a338551fbe..da42c017541 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -76,11 +76,7 @@ export default {
try {
const url = groupsPath(this.groupsFilter, this.parentGroupID);
const { data = [], headers } = await axios.get(url, { params });
- groups = data.map((group) => ({
- ...group,
- text: group.full_name,
- value: String(group.id),
- }));
+ groups = data.map((group) => this.mapGroupData(group));
totalPages = parseIntPagination(normalizeHeaders(headers)).totalPages;
} catch (error) {
@@ -88,15 +84,19 @@ export default {
}
return { items: groups, totalPages };
},
- async fetchGroupName(groupId) {
- let groupName = '';
+ async fetchInitialGroup(groupId) {
try {
const group = await Api.group(groupId);
- groupName = group.full_name;
+
+ return this.mapGroupData(group);
} catch (error) {
this.handleError({ message: FETCH_GROUP_ERROR, error });
+
+ return {};
}
- return groupName;
+ },
+ mapGroupData(group) {
+ return { ...group, text: group.full_name, value: String(group.id) };
},
handleError({ message, error }) {
Sentry.captureException(error);
@@ -123,7 +123,7 @@ export default {
:header-text="$options.i18n.selectGroup"
:default-toggle-text="$options.i18n.toggleText"
:fetch-items="fetchGroups"
- :fetch-initial-selection-text="fetchGroupName"
+ :fetch-initial-selection="fetchInitialGroup"
v-on="$listeners"
>
<template #error>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
index d068d86d95b..9f4671abbb1 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
@@ -1,10 +1,11 @@
<script>
import { GlAlert } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import getCurrentUserOrganizationsQuery from '~/organizations/shared/graphql/queries/organizations.query.graphql';
import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
+import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
+import { DEFAULT_PER_PAGE } from '~/api';
import {
ORGANIZATION_TOGGLE_TEXT,
ORGANIZATION_HEADER_TEXT,
@@ -62,54 +63,60 @@ export default {
data() {
return {
errorMessage: '',
+ endCursor: null,
};
},
methods: {
- async fetchOrganizations() {
+ async fetchOrganizations(search, page = 1) {
+ if (page === 1) {
+ this.endCursor = null;
+ }
+
try {
- const {
- data: {
- currentUser: {
- organizations: { nodes },
- },
- },
- } = await this.$apollo.query({
+ const response = await this.$apollo.query({
query: getCurrentUserOrganizationsQuery,
- // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/433954.
+ variables: { after: this.endCursor, first: DEFAULT_PER_PAGE },
});
+ const { nodes, pageInfo } = response.data.currentUser.organizations;
+ this.endCursor = pageInfo.endCursor;
return {
- items: nodes.map((organization) => ({
- text: organization.name,
- value: getIdFromGraphQLId(organization.id),
- })),
- // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
- totalPages: 1,
+ items: nodes.map((organization) => this.mapOrganizationData(organization)),
+ // `EntitySelect` expects a `totalPages` key but GraphQL requests don't provide this data
+ // because it uses keyset pagination. Since the dropdown uses infinite scroll it
+ // only needs to know if there is a next page. We pass `page + 1` if there is a next page,
+ // otherwise we just set this to the current page.
+ totalPages: pageInfo.hasNextPage ? page + 1 : page,
};
} catch (error) {
+ this.endCursor = null;
this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
return { items: [], totalPages: 0 };
}
},
- async fetchOrganizationName(id) {
+ async fetchInitialOrganization(id) {
try {
- const {
- data: {
- organization: { name },
- },
- } = await this.$apollo.query({
+ const response = await this.$apollo.query({
query: getOrganizationQuery,
- variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
+ variables: { id: convertToGraphQLId(TYPE_ORGANIZATION, id) },
});
- return name;
+ return this.mapOrganizationData(response.data.organization);
} catch (error) {
this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
- return '';
+ return {};
}
},
+ mapOrganizationData(organization) {
+ return {
+ ...organization,
+ text: organization.name,
+ value: getIdFromGraphQLId(organization.id),
+ };
+ },
handleError({ message, error }) {
Sentry.captureException(error);
this.errorMessage = message;
@@ -137,7 +144,7 @@ export default {
:header-text="$options.i18n.selectGroup"
:default-toggle-text="$options.i18n.toggleText"
:fetch-items="fetchOrganizations"
- :fetch-initial-selection-text="fetchOrganizationName"
+ :fetch-initial-selection="fetchInitialOrganization"
:toggle-class="toggleClass"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
index 8c371e3d4ce..8c873d39496 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -120,24 +120,29 @@ export default {
membership: this.membership,
});
})();
- projects = data.map((item) => ({
- text: item.name_with_namespace || item.name,
- value: String(item.id),
- }));
+ projects = data.map((project) => this.mapProjectData(project));
} catch (error) {
this.handleError({ message: FETCH_PROJECTS_ERROR, error });
}
return { items: projects, totalPages: 1 };
},
- async fetchProjectName(projectId) {
- let projectName = '';
+ async fetchInitialProject(projectId) {
try {
- const { data: project } = await Api.project(projectId);
- projectName = project.name_with_namespace;
+ const response = await Api.project(projectId);
+
+ return this.mapProjectData(response.data);
} catch (error) {
this.handleError({ message: FETCH_PROJECT_ERROR, error });
+
+ return {};
}
- return projectName;
+ },
+ mapProjectData(project) {
+ return {
+ ...project,
+ text: project.name_with_namespace || project.name,
+ value: String(project.id),
+ };
},
handleError({ message, error }) {
Sentry.captureException(error);
@@ -163,7 +168,7 @@ export default {
:header-text="$options.i18n.selectProject"
:default-toggle-text="$options.i18n.searchForProject"
:fetch-items="fetchProjects"
- :fetch-initial-selection-text="fetchProjectName"
+ :fetch-initial-selection="fetchInitialProject"
:block="block"
clearable
v-on="$listeners"
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 6a10557c6bc..4738d0f5a38 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -90,12 +90,6 @@ export default {
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use :href="spriteHref" />
</svg>
- <gl-icon
- v-else
- :name="folderIconName"
- :size="size"
- class="folder-icon"
- data-qa-selector="folder_icon_content"
- />
+ <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index cecd1be82e9..6ac75230d88 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -132,11 +132,7 @@ export default {
@click="clickFile"
@mouseleave="$emit('mouseleave', $event)"
>
- <div
- class="file-row-name-container"
- data-qa-selector="file_row_container"
- :data-qa-file-name="file.name"
- >
+ <div class="file-row-name-container">
<span
ref="textOutput"
class="file-row-name"
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 c698b94749d..5362ceac9ee 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
@@ -31,6 +31,8 @@ export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE];
+export const OPERATORS_TO_GROUP = [OPERATOR_OR, OPERATOR_NOT];
+
export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') };
@@ -66,6 +68,7 @@ export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_CONTACT = s__('Crm|Contact');
export const TOKEN_TITLE_GROUP = __('Group');
export const TOKEN_TITLE_LABEL = __('Label');
+export const TOKEN_TITLE_PROJECT = __('Project');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
@@ -76,6 +79,7 @@ export const TOKEN_TITLE_STATUS = __('Status');
export const TOKEN_TITLE_JOBS_RUNNER_TYPE = s__('Job|Runner type');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_VERSION = __('Version');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
export const TOKEN_TITLE_CREATED = __('Created date');
export const TOKEN_TITLE_CLOSED = __('Closed date');
@@ -91,6 +95,7 @@ export const TOKEN_TYPE_EPIC = 'epic';
// this is in the shared constants. Until we have not decoupled the EE filtered search bar
// from the CE component, we need to keep this in the CE code.
// https://gitlab.com/gitlab-org/gitlab/-/issues/377838
+export const TOKEN_TYPE_PROJECT = 'project';
export const TOKEN_TYPE_HEALTH = 'health';
export const TOKEN_TYPE_ITERATION = 'iteration';
export const TOKEN_TYPE_LABEL = 'label';
@@ -104,6 +109,7 @@ export const TOKEN_TYPE_STATUS = 'status';
export const TOKEN_TYPE_JOBS_RUNNER_TYPE = 'jobs-runner-type';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
+export const TOKEN_TYPE_VERSION = 'version';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
export const TOKEN_TYPE_CREATED = 'created';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index d39e4d2ee42..364ba10e888 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -1,13 +1,5 @@
<script>
-import {
- GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlFormCheckbox,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlFilteredSearch, GlSorting, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
@@ -22,10 +14,7 @@ import { filterEmptySearchTerm, uniqueTokens } from './filtered_search_utils';
export default {
components: {
GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlSorting,
GlFormCheckbox,
},
directives: {
@@ -118,8 +107,7 @@ export default {
recentSearchesPromise: null,
recentSearches: [],
filterValue: this.initialFilterValue,
- selectedSortOption: this.sortOptions[0],
- selectedSortDirection: SORT_DIRECTION.descending,
+ ...this.getInitialSort(),
};
},
computed: {
@@ -141,15 +129,14 @@ export default {
{},
);
},
- sortDirectionIcon() {
- return this.selectedSortDirection === SORT_DIRECTION.ascending
- ? 'sort-lowest'
- : 'sort-highest';
+ transformedSortOptions() {
+ return this.sortOptions.map(({ id: value, title: text }) => ({ value, text }));
},
- sortDirectionTooltip() {
- return this.selectedSortDirection === SORT_DIRECTION.ascending
- ? __('Sort direction: Ascending')
- : __('Sort direction: Descending');
+ selectedSortDirection() {
+ return this.sortDirectionAscending ? SORT_DIRECTION.ascending : SORT_DIRECTION.descending;
+ },
+ selectedSortOption() {
+ return this.sortOptions.find((sortOption) => sortOption.id === this.sortById);
},
/**
* This prop fixes a behaviour affecting GlFilteredSearch
@@ -184,14 +171,13 @@ export default {
this.filterValue = newValue;
}
},
- initialSortBy(newValue) {
- if (this.syncFilterAndSort) {
- this.updateSelectedSortValues(newValue);
+ initialSortBy(newInitialSortBy) {
+ if (this.syncFilterAndSort && newInitialSortBy) {
+ this.updateSelectedSortValues();
}
},
},
created() {
- this.updateSelectedSortValues(this.initialSortBy);
if (this.recentSearchesStorageKey) this.setupRecentSearch();
},
methods: {
@@ -273,15 +259,12 @@ export default {
return filter;
});
},
- handleSortOptionClick(sortBy) {
- this.selectedSortOption = sortBy;
- this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
+ handleSortByChange(sortById) {
+ this.sortById = sortById;
+ this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
- handleSortDirectionClick() {
- this.selectedSortDirection =
- this.selectedSortDirection === SORT_DIRECTION.ascending
- ? SORT_DIRECTION.descending
- : SORT_DIRECTION.ascending;
+ handleSortDirectionChange(isAscending) {
+ this.sortDirectionAscending = isAscending;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
@@ -328,18 +311,30 @@ export default {
const cleared = true;
this.$emit('onFilter', [], cleared);
},
- updateSelectedSortValues(sort) {
- if (!sort) {
- return;
+ updateSelectedSortValues() {
+ Object.assign(this, this.getInitialSort());
+ },
+ getInitialSort() {
+ for (const sortOption of this.sortOptions) {
+ if (sortOption.sortDirection.ascending === this.initialSortBy) {
+ return {
+ sortById: sortOption.id,
+ sortDirectionAscending: true,
+ };
+ }
+
+ if (sortOption.sortDirection.descending === this.initialSortBy) {
+ return {
+ sortById: sortOption.id,
+ sortDirectionAscending: false,
+ };
+ }
}
- this.selectedSortOption = this.sortOptions.find(
- (sortBy) =>
- sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
- );
- this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find(
- (key) => this.selectedSortOption.sortDirection[key] === sort,
- );
+ return {
+ sortById: this.sortOptions[0]?.id,
+ sortDirectionAscending: false,
+ };
},
},
};
@@ -390,25 +385,14 @@ export default {
</template>
</template>
</gl-filtered-search>
- <gl-button-group v-if="selectedSortOption" class="sort-dropdown-container d-flex">
- <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
- <gl-dropdown-item
- v-for="sortBy in sortOptions"
- :key="sortBy.id"
- is-check-item
- :is-checked="sortBy.id === selectedSortOption.id"
- @click="handleSortOptionClick(sortBy)"
- >{{ sortBy.title }}</gl-dropdown-item
- >
- </gl-dropdown>
- <gl-button
- v-gl-tooltip
- :title="sortDirectionTooltip"
- :aria-label="sortDirectionTooltip"
- :icon="sortDirectionIcon"
- class="flex-shrink-1"
- @click="handleSortDirectionClick"
- />
- </gl-button-group>
+ <gl-sorting
+ v-if="selectedSortOption"
+ :sort-options="transformedSortOptions"
+ :sort-by="sortById"
+ :is-ascending="sortDirectionAscending"
+ class="sort-dropdown-container"
+ @sortByChange="handleSortByChange"
+ @sortDirectionChange="handleSortDirectionChange"
+ />
</div>
</template>
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 3857dd9c55d..5d72ac34e73 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
@@ -11,7 +11,13 @@ import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
+import {
+ DEBOUNCE_DELAY,
+ FILTERS_NONE_ANY,
+ OPERATOR_NOT,
+ OPERATOR_OR,
+ OPERATORS_TO_GROUP,
+} from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@@ -102,7 +108,7 @@ export default {
},
activeTokenValue() {
const data =
- this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ this.multiSelectEnabled && Array.isArray(this.value.data)
? last(this.value.data)
: this.value.data;
return this.getActiveTokenValue(this.suggestions, data);
@@ -153,6 +159,22 @@ export default {
? this.activeTokenValue[this.searchBy]
: undefined;
},
+ multiSelectEnabled() {
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
+ },
+ validatedConfig() {
+ if (this.config.multiSelect && !this.multiSelectEnabled) {
+ return {
+ ...this.config,
+ multiSelect: false,
+ };
+ }
+ return this.config;
+ },
},
watch: {
active: {
@@ -199,7 +221,7 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- if (this.glFeatures.groupMultiSelectTokens) {
+ if (this.multiSelectEnabled) {
this.$emit('token-selected', selectedValue);
}
@@ -228,7 +250,7 @@ export default {
<template>
<gl-filtered-search-token
- :config="config"
+ :config="validatedConfig"
:value="value"
:active="active"
:multi-select-values="multiSelectValues"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index c5326ead60d..87e295d00dd 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -7,7 +7,7 @@ import { __ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { OPTIONS_NONE_ANY } from '../constants';
+import { OPERATORS_TO_GROUP, OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -57,7 +57,11 @@ export default {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
multiSelectEnabled() {
- return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
},
},
watch: {
@@ -94,7 +98,7 @@ export default {
return user?.avatarUrl || user?.avatar_url;
},
displayNameFor(username) {
- return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ return this.getActiveUser(this.allUsers, username)?.name || username;
},
avatarFor(username) {
const user = this.getActiveUser(this.allUsers, username);
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 0455685627d..b03da19a896 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
@@ -179,7 +179,6 @@ export default {
:aria-label="toggleVisibilityLabel"
:icon="toggleVisibilityIcon"
data-testid="toggle-visibility-button"
- data-qa-selector="toggle_visibility_button"
@click.stop="handleToggleVisibilityButtonClick"
/>
<clipboard-button
diff --git a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue b/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
deleted file mode 100644
index d68c4399275..00000000000
--- a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-export default {
- props: {
- slotKey: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- aliveSlotsLookup: {},
- };
- },
- computed: {
- aliveSlots() {
- return Object.keys(this.aliveSlotsLookup);
- },
- },
- watch: {
- slotKey: {
- handler(val) {
- if (!val) {
- return;
- }
-
- this.$set(this.aliveSlotsLookup, val, true);
- },
- immediate: true,
- },
- },
- methods: {
- isCurrentSlot(key) {
- return key === this.slotKey;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div
- v-for="slot in aliveSlots"
- v-show="isCurrentSlot(slot)"
- :key="slot"
- class="gl-h-full gl-w-full"
- >
- <slot :name="slot"></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
index cff9c56a1c0..ad826c6f3e5 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/constants.js
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -1,6 +1,26 @@
import { __ } from '~/locale';
+import UserItem from './user_item.vue';
+import GroupItem from './group_item.vue';
+import DeployKeyItem from './deploy_key_item.vue';
export const CONFIG = {
- users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
- groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
+ users: {
+ title: __('Users'),
+ icon: 'user',
+ filterKey: 'username',
+ showNamespaceDropdown: true,
+ component: UserItem,
+ },
+ groups: {
+ title: __('Groups'),
+ icon: 'group',
+ filterKey: 'name',
+ component: GroupItem,
+ },
+ deployKeys: {
+ title: __('Deploy keys'),
+ icon: 'key',
+ filterKey: 'name',
+ component: DeployKeyItem,
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
new file mode 100644
index 00000000000..4dbbd44f0b5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'DeployKeyItem',
+ components: { GlButton, GlIcon },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ const { title, owner, id } = this.data;
+ return {
+ deleteButtonLabel: sprintf(__('Delete %{name}'), { name: title }),
+ title,
+ owner,
+ id,
+ };
+ },
+};
+</script>
+
+<template>
+ <span
+ class="gl-display-flex gl-align-items-center gl-gap-3"
+ data-testid="deploy-key-wrapper"
+ @click="$emit('select', id)"
+ >
+ <gl-icon name="key" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ title }}</span>
+ <span class="gl-text-gray-600">@{{ owner }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click.stop="$emit('delete', id)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
index b8480a0c496..d79a8d6a00c 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/index.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -5,8 +5,6 @@ import { createAlert } from '~/alert';
import { __ } from '~/locale';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import Api from '~/api';
-import UserItem from './user_item.vue';
-import GroupItem from './group_item.vue';
import { CONFIG } from './constants';
const I18N = {
@@ -25,10 +23,6 @@ export default {
GlCollapsibleListbox,
},
props: {
- title: {
- type: String,
- required: true,
- },
type: {
type: String,
required: true,
@@ -61,12 +55,6 @@ export default {
config() {
return CONFIG[this.type];
},
- isUserVariant() {
- return this.type === 'users';
- },
- component() {
- return this.isUserVariant ? UserItem : GroupItem;
- },
namespaceDropdownText() {
return parseBoolean(this.isProjectNamespace)
? this.$options.i18n.projectGroups
@@ -77,12 +65,14 @@ export default {
async handleSearchInput(search) {
this.$refs.results.open();
+ const searchMethod = {
+ users: this.fetchUsersBySearchTerm,
+ groups: this.fetchGroupsBySearchTerm,
+ deployKeys: this.fetchDeployKeysBySearchTerm,
+ };
+
try {
- if (this.isUserVariant) {
- this.items = await this.fetchUsersBySearchTerm(search);
- } else {
- this.items = await this.fetchGroupsBySearchTerm(search);
- }
+ this.items = await searchMethod[this.type](search);
} catch (e) {
createAlert({
message: this.$options.i18n.apiErrorMessage,
@@ -114,6 +104,10 @@ export default {
})),
);
},
+ fetchDeployKeysBySearchTerm() {
+ // TODO - implement API request (follow-up)
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/432494
+ },
getItemByKey(key) {
return this.items.find((item) => item[this.config.filterKey] === key);
},
@@ -139,7 +133,7 @@ export default {
<gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
<template #header
><strong data-testid="list-selector-title"
- >{{ title }}
+ >{{ config.title }}
<span class="gl-text-gray-700 gl-ml-3"
><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
></strong
@@ -166,7 +160,7 @@ export default {
</template>
<template #list-item="{ item }">
- <component :is="component" :data="item" @select="handleSelectItem" />
+ <component :is="config.component" :data="item" @select="handleSelectItem" />
</template>
</gl-collapsible-listbox>
@@ -180,7 +174,7 @@ export default {
</div>
<component
- :is="component"
+ :is="config.component"
v-for="(item, index) of selectedItems"
:key="index"
:class="{ 'gl-border-t': index > 0 }"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index d99b90fa561..a7dfc1e2cdb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -88,7 +88,7 @@ export default {
placement="right"
searchable
size="small"
- class="comment-template-dropdown gl-mr-3"
+ class="comment-template-dropdown gl-mr-2"
positioning-strategy="fixed"
:searching="$apollo.queries.savedReplies.loading"
@shown="fetchCommentTemplates"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 24211833026..e80f5c7f092 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -361,7 +361,7 @@ export default {
<template>
<div
ref="gl-form"
- class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden"
+ class="js-vue-markdown-field md-area position-relative gfm-form"
:data-uploads-path="uploadsPath"
>
<markdown-header
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index cc3c95a047b..cffd8471d18 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -21,6 +21,7 @@ import { updateText } from '~/lib/utils/text_markdown';
import ToolbarButton from './toolbar_button.vue';
import DrawioToolbarButton from './drawio_toolbar_button.vue';
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
+import HeaderDivider from './header_divider.vue';
export default {
components: {
@@ -30,6 +31,7 @@ export default {
DrawioToolbarButton,
CommentTemplatesDropdown,
AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'),
+ HeaderDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -188,14 +190,6 @@ export default {
})
.catch(() => {});
},
- handleAttachFile(e) {
- e.preventDefault();
- const $gfmForm = $(this.$el).closest('.gfm-form');
- const $gfmTextarea = $gfmForm.find('.js-gfm-input');
-
- $gfmForm.find('.div-dropzone').click();
- $gfmTextarea.focus();
- },
insertIntoTextarea(text) {
const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
if (textArea) {
@@ -254,252 +248,281 @@ export default {
</script>
<template>
- <div class="md-header gl-border-b gl-border-gray-100 gl-px-3">
+ <div
+ class="md-header gl-bg-white gl-border-b gl-border-gray-100 gl-rounded-lg gl-rounded-bottom-left-none gl-rounded-bottom-right-none gl-px-3"
+ :class="{ 'md-header-preview': previewMarkdown }"
+ >
<div class="gl-display-flex gl-align-items-center gl-flex-wrap">
<div
data-testid="md-header-toolbar"
- class="md-header-toolbar gl-display-flex gl-py-3 gl-flex-wrap gl-row-gap-3"
+ class="md-header-toolbar gl-display-flex gl-py-3 gl-row-gap-2 gl-flex-grow-1 gl-align-items-flex-start"
>
- <gl-button
- v-if="enablePreview"
- data-testid="preview-toggle"
- :value="previewMarkdown ? 'preview' : 'edit'"
- :label="$options.i18n.previewTabTitle"
- class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
- size="small"
- category="tertiary"
- @click="switchPreview"
- >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
- >
- <template v-if="!previewMarkdown && canSuggest">
+ <div class="gl-display-flex gl-flex-wrap gl-row-gap-2">
+ <gl-button
+ v-if="enablePreview"
+ data-testid="preview-toggle"
+ :value="previewMarkdown ? 'preview' : 'edit'"
+ :label="$options.i18n.previewTabTitle"
+ class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!"
+ size="small"
+ category="tertiary"
+ @click="switchPreview"
+ >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
+ >
+ <template v-if="!previewMarkdown && canSuggest">
+ <div class="gl-display-flex gl-row-gap-2">
+ <header-divider :preview-markdown="previewMarkdown" />
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ tracking-property="codeSuggestion"
+ icon="doc-code"
+ data-testid="suggestion-button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ triggers=""
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="confirm"
+ category="primary"
+ size="small"
+ data-testid="dismiss-suggestion-popover-button"
+ @click="handleSuggestDismissed"
+ >
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </div>
+ </template>
+ <div class="gl-display-flex gl-row-gap-2">
+ <div
+ v-if="!previewMarkdown && editorAiActions.length"
+ class="gl-display-flex gl-row-gap-2"
+ >
+ <header-divider :preview-markdown="previewMarkdown" />
+ <ai-actions-dropdown
+ :actions="editorAiActions"
+ @input="insertAIAction"
+ @replace="replaceTextarea"
+ />
+ </div>
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
<toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
+ v-show="!previewMarkdown"
+ tag="**"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.bold"
+ icon="bold"
+ tracking-property="bold"
+ />
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="_"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.italic"
+ icon="italic"
+ tracking-property="italic"
+ />
+ <div class="gl-display-flex gl-row-gap-2">
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('strikethrough')"
+ v-show="!previewMarkdown"
+ tag="~~"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), {
+ modifierKey,
+ shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.strikethrough"
+ icon="strikethrough"
+ tracking-property="strike"
+ />
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('quote')"
+ v-show="!previewMarkdown"
:prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- tracking-property="codeSuggestion"
- icon="doc-code"
- data-testid="suggestion-button"
- class="js-suggestion-btn"
- @click="handleSuggestDismissed"
+ :tag="tag"
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ tracking-property="blockquote"
+ @click="handleQuote"
/>
- <gl-popover
- v-if="suggestPopoverVisible"
- :target="$refs.suggestButton.$el"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="suggestPopoverVisible"
- triggers=""
- >
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="confirm"
- category="primary"
- size="small"
- data-testid="dismiss-suggestion-popover-button"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
- <ai-actions-dropdown
- v-if="!previewMarkdown && editorAiActions.length"
- :actions="editorAiActions"
- @input="insertAIAction"
- @replace="replaceTextarea"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="**"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.bold"
- icon="bold"
- tracking-property="bold"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="_"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.italic"
- icon="italic"
- tracking-property="italic"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('strikethrough')"
- v-show="!previewMarkdown"
- tag="~~"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), {
- modifierKey,
- shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.strikethrough"
- icon="strikethrough"
- tracking-property="strike"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('quote')"
- v-show="!previewMarkdown"
- :prepend="true"
- :tag="tag"
- :button-title="__('Insert a quote')"
- icon="quote"
- tracking-property="blockquote"
- @click="handleQuote"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="`"
- tag-block="```"
- :button-title="__('Insert code')"
- icon="code"
- tracking-property="code"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="[{text}](url)"
- tag-select="url"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.link"
- icon="link"
- tracking-property="link"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('bullet-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="- "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- tracking-property="bulletList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('numbered-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- tracking-property="orderedList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('task-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="- [ ] "
- :button-title="__('Add a checklist')"
- icon="list-task"
- tracking-property="taskList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('indent')"
- v-show="!previewMarkdown"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.indent"
- command="indentLines"
- icon="list-indent"
- tracking-property="indent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('outdent')"
- v-show="!previewMarkdown"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.outdent"
- command="outdentLines"
- icon="list-outdent"
- tracking-property="outdent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('collapsible-section')"
- v-show="!previewMarkdown"
- :tag="mdCollapsibleSection"
- :prepend="true"
- tag-select="Click to expand"
- :button-title="__('Add a collapsible section')"
- icon="details-block"
- tracking-property="details"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('table')"
- v-show="!previewMarkdown"
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- tracking-property="table"
- />
- <toolbar-button
- v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')"
- data-testid="button-attach-file"
- :button-title="__('Attach a file or image')"
- icon="paperclip"
- class="gl-mr-3"
- tracking-property="upload"
- @click="handleAttachFile"
- />
- <drawio-toolbar-button
- v-if="!previewMarkdown && drawioEnabled"
- :uploads-path="uploadsPath"
- :markdown-preview-path="markdownPreviewPath"
- />
- <!-- TODO Add icon and trigger functionality from here -->
- <toolbar-button
- v-if="supportsQuickActions"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="/"
- :button-title="__('Add a quick action')"
- icon="quick-actions"
- tracking-property="quickAction"
- />
- <comment-templates-dropdown
- v-if="!previewMarkdown && newCommentTemplatePath"
- :new-comment-template-path="newCommentTemplatePath"
- @select="insertSavedReply"
- />
- <div v-if="!previewMarkdown" class="full-screen">
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="`"
+ tag-block="```"
+ :button-title="__('Insert code')"
+ icon="code"
+ tracking-property="code"
+ />
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.link"
+ icon="link"
+ tracking-property="link"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('bullet-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="- "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ tracking-property="bulletList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('numbered-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ tracking-property="orderedList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('task-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="- [ ] "
+ :button-title="__('Add a checklist')"
+ icon="list-task"
+ tracking-property="taskList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ v-show="!previewMarkdown"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ tracking-property="indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ v-show="!previewMarkdown"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ tracking-property="outdent"
+ />
+ <div class="gl-display-flex gl-row-gap-2">
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('collapsible-section')"
+ v-show="!previewMarkdown"
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ tracking-property="details"
+ />
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('table')"
+ v-show="!previewMarkdown"
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ tracking-property="table"
+ />
+ <!--
+ The attach file button's click behavior is added by
+ dropzone_input.js.
+ -->
+ <toolbar-button
+ v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')"
+ data-testid="button-attach-file"
+ data-button-type="attach-file"
+ :button-title="__('Attach a file or image')"
+ icon="paperclip"
+ class="gl-mr-2"
+ tracking-property="upload"
+ />
+ <drawio-toolbar-button
+ v-if="!previewMarkdown && drawioEnabled"
+ :uploads-path="uploadsPath"
+ :markdown-preview-path="markdownPreviewPath"
+ />
+ <!-- TODO Add icon and trigger functionality from here -->
+ <toolbar-button
+ v-if="supportsQuickActions"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="/"
+ :button-title="__('Add a quick action')"
+ icon="quick-actions"
+ tracking-property="quickAction"
+ />
+ <comment-templates-dropdown
+ v-if="!previewMarkdown && newCommentTemplatePath"
+ :new-comment-template-path="newCommentTemplatePath"
+ @select="insertSavedReply"
+ />
+ </div>
+ <div
+ v-if="!previewMarkdown"
+ class="full-screen gl-flex-grow-1 gl-justify-content-end gl-display-flex"
+ >
<toolbar-button
v-if="!restrictedToolBarItems.includes('full-screen')"
- class="js-zen-enter"
+ class="js-zen-enter gl-mr-0!"
icon="maximize"
:button-title="__('Go full screen')"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue
new file mode 100644
index 00000000000..d08a3d4cd34
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ props: {
+ previewMarkdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="!previewMarkdown" class="md-toolbar-divider gl-display-flex gl-py-2">
+ <div class="gl-border-l gl-pl-3 gl-ml-2"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index cf484443c07..182da7945ff 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -111,7 +111,7 @@ export default {
type="button"
category="tertiary"
size="small"
- class="js-md gl-mr-3"
+ class="js-md gl-mr-2"
data-container="body"
@click="$emit('click', $event)"
/>
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 3bee539688b..1ee752e8c19 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -72,7 +72,7 @@ export default {
};
</script>
<template>
- <div class="issuable-note-warning">
+ <div class="issuable-note-warning" data-testid="issuable-note-warning">
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js
new file mode 100644
index 00000000000..59b1967ad31
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js
@@ -0,0 +1,34 @@
+import NumberToHumanSize from './number_to_human_size.vue';
+
+export default {
+ component: NumberToHumanSize,
+ title: 'vue_shared/number_to_human_size',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { NumberToHumanSize },
+ props: Object.keys(argTypes),
+ template: '<number-to-human-size v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ value: 42.55 * 1024 * 1024 * 1024,
+ fractionDigits: 1,
+ labelClass: '',
+ plainZero: false,
+};
+
+export const PlainZero = Template.bind({});
+PlainZero.args = {
+ ...Default.args,
+ value: 0,
+ plainZero: true,
+};
+
+export const CustomStyles = Template.bind({});
+CustomStyles.args = {
+ ...Default.args,
+ class: 'gl-font-weight-bold',
+ labelClass: 'gl-font-sm gl-text-gray-500',
+};
diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue
new file mode 100644
index 00000000000..d6c56b2c465
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue
@@ -0,0 +1,48 @@
+<script>
+import { numberToHumanSizeSplit } from '~/lib/utils/number_utils';
+
+export default {
+ name: 'NumberToHumanSize',
+ props: {
+ value: {
+ type: Number,
+ required: true,
+ },
+ fractionDigits: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ labelClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ plainZero: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ formattedValue() {
+ if (this.plainZero && this.value === 0) {
+ return ['0'];
+ }
+
+ return numberToHumanSizeSplit(this.value, this.fractionDigits);
+ },
+ number() {
+ return this.formattedValue[0];
+ },
+ label() {
+ return this.formattedValue[1];
+ },
+ },
+};
+</script>
+<template>
+ <span
+ >{{ number }}<span v-if="label" :class="labelClass"> {{ label }}</span></span
+ >
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 67ad7769c7c..f3b483c5f53 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
@@ -100,7 +100,7 @@ export default {
type="search"
class="mb-3"
autofocus
- data-qa-selector="project_search_field"
+ data-testid="project-search-field"
@input="onInput"
/>
<div class="d-flex flex-column">
@@ -120,7 +120,7 @@ export default {
:project="project"
:matcher="searchQuery"
class="js-project-list-item"
- data-qa-selector="project_list_item"
+ data-testid="project-list-item"
@click="projectClicked(project)"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
index 72e06b45561..85b4ea241ef 100644
--- a/app/assets/javascripts/vue_shared/components/registry/details_row.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
@@ -32,12 +32,14 @@ export default {
<template>
<div
- class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
+ class="gl-display-flex gl-align-items-top gl-font-monospace gl-font-sm gl-word-break-all"
:class="[padding, borderClass]"
>
- <gl-icon v-if="icon" :name="icon" class="gl-mr-4" />
- <span>
+ <div v-if="icon" class="gl-w-5 gl-mr-4">
+ <gl-icon :name="icon" />
+ </div>
+ <div>
<slot></slot>
- </span>
+ </div>
</div>
</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 ccda8c5fea7..868e348adc0 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -70,9 +70,11 @@ export default {
<slot name="left-action"></slot>
</div>
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
>
- <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1"
+ >
<div
v-if="
$slots['left-primary'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index 3b6dcace8fe..89b64f03e1f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -66,6 +66,10 @@ export default {
const page = getPageParamValue(this.number);
return getPageSearchString(this.blamePath, page);
},
+ codeStyling() {
+ const defaultGutterWidth = 96;
+ return { marginLeft: `${this.$refs.lineNumbers?.offsetWidth || defaultGutterWidth}px` };
+ },
},
methods: {
handleChunkAppear() {
@@ -80,7 +84,7 @@ export default {
</script>
<template>
<div class="gl-display-flex">
- <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column gl-absolute">
<div
v-for="(n, index) in totalLines"
:key="index"
@@ -102,14 +106,14 @@ export default {
</div>
</div>
- <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <div v-else ref="lineNumbers" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
<!-- Placeholder for line numbers while content is not highlighted -->
</div>
<gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear">
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
- ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ ><code v-if="shouldHighlight" v-safe-html="highlightedContent" :style="codeStyling" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
</gl-intersection-observer>
</div>
</template>
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 582093e5739..47b802d9d17 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -14,6 +14,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
clean: 'clean',
clojure: 'clojure',
cmake: 'cmake',
+ codeowners: 'codeowners',
coffeescript: 'coffeescript',
coq: 'coq',
cpp: 'cpp',
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
index a5f3f348cfc..c497224cde3 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
@@ -14,6 +14,7 @@ query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine:
span
commit {
id
+ authorName
titleHtml
message
authoredDate
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index dcefa66c403..bc46f11ab2d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -5,7 +5,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import LineHighlighter from '~/blob/line_highlighter';
-import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER, CODEOWNERS_FILE_NAME } from './constants';
import Chunk from './components/chunk_new.vue';
import Blame from './components/blame_info.vue';
import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
@@ -21,6 +21,7 @@ export default {
components: {
Chunk,
Blame,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
directives: {
SafeHtml,
@@ -45,6 +46,10 @@ export default {
type: String,
required: true,
},
+ currentRef: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -66,6 +71,9 @@ export default {
return result;
}, []);
},
+ isCodeownersFile() {
+ return this.blob.name === CODEOWNERS_FILE_NAME;
+ },
},
watch: {
showBlame: {
@@ -136,11 +144,19 @@ export default {
<blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
<div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full"
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full blob-viewer"
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
+ data-testid="blob-viewer-file-content"
>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
v-for="(chunk, index) in chunks"
:key="index"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
index 8d8e945cd5f..057a1c2d113 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -1,13 +1,35 @@
import hljs from 'highlight.js/lib/core';
-import json from 'highlight.js/lib/languages/json';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { registerPlugins } from '../plugins/index';
import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants';
-const initHighlightJs = (fileType, content, language) => {
- // The Highlight Worker is currently scoped to JSON files.
- // See the following issue for more: https://gitlab.com/gitlab-org/gitlab/-/issues/415753
- hljs.registerLanguage(language, json);
+const loadLanguage = async (language) => {
+ const languageDefinition = await languageLoader[language]();
+ hljs.registerLanguage(language, languageDefinition.default);
+};
+
+const loadSubLanguages = async (languageDefinition) => {
+ // Some files can contain sub-languages (i.e., Svelte); this ensures that sub-languages are also loaded
+ if (!languageDefinition?.contains) return;
+
+ // generate list of languages to load
+ const languages = new Set(
+ languageDefinition.contains
+ .filter((component) => Boolean(component.subLanguage))
+ .map((component) => component.subLanguage),
+ );
+
+ if (languageDefinition.subLanguage) {
+ languages.add(languageDefinition.subLanguage);
+ }
+
+ await Promise.all([...languages].map(loadLanguage));
+};
+
+const initHighlightJs = async (fileType, content, language) => {
registerPlugins(hljs, fileType, content, true);
+ await loadLanguage(language);
+ await loadSubLanguages(hljs.getLanguage(language));
};
const splitByLineBreaks = (content = '') => content.split(/\r?\n/);
@@ -35,12 +57,12 @@ const splitIntoChunks = (language, rawContent, highlightedContent) => {
return result;
};
-const highlight = (fileType, rawContent, lang) => {
+const highlight = async (fileType, rawContent, lang) => {
const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()];
let result;
if (language) {
- initHighlightJs(fileType, rawContent, language);
+ await initHighlightJs(fileType, rawContent, language);
const highlightedContent = hljs.highlight(rawContent, { language }).value;
result = splitIntoChunks(language, rawContent, highlightedContent);
}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
index 535e857d7a9..49afaba3d2f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
@@ -4,7 +4,7 @@ import { highlight } from './highlight_utils';
* A webworker for highlighting large amounts of content with Highlight.js
*/
// eslint-disable-next-line no-restricted-globals
-self.addEventListener('message', ({ data: { fileType, content, language } }) => {
+self.addEventListener('message', async ({ data: { fileType, content, language } }) => {
// eslint-disable-next-line no-restricted-globals
- self.postMessage(highlight(fileType, content, language));
+ self.postMessage(await highlight(fileType, content, language));
});
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 779a2ab5461..45d49e5339a 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
@@ -21,9 +21,11 @@ export default {
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
>
- <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1"
+ >
<div
v-if="
/* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
diff --git a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
index e5558c038b3..43e35f2b1f0 100644
--- a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
@@ -12,11 +12,18 @@ export default {
components: {
GlBadge,
},
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
};
</script>
<template>
- <gl-badge class="gl-bg-transparent! gl-inset-border-1-gray-100!">
+ <gl-badge :size="size" class="gl-bg-transparent! gl-inset-border-1-gray-100!">
<slot></slot>
</gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
deleted file mode 100644
index 46496d2e483..00000000000
--- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<script>
-export default {
- provide() {
- return {
- vuexModule: this.vuexModule,
- };
- },
- props: {
- vuexModule: {
- type: String,
- required: true,
- },
- },
- render() {
- return this.$scopedSlots.default?.();
- },
-};
-</script>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
index b4afb27c497..96b2bd37080 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
@@ -82,7 +82,6 @@ export default {
attributes: {
href: this.forkPath,
variant: 'confirm',
- 'data-qa-selector': 'fork_project_button',
},
},
};
@@ -94,7 +93,6 @@ export default {
<template>
<gl-modal
:visible="visible"
- data-qa-selector="confirm_fork_modal"
:modal-id="modalId"
:title="$options.i18n.title"
:action-primary="btnActions.primary"
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 441b4c31b3a..3514a9c2d5d 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -145,6 +145,11 @@ export default {
required: false,
default: '',
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: 'gl-sm-ml-3',
+ },
},
data() {
return {
@@ -329,7 +334,7 @@ export default {
</script>
<template>
- <div class="gl-sm-ml-3">
+ <div :class="cssClasses">
<gl-disclosure-dropdown
v-if="hasActions"
:variant="isBlob ? 'confirm' : 'default'"
diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js
index 14ea0389bad..b3840a0adbf 100644
--- a/app/assets/javascripts/vue_shared/global_search/constants.js
+++ b/app/assets/javascripts/vue_shared/global_search/constants.js
@@ -1,11 +1,10 @@
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
export const AUTOCOMPLETE_ERROR_MESSAGE = s__(
'GlobalSearch|There was an error fetching search autocomplete suggestions.',
);
export const ALL_GITLAB = __('All GitLab');
-export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab');
export const PLACES = s__('GlobalSearch|Places');
export const COMMAND_PALETTE = s__('GlobalSearch|Command palette');
@@ -24,17 +23,9 @@ export const SEARCH_DESCRIBED_BY_UPDATED = s__(
);
export const SEARCH_RESULTS_LOADING = s__('GlobalSearch|Search results are loading');
export const SEARCH_RESULTS_SCOPE = s__('GlobalSearch|in %{scope}');
-export const KBD_HELP = sprintf(
- s__('GlobalSearch|Use the shortcut key %{kbdOpen}/%{kbdClose} to start a search'),
- { kbdOpen: '<kbd>', kbdClose: '</kbd>' },
- false,
-);
export const MIN_SEARCH_TERM = s__(
'GlobalSearch|The search term must be at least 3 characters long.',
);
-
-export const SCOPED_SEARCH_ITEM_ARIA_LABEL = s__('GlobalSearch| %{search} %{description} %{scope}');
-
export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created");
@@ -76,8 +67,6 @@ export const SEARCH_RESULTS_ORDER = [
SETTINGS_CATEGORY,
HELP_CATEGORY,
];
-export const DROPDOWN_ORDER = SEARCH_RESULTS_ORDER;
-
export const SEARCH_LABELS = s__('GlobalSearch|Search labels');
export const DROPDOWN_HEADER = s__('GlobalSearch|Labels');
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
index b4287d86289..1828208bd0f 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
@@ -18,6 +18,7 @@ export default {
'initialLabels',
'issuableType',
'labelType',
+ 'issuableSupportsLockOnMerge',
'variant',
'workspaceType',
],
@@ -76,6 +77,7 @@ export default {
:issuable-type="issuableType"
:label-create-type="labelType"
:selected-labels="selectedLabels"
+ :issuable-supports-lock-on-merge="issuableSupportsLockOnMerge"
@updateSelectedLabels="handleUpdateSelectedLabels"
@onLabelRemove="handleLabelRemove"
>
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 0db7417cebc..ad908a674d3 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
@@ -6,6 +6,7 @@ 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 { __ } from '~/locale';
+import { DRAG_DELAY } from '~/sortable/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -24,6 +25,8 @@ export default {
forceFallback: true,
ghostClass: 'gl-visibility-hidden',
tag: 'ul',
+ delay: DRAG_DELAY,
+ delayOnTouchOnly: true,
},
components: {
GlAlert,
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 dae3ddfe016..bac71c1eda2 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
@@ -31,6 +31,10 @@ export default {
type: Boolean,
required: true,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ },
enableAutocomplete: {
type: Boolean,
required: true,
@@ -166,6 +170,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :hide-edit-button="hideEditButton"
:workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
@@ -181,12 +186,12 @@ export default {
:task-list-update-path="taskListUpdatePath"
/>
<slot name="secondary-content"></slot>
- <small v-if="isUpdated" class="edited-text gl-font-sm!">
+ <small v-if="isUpdated" class="edited-text gl-font-sm! gl-text-secondary">
{{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
<span v-if="updatedBy">
{{ __('by') }}
- <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm!">
+ <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm! gl-text-secondary">
<span>{{ updatedBy.name }}</span>
</gl-link>
</span>
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 7c3dd5c3623..3353374310f 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
@@ -153,7 +153,10 @@ export default {
</template>
</markdown-field>
</gl-form-group>
- <div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix">
+ <div
+ data-testid="actions"
+ class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix gl-display-flex gl-gap-3"
+ >
<slot
name="edit-form-actions"
:issuable-title="title"
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 62a2b44e660..1b95a2abdf9 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
@@ -155,8 +155,8 @@ export default {
</script>
<template>
- <div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row">
- <div class="detail-page-header-body gl-flex-wrap gl-gap-2">
+ <div class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row">
+ <div class="detail-page-header-body gl-flex-wrap gl-column-gap-2">
<gl-badge :variant="badgeVariant" data-testid="issue-state-badge">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }">
@@ -221,7 +221,7 @@ export default {
@click="handleRightSidebarToggleClick"
/>
</div>
- <div class="detail-page-header-actions gl-align-self-center gl-display-flex">
+ <div class="detail-page-header-actions gl-align-self-center gl-display-flex gl-gap-3">
<slot name="header-actions"></slot>
</div>
</div>
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 040f49c7c25..1d44c4a1c14 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
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
enableAutocomplete: {
type: Boolean,
required: false,
@@ -137,6 +142,7 @@ export default {
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:enable-edit="enableEdit"
+ :hide-edit-button="hideEditButton"
:enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave"
:enable-zen-mode="enableZenMode"
@@ -169,6 +175,9 @@ export default {
</issuable-discussion>
<issuable-sidebar>
+ <template #right-sidebar-top-items="{ sidebarExpanded, toggleSidebar }">
+ <slot name="right-sidebar-top-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
+ </template>
<template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }">
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
</template>
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 5387e39e3eb..3dae894b127 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
@@ -33,6 +33,10 @@ export default {
type: Boolean,
required: true,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ },
workspaceType: {
type: String,
required: false,
@@ -70,7 +74,7 @@ export default {
data-testid="issuable-title"
></h1>
<gl-button
- v-if="enableEdit"
+ v-if="enableEdit && !hideEditButton"
v-gl-tooltip.bottom
:title="$options.i18n.editTitleAndDescription"
:aria-label="$options.i18n.editTitleAndDescription"
diff --git a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
index 774267639fc..cb9ad6418a4 100644
--- a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
@@ -1,13 +1,17 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants';
export default {
components: {
- GlIcon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
data() {
const userExpanded = !parseBoolean(getCookie(USER_COLLAPSED_GUTTER_COOKIE));
@@ -20,6 +24,20 @@ export default {
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
};
},
+ computed: {
+ toggleLabel() {
+ return this.isExpanded ? __('Collapse sidebar') : __('Expand sidebar');
+ },
+ toggleIcon() {
+ return this.isExpanded ? 'chevron-double-lg-right' : 'chevron-double-lg-left';
+ },
+ expandedToggleClass() {
+ return this.isExpanded ? 'block' : '';
+ },
+ collapsedToggleClass() {
+ return !this.isExpanded ? 'block' : '';
+ },
+ },
mounted() {
window.addEventListener('resize', this.handleWindowResize);
this.updatePageContainerClass();
@@ -59,23 +77,24 @@ export default {
class="right-sidebar"
aria-live="polite"
>
- <button
- class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
- data-testid="toggle-right-sidebar-button"
- :title="__('Toggle sidebar')"
- @click="toggleSidebar"
- >
- <span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
- __('Collapse sidebar')
- }}</span>
- <gl-icon v-show="isExpanded" data-testid="icon-collapse" name="chevron-double-lg-right" />
- <gl-icon
- v-show="!isExpanded"
- data-testid="icon-expand"
- name="chevron-double-lg-left"
- class="gl-ml-2"
+ <div class="right-sidebar-header" :class="expandedToggleClass">
+ <gl-button
+ v-gl-tooltip.hover.left
+ category="tertiary"
+ size="small"
+ class="gl-float-right gutter-toggle toggle-right-sidebar-button js-toggle-right-sidebar-button gl-shadow-none!"
+ :class="collapsedToggleClass"
+ data-testid="toggle-right-sidebar-button"
+ :icon="toggleIcon"
+ :title="toggleLabel"
+ :aria-label="toggleLabel"
+ @click="toggleSidebar"
/>
- </button>
+ <slot
+ name="right-sidebar-top-items"
+ v-bind="{ sidebarExpanded: isExpanded, toggleSidebar }"
+ ></slot>
+ </div>
<div data-testid="sidebar-items" class="issuable-sidebar">
<slot
name="right-sidebar-items"
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index 61e45fa5195..438da925937 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -1,4 +1,4 @@
-import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
+import { getTimeago, localeDateFormat, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
@@ -12,7 +12,7 @@ export default {
},
tooltipTitle(time) {
- return formatDate(time);
+ return localeDateFormat.asDateTimeFull.format(time);
},
},
};
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 3412848a9b7..a5c34b4b619 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
@@ -86,11 +86,7 @@ export default {
},
showSuperSidebarToggle() {
- return gon.use_new_navigation && sidebarState.isCollapsed;
- },
-
- topBarClasses() {
- return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
+ return sidebarState.isCollapsed;
},
},
@@ -124,7 +120,7 @@ export default {
<template>
<div>
- <div :class="topBarClasses" data-testid="top-bar">
+ <div class="top-bar-fixed container-fluid" data-testid="top-bar">
<div
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index c1ec39e1545..dccff4a288f 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants';
+import { featureToMutationMap } from 'ee_else_ce/security_configuration/constants';
import { parseErrorMessage } from '~/lib/utils/error_message';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { sprintf, s__ } from '~/locale';
@@ -110,7 +110,6 @@ export default {
:loading="isLoading"
:variant="variant"
:category="category"
- :data-qa-selector="`${feature.type}_mr_button`"
@click="mutate"
>{{ $options.i18n.buttonLabel }}</gl-button
>