diff --git a/.rubocop.yml b/.rubocop.yml index 5757a273926..1b2e7ea470a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -666,6 +666,7 @@ Gitlab/NamespacedClass: - 'ee/elastic/**/*.rb' - 'scripts/**/*' - 'spec/migrations/**/*.rb' + - 'app/experiments/**/*_experiment.rb' Lint/HashCompareByIdentity: Enabled: true diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 69331ff1a06..d04896bf6e5 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -86,6 +86,7 @@ export const defaultAutocompleteConfig = { labels: true, snippets: true, vulnerabilities: true, + contacts: true, }; class GfmAutoComplete { @@ -127,6 +128,7 @@ class GfmAutoComplete { if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) this.setupLabels($input); if (this.enableMap.snippets) this.setupSnippets($input); + if (this.enableMap.contacts) this.setupContacts($input); $input.filter('[data-supports-quick-actions="true"]').atwho({ at: '/', @@ -174,9 +176,16 @@ class GfmAutoComplete { let tpl = '/${name} '; let referencePrefix = null; if (value.params.length > 0) { - [[referencePrefix]] = value.params; - if (/^[@%~]/.test(referencePrefix)) { + const regexp = /\[[a-z]+:/; + const match = regexp.exec(value.params); + if (match) { + [referencePrefix] = match; tpl += '<%- referencePrefix %>'; + } else { + [[referencePrefix]] = value.params; + if (/^[@%~]/.test(referencePrefix)) { + tpl += '<%- referencePrefix %>'; + } } } return template(tpl, { interpolate: /<%=([\s\S]+?)%>/g })({ referencePrefix }); @@ -619,6 +628,42 @@ class GfmAutoComplete { }); } + setupContacts($input) { + $input.atwho({ + at: '[contact:', + suffix: ']', + alias: 'contacts', + searchKey: 'search', + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.email != null) { + tmpl = GfmAutoComplete.Contacts.templateFunction(value); + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string + insertTpl: '${atwho-at}${email}', + callbacks: { + ...this.getDefaultCallbacks(), + beforeSave(contacts) { + return $.map(contacts, (m) => { + if (m.email == null) { + return m; + } + return { + id: m.id, + email: m.email, + firstName: m.first_name, + lastName: m.last_name, + search: `${m.email}`, + }; + }); + }, + }, + }); + } + getDefaultCallbacks() { const self = this; @@ -790,6 +835,7 @@ GfmAutoComplete.atTypeMap = { '/': 'commands', '[vulnerability:': 'vulnerabilities', $: 'snippets', + '[contact:': 'contacts', }; GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; @@ -883,6 +929,11 @@ GfmAutoComplete.Milestones = { return `
  • ${escape(title)}
  • `; }, }; +GfmAutoComplete.Contacts = { + templateFunction({ email, firstName, lastName }) { + return `
  • ${firstName} ${lastName} ${escape(email)}
  • `; + }, +}; GfmAutoComplete.Loading = { template: '
  • Loading...
  • ', diff --git a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue new file mode 100644 index 00000000000..8f9198855c6 --- /dev/null +++ b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 603ad71adb9..cbf38984e23 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -9,6 +9,7 @@ import axios from '~/lib/utils/axios_utils'; import { stripHtml } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import MarkdownHeader from './header.vue'; import MarkdownToolbar from './toolbar.vue'; @@ -23,6 +24,7 @@ export default { GlIcon, Suggestions, }, + mixins: [glFeatureFlagsMixin()], props: { /** * This prop should be bound to the value of the `