Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/pages
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/labels/index/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue18
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/milestones/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/milestones/new/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js (renamed from app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js)0
-rw-r--r--app/assets/javascripts/pages/groups/settings/repository/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/pages/help/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue126
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js42
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js4
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue17
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue10
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue12
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue10
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js82
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js16
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql7
-rw-r--r--app/assets/javascripts/pages/projects/milestones/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/new/components/app.vue148
-rw-r--r--app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue66
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js54
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js18
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js8
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue298
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
39 files changed, 864 insertions, 187 deletions
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index cf06ee2c22a..d6fa1be29b0 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,3 +1,5 @@
-import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
+import initDevOpsScore from '~/analytics/devops_report/devops_score';
+import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
-initDevOpsScoreEmptyState();
+initDevOpsScoreDisabledUsagePing();
+initDevOpsScore();
diff --git a/app/assets/javascripts/pages/admin/labels/index/index.js b/app/assets/javascripts/pages/admin/labels/index/index.js
index e5ab5d43bbf..17ee7c03ed6 100644
--- a/app/assets/javascripts/pages/admin/labels/index/index.js
+++ b/app/assets/javascripts/pages/admin/labels/index/index.js
@@ -1,3 +1,21 @@
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
+document.addEventListener('DOMContentLoaded', () => {
+ const pagination = document.querySelector('.labels .gl-pagination');
+ const emptyState = document.querySelector('.labels .nothing-here-block.hidden');
-document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
+ function removeLabelSuccessCallback() {
+ this.closest('li').classList.add('gl-display-none!');
+
+ const labelsCount = document.querySelectorAll(
+ 'ul.manage-labels-list li:not(.gl-display-none\\!)',
+ ).length;
+
+ // display the empty state if there are no more labels
+ if (labelsCount < 1 && !pagination && emptyState) {
+ emptyState.classList.remove('hidden');
+ }
+ }
+
+ document.querySelectorAll('.js-remove-label').forEach((row) => {
+ row.addEventListener('ajax:success', removeLabelSuccessCallback);
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 20407334b3f..a3b78da6ef5 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,6 +1,8 @@
<script>
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { s__, sprintf } from '~/locale';
+import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
export default {
components: {
@@ -8,6 +10,7 @@ export default {
GlButton,
GlFormInput,
GlSprintf,
+ OncallSchedulesList,
},
props: {
title: {
@@ -42,6 +45,11 @@ export default {
type: String,
required: true,
},
+ oncallSchedules: {
+ type: String,
+ required: false,
+ default: '[]',
+ },
},
data() {
return {
@@ -58,6 +66,14 @@ export default {
canSubmit() {
return this.enteredUsername === this.username;
},
+ schedules() {
+ try {
+ return JSON.parse(this.oncallSchedules);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+ return [];
+ },
},
methods: {
show() {
@@ -96,6 +112,8 @@ export default {
</gl-sprintf>
</p>
+ <oncall-schedules-list v-if="schedules.length" :schedules="schedules" />
+
<p>
<gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
<template #username>
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index b1079c3b068..9a8b0c9990f 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
-import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
-import initTabs from '~/admin/users/tabs';
+import { initAdminUsersApp } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
import csrf from '~/lib/utils/csrf';
import Translate from '~/vue_shared/translate';
@@ -62,6 +61,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
initConfirmModal();
- initCohortsEmptyState();
- initTabs();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index b60607e8857..76db578f6f9 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
-import initIssuablesList from '~/issues_list';
+import { mountIssuablesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
@@ -12,8 +12,6 @@ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
-initIssuablesList();
-
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
@@ -22,3 +20,7 @@ initFilteredSearch({
});
projectSelect();
initManualOrdering();
+
+if (gon.features?.vueIssuablesList) {
+ mountIssuablesListApp();
+}
diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js
index af0264c7992..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/groups/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js
index af0264c7992..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/groups/milestones/new/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js b/app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
index 3b922622d2c..3b922622d2c 100644
--- a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
+++ b/app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
diff --git a/app/assets/javascripts/pages/groups/settings/repository/show/index.js b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
index 92405f205cb..f048955dadf 100644
--- a/app/assets/javascripts/pages/groups/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
@@ -1,7 +1,8 @@
-import DueDateSelectors from '~/due_date_select';
+import initDatePicker from '~/behaviors/date_picker';
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
-new DueDateSelectors(); // eslint-disable-line no-new
+// Used for deploy tokens "expires at" field
+initDatePicker();
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 9e75985c130..2aec0617b5a 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -3,6 +3,7 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
@@ -24,4 +25,5 @@ export default function initGroupDetails(actionName = 'show') {
new ProjectsList();
initInviteMembersBanner();
+ initInviteMembersModal();
}
diff --git a/app/assets/javascripts/pages/help/show/index.js b/app/assets/javascripts/pages/help/show/index.js
deleted file mode 100644
index ec426a850b6..00000000000
--- a/app/assets/javascripts/pages/help/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initHelp from '~/help/help';
-
-document.addEventListener('DOMContentLoaded', initHelp);
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index fc2702b8c37..8a8ce70e998 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,25 +1,35 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
+import createDefaultClient from '~/lib/graphql';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
const viewBlobEl = document.querySelector('#js-view-blob-app');
if (viewBlobEl) {
- const { blobPath } = viewBlobEl.dataset;
+ const { blobPath, projectPath } = viewBlobEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
+ apolloProvider,
render(createElement) {
return createElement(BlobContentViewer, {
props: {
path: blobPath,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 90a663802d2..d75c3cc6b8b 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -33,7 +33,7 @@ if (filesContainer.length) {
axios
.get(batchPath)
.then(({ data }) => {
- filesContainer.html($(data.html));
+ filesContainer.html($(data));
syntaxHighlight(filesContainer);
handleLocationHash();
new Diff();
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
deleted file mode 100644
index 768da8fb236..00000000000
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initCompareAutocomplete from '~/compare_autocomplete';
-
-document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 288d6711682..07cc0ce46bc 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -20,6 +20,7 @@ import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import validation from '~/vue_shared/directives/validation';
const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
@@ -31,6 +32,13 @@ const ALLOWED_VISIBILITY = {
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
};
+const initFormField = ({ value, required = true, skipValidation = false }) => ({
+ value,
+ required,
+ state: skipValidation ? true : null,
+ feedback: null,
+});
+
export default {
components: {
GlForm,
@@ -46,6 +54,9 @@ export default {
GlFormRadioGroup,
GlFormSelect,
},
+ directives: {
+ validation: validation(),
+ },
inject: {
newGroupPath: {
default: '',
@@ -77,7 +88,8 @@ export default {
},
projectDescription: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
projectVisibility: {
type: String,
@@ -85,16 +97,30 @@ export default {
},
},
data() {
+ const form = {
+ state: false,
+ showValidation: false,
+ fields: {
+ namespace: initFormField({
+ value: null,
+ }),
+ name: initFormField({ value: this.projectName }),
+ slug: initFormField({ value: this.projectPath }),
+ description: initFormField({
+ value: this.projectDescription,
+ required: false,
+ skipValidation: true,
+ }),
+ visibility: initFormField({
+ value: this.projectVisibility,
+ skipValidation: true,
+ }),
+ },
+ };
return {
isSaving: false,
namespaces: [],
- selectedNamespace: {},
- fork: {
- name: this.projectName,
- slug: this.projectPath,
- description: this.projectDescription,
- visibility: this.projectVisibility,
- },
+ form,
};
},
computed: {
@@ -106,7 +132,7 @@ export default {
},
namespaceAllowedVisibility() {
return (
- ALLOWED_VISIBILITY[this.selectedNamespace.visibility] ||
+ ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
);
},
@@ -139,16 +165,17 @@ export default {
},
},
watch: {
- selectedNamespace(newVal) {
+ // eslint-disable-next-line func-names
+ 'form.fields.namespace.value': function (newVal) {
const { visibility } = newVal;
if (this.projectAllowedVisibility.includes(visibility)) {
- this.fork.visibility = visibility;
+ this.form.fields.visibility.value = visibility;
}
},
// eslint-disable-next-line func-names
- 'fork.name': function (newVal) {
- this.fork.slug = kebabCase(newVal);
+ 'form.fields.name.value': function (newVal) {
+ this.form.fields.slug.value = kebabCase(newVal);
},
},
mounted() {
@@ -166,19 +193,25 @@ export default {
);
},
async onSubmit() {
+ this.form.showValidation = true;
+
+ if (!this.form.state) {
+ return;
+ }
+
this.isSaving = true;
+ this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility } = this.fork;
- const { id: namespaceId } = this.selectedNamespace;
+ const { name, slug, description, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
- name,
- namespace_id: namespaceId,
- path: slug,
- description,
- visibility,
+ name: name.value,
+ namespace_id: namespace.value.id,
+ path: slug.value,
+ description: description.value,
+ visibility: visibility.value,
};
const forkProjectPath = `/api/:version/projects/:id/fork`;
@@ -198,16 +231,34 @@ export default {
</script>
<template>
- <gl-form method="POST" @submit.prevent="onSubmit">
+ <gl-form novalidate method="POST" @submit.prevent="onSubmit">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <gl-form-group label="Project name" label-for="fork-name">
- <gl-form-input id="fork-name" v-model="fork.name" data-testid="fork-name-input" required />
+ <gl-form-group
+ :label="__('Project name')"
+ label-for="fork-name"
+ :invalid-feedback="form.fields.name.feedback"
+ >
+ <gl-form-input
+ id="fork-name"
+ v-model="form.fields.name.value"
+ v-validation:[form.showValidation]
+ name="name"
+ data-testid="fork-name-input"
+ :state="form.fields.name.state"
+ required
+ />
</gl-form-group>
<div class="gl-md-display-flex">
<div class="gl-flex-basis-half">
- <gl-form-group label="Project URL" label-for="fork-url" class="gl-md-mr-3">
+ <gl-form-group
+ :label="__('Project URL')"
+ label-for="fork-url"
+ class="gl-md-mr-3"
+ :state="form.fields.namespace.state"
+ :invalid-feedback="s__('ForkProject|Please select a namespace')"
+ >
<gl-form-input-group>
<template #prepend>
<gl-input-group-text>
@@ -216,9 +267,12 @@ export default {
</template>
<gl-form-select
id="fork-url"
- v-model="selectedNamespace"
+ v-model="form.fields.namespace.value"
+ v-validation:[form.showValidation]
+ name="namespace"
data-testid="fork-url-input"
data-qa-selector="fork_namespace_dropdown"
+ :state="form.fields.namespace.state"
required
>
<template slot="first">
@@ -232,11 +286,19 @@ export default {
</gl-form-group>
</div>
<div class="gl-flex-basis-half">
- <gl-form-group label="Project slug" label-for="fork-slug" class="gl-md-ml-3">
+ <gl-form-group
+ :label="__('Project slug')"
+ label-for="fork-slug"
+ class="gl-md-ml-3"
+ :invalid-feedback="form.fields.slug.feedback"
+ >
<gl-form-input
id="fork-slug"
- v-model="fork.slug"
+ v-model="form.fields.slug.value"
+ v-validation:[form.showValidation]
data-testid="fork-slug-input"
+ name="slug"
+ :state="form.fields.slug.state"
required
/>
</gl-form-group>
@@ -250,11 +312,13 @@ export default {
</gl-link>
</p>
- <gl-form-group label="Project description (optional)" label-for="fork-description">
+ <gl-form-group :label="__('Project description (optional)')" label-for="fork-description">
<gl-form-textarea
id="fork-description"
- v-model="fork.description"
+ v-model="form.fields.description.value"
data-testid="fork-description-textarea"
+ name="description"
+ :state="form.fields.description.state"
/>
</gl-form-group>
@@ -266,8 +330,9 @@ export default {
</gl-link>
</label>
<gl-form-radio-group
- v-model="fork.visibility"
+ v-model="form.fields.visibility.value"
data-testid="fork-visibility-radio-group"
+ name="visibility"
required
>
<gl-form-radio
@@ -291,6 +356,7 @@ export default {
type="submit"
category="primary"
variant="confirm"
+ class="js-no-auto-disable"
data-testid="submit-button"
data-qa-selector="fork_project_button"
:loading="isSaving"
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 85489ae8687..8cd703133f5 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,36 +1,38 @@
-/* eslint-disable no-new */
-
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
import IssuableIndex from '~/issuable_index';
-import initIssuablesList, { initIssuesListApp } from '~/issues_list';
+import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
-IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
-
-initFilteredSearch({
- page: FILTERED_SEARCH.ISSUES,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
- useDefaultState: true,
-});
-
if (gon.features?.vueIssuesList) {
- new IssuableIndex();
+ mountIssuesListApp();
} else {
- new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+
+ initFilteredSearch({
+ page: FILTERED_SEARCH.ISSUES,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
+ });
+
+ new IssuableIndex(ISSUABLE_INDEX.ISSUE); // eslint-disable-line no-new
+ new UsersSelect(); // eslint-disable-line no-new
+
+ initCsvImportExportButtons();
+ initIssuableByEmail();
+ initManualOrdering();
+
+ if (gon.features?.vueIssuablesList) {
+ mountIssuablesListApp();
+ }
}
-new ShortcutsNavigation();
-new UsersSelect();
+new ShortcutsNavigation(); // eslint-disable-line no-new
-initManualOrdering();
-initIssuablesList();
-initIssuableByEmail();
-initCsvImportExportButtons();
-initIssuesListApp();
+mountJiraIssuesListApp();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 5be9f6117dc..d906c579697 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,4 +1,4 @@
-import initIssuablesList from '~/issues_list';
+import { mountIssuablesListApp } from '~/issues_list';
import FilteredSearchServiceDesk from './filtered_search';
const supportBotData = JSON.parse(
@@ -11,5 +11,5 @@ if (document.querySelector('.filtered-search')) {
}
if (gon.features?.vueIssuablesList) {
- initIssuablesList();
+ mountIssuablesListApp();
}
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 2b679a83eac..3143ff5adac 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,8 +1,6 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
@@ -58,7 +56,5 @@ export default function initShowIssue() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
}
}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
index ef9e13f7ccf..51980b2d971 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -18,9 +18,13 @@ export default {
required: true,
type: Object,
},
+ sections: {
+ required: true,
+ type: Object,
+ },
},
maxValue: Object.keys(ACTION_LABELS).length,
- sections: Object.keys(ACTION_SECTIONS),
+ actionSections: Object.keys(ACTION_SECTIONS),
computed: {
progressValue() {
return Object.values(this.actions).filter((a) => a.completed).length;
@@ -38,6 +42,9 @@ export default {
);
return actions;
},
+ svgFor(section) {
+ return this.sections[section].svg;
+ },
},
};
</script>
@@ -59,8 +66,12 @@ export default {
<gl-progress-bar :value="progressValue" :max="$options.maxValue" />
</div>
<div class="row row-cols-1 row-cols-md-3 gl-mt-5">
- <div v-for="section in $options.sections" :key="section" class="col gl-mb-6">
- <learn-gitlab-section-card :section="section" :actions="actionsFor(section)" />
+ <div v-for="section in $options.actionSections" :key="section" class="col gl-mb-6">
+ <learn-gitlab-section-card
+ :section="section"
+ :svg="svgFor(section)"
+ :actions="actionsFor(section)"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
index 6cd3bbc359b..ad6dfbf41ca 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
@@ -64,7 +64,15 @@ export default {
<img :src="svg" :alt="actionLabel" />
<h6>{{ title }}</h6>
<p class="gl-font-sm gl-text-gray-700">{{ description }}</p>
- <gl-link :href="url" target="_blank">{{ actionLabel }}</gl-link>
+ <gl-link
+ :href="url"
+ target="_blank"
+ rel="noopener noreferrer"
+ data-track-action="click_link"
+ :data-track-label="actionLabel"
+ data-track-property="Growth::Activation::Experiment::LearnGitLabB"
+ >{{ actionLabel }}</gl-link
+ >
</div>
</gl-card>
</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
index db694a66afd..6a196687a76 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
@@ -1,6 +1,5 @@
<script>
import { GlCard } from '@gitlab/ui';
-import { imagePath } from '~/lib/utils/common_utils';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
@@ -16,6 +15,10 @@ export default {
required: true,
type: String,
},
+ svg: {
+ required: true,
+ type: String,
+ },
actions: {
required: true,
type: Object,
@@ -28,17 +31,12 @@ export default {
);
},
},
- methods: {
- svg(section) {
- return imagePath(`learn_gitlab/section_${section}.svg`);
- },
- },
};
</script>
<template>
<gl-card class="gl-pt-0 learn-gitlab-section-card">
<div class="learn-gitlab-section-card-header">
- <img :src="svg(section)" />
+ <img :src="svg" />
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
</div>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 6f51c7372fd..3d31ac6c267 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -34,7 +34,15 @@ export default {
{{ $options.i18n.ACTION_LABELS[action].title }}
</span>
<span v-else>
- <gl-link :href="value.url">{{ $options.i18n.ACTION_LABELS[action].title }}</gl-link>
+ <gl-link
+ target="_blank"
+ :href="value.url"
+ data-track-action="click_link"
+ :data-track-label="$options.i18n.ACTION_LABELS[action].title"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ >
+ {{ $options.i18n.ACTION_LABELS[action].title }}
+ </gl-link>
</span>
<span v-if="trialOnly" class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
- {{ $options.i18n.trialOnly }}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index c4dec89b984..ac7c94bdd9e 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import trackLearnGitlab from '~/learn_gitlab/track_learn_gitlab';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlabA from '../components/learn_gitlab_a.vue';
import LearnGitlabB from '../components/learn_gitlab_b.vue';
@@ -11,13 +12,18 @@ function initLearnGitlab() {
}
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
+ const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const { learnGitlabA } = gon.experiments;
+ trackLearnGitlab(learnGitlabA);
+
return new Vue({
el,
render(createElement) {
- return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
+ return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, {
+ props: { actions, sections },
+ });
},
});
}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index 1a0fa6e544e..8d152ec4ba6 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import initCompareAutocomplete from '~/compare_autocomplete';
import axios from '~/lib/utils/axios_utils';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import initCompareAutocomplete from './compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
new file mode 100644
index 00000000000..68ab7021cf3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -0,0 +1,82 @@
+/* eslint-disable func-names */
+
+import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
+import { fixTitle } from '~/tooltips';
+
+export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
+ $('.js-compare-dropdown').each(function () {
+ const $dropdown = $(this);
+ const selected = $dropdown.data('selected');
+ const $dropdownContainer = $dropdown.closest('.dropdown');
+ const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
+ const $filterInput = $('input[type="search"]', $dropdownContainer);
+ initDeprecatedJQueryDropdown($dropdown, {
+ data(term, callback) {
+ const params = {
+ ref: $dropdown.data('ref'),
+ search: term,
+ };
+
+ if (limitTo) {
+ params.find = limitTo;
+ }
+
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params,
+ })
+ .then(({ data }) => {
+ if (limitTo) {
+ callback(data[capitalizeFirstCharacter(limitTo)] || []);
+ } else {
+ callback(data);
+ }
+ })
+ .catch(() => flash(__('Error fetching refs')));
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: Boolean($dropdown.data('refsUrl')),
+ fieldName: $dropdown.data('fieldName'),
+ filterInput: 'input[type="search"]',
+ renderRow(ref) {
+ const link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', ref);
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ }
+ return $('<li />').append(link);
+ },
+ id(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: () => clickHandler($dropdown),
+ });
+ $filterInput.on('keyup', (e) => {
+ const keyCode = e.keyCode || e.which;
+ if (keyCode !== 13) return;
+ const text = $filterInput.val();
+ $fieldInput.val(text);
+ $('.dropdown-toggle-text', $dropdown).text(text);
+ $dropdownContainer.removeClass('open');
+ });
+
+ $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
+ if ($dropdown.hasClass('has-tooltip')) {
+ fixTitle($dropdown);
+ }
+ });
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index a5118e3529a..6cd3202815b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,16 +1,17 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import StatusBox from '~/issuable/components/status_box.vue';
+import createDefaultClient from '~/lib/graphql';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
+import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
@@ -28,15 +29,20 @@ export default function initMergeRequestShow() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
const el = document.querySelector('.js-mr-status-box');
+ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
+ provide: {
+ query: getStateQuery,
+ projectPath: el.dataset.projectPath,
+ iid: el.dataset.iid,
+ },
render(h) {
return h(StatusBox, {
props: {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
new file mode 100644
index 00000000000..b5a82b9428e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
@@ -0,0 +1,7 @@
+query getMergeRequestState($projectPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $projectPath) {
+ issuable: mergeRequest(iid: $iid) {
+ state
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js
index 364b0d95d9c..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/projects/milestones/new/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
initForm();
diff --git a/app/assets/javascripts/pages/projects/new/components/app.vue b/app/assets/javascripts/pages/projects/new/components/app.vue
new file mode 100644
index 00000000000..60a4fbc3e6b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/components/app.vue
@@ -0,0 +1,148 @@
+<script>
+import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg';
+import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
+import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
+import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { experiment } from '~/experimentation/utils';
+import { s__ } from '~/locale';
+import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
+import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
+
+const NEW_REPO_EXPERIMENT = 'new_repo';
+const CI_CD_PANEL = 'cicd_for_external_repo';
+const PANELS = [
+ {
+ key: 'blank',
+ name: 'blank_project',
+ selector: '#blank-project-pane',
+ title: s__('ProjectsNew|Create blank project'),
+ description: s__(
+ 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.',
+ ),
+ illustration: blankProjectIllustration,
+ },
+ {
+ key: 'template',
+ name: 'create_from_template',
+ selector: '#create-from-template-pane',
+ title: s__('ProjectsNew|Create from template'),
+ description: s__(
+ 'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.',
+ ),
+ illustration: createFromTemplateIllustration,
+ },
+ {
+ key: 'import',
+ name: 'import_project',
+ selector: '#import-project-pane',
+ title: s__('ProjectsNew|Import project'),
+ description: s__(
+ 'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
+ ),
+ illustration: importProjectIllustration,
+ },
+ {
+ key: 'ci',
+ name: CI_CD_PANEL,
+ selector: '#ci-cd-project-pane',
+ title: s__('ProjectsNew|Run CI/CD for external repository'),
+ description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'),
+ illustration: ciCdProjectIllustration,
+ },
+];
+
+export default {
+ components: {
+ NewNamespacePage,
+ NewProjectPushTipPopover,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ hasErrors: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isCiCdAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ newProjectGuidelines: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ computed: {
+ decoratedPanels() {
+ const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
+ use: () => ({
+ blank: s__('ProjectsNew|Create blank project'),
+ import: s__('ProjectsNew|Import project'),
+ }),
+ try: () => ({
+ blank: s__('ProjectsNew|Create blank project/repository'),
+ import: s__('ProjectsNew|Import project/repository'),
+ }),
+ });
+
+ return PANELS.map(({ key, title, ...el }) => ({
+ ...el,
+ title: PANEL_TITLES[key] ?? title,
+ }));
+ },
+
+ availablePanels() {
+ return this.isCiCdAvailable
+ ? this.decoratedPanels
+ : this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
+ },
+ },
+
+ methods: {
+ resetProjectErrors() {
+ const errorsContainer = document.querySelector('.project-edit-errors');
+ if (errorsContainer) {
+ errorsContainer.innerHTML = '';
+ }
+ },
+ },
+ EXPERIMENT: NEW_REPO_EXPERIMENT,
+};
+</script>
+
+<template>
+ <new-namespace-page
+ :initial-breadcrumb="s__('New project')"
+ :panels="availablePanels"
+ :jump-to-last-persisted-panel="hasErrors"
+ :title="s__('ProjectsNew|Create new project')"
+ :experiment="$options.EXPERIMENT"
+ persistence-key="new_project_last_active_tab"
+ @panel-change="resetProjectErrors"
+ >
+ <template #extra-description>
+ <div
+ v-if="newProjectGuidelines"
+ id="new-project-guideline"
+ v-safe-html="newProjectGuidelines"
+ ></div>
+ </template>
+ <template #welcome-footer>
+ <div class="gl-pt-5 gl-text-center">
+ <p>
+ {{ __('You can also create a project from the command line.') }}
+ <a ref="clipTip" href="#" @click.prevent>
+ {{ __('Show command') }}
+ </a>
+ <new-project-push-tip-popover :target="() => $refs.clipTip" />
+ </p>
+ </div>
+ </template>
+ </new-namespace-page>
+</template>
diff --git a/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue
new file mode 100644
index 00000000000..e42d9154866
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlPopover, GlFormInputGroup } from '@gitlab/ui';
+import { __ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ components: {
+ GlPopover,
+ GlFormInputGroup,
+ ClipboardButton,
+ },
+ inject: ['pushToCreateProjectCommand', 'workingWithProjectsHelpPath'],
+ props: {
+ target: {
+ type: [Function, HTMLElement],
+ required: true,
+ },
+ },
+ i18n: {
+ clipboardButtonTitle: __('Copy command'),
+ commandInputAriaLabel: __('Push project from command line'),
+ helpLinkText: __('What does this command do?'),
+ labelText: __('Private projects can be created in your personal namespace with:'),
+ popoverTitle: __('Push to create a project'),
+ },
+};
+</script>
+<template>
+ <gl-popover
+ :target="target"
+ :title="$options.i18n.popoverTitle"
+ triggers="click blur"
+ placement="top"
+ >
+ <p>
+ <label for="push-to-create-tip" class="gl-font-weight-normal">
+ {{ $options.i18n.labelText }}
+ </label>
+ </p>
+ <p>
+ <gl-form-input-group
+ id="push-to-create-tip"
+ :value="pushToCreateProjectCommand"
+ readonly
+ select-on-click
+ :aria-label="$options.i18n.commandInputAriaLabel"
+ >
+ <template #append>
+ <clipboard-button
+ :text="pushToCreateProjectCommand"
+ :title="$options.i18n.clipboardButtonTitle"
+ tooltip-placement="right"
+ />
+ </template>
+ </gl-form-input-group>
+ </p>
+ <p>
+ <a
+ :href="`${workingWithProjectsHelpPath}#push-to-create-a-new-project`"
+ class="gl-font-sm"
+ target="_blank"
+ >{{ $options.i18n.helpLinkText }}</a
+ >
+ </p>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index e10e2872dce..f469c56e808 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,28 +1,44 @@
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
+import NewProjectCreationApp from './components/app.vue';
initProjectVisibilitySelector();
initProjectNew.bindEvents();
-import(
- /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
-)
- .then((m) => {
- const el = document.querySelector('.js-experiment-new-project-creation');
+function initNewProjectCreation(el) {
+ const {
+ pushToCreateProjectCommand,
+ workingWithProjectsHelpPath,
+ newProjectGuidelines,
+ hasErrors,
+ isCiCdAvailable,
+ } = el.dataset;
- if (!el) {
- return;
- }
+ const props = {
+ hasErrors: parseBoolean(hasErrors),
+ isCiCdAvailable: parseBoolean(isCiCdAvailable),
+ newProjectGuidelines,
+ };
- const config = {
- hasErrors: 'hasErrors' in el.dataset,
- isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
- newProjectGuidelines: el.dataset.newProjectGuidelines,
- };
- m.default(el, config);
- })
- .catch(() => {
- createFlash(__('An error occurred while loading project creation UI'));
+ const provide = {
+ workingWithProjectsHelpPath,
+ pushToCreateProjectCommand,
+ };
+
+ return new Vue({
+ el,
+ components: {
+ NewProjectCreationApp,
+ },
+ provide,
+ render(h) {
+ return h(NewProjectCreationApp, { props });
+ },
});
+}
+
+const el = document.querySelector('.js-new-project-creation');
+
+initNewProjectCreation(el);
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 32299287a9c..e1f71965853 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,17 +1,3 @@
-import $ from 'jquery';
-import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
-import NewBranchForm from '~/new_branch_form';
-import initNewPipeline from '~/pipeline_new/index';
+import initNewPipelineForm from '~/pipeline_new/index';
-const el = document.getElementById('js-new-pipeline');
-
-if (el) {
- initNewPipeline();
-} else {
- new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
-
- setupNativeFormVariableList({
- container: $('.js-ci-variable-list-section'),
- formField: 'variables_attributes',
- });
-}
+initNewPipelineForm();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index be9259ec3ca..10105af3561 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -3,9 +3,9 @@ import SecretValues from '~/behaviors/secret_values';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
+import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
-import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
@@ -36,10 +36,6 @@ document.addEventListener('DOMContentLoaded', () => {
initSettingsPipelinesTriggers();
initArtifactsSettings();
-
- if (gon?.features?.vueifySharedRunnersToggle) {
- initSharedRunnersToggle();
- }
-
+ initSharedRunnersToggle();
initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js b/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
new file mode 100644
index 00000000000..93c6a2c63a3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
@@ -0,0 +1,5 @@
+import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
+import initSettingsPanels from '~/settings_panels';
+
+registrySettingsApp();
+initSettingsPanels();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index 8d390c8586b..380091a3501 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
+import initDatePicker from '~/behaviors/date_picker';
import initDeployKeys from '~/deploy_keys';
-import DueDateSelectors from '~/due_date_select';
import fileUpload from '~/lib/utils/file_upload';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
@@ -16,6 +16,6 @@ export default () => {
initSettingsPanels();
new ProtectedBranchCreate({ hasLicense: false });
new ProtectedBranchEditList();
- new DueDateSelectors();
+ initDatePicker(); // Used for deploy token "expires at" field
fileUpload('.js-choose-file', '.js-object-map-input');
};
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index f955a41e18a..c719601ee0b 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -1 +1,9 @@
import '~/snippet/snippet_show';
+
+const awardEmojiEl = document.getElementById('js-vue-awards-block');
+
+if (awardEmojiEl) {
+ import('~/emoji/awards_app')
+ .then((m) => m.default(awardEmojiEl))
+ .catch(() => {});
+}
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 6afc33ec8a5..43753926039 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -1,9 +1,21 @@
<script>
-import { GlForm, GlIcon, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
+import {
+ GlForm,
+ GlIcon,
+ GlLink,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ GlLoadingIcon,
+ GlModal,
+ GlModalDirective,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
-import { __, s__, sprintf } from '~/locale';
+import { s__, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const MARKDOWN_LINK_TEXT = {
markdown: '[Link Title](page-slug)',
@@ -13,21 +25,98 @@ const MARKDOWN_LINK_TEXT = {
};
export default {
+ i18n: {
+ title: {
+ label: s__('WikiPage|Title'),
+ placeholder: s__('WikiPage|Page title'),
+ helpText: {
+ existingPage: s__(
+ 'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
+ ),
+ newPage: s__(
+ 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
+ ),
+ moreInformation: s__('WikiPage|More Information.'),
+ },
+ },
+ format: {
+ label: s__('WikiPage|Format'),
+ },
+ content: {
+ label: s__('WikiPage|Content'),
+ placeholder: s__('WikiPage|Write your content or drag files here…'),
+ },
+ contentEditor: {
+ renderFailed: {
+ message: s__(
+ 'WikiPage|An error occured while trying to render the content editor. Please try again later.',
+ ),
+ primaryAction: s__('WikiPage|Retry'),
+ },
+ useNewEditor: s__('WikiPage|Use new editor'),
+ switchToOldEditor: {
+ label: s__('WikiPage|Switch to old editor'),
+ helpText: s__("WikiPage|Switching will discard any changes you've made in the new editor."),
+ modal: {
+ title: s__('WikiPage|Are you sure you want to switch to the old editor?'),
+ primary: s__('WikiPage|Switch to old editor'),
+ cancel: s__('WikiPage|Keep editing'),
+ text: s__(
+ "WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
+ ),
+ },
+ },
+ helpText: s__(
+ "WikiPage|This editor is in beta and may not display the page's contents properly.",
+ ),
+ },
+ linksHelpText: s__(
+ 'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
+ ),
+ commitMessage: {
+ label: s__('WikiPage|Commit message'),
+ value: {
+ existingPage: s__('WikiPage|Update %{pageTitle}'),
+ newPage: s__('WikiPage|Create %{pageTitle}'),
+ },
+ },
+ submitButton: {
+ existingPage: s__('WikiPage|Save changes'),
+ newPage: s__('WikiPage|Create page'),
+ },
+ cancel: s__('WikiPage|Cancel'),
+ },
components: {
+ GlAlert,
GlForm,
GlSprintf,
GlIcon,
GlLink,
GlButton,
+ GlModal,
MarkdownField,
+ GlLoadingIcon,
+ ContentEditor: () =>
+ import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
+ ),
+ },
+ directives: {
+ GlModalDirective,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
title: this.pageInfo.title?.trim() || '',
format: this.pageInfo.format || 'markdown',
content: this.pageInfo.content?.trim() || '',
+ isContentEditorLoading: true,
+ useContentEditor: false,
commitMessage: '',
+ contentEditor: null,
+ isDirty: false,
+ contentEditorRenderFailed: false,
};
},
computed: {
@@ -45,15 +134,21 @@ export default {
},
commitMessageI18n() {
return this.pageInfo.persisted
- ? s__('WikiPage|Update %{pageTitle}')
- : s__('WikiPage|Create %{pageTitle}');
+ ? this.$options.i18n.commitMessage.value.existingPage
+ : this.$options.i18n.commitMessage.value.newPage;
},
linkExample() {
return MARKDOWN_LINK_TEXT[this.format];
},
submitButtonText() {
- if (this.pageInfo.persisted) return __('Save changes');
- return s__('WikiPage|Create page');
+ return this.pageInfo.persisted
+ ? this.$options.i18n.submitButton.existingPage
+ : this.$options.i18n.submitButton.newPage;
+ },
+ titleHelpText() {
+ return this.pageInfo.persisted
+ ? this.$options.i18n.title.helpText.existingPage
+ : this.$options.i18n.title.helpText.newPage;
},
cancelFormPath() {
if (this.pageInfo.persisted) return this.pageInfo.path;
@@ -62,20 +157,53 @@ export default {
wikiSpecificMarkdownHelpPath() {
return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown');
},
+ isMarkdownFormat() {
+ return this.format === 'markdown';
+ },
+ showContentEditorButton() {
+ return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor;
+ },
+ disableSubmitButton() {
+ return !this.content || !this.title || this.contentEditorRenderFailed;
+ },
+ isContentEditorActive() {
+ return this.isMarkdownFormat && this.useContentEditor;
+ },
},
mounted() {
this.updateCommitMessage();
+
+ window.addEventListener('beforeunload', this.onPageUnload);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
+ getContentHTML(content) {
+ return axios
+ .post(this.pageInfo.markdownPreviewPath, { text: content })
+ .then(({ data }) => data.body);
+ },
+
handleFormSubmit() {
- window.removeEventListener('beforeunload', this.onBeforeUnload);
+ if (this.useContentEditor) {
+ this.content = this.contentEditor.getSerializedContent();
+ }
+
+ this.isDirty = false;
},
handleContentChange() {
- window.addEventListener('beforeunload', this.onBeforeUnload);
+ this.isDirty = true;
},
- onBeforeUnload() {
+ onPageUnload(event) {
+ if (!this.isDirty) return undefined;
+
+ event.preventDefault();
+
+ // eslint-disable-next-line no-param-reassign
+ event.returnValue = '';
return '';
},
@@ -88,6 +216,48 @@ export default {
const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: newTitle }, false);
this.commitMessage = newCommitMessage;
},
+
+ async initContentEditor() {
+ this.isContentEditorLoading = true;
+ this.useContentEditor = true;
+
+ const { createContentEditor } = await import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_content_editor'
+ );
+ this.contentEditor =
+ this.contentEditor ||
+ createContentEditor({
+ renderMarkdown: (markdown) => this.getContentHTML(markdown),
+ tiptapOptions: {
+ onUpdate: () => this.handleContentChange(),
+ },
+ });
+
+ try {
+ await this.contentEditor.setSerializedContent(this.content);
+ this.isContentEditorLoading = false;
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ retryInitContentEditor() {
+ this.contentEditorRenderFailed = false;
+ this.initContentEditor();
+ },
+
+ switchToOldEditor() {
+ this.useContentEditor = false;
+ },
+
+ confirmSwitchToOldEditor() {
+ if (this.contentEditorRenderFailed) {
+ this.contentEditorRenderFailed = false;
+ this.switchToOldEditor();
+ } else {
+ this.$refs.confirmSwitchToOldEditorModal.show();
+ }
+ },
},
};
</script>
@@ -99,6 +269,19 @@ export default {
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
>
+ <gl-alert
+ v-if="isContentEditorActive && contentEditorRenderFailed"
+ class="gl-mb-6"
+ :dismissible="false"
+ variant="danger"
+ :primary-button-text="$options.i18n.contentEditor.renderFailed.primaryAction"
+ @primaryAction="retryInitContentEditor()"
+ >
+ <p>
+ {{ $options.i18n.contentEditor.renderFailed.message }}
+ </p>
+ </gl-alert>
+
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
<input
@@ -109,7 +292,9 @@ export default {
/>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
- <label class="control-label-full-width" for="wiki_title">{{ s__('WikiPage|Title') }}</label>
+ <label class="control-label-full-width" for="wiki_title">{{
+ $options.i18n.title.label
+ }}</label>
</div>
<div class="col-sm-10">
<input
@@ -121,22 +306,15 @@ export default {
data-qa-selector="wiki_title_textbox"
:required="true"
:autofocus="!pageInfo.persisted"
- :placeholder="s__('WikiPage|Page title')"
+ :placeholder="$options.i18n.title.placeholder"
@input="updateCommitMessage"
/>
<span class="gl-display-inline-block gl-max-w-full gl-mt-2 gl-text-gray-600">
<gl-icon class="gl-mr-n1" name="bulb" />
- {{
- pageInfo.persisted
- ? s__(
- 'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
- )
- : s__(
- 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
- )
- }}
- <gl-link :href="helpPath" target="_blank" data-testid="wiki-title-help-link"
- ><gl-icon name="question-o" /> {{ __('More Information.') }}</gl-link
+ {{ titleHelpText }}
+ <gl-link :href="helpPath" target="_blank"
+ ><gl-icon name="question-o" />
+ {{ $options.i18n.title.helpText.moreInformation }}</gl-link
>
</span>
</div>
@@ -144,25 +322,63 @@ export default {
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_format">{{
- s__('WikiPage|Format')
+ $options.i18n.format.label
}}</label>
</div>
<div class="col-sm-10">
- <select id="wiki_format" v-model="format" class="form-control" name="wiki[format]">
+ <select
+ id="wiki_format"
+ v-model="format"
+ class="form-control"
+ name="wiki[format]"
+ :disabled="isContentEditorActive"
+ >
<option v-for="(key, label) of formatOptions" :key="key" :value="key">
{{ label }}
</option>
</select>
+ <div>
+ <gl-button
+ v-if="showContentEditorButton"
+ category="secondary"
+ variant="confirm"
+ class="gl-mt-4"
+ @click="initContentEditor"
+ >{{ $options.i18n.contentEditor.useNewEditor }}</gl-button
+ >
+ <div v-if="isContentEditorActive" class="gl-mt-4 gl-display-flex">
+ <div class="gl-mr-4">
+ <gl-button category="secondary" variant="confirm" @click="confirmSwitchToOldEditor">{{
+ $options.i18n.contentEditor.switchToOldEditor.label
+ }}</gl-button>
+ </div>
+ <div class="gl-mt-2">
+ <gl-icon name="warning" />
+ {{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
+ </div>
+ </div>
+ <gl-modal
+ ref="confirmSwitchToOldEditorModal"
+ modal-id="confirm-switch-to-old-editor"
+ :title="$options.i18n.contentEditor.switchToOldEditor.modal.title"
+ :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }"
+ :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }"
+ @primary="switchToOldEditor"
+ >
+ {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }}
+ </gl-modal>
+ </div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_content">{{
- s__('WikiPage|Content')
+ $options.i18n.content.label
}}</label>
</div>
<div class="col-sm-10">
<markdown-field
+ v-if="!isContentEditorActive"
:markdown-preview-path="pageInfo.markdownPreviewPath"
:can-attach-file="true"
:enable-autocomplete="true"
@@ -182,24 +398,25 @@ export default {
data-supports-quick-actions="false"
data-qa-selector="wiki_content_textarea"
:autofocus="pageInfo.persisted"
- :aria-label="s__('WikiPage|Content')"
- :placeholder="s__('WikiPage|Write your content or drag files here…')"
+ :aria-label="$options.i18n.content.label"
+ :placeholder="$options.i18n.content.placeholder"
@input="handleContentChange"
>
</textarea>
</template>
</markdown-field>
+
+ <div v-if="isContentEditorActive">
+ <gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" />
+ <content-editor v-else :content-editor="contentEditor" />
+ <input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
+ </div>
+
<div class="clearfix"></div>
<div class="error-alert"></div>
<div class="form-text gl-text-gray-600">
- <gl-sprintf
- :message="
- s__(
- 'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
- )
- "
- >
+ <gl-sprintf v-if="!isContentEditorActive" :message="$options.i18n.linksHelpText">
<template #linkExample
><code>{{ linkExample }}</code></template
>
@@ -214,13 +431,16 @@ export default {
></template
>
</gl-sprintf>
+ <span v-else>
+ {{ $options.i18n.contentEditor.helpText }}
+ </span>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_message">{{
- s__('WikiPage|Commit message')
+ $options.i18n.commitMessage.label
}}</label>
</div>
<div class="col-sm-10">
@@ -231,7 +451,7 @@ export default {
type="text"
class="form-control"
data-qa-selector="wiki_message_textbox"
- :placeholder="s__('WikiPage|Commit message')"
+ :placeholder="$options.i18n.commitMessage.label"
/>
</div>
</div>
@@ -242,12 +462,10 @@ export default {
type="submit"
data-qa-selector="wiki_submit_button"
data-testid="wiki-submit-button"
- :disabled="!content || !title"
+ :disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
- <gl-button :href="cancelFormPath" class="float-right" data-testid="wiki-cancel-button">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button :href="cancelFormPath" class="float-right">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index d236dc4610a..c416106fdd8 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -247,7 +247,7 @@ export default class ActivityCalendar {
renderKey() {
const keyValues = [
- __('no contributions'),
+ __('No contributions'),
__('1-9 contributions'),
__('10-19 contributions'),
__('20-29 contributions'),
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 80e14842f51..f9d70845560 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -223,14 +223,14 @@ export default class UserTabs {
.then((data) => UserTabs.renderActivityCalendar(data, $calendarWrap))
.catch(() => {
const cWrap = $calendarWrap[0];
- cWrap.querySelector('.spinner').classList.add('invisible');
+ cWrap.querySelector('.gl-spinner').classList.add('invisible');
cWrap.querySelector('.user-calendar-error').classList.remove('invisible');
cWrap
.querySelector('.user-calendar-error .js-retry-load')
.addEventListener('click', (e) => {
e.preventDefault();
cWrap.querySelector('.user-calendar-error').classList.add('invisible');
- cWrap.querySelector('.spinner').classList.remove('invisible');
+ cWrap.querySelector('.gl-spinner').classList.remove('invisible');
this.loadActivityCalendar();
});
});