diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/pages | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
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(); }); }); |