diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index a6faa04b440..689f2f0898e 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -399,6 +399,12 @@ export const ISSUABLE_CHANGE_LABEL = {
defaultKeys: ['l'],
};
+export const ISSUABLE_COPY_REF = {
+ id: 'issuables.copyIssuableRef',
+ description: __('Copy reference'),
+ defaultKeys: ['c r'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
export const ISSUE_MR_CHANGE_ASSIGNEE = {
id: 'issuesMRs.changeAssignee',
description: __('Change assignee'),
@@ -606,7 +612,12 @@ const PROJECT_FILES_SHORTCUTS_GROUP = {
const ISSUABLE_SHORTCUTS_GROUP = {
id: 'issuables',
name: __('Epics, issues, and merge requests'),
- keybindings: [ISSUABLE_COMMENT_OR_REPLY, ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_CHANGE_LABEL],
+ keybindings: [
+ ISSUABLE_COMMENT_OR_REPLY,
+ ISSUABLE_EDIT_DESCRIPTION,
+ ISSUABLE_CHANGE_LABEL,
+ ISSUABLE_COPY_REF,
+ ],
};
const ISSUE_MR_SHORTCUTS_GROUP = {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 0c882ff9ea2..b0e515ac19d 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -14,6 +14,7 @@ import {
ISSUABLE_COMMENT_OR_REPLY,
ISSUABLE_EDIT_DESCRIPTION,
MR_COPY_SOURCE_BRANCH_NAME,
+ ISSUABLE_COPY_REF,
} from './keybindings';
import Shortcuts from './shortcuts';
@@ -21,15 +22,24 @@ export default class ShortcutsIssuable extends Shortcuts {
constructor() {
super();
- this.inMemoryButton = document.createElement('button');
- this.clipboardInstance = new ClipboardJS(this.inMemoryButton);
- this.clipboardInstance.on('success', () => {
+ this.branchInMemoryButton = document.createElement('button');
+ this.branchClipboardInstance = new ClipboardJS(this.branchInMemoryButton);
+ this.branchClipboardInstance.on('success', () => {
toast(s__('GlobalShortcuts|Copied source branch name to clipboard.'));
});
- this.clipboardInstance.on('error', () => {
+ this.branchClipboardInstance.on('error', () => {
toast(s__('GlobalShortcuts|Unable to copy the source branch name at this time.'));
});
+ this.refInMemoryButton = document.createElement('button');
+ this.refClipboardInstance = new ClipboardJS(this.refInMemoryButton);
+ this.refClipboardInstance.on('success', () => {
+ toast(s__('GlobalShortcuts|Copied reference to clipboard.'));
+ });
+ this.refClipboardInstance.on('error', () => {
+ toast(s__('GlobalShortcuts|Unable to copy the reference at this time.'));
+ });
+
this.bindCommands([
[ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsIssuable.openSidebarDropdown('assignee')],
[ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsIssuable.openSidebarDropdown('milestone')],
@@ -37,6 +47,7 @@ export default class ShortcutsIssuable extends Shortcuts {
[ISSUABLE_COMMENT_OR_REPLY, ShortcutsIssuable.replyWithSelectedText],
[ISSUABLE_EDIT_DESCRIPTION, ShortcutsIssuable.editIssue],
[MR_COPY_SOURCE_BRANCH_NAME, () => this.copyBranchName()],
+ [ISSUABLE_COPY_REF, () => this.copyIssuableRef()],
]);
/**
@@ -163,9 +174,20 @@ export default class ShortcutsIssuable extends Shortcuts {
const branchName = button?.dataset.clipboardText;
if (branchName) {
- this.inMemoryButton.dataset.clipboardText = branchName;
+ this.branchInMemoryButton.dataset.clipboardText = branchName;
- this.inMemoryButton.dispatchEvent(new CustomEvent('click'));
+ this.branchInMemoryButton.dispatchEvent(new CustomEvent('click'));
+ }
+ }
+
+ async copyIssuableRef() {
+ const refButton = document.querySelector('.js-copy-reference');
+ const copiedRef = refButton?.dataset.clipboardText;
+
+ if (copiedRef) {
+ this.refInMemoryButton.dataset.clipboardText = copiedRef;
+
+ this.refInMemoryButton.dispatchEvent(new CustomEvent('click'));
}
}
}
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 5468e42b6b3..86c0f34215e 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -25,6 +25,7 @@ import {
AWS_TOKEN_CONSTANTS,
ADD_CI_VARIABLE_MODAL_ID,
AWS_TIP_DISMISSED_COOKIE_NAME,
+ AWS_TIP_TITLE,
AWS_TIP_MESSAGE,
CONTAINS_VARIABLE_REFERENCE_MESSAGE,
defaultVariableState,
@@ -62,10 +63,6 @@ export default {
},
mixins: [glFeatureFlagsMixin(), trackingMixin],
inject: [
- 'awsLogoSvgPath',
- 'awsTipCommandsLink',
- 'awsTipDeployLink',
- 'awsTipLearnLink',
'containsVariableReferenceLink',
'environmentScopeLink',
'isProtectedByDefault',
@@ -295,6 +292,7 @@ export default {
},
},
i18n: {
+ awsTipTitle: AWS_TIP_TITLE,
awsTipMessage: AWS_TIP_MESSAGE,
containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
defaultScope: allEnvironments.text,
@@ -305,6 +303,9 @@ export default {
flagLink: helpPagePath('ci/variables/index', {
anchor: 'define-a-cicd-variable-in-the-ui',
}),
+ oidcLink: helpPagePath('ci/cloud_services/index', {
+ anchor: 'oidc-authorization-with-your-cloud-provider',
+ }),
modalId: ADD_CI_VARIABLE_MODAL_ID,
tokens: awsTokens,
tokenList: awsTokenList,
@@ -322,6 +323,23 @@ export default {
@hidden="resetModalHandler"
@shown="onShow"
>
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
-
-
-
-
-
-
-
- {{ content }}
-
-
- {{ content }}
-
-
-
-
- {{ __('Learn more about deploying to AWS') }}
-
-
-
![]()
-
-
-
+
{
const {
- awsLogoSvgPath,
- awsTipCommandsLink,
- awsTipDeployLink,
- awsTipLearnLink,
containsVariableReferenceLink,
endpoint,
environmentScopeLink,
@@ -57,10 +53,6 @@ const mountCiVariableListApp = (containerEl) => {
el: containerEl,
apolloProvider,
provide: {
- awsLogoSvgPath,
- awsTipCommandsLink,
- awsTipDeployLink,
- awsTipLearnLink,
containsVariableReferenceLink,
endpoint,
environmentScopeLink,
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index a1b264cfe54..0871d543d46 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -1,13 +1,5 @@
+
+
+
diff --git a/app/assets/javascripts/pages/shared/wikis/show.js b/app/assets/javascripts/pages/shared/wikis/show.js
index 9906cb595f8..9bc399d07b3 100644
--- a/app/assets/javascripts/pages/shared/wikis/show.js
+++ b/app/assets/javascripts/pages/shared/wikis/show.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Wikis from './wikis';
import WikiContent from './components/wiki_content.vue';
+import WikiExport from './components/wiki_export.vue';
const mountWikiContentApp = () => {
const el = document.querySelector('.js-async-wiki-page-content');
@@ -20,8 +21,28 @@ const mountWikiContentApp = () => {
}
};
+const mountWikiExportApp = () => {
+ const el = document.querySelector('#js-export-actions');
+
+ if (!el) return false;
+ const { target, title, stylesheet } = JSON.parse(el.dataset.options);
+
+ return new Vue({
+ el,
+ provide: {
+ target,
+ title,
+ stylesheet,
+ },
+ render(createElement) {
+ return createElement(WikiExport);
+ },
+ });
+};
+
export const mountApplications = () => {
// eslint-disable-next-line no-new
new Wikis();
mountWikiContentApp();
+ mountWikiExportApp();
};
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 f90633c6e03..a1c1b1141a7 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
@@ -8,7 +8,9 @@ import {
I18N_CARD_TITLE,
I18N_GENERIC_ERROR,
I18N_FEEDBACK_PARAGRAPH,
+ I18N_TOAST_SAVED,
} from '../custom_email_constants';
+import CustomEmailForm from './custom_email_form.vue';
export default {
components: {
@@ -18,11 +20,13 @@ export default {
GlSprintf,
GlLink,
GlCard,
+ CustomEmailForm,
},
FEEDBACK_ISSUE_URL,
I18N_LOADING_LABEL,
I18N_CARD_TITLE,
I18N_FEEDBACK_PARAGRAPH,
+ I18N_TOAST_SAVED,
props: {
incomingEmail: {
type: String,
@@ -38,6 +42,7 @@ export default {
data() {
return {
loading: true,
+ submitting: false,
customEmail: null,
enabled: false,
verificationState: null,
@@ -47,6 +52,11 @@ export default {
alertMessage: null,
};
},
+ computed: {
+ customEmailNotSetUp() {
+ return !this.enabled && this.verificationState === null && this.customEmail === null;
+ },
+ },
mounted() {
this.getCustomEmailDetails();
},
@@ -76,6 +86,21 @@ export default {
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);
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.submitting = false;
+ });
+ },
},
};
@@ -108,11 +133,20 @@ export default {
{{ alertMessage }}
+
+
+
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..7088627a487
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -0,0 +1,291 @@
+
+
+
+
+
{{ $options.I18N_FORM_INTRODUCTION_PARAGRAPH }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $options.I18N_FORM_SUBMIT_LABEL }}
+
+
+
+
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 9770a1f4df9..cc5dc8af2e4 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
@@ -8,3 +8,46 @@ 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_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.',
+);
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index dd9585734db..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');
diff --git a/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue b/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue
new file mode 100644
index 00000000000..a15c8ee2e9f
--- /dev/null
+++ b/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue b/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
new file mode 100644
index 00000000000..0679d31a8b8
--- /dev/null
+++ b/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ {{ $options.i18n.infoBannerAdminNote }}
{{ serviceDeskEmailAddress }}
+
+ {{ $options.i18n.infoBannerUserNote }}
+
+ {{ $options.i18n.learnMore }}
+
+
+
+
+
+
+
+ {{ $options.i18n.infoBannerUserNote }}
+
+ {{ $options.i18n.learnMore }}
+
+
+
+
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
index e4b8142e153..32b219a9fa9 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
@@ -1,5 +1,4 @@