summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-17 11:33:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-17 11:33:21 +0000
commit7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch)
tree5bdc2229f5198d516781f8d24eace62fc7e589e9 /app/assets/javascripts/vue_shared/components
parent185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff)
downloadgitlab-ce-7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0.tar.gz
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/group_select.vue195
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js54
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue212
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js49
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js64
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js2
41 files changed, 783 insertions, 380 deletions
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index dc4d1bd56e9..ed0eb9cc0b8 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -15,8 +15,14 @@ export default {
mounted() {
handleBlobRichViewer(this.$refs.content, this.type);
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['copy-code'],
+ },
};
</script>
<template>
- <markdown-field-view ref="content" v-safe-html="richViewer || content" />
+ <markdown-field-view
+ ref="content"
+ v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
+ />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/code_block.stories.js b/app/assets/javascripts/vue_shared/components/code_block.stories.js
index e02a346c1de..994913dc1a8 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.stories.js
+++ b/app/assets/javascripts/vue_shared/components/code_block.stories.js
@@ -13,6 +13,5 @@ const Template = (args, { argTypes }) => ({
export const Default = Template.bind({});
Default.args = {
- // eslint-disable-next-line @gitlab/require-i18n-strings
code: `git commit -a "Message"\ngit push`,
};
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 5b9efff1c06..2bdc8a174d0 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,6 +36,11 @@ export default {
required: false,
default: 'confirm-danger-button',
},
+ buttonQaSelector: {
+ type: String,
+ required: false,
+ default: null,
+ },
buttonVariant: {
type: String,
required: false,
@@ -53,7 +58,7 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
- data-qa-selector="confirm_danger_button"
+ :data-qa-selector="buttonQaSelector"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
index 7ecc309db52..b56434f746e 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import ConfirmDanger from './confirm_danger.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
index 8256d953466..a48b8bcfa8e 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import { __ } from '~/locale';
import DropdownWidget from './dropdown_widget.vue';
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 2227047a909..8a3a174f414 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -45,7 +45,7 @@ export default {
},
levelIndentation() {
return {
- marginLeft: this.level ? `${this.level * 16}px` : null,
+ marginLeft: this.level ? `${this.level * 8}px` : null,
};
},
fileClass() {
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 4873996d357..755ce004aa9 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
@@ -13,11 +13,19 @@ export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
export const OPERATOR_IS_NOT = '!=';
-export const OPERATOR_IS_NOT_TEXT = __('is not');
+export const OPERATOR_IS_NOT_TEXT = __('is not one of');
+export const OPERATOR_OR = '||';
+export const OPERATOR_OR_TEXT = __('is one of');
export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }];
+export const OPERATOR_OR_ONLY = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
+export const OPERATOR_IS_NOT_OR = [
+ ...OPERATOR_IS_ONLY,
+ ...OPERATOR_IS_NOT_ONLY,
+ ...OPERATOR_OR_ONLY,
+];
export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
@@ -55,10 +63,26 @@ export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
+export const TOKEN_TITLE_STATUS = __('Status');
+export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TYPE_ASSIGNEE = 'assignee';
+export const TOKEN_TYPE_AUTHOR = 'author';
+export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
+export const TOKEN_TYPE_CONTACT = 'contact';
+export const TOKEN_TYPE_EPIC = 'epic';
// As health status gets reused between issue lists and boards
// 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_HEALTH = 'health_status';
+export const TOKEN_TYPE_HEALTH = 'health';
+export const TOKEN_TYPE_ITERATION = 'iteration';
+export const TOKEN_TYPE_LABEL = 'label';
+export const TOKEN_TYPE_MILESTONE = 'milestone';
+export const TOKEN_TYPE_MY_REACTION = 'my-reaction';
+export const TOKEN_TYPE_ORGANIZATION = 'organization';
+export const TOKEN_TYPE_RELEASE = 'release';
+export const TOKEN_TYPE_TYPE = 'type';
+export const TOKEN_TYPE_WEIGHT = 'weight';
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 8821084ef35..0d0787e7033 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
@@ -89,6 +89,11 @@ export default {
required: false,
default: () => ({}),
},
+ showFriendlyText: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
syncFilterAndSort: {
type: Boolean,
required: false,
@@ -319,7 +324,7 @@ export default {
(sortBy) =>
sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
);
- this.selectedSortDirection = Object.keys(this.selectedSortOption.sortDirection).find(
+ this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find(
(key) => this.selectedSortOption.sortDirection[key] === sort,
);
},
@@ -351,6 +356,7 @@ export default {
:close-button-title="__('Close')"
:clear-recent-searches-text="__('Clear recent searches')"
:no-recent-searches-text="__(`You don't have any recent searches`)"
+ :show-friendly-text="showFriendlyText"
class="flex-grow-1"
@history-item-selected="handleHistoryItemSelected"
@clear="onClear"
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 482a2964b4c..2f10e068542 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
@@ -129,6 +129,8 @@ export default {
v-gl-tooltip.hover="toggleVisibilityLabel"
: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/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
deleted file mode 100644
index c2be5e4f7a1..00000000000
--- a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
-import { GlBadge } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-const STATUS_TYPES = {
- SUCCESS: 'success',
- WARNING: 'warning',
- DANGER: 'danger',
-};
-
-const UPGRADE_DOCS_URL = helpPagePath('update/index');
-
-export default {
- name: 'GitlabVersionCheck',
- components: {
- GlBadge,
- },
- mixins: [Tracking.mixin()],
- props: {
- size: {
- type: String,
- required: false,
- default: 'md',
- },
- },
- data() {
- return {
- status: null,
- };
- },
- computed: {
- title() {
- if (this.status === STATUS_TYPES.SUCCESS) {
- return s__('VersionCheck|Up to date');
- } else if (this.status === STATUS_TYPES.WARNING) {
- return s__('VersionCheck|Update available');
- } else if (this.status === STATUS_TYPES.DANGER) {
- return s__('VersionCheck|Update ASAP');
- }
-
- return null;
- },
- },
- created() {
- this.checkGitlabVersion();
- },
- methods: {
- checkGitlabVersion() {
- axios
- .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
- .then((res) => {
- if (res.data) {
- this.status = res.data.severity;
-
- this.track('rendered_version_badge', {
- label: this.title,
- });
- }
- })
- .catch(() => {
- // Silently fail
- this.status = null;
- });
- },
- onClick() {
- this.track('click_version_badge', { label: this.title });
- },
- },
- UPGRADE_DOCS_URL,
-};
-</script>
-
-<template>
- <!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
- <!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
- <span v-if="status" data-testid="badge-click-wrapper" @click="onClick">
- <gl-badge
- :href="$options.UPGRADE_DOCS_URL"
- class="version-check-badge"
- :variant="status"
- :size="size"
- >{{ title }}</gl-badge
- >
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js
new file mode 100644
index 00000000000..bc70936eb36
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/constants.js
@@ -0,0 +1,6 @@
+import { __ } from '~/locale';
+
+export const TOGGLE_TEXT = __('Search for a group');
+export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.');
+export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.');
+export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.');
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
new file mode 100644
index 00000000000..1de6c0121bc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
@@ -0,0 +1,195 @@
+<script>
+import { debounce } from 'lodash';
+import { GlListbox } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import Api from '~/api';
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { createAlert } from '~/flash';
+import { groupsPath } from './utils';
+import {
+ TOGGLE_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+ QUERY_TOO_SHORT_MESSAGE,
+} from './constants';
+
+const MINIMUM_QUERY_LENGTH = 3;
+
+export default {
+ components: {
+ GlListbox,
+ },
+ props: {
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ parentGroupID: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupsFilter: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ pristine: true,
+ searching: false,
+ searchString: '',
+ groups: [],
+ selectedValue: null,
+ selectedText: null,
+ };
+ },
+ computed: {
+ selected: {
+ set(value) {
+ this.selectedValue = value;
+ this.selectedText =
+ value === null ? null : this.groups.find((group) => group.value === value).full_name;
+ },
+ get() {
+ return this.selectedValue;
+ },
+ },
+ toggleText() {
+ return this.selectedText ?? this.$options.i18n.toggleText;
+ },
+ inputValue() {
+ return this.selectedValue ? this.selectedValue : '';
+ },
+ isSearchQueryTooShort() {
+ return this.searchString && this.searchString.length < MINIMUM_QUERY_LENGTH;
+ },
+ noResultsText() {
+ return this.isSearchQueryTooShort
+ ? this.$options.i18n.searchQueryTooShort
+ : this.$options.i18n.noResultsText;
+ },
+ },
+ created() {
+ this.fetchInitialSelection();
+ },
+ methods: {
+ search: debounce(function debouncedSearch(searchString) {
+ this.searchString = searchString;
+ if (this.isSearchQueryTooShort) {
+ this.groups = [];
+ } else {
+ this.fetchGroups(searchString);
+ }
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ async fetchGroups(searchString = '') {
+ this.searching = true;
+
+ try {
+ const { data } = await axios.get(
+ Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
+ {
+ params: {
+ search: searchString,
+ },
+ },
+ );
+ const groups = data.length ? data : data.results || [];
+
+ this.groups = groups.map((group) => ({
+ ...group,
+ value: String(group.id),
+ }));
+
+ this.searching = false;
+ } catch (error) {
+ createAlert({
+ message: FETCH_GROUPS_ERROR,
+ error,
+ parent: this.$el,
+ });
+ }
+ },
+ async fetchInitialSelection() {
+ if (!this.initialSelection) {
+ this.pristine = false;
+ return;
+ }
+ this.searching = true;
+ try {
+ const group = await Api.group(this.initialSelection);
+ this.selectedValue = this.initialSelection;
+ this.selectedText = group.full_name;
+ this.pristine = false;
+ this.searching = false;
+ } catch (error) {
+ createAlert({
+ message: FETCH_GROUP_ERROR,
+ error,
+ parent: this.$el,
+ });
+ }
+ },
+ onShown() {
+ if (!this.searchString && !this.groups.length) {
+ this.fetchGroups();
+ }
+ },
+ onReset() {
+ this.selected = null;
+ },
+ },
+ i18n: {
+ toggleText: TOGGLE_TEXT,
+ selectGroup: __('Select a group'),
+ reset: __('Reset'),
+ noResultsText: __('No results found.'),
+ searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-listbox
+ ref="listbox"
+ v-model="selected"
+ :header-text="$options.i18n.selectGroup"
+ :reset-button-label="$options.i18n.reset"
+ :toggle-text="toggleText"
+ :loading="searching && pristine"
+ :searching="searching"
+ :items="groups"
+ :no-results-text="noResultsText"
+ searchable
+ @shown="onShown"
+ @search="search"
+ @reset="onReset"
+ >
+ <template #list-item="{ item }">
+ <div class="gl-font-weight-bold">
+ {{ item.full_name }}
+ </div>
+ <div class="gl-text-gray-300">{{ item.full_path }}</div>
+ </template>
+ </gl-listbox>
+ <div class="flash-container"></div>
+ <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index 1b89bd324c6..f349aa78bac 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -20,6 +20,11 @@ export default {
required: false,
default: () => ({}),
},
+ icon: {
+ type: String,
+ required: false,
+ default: 'question-o',
+ },
},
methods: {
targetFn() {
@@ -30,7 +35,7 @@ export default {
</script>
<template>
<span>
- <gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
+ <gl-button ref="popoverTrigger" variant="link" :icon="icon" :aria-label="__('Help')" />
<gl-popover :target="targetFn" v-bind="options">
<template v-if="options.title" #title>
<span v-safe-html="options.title"></span>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index b38772d5aa5..c0712e46613 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -72,7 +72,7 @@ export default {
required: false,
default: '',
},
- initOnAutofocus: {
+ autofocus: {
type: Boolean,
required: false,
default: false,
@@ -87,20 +87,20 @@ export default {
return {
editingMode: EDITING_MODE_MARKDOWN_FIELD,
switchEditingControlEnabled: true,
- autofocus: this.initOnAutofocus,
+ autofocused: false,
};
},
computed: {
isContentEditorActive() {
return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
},
- contentEditorAutofocus() {
+ contentEditorAutofocused() {
// Match textarea focus behavior
- return this.autofocus ? 'end' : false;
+ return this.autofocus && !this.autofocused ? 'end' : false;
},
},
mounted() {
- this.autofocusTextarea(this.editingMode);
+ this.autofocusTextarea();
},
methods: {
updateMarkdownFromContentEditor({ markdown }) {
@@ -120,7 +120,6 @@ export default {
},
onEditingModeChange(editingMode) {
this.notifyEditingModeChange(editingMode);
- this.enableAutofocus(editingMode);
},
onEditingModeRestored(editingMode) {
this.notifyEditingModeChange(editingMode);
@@ -128,15 +127,15 @@ export default {
notifyEditingModeChange(editingMode) {
this.$emit(editingMode);
},
- enableAutofocus(editingMode) {
- this.autofocus = true;
- this.autofocusTextarea(editingMode);
- },
- autofocusTextarea(editingMode) {
- if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
+ autofocusTextarea() {
+ if (this.autofocus && this.editingMode === EDITING_MODE_MARKDOWN_FIELD) {
this.$refs.textarea.focus();
+ this.setEditorAsAutofocused();
}
},
+ setEditorAsAutofocused() {
+ this.autofocused = true;
+ },
},
switchEditingControlOptions: [
{ text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
@@ -197,7 +196,8 @@ export default {
:render-markdown="renderMarkdown"
:uploads-path="uploadsPath"
:markdown="value"
- :autofocus="contentEditorAutofocus"
+ :autofocus="contentEditorAutofocused"
+ @initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
@loading="disableSwitchEditingControl"
@loadingSuccess="enableSwitchEditingControl"
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
new file mode 100644
index 00000000000..03bd64e2a57
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
@@ -0,0 +1,54 @@
+import { GlButton } from '@gitlab/ui';
+import { MOCK_HTML } from '../../../../../../spec/frontend/vue_shared/components/markdown_drawer/mock_data';
+import MarkdownDrawer from './markdown_drawer.vue';
+
+export default {
+ component: MarkdownDrawer,
+ title: 'vue_shared/markdown_drawer',
+ parameters: {
+ mirage: {
+ timing: 1000,
+ handlers: {
+ get: {
+ '/help/user/search/global_search/advanced_search_syntax.json': [
+ 200,
+ {},
+ { html: MOCK_HTML },
+ ],
+ },
+ },
+ },
+ },
+};
+
+const createStory = ({ ...options }) => (_, { argTypes }) => ({
+ components: { MarkdownDrawer, GlButton },
+ props: Object.keys(argTypes),
+ data() {
+ return {
+ render: false,
+ };
+ },
+ methods: {
+ toggleDrawer() {
+ this.$refs.drawer.toggleDrawer();
+ },
+ },
+ mounted() {
+ window.requestAnimationFrame(() => {
+ this.render = true;
+ });
+ },
+ template: `
+ <div v-if="render">
+ <gl-button @click="toggleDrawer">Open Drawer</gl-button>
+ <markdown-drawer
+ :documentPath="'user/search/global_search/advanced_search_syntax.json'"
+ ref="drawer"
+ />
+ </div>
+ `,
+ ...options,
+});
+
+export const Default = createStory({});
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
new file mode 100644
index 00000000000..a4b509f8656
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
@@ -0,0 +1,117 @@
+<script>
+import { GlSafeHtmlDirective as SafeHtml, GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
+import { s__ } from '~/locale';
+import { contentTop } from '~/lib/utils/common_utils';
+import { getRenderedMarkdown } from './utils/fetch';
+
+export const cache = {};
+
+export default {
+ name: 'MarkdownDrawer',
+ components: {
+ GlDrawer,
+ GlAlert,
+ GlSkeletonLoader,
+ },
+ directives: {
+ SafeHtml,
+ },
+ i18n: {
+ alert: s__('MardownDrawer|Could not fetch help contents.'),
+ },
+ props: {
+ documentPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ hasFetchError: false,
+ title: '',
+ body: null,
+ open: false,
+ };
+ },
+ computed: {
+ drawerOffsetTop() {
+ return `${contentTop()}px`;
+ },
+ },
+ watch: {
+ documentPath: {
+ immediate: true,
+ handler: 'fetchMarkdown',
+ },
+ open(open) {
+ if (open && this.body) {
+ this.renderGLFM();
+ }
+ },
+ },
+ methods: {
+ async fetchMarkdown() {
+ const cached = cache[this.documentPath];
+ this.hasFetchError = false;
+ this.title = '';
+ if (cached) {
+ this.title = cached.title;
+ this.body = cached.body;
+ if (this.open) {
+ this.renderGLFM();
+ }
+ } else {
+ this.loading = true;
+ const { body, title, hasFetchError } = await getRenderedMarkdown(this.documentPath);
+ this.title = title;
+ this.body = body;
+ this.loading = false;
+ this.hasFetchError = hasFetchError;
+ if (this.open) {
+ this.renderGLFM();
+ }
+ cache[this.documentPath] = { title, body };
+ }
+ },
+ renderGLFM() {
+ this.$nextTick(() => {
+ $(this.$refs['content-element']).renderGFM();
+ });
+ },
+ closeDrawer() {
+ this.open = false;
+ },
+ toggleDrawer() {
+ this.open = !this.open;
+ },
+ openDrawer() {
+ this.open = true;
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['copy-code'],
+ },
+};
+</script>
+<template>
+ <gl-drawer :header-height="drawerOffsetTop" :open="open" header-sticky @close="closeDrawer">
+ <template #title>
+ <h4 data-testid="title-element" class="gl-m-0">{{ title }}</h4>
+ </template>
+ <template #default>
+ <div v-if="hasFetchError">
+ <gl-alert :dismissible="false" variant="danger">{{ $options.i18n.alert }}</gl-alert>
+ </div>
+ <gl-skeleton-loader v-else-if="loading" />
+ <div
+ v-else
+ ref="content-element"
+ v-safe-html:[$options.safeHtmlConfig]="body"
+ class="md"
+ ></div>
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
new file mode 100644
index 00000000000..7c8e1bc160a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -0,0 +1,32 @@
+import * as Sentry from '@sentry/browser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import axios from '~/lib/utils/axios_utils';
+
+export const splitDocument = (htmlString) => {
+ const htmlDocument = new DOMParser().parseFromString(htmlString, 'text/html');
+ const title = htmlDocument.querySelector('h1')?.innerText;
+ htmlDocument.querySelector('h1')?.remove();
+ return {
+ title,
+ body: htmlDocument.querySelector('body').innerHTML.toString(),
+ };
+};
+
+export const getRenderedMarkdown = (documentPath) => {
+ return axios
+ .get(helpPagePath(documentPath))
+ .then(({ data }) => {
+ const { body, title } = splitDocument(data.html);
+ return {
+ body,
+ title,
+ hasFetchError: false,
+ };
+ })
+ .catch((e) => {
+ Sentry.captureException(e);
+ return {
+ hasFetchError: true,
+ };
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
deleted file mode 100644
index ba9edc7620a..00000000000
--- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
+++ /dev/null
@@ -1,212 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export const EMPTY_NAMESPACE_ID = -1;
-export const i18n = {
- DEFAULT_TEXT: __('Select a new namespace'),
- DEFAULT_EMPTY_NAMESPACE_TEXT: __('No namespace'),
- GROUPS: __('Groups'),
- USERS: __('Users'),
-};
-
-const filterByName = (data, searchTerm = '') => {
- if (!searchTerm) {
- return data;
- }
-
- return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase()));
-};
-
-export default {
- name: 'NamespaceSelectDeprecated',
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
- },
- props: {
- groupNamespaces: {
- type: Array,
- required: false,
- default: () => [],
- },
- userNamespaces: {
- type: Array,
- required: false,
- default: () => [],
- },
- fullWidth: {
- type: Boolean,
- required: false,
- default: false,
- },
- defaultText: {
- type: String,
- required: false,
- default: i18n.DEFAULT_TEXT,
- },
- includeHeaders: {
- type: Boolean,
- required: false,
- default: true,
- },
- emptyNamespaceTitle: {
- type: String,
- required: false,
- default: i18n.DEFAULT_EMPTY_NAMESPACE_TEXT,
- },
- includeEmptyNamespace: {
- type: Boolean,
- required: false,
- default: false,
- },
- hasNextPageOfGroups: {
- type: Boolean,
- required: false,
- default: false,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- isSearchLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldFilterNamespaces: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- searchTerm: '',
- selectedNamespace: null,
- };
- },
- computed: {
- hasUserNamespaces() {
- return this.userNamespaces.length;
- },
- hasGroupNamespaces() {
- return this.groupNamespaces.length;
- },
- filteredGroupNamespaces() {
- if (!this.shouldFilterNamespaces) return this.groupNamespaces;
- if (!this.hasGroupNamespaces) return [];
- return filterByName(this.groupNamespaces, this.searchTerm);
- },
- filteredUserNamespaces() {
- if (!this.shouldFilterNamespaces) return this.userNamespaces;
- if (!this.hasUserNamespaces) return [];
- return filterByName(this.userNamespaces, this.searchTerm);
- },
- selectedNamespaceText() {
- return this.selectedNamespace?.humanName || this.defaultText;
- },
- filteredEmptyNamespaceTitle() {
- const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this;
-
- if (!includeEmptyNamespace) {
- return '';
- }
- if (!searchTerm) {
- return emptyNamespaceTitle;
- }
-
- return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
- },
- },
- watch: {
- searchTerm() {
- this.$emit('search', this.searchTerm);
- },
- },
- methods: {
- handleSelect(item) {
- this.selectedNamespace = item;
- this.searchTerm = '';
- this.$emit('select', item);
- },
- handleSelectEmptyNamespace() {
- this.handleSelect({ id: EMPTY_NAMESPACE_ID, humanName: this.emptyNamespaceTitle });
- },
- },
- i18n,
-};
-</script>
-<template>
- <gl-dropdown
- :text="selectedNamespaceText"
- :block="fullWidth"
- data-qa-selector="namespaces_list"
- @show="$emit('show')"
- >
- <template #header>
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :is-loading="isSearchLoading"
- data-qa-selector="namespaces_list_search"
- />
- </template>
- <div v-if="filteredEmptyNamespaceTitle">
- <gl-dropdown-item
- data-qa-selector="namespaces_list_item"
- @click="handleSelectEmptyNamespace()"
- >
- {{ emptyNamespaceTitle }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- </div>
- <div
- v-if="hasUserNamespaces"
- data-qa-selector="namespaces_list_users"
- data-testid="namespace-list-users"
- >
- <gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.USERS
- }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in filteredUserNamespaces"
- :key="item.id"
- data-qa-selector="namespaces_list_item"
- @click="handleSelect(item)"
- >{{ item.humanName }}</gl-dropdown-item
- >
- </div>
- <div
- v-if="hasGroupNamespaces"
- data-qa-selector="namespaces_list_groups"
- data-testid="namespace-list-groups"
- >
- <gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.GROUPS
- }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in filteredGroupNamespaces"
- :key="item.id"
- data-qa-selector="namespaces_list_item"
- @click="handleSelect(item)"
- >{{ item.humanName }}</gl-dropdown-item
- >
- </div>
- <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
- <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" />
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index a5027d2ca5c..867222279b2 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -9,9 +9,12 @@ import {
} from '@gitlab/ui';
import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
@@ -112,7 +115,7 @@ export default {
{
type: 'author_username',
icon: 'user',
- title: __('Author'),
+ title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
token: AuthorToken,
@@ -123,7 +126,7 @@ export default {
{
type: 'assignee_username',
icon: 'user',
- title: __('Assignee'),
+ title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
token: AuthorToken,
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
index f16afc77164..fd9d69bae22 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import PaginationBar from './pagination_bar.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
index 0f5560ff628..02323e5a0c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
@@ -43,6 +43,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -128,7 +133,7 @@ export default {
</script>
<template>
- <div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
+ <div class="js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
<div
v-gl-tooltip.left.viewport
data-testid="move-collapsed"
@@ -141,7 +146,7 @@ export default {
<gl-dropdown
ref="dropdown"
:block="true"
- :disabled="moveInProgress"
+ :disabled="moveInProgress || disabled"
class="hide-collapsed"
toggle-class="js-sidebar-dropdown-toggle"
@shown="fetchProjects"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 0127df730b8..27186281c42 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -194,6 +194,7 @@ export default {
ref="dropdown"
:text="buttonText"
class="gl-w-full"
+ block
data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
index caeee2df7e5..314ffbaf84c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center gl-word-break-word">
<span
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ 'background-color': label.color }"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 0e8da7281d8..2c27a69d587 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -129,9 +129,6 @@ export default {
issuableId() {
return this.issuable?.id;
},
- isRealtimeEnabled() {
- return this.glFeatures.realtimeLabels;
- },
},
apollo: {
issuable: {
@@ -163,7 +160,7 @@ export default {
};
},
skip() {
- return !this.issuableId || !this.isDropdownVariantSidebar || !this.isRealtimeEnabled;
+ return !this.issuableId || !this.isDropdownVariantSidebar;
},
updateQuery(
_,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
new file mode 100644
index 00000000000..a1b16b378b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
@@ -0,0 +1,22 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
+ mergeRequestReviewersUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ reviewers {
+ nodes {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ canUpdate
+ approved
+ reviewed
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
index 8a2bab4cb9a..465ee9aa0d4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import TodoButton from './todo_button.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index 9683288f937..a2d8b7cbd15 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -1,5 +1,6 @@
<script>
import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
+import { scrollToElement } from '~/lib/utils/common_utils';
import ChunkLine from './chunk_line.vue';
/*
@@ -23,6 +24,11 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
props: {
+ isFirstChunk: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
chunkIndex: {
type: Number,
required: false,
@@ -46,6 +52,11 @@ export default {
required: false,
default: 0,
},
+ totalChunks: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
language: {
type: String,
required: false,
@@ -56,53 +67,68 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
computed: {
lines() {
return this.content.split('\n');
},
},
+
+ created() {
+ if (this.isFirstChunk) {
+ this.isLoading = false;
+ return;
+ }
+
+ window.requestIdleCallback(() => {
+ this.isLoading = false;
+ const { hash } = this.$route;
+ if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) {
+ // when the last chunk is loaded scroll to the hash
+ scrollToElement(hash, { behavior: 'auto' });
+ }
+ });
+ },
methods: {
handleChunkAppear() {
if (!this.isHighlighted) {
this.$emit('appear', this.chunkIndex);
}
},
+ calculateLineNumber(index) {
+ return this.startingFrom + index + 1;
+ },
},
};
</script>
<template>
- <div>
- <gl-intersection-observer @appear="handleChunkAppear">
- <div v-if="isHighlighted">
- <chunk-line
- v-for="(line, index) in lines"
+ <gl-intersection-observer @appear="handleChunkAppear">
+ <div v-if="isHighlighted">
+ <chunk-line
+ v-for="(line, index) in lines"
+ :key="index"
+ :number="calculateLineNumber(index)"
+ :content="line"
+ :language="language"
+ :blame-path="blamePath"
+ />
+ </div>
+ <div v-else-if="!isLoading" class="gl-display-flex gl-text-transparent">
+ <div class="gl-display-flex gl-flex-direction-column content-visibility-auto">
+ <span
+ v-for="(n, index) in totalLines"
+ v-once
+ :id="`L${calculateLineNumber(index)}`"
:key="index"
- :number="startingFrom + index + 1"
- :content="line"
- :language="language"
- :blame-path="blamePath"
- />
- </div>
- <div v-else class="gl-display-flex">
- <div class="gl-display-flex gl-flex-direction-column">
- <a
- v-for="(n, index) in totalLines"
- :id="`L${startingFrom + index + 1}`"
- :key="index"
- class="gl-ml-5 gl-text-transparent"
- :href="`#L${startingFrom + index + 1}`"
- :data-line-number="startingFrom + index + 1"
- data-testid="line-number"
- >
- {{ startingFrom + index + 1 }}
- </a>
- </div>
- <div
- class="gl-white-space-pre-wrap! gl-text-transparent"
- data-testid="content"
- v-text="content"
- ></div>
+ data-testid="line-number"
+ v-text="calculateLineNumber(index)"
+ ></span>
</div>
- </gl-intersection-observer>
- </div>
+ <div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div>
+ </div>
+ </gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index ffd0eea63a1..0bf19f83d86 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,6 +1,7 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getPageParamValue, getPageSearchString } from '~/blob/utils';
export default {
directives: {
@@ -25,6 +26,13 @@ export default {
required: true,
},
},
+ computed: {
+ pageSearchString() {
+ if (!this.glFeatures.fileLineBlame) return '';
+ const page = getPageParamValue(this.number);
+ return getPageSearchString(this.blamePath, page);
+ },
+ },
};
</script>
<template>
@@ -35,7 +43,7 @@ export default {
<a
v-if="glFeatures.fileLineBlame"
class="gl-user-select-none gl-shadow-none! file-line-blame"
- :href="`${blamePath}#L${number}`"
+ :href="`${blamePath}${pageSearchString}#L${number}`"
></a>
<a
:id="`L${number}`"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
index d957990fe7f..fca2616f069 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -1,9 +1,17 @@
import packageJsonLinker from './utils/package_json_linker';
import gemspecLinker from './utils/gemspec_linker';
+import godepsJsonLinker from './utils/godeps_json_linker';
+import gemfileLinker from './utils/gemfile_linker';
+import podspecJsonLinker from './utils/podspec_json_linker';
+import composerJsonLinker from './utils/composer_json_linker';
const DEPENDENCY_LINKERS = {
package_json: packageJsonLinker,
gemspec: gemspecLinker,
+ godeps_json: godepsJsonLinker,
+ gemfile: gemfileLinker,
+ podspec_json: podspecJsonLinker,
+ composer_json: composerJsonLinker,
};
/**
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js
new file mode 100644
index 00000000000..f5c4c886546
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js
@@ -0,0 +1,49 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const PACKAGIST_URL = 'https://packagist.org/packages/';
+const DRUPAL_URL = 'https://www.drupal.org/project/';
+
+const attrOpenTag = generateHLJSOpenTag('attr');
+const stringOpenTag = generateHLJSOpenTag('string');
+const closeTag = '&quot;</span>';
+const DRUPAL_PROJECT_SEPARATOR = 'drupal/';
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;composer/installers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^1.2&quot;</span>
+ * Group 1: composer/installers
+ * Group 2: ^1.2
+ */
+ `${attrOpenTag}([^/]+/[^/]+.)${closeTag}.*${stringOpenTag}(.*[0-9].*)(${closeTag})`,
+ 'gm',
+);
+
+const handleReplace = (original, packageName, version, dependenciesToLink) => {
+ const isDrupalDependency = packageName.includes(DRUPAL_PROJECT_SEPARATOR);
+ const href = isDrupalDependency
+ ? `${DRUPAL_URL}${packageName.split(DRUPAL_PROJECT_SEPARATOR)[1]}`
+ : `${PACKAGIST_URL}${packageName}`;
+ const packageLink = createLink(href, packageName);
+ const versionLink = createLink(href, version);
+ const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
+ const dependencyToLink = dependenciesToLink[packageName];
+
+ if (dependencyToLink && dependencyToLink === version) {
+ return `${attrOpenTag}${packageLink}${closeAndOpenTag}${versionLink}${closeTag}`;
+ }
+
+ return original;
+};
+
+export default (result, raw) => {
+ const rawParsed = JSON.parse(raw);
+
+ const dependenciesToLink = {
+ ...rawParsed.require,
+ ...rawParsed['require-dev'],
+ };
+
+ return result.value.replace(DEPENDENCY_REGEX, (original, packageName, version) =>
+ handleReplace(original, packageName, version, dependenciesToLink),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index 49704421d6e..c1a1101afad 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -1,7 +1,27 @@
import { escape } from 'lodash';
export const createLink = (href, innerText) =>
- `<a href="${escape(href)}" rel="nofollow noreferrer noopener">${escape(innerText)}</a>`;
+ `<a href="${escape(href)}" target="_blank" rel="nofollow noreferrer noopener">${escape(
+ innerText,
+ )}</a>`;
export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
`<span class="hljs-${escape(type)}">${delimiter}`;
+
+export const getObjectKeysByKeyName = (obj, keyName, acc) => {
+ if (obj instanceof Array) {
+ obj.map((subObj) => getObjectKeysByKeyName(subObj, keyName, acc));
+ } else {
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ if (key === keyName) {
+ acc.push(...Object.keys(obj[key]));
+ }
+ if (obj[key] instanceof Object || obj[key] instanceof Array) {
+ getObjectKeysByKeyName(obj[key], keyName, acc);
+ }
+ }
+ }
+ }
+ return acc;
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js
new file mode 100644
index 00000000000..81389763f49
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js
@@ -0,0 +1,25 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const GEM_URL = 'https://rubygems.org/gems/';
+const GEM_STRING = 'gem </span>';
+const delimiter = '&#39;';
+const stringOpenTag = generateHLJSOpenTag('string', delimiter);
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: 'gem </span><span class="hljs-string">&#39;paranoia&#39;'
+ * Group 1 (packageName) : 'paranoia'
+ */
+ `${GEM_STRING}${stringOpenTag}(.+?(?=${delimiter}))`,
+ 'gm',
+);
+
+const handleReplace = (packageName) => {
+ const href = `${GEM_URL}${packageName}`;
+ const packageLink = createLink(href, packageName);
+ return `${GEM_STRING}${stringOpenTag}${packageLink}`;
+};
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, packageName) => handleReplace(packageName));
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js
new file mode 100644
index 00000000000..bff8e3cf410
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js
@@ -0,0 +1,64 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const PROTOCOL = 'https://';
+const GODOCS_DOMAIN = 'godoc.org/';
+const REPO_PATH = '/tree/master/';
+const GODOCS_REGEX = /golang.org/;
+const GITLAB_REPO_PATH = `/_${REPO_PATH}`;
+const REPO_REGEX = `[^/'"]+/[^/'"]+`;
+const NESTED_REPO_REGEX = '([^/]+/)+[^/]+?';
+const GITHUB_REPO_REGEX = new RegExp(`(github.com/${REPO_REGEX})/(.+)`);
+const GITLAB_REPO_REGEX = new RegExp(`(gitlab.com/${REPO_REGEX})/(.+)`);
+const GITLAB_NESTED_REPO_REGEX = new RegExp(`(gitlab.com/${NESTED_REPO_REGEX}).git/(.+)`);
+const attrOpenTag = generateHLJSOpenTag('attr');
+const stringOpenTag = generateHLJSOpenTag('string');
+const closeTag = '&quot;</span>';
+const importPathString =
+ 'ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span>';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;github.com/ayufan/golang-kardianos-service&quot;</span>
+ * Group 1: github.com/ayufan/golang-kardianos-service
+ */
+ `${importPathString}${stringOpenTag}(.*)${closeTag}`,
+ 'gm',
+);
+
+const replaceRepoPath = (dependency, regex, repoPath) =>
+ dependency.replace(regex, (_, repo, path) => `${PROTOCOL}${repo}${repoPath}${path}`);
+
+const regexConfigs = [
+ {
+ matcher: GITHUB_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITHUB_REPO_REGEX, REPO_PATH),
+ },
+ {
+ matcher: GITLAB_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITLAB_REPO_REGEX, GITLAB_REPO_PATH),
+ },
+ {
+ matcher: GITLAB_NESTED_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITLAB_NESTED_REPO_REGEX, GITLAB_REPO_PATH),
+ },
+ {
+ matcher: GODOCS_REGEX,
+ resolver: (dep) => `${PROTOCOL}${GODOCS_DOMAIN}${dep}`,
+ },
+];
+
+const getLinkHref = (dependency) => {
+ const regexConfig = regexConfigs.find((config) => dependency.match(config.matcher));
+ return regexConfig ? regexConfig.resolver(dependency) : `${PROTOCOL}${dependency}`;
+};
+
+const handleReplace = (dependency) => {
+ const linkHref = getLinkHref(dependency);
+ const link = createLink(linkHref, dependency);
+ return `${importPathString}${attrOpenTag}${link}${closeTag}`;
+};
+
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, dependency) => handleReplace(dependency));
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js
new file mode 100644
index 00000000000..e2007fe408b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js
@@ -0,0 +1,32 @@
+import { createLink, generateHLJSOpenTag, getObjectKeysByKeyName } from './dependency_linker_util';
+
+const COCOAPODS_URL = 'https://cocoapods.org/pods/';
+const beginString = generateHLJSOpenTag('attr');
+const endString =
+ '&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">\\[';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;AFNetworking/Security&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation"> [
+ * Group 1: AFNetworking/Serialization
+ */
+ `${beginString}([^/]+/?[^/]+.)${endString}`,
+ 'gm',
+);
+
+const handleReplace = (original, dependency, dependenciesToLink) => {
+ if (dependenciesToLink.includes(dependency)) {
+ const href = `${COCOAPODS_URL}${dependency.split('/')[0]}`;
+ const link = createLink(href, dependency);
+ return `${beginString}${link}${endString.replace('\\', '')}`;
+ }
+ return original;
+};
+
+export default (result, raw) => {
+ const dependenciesToLink = getObjectKeysByKeyName(JSON.parse(raw), 'dependencies', []);
+ return result.value.replace(DEPENDENCY_REGEX, (original, dependency) =>
+ handleReplace(original, dependency, dependenciesToLink),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
index e0ba4b730a7..3540ac6caf1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
@@ -22,7 +22,7 @@ const format = (node, kind = '') => {
.split(newlineRegex)
.map((newline) => generateHLJSTag(kind, newline, true))
.join('\n');
- } else if (node.kind) {
+ } else if (node.kind || node.sublanguage) {
const { children } = node;
if (children.length && children.length === 1) {
buffer += format(children[0], node.kind);
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 536b2c8a281..f621a23734a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -65,6 +65,9 @@ export default {
!supportedLanguages.includes(this.blob.language?.toLowerCase())
);
},
+ totalChunks() {
+ return Object.keys(this.chunks).length;
+ },
},
async created() {
addBlobLinksTracking();
@@ -200,6 +203,7 @@ export default {
:content="firstChunk.content"
:starting-from="firstChunk.startingFrom"
:is-highlighted="firstChunk.isHighlighted"
+ is-first-chunk
:language="firstChunk.language"
:blame-path="blob.blamePath"
/>
@@ -217,6 +221,7 @@ export default {
:chunk-index="index"
:language="chunk.language"
:blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
@appear="highlightChunk"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
index e621442e601..84615386fe2 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import TooltipOnTruncate from './tooltip_on_truncate.vue';
const defaultWidth = '250px';
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
index 1f0f4cde234..0815fdd9aac 100644
--- a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import { OBSTACLE_TYPES } from './constants';
import UserDeletionObstaclesList from './user_deletion_obstacles_list.vue';