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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/projects')
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue1
-rw-r--r--app/assets/javascripts/projects/commit/components/projects_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/commit/store/index.js1
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue8
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue1
-rw-r--r--app/assets/javascripts/projects/commits/index.js1
-rw-r--r--app/assets/javascripts/projects/commits/store/index.js1
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue148
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_modal.vue155
-rw-r--r--app/assets/javascripts/projects/feature_flags_user_lists/show/index.js1
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue4
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js5
-rw-r--r--app/assets/javascripts/projects/project_name_rules.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js2
-rw-r--r--app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue41
-rw-r--r--app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js8
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue70
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue50
-rw-r--r--app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue2
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue139
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_confirm_modal.vue74
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue291
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue245
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue18
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js146
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js5
27 files changed, 1235 insertions, 186 deletions
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index 77e809e88ce..c44d97c9bf8 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -1,5 +1,6 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { debounce, uniqBy } from 'lodash';
import {
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 28bbf67c090..44b8ccb57ca 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -1,5 +1,6 @@
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import api from '~/api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
diff --git a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
index fe54b62e2c8..e2b004e0892 100644
--- a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
@@ -1,5 +1,6 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
+// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import { debounce, uniqBy } from 'lodash';
import {
diff --git a/app/assets/javascripts/projects/commit/store/index.js b/app/assets/javascripts/projects/commit/store/index.js
index 83802f6a36f..450b40091dd 100644
--- a/app/assets/javascripts/projects/commit/store/index.js
+++ b/app/assets/javascripts/projects/commit/store/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 84e7edb48c1..6ff9bd7390f 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -7,7 +7,7 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue';
+import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
@@ -23,7 +23,7 @@ export default {
},
components: {
GlLoadingIcon,
- GraphqlPipelineMiniGraph,
+ LegacyPipelineMiniGraph,
PipelineMiniGraph,
},
mixins: [glFeatureFlagsMixin()],
@@ -139,14 +139,14 @@ export default {
<div>
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
<template v-else>
- <graphql-pipeline-mini-graph
+ <pipeline-mini-graph
v-if="isUsingPipelineMiniGraphQueries"
data-testid="commit-box-pipeline-mini-graph"
:pipeline-etag="graphqlResourceEtag"
:full-path="fullPath"
:iid="iid"
/>
- <pipeline-mini-graph
+ <legacy-pipeline-mini-graph
v-else
data-testid="commit-box-pipeline-mini-graph"
:downstream-pipelines="downstreamPipelines"
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index cf251bc7465..8bc7a27bcad 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -1,6 +1,7 @@
<script>
import { GlAvatar, GlCollapsibleListbox, GlTooltipDirective } from '@gitlab/ui';
import { debounce } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import { queryToObject, visitUrl } from '~/lib/utils/url_utility';
import { n__, __ } from '~/locale';
diff --git a/app/assets/javascripts/projects/commits/index.js b/app/assets/javascripts/projects/commits/index.js
index d37c1800718..ff7ad67c0a5 100644
--- a/app/assets/javascripts/projects/commits/index.js
+++ b/app/assets/javascripts/projects/commits/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
import RefSelector from '~/ref/components/ref_selector.vue';
diff --git a/app/assets/javascripts/projects/commits/store/index.js b/app/assets/javascripts/projects/commits/store/index.js
index e864ef5716e..4fb1bc093c7 100644
--- a/app/assets/javascripts/projects/commits/store/index.js
+++ b/app/assets/javascripts/projects/commits/store/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import actions from './actions';
import mutations from './mutations';
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index 06c0230c8e0..c749034d2a8 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -1,19 +1,14 @@
<script>
-import { GlModal, GlModalDirective, GlFormInput, GlButton, GlAlert, GlSprintf } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { GlButton, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
+import DeleteModal from './delete_modal.vue';
export default {
components: {
- GlAlert,
- GlModal,
- GlFormInput,
GlButton,
- GlSprintf,
- },
- directives: {
- GlModal: GlModalDirective,
+ GlForm,
+ DeleteModal,
},
props: {
confirmPhrase: {
@@ -47,139 +42,54 @@ export default {
},
data() {
return {
- userInput: null,
- modalId: uniqueId('delete-project-modal-'),
+ isModalVisible: false,
};
},
computed: {
- confirmDisabled() {
- return this.userInput !== this.confirmPhrase;
- },
csrfToken() {
return csrf.token;
},
- modalActionProps() {
- return {
- primary: {
- text: __('Yes, delete project'),
- attributes: {
- variant: 'danger',
- disabled: this.confirmDisabled,
- 'data-qa-selector': 'confirm_delete_button',
- },
- },
- cancel: {
- text: __('Cancel, keep project'),
- },
- };
- },
},
methods: {
submitForm() {
- this.$refs.form.submit();
+ this.$refs.form.$el.submit();
+ },
+ onButtonClick() {
+ this.isModalVisible = true;
},
},
- strings: {
+ i18n: {
deleteProject: __('Delete project'),
- title: __('Are you absolutely sure?'),
- confirmText: __('Enter the following to confirm:'),
- isForkAlertTitle: __('You are about to delete this forked project containing:'),
- isNotForkAlertTitle: __('You are about to delete this project containing:'),
- isForkAlertBody: __('This process deletes the project repository and all related resources.'),
- isNotForkAlertBody: __(
- 'This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources.',
- ),
- isNotForkMessage: __(
- 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
- ),
},
};
</script>
<template>
- <form ref="form" :action="formPath" method="post">
+ <gl-form ref="form" :action="formPath" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <delete-modal
+ v-model="isModalVisible"
+ :confirm-phrase="confirmPhrase"
+ :is-fork="isFork"
+ :issues-count="issuesCount"
+ :merge-requests-count="mergeRequestsCount"
+ :forks-count="forksCount"
+ :stars-count="starsCount"
+ @primary="submitForm"
+ >
+ <template #modal-footer>
+ <slot name="modal-footer"></slot>
+ </template>
+ </delete-modal>
+
<gl-button
- v-gl-modal="modalId"
category="primary"
variant="danger"
data-qa-selector="delete_button"
- >{{ $options.strings.deleteProject }}</gl-button
+ @click="onButtonClick"
+ >{{ $options.i18n.deleteProject }}</gl-button
>
-
- <gl-modal
- ref="removeModal"
- :modal-id="modalId"
- ok-variant="danger"
- footer-class="gl-bg-gray-10 gl-p-5"
- title-class="gl-text-red-500"
- :action-primary="modalActionProps.primary"
- :action-cancel="modalActionProps.cancel"
- @ok="submitForm"
- >
- <template #modal-title>{{ $options.strings.title }}</template>
- <div>
- <gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
- <h4 v-if="isFork" data-testid="delete-alert-title" class="gl-alert-title">
- {{ $options.strings.isForkAlertTitle }}
- </h4>
- <h4 v-else data-testid="delete-alert-title" class="gl-alert-title">
- {{ $options.strings.isNotForkAlertTitle }}
- </h4>
- <ul>
- <li>
- <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
- <template #issuesCount>{{ issuesCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf
- :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
- >
- <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
- <template #forksCount>{{ forksCount }}</template>
- </gl-sprintf>
- </li>
- <li>
- <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
- <template #starsCount>{{ starsCount }}</template>
- </gl-sprintf>
- </li>
- </ul>
- <gl-sprintf
- v-if="isFork"
- data-testid="delete-alert-body"
- :message="$options.strings.isForkAlertBody"
- />
- <gl-sprintf
- v-else
- data-testid="delete-alert-body"
- :message="$options.strings.isNotForkAlertBody"
- >
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </gl-alert>
- <p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
- <p>
- <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
- </p>
- <gl-form-input
- id="confirm_name_input"
- v-model="userInput"
- name="confirm_name_input"
- type="text"
- data-qa-selector="confirm_name_field"
- />
- <slot name="modal-footer"></slot>
- </div>
- </gl-modal>
- </form>
+ </gl-form>
</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_modal.vue b/app/assets/javascripts/projects/components/shared/delete_modal.vue
new file mode 100644
index 00000000000..44e29d00d45
--- /dev/null
+++ b/app/assets/javascripts/projects/components/shared/delete_modal.vue
@@ -0,0 +1,155 @@
+<script>
+import { GlModal, GlAlert, GlSprintf, GlFormInput } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ deleteProject: __('Delete project'),
+ title: __('Are you absolutely sure?'),
+ confirmText: __('Enter the following to confirm:'),
+ isForkAlertTitle: __('You are about to delete this forked project containing:'),
+ isNotForkAlertTitle: __('You are about to delete this project containing:'),
+ isForkAlertBody: __('This process deletes the project repository and all related resources.'),
+ isNotForkAlertBody: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork. This process deletes the project repository and all related resources.',
+ ),
+ isNotForkMessage: __(
+ 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
+ ),
+ },
+ components: { GlModal, GlAlert, GlSprintf, GlFormInput },
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ isFork: {
+ type: Boolean,
+ required: true,
+ },
+ issuesCount: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ mergeRequestsCount: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ forksCount: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ starsCount: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ userInput: null,
+ modalId: uniqueId('delete-project-modal-'),
+ };
+ },
+ computed: {
+ confirmDisabled() {
+ return this.userInput !== this.confirmPhrase;
+ },
+ modalActionProps() {
+ return {
+ primary: {
+ text: __('Yes, delete project'),
+ attributes: {
+ variant: 'danger',
+ disabled: this.confirmDisabled,
+ 'data-qa-selector': 'confirm_delete_button',
+ },
+ },
+ cancel: {
+ text: __('Cancel, keep project'),
+ },
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :visible="visible"
+ :modal-id="modalId"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ title-class="gl-text-red-500"
+ :action-primary="modalActionProps.primary"
+ :action-cancel="modalActionProps.cancel"
+ @primary="$emit('primary', $event)"
+ @change="$emit('change', $event)"
+ >
+ <template #modal-title>{{ $options.i18n.title }}</template>
+ <div>
+ <gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
+ <h4 v-if="isFork" class="gl-alert-title">
+ {{ $options.i18n.isForkAlertTitle }}
+ </h4>
+ <h4 v-else class="gl-alert-title">
+ {{ $options.i18n.isNotForkAlertTitle }}
+ </h4>
+ <ul>
+ <li v-if="issuesCount !== null">
+ <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)">
+ <template #issuesCount>{{ issuesCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="mergeRequestsCount !== null">
+ <gl-sprintf
+ :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)"
+ >
+ <template #mergeRequestsCount>{{ mergeRequestsCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="forksCount !== null">
+ <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)">
+ <template #forksCount>{{ forksCount }}</template>
+ </gl-sprintf>
+ </li>
+ <li v-if="starsCount !== null">
+ <gl-sprintf :message="n__('%d star', '%d stars', starsCount)">
+ <template #starsCount>{{ starsCount }}</template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ <gl-sprintf v-if="isFork" :message="$options.i18n.isForkAlertBody" />
+ <gl-sprintf v-else :message="$options.i18n.isNotForkAlertBody">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <p class="gl-mb-1">{{ $options.i18n.confirmText }}</p>
+ <p>
+ <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
+ </p>
+
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="userInput"
+ name="confirm_name_input"
+ type="text"
+ data-qa-selector="confirm_name_field"
+ />
+ <slot name="modal-footer"></slot>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js b/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js
index 2bd3e57322d..59210b31d32 100644
--- a/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js
+++ b/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import UserList from '~/user_lists/components/user_list.vue';
import createStore from '~/user_lists/store/show';
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index d6d88b5b297..ef2a2aa5526 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -157,7 +157,7 @@ export default {
<gl-dropdown
class="js-group-namespace-dropdown gl-flex-grow-1"
:toggle-class="`gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20 ${dropdownPlaceholderClass}`"
- data-qa-selector="select_namespace_dropdown"
+ data-testid="select-namespace-dropdown"
@show="trackDropdownShow"
@shown="handleDropdownShown"
>
@@ -173,7 +173,7 @@ export default {
ref="search"
v-model.trim="search"
:is-loading="$apollo.queries.currentUser.loading"
- data-qa-selector="select_namespace_dropdown_search_field"
+ data-testid="select-namespace-dropdown-search-field"
/>
<template v-if="!$apollo.queries.currentUser.loading">
<template v-if="hasGroupMatches">
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index 0cfea401be6..35c8046bfe7 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -2,6 +2,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import ProjectPipelinesCharts from './components/app.vue';
Vue.use(VueApollo);
@@ -12,6 +14,7 @@ const apolloProvider = new VueApollo({
const mountPipelineChartsApp = (el) => {
const {
+ projectId,
projectPath,
failedPipelinesLink,
coverageChartPath,
@@ -22,6 +25,7 @@ const mountPipelineChartsApp = (el) => {
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary);
+ const contextId = convertToGraphQLId(TYPENAME_PROJECT, projectId);
return new Vue({
el,
@@ -39,6 +43,7 @@ const mountPipelineChartsApp = (el) => {
defaultBranch,
testRunsEmptyStateImagePath,
projectQualitySummaryFeedbackImagePath,
+ contextId,
},
render: (createElement) => createElement(ProjectPipelinesCharts, {}),
});
diff --git a/app/assets/javascripts/projects/project_name_rules.js b/app/assets/javascripts/projects/project_name_rules.js
index 4f62aa29ce4..90f9290ffb8 100644
--- a/app/assets/javascripts/projects/project_name_rules.js
+++ b/app/assets/javascripts/projects/project_name_rules.js
@@ -8,7 +8,7 @@ export const START_RULE = {
export const CONTAINS_RULE = {
reg: /^[a-zA-Z0-9\p{Pd}\u{002B}\u{00A9}-\u{1f9ff}_. ]+$/u,
msg: __(
- 'Name can contain only lowercase or uppercase letters, digits, emojis, spaces, dots, underscores, dashes, or pluses.',
+ 'Name can contain only lowercase or uppercase letters, digits, emoji, spaces, dots, underscores, dashes, or pluses.',
),
};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 33320f59b0f..2b5e2dcb301 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -76,7 +76,7 @@ const namespaceError = () => document.querySelector('.js-group-namespace-error')
const validateGroupNamespaceDropdown = (e) => {
if (selectedNamespaceId() && !selectedNamespaceId().attributes.value) {
- document.querySelector('input[data-qa-selector="project_name"]').reportValidity();
+ document.querySelector('#project_name').reportValidity();
e.preventDefault();
dropdownButton().classList.add(invalidDropdownClass);
namespaceButton().classList.add(invalidDropdownClass);
diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
index b8e7e9e15db..a02a33992b5 100644
--- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
+++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlToggle } from '@gitlab/ui';
+import { GlAlert, GlLink, GlToggle, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import { CC_VALIDATION_REQUIRED_ERROR } from '../constants';
@@ -15,7 +15,9 @@ export default {
},
components: {
GlAlert,
+ GlLink,
GlToggle,
+ GlSprintf,
CcValidationRequiredAlert: () =>
import('ee_component/billings/components/cc_validation_required_alert.vue'),
},
@@ -36,6 +38,16 @@ export default {
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupSettingsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -57,6 +69,9 @@ export default {
!this.ccAlertDismissed
);
},
+ isGroupSettingsAvailable() {
+ return this.groupSettingsPath && this.groupName;
+ },
},
methods: {
creditCardValidated() {
@@ -103,16 +118,6 @@ export default {
{{ errorMessage }}
</gl-alert>
- <gl-alert
- v-if="isDisabledAndUnoverridable"
- data-testid="unoverridable-alert"
- variant="warning"
- :dismissible="false"
- class="gl-mb-5"
- >
- {{ s__('Runners|Shared runners are disabled in the group settings') }}
- </gl-alert>
-
<gl-toggle
ref="sharedRunnersToggle"
:disabled="isDisabledAndUnoverridable"
@@ -121,7 +126,19 @@ export default {
:value="isSharedRunnerEnabled"
data-testid="toggle-shared-runners"
@change="toggleSharedRunners"
- />
+ >
+ <template v-if="isDisabledAndUnoverridable" #help>
+ {{ s__('Runners|Shared runners are disabled in the group settings.') }}
+ <gl-sprintf
+ v-if="isGroupSettingsAvailable"
+ :message="s__('Runners|Go to %{groupLink} to enable them.')"
+ >
+ <template #groupLink>
+ <gl-link :href="groupSettingsPath">{{ groupName }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-toggle>
</section>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
index 54120b3525d..ace5fd5c6e4 100644
--- a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
+++ b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
@@ -9,10 +9,15 @@ export default (containerId = 'toggle-shared-runners-form') => {
}
const {
+ // required
isDisabledAndUnoverridable,
isEnabled,
updatePath,
isCreditCardValidationRequired,
+
+ // optional
+ groupName,
+ groupSettingsPath,
} = containerEl.dataset;
return new Vue({
@@ -24,6 +29,9 @@ export default (containerId = 'toggle-shared-runners-form') => {
isEnabled: parseBoolean(isEnabled),
isCreditCardValidationRequired: parseBoolean(isCreditCardValidationRequired),
updatePath,
+
+ groupName,
+ groupSettingsPath,
},
});
},
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index dcf5155644d..7753b850744 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlCard, GlIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { expandSection } from '~/settings_panels';
@@ -14,6 +14,8 @@ export default {
BranchRule,
GlButton,
GlModal,
+ GlCard,
+ GlIcon,
},
directives: {
GlModal: GlModalDirective,
@@ -55,29 +57,47 @@ export default {
</script>
<template>
- <div class="settings-content gl-mb-0">
- <branch-rule
- v-for="(rule, index) in branchRules"
- :key="`${rule.name}-${index}`"
- :name="rule.name"
- :is-default="rule.isDefault"
- :branch-protection="rule.branchProtection"
- :status-checks-total="rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0"
- :approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
- :matching-branches-count="rule.matchingBranchesCount"
- />
-
- <div v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</div>
-
- <gl-button
- v-gl-modal="$options.modalId"
- class="gl-mt-5"
- data-qa-selector="add_branch_rule_button"
- category="secondary"
- variant="info"
- >{{ $options.i18n.addBranchRule }}</gl-button
- >
-
+ <gl-card
+ class="gl-new-card gl-overflow-hidden"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-px-0"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper" data-testid="title">
+ <h3 class="gl-new-card-title">
+ {{ __('Branch Rules') }}
+ </h3>
+ <div class="gl-new-card-count">
+ <gl-icon name="branch" class="gl-mr-2" />
+ {{ branchRules.length }}
+ </div>
+ </div>
+ <gl-button
+ v-gl-modal="$options.modalId"
+ size="small"
+ class="gl-ml-3"
+ data-qa-selector="add_branch_rule_button"
+ >{{ $options.i18n.addBranchRule }}</gl-button
+ >
+ </template>
+ <ul class="content-list">
+ <branch-rule
+ v-for="(rule, index) in branchRules"
+ :key="`${rule.name}-${index}`"
+ :name="rule.name"
+ :is-default="rule.isDefault"
+ :branch-protection="rule.branchProtection"
+ :status-checks-total="
+ rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0
+ "
+ :approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
+ :matching-branches-count="rule.matchingBranchesCount"
+ class="gl-px-5! gl-py-4!"
+ />
+ <div v-if="!branchRules.length" class="gl-new-card-empty gl-px-5 gl-py-4" data-testid="empty">
+ {{ $options.i18n.emptyState }}
+ </div>
+ </ul>
<gl-modal
:ref="$options.modalId"
:modal-id="$options.modalId"
@@ -88,5 +108,5 @@ export default {
<p>{{ $options.i18n.branchRuleModalDescription }}</p>
<p>{{ $options.i18n.branchRuleModalContent }}</p>
</gl-modal>
- </div>
+ </gl-card>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index a5ff478a826..f45a5b12db6 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -6,7 +6,7 @@ import { getAccessLevels } from '../../../utils';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
- detailsButtonLabel: s__('BranchRules|Details'),
+ detailsButtonLabel: s__('BranchRules|View details'),
allowForcePush: s__('BranchRules|Allowed to force push'),
codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
statusChecks: s__('BranchRules|%{total} status %{subject}'),
@@ -153,28 +153,36 @@ export default {
</script>
<template>
- <div
- class="gl-border-b gl-pt-5 gl-pb-5 gl-display-flex gl-justify-content-space-between"
- data-qa-selector="branch_content"
- :data-qa-branch-name="name"
- >
- <div>
- <strong class="gl-font-monospace">{{ name }}</strong>
+ <li>
+ <div
+ class="gl-display-flex gl-justify-content-space-between"
+ data-qa-selector="branch_content"
+ :data-qa-branch-name="name"
+ >
+ <div>
+ <strong class="gl-font-monospace">{{ name }}</strong>
- <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
- $options.i18n.defaultLabel
- }}</gl-badge>
+ <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
+ $options.i18n.defaultLabel
+ }}</gl-badge>
- <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
- $options.i18n.protectedLabel
- }}</gl-badge>
+ <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
+ $options.i18n.protectedLabel
+ }}</gl-badge>
- <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
- <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
- </ul>
+ <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
+ <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
+ </ul>
+ </div>
+ <gl-button
+ class="gl-align-self-start"
+ category="tertiary"
+ size="small"
+ data-qa-selector="details_button"
+ :href="detailsPath"
+ >
+ {{ $options.i18n.detailsButtonLabel }}</gl-button
+ >
</div>
- <gl-button class="gl-align-self-start" data-qa-selector="details_button" :href="detailsPath">
- {{ $options.i18n.detailsButtonLabel }}</gl-button
- >
- </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
index 47477d39b8a..2f980e20c1e 100644
--- a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
+++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
@@ -60,7 +60,7 @@ export default {
return this.selectedTokens.length ? '' : this.$options.i18n.placeholder;
},
topicsHelpUrl() {
- return helpPagePath('user/admin_area/index.html', {
+ return helpPagePath('administration/index', {
anchor: 'administering-topics',
});
},
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
new file mode 100644
index 00000000000..f7a9949db4b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
@@ -0,0 +1,139 @@
+<script>
+import { GlBadge, GlButton, GlSprintf, GlToggle } from '@gitlab/ui';
+import {
+ I18N_STATE_INTRO_PARAGRAPH,
+ I18N_STATE_VERIFICATION_FINISHED_INTRO_PARAGRAPH,
+ I18N_STATE_VERIFICATION_STARTED,
+ I18N_RESET_BUTTON_LABEL,
+ I18N_STATE_VERIFICATION_FAILED,
+ I18N_STATE_VERIFICATION_FINISHED_TOGGLE_LABEL,
+ I18N_STATE_VERIFICATION_FINISHED_TOGGLE_HELP,
+ I18N_STATE_RESET_PARAGRAPH,
+ I18N_VERIFICATION_ERRORS,
+} from '../custom_email_constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlButton,
+ GlSprintf,
+ GlToggle,
+ },
+ I18N_STATE_VERIFICATION_STARTED,
+ I18N_STATE_VERIFICATION_FAILED,
+ I18N_STATE_VERIFICATION_FINISHED_TOGGLE_LABEL,
+ I18N_STATE_VERIFICATION_FINISHED_TOGGLE_HELP,
+ I18N_RESET_BUTTON_LABEL,
+ props: {
+ customEmail: {
+ type: String,
+ required: true,
+ },
+ smtpAddress: {
+ type: String,
+ required: true,
+ },
+ verificationState: {
+ type: String,
+ required: true,
+ },
+ verificationError: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ isVerificationFailed() {
+ return this.verificationState === 'failed';
+ },
+ isVerificationFinished() {
+ return this.verificationState === 'finished';
+ },
+ containerClass() {
+ return this.isVerificationFinished ? '' : 'gl-text-center';
+ },
+ introNote() {
+ return this.isVerificationFinished
+ ? I18N_STATE_VERIFICATION_FINISHED_INTRO_PARAGRAPH
+ : I18N_STATE_INTRO_PARAGRAPH;
+ },
+ badgeVariant() {
+ return this.isVerificationFailed ? 'danger' : 'info';
+ },
+ badgeContent() {
+ return this.isVerificationFailed
+ ? I18N_STATE_VERIFICATION_FAILED
+ : I18N_STATE_VERIFICATION_STARTED;
+ },
+ verificationErrorI18nObject() {
+ return I18N_VERIFICATION_ERRORS[this.verificationError];
+ },
+ errorLabel() {
+ return this.verificationErrorI18nObject?.label;
+ },
+ errorDescription() {
+ return this.verificationErrorI18nObject?.description;
+ },
+ resetNote() {
+ return I18N_STATE_RESET_PARAGRAPH[this.verificationState];
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="containerClass">
+ <p>
+ <gl-sprintf :message="introNote">
+ <template #customEmail>
+ <strong>{{ customEmail }}</strong>
+ </template>
+ <template #smtpAddress>
+ <strong>{{ smtpAddress }}</strong>
+ </template>
+ <template #badge="{ content }">
+ <gl-badge variant="success">{{ content }}</gl-badge>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div v-if="!isVerificationFinished" class="gl-mb-5">
+ <gl-badge :variant="badgeVariant">{{ badgeContent }}</gl-badge>
+ </div>
+
+ <template v-if="isVerificationFinished">
+ <gl-toggle
+ :value="isEnabled"
+ :is-loading="isSubmitting"
+ :label="$options.I18N_STATE_VERIFICATION_FINISHED_TOGGLE_LABEL"
+ :help="$options.I18N_STATE_VERIFICATION_FINISHED_TOGGLE_HELP"
+ label-position="top"
+ @change="$emit('toggle', $event)"
+ />
+ <hr />
+ </template>
+
+ <template v-if="verificationError">
+ <p class="gl-mb-0">
+ <strong>{{ errorLabel }}</strong>
+ </p>
+ <p>{{ errorDescription }}</p>
+ </template>
+
+ <p>{{ resetNote }}</p>
+ <gl-button :loading="isSubmitting" @click="$emit('reset')">
+ {{ $options.I18N_RESET_BUTTON_LABEL }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_confirm_modal.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_confirm_modal.vue
new file mode 100644
index 00000000000..2fb1ea52e05
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_confirm_modal.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ I18N_MODAL_TITLE,
+ I18N_MODAL_CANCEL_BUTTON_LABEL,
+ I18N_RESET_BUTTON_LABEL,
+ I18N_MODAL_DISABLE_CUSTOM_EMAIL_PARAGRAPH,
+ I18N_MODAL_SET_UP_AGAIN_PARAGRAPH,
+} from '../custom_email_constants';
+
+export default {
+ components: {
+ GlModal,
+ GlSprintf,
+ },
+ I18N_MODAL_TITLE,
+ I18N_RESET_BUTTON_LABEL,
+ I18N_MODAL_DISABLE_CUSTOM_EMAIL_PARAGRAPH,
+ I18N_MODAL_SET_UP_AGAIN_PARAGRAPH,
+ props: {
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ customEmail: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ primaryButtonAttributes() {
+ return {
+ text: I18N_RESET_BUTTON_LABEL,
+ attributes: {
+ variant: 'danger',
+ },
+ };
+ },
+ cancelButtonAttributes() {
+ return {
+ text: I18N_MODAL_CANCEL_BUTTON_LABEL,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ modal-id="custom-email-confirm-modal"
+ :title="$options.I18N_MODAL_TITLE"
+ :action-primary="primaryButtonAttributes"
+ :action-cancel="cancelButtonAttributes"
+ :visible="visible"
+ @primary="$emit('remove')"
+ @canceled="$emit('cancel')"
+ @hidden="$emit('cancel')"
+ >
+ <p>
+ <gl-sprintf :message="$options.I18N_MODAL_DISABLE_CUSTOM_EMAIL_PARAGRAPH">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #customEmail>
+ <code>{{ customEmail }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ {{ $options.I18N_MODAL_SET_UP_AGAIN_PARAGRAPH }}
+ </p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
new file mode 100644
index 00000000000..4affcd926d4
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -0,0 +1,291 @@
+<script>
+import { GlButton, GlForm, GlFormGroup, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
+import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ I18N_FORM_INTRODUCTION_PARAGRAPH,
+ I18N_FORM_CUSTOM_EMAIL_LABEL,
+ I18N_FORM_CUSTOM_EMAIL_DESCRIPTION,
+ I18N_FORM_FORWARDING_LABEL,
+ I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE,
+ I18N_FORM_SMTP_ADDRESS_LABEL,
+ I18N_FORM_SMTP_PORT_LABEL,
+ I18N_FORM_SMTP_PORT_DESCRIPTION,
+ I18N_FORM_SMTP_USERNAME_LABEL,
+ I18N_FORM_SMTP_PASSWORD_LABEL,
+ I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SUBMIT_LABEL,
+ I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_PORT,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_USERNAME,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_PASSWORD,
+} from '../custom_email_constants';
+
+export default {
+ components: {
+ ClipboardButton,
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ },
+ I18N_FORM_INTRODUCTION_PARAGRAPH,
+ I18N_FORM_CUSTOM_EMAIL_LABEL,
+ I18N_FORM_CUSTOM_EMAIL_DESCRIPTION,
+ I18N_FORM_FORWARDING_LABEL,
+ I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE,
+ I18N_FORM_SMTP_ADDRESS_LABEL,
+ I18N_FORM_SMTP_PORT_LABEL,
+ I18N_FORM_SMTP_PORT_DESCRIPTION,
+ I18N_FORM_SMTP_USERNAME_LABEL,
+ I18N_FORM_SMTP_PASSWORD_LABEL,
+ I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SUBMIT_LABEL,
+ I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_PORT,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_USERNAME,
+ I18N_FORM_INVALID_FEEDBACK_SMTP_PASSWORD,
+ props: {
+ incomingEmail: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ customEmail: '',
+ forwardingConfigured: false,
+ smtpAddress: '',
+ smtpPort: '587',
+ smtpUsername: '',
+ smtpPassword: '',
+ validationState: {
+ customEmail: null,
+ smtpAddress: null,
+ smtpPort: true,
+ smtpUsername: null,
+ smtpPassword: null,
+ },
+ };
+ },
+ computed: {
+ isFormValid() {
+ return Object.values(this.validationState).every(Boolean);
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.triggerVerification();
+
+ if (!this.isFormValid) {
+ return;
+ }
+
+ this.$emit('submit', this.getRequestFormData());
+ },
+ getRequestFormData() {
+ return {
+ custom_email: this.customEmail,
+ smtp_address: this.smtpAddress,
+ smtp_port: this.smtpPort,
+ smtp_username: this.smtpUsername,
+ smtp_password: this.smtpPassword,
+ };
+ },
+ onCustomEmailChange() {
+ this.validateCustomEmail();
+
+ if (this.validationState.customEmail && isEmptyValue(this.smtpUsername)) {
+ this.smtpUsername = this.customEmail;
+ this.validateSmtpUsername();
+ }
+ },
+ validateCustomEmail() {
+ this.validationState.customEmail = isEmail(this.customEmail);
+ },
+ validateSmtpAddress() {
+ this.validationState.smtpAddress = !isEmptyValue(this.smtpAddress);
+ },
+ validateSmtpPort() {
+ this.validationState.smtpPort = isIntegerGreaterThan(this.smtpPort, 0);
+ },
+ validateSmtpUsername() {
+ this.validationState.smtpUsername = !isEmptyValue(this.smtpUsername);
+ },
+ validateSmtpPassword() {
+ this.validationState.smtpPassword = hasMinimumLength(this.smtpPassword, 8);
+ },
+ triggerVerification() {
+ this.validateCustomEmail();
+ this.validateSmtpAddress();
+ this.validateSmtpPort();
+ this.validateSmtpUsername();
+ this.validateSmtpPassword();
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <p>{{ $options.I18N_FORM_INTRODUCTION_PARAGRAPH }}</p>
+ <gl-form class="js-quick-submit" @submit.prevent="onSubmit">
+ <gl-form-group
+ :label="$options.I18N_FORM_FORWARDING_LABEL"
+ label-for="custom-email-form-forwarding"
+ class="gl-mt-3"
+ >
+ <gl-form-input-group>
+ <gl-form-input
+ id="custom-email-form-forwarding"
+ ref="service-desk-incoming-email"
+ type="text"
+ data-testid="custom-email-form-forwarding"
+ :aria-label="$options.I18N_FORM_FORWARDING_LABEL"
+ :value="incomingEmail"
+ :disabled="true"
+ />
+ <template #append>
+ <clipboard-button
+ :title="$options.I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE"
+ :text="incomingEmail"
+ css-class="input-group-text"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
+ label-for="custom-email-form-custom-email"
+ data-testid="form-group-custom-email"
+ :invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
+ class="gl-mt-3"
+ :description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
+ >
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
+ <gl-form-input
+ id="custom-email-form-custom-email"
+ v-model.trim="customEmail"
+ data-testid="form-custom-email"
+ :aria-label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
+ placeholder="contact@example.com"
+ type="email"
+ :state="validationState.customEmail"
+ :required="true"
+ :disabled="isSubmitting"
+ @change="onCustomEmailChange"
+ />
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
+ label-for="custom-email-form-smtp-address"
+ data-testid="form-group-smtp-address"
+ :invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
+ class="gl-mt-3"
+ >
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
+ <gl-form-input
+ id="custom-email-form-smtp-address"
+ v-model.trim="smtpAddress"
+ data-testid="form-smtp-address"
+ :aria-label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
+ placeholder="smtp.example.com"
+ type="email"
+ :state="validationState.smtpAddress"
+ :required="true"
+ :disabled="isSubmitting"
+ @change="validateSmtpAddress"
+ />
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_PORT_LABEL"
+ label-for="custom-email-form-smtp-port"
+ :invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_PORT"
+ class="gl-mt-3"
+ :description="$options.I18N_FORM_SMTP_PORT_DESCRIPTION"
+ >
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
+ <gl-form-input
+ id="custom-email-form-smtp-port"
+ v-model.trim="smtpPort"
+ data-testid="form-smtp-port"
+ :aria-label="$options.I18N_FORM_SMTP_PORT_LABEL"
+ placeholder="587"
+ type="number"
+ :state="validationState.smtpPort"
+ :required="true"
+ :disabled="isSubmitting"
+ @change="validateSmtpPort"
+ />
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_USERNAME_LABEL"
+ label-for="custom-email-form-smtp-username"
+ :invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_USERNAME"
+ class="gl-mt-3"
+ >
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
+ <gl-form-input
+ id="custom-email-form-smtp-username"
+ v-model.trim="smtpUsername"
+ data-testid="form-smtp-username"
+ :aria-label="$options.I18N_FORM_SMTP_USERNAME_LABEL"
+ placeholder="contact@example.com"
+ :state="validationState.smtpUsername"
+ :required="true"
+ :disabled="isSubmitting"
+ @change="validateSmtpUsername"
+ />
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_PASSWORD_LABEL"
+ label-for="custom-email-form-smtp-password"
+ :invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_PASSWORD"
+ class="gl-mt-3"
+ :description="$options.I18N_FORM_SMTP_PASSWORD_DESCRIPTION"
+ >
+ <gl-form-input
+ id="custom-email-form-smtp-password"
+ v-model.trim="smtpPassword"
+ data-testid="form-smtp-password"
+ :aria-label="$options.I18N_FORM_SMTP_PASSWORD_LABEL"
+ type="password"
+ :state="validationState.smtpPassword"
+ :required="true"
+ :disabled="isSubmitting"
+ @change="validateSmtpPassword"
+ />
+ </gl-form-group>
+
+ <gl-button
+ type="submit"
+ variant="confirm"
+ class="gl-mt-5"
+ data-testid="form-submit"
+ :disabled="!isFormValid"
+ :loading="isSubmitting"
+ @click="onSubmit"
+ >
+ {{ $options.I18N_FORM_SUBMIT_LABEL }}
+ </gl-button>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
new file mode 100644
index 00000000000..7e040e6001a
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -0,0 +1,245 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlSprintf, GlLink, GlCard } from '@gitlab/ui';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
+import axios from '~/lib/utils/axios_utils';
+import {
+ FEEDBACK_ISSUE_URL,
+ I18N_LOADING_LABEL,
+ I18N_CARD_TITLE,
+ I18N_GENERIC_ERROR,
+ I18N_FEEDBACK_PARAGRAPH,
+ I18N_TOAST_SAVED,
+ I18N_TOAST_DELETED,
+ I18N_TOAST_ENABLED,
+ I18N_TOAST_DISABLED,
+} from '../custom_email_constants';
+import CustomEmailConfirmModal from './custom_email_confirm_modal.vue';
+import CustomEmailForm from './custom_email_form.vue';
+import CustomEmail from './custom_email.vue';
+
+export default {
+ components: {
+ BetaBadge,
+ GlAlert,
+ GlLoadingIcon,
+ GlSprintf,
+ GlLink,
+ GlCard,
+ CustomEmailConfirmModal,
+ CustomEmailForm,
+ CustomEmail,
+ },
+ FEEDBACK_ISSUE_URL,
+ I18N_LOADING_LABEL,
+ I18N_CARD_TITLE,
+ I18N_FEEDBACK_PARAGRAPH,
+ I18N_TOAST_SAVED,
+ I18N_TOAST_DELETED,
+ props: {
+ incomingEmail: {
+ type: String,
+ required: true,
+ },
+ customEmailEndpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: true,
+ isSubmitting: false,
+ confirmModalVisible: false,
+ customEmail: null,
+ isEnabled: false,
+ verificationState: null,
+ verificationError: null,
+ smtpAddress: null,
+ alertMessage: null,
+ };
+ },
+ computed: {
+ customEmailNotSetUp() {
+ return !this.isEnabled && this.verificationState === null && this.customEmail === null;
+ },
+ toastToggleText() {
+ return this.isEnabled ? I18N_TOAST_ENABLED : I18N_TOAST_DISABLED;
+ },
+ },
+ mounted() {
+ this.getCustomEmailDetails();
+ },
+ methods: {
+ dismissAlert() {
+ this.alertMessage = null;
+ },
+ getCustomEmailDetails() {
+ axios
+ .get(this.customEmailEndpoint)
+ .then(({ data }) => {
+ this.updateData(data);
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.isLoading = false;
+ this.enqueueReFetchVerification();
+ });
+ },
+ enqueueReFetchVerification() {
+ setTimeout(this.reFetchVerification, 8000);
+ },
+ reFetchVerification() {
+ if (this.verificationState !== 'started') {
+ return;
+ }
+ this.getCustomEmailDetails();
+ },
+ handleRequestError() {
+ this.alertMessage = I18N_GENERIC_ERROR;
+ },
+ updateData(data) {
+ this.customEmail = data.custom_email;
+ this.isEnabled = data.custom_email_enabled;
+ this.verificationState = data.custom_email_verification_state;
+ this.verificationError = data.custom_email_verification_error;
+ this.smtpAddress = data.custom_email_smtp_address;
+ },
+ onSaveCustomEmail(requestData) {
+ this.alertMessage = null;
+ this.isSubmitting = true;
+
+ axios
+ .post(this.customEmailEndpoint, requestData)
+ .then(({ data }) => {
+ this.updateData(data);
+ this.$toast.show(this.$options.I18N_TOAST_SAVED);
+ this.enqueueReFetchVerification();
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ },
+ onResetCustomEmail() {
+ this.confirmModalVisible = true;
+ },
+ onConfirmModalCanceled() {
+ this.confirmModalVisible = false;
+ },
+ onConfirmModalProceed() {
+ this.isSubmitting = true;
+ this.confirmModalVisible = false;
+
+ this.deleteCustomEmail();
+ },
+ deleteCustomEmail() {
+ axios
+ .delete(this.customEmailEndpoint)
+ .then(({ data }) => {
+ this.updateData(data);
+ this.$toast.show(I18N_TOAST_DELETED);
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ },
+ onToggleCustomEmail(isChecked) {
+ this.isEnabled = isChecked;
+ this.isSubmitting = true;
+
+ const body = {
+ custom_email_enabled: this.isEnabled,
+ };
+
+ axios
+ .put(this.customEmailEndpoint, body)
+ .then(({ data }) => {
+ this.updateData(data);
+ this.$toast.show(this.toastToggleText);
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row gl-mt-7">
+ <div class="col-md-9">
+ <gl-card>
+ <template #header>
+ <div class="gl-display-flex align-items-center justify-content-between">
+ <h5 class="gl-my-0">{{ $options.I18N_CARD_TITLE }}</h5>
+ <beta-badge />
+ </div>
+ </template>
+
+ <template #default>
+ <div v-if="isLoading" class="gl-p-3 gl-text-center">
+ <gl-loading-icon
+ :label="$options.I18N_LOADING_LABEL"
+ size="md"
+ color="dark"
+ variant="spinner"
+ />
+ {{ $options.I18N_LOADING_LABEL }}
+ </div>
+
+ <custom-email-confirm-modal
+ :visible="confirmModalVisible"
+ :custom-email="customEmail"
+ @remove="onConfirmModalProceed"
+ @cancel="onConfirmModalCanceled"
+ />
+
+ <gl-alert
+ v-if="alertMessage"
+ variant="warning"
+ class="gl-mt-n5 gl-mb-4 gl-mx-n5"
+ @dismiss="dismissAlert"
+ >
+ {{ alertMessage }}
+ </gl-alert>
+
+ <!-- Use v-show to preserve form data after verification failure
+ without the need to maintain a state in this component. -->
+ <custom-email-form
+ v-show="customEmailNotSetUp && !isLoading"
+ :incoming-email="incomingEmail"
+ :is-submitting="isSubmitting"
+ @submit="onSaveCustomEmail"
+ />
+
+ <custom-email
+ v-if="customEmail"
+ :custom-email="customEmail"
+ :smtp-address="smtpAddress"
+ :verification-state="verificationState"
+ :verification-error="verificationError"
+ :is-enabled="isEnabled"
+ :is-submitting="isSubmitting"
+ @toggle="onToggleCustomEmail"
+ @reset="onResetCustomEmail"
+ />
+ </template>
+
+ <template #footer>
+ <gl-sprintf :message="$options.I18N_FEEDBACK_PARAGRAPH">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.FEEDBACK_ISSUE_URL"
+ target="_blank"
+ class="gl-text-blue-600 font-size-inherit"
+ >{{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-card>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index ae28694f5d2..2b2722ab329 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -4,8 +4,11 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ServiceDeskSetting from './service_desk_setting.vue';
+const CustomEmailWrapper = () => import('./custom_email_wrapper.vue');
+
export default {
serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk.html', {
anchor: 'use-an-additional-service-desk-alias-email',
@@ -15,10 +18,12 @@ export default {
GlSprintf,
GlLink,
ServiceDeskSetting,
+ CustomEmailWrapper,
},
directives: {
SafeHtml,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
initialIsEnabled: {
default: false,
@@ -56,6 +61,9 @@ export default {
publicProject: {
default: false,
},
+ customEmailEndpoint: {
+ default: '',
+ },
},
data() {
return {
@@ -68,6 +76,11 @@ export default {
updatedServiceDeskEmail: this.serviceDeskEmail,
};
},
+ computed: {
+ showCustomEmailWrapper() {
+ return this.glFeatures.serviceDeskCustomEmail && this.isEnabled && this.isIssueTrackerEnabled;
+ },
+ },
methods: {
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
@@ -179,5 +192,10 @@ export default {
@save="onSaveTemplate"
@toggle="onEnableToggled"
/>
+ <custom-email-wrapper
+ v-if="showCustomEmailWrapper"
+ :incoming-email="incomingEmail"
+ :custom-email-endpoint="customEmailEndpoint"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
new file mode 100644
index 00000000000..cdf2e53982e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -0,0 +1,146 @@
+import { s__, __ } from '~/locale';
+
+export const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/416637';
+
+export const I18N_LOADING_LABEL = __('Loading');
+export const I18N_CARD_TITLE = s__('ServiceDesk|Configure a custom email address');
+export const I18N_FEEDBACK_PARAGRAPH = s__(
+ 'ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}',
+);
+export const I18N_GENERIC_ERROR = __('An error occurred. Please try again.');
+
+export const I18N_TOAST_SAVED = s__(
+ 'ServiceDesk|Saved custom email address and started verification.',
+);
+export const I18N_TOAST_DELETED = s__('ServiceDesk|Reset custom email address.');
+export const I18N_TOAST_ENABLED = s__('ServiceDesk|Custom email enabled.');
+export const I18N_TOAST_DISABLED = s__('ServiceDesk|Custom email disabled.');
+
+export const I18N_FORM_INTRODUCTION_PARAGRAPH = s__(
+ 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials.',
+);
+export const I18N_FORM_FORWARDING_LABEL = s__(
+ 'ServiceDesk|Service Desk email address to forward emails to',
+);
+export const I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE = s__(
+ 'ServiceDesk|Copy Service Desk email address',
+);
+export const I18N_FORM_CUSTOM_EMAIL_LABEL = s__('ServiceDesk|Custom email address');
+export const I18N_FORM_CUSTOM_EMAIL_DESCRIPTION = s__(
+ 'ServiceDesk|Email address your customers can use to send support requests. It must support sub-addressing.',
+);
+export const I18N_FORM_SMTP_ADDRESS_LABEL = s__('ServiceDesk|SMTP host');
+export const I18N_FORM_SMTP_PORT_LABEL = s__('ServiceDesk|SMTP port');
+export const I18N_FORM_SMTP_PORT_DESCRIPTION = s__(
+ 'ServiceDesk|Common ports are 587 when using TLS, and 25 when not.',
+);
+export const I18N_FORM_SMTP_USERNAME_LABEL = s__('ServiceDesk|SMTP username');
+export const I18N_FORM_SMTP_PASSWORD_LABEL = s__('ServiceDesk|SMTP password');
+export const I18N_FORM_SMTP_PASSWORD_DESCRIPTION = s__('ServiceDesk|Minimum 8 characters long.');
+export const I18N_FORM_SUBMIT_LABEL = s__('ServiceDesk|Save and test connection');
+
+export const I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL = s__(
+ 'ServiceDesk|Custom email is required and must be a valid email address.',
+);
+export const I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS = s__(
+ 'ServiceDesk|SMTP address is required and must be resolvable.',
+);
+export const I18N_FORM_INVALID_FEEDBACK_SMTP_PORT = s__(
+ 'ServiceDesk|SMTP port is required and must be a port number larger than 0.',
+);
+export const I18N_FORM_INVALID_FEEDBACK_SMTP_USERNAME = s__(
+ 'ServiceDesk|SMTP username is required.',
+);
+export const I18N_FORM_INVALID_FEEDBACK_SMTP_PASSWORD = s__(
+ 'ServiceDesk|SMTP password is required and must be at least 8 characters long.',
+);
+
+export const I18N_MODAL_TITLE = s__(
+ 'ServiceDesk|Reset custom email address and delete credentials',
+);
+export const I18N_MODAL_CANCEL_BUTTON_LABEL = s__('ServiceDesk|Keep custom email');
+export const I18N_MODAL_DISABLE_CUSTOM_EMAIL_PARAGRAPH = s__(
+ 'ServiceDesk|You are about to %{strongStart}disable the custom email address%{strongEnd} %{customEmail} %{strongStart}and delete its credentials%{strongEnd}.',
+);
+export const I18N_MODAL_SET_UP_AGAIN_PARAGRAPH = s__(
+ "ServiceDesk|To use a custom email address for this Service Desk, you'll need to configure and verify an email address again.",
+);
+
+export const I18N_STATE_INTRO_PARAGRAPH = s__(
+ 'ServiceDesk|Verify %{customEmail} with SMTP host %{smtpAddress}:',
+);
+export const I18N_STATE_VERIFICATION_STARTED = s__('ServiceDesk|Verification started');
+export const I18N_STATE_VERIFICATION_STARTED_RESET_PARAGRAPH = s__(
+ 'ServiceDesk|A verification email has been sent to a sub-address of your custom email address. This can take up to 30 minutes. The screen refreshes automatically.',
+);
+export const I18N_RESET_BUTTON_LABEL = s__('ServiceDesk|Reset custom email');
+
+export const I18N_STATE_VERIFICATION_FINISHED_INTRO_PARAGRAPH = s__(
+ 'ServiceDesk|%{customEmail} with SMTP host %{smtpAddress} is %{badgeStart}verified%{badgeEnd}',
+);
+export const I18N_STATE_VERIFICATION_FINISHED_TOGGLE_LABEL = s__(
+ 'ServiceDesk|Enable custom email address',
+);
+export const I18N_STATE_VERIFICATION_FINISHED_TOGGLE_HELP = s__(
+ 'ServiceDesk|When enabled, Service Desk emails will be sent using the provided credentials.',
+);
+export const I18N_STATE_VERIFICATION_FINISHED_RESET_PARAGRAPH = s__(
+ 'ServiceDesk|Or reset and connect a new custom email address to this Service Desk.',
+);
+
+export const I18N_STATE_VERIFICATION_FAILED = s__('ServiceDesk|Verification failed');
+export const I18N_STATE_VERIFICATION_FAILED_RESET_PARAGRAPH = s__(
+ 'ServiceDesk|Please try again. Check email forwarding settings and credentials, and then restart verification.',
+);
+
+export const I18N_STATE_RESET_PARAGRAPH = {
+ started: I18N_STATE_VERIFICATION_STARTED_RESET_PARAGRAPH,
+ failed: I18N_STATE_VERIFICATION_FAILED_RESET_PARAGRAPH,
+ finished: I18N_STATE_VERIFICATION_FINISHED_RESET_PARAGRAPH,
+};
+
+export const I18N_ERROR_SMTP_HOST_ISSUE_LABEL = s__('ServiceDesk|SMTP host issue');
+export const I18N_ERROR_SMTP_HOST_ISSUE_DESC = s__(
+ 'ServiceDesk|A connection to the specified host could not be made or an SSL issue occurred.',
+);
+export const I18N_ERROR_INVALID_CREDENTIALS_LABEL = s__('ServiceDesk|Invalid credentials');
+export const I18N_ERROR_INVALID_CREDENTIALS_DESC = s__(
+ 'ServiceDesk|The given credentials (username and password) were rejected by the SMTP server.',
+);
+export const I18N_ERROR_MAIL_NOT_RECEIVED_IN_TIMEFRAME_LABEL = s__(
+ 'ServiceDesk|Verification email not received within timeframe',
+);
+export const I18N_ERROR_MAIL_NOT_RECEIVED_IN_TIMEFRAME_DESC = s__(
+ "ServiceDesk|The verification email wasn't received in time. There is a 30 minutes timeframe for verification emails to appear in your instance's Service Desk. Make sure that you have set up email forwarding correctly.",
+);
+export const I18N_ERROR_INCORRECT_FROM_LABEL = s__('ServiceDesk|Incorrect From header');
+export const I18N_ERROR_INCORRECT_FROM_DESC = s__(
+ 'ServiceDesk|Check your forwarding settings and make sure the original email sender remains in the From header.',
+);
+export const I18N_ERROR_INCORRECT_TOKEN_LABEL = s__('ServiceDesk|Incorrect verification token');
+export const I18N_ERROR_INCORRECT_TOKEN_DESC = s__(
+ "ServiceDesk|The received email didn't contain the verification token that was sent to your email address.",
+);
+
+export const I18N_VERIFICATION_ERRORS = {
+ smtp_host_issue: {
+ label: I18N_ERROR_SMTP_HOST_ISSUE_LABEL,
+ description: I18N_ERROR_SMTP_HOST_ISSUE_DESC,
+ },
+ invalid_credentials: {
+ label: I18N_ERROR_INVALID_CREDENTIALS_LABEL,
+ description: I18N_ERROR_INVALID_CREDENTIALS_DESC,
+ },
+ mail_not_received_within_timeframe: {
+ label: I18N_ERROR_MAIL_NOT_RECEIVED_IN_TIMEFRAME_LABEL,
+ description: I18N_ERROR_MAIL_NOT_RECEIVED_IN_TIMEFRAME_DESC,
+ },
+ incorrect_from: {
+ label: I18N_ERROR_INCORRECT_FROM_LABEL,
+ description: I18N_ERROR_INCORRECT_FROM_DESC,
+ },
+ incorrect_token: {
+ label: I18N_ERROR_INCORRECT_TOKEN_LABEL,
+ description: I18N_ERROR_INCORRECT_TOKEN_DESC,
+ },
+};
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index 0f4c747a7b6..c4d4f42576f 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -1,7 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskRoot from './components/service_desk_root.vue';
+Vue.use(GlToast);
+
export default () => {
const el = document.querySelector('.js-service-desk-setting-root');
@@ -22,6 +25,7 @@ export default () => {
selectedFileTemplateProjectId,
templates,
publicProject,
+ customEmailEndpoint,
} = el.dataset;
return new Vue({
@@ -39,6 +43,7 @@ export default () => {
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
publicProject: parseBoolean(publicProject),
+ customEmailEndpoint,
},
render: (createElement) => createElement(ServiceDeskRoot),
});