diff options
Diffstat (limited to 'app/assets/javascripts/projects')
9 files changed, 534 insertions, 2 deletions
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js index 0a52a92ae9d..f0832bd36a5 100644 --- a/app/assets/javascripts/projects/commits/store/actions.js +++ b/app/assets/javascripts/projects/commits/store/actions.js @@ -18,7 +18,7 @@ export default { fetchAuthors({ dispatch, state }, author = null) { const { projectId } = state; return axios - .get(joinPaths(gon.relative_url_root || '', '/autocomplete/users.json'), { + .get(joinPaths(gon.relative_url_root || '', '/-/autocomplete/users.json'), { params: { project_id: projectId, active: true, diff --git a/app/assets/javascripts/projects/components/remove_modal.vue b/app/assets/javascripts/projects/components/remove_modal.vue new file mode 100644 index 00000000000..37f58efcb30 --- /dev/null +++ b/app/assets/javascripts/projects/components/remove_modal.vue @@ -0,0 +1,108 @@ +<script> +import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { rstrip } from '~/lib/utils/common_utils'; +import csrf from '~/lib/utils/csrf'; + +export default { + components: { + GlModal, + GlSprintf, + GlFormInput, + GlButton, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + confirmPhrase: { + type: String, + required: true, + }, + warningMessage: { + type: String, + required: true, + }, + formPath: { + type: String, + required: true, + }, + }, + data() { + return { + userInput: null, + }; + }, + computed: { + buttonDisabled() { + return rstrip(this.userInput) !== this.confirmPhrase; + }, + csrfToken() { + return csrf.token; + }, + }, + methods: { + submitForm() { + this.$refs.form.submit(); + }, + }, + strings: { + removeProject: __('Remove project'), + title: __('Confirmation required'), + confirm: __('Confirm'), + dataLoss: __( + 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.', + ), + confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'), + }, + modalId: 'remove-project-modal', +}; +</script> + +<template> + <form ref="form" :action="formPath" method="post"> + <input type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{ + $options.strings.removeProject + }}</gl-button> + <gl-modal + ref="removeModal" + :modal-id="$options.modalId" + size="sm" + ok-variant="danger" + footer-class="bg-gray-light gl-p-5" + > + <template #modal-title>{{ $options.strings.title }}</template> + <template #modal-footer> + <div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"> + <gl-button + :disabled="buttonDisabled" + category="primary" + variant="danger" + @click="submitForm" + > + {{ $options.strings.confirm }} + </gl-button> + </div> + </template> + <div> + <p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p> + <p class="gl-mb-0">{{ $options.strings.dataLoss }}</p> + <p> + <gl-sprintf :message="$options.strings.confirmText"> + <template #phrase_code> + <code>{{ confirmPhrase }}</code> + </template> + </gl-sprintf> + </p> + <gl-form-input + id="confirm_name_input" + v-model="userInput" + name="confirm_name_input" + type="text" + /> + </div> + </gl-modal> + </form> +</template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue index d701f238a2e..d726196aadf 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue @@ -28,7 +28,7 @@ export default { }; </script> <template> - <div class="prepend-top-default"> + <div class="gl-mt-3"> <p> <slot></slot> </p> diff --git a/app/assets/javascripts/projects/project_remove_modal.js b/app/assets/javascripts/projects/project_remove_modal.js new file mode 100644 index 00000000000..dbdad1bf6f1 --- /dev/null +++ b/app/assets/javascripts/projects/project_remove_modal.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import RemoveProjectModal from './components/remove_modal.vue'; + +export default (selector = '#js-confirm-project-remove') => { + const el = document.querySelector(selector); + + if (!el) return; + + const { formPath, confirmPhrase, warningMessage } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(RemoveProjectModal, { + props: { + confirmPhrase, + warningMessage, + formPath, + }, + }); + }, + }); +}; 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 new file mode 100644 index 00000000000..d61569fcd6e --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -0,0 +1,160 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ServiceDeskSetting from './service_desk_setting.vue'; +import ServiceDeskService from '../services/service_desk_service'; +import eventHub from '../event_hub'; + +export default { + name: 'ServiceDeskRoot', + components: { + GlAlert, + ServiceDeskSetting, + }, + props: { + initialIsEnabled: { + type: Boolean, + required: true, + }, + endpoint: { + type: String, + required: true, + }, + initialIncomingEmail: { + type: String, + required: false, + default: '', + }, + selectedTemplate: { + type: String, + required: false, + default: '', + }, + outgoingName: { + type: String, + required: false, + default: '', + }, + projectKey: { + type: String, + required: false, + default: '', + }, + templates: { + type: Array, + required: false, + default: () => [], + }, + }, + + data() { + return { + isEnabled: this.initialIsEnabled, + incomingEmail: this.initialIncomingEmail, + isTemplateSaving: false, + isAlertShowing: false, + alertVariant: 'danger', + alertMessage: '', + }; + }, + + created() { + eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); + eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate); + + this.service = new ServiceDeskService(this.endpoint); + + if (this.isEnabled && !this.incomingEmail) { + this.fetchIncomingEmail(); + } + }, + + beforeDestroy() { + eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); + eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate); + }, + + methods: { + fetchIncomingEmail() { + this.service + .fetchIncomingEmail() + .then(({ data }) => { + const email = data.service_desk_address; + if (!email) { + throw new Error(__("Response didn't include `service_desk_address`")); + } + + this.incomingEmail = email; + }) + .catch(() => + this.showAlert(__('An error occurred while fetching the Service Desk address.')), + ); + }, + + onEnableToggled(isChecked) { + this.isEnabled = isChecked; + this.incomingEmail = ''; + + this.service + .toggleServiceDesk(isChecked) + .then(({ data }) => { + const email = data.service_desk_address; + if (isChecked && !email) { + throw new Error(__("Response didn't include `service_desk_address`")); + } + + this.incomingEmail = email; + }) + .catch(() => { + const message = isChecked + ? __('An error occurred while enabling Service Desk.') + : __('An error occurred while disabling Service Desk.'); + + this.showAlert(message); + }); + }, + + onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) { + this.isTemplateSaving = true; + this.service + .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled) + .then(() => this.showAlert(__('Template was successfully saved.'), 'success')) + .catch(() => + this.showAlert( + __('An error occurred while saving the template. Please check if the template exists.'), + ), + ) + .finally(() => { + this.isTemplateSaving = false; + }); + }, + + showAlert(message, variant = 'danger') { + this.isAlertShowing = true; + this.alertMessage = message; + this.alertVariant = variant; + }, + + onDismiss() { + this.isAlertShowing = false; + }, + }, +}; +</script> + +<template> + <div> + <gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss"> + {{ alertMessage }} + </gl-alert> + <service-desk-setting + :is-enabled="isEnabled" + :incoming-email="incomingEmail" + :initial-selected-template="selectedTemplate" + :initial-outgoing-name="outgoingName" + :initial-project-key="projectKey" + :templates="templates" + :is-template-saving="isTemplateSaving" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue new file mode 100644 index 00000000000..43c20fea43e --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -0,0 +1,169 @@ +<script> +import { GlDeprecatedButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import eventHub from '../event_hub'; + +export default { + name: 'ServiceDeskSetting', + directives: { + tooltip, + }, + components: { + ClipboardButton, + GlDeprecatedButton, + GlFormSelect, + GlToggle, + GlLoadingIcon, + }, + mixins: [glFeatureFlagsMixin()], + props: { + isEnabled: { + type: Boolean, + required: true, + }, + incomingEmail: { + type: String, + required: false, + default: '', + }, + initialSelectedTemplate: { + type: String, + required: false, + default: '', + }, + initialOutgoingName: { + type: String, + required: false, + default: '', + }, + initialProjectKey: { + type: String, + required: false, + default: '', + }, + templates: { + type: Array, + required: false, + default: () => [], + }, + isTemplateSaving: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + selectedTemplate: this.initialSelectedTemplate, + outgoingName: this.initialOutgoingName || __('GitLab Support Bot'), + projectKey: this.initialProjectKey, + }; + }, + computed: { + templateOptions() { + return [''].concat(this.templates); + }, + hasProjectKeySupport() { + return Boolean(this.glFeatures.serviceDeskCustomAddress); + }, + }, + methods: { + onCheckboxToggle(isChecked) { + eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked); + }, + onSaveTemplate() { + eventHub.$emit('serviceDeskTemplateSave', { + selectedTemplate: this.selectedTemplate, + outgoingName: this.outgoingName, + projectKey: this.projectKey, + }); + }, + }, +}; +</script> + +<template> + <div> + <gl-toggle + id="service-desk-checkbox" + :value="isEnabled" + class="d-inline-block align-middle mr-1" + label="Service desk" + label-position="left" + @change="onCheckboxToggle" + /> + <label class="align-middle" for="service-desk-checkbox"> + {{ __('Activate Service Desk') }} + </label> + <div v-if="isEnabled" class="row mt-3"> + <div class="col-md-9 mb-0"> + <strong id="incoming-email-describer" class="d-block mb-1"> + {{ __('Forward external support email address to') }} + </strong> + <template v-if="incomingEmail"> + <div class="input-group"> + <input + ref="service-desk-incoming-email" + type="text" + class="form-control incoming-email h-auto" + :placeholder="__('Incoming email')" + :aria-label="__('Incoming email')" + aria-describedby="incoming-email-describer" + :value="incomingEmail" + disabled="true" + /> + <div class="input-group-append"> + <clipboard-button + :title="__('Copy')" + :text="incomingEmail" + css-class="btn qa-clipboard-button" + /> + </div> + </div> + </template> + <template v-else> + <gl-loading-icon :inline="true" /> + <span class="sr-only">{{ __('Fetching incoming email') }}</span> + </template> + + <label for="service-desk-template-select" class="mt-3"> + {{ __('Template to append to all Service Desk issues') }} + </label> + <gl-form-select + id="service-desk-template-select" + v-model="selectedTemplate" + :options="templateOptions" + /> + <label for="service-desk-email-from-name" class="mt-3"> + {{ __('Email display name') }} + </label> + <input id="service-desk-email-from-name" v-model.trim="outgoingName" class="form-control" /> + <span class="form-text text-muted"> + {{ __('Emails sent from Service Desk will have this name') }} + </span> + <template v-if="hasProjectKeySupport"> + <label for="service-desk-project-suffix" class="mt-3"> + {{ __('Project name suffix') }} + </label> + <input id="service-desk-project-suffix" v-model.trim="projectKey" class="form-control" /> + <span class="form-text text-muted mb-3"> + {{ + __( + 'Project name suffix is a user-defined string which will be appended to the project path, and will form the Service Desk email address.', + ) + }} + </span> + </template> + <gl-deprecated-button + variant="success" + :disabled="isTemplateSaving" + @click="onSaveTemplate" + >{{ __('Save template') }}</gl-deprecated-button + > + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/settings_service_desk/event_hub.js b/app/assets/javascripts/projects/settings_service_desk/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js new file mode 100644 index 00000000000..15c077de72e --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import ServiceDeskRoot from './components/service_desk_root.vue'; + +export default () => { + const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root'); + if (serviceDeskRootElement) { + // eslint-disable-next-line no-new + new Vue({ + el: serviceDeskRootElement, + components: { + ServiceDeskRoot, + }, + data() { + const { dataset } = serviceDeskRootElement; + return { + initialIsEnabled: parseBoolean(dataset.enabled), + endpoint: dataset.endpoint, + incomingEmail: dataset.incomingEmail, + selectedTemplate: dataset.selectedTemplate, + outgoingName: dataset.outgoingName, + projectKey: dataset.projectKey, + templates: JSON.parse(dataset.templates), + }; + }, + render(createElement) { + return createElement('service-desk-root', { + props: { + initialIsEnabled: this.initialIsEnabled, + endpoint: this.endpoint, + initialIncomingEmail: this.incomingEmail, + selectedTemplate: this.selectedTemplate, + outgoingName: this.outgoingName, + projectKey: this.projectKey, + templates: this.templates, + }, + }); + }, + }); + } +}; diff --git a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js new file mode 100644 index 00000000000..d707763c64e --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js @@ -0,0 +1,27 @@ +import axios from '~/lib/utils/axios_utils'; + +class ServiceDeskService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchIncomingEmail() { + return axios.get(this.endpoint); + } + + toggleServiceDesk(enable) { + return axios.put(this.endpoint, { service_desk_enabled: enable }); + } + + updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) { + const body = { + issue_template_key: selectedTemplate, + outgoing_name: outgoingName, + project_key: projectKey, + service_desk_enabled: isEnabled, + }; + return axios.put(this.endpoint, body); + } +} + +export default ServiceDeskService; |