summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
commit71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch)
tree6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /app/assets/javascripts/vue_shared/components
parenta7253423e3403b8c08f8a161e5937e1488f5f407 (diff)
downloadgitlab-ce-71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e.tar.gz
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue (renamed from app/assets/javascripts/vue_shared/components/group_select/group_select.vue)140
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue137
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js (renamed from app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js48
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue168
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/utils.js (renamed from app/assets/javascripts/vue_shared/components/group_select/utils.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js610
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/file_tree.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/incubation/incubation_alert.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/incubation/pagination.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/constants.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql18
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql21
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown.js46
-rw-r--r--app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue208
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue118
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue213
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue227
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js50
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue1
48 files changed, 1523 insertions, 1114 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 49181bb847d..3a3929fba9b 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
@@ -16,7 +16,7 @@ export default {
handleBlobRichViewer(this.$refs.content, this.type);
},
safeHtmlConfig: {
- ADD_TAGS: ['copy-code'],
+ ADD_TAGS: ['gl-emoji', 'copy-code'],
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 271cfd210a6..52a5d6e1b86 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -42,11 +42,6 @@ export default {
required: false,
default: true,
},
- iconClasses: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
title() {
@@ -73,7 +68,7 @@ export default {
:href="detailsPath"
@click="$emit('ciStatusBadgeClick')"
>
- <ci-icon :status="status" :css-classes="iconClasses" />
+ <ci-icon :status="status" />
<template v-if="showText">
<span class="gl-ml-2">{{ status.text }}</span>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
new file mode 100644
index 00000000000..0fb5a2d5534
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
@@ -0,0 +1,16 @@
+import { __, s__ } from '~/locale';
+
+export const RESET_LABEL = __('Reset');
+export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.');
+
+// Groups
+export const GROUP_TOGGLE_TEXT = __('Search for a group');
+export const GROUP_HEADER_TEXT = __('Select 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.');
+
+// Projects
+export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
+export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
+export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
+export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index d295052e2ce..45c50dce8ce 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -1,28 +1,15 @@
<script>
import { debounce } from 'lodash';
-import { GlFormGroup, GlAlert, GlCollapsibleListbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import axios from '~/lib/utils/axios_utils';
-import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import Api from '~/api';
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { groupsPath } from './utils';
-import {
- TOGGLE_TEXT,
- RESET_LABEL,
- FETCH_GROUPS_ERROR,
- FETCH_GROUP_ERROR,
- QUERY_TOO_SHORT_MESSAGE,
-} from './constants';
+import { RESET_LABEL, QUERY_TOO_SHORT_MESSAGE } from './constants';
const MINIMUM_QUERY_LENGTH = 3;
-const GROUPS_PER_PAGE = 20;
export default {
components: {
GlFormGroup,
- GlAlert,
GlCollapsibleListbox,
},
props: {
@@ -48,13 +35,20 @@ export default {
required: false,
default: false,
},
- parentGroupID: {
+ headerText: {
type: String,
- required: false,
- default: null,
+ required: true,
},
- groupsFilter: {
+ defaultToggleText: {
type: String,
+ required: true,
+ },
+ fetchItems: {
+ type: Function,
+ required: true,
+ },
+ fetchInitialSelectionText: {
+ type: Function,
required: false,
default: null,
},
@@ -63,10 +57,10 @@ export default {
return {
pristine: true,
searching: false,
- hasMoreGroups: true,
+ hasMoreItems: true,
infiniteScrollLoading: false,
searchString: '',
- groups: [],
+ items: [],
page: 1,
selectedValue: null,
selectedText: null,
@@ -78,14 +72,14 @@ export default {
set(value) {
this.selectedValue = value;
this.selectedText =
- value === null ? null : this.groups.find((group) => group.value === value).full_name;
+ value === null ? null : this.items.find((item) => item.value === value).text;
},
get() {
return this.selectedValue;
},
},
toggleText() {
- return this.selectedText ?? this.$options.i18n.toggleText;
+ return this.selectedText ?? this.defaultToggleText;
},
resetButtonLabel() {
return this.clearable ? RESET_LABEL : '';
@@ -109,90 +103,64 @@ export default {
search: debounce(function debouncedSearch(searchString) {
this.searchString = searchString;
if (this.isSearchQueryTooShort) {
- this.groups = [];
+ this.items = [];
} else {
- this.fetchGroups();
+ this.fetchEntities();
}
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- async fetchGroups(page = 1) {
+ async fetchEntities(page = 1) {
if (page === 1) {
this.searching = true;
- this.groups = [];
- this.hasMoreGroups = true;
+ this.items = [];
+ this.hasMoreItems = true;
} else {
this.infiniteScrollLoading = true;
}
- try {
- const { data, headers } = await axios.get(
- Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
- {
- params: {
- search: this.searchString,
- per_page: GROUPS_PER_PAGE,
- page,
- },
- },
- );
- const groups = data.length ? data : data.results || [];
-
- this.groups.push(
- ...groups.map((group) => ({
- ...group,
- value: String(group.id),
- })),
- );
+ const { items, totalPages } = await this.fetchItems(this.searchString, page);
- const { totalPages } = parseIntPagination(normalizeHeaders(headers));
- if (page === totalPages) {
- this.hasMoreGroups = false;
- }
+ this.items.push(...items);
- this.page = page;
- this.searching = false;
- this.infiniteScrollLoading = false;
- } catch (error) {
- this.handleError({ message: FETCH_GROUPS_ERROR, error });
+ if (page === totalPages) {
+ this.hasMoreItems = false;
}
+
+ this.page = page;
+ this.searching = false;
+ this.infiniteScrollLoading = false;
},
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) {
- this.handleError({ message: FETCH_GROUP_ERROR, error });
+
+ if (!this.fetchInitialSelectionText) {
+ 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.pristine = false;
+ this.searching = false;
},
onShown() {
- if (!this.searchString && !this.groups.length) {
- this.fetchGroups();
+ if (!this.searchString && !this.items.length) {
+ this.fetchEntities();
}
},
onReset() {
this.selected = null;
},
onBottomReached() {
- this.fetchGroups(this.page + 1);
- },
- handleError({ message, error }) {
- Sentry.captureException(error);
- this.errorMessage = message;
- },
- dismissError() {
- this.errorMessage = '';
+ this.fetchEntities(this.page + 1);
},
},
i18n: {
- toggleText: TOGGLE_TEXT,
- selectGroup: __('Select a group'),
noResultsText: __('No results found.'),
searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE,
},
@@ -201,20 +169,21 @@ export default {
<template>
<gl-form-group :label="label">
- <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
- errorMessage
- }}</gl-alert>
+ <slot name="error"></slot>
+ <template v-if="Boolean($scopedSlots.label)" #label>
+ <slot name="label"></slot>
+ </template>
<gl-collapsible-listbox
ref="listbox"
v-model="selected"
- :header-text="$options.i18n.selectGroup"
+ :header-text="headerText"
:reset-button-label="resetButtonLabel"
:toggle-text="toggleText"
:loading="searching && pristine"
:searching="searching"
- :items="groups"
+ :items="items"
:no-results-text="noResultsText"
- :infinite-scroll="hasMoreGroups"
+ :infinite-scroll="hasMoreItems"
:infinite-scroll-loading="infiniteScrollLoading"
searchable
@shown="onShown"
@@ -223,10 +192,7 @@ export default {
@bottom-reached="onBottomReached"
>
<template #list-item="{ item }">
- <div class="gl-font-weight-bold">
- {{ item.full_name }}
- </div>
- <div class="gl-text-gray-300">{{ item.full_path }}</div>
+ <slot name="list-item" :item="item"></slot>
</template>
</gl-collapsible-listbox>
<input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
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
new file mode 100644
index 00000000000..ff137d764ee
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import axios from '~/lib/utils/axios_utils';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
+import Api, { DEFAULT_PER_PAGE } from '~/api';
+import { groupsPath } from './utils';
+import {
+ GROUP_TOGGLE_TEXT,
+ GROUP_HEADER_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+} from './constants';
+import EntitySelect from './entity_select.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EntitySelect,
+ },
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ 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 {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchGroups(searchString = '', page = 1) {
+ let groups = [];
+ let totalPages = 0;
+ try {
+ const { data = [], headers } = await axios.get(
+ Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
+ {
+ params: {
+ search: searchString,
+ per_page: DEFAULT_PER_PAGE,
+ page,
+ },
+ },
+ );
+ groups = data.map((group) => ({
+ ...group,
+ text: group.full_name,
+ value: String(group.id),
+ }));
+
+ totalPages = parseIntPagination(normalizeHeaders(headers)).totalPages;
+ } catch (error) {
+ this.handleError({ message: FETCH_GROUPS_ERROR, error });
+ }
+ return { items: groups, totalPages };
+ },
+ async fetchGroupName(groupId) {
+ let groupName = '';
+ try {
+ const group = await Api.group(groupId);
+ groupName = group.full_name;
+ } catch (error) {
+ this.handleError({ message: FETCH_GROUP_ERROR, error });
+ }
+ return groupName;
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ toggleText: GROUP_TOGGLE_TEXT,
+ selectGroup: GROUP_HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-select
+ :label="label"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :clearable="clearable"
+ :header-text="$options.i18n.selectGroup"
+ :default-toggle-text="$options.i18n.toggleText"
+ :fetch-items="fetchGroups"
+ :fetch-initial-selection-text="fetchGroupName"
+ >
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ <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>
+ </entity-select>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js b/app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js
index dbfac8a0339..dbfac8a0339 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/init_group_selects.js
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js
new file mode 100644
index 00000000000..1afbeda74c4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import ProjectSelect from './project_select.vue';
+
+const SELECTOR = '.js-vue-project-select';
+
+export const initProjectSelects = () => {
+ if (process.env.NODE_ENV !== 'production' && document.querySelector(SELECTOR) === null) {
+ // eslint-disable-next-line no-console
+ console.warn(`Attempted to initialize ProjectSelect but '${SELECTOR}' not found in the page`);
+ }
+
+ document.querySelectorAll(SELECTOR).forEach((el) => {
+ const {
+ label,
+ inputName,
+ inputId,
+ groupId,
+ userId,
+ orderBy,
+ selected: initialSelection,
+ } = el.dataset;
+ const includeSubgroups = parseBoolean(el.dataset.includeSubgroups);
+ const membership = parseBoolean(el.dataset.membership);
+ const hasHtmlLabel = parseBoolean(el.dataset.hasHtmlLabel);
+
+ return new Vue({
+ el,
+ name: 'ProjectSelectRoot',
+ render(createElement) {
+ return createElement(ProjectSelect, {
+ props: {
+ label,
+ hasHtmlLabel,
+ inputName,
+ inputId,
+ groupId,
+ userId,
+ orderBy,
+ includeSubgroups,
+ membership,
+ initialSelection,
+ },
+ });
+ },
+ });
+ });
+};
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
new file mode 100644
index 00000000000..393991d746e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -0,0 +1,168 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import Api from '~/api';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import {
+ PROJECT_TOGGLE_TEXT,
+ PROJECT_HEADER_TEXT,
+ FETCH_PROJECTS_ERROR,
+ FETCH_PROJECT_ERROR,
+} from './constants';
+import EntitySelector from './entity_select.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EntitySelector,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ hasHtmlLabel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ groupId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ userId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ includeSubgroups: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ membership: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ orderBy: {
+ type: String,
+ required: false,
+ default: 'similarity',
+ },
+ initialSelection: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchProjects(searchString = '') {
+ let projects = [];
+ try {
+ const { data = [] } = await (() => {
+ const commonParams = {
+ order_by: this.orderBy,
+ simple: true,
+ };
+
+ if (this.groupId) {
+ return Api.groupProjects(this.groupId, searchString, {
+ ...commonParams,
+ with_shared: true,
+ include_subgroups: this.includeSubgroups,
+ simple: true,
+ });
+ }
+ // Note: the whole userId handling supports a single project selector that is slated for
+ // removal. Once we have deleted app/views/clusters/clusters/_advanced_settings.html.haml,
+ // we should be able to clean this up.
+ if (this.userId) {
+ return Api.userProjects(
+ this.userId,
+ searchString,
+ {
+ with_shared: true,
+ include_subgroups: this.includeSubgroups,
+ },
+ (res) => ({ data: res }),
+ );
+ }
+ return Api.projects(searchString, {
+ ...commonParams,
+ membership: this.membership,
+ });
+ })();
+ projects = data.map((item) => ({
+ text: item.name_with_namespace || item.name,
+ value: String(item.id),
+ }));
+ } catch (error) {
+ this.handleError({ message: FETCH_PROJECTS_ERROR, error });
+ }
+ return { items: projects, totalPages: 1 };
+ },
+ async fetchProjectName(projectId) {
+ let projectName = '';
+ try {
+ const { data: project } = await Api.project(projectId);
+ projectName = project.name_with_namespace;
+ } catch (error) {
+ this.handleError({ message: FETCH_PROJECT_ERROR, error });
+ }
+ return projectName;
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ searchForProject: PROJECT_TOGGLE_TEXT,
+ selectProject: PROJECT_HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-selector
+ :label="label"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :header-text="$options.i18n.selectProject"
+ :default-toggle-text="$options.i18n.searchForProject"
+ :fetch-items="fetchProjects"
+ :fetch-initial-selection-text="fetchProjectName"
+ clearable
+ >
+ <template v-if="hasHtmlLabel" #label>
+ <span v-safe-html="label"></span>
+ </template>
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ </entity-selector>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/utils.js b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
index 0a4622269f4..0a4622269f4 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/utils.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index adf34f822ed..6a10557c6bc 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,7 +1,7 @@
<script>
+import { getIconForFile } from '@gitlab/svgs/src/file_icon_map';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { FILE_SYMLINK_MODE } from '../constants';
-import getIconForFile from './file_icon/file_icon_map';
/* This is a re-usable vue component for rendering a svg sprite
icon
@@ -88,7 +88,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
<gl-icon v-else-if="isSymlink" name="symlink" :size="size" />
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
- <use v-bind="{ 'xlink:href': spriteHref }" />
+ <use :href="spriteHref" />
</svg>
<gl-icon
v-else
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
deleted file mode 100644
index 8686d317c8a..00000000000
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ /dev/null
@@ -1,610 +0,0 @@
-const fileExtensionIcons = {
- html: 'html',
- htm: 'html',
- html_vm: 'html',
- asp: 'html',
- jade: 'pug',
- pug: 'pug',
- md: 'markdown',
- markdown: 'markdown',
- mdown: 'markdown',
- mkd: 'markdown',
- mkdn: 'markdown',
- rst: 'markdown',
- blink: 'blink',
- css: 'css',
- scss: 'sass',
- sass: 'sass',
- less: 'less',
- json: 'json',
- yaml: 'yaml',
- yml: 'yaml',
- xml: 'xml',
- plist: 'xml',
- xsd: 'xml',
- dtd: 'xml',
- xsl: 'xml',
- xslt: 'xml',
- resx: 'xml',
- iml: 'xml',
- xquery: 'xml',
- tmLanguage: 'xml',
- manifest: 'xml',
- project: 'xml',
- png: 'image',
- jpeg: 'image',
- jpg: 'image',
- gif: 'image',
- svg: 'image',
- ico: 'image',
- tif: 'image',
- tiff: 'image',
- psd: 'image',
- psb: 'image',
- ami: 'image',
- apx: 'image',
- bmp: 'image',
- bpg: 'image',
- brk: 'image',
- cur: 'image',
- dds: 'image',
- dng: 'image',
- exr: 'image',
- fpx: 'image',
- gbr: 'image',
- img: 'image',
- jbig2: 'image',
- jb2: 'image',
- jng: 'image',
- jxr: 'image',
- pbm: 'image',
- pgf: 'image',
- pic: 'image',
- raw: 'image',
- webp: 'image',
- js: 'javascript',
- ejs: 'javascript',
- esx: 'javascript',
- jsx: 'react',
- tsx: 'react',
- ini: 'settings',
- dlc: 'settings',
- dll: 'settings',
- config: 'settings',
- conf: 'settings',
- properties: 'settings',
- prop: 'settings',
- settings: 'settings',
- option: 'settings',
- props: 'settings',
- toml: 'settings',
- prefs: 'settings',
- ts: 'typescript',
- marko: 'markojs',
- pdf: 'pdf',
- xlsx: 'table',
- xls: 'table',
- ods: 'table',
- csv: 'table',
- tsv: 'table',
- vscodeignore: 'vscode',
- vsixmanifest: 'vscode',
- vsix: 'vscode',
- suo: 'visualstudio',
- sln: 'visualstudio',
- csproj: 'visualstudio',
- vb: 'visualstudio',
- pdb: 'database',
- sql: 'database',
- pks: 'database',
- pkb: 'database',
- accdb: 'database',
- mdb: 'database',
- sqlite: 'database',
- cs: 'csharp',
- zip: 'zip',
- tar: 'zip',
- gz: 'zip',
- xz: 'zip',
- bzip2: 'zip',
- gzip: 'zip',
- rar: 'zip',
- tgz: 'zip',
- exe: 'exe',
- msi: 'exe',
- java: 'java',
- jar: 'java',
- jsp: 'java',
- c: 'c',
- m: 'c',
- h: 'h',
- cc: 'cpp',
- cpp: 'cpp',
- mm: 'cpp',
- cxx: 'cpp',
- hpp: 'hpp',
- go: 'go',
- py: 'python',
- url: 'url',
- sh: 'console',
- ksh: 'console',
- csh: 'console',
- tcsh: 'console',
- zsh: 'console',
- bash: 'console',
- bat: 'console',
- cmd: 'console',
- ps1: 'powershell',
- psm1: 'powershell',
- psd1: 'powershell',
- ps1xml: 'powershell',
- psc1: 'powershell',
- pssc: 'powershell',
- gradle: 'gradle',
- doc: 'word',
- docx: 'word',
- odt: 'word',
- rtf: 'word',
- cer: 'certificate',
- cert: 'certificate',
- crt: 'certificate',
- pub: 'key',
- key: 'key',
- pem: 'key',
- asc: 'key',
- gpg: 'key',
- woff: 'font',
- woff2: 'font',
- ttf: 'font',
- eot: 'font',
- suit: 'font',
- otf: 'font',
- bmap: 'font',
- fnt: 'font',
- odttf: 'font',
- ttc: 'font',
- font: 'font',
- fonts: 'font',
- sui: 'font',
- ntf: 'font',
- mrf: 'font',
- lib: 'lib',
- bib: 'lib',
- rb: 'ruby',
- erb: 'ruby',
- fs: 'fsharp',
- fsx: 'fsharp',
- fsi: 'fsharp',
- fsproj: 'fsharp',
- swift: 'swift',
- ino: 'arduino',
- dockerignore: 'docker',
- dockerfile: 'docker',
- tex: 'tex',
- cls: 'tex',
- sty: 'tex',
- pptx: 'powerpoint',
- ppt: 'powerpoint',
- pptm: 'powerpoint',
- potx: 'powerpoint',
- pot: 'powerpoint',
- potm: 'powerpoint',
- ppsx: 'powerpoint',
- ppsm: 'powerpoint',
- pps: 'powerpoint',
- ppam: 'powerpoint',
- ppa: 'powerpoint',
- odp: 'powerpoint',
- webm: 'movie',
- mkv: 'movie',
- flv: 'movie',
- vob: 'movie',
- ogv: 'movie',
- ogg: 'music',
- gifv: 'movie',
- avi: 'movie',
- mov: 'movie',
- qt: 'movie',
- wmv: 'movie',
- yuv: 'movie',
- rm: 'movie',
- rmvb: 'movie',
- mp4: 'movie',
- m4v: 'movie',
- mpg: 'movie',
- mp2: 'movie',
- mpeg: 'movie',
- mpe: 'movie',
- mpv: 'movie',
- m2v: 'movie',
- vdi: 'virtual',
- vbox: 'virtual',
- ics: 'email',
- mp3: 'music',
- flac: 'music',
- m4a: 'music',
- wma: 'music',
- aiff: 'music',
- coffee: 'coffee',
- txt: 'document',
- graphql: 'graphql',
- rs: 'rust',
- raml: 'raml',
- xaml: 'xaml',
- hs: 'haskell',
- kt: 'kotlin',
- kts: 'kotlin',
- patch: 'git',
- lua: 'lua',
- clj: 'clojure',
- cljs: 'clojure',
- groovy: 'groovy',
- r: 'r',
- rmd: 'r',
- dart: 'dart',
- as: 'actionscript',
- mxml: 'mxml',
- ahk: 'autohotkey',
- swf: 'flash',
- swc: 'swc',
- cmake: 'cmake',
- asm: 'assembly',
- a51: 'assembly',
- inc: 'assembly',
- nasm: 'assembly',
- s: 'assembly',
- ms: 'assembly',
- agc: 'assembly',
- ags: 'assembly',
- aea: 'assembly',
- argus: 'assembly',
- mitigus: 'assembly',
- binsource: 'assembly',
- vue: 'vue',
- ml: 'ocaml',
- mli: 'ocaml',
- cmx: 'ocaml',
- lock: 'lock',
- hbs: 'handlebars',
- mustache: 'handlebars',
- pl: 'perl',
- pm: 'perl',
- hx: 'haxe',
- pp: 'puppet',
- ex: 'elixir',
- exs: 'elixir',
- ls: 'livescript',
- erl: 'erlang',
- twig: 'twig',
- jl: 'julia',
- elm: 'elm',
- pure: 'purescript',
- tpl: 'smarty',
- styl: 'stylus',
- re: 'reason',
- rei: 'reason',
- cmj: 'bucklescript',
- merlin: 'merlin',
- v: 'verilog',
- vhd: 'verilog',
- sv: 'verilog',
- svh: 'verilog',
- nb: 'mathematica',
- wl: 'wolframlanguage',
- wls: 'wolframlanguage',
- njk: 'nunjucks',
- nunjucks: 'nunjucks',
- robot: 'robot',
- sol: 'solidity',
- au3: 'autoit',
- haml: 'haml',
- yang: 'yang',
- tf: 'terraform',
- tfvars: 'terraform',
- tfstate: 'terraform',
- applescript: 'applescript',
- cake: 'cake',
- feature: 'cucumber',
- nim: 'nim',
- nimble: 'nim',
- apib: 'apiblueprint',
- apiblueprint: 'apiblueprint',
- tag: 'riot',
- vfl: 'vfl',
- kl: 'kl',
- pcss: 'postcss',
- sss: 'postcss',
- todo: 'todo',
- cfml: 'coldfusion',
- cfc: 'coldfusion',
- lucee: 'coldfusion',
- cabal: 'cabal',
- nix: 'nix',
- slim: 'slim',
- http: 'http',
- rest: 'http',
- rql: 'restql',
- restql: 'restql',
- kv: 'kivy',
- graphcool: 'graphcool',
- sbt: 'sbt',
- cr: 'crystal',
- cu: 'cuda',
- cuh: 'cuda',
- log: 'log',
-};
-
-const twoFileExtensionIcons = {
- 'gradle.kts': 'gradle',
- 'md.rendered': 'markdown',
- 'markdown.rendered': 'markdown',
- 'mdown.rendered': 'markdown',
- 'mkd.rendered': 'markdown',
- 'mkdn.rendered': 'markdown',
- 'YAML-tmLanguage': 'yaml',
- 'sln.dotsettings': 'settings',
- 'sln.dotsettings.user': 'settings',
- 'd.ts': 'typescript-def',
- 'code-workplace': 'vscode',
- '7z': 'zip',
- 'c++': 'cpp',
- 'vbox-prev': 'virtual',
- 'js.map': 'javascript-map',
- 'css.map': 'css-map',
- 'spec.ts': 'test-ts',
- 'test.ts': 'test-ts',
- 'ts.snap': 'test-ts',
- 'spec.tsx': 'test-jsx',
- 'test.tsx': 'test-jsx',
- 'tsx.snap': 'test-jsx',
- 'spec.jsx': 'test-jsx',
- 'test.jsx': 'test-jsx',
- 'jsx.snap': 'test-jsx',
- 'spec.js': 'test-js',
- 'test.js': 'test-js',
- 'js.snap': 'test-js',
- 'routing.ts': 'angular-routing',
- 'routing.js': 'angular-routing',
- 'module.ts': 'angular',
- 'module.js': 'angular',
- 'ng-template': 'angular',
- 'component.ts': 'angular-component',
- 'component.js': 'angular-component',
- 'guard.ts': 'angular-guard',
- 'guard.js': 'angular-guard',
- 'service.ts': 'angular-service',
- 'service.js': 'angular-service',
- 'pipe.ts': 'angular-pipe',
- 'pipe.js': 'angular-pipe',
- 'filter.js': 'angular-pipe',
- 'directive.ts': 'angular-directive',
- 'directive.js': 'angular-directive',
- 'resolver.ts': 'angular-resolver',
- 'resolver.js': 'angular-resolver',
- 'tf.json': 'terraform',
- 'blade.php': 'laravel',
- 'inky.php': 'laravel',
- 'reducer.ts': 'ngrx-reducer',
- 'rootReducer.ts': 'ngrx-reducer',
- 'state.ts': 'ngrx-state',
- 'actions.ts': 'ngrx-actions',
- 'effects.ts': 'ngrx-effects',
- 'drone.yml': 'drone',
-};
-
-const fileNameIcons = {
- '.jscsrc': 'json',
- '.jshintrc': 'json',
- 'tsconfig.json': 'json',
- 'tslint.json': 'json',
- 'composer.lock': 'json',
- '.jsbeautifyrc': 'json',
- '.esformatter': 'json',
- 'cdp.pid': 'json',
- '.htaccess': 'xml',
- '.jshintignore': 'settings',
- '.buildignore': 'settings',
- makefile: 'settings',
- '.mrconfig': 'settings',
- '.yardopts': 'settings',
- 'gradle.properties': 'gradle',
- gradlew: 'gradle',
- 'gradle-wrapper.properties': 'gradle',
- COPYING: 'certificate',
- 'COPYING.LESSER': 'certificate',
- LICENSE: 'certificate',
- LICENCE: 'certificate',
- 'LICENSE.md': 'certificate',
- 'LICENCE.md': 'certificate',
- 'LICENSE.txt': 'certificate',
- 'LICENCE.txt': 'certificate',
- '.gitlab-license': 'certificate',
- dockerfile: 'docker',
- 'docker-compose.yml': 'docker',
- '.mailmap': 'email',
- '.gitignore': 'git',
- '.gitconfig': 'git',
- '.gitattributes': 'git',
- '.gitmodules': 'git',
- '.gitkeep': 'git',
- 'git-history': 'git',
- '.Rhistory': 'r',
- 'cmakelists.txt': 'cmake',
- 'cmakecache.txt': 'cmake',
- 'angular-cli.json': 'angular',
- '.angular-cli.json': 'angular',
- '.vfl': 'vfl',
- '.kl': 'kl',
- 'postcss.config.js': 'postcss',
- '.postcssrc.js': 'postcss',
- 'project.graphcool': 'graphcool',
- 'webpack.js': 'webpack',
- 'webpack.ts': 'webpack',
- 'webpack.base.js': 'webpack',
- 'webpack.base.ts': 'webpack',
- 'webpack.config.js': 'webpack',
- 'webpack.config.ts': 'webpack',
- 'webpack.common.js': 'webpack',
- 'webpack.common.ts': 'webpack',
- 'webpack.config.common.js': 'webpack',
- 'webpack.config.common.ts': 'webpack',
- 'webpack.config.common.babel.js': 'webpack',
- 'webpack.config.common.babel.ts': 'webpack',
- 'webpack.dev.js': 'webpack',
- 'webpack.dev.ts': 'webpack',
- 'webpack.config.dev.js': 'webpack',
- 'webpack.config.dev.ts': 'webpack',
- 'webpack.config.dev.babel.js': 'webpack',
- 'webpack.config.dev.babel.ts': 'webpack',
- 'webpack.prod.js': 'webpack',
- 'webpack.prod.ts': 'webpack',
- 'webpack.server.js': 'webpack',
- 'webpack.server.ts': 'webpack',
- 'webpack.client.js': 'webpack',
- 'webpack.client.ts': 'webpack',
- 'webpack.config.server.js': 'webpack',
- 'webpack.config.server.ts': 'webpack',
- 'webpack.config.client.js': 'webpack',
- 'webpack.config.client.ts': 'webpack',
- 'webpack.config.production.babel.js': 'webpack',
- 'webpack.config.production.babel.ts': 'webpack',
- 'webpack.config.prod.babel.js': 'webpack',
- 'webpack.config.prod.babel.ts': 'webpack',
- 'webpack.config.prod.js': 'webpack',
- 'webpack.config.prod.ts': 'webpack',
- 'webpack.config.production.js': 'webpack',
- 'webpack.config.production.ts': 'webpack',
- 'webpack.config.staging.js': 'webpack',
- 'webpack.config.staging.ts': 'webpack',
- 'webpack.config.babel.js': 'webpack',
- 'webpack.config.babel.ts': 'webpack',
- 'webpack.config.base.babel.js': 'webpack',
- 'webpack.config.base.babel.ts': 'webpack',
- 'webpack.config.base.js': 'webpack',
- 'webpack.config.base.ts': 'webpack',
- 'webpack.config.staging.babel.js': 'webpack',
- 'webpack.config.staging.babel.ts': 'webpack',
- 'webpack.config.coffee': 'webpack',
- 'webpack.config.test.js': 'webpack',
- 'webpack.config.test.ts': 'webpack',
- 'webpack.config.vendor.js': 'webpack',
- 'webpack.config.vendor.ts': 'webpack',
- 'webpack.config.vendor.production.js': 'webpack',
- 'webpack.config.vendor.production.ts': 'webpack',
- 'webpack.test.js': 'webpack',
- 'webpack.test.ts': 'webpack',
- 'webpack.dist.js': 'webpack',
- 'webpack.dist.ts': 'webpack',
- 'webpackfile.js': 'webpack',
- 'webpackfile.ts': 'webpack',
- 'ionic.config.json': 'ionic',
- '.io-config.json': 'ionic',
- 'gulpfile.js': 'gulp',
- 'gulpfile.ts': 'gulp',
- 'gulpfile.babel.js': 'gulp',
- 'package.json': 'nodejs',
- 'package-lock.json': 'nodejs',
- '.nvmrc': 'nodejs',
- '.npmignore': 'npm',
- '.npmrc': 'npm',
- '.yarnrc': 'yarn',
- '.yarnrc.yml': 'yarn',
- 'yarn.lock': 'yarn',
- '.yarnclean': 'yarn',
- '.yarn-integrity': 'yarn',
- 'yarn-error.log': 'yarn',
- 'androidmanifest.xml': 'android',
- '.env': 'tune',
- '.env.example': 'tune',
- '.babelrc': 'babel',
- 'contributing.md': 'contributing',
- 'contributing.md.rendered': 'contributing',
- 'readme.md': 'readme',
- 'readme.md.rendered': 'readme',
- changelog: 'changelog',
- 'changelog.md': 'changelog',
- 'changelog.md.rendered': 'changelog',
- CREDITS: 'credits',
- 'credits.txt': 'credits',
- 'credits.md': 'credits',
- 'credits.md.rendered': 'credits',
- '.flowconfig': 'flow',
- 'favicon.png': 'favicon',
- 'karma.conf.js': 'karma',
- 'karma.conf.ts': 'karma',
- 'karma.conf.coffee': 'karma',
- 'karma.config.js': 'karma',
- 'karma.config.ts': 'karma',
- 'karma-main.js': 'karma',
- 'karma-main.ts': 'karma',
- '.bithoundrc': 'bithound',
- 'appveyor.yml': 'appveyor',
- '.travis.yml': 'travis',
- 'protractor.conf.js': 'protractor',
- 'protractor.conf.ts': 'protractor',
- 'protractor.conf.coffee': 'protractor',
- 'protractor.config.js': 'protractor',
- 'protractor.config.ts': 'protractor',
- 'fuse.js': 'fusebox',
- procfile: 'heroku',
- '.editorconfig': 'editorconfig',
- '.gitlab-ci.yml': 'gitlab',
- '.bowerrc': 'bower',
- 'bower.json': 'bower',
- '.eslintrc.js': 'eslint',
- '.eslintrc.yaml': 'eslint',
- '.eslintrc.yml': 'eslint',
- '.eslintrc.json': 'eslint',
- '.eslintrc': 'eslint',
- '.eslintignore': 'eslint',
- 'code_of_conduct.md': 'conduct',
- 'code_of_conduct.md.rendered': 'conduct',
- '.watchmanconfig': 'watchman',
- 'aurelia.json': 'aurelia',
- 'mocha.opts': 'mocha',
- jenkinsfile: 'jenkins',
- 'firebase.json': 'firebase',
- '.firebaserc': 'firebase',
- Rakefile: 'ruby',
- 'rollup.config.js': 'rollup',
- 'rollup.config.ts': 'rollup',
- 'rollup-config.js': 'rollup',
- 'rollup-config.ts': 'rollup',
- 'rollup.config.prod.js': 'rollup',
- 'rollup.config.prod.ts': 'rollup',
- 'rollup.config.dev.js': 'rollup',
- 'rollup.config.dev.ts': 'rollup',
- 'rollup.config.prod.vendor.js': 'rollup',
- 'rollup.config.prod.vendor.ts': 'rollup',
- '.hhconfig': 'hack',
- '.stylelintrc': 'stylelint',
- 'stylelint.config.js': 'stylelint',
- '.stylelintrc.json': 'stylelint',
- '.stylelintrc.yaml': 'stylelint',
- '.stylelintrc.yml': 'stylelint',
- '.stylelintrc.js': 'stylelint',
- '.stylelintignore': 'stylelint',
- '.codeclimate.yml': 'code-climate',
- '.prettierrc': 'prettier',
- 'prettier.config.js': 'prettier',
- '.prettierrc.js': 'prettier',
- '.prettierrc.json': 'prettier',
- '.prettierrc.yaml': 'prettier',
- '.prettierrc.yml': 'prettier',
- '.prettierignore': 'prettier',
- 'nodemon.json': 'nodemon',
- '.sonarrc': 'sonar',
- browserslist: 'browserlist',
- '.browserslistrc': 'browserlist',
- '.snyk': 'snyk',
- '.drone.yml': 'drone',
-};
-
-export default function getIconForFile(name) {
- return (
- fileNameIcons[name] ||
- twoFileExtensionIcons[name ? name.split('.').slice(-2).join('.') : ''] ||
- fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] ||
- ''
- );
-}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 8a3a174f414..dfeb12d5cf5 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -43,11 +43,6 @@ export default {
isBlob() {
return this.file.type === 'blob';
},
- levelIndentation() {
- return {
- marginLeft: this.level ? `${this.level * 8}px` : null,
- };
- },
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
@@ -144,7 +139,6 @@ export default {
>
<span
ref="textOutput"
- :style="levelIndentation"
class="file-row-name"
:title="file.name"
data-qa-selector="file_name_content"
@@ -198,6 +192,7 @@ export default {
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
+ margin-left: calc(var(--level) * 16px);
}
.file-row-name .file-row-icon {
diff --git a/app/assets/javascripts/vue_shared/components/file_tree.vue b/app/assets/javascripts/vue_shared/components/file_tree.vue
index e7817b8f910..2e0cdbb12f9 100644
--- a/app/assets/javascripts/vue_shared/components/file_tree.vue
+++ b/app/assets/javascripts/vue_shared/components/file_tree.vue
@@ -20,11 +20,16 @@ export default {
return this.file.isHeader ? 0 : this.level + 1;
},
},
+ methods: {
+ hasChildren(childFile) {
+ return childFile.tree?.length;
+ },
+ },
};
</script>
<template>
- <div>
+ <div :style="{ '--level': level }">
<component
:is="fileRowComponent"
:level="level"
@@ -39,6 +44,8 @@ export default {
:file-row-component="fileRowComponent"
:level="childFilesLevel"
:file="childFile"
+ :class="{ 'tree-list-parent': hasChildren(childFile) }"
+ class="gl-relative"
v-bind="$attrs"
v-on="$listeners"
/>
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 993b4c11c0e..5b98af8c732 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
@@ -50,6 +50,7 @@ export const TOKEN_TITLE_ASSIGNEE = s__('SearchToken|Assignee');
export const TOKEN_TITLE_AUTHOR = __('Author');
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_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
@@ -67,6 +68,7 @@ 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_GROUP = 'group';
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
@@ -85,5 +87,4 @@ export const TOKEN_TYPE_STATUS = 'status';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
-
export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
index e0fa06c159e..c8aeac75645 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -2,6 +2,7 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
+import { TYPENAME_CRM_CONTACT } from '~/graphql_shared/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
@@ -93,7 +94,7 @@ export default {
return `${getIdFromGraphQLId(contact.id)}`;
},
formatContactGraphQLId(id) {
- return convertToGraphQLId('CustomerRelations::Contact', id);
+ return convertToGraphQLId(TYPENAME_CRM_CONTACT, id);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
index 3f030c8698c..ff0571031b5 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -2,6 +2,7 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
+import { TYPENAME_CRM_ORGANIZATION } from '~/graphql_shared/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
@@ -90,7 +91,7 @@ export default {
return `${getIdFromGraphQLId(organization.id)}`;
},
formatOrganizationGraphQLId(id) {
- return convertToGraphQLId('CustomerRelations::Organization', id);
+ return convertToGraphQLId(TYPENAME_CRM_ORGANIZATION, id);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 71c50ef292a..9449e071a0d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -79,6 +79,9 @@ export default {
// labels.json and /groups/:id/labels & /projects/:id/labels
// return response differently.
this.labels = Array.isArray(res) ? res : res.data;
+ if (this.config.fetchLatestLabels) {
+ this.fetchLatestLabels(searchTerm);
+ }
})
.catch(() =>
createAlert({
@@ -89,6 +92,21 @@ export default {
this.loading = false;
});
},
+ fetchLatestLabels(searchTerm) {
+ this.config
+ .fetchLatestLabels(searchTerm)
+ .then((res) => {
+ // We'd want to avoid doing this check but
+ // labels.json and /groups/:id/labels & /projects/:id/labels
+ // return response differently.
+ this.labels = Array.isArray(res) ? res : res.data;
+ })
+ .catch(() =>
+ createAlert({
+ message: __('There was a problem fetching latest labels.'),
+ }),
+ );
+ },
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js
deleted file mode 100644
index 06537d682fe..00000000000
--- a/app/assets/javascripts/vue_shared/components/group_select/constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { __ } from '~/locale';
-
-export const TOGGLE_TEXT = __('Search for a group');
-export const RESET_LABEL = __('Reset');
-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/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 8e459cc21ac..28baabbdb81 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiIconBadge from './ci_badge_link.vue';
+import CiBadgeLink from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
/**
@@ -16,7 +16,7 @@ import TimeagoTooltip from './time_ago_tooltip.vue';
*/
export default {
components: {
- CiIconBadge,
+ CiBadgeLink,
TimeagoTooltip,
GlButton,
GlAvatarLink,
@@ -120,7 +120,7 @@ export default {
data-testid="ci-header-content"
>
<section class="header-main-content gl-mr-3">
- <ci-icon-badge :status="status" />
+ <ci-badge-link class="gl-mr-3" :status="status" />
<strong data-testid="ci-header-item-text">{{ item }}</strong>
diff --git a/app/assets/javascripts/vue_shared/components/incubation/incubation_alert.vue b/app/assets/javascripts/vue_shared/components/incubation/incubation_alert.vue
new file mode 100644
index 00000000000..b704cec2475
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/incubation/incubation_alert.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ name: 'IncubationAlert',
+ components: { GlAlert, GlLink },
+ props: {
+ featureName: {
+ type: String,
+ required: true,
+ },
+ linkToFeedbackIssue: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ shouldShowAlert() {
+ return !this.isAlertDismissed;
+ },
+ titleLabel() {
+ return sprintf(this.$options.i18n.titleLabel, { featureName: this.featureName });
+ },
+ },
+ methods: {
+ dismissAlert() {
+ this.isAlertDismissed = true;
+ },
+ },
+ i18n: {
+ titleLabel: s__('Incubation|%{featureName} is in incubating phase'),
+ contentLabel: s__(
+ 'Incubation|GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.',
+ ),
+ learnMoreLabel: s__('Incubation|Learn more about incubating features'),
+ feedbackLabel: s__('Incubation|Give feedback on this feature'),
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="shouldShowAlert"
+ :title="titleLabel"
+ variant="warning"
+ :primary-button-text="$options.i18n.feedbackLabel"
+ :primary-button-link="linkToFeedbackIssue"
+ @dismiss="dismissAlert"
+ >
+ {{ $options.i18n.contentLabel }}
+ <gl-link href="https://about.gitlab.com/handbook/engineering/incubation/" target="_blank">{{
+ $options.i18n.learnMoreLabel
+ }}</gl-link>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
new file mode 100644
index 00000000000..b5afe92316a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+
+export default {
+ name: 'KeysetPagination',
+ components: {
+ GlKeysetPagination,
+ },
+ props: {
+ startCursor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ endCursor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ hasNextPage: {
+ type: Boolean,
+ required: true,
+ },
+ hasPreviousPage: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ previousPageLink() {
+ return setUrlParams({ cursor: this.startCursor });
+ },
+ nextPageLink() {
+ return setUrlParams({ cursor: this.endCursor });
+ },
+ isPaginationVisible() {
+ return this.hasPreviousPage || this.hasNextPage;
+ },
+ },
+ i18n: {
+ previousPageButtonLabel: __('Prev'),
+ nextPageButtonLabel: __('Next'),
+ },
+};
+</script>
+
+<template>
+ <div v-if="isPaginationVisible" class="gl--flex-center">
+ <gl-keyset-pagination
+ :start-cursor="startCursor"
+ :end-cursor="endCursor"
+ :has-previous-page="hasPreviousPage"
+ :has-next-page="hasNextPage"
+ :prev-text="$options.i18n.previousPageButtonLabel"
+ :next-text="$options.i18n.nextPageButtonLabel"
+ :prev-button-link="previousPageLink"
+ :next-button-link="nextPageLink"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
index d80c1ff8b0c..9a88ab44f3d 100644
--- a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
@@ -1,9 +1,10 @@
import { issuableTypes } from '~/boards/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import blockingIssuesQuery from './graphql/blocking_issues.query.graphql';
import blockingEpicsQuery from './graphql/blocking_epics.query.graphql';
export const blockingIssuablesQueries = {
- [issuableTypes.issue]: {
+ [TYPE_ISSUE]: {
query: blockingIssuesQuery,
},
[issuableTypes.epic]: {
diff --git a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
index 253aca8837d..f5b4870d59f 100644
--- a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
@@ -1,8 +1,9 @@
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { issuableTypes } from '~/boards/constants';
-import { TYPE_ISSUE, TYPE_EPIC } from '~/graphql_shared/constants';
+import { TYPENAME_ISSUE, TYPENAME_EPIC } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ISSUE } from '~/issues/constants';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
import { blockingIssuablesQueries } from './constants';
@@ -10,16 +11,16 @@ import { blockingIssuablesQueries } from './constants';
export default {
i18n: {
issuableType: {
- [issuableTypes.issue]: __('issue'),
+ [TYPE_ISSUE]: __('issue'),
[issuableTypes.epic]: __('epic'),
},
},
graphQLIdType: {
- [issuableTypes.issue]: TYPE_ISSUE,
- [issuableTypes.epic]: TYPE_EPIC,
+ [TYPE_ISSUE]: TYPENAME_ISSUE,
+ [issuableTypes.epic]: TYPENAME_EPIC,
},
referenceFormatter: {
- [issuableTypes.issue]: (r) => r.split('/')[1],
+ [TYPE_ISSUE]: (r) => r.split('/')[1],
},
defaultDisplayLimit: 3,
textTruncateWidth: 80,
@@ -42,7 +43,7 @@ export default {
type: String,
required: true,
validator(value) {
- return [issuableTypes.issue, issuableTypes.epic].includes(value);
+ return [TYPE_ISSUE, issuableTypes.epic].includes(value);
},
},
},
@@ -119,7 +120,7 @@ export default {
);
},
blockIcon() {
- return this.issuableType === issuableTypes.issue ? 'issue-block' : 'entity-blocked';
+ return this.issuableType === TYPE_ISSUE ? 'issue-block' : 'entity-blocked';
},
glIconId() {
return `blocked-icon-${this.uniqueId}`;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 7b76fc3fc6d..6f4cddbdfa2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -82,6 +82,11 @@ export default {
required: false,
default: true,
},
+ autocompleteDataSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
line: {
type: Object,
required: false,
@@ -257,6 +262,7 @@ export default {
contacts: this.enableAutocomplete,
},
true,
+ this.autocompleteDataSources,
);
},
beforeDestroy() {
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 c53118b9f62..7e6b0e4a63b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -41,33 +41,25 @@ export default {
required: false,
default: true,
},
- formFieldId: {
- type: String,
- required: true,
- },
- formFieldName: {
- type: String,
- required: true,
- },
enablePreview: {
type: Boolean,
required: false,
default: true,
},
+ autocompleteDataSources: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
- formFieldPlaceholder: {
- type: String,
- required: false,
- default: '',
- },
- formFieldAriaLabel: {
- type: String,
- required: false,
- default: '',
+ formFieldProps: {
+ type: Object,
+ required: true,
+ validator: (prop) => prop.id && prop.name,
},
autofocus: {
type: Boolean,
@@ -152,6 +144,7 @@ export default {
:textarea-value="value"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :autocomplete-data-sources="autocompleteDataSources"
:uploads-path="uploadsPath"
:enable-preview="enablePreview"
show-content-editor-switcher
@@ -160,16 +153,13 @@ export default {
>
<template #textarea>
<textarea
- :id="formFieldId"
+ v-bind="formFieldProps"
ref="textarea"
:value="value"
- :name="formFieldName"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
:data-supports-quick-actions="supportsQuickActions"
data-qa-selector="markdown_editor_form_field"
- :aria-label="formFieldAriaLabel"
- :placeholder="formFieldPlaceholder"
@input="updateMarkdownFromMarkdownField"
@keydown="$emit('keydown', $event)"
>
@@ -189,9 +179,8 @@ export default {
@enableMarkdownEditor="onEditingModeChange('markdownField')"
/>
<input
- :id="formFieldId"
+ v-bind="formFieldProps"
:value="value"
- :name="formFieldName"
data-qa-selector="markdown_editor_form_field"
type="hidden"
/>
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/constants.js
new file mode 100644
index 00000000000..e5dca170965
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/constants.js
@@ -0,0 +1,26 @@
+import { __ } from '~/locale';
+
+export const RESOURCE_TYPE_ISSUE = 'issue';
+export const RESOURCE_TYPE_MERGE_REQUEST = 'merge-request';
+export const RESOURCE_TYPE_MILESTONE = 'milestone';
+
+export const RESOURCE_TYPES = [
+ RESOURCE_TYPE_ISSUE,
+ RESOURCE_TYPE_MERGE_REQUEST,
+ RESOURCE_TYPE_MILESTONE,
+];
+
+export const RESOURCE_OPTIONS = {
+ [RESOURCE_TYPE_ISSUE]: {
+ path: 'issues/new',
+ label: __('issue'),
+ },
+ [RESOURCE_TYPE_MERGE_REQUEST]: {
+ path: 'merge_requests/new',
+ label: __('merge request'),
+ },
+ [RESOURCE_TYPE_MILESTONE]: {
+ path: 'milestones/new',
+ label: __('milestone'),
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql
new file mode 100644
index 00000000000..578914dbbaf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql
@@ -0,0 +1,18 @@
+query searchUserGroupProjectsWithMergeRequestsEnabled($fullPath: ID!, $search: String) {
+ group(fullPath: $fullPath) {
+ id
+ projects(
+ search: $search
+ withMergeRequestsEnabled: true
+ includeSubgroups: true
+ sort: ACTIVITY_DESC
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql
new file mode 100644
index 00000000000..8fe92cf7c6c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql
@@ -0,0 +1,21 @@
+query searchUserGroupsAndProjects($username: String!, $search: String) {
+ projects(sort: "latest_activity_desc", membership: true) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
+ }
+
+ user(username: $username) {
+ id
+ groups(search: $search) {
+ nodes {
+ id
+ name
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql
new file mode 100644
index 00000000000..a630c885d28
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql
@@ -0,0 +1,15 @@
+query searchUserProjectsWithIssuesEnabled($search: String) {
+ projects(
+ search: $search
+ membership: true
+ withIssuesEnabled: true
+ sort: "latest_activity_desc"
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql
new file mode 100644
index 00000000000..44ebf755728
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql
@@ -0,0 +1,15 @@
+query searchUserProjectsWithMergeRequestsEnabled($search: String) {
+ projects(
+ search: $search
+ membership: true
+ withMergeRequestsEnabled: true
+ sort: "latest_activity_desc"
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown.js b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown.js
new file mode 100644
index 00000000000..f3905dabedd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import NewResourceDropdown from './new_resource_dropdown.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initNewResourceDropdown = (props = {}) => {
+ const el = document.querySelector('.js-new-resource-dropdown');
+
+ if (!el) {
+ return false;
+ }
+
+ const { groupId, fullPath, username } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(createElement) {
+ return createElement(NewResourceDropdown, {
+ props: {
+ withLocalStorage: true,
+ groupId,
+ queryVariables: {
+ ...(fullPath
+ ? {
+ fullPath,
+ }
+ : {}),
+ ...(username
+ ? {
+ username,
+ }
+ : {}),
+ },
+ ...props,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue
new file mode 100644
index 00000000000..b079181bd10
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue
@@ -0,0 +1,208 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { createAlert } from '~/flash';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import AccessorUtilities from '~/lib/utils/accessor';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import searchUserProjectsWithIssuesEnabled from './graphql/search_user_projects_with_issues_enabled.query.graphql';
+import { RESOURCE_TYPE_ISSUE, RESOURCE_TYPES, RESOURCE_OPTIONS } from './constants';
+
+export default {
+ i18n: {
+ noMatchesFound: __('No matches found'),
+ toggleButtonLabel: __('Toggle project select'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ LocalStorageSync,
+ },
+ props: {
+ resourceType: {
+ type: String,
+ required: false,
+ default: RESOURCE_TYPE_ISSUE,
+ validator: (value) => RESOURCE_TYPES.includes(value),
+ },
+ query: {
+ type: Object,
+ required: false,
+ default: () => searchUserProjectsWithIssuesEnabled,
+ },
+ groupId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ queryVariables: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ extractProjects: {
+ type: Function,
+ required: false,
+ default: (data) => data?.projects?.nodes,
+ },
+ withLocalStorage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ search: '',
+ selectedProject: {},
+ shouldSkipQuery: true,
+ };
+ },
+ apollo: {
+ projects: {
+ query() {
+ return this.query;
+ },
+ variables() {
+ return {
+ search: this.search,
+ ...this.queryVariables,
+ };
+ },
+ update(data) {
+ return this.extractProjects(data) || [];
+ },
+ error(error) {
+ createAlert({
+ message: __('An error occurred while loading projects.'),
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return this.shouldSkipQuery;
+ },
+ debounce: DEBOUNCE_DELAY,
+ },
+ },
+ computed: {
+ localStorageKey() {
+ return `group-${this.groupId}-new-${this.resourceType}-recent-project`;
+ },
+ resourceOptions() {
+ return RESOURCE_OPTIONS[this.resourceType];
+ },
+ defaultDropdownText() {
+ return sprintf(__('Select project to create %{type}'), { type: this.resourceOptions.label });
+ },
+ dropdownHref() {
+ return this.hasSelectedProject
+ ? joinPaths(this.selectedProject.webUrl, DASH_SCOPE, this.resourceOptions.path)
+ : undefined;
+ },
+ dropdownText() {
+ return this.hasSelectedProject
+ ? sprintf(__('New %{type} in %{project}'), {
+ type: this.resourceOptions.label,
+ project: this.selectedProject.name,
+ })
+ : this.defaultDropdownText;
+ },
+ hasSelectedProject() {
+ return this.selectedProject.webUrl;
+ },
+ showNoSearchResultsText() {
+ return !this.projects.length && this.search;
+ },
+ canUseLocalStorage() {
+ return this.withLocalStorage && AccessorUtilities.canUseLocalStorage();
+ },
+ selectedProjectForLocalStorage() {
+ const { webUrl, name } = this.selectedProject;
+
+ return { webUrl, name };
+ },
+ },
+ methods: {
+ handleDropdownClick() {
+ if (!this.dropdownHref) {
+ this.$refs.dropdown.show();
+ }
+ },
+ handleDropdownShown() {
+ if (this.shouldSkipQuery) {
+ this.shouldSkipQuery = false;
+ }
+ this.$refs.search.focusInput();
+ },
+ selectProject(project) {
+ this.selectedProject = project;
+ },
+ initFromLocalStorage(storedProject) {
+ // Historically, the selected project was saved with the URL as the `url` property, so we are
+ // falling back to that legacy property if `webUrl` is empty. This ensures that we support
+ // localStorage data that was persisted prior to this change.
+ let webUrl = storedProject.webUrl || storedProject.url;
+
+ // The select2 implementation used to include the resource path in the local storage. We
+ // need to clean this up so that we can then re-build a fresh URL in the computed prop.
+ webUrl = webUrl.endsWith(this.resourceOptions.path)
+ ? webUrl.slice(0, webUrl.length - this.resourceOptions.path.length)
+ : webUrl;
+ const dashSuffix = `${DASH_SCOPE}/`;
+ webUrl = webUrl.endsWith(dashSuffix)
+ ? webUrl.slice(0, webUrl.length - dashSuffix.length)
+ : webUrl;
+
+ this.selectedProject = { webUrl, name: storedProject.name };
+ },
+ },
+};
+</script>
+
+<template>
+ <local-storage-sync
+ :storage-key="localStorageKey"
+ :value="selectedProjectForLocalStorage"
+ @input="initFromLocalStorage"
+ >
+ <gl-dropdown
+ ref="dropdown"
+ right
+ split
+ :split-href="dropdownHref"
+ :text="dropdownText"
+ :toggle-text="$options.i18n.toggleButtonLabel"
+ variant="confirm"
+ data-testid="new-resource-dropdown"
+ @click="handleDropdownClick"
+ @shown="handleDropdownShown"
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="search" />
+ <gl-loading-icon v-if="$apollo.queries.projects.loading" />
+ <template v-else>
+ <gl-dropdown-item
+ v-for="project of projects"
+ :key="project.id"
+ @click="selectProject(project)"
+ >
+ {{ project.nameWithNamespace || project.name }}
+ </gl-dropdown-item>
+ <gl-dropdown-text v-if="showNoSearchResultsText">
+ {{ $options.i18n.noMatchesFound }}
+ </gl-dropdown-text>
+ </template>
+ </gl-dropdown>
+ </local-storage-sync>
+</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 5516c9943b8..5d0ee6adffe 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -33,6 +33,7 @@ export default {
'gl-border-t-transparent': !this.first && !this.selected,
'gl-border-t-gray-100': this.first && !this.selected,
'gl-border-b-gray-100': !this.selected,
+ 'gl-border-t-transparent!': this.selected && !this.first,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
@@ -126,10 +127,9 @@ export default {
<slot name="right-action"></slot>
</div>
</div>
- <div class="gl-display-flex">
+ <div v-if="isDetailsShown" class="gl-display-flex">
<div class="gl-w-7"></div>
<div
- v-if="isDetailsShown"
class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100 gl-mb-3"
>
<div
diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue
deleted file mode 100644
index e3e3b9abc3c..00000000000
--- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import RunnerAwsDeploymentsModal from './runner_aws_deployments_modal.vue';
-
-export default {
- components: {
- GlButton,
- RunnerAwsDeploymentsModal,
- },
- directives: {
- GlModalDirective,
- },
- modalId: 'runner-aws-deployments-modal',
- i18n: {
- buttonText: s__('Runners|Deploy GitLab Runner in AWS'),
- },
- data() {
- return {
- opened: false,
- };
- },
- methods: {
- onClick() {
- this.opened = true;
- },
- },
-};
-</script>
-<template>
- <div>
- <gl-button
- v-gl-modal-directive="$options.modalId"
- class="gl-mt-4"
- data-testid="show-modal-button"
- variant="confirm"
- @click="onClick"
- >
- {{ $options.i18n.buttonText }}
- </gl-button>
- <runner-aws-deployments-modal v-if="opened" :modal-id="$options.modalId" />
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
deleted file mode 100644
index 08acde1aefc..00000000000
--- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<script>
-import { GlModal } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
-
-export default {
- components: {
- GlModal,
- RunnerAwsInstructions,
- },
- props: {
- modalId: {
- type: String,
- required: true,
- },
- },
- methods: {
- onClose() {
- this.$refs.modal.close();
- },
- },
- i18n_title: s__('Runners|Deploy GitLab Runner in AWS'),
-};
-</script>
-<template>
- <gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n_title" hide-footer size="sm">
- <runner-aws-instructions @close="onClose" />
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index 3dbc5246c3d..b66c89d1372 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -4,6 +4,7 @@ export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
export const PLATFORM_DOCKER = 'docker';
export const PLATFORM_KUBERNETES = 'kubernetes';
+export const PLATFORM_AWS = 'aws';
export const AWS_README_URL =
'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md';
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
index cafebdfe5f4..8a234889e6f 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
@@ -2,6 +2,7 @@
import {
GlButton,
GlSprintf,
+ GlIcon,
GlLink,
GlFormRadioGroup,
GlFormRadio,
@@ -11,6 +12,7 @@ import {
import Tracking from '~/tracking';
import { getBaseURL, objectToQuery, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
AWS_README_URL,
AWS_CF_BASE_URL,
@@ -22,13 +24,22 @@ export default {
components: {
GlButton,
GlSprintf,
+ GlIcon,
GlLink,
GlFormRadioGroup,
GlFormRadio,
GlAccordion,
GlAccordionItem,
+ ModalCopyButton,
},
mixins: [Tracking.mixin()],
+ props: {
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
data() {
return {
selectedIndex: 0,
@@ -65,16 +76,20 @@ export default {
},
},
i18n: {
- title: s__('Runners|Deploy GitLab Runner in AWS'),
instructions: s__(
- 'Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.',
+ 'Runners|Select your preferred runner, then choose the capacity for the runner in the AWS CloudFormation console.',
),
chooseRunner: s__('Runners|Choose your preferred GitLab Runner'),
dontSeeWhatYouAreLookingFor: s__(
"Runners|Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}.",
),
+ runnerRegistrationToken: s__('Runners|Runner Registration token'),
+ copyInstructions: s__('Runners|Copy registration token'),
moreDetails: __('More Details'),
lessDetails: __('Less Details'),
+ close: __('Close'),
+ deployRunnerInAws: s__('Runners|Deploy GitLab Runner in AWS'),
+ externalLink: __('(external link)'),
},
readmeUrl: AWS_README_URL,
easyButtons: AWS_EASY_BUTTONS,
@@ -83,6 +98,7 @@ export default {
<template>
<div>
<p>{{ $options.i18n.instructions }}</p>
+
<gl-form-radio-group v-model="selectedIndex" :label="$options.i18n.chooseRunner" label-sr-only>
<gl-form-radio
v-for="(easyButton, idx) in $options.easyButtons"
@@ -113,10 +129,23 @@ export default {
</template>
</gl-sprintf>
</p>
+ <template v-if="registrationToken">
+ <h5 class="gl-mb-3">{{ $options.i18n.runnerRegistrationToken }}</h5>
+ <div class="gl-display-flex">
+ <pre class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line">{{ registrationToken }}</pre>
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="registrationToken"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
<footer class="gl-display-flex gl-justify-content-end gl-pt-3 gl-gap-3">
- <gl-button @click="onClose()">{{ __('Close') }}</gl-button>
+ <gl-button @click="onClose()">{{ $options.i18n.close }}</gl-button>
<gl-button variant="confirm" @click="onOk()">
- {{ s__('Runners|Deploy GitLab Runner in AWS') }}
+ {{ $options.i18n.deployRunnerInAws }}
+ <gl-icon name="external-link" :aria-label="$options.i18n.externalLink" />
</gl-button>
</footer>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index 729fe9c462c..22d9b88fa41 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -14,11 +14,12 @@ import {
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { __, s__ } from '~/locale';
import getRunnerPlatformsQuery from './graphql/get_runner_platforms.query.graphql';
-import { PLATFORM_DOCKER, PLATFORM_KUBERNETES } from './constants';
+import { PLATFORM_DOCKER, PLATFORM_KUBERNETES, PLATFORM_AWS } from './constants';
import RunnerCliInstructions from './instructions/runner_cli_instructions.vue';
import RunnerDockerInstructions from './instructions/runner_docker_instructions.vue';
import RunnerKubernetesInstructions from './instructions/runner_kubernetes_instructions.vue';
+import RunnerAwsInstructions from './instructions/runner_aws_instructions.vue';
export default {
components: {
@@ -104,6 +105,8 @@ export default {
return RunnerDockerInstructions;
case PLATFORM_KUBERNETES:
return RunnerKubernetesInstructions;
+ case PLATFORM_AWS:
+ return RunnerAwsInstructions;
default:
return null;
}
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 28a16cd846a..092e8ba6c15 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,64 +1,55 @@
<script>
import { GlIntersectionObserver } from '@gitlab/ui';
-import LineHighlighter from '~/blob/line_highlighter';
-import ChunkLine from './chunk_line.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { getPageParamValue, getPageSearchString } from '~/blob/utils';
/*
* We only highlight the chunk that is currently visible to the user.
* By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly.
*
- * Content that is not visible to the user (i.e. not highlighted) do not need to look nice,
- * so by making text transparent and rendering raw (non-highlighted) text,
- * the browser spends less resources on painting content that is not immediately relevant.
- *
- * Why use transparent text as opposed to hiding content entirely?
- * 1. If content is hidden entirely, native find text (⌘ + F) won't work.
- * 2. When URL contains line numbers, the browser needs to be able to jump to the correct line.
+ * Content that is not visible to the user (i.e. not highlighted) does not need to look nice,
+ * so by rendering raw (non-highlighted) text, the browser spends less resources on painting
+ * content that is not immediately relevant.
+ * Why use plaintext as opposed to hiding content entirely?
+ * If content is hidden entirely, native find text (⌘ + F) won't work.
*/
export default {
components: {
- ChunkLine,
GlIntersectionObserver,
},
+ directives: {
+ SafeHtml,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
- isFirstChunk: {
+ isHighlighted: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
chunkIndex: {
type: Number,
required: false,
default: 0,
},
- isHighlighted: {
- type: Boolean,
+ rawContent: {
+ type: String,
required: true,
},
- content: {
+ highlightedContent: {
type: String,
required: true,
},
- startingFrom: {
- type: Number,
- required: false,
- default: 0,
- },
totalLines: {
type: Number,
required: false,
default: 0,
},
- totalChunks: {
+ startingFrom: {
type: Number,
required: false,
default: 0,
},
- language: {
- type: String,
- required: false,
- default: null,
- },
blamePath: {
type: String,
required: true,
@@ -66,37 +57,37 @@ export default {
},
data() {
return {
+ hasAppeared: false,
isLoading: true,
};
},
computed: {
+ shouldHighlight() {
+ return Boolean(this.highlightedContent) && (this.hasAppeared || this.isHighlighted);
+ },
lines() {
return this.content.split('\n');
},
+ pageSearchString() {
+ if (!this.glFeatures.fileLineBlame) return '';
+ const page = getPageParamValue(this.number);
+ return getPageSearchString(this.blamePath, page);
+ },
},
-
created() {
- if (this.isFirstChunk) {
+ if (this.chunkIndex === 0) {
+ // Display first chunk ASAP in order to improve perceived performance
this.isLoading = false;
return;
}
- window.requestIdleCallback(async () => {
+ 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
- await this.$nextTick();
- const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
- lineHighlighter.highlightHash(hash);
- }
});
},
methods: {
handleChunkAppear() {
- if (!this.isHighlighted) {
- this.$emit('appear', this.chunkIndex);
- }
+ this.hasAppeared = true;
},
calculateLineNumber(index) {
return this.startingFrom + index + 1;
@@ -106,28 +97,37 @@ export default {
</script>
<template>
<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
+ <div class="gl-display-flex">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div
v-for="(n, index) in totalLines"
- v-once
- :id="`L${calculateLineNumber(index)}`"
:key="index"
- data-testid="line-number"
- v-text="calculateLineNumber(index)"
- ></span>
+ data-testid="line-numbers"
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ v-if="glFeatures.fileLineBlame"
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
+ ></a>
+ <a
+ :id="`L${calculateLineNumber(index)}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${calculateLineNumber(index)}`"
+ :data-line-number="calculateLineNumber(index)"
+ >
+ {{ calculateLineNumber(index) }}
+ </a>
+ </div>
</div>
- <div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div>
+
+ <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <!-- Placeholder for line numbers while content is not highlighted -->
+ </div>
+
+ <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-once v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
</div>
</gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue
new file mode 100644
index 00000000000..28a16cd846a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_deprecated.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlIntersectionObserver } from '@gitlab/ui';
+import LineHighlighter from '~/blob/line_highlighter';
+import ChunkLine from './chunk_line.vue';
+
+/*
+ * We only highlight the chunk that is currently visible to the user.
+ * By making use of the Intersection Observer API we can determine when a chunk becomes visible and highlight it accordingly.
+ *
+ * Content that is not visible to the user (i.e. not highlighted) do not need to look nice,
+ * so by making text transparent and rendering raw (non-highlighted) text,
+ * the browser spends less resources on painting content that is not immediately relevant.
+ *
+ * Why use transparent text as opposed to hiding content entirely?
+ * 1. If content is hidden entirely, native find text (⌘ + F) won't work.
+ * 2. When URL contains line numbers, the browser needs to be able to jump to the correct line.
+ */
+export default {
+ components: {
+ ChunkLine,
+ GlIntersectionObserver,
+ },
+ props: {
+ isFirstChunk: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ chunkIndex: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ isHighlighted: {
+ type: Boolean,
+ required: true,
+ },
+ content: {
+ type: String,
+ required: true,
+ },
+ startingFrom: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ totalLines: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ totalChunks: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ language: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ blamePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ computed: {
+ lines() {
+ return this.content.split('\n');
+ },
+ },
+
+ created() {
+ if (this.isFirstChunk) {
+ this.isLoading = false;
+ return;
+ }
+
+ window.requestIdleCallback(async () => {
+ 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
+ await this.$nextTick();
+ const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ lineHighlighter.highlightHash(hash);
+ }
+ });
+ },
+ methods: {
+ handleChunkAppear() {
+ if (!this.isHighlighted) {
+ this.$emit('appear', this.chunkIndex);
+ }
+ },
+ calculateLineNumber(index) {
+ return this.startingFrom + index + 1;
+ },
+ },
+};
+</script>
+<template>
+ <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"
+ data-testid="line-number"
+ v-text="calculateLineNumber(index)"
+ ></span>
+ </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/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index f382ded90d7..15335ea6edc 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -120,6 +120,8 @@ export const EVENT_LABEL_FALLBACK = 'legacy_fallback';
export const LINES_PER_CHUNK = 70;
+export const NEWLINE = '\n';
+
export const BIDI_CHARS = [
'\u202A', // Left-to-Right Embedding (Try treating following text as left-to-right)
'\u202B', // Right-to-Left Embedding (Try treating following text as right-to-left)
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 efafa67a733..11708b6f1f6 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
@@ -1,192 +1,40 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
-import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
- LEGACY_FALLBACKS,
-} from './constants';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
import Chunk from './components/chunk.vue';
-import { registerPlugins } from './plugins/index';
-/*
- * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
- * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible.
- *
- * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback).
- * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes,
- * it does not trigger a repaint on a parent element that wraps all 1000 lines.
- */
export default {
components: {
- GlLoadingIcon,
Chunk,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [Tracking.mixin()],
+ inject: {
+ highlightWorker: { default: null },
+ },
props: {
blob: {
type: Object,
required: true,
},
- },
- data() {
- return {
- languageDefinition: null,
- content: this.blob.rawTextBlob,
- language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()],
- hljs: null,
- firstChunk: null,
- chunks: {},
- isLoading: true,
- isLineSelected: false,
- lineHighlighter: null,
- };
- },
- computed: {
- splitContent() {
- return this.content.split(/\r?\n/);
- },
- lineNumbers() {
- return this.splitContent.length;
- },
- unsupportedLanguage() {
- const supportedLanguages = Object.keys(languageLoader);
- const unsupportedLanguage =
- !supportedLanguages.includes(this.language) &&
- !supportedLanguages.includes(this.blob.language?.toLowerCase());
-
- return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage;
- },
- totalChunks() {
- return Object.keys(this.chunks).length;
+ chunks: {
+ type: Array,
+ required: false,
+ default: () => [],
},
},
- async created() {
+ created() {
+ this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
- this.trackEvent(EVENT_LABEL_VIEWER);
-
- if (this.unsupportedLanguage) {
- this.handleUnsupportedLanguage();
- return;
- }
-
- this.generateFirstChunk();
- this.hljs = await this.loadHighlightJS();
-
- if (this.language) {
- this.languageDefinition = await this.loadLanguage();
- }
-
- // Highlight the first chunk as soon as highlight.js is available
- this.highlightChunk(null, true);
-
- window.requestIdleCallback(async () => {
- // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first
- this.generateRemainingChunks();
- this.isLoading = false;
- await this.$nextTick();
- this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
- });
- },
- methods: {
- trackEvent(label) {
- this.track(EVENT_ACTION, { label, property: this.blob.language });
- },
- handleUnsupportedLanguage() {
- this.trackEvent(EVENT_LABEL_FALLBACK);
- this.$emit('error');
- },
- generateFirstChunk() {
- const lines = this.splitContent.splice(0, LINES_PER_CHUNK);
- this.firstChunk = this.createChunk(lines);
- },
- generateRemainingChunks() {
- const result = {};
- for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) {
- const chunkIndex = Math.floor(i / LINES_PER_CHUNK);
- const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK);
- result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK);
- }
-
- this.chunks = result;
- },
- createChunk(lines, startingFrom = 0) {
- return {
- content: lines.join('\n'),
- startingFrom,
- totalLines: lines.length,
- language: this.language,
- isHighlighted: false,
- };
- },
- highlightChunk(index, isFirstChunk) {
- const chunk = isFirstChunk ? this.firstChunk : this.chunks[index];
-
- if (chunk.isHighlighted) {
- return;
- }
-
- const { highlightedContent, language } = this.highlight(chunk.content, this.language);
-
- Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true });
-
- this.selectLine();
-
- this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path));
- },
- highlight(content, language) {
- let detectedLanguage = language;
- let highlightedContent;
- if (this.hljs) {
- registerPlugins(this.hljs, this.blob.fileType, this.content);
- if (!detectedLanguage) {
- const hljsHighlightAuto = this.hljs.highlightAuto(content);
- highlightedContent = hljsHighlightAuto.value;
- detectedLanguage = hljsHighlightAuto.language;
- } else if (this.languageDefinition) {
- highlightedContent = this.hljs.highlight(content, { language: this.language }).value;
- }
- }
-
- return { highlightedContent, language: detectedLanguage };
- },
- loadHighlightJS() {
- // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint)
- return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
- },
- async loadLanguage() {
- let languageDefinition;
-
- try {
- languageDefinition = await languageLoader[this.language]();
- this.hljs.registerLanguage(this.language, languageDefinition.default);
- } catch (message) {
- this.$emit('error', message);
- }
-
- return languageDefinition;
- },
- async selectLine() {
- if (this.isLineSelected || !this.lineHighlighter) {
- return;
- }
-
- this.isLineSelected = true;
- await this.$nextTick();
- this.lineHighlighter.highlightHash(this.$route.hash);
- },
},
userColorScheme: window.gon.user_color_scheme,
- currentlySelectedLine: null,
};
</script>
+
<template>
<div
class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
@@ -196,32 +44,15 @@ export default {
data-qa-selector="blob_viewer_file_content"
>
<chunk
- v-if="firstChunk"
- :lines="firstChunk.lines"
- :total-lines="firstChunk.totalLines"
- :content="firstChunk.content"
- :starting-from="firstChunk.startingFrom"
- :is-highlighted="firstChunk.isHighlighted"
- is-first-chunk
- :language="firstChunk.language"
- :blame-path="blob.blamePath"
- />
-
- <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
- <chunk
- v-for="(chunk, key, index) in chunks"
- v-else
- :key="key"
- :lines="chunk.lines"
- :content="chunk.content"
+ v-for="(chunk, _, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
- :is-highlighted="chunk.isHighlighted"
- :chunk-index="index"
- :language="chunk.language"
:blame-path="blob.blamePath"
- :total-chunks="totalChunks"
- @appear="highlightChunk"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue
new file mode 100644
index 00000000000..26cf45c7570
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_deprecated.vue
@@ -0,0 +1,227 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import Tracking from '~/tracking';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+ LEGACY_FALLBACKS,
+} from './constants';
+import Chunk from './components/chunk_deprecated.vue';
+import { registerPlugins } from './plugins/index';
+
+/*
+ * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
+ * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible.
+ *
+ * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback).
+ * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes,
+ * it does not trigger a repaint on a parent element that wraps all 1000 lines.
+ */
+export default {
+ components: {
+ GlLoadingIcon,
+ Chunk,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ languageDefinition: null,
+ content: this.blob.rawTextBlob,
+ language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()],
+ hljs: null,
+ firstChunk: null,
+ chunks: {},
+ isLoading: true,
+ isLineSelected: false,
+ lineHighlighter: null,
+ };
+ },
+ computed: {
+ splitContent() {
+ return this.content.split(/\r?\n/);
+ },
+ lineNumbers() {
+ return this.splitContent.length;
+ },
+ unsupportedLanguage() {
+ const supportedLanguages = Object.keys(languageLoader);
+ const unsupportedLanguage =
+ !supportedLanguages.includes(this.language) &&
+ !supportedLanguages.includes(this.blob.language?.toLowerCase());
+
+ return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage;
+ },
+ totalChunks() {
+ return Object.keys(this.chunks).length;
+ },
+ },
+ async created() {
+ addBlobLinksTracking();
+ this.trackEvent(EVENT_LABEL_VIEWER);
+
+ if (this.unsupportedLanguage) {
+ this.handleUnsupportedLanguage();
+ return;
+ }
+
+ this.generateFirstChunk();
+ this.hljs = await this.loadHighlightJS();
+
+ if (this.language) {
+ this.languageDefinition = await this.loadLanguage();
+ }
+
+ // Highlight the first chunk as soon as highlight.js is available
+ this.highlightChunk(null, true);
+
+ window.requestIdleCallback(async () => {
+ // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first
+ this.generateRemainingChunks();
+ this.isLoading = false;
+ await this.$nextTick();
+ this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ });
+ },
+ methods: {
+ trackEvent(label) {
+ this.track(EVENT_ACTION, { label, property: this.blob.language });
+ },
+ handleUnsupportedLanguage() {
+ this.trackEvent(EVENT_LABEL_FALLBACK);
+ this.$emit('error');
+ },
+ generateFirstChunk() {
+ const lines = this.splitContent.splice(0, LINES_PER_CHUNK);
+ this.firstChunk = this.createChunk(lines);
+ },
+ generateRemainingChunks() {
+ const result = {};
+ for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) {
+ const chunkIndex = Math.floor(i / LINES_PER_CHUNK);
+ const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK);
+ result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK);
+ }
+
+ this.chunks = result;
+ },
+ createChunk(lines, startingFrom = 0) {
+ return {
+ content: lines.join('\n'),
+ startingFrom,
+ totalLines: lines.length,
+ language: this.language,
+ isHighlighted: false,
+ };
+ },
+ highlightChunk(index, isFirstChunk) {
+ const chunk = isFirstChunk ? this.firstChunk : this.chunks[index];
+
+ if (chunk.isHighlighted) {
+ return;
+ }
+
+ const { highlightedContent, language } = this.highlight(chunk.content, this.language);
+
+ Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true });
+
+ this.selectLine();
+
+ this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path));
+ },
+ highlight(content, language) {
+ let detectedLanguage = language;
+ let highlightedContent;
+ if (this.hljs) {
+ registerPlugins(this.hljs, this.blob.fileType, this.content);
+ if (!detectedLanguage) {
+ const hljsHighlightAuto = this.hljs.highlightAuto(content);
+ highlightedContent = hljsHighlightAuto.value;
+ detectedLanguage = hljsHighlightAuto.language;
+ } else if (this.languageDefinition) {
+ highlightedContent = this.hljs.highlight(content, { language: this.language }).value;
+ }
+ }
+
+ return { highlightedContent, language: detectedLanguage };
+ },
+ loadHighlightJS() {
+ // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint)
+ return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
+ },
+ async loadLanguage() {
+ let languageDefinition;
+
+ try {
+ languageDefinition = await languageLoader[this.language]();
+ this.hljs.registerLanguage(this.language, languageDefinition.default);
+ } catch (message) {
+ this.$emit('error', message);
+ }
+
+ return languageDefinition;
+ },
+ async selectLine() {
+ if (this.isLineSelected || !this.lineHighlighter) {
+ return;
+ }
+
+ this.isLineSelected = true;
+ await this.$nextTick();
+ this.lineHighlighter.highlightHash(this.$route.hash);
+ },
+ },
+ userColorScheme: window.gon.user_color_scheme,
+ currentlySelectedLine: null,
+};
+</script>
+<template>
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ data-qa-selector="blob_viewer_file_content"
+ >
+ <chunk
+ v-if="firstChunk"
+ :lines="firstChunk.lines"
+ :total-lines="firstChunk.totalLines"
+ :content="firstChunk.content"
+ :starting-from="firstChunk.startingFrom"
+ :is-highlighted="firstChunk.isHighlighted"
+ is-first-chunk
+ :language="firstChunk.language"
+ :blame-path="blob.blamePath"
+ />
+
+ <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
+ <chunk
+ v-for="(chunk, key, index) in chunks"
+ v-else
+ :key="key"
+ :lines="chunk.lines"
+ :content="chunk.content"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :is-highlighted="chunk.isHighlighted"
+ :chunk-index="index"
+ :language="chunk.language"
+ :blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
+ @appear="highlightChunk"
+ />
+ </div>
+</template>
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 0da57f9e6fa..142c135e9c1 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,15 +1,47 @@
-import hljs from 'highlight.js/lib/core';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import hljs from 'highlight.js';
import { registerPlugins } from '../plugins/index';
+import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants';
-const initHighlightJs = async (fileType, content, language) => {
- const languageDefinition = await languageLoader[language]();
-
+const initHighlightJs = (fileType, content) => {
registerPlugins(hljs, fileType, content);
- hljs.registerLanguage(language, languageDefinition.default);
};
-export const highlight = (fileType, content, language) => {
- initHighlightJs(fileType, content, language);
- return hljs.highlight(content, { language }).value;
+const splitByLineBreaks = (content = '') => content.split(/\r?\n/);
+
+const createChunk = (language, rawChunkLines, highlightedChunkLines = [], startingFrom = 0) => ({
+ highlightedContent: highlightedChunkLines.join(NEWLINE),
+ rawContent: rawChunkLines.join(NEWLINE),
+ totalLines: rawChunkLines.length,
+ startingFrom,
+ language,
+});
+
+const splitIntoChunks = (language, rawContent, highlightedContent) => {
+ const result = [];
+ const splitRawContent = splitByLineBreaks(rawContent);
+ const splitHighlightedContent = splitByLineBreaks(highlightedContent);
+
+ for (let i = 0; i < splitRawContent.length; i += LINES_PER_CHUNK) {
+ const chunkIndex = Math.floor(i / LINES_PER_CHUNK);
+ const highlightedChunk = splitHighlightedContent.slice(i, i + LINES_PER_CHUNK);
+ const rawChunk = splitRawContent.slice(i, i + LINES_PER_CHUNK);
+ result[chunkIndex] = createChunk(language, rawChunk, highlightedChunk, i);
+ }
+
+ return result;
+};
+
+const highlight = (fileType, rawContent, lang) => {
+ const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()];
+ let result;
+
+ if (language) {
+ initHighlightJs(fileType, rawContent, language);
+ const highlightedContent = hljs.highlight(rawContent, { language }).value;
+ result = splitIntoChunks(language, rawContent, highlightedContent);
+ }
+
+ return result;
};
+
+export { highlight, splitIntoChunks };
diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue
index bd5b7b77017..ad81c14d9e5 100644
--- a/app/assets/javascripts/vue_shared/components/url_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/url_sync.vue
@@ -1,7 +1,9 @@
<script>
-import { historyPushState } from '~/lib/utils/common_utils';
+import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils';
import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
+export const HISTORY_PUSH_UPDATE_METHOD = 'push';
+export const HISTORY_REPLACE_UPDATE_METHOD = 'replace';
export const URL_SET_PARAMS_STRATEGY = 'set';
export const URL_MERGE_PARAMS_STRATEGY = 'merge';
@@ -24,6 +26,13 @@ export default {
default: URL_MERGE_PARAMS_STRATEGY,
validator: (value) => [URL_MERGE_PARAMS_STRATEGY, URL_SET_PARAMS_STRATEGY].includes(value),
},
+ historyUpdateMethod: {
+ type: String,
+ required: false,
+ default: HISTORY_PUSH_UPDATE_METHOD,
+ validator: (value) =>
+ [HISTORY_PUSH_UPDATE_METHOD, HISTORY_REPLACE_UPDATE_METHOD].includes(value),
+ },
},
watch: {
query: {
@@ -40,9 +49,14 @@ export default {
updateQuery(newQuery) {
const url =
this.urlParamsUpdateStrategy === URL_SET_PARAMS_STRATEGY
- ? setUrlParams(this.query, window.location.href, true)
+ ? setUrlParams(this.query, window.location.href, true, true, true)
: mergeUrlParams(newQuery, window.location.href, { spreadArrays: true });
- historyPushState(url);
+
+ if (this.historyUpdateMethod === HISTORY_PUSH_UPDATE_METHOD) {
+ historyPushState(url);
+ } else {
+ historyReplaceState(url);
+ }
},
},
render() {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
index 231f5ff3d1f..167db3ce1f2 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -74,8 +74,8 @@ export default {
<user-avatar-link
v-for="item in visibleItems"
:key="item.id"
- :link-href="item.web_url"
- :img-src="item.avatar_url"
+ :link-href="item.web_url || item.webUrl"
+ :img-src="item.avatar_url || item.avatarUrl"
:img-alt="item.name"
:tooltip-text="item.name"
:img-size="imgSize"
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 86a99b8f0ed..edcfabe7da3 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -2,18 +2,19 @@
import { debounce } from 'lodash';
import {
GlDropdown,
- GlDropdownForm,
GlDropdownDivider,
+ GlDropdownForm,
GlDropdownItem,
- GlSearchBoxByType,
GlLoadingIcon,
+ GlSearchBoxByType,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
+import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
export default {
@@ -47,7 +48,8 @@ export default {
},
iid: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
value: {
type: Array,
@@ -65,7 +67,7 @@ export default {
issuableType: {
type: String,
required: false,
- default: IssuableType.Issue,
+ default: TYPE_ISSUE,
},
isEditing: {
type: Boolean,
@@ -160,20 +162,17 @@ export default {
}
return {
...variables,
- mergeRequestId: convertToGraphQLId('MergeRequest', this.issuableId),
+ mergeRequestId: convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.issuableId),
};
},
isLoading() {
return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading;
},
users() {
- if (!this.participants) {
- return [];
- }
-
- const filteredParticipants = this.participants.filter(
- (user) => user.name.includes(this.search) || user.username.includes(this.search),
- );
+ const filteredParticipants =
+ this.participants?.filter(
+ (user) => user.name.includes(this.search) || user.username.includes(this.search),
+ ) || [];
// TODO this de-duplication is temporary (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/327822
@@ -254,6 +253,10 @@ export default {
this.$emit('input', selected);
}
},
+ unassign() {
+ this.$emit('input', []);
+ this.$refs.dropdown.hide();
+ },
unselect(name) {
const selected = this.value.filter((user) => user.username !== name);
this.$emit('input', selected);
@@ -323,7 +326,7 @@ export default {
:is-checked="selectedIsEmpty"
is-check-centered
data-testid="unassign"
- @click.native.capture.stop="$emit('input', [])"
+ @click.native.capture.stop="unassign"
>
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{
$options.i18n.unassigned
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 98630512308..28bec63b244 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -423,6 +423,7 @@ export default {
target="_blank"
:href="webIdeUrl"
block
+ @click="dismissCalloutOnActionClicked(dismiss)"
>
{{ __('Try it out now') }}
</gl-link>