diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-16 12:10:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-16 12:10:03 +0300 |
commit | 06672560caf7701c357eb468ca17cce817b57239 (patch) | |
tree | b11d305f9242a0ef1586d49fa7c875e8fb8fbca0 /app/assets/javascripts/projects | |
parent | 0754d9a52440799546ead06a820a0d58f4a8e85b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/projects')
11 files changed, 471 insertions, 330 deletions
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue index bf87db835ad..b40b28adab9 100644 --- a/app/assets/javascripts/projects/compare/components/app.vue +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -71,8 +71,8 @@ export default { type: Object, required: true, }, - targetProjectsPath: { - type: String, + projects: { + type: Array, required: true, }, straight: { @@ -83,6 +83,7 @@ export default { data() { return { from: { + projects: this.projects, selectedProject: this.targetProject, revision: this.paramsFrom, refsProjectPath: this.targetProjectRefsPath, @@ -100,7 +101,7 @@ export default { this.$refs.form.submit(); }, onSelectProject({ direction, project }) { - const refsPath = joinPaths(gon.relative_url_root || '', `/${project.text}`, '/refs'); + const refsPath = joinPaths(gon.relative_url_root || '', `/${project.name}`, '/refs'); // direction is either 'from' or 'to' this[direction].refsProjectPath = refsPath; this[direction].selectedProject = project; @@ -148,8 +149,8 @@ export default { :refs-project-path="to.refsProjectPath" :revision-text="$options.i18n.source" params-name="to" - :endpoint="targetProjectsPath" :params-branch="to.revision" + :projects="to.projects" :selected-project="to.selectedProject" @selectProject="onSelectProject" @selectRevision="onSelectRevision" @@ -178,7 +179,6 @@ export default { :params-branch="from.revision" :projects="from.projects" :selected-project="from.selectedProject" - :endpoint="targetProjectsPath" @selectProject="onSelectProject" @selectRevision="onSelectRevision" /> diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue index 84cb5d1bcb4..4c0b5d0b1f6 100644 --- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue +++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue @@ -1,9 +1,5 @@ <script> import { GlCollapsibleListbox } from '@gitlab/ui'; -import { debounce } from 'lodash'; -import axios from '~/lib/utils/axios_utils'; -import { createAlert } from '~/alert'; -import { __ } from '~/locale'; export default { components: { @@ -14,10 +10,10 @@ export default { type: String, required: true, }, - endpoint: { - type: String, + projects: { + type: Array, required: false, - default: '', + default: null, }, selectedProject: { type: Object, @@ -26,57 +22,34 @@ export default { }, data() { return { - isLoading: false, + searchTerm: '', selectedProjectId: this.selectedProject.id, - projects: [], - searchStr: '', }; }, computed: { - isDropdownDisabled() { - return this.paramsName === 'to'; + disableRepoDropdown() { + return this.projects === null; + }, + filteredRepos() { + if (this.disableRepoDropdown) return []; + + const lowerCaseSearchTerm = this.searchTerm.toLowerCase(); + return this.projects + .filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm)) + .map((project) => ({ text: project.name, value: project.id })); }, inputName() { return `${this.paramsName}_project_id`; }, }, - created() { - if (!this.isDropdownDisabled) { - this.fetchProjects(); - } - this.debouncedProjectsSearch = debounce(this.fetchProjects, 500); - }, methods: { emitTargetProject(projectId) { - if (this.isDropdownDisabled) return; - const project = this.projects.find(({ value }) => value === projectId); + if (this.disableRepoDropdown) return; + const project = this.projects.find(({ id }) => id === projectId); this.$emit('selectProject', { direction: this.paramsName, project }); }, - async fetchProjects() { - if (!this.endpoint) return; - - this.isLoading = true; - - try { - const { data } = await axios.get(this.endpoint, { - params: { search: this.searchStr }, - }); - - this.projects = data.map((p) => ({ - value: `${p.id}`, - text: p.full_path.replace(/^\//, ''), - })); - } catch { - createAlert({ - message: __('Error fetching data. Please try again.'), - primaryButton: { text: __('Try again'), clickHandler: () => this.fetchProjects() }, - }); - } - this.isLoading = false; - }, - searchProjects(search) { - this.searchStr = search; - this.debouncedProjectsSearch(); + onSearch(searchTerm) { + this.searchTerm = searchTerm; }, }, }; @@ -87,17 +60,16 @@ export default { <input type="hidden" :name="inputName" :value="selectedProjectId" /> <gl-collapsible-listbox v-model="selectedProjectId" - :toggle-text="selectedProject.text" - :loading="isLoading" + :toggle-text="selectedProject.name" :header-text="s__(`CompareRevisions|Select target project`)" class="gl-font-monospace" toggle-class="gl-min-w-0" - :disabled="isDropdownDisabled" - :items="projects" + :disabled="disableRepoDropdown" + :items="filteredRepos" block searchable @select="emitTargetProject" - @search="searchProjects" + @search="onSearch" /> </div> </template> diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue index b6238f80b2c..212937c87c6 100644 --- a/app/assets/javascripts/projects/compare/components/revision_card.vue +++ b/app/assets/javascripts/projects/compare/components/revision_card.vue @@ -25,8 +25,8 @@ export default { required: false, default: null, }, - endpoint: { - type: String, + projects: { + type: Array, required: false, default: null, }, @@ -47,7 +47,7 @@ export default { <repo-dropdown class="gl-sm-w-half" :params-name="paramsName" - :endpoint="endpoint" + :projects="projects" :selected-project="selectedProject" v-on="$listeners" /> diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue index 64909a8c93b..8af1667e26b 100644 --- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue @@ -146,7 +146,7 @@ export default { </gl-dropdown-section-header> <gl-dropdown-item v-for="branch in branches" - :key="`${branch}-branch`" + :key="branch" is-check-item :is-checked="selectedRevision === branch" data-testid="branches-dropdown-item" @@ -159,7 +159,7 @@ export default { </gl-dropdown-section-header> <gl-dropdown-item v-for="tag in tags" - :key="`${tag}-tag`" + :key="tag" is-check-item :is-checked="selectedRevision === tag" data-testid="tags-dropdown-item" diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js index 4647ae0ef9f..284cee6d7f1 100644 --- a/app/assets/javascripts/projects/compare/index.js +++ b/app/assets/javascripts/projects/compare/index.js @@ -16,7 +16,7 @@ export default function init() { createMrPath, sourceProject, targetProject, - targetProjectsPath, + projectsFrom, } = el.dataset; return new Vue({ @@ -35,9 +35,9 @@ export default function init() { projectCompareIndexPath, projectMergeRequestPath, createMrPath, - targetProjectsPath, sourceProject: JSON.parse(sourceProject), targetProject: JSON.parse(targetProject), + projects: JSON.parse(projectsFrom), }, }); }, 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 index 79c809fe13b..f7a9949db4b 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue @@ -1,227 +1,139 @@ <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 { GlBadge, GlButton, GlSprintf, GlToggle } from '@gitlab/ui'; import { - FEEDBACK_ISSUE_URL, - I18N_LOADING_LABEL, - I18N_CARD_TITLE, - I18N_GENERIC_ERROR, - I18N_FEEDBACK_PARAGRAPH, - I18N_TOAST_SAVED, - I18N_TOAST_DELETED, + 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'; -import CustomEmailConfirmModal from './custom_email_confirm_modal.vue'; -import CustomEmailForm from './custom_email_form.vue'; -import CustomEmailStateStarted from './custom_email_state_started.vue'; export default { components: { - BetaBadge, - GlAlert, - GlLoadingIcon, + GlBadge, + GlButton, GlSprintf, - GlLink, - GlCard, - CustomEmailConfirmModal, - CustomEmailForm, - CustomEmailStateStarted, + GlToggle, }, - FEEDBACK_ISSUE_URL, - I18N_LOADING_LABEL, - I18N_CARD_TITLE, - I18N_FEEDBACK_PARAGRAPH, - I18N_TOAST_SAVED, - I18N_TOAST_DELETED, + 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: { - incomingEmail: { + customEmail: { + type: String, + required: true, + }, + smtpAddress: { type: String, required: true, - default: '', }, - customEmailEndpoint: { + verificationState: { type: String, required: true, + }, + verificationError: { + type: String, + required: false, default: '', }, - }, - data() { - return { - loading: true, - submitting: false, - confirmModalVisible: false, - customEmail: null, - enabled: false, - verificationState: null, - verificationError: null, - smtpAddress: null, - errorMessage: null, - alertMessage: null, - }; + isEnabled: { + type: Boolean, + required: false, + default: false, + }, + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - customEmailNotSetUp() { - return !this.enabled && this.verificationState === null && this.customEmail === null; + isVerificationFailed() { + return this.verificationState === 'failed'; }, - }, - mounted() { - this.getCustomEmailDetails(); - }, - methods: { - dismissAlert() { - this.alertMessage = null; - }, - getCustomEmailDetails() { - axios - .get(this.customEmailEndpoint) - .then(({ data }) => { - this.updateData(data); - }) - .catch(this.handleRequestError) - .finally(() => { - this.loading = 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.enabled = 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; - this.errorMessage = data.error_message; - }, - onSaveCustomEmail(requestData) { - this.alertMessage = null; - this.submitting = 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.submitting = false; - }); - }, - onResetCustomEmail() { - this.confirmModalVisible = true; - }, - onConfirmModalCanceled() { - this.confirmModalVisible = false; - }, - onConfirmModalProceed() { - this.submitting = 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.submitting = false; - }); + 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="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> + <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> - <template #default> - <template v-if="loading"> - <div class="gl-p-3 gl-text-center"> - <gl-loading-icon - :label="$options.I18N_LOADING_LABEL" - size="md" - color="dark" - variant="spinner" - :inline="false" - /> - {{ $options.I18N_LOADING_LABEL }} - </div> - </template> - - <custom-email-confirm-modal - :visible="confirmModalVisible" - :custom-email="customEmail" - @remove="onConfirmModalProceed" - @cancel="onConfirmModalCanceled" - /> + <div v-if="!isVerificationFinished" class="gl-mb-5"> + <gl-badge :variant="badgeVariant">{{ badgeContent }}</gl-badge> + </div> - <gl-alert - v-if="alertMessage" - variant="warning" - class="gl-mt-n5 gl-mb-4 gl-mx-n5" - @dismiss="dismissAlert" - > - {{ alertMessage }} - </gl-alert> + <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> - <!-- 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 && !loading" - :incoming-email="incomingEmail" - :submitting="submitting" - @submit="onSaveCustomEmail" - /> + <template v-if="verificationError"> + <p class="gl-mb-0"> + <strong>{{ errorLabel }}</strong> + </p> + <p>{{ errorDescription }}</p> + </template> - <custom-email-state-started - v-if="verificationState === 'started'" - :custom-email="customEmail" - :smtp-address="smtpAddress" - :submitting="submitting" - @reset="onResetCustomEmail" - /> - </template> - - <template #footer> - <span> - <gl-sprintf :message="$options.I18N_FEEDBACK_PARAGRAPH"> - <template #link="{ content }"> - <gl-link - :href="$options.FEEDBACK_ISSUE_URL" - data-testid="feedback-link" - target="_blank" - class="gl-text-blue-600 font-size-inherit" - >{{ content }} - </gl-link> - </template> - </gl-sprintf> - </span> - </template> - </gl-card> - </div> + <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_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue index 7088627a487..4affcd926d4 100644 --- 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 @@ -54,7 +54,7 @@ export default { required: false, default: '', }, - submitting: { + isSubmitting: { type: Boolean, required: false, default: false, @@ -182,7 +182,7 @@ export default { type="email" :state="validationState.customEmail" :required="true" - :disabled="submitting" + :disabled="isSubmitting" @change="onCustomEmailChange" /> <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> @@ -205,7 +205,7 @@ export default { type="email" :state="validationState.smtpAddress" :required="true" - :disabled="submitting" + :disabled="isSubmitting" @change="validateSmtpAddress" /> <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> @@ -228,7 +228,7 @@ export default { type="number" :state="validationState.smtpPort" :required="true" - :disabled="submitting" + :disabled="isSubmitting" @change="validateSmtpPort" /> <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> @@ -249,7 +249,7 @@ export default { placeholder="contact@example.com" :state="validationState.smtpUsername" :required="true" - :disabled="submitting" + :disabled="isSubmitting" @change="validateSmtpUsername" /> <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> @@ -270,7 +270,7 @@ export default { type="password" :state="validationState.smtpPassword" :required="true" - :disabled="submitting" + :disabled="isSubmitting" @change="validateSmtpPassword" /> </gl-form-group> @@ -281,7 +281,7 @@ export default { class="gl-mt-5" data-testid="form-submit" :disabled="!isFormValid" - :loading="submitting" + :loading="isSubmitting" @click="onSubmit" > {{ $options.I18N_FORM_SUBMIT_LABEL }} diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_state_started.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_state_started.vue deleted file mode 100644 index e7428a1654d..00000000000 --- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_state_started.vue +++ /dev/null @@ -1,60 +0,0 @@ -<script> -import { GlBadge, GlButton, GlSprintf } from '@gitlab/ui'; -import { - I18N_STATE_INTRO_PARAGRAPH, - I18N_STATE_VERIFICATION_STARTED, - I18N_STATE_VERIFICATION_STARTED_INFO_PARAGRAPH, - I18N_RESET_BUTTON_LABEL, -} from '../custom_email_constants'; - -export default { - components: { - GlBadge, - GlButton, - GlSprintf, - }, - I18N_STATE_INTRO_PARAGRAPH, - I18N_STATE_VERIFICATION_STARTED, - I18N_STATE_VERIFICATION_STARTED_INFO_PARAGRAPH, - I18N_RESET_BUTTON_LABEL, - props: { - customEmail: { - type: String, - required: false, - default: '', - }, - smtpAddress: { - type: String, - required: false, - default: '', - }, - submitting: { - type: Boolean, - required: false, - default: false, - }, - }, -}; -</script> - -<template> - <div class="text-center"> - <p> - <gl-sprintf :message="$options.I18N_STATE_INTRO_PARAGRAPH"> - <template #customEmail> - <strong>{{ customEmail }}</strong> - </template> - <template #smtpAddress> - <strong>{{ smtpAddress }}</strong> - </template> - </gl-sprintf> - </p> - <div class="gl-mb-5"> - <gl-badge variant="info">{{ $options.I18N_STATE_VERIFICATION_STARTED }}</gl-badge> - </div> - <p>{{ $options.I18N_STATE_VERIFICATION_STARTED_INFO_PARAGRAPH }}</p> - <gl-button :loading="submitting" @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_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 3af4ea8af65..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 @@ -7,7 +7,7 @@ import { __, sprintf } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ServiceDeskSetting from './service_desk_setting.vue'; -const CustomEmail = () => import('./custom_email.vue'); +const CustomEmailWrapper = () => import('./custom_email_wrapper.vue'); export default { serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk.html', { @@ -18,7 +18,7 @@ export default { GlSprintf, GlLink, ServiceDeskSetting, - CustomEmail, + CustomEmailWrapper, }, directives: { SafeHtml, @@ -77,7 +77,7 @@ export default { }; }, computed: { - showCustomEmail() { + showCustomEmailWrapper() { return this.glFeatures.serviceDeskCustomEmail && this.isEnabled && this.isIssueTrackerEnabled; }, }, @@ -192,8 +192,8 @@ export default { @save="onSaveTemplate" @toggle="onEnableToggled" /> - <custom-email - v-if="showCustomEmail" + <custom-email-wrapper + v-if="showCustomEmailWrapper" :incoming-email="incomingEmail" :custom-email-endpoint="customEmailEndpoint" /> 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 index 5042b0589e8..cdf2e53982e 100644 --- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js +++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js @@ -13,6 +13,8 @@ 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.', @@ -68,7 +70,77 @@ 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_INFO_PARAGRAPH = s__( +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, + }, +}; |