diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 87da2143dd2..27fb81c9ced 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9de3dd28a5c8248903160ea35d9f718899f51c89 +4892c8502cc45217903a8a584a7b5edb15edf86e diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue new file mode 100644 index 00000000000..733d0f69f5d --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js new file mode 100644 index 00000000000..f0908a60ac5 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; + +export function confirmViaGlModal(message, element) { + return new Promise((resolve) => { + let confirmed = false; + + const props = {}; + + const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant'); + + if (confirmBtnVariant) { + props.primaryVariant = confirmBtnVariant; + } + const screenReaderText = + element.querySelector('.gl-sr-only')?.textContent || + element.querySelector('.sr-only')?.textContent || + element.getAttribute('aria-label'); + + if (screenReaderText) { + props.primaryText = screenReaderText; + } + + const component = new Vue({ + components: { + ConfirmModal: () => import('./confirm_modal.vue'), + }, + render(h) { + return h( + 'confirm-modal', + { + props, + on: { + confirmed() { + confirmed = true; + }, + closed() { + component.$destroy(); + resolve(confirmed); + }, + }, + }, + [message], + ); + }, + }).$mount(); + }); +} diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index 8b40cc7bd11..6b1985a23ba 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -1,4 +1,42 @@ import Rails from '@rails/ujs'; +import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal'; + +function monkeyPatchConfirmModal() { + /** + * This function is used to replace the `Rails.confirm` which uses `window.confirm` + * + * This function opens a confirmation modal which will resolve in a promise. + * Because the `Rails.confirm` API is synchronous, we go with a little hack here: + * + * 1. User clicks on something with `data-confirm` + * 2. We open the modal and return `false`, ending the "Rails" event chain + * 3. If the modal is closed and the user "confirmed" the action + * 1. replace the `Rails.confirm` with a function that always returns `true` + * 2. click the same element programmatically + * + * @param message {String} Message to be shown in the modal + * @param element {HTMLElement} Element that was clicked on + * @returns {boolean} + */ + function confirmViaModal(message, element) { + confirmViaGlModal(message, element) + .then((confirmed) => { + if (confirmed) { + Rails.confirm = () => true; + element.click(); + Rails.confirm = confirmViaModal; + } + }) + .catch(() => {}); + return false; + } + + Rails.confirm = confirmViaModal; +} + +if (gon?.features?.bootstrapConfirmationModals) { + monkeyPatchConfirmModal(); +} export const initRails = () => { // eslint-disable-next-line no-underscore-dangle diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index 2ce1f0366c1..6f19a9f4379 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -16,9 +16,11 @@ import { __, s__, sprintf } from '~/locale'; import Tracking from '~/tracking'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { - WIKI_CONTENT_EDITOR_TRACKING_LABEL, CONTENT_EDITOR_LOADED_ACTION, SAVED_USING_CONTENT_EDITOR_ACTION, + WIKI_CONTENT_EDITOR_TRACKING_LABEL, + WIKI_FORMAT_LABEL, + WIKI_FORMAT_UPDATED_ACTION, } from '../constants'; const trackingMixin = Tracking.mixin({ @@ -219,6 +221,8 @@ export default { this.trackFormSubmit(); } + this.trackWikiFormat(); + // Wait until form field values are refreshed await this.$nextTick(); @@ -304,6 +308,14 @@ export default { } }, + trackWikiFormat() { + this.track(WIKI_FORMAT_UPDATED_ACTION, { + label: WIKI_FORMAT_LABEL, + value: this.format, + extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format }, + }); + }, + dismissContentEditorAlert() { this.isContentEditorAlertDismissed = true; }, diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js index b358ac9cf52..94d086158f1 100644 --- a/app/assets/javascripts/pages/shared/wikis/constants.js +++ b/app/assets/javascripts/pages/shared/wikis/constants.js @@ -1,4 +1,5 @@ -export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor'; - export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded'; export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor'; +export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor'; +export const WIKI_FORMAT_LABEL = 'wiki_format'; +export const WIKI_FORMAT_UPDATED_ACTION = 'wiki_format_updated'; 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 index d964a701b08..b8053bf9ab5 100644 --- 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 @@ -75,6 +75,7 @@ export default { outgoingName: this.initialOutgoingName || __('GitLab Support Bot'), projectKey: this.initialProjectKey, searchTerm: '', + projectKeyError: null, }; }, computed: { @@ -104,6 +105,14 @@ export default { this.selectedFileTemplateProjectId = selectedFileTemplateProjectId; this.selectedTemplate = selectedTemplate; }, + validateProjectKey() { + if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) { + this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.'); + return; + } + + this.projectKeyError = null; + }, }, }; @@ -169,8 +178,17 @@ export default { v-model.trim="projectKey" data-testid="project-suffix" class="form-control" + :state="!projectKeyError" + @blur="validateProjectKey" /> - + + {{ projectKeyError }} + + {{ __('A string appended to the project path to form the Service Desk email address.') }} diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue index 06562e618a8..6ad2023a866 100644 --- a/app/assets/javascripts/runner/components/runner_tag.vue +++ b/app/assets/javascripts/runner/components/runner_tag.vue @@ -1,11 +1,15 @@ diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue index aec0d8e2c66..8da5e33076f 100644 --- a/app/assets/javascripts/runner/components/runner_tags.vue +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -14,13 +14,19 @@ export default { size: { type: String, required: false, - default: 'md', + default: 'sm', }, }, }; diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index c0b256ec7fa..3952e2398e0 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -27,7 +27,7 @@ export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__( export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects'); export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs'); -export const RUNNER_TAG_BADGE_VARIANT = 'info'; +export const RUNNER_TAG_BADGE_VARIANT = 'neutral'; export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100'; // Filtered search parameter names diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 755e6f1f224..8877cfa39fb 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -26,6 +26,7 @@ import { import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; +import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import initMRPopovers from '~/mr_popover/'; import noteHeader from '~/notes/components/note_header.vue'; @@ -61,6 +62,9 @@ export default { data() { return { expanded: false, + lines: [], + showLines: false, + loadingDiff: false, }; }, computed: { @@ -94,10 +98,25 @@ export default { }, methods: { ...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']), + async toggleDiff() { + this.showLines = !this.showLines; + + if (!this.lines.length) { + this.loadingDiff = true; + const { data } = await axios.get(this.note.outdated_line_change_path); + + this.lines = data.map((l) => ({ + ...l, + rich_text: l.rich_text.replace(/^[+ -]/, ''), + })); + this.loadingDiff = false; + } + }, }, safeHtmlConfig: { ADD_TAGS: ['use'], // to support icon SVGs }, + userColorSchemeClass: window.gon.user_color_scheme, }; @@ -112,15 +131,28 @@ export default {
-