diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-11 15:10:41 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-11 15:10:41 +0300 |
commit | 1c7411c597334e20d2e92cc948f0699d339d2710 (patch) | |
tree | e88e76f1f563b71a4b9113373f24849bc5d1d79e /app | |
parent | 3e9023894d319cf56b7b844910953df19ca010b1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
19 files changed, 391 insertions, 39 deletions
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 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + cancelAction: { text: __('Cancel') }, + components: { + GlModal, + }, + props: { + primaryText: { + type: String, + required: false, + default: __('OK'), + }, + primaryVariant: { + type: String, + required: false, + default: 'confirm', + }, + }, + computed: { + primaryAction() { + return { text: this.primaryText, attributes: { variant: this.primaryVariant } }; + }, + }, + mounted() { + this.$refs.modal.show(); + }, +}; +</script> + +<template> + <gl-modal + ref="modal" + size="sm" + modal-id="confirmationModal" + body-class="gl-display-flex" + :action-primary="primaryAction" + :action-cancel="$options.cancelAction" + hide-header + @primary="$emit('confirmed')" + @hidden="$emit('closed')" + > + <div class="gl-align-self-center"><slot></slot></div> + </gl-modal> +</template> 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; + }, }, }; </script> @@ -169,8 +178,17 @@ export default { v-model.trim="projectKey" data-testid="project-suffix" class="form-control" + :state="!projectKeyError" + @blur="validateProjectKey" /> - <span v-if="hasProjectKeySupport" class="form-text text-muted"> + <span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger"> + {{ projectKeyError }} + </span> + <span + v-if="hasProjectKeySupport" + class="form-text text-muted" + :class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }" + > {{ __('A string appended to the project path to form the Service Desk email address.') }} </span> <span v-else class="form-text text-muted"> 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 @@ <script> -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui'; import { RUNNER_TAG_BADGE_VARIANT } from '../constants'; export default { components: { GlBadge, }, + directives: { + GlTooltip: GlTooltipDirective, + GlResizeObserver: GlResizeObserverDirective, + }, props: { tag: { type: String, @@ -14,14 +18,39 @@ export default { size: { type: String, required: false, - default: 'md', + default: 'sm', + }, + }, + data() { + return { + overflowing: false, + }; + }, + computed: { + tooltip() { + if (this.overflowing) { + return this.tag; + } + return ''; + }, + }, + methods: { + onResize() { + const { scrollWidth, offsetWidth } = this.$el; + this.overflowing = scrollWidth > offsetWidth; }, }, RUNNER_TAG_BADGE_VARIANT, }; </script> <template> - <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT"> + <gl-badge + v-gl-tooltip="tooltip" + v-gl-resize-observer="onResize" + class="gl-display-inline-block gl-max-w-full gl-text-truncate" + :size="size" + :variant="$options.RUNNER_TAG_BADGE_VARIANT" + > {{ tag }} </gl-badge> </template> 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', }, }, }; </script> <template> <div> - <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" /> + <runner-tag + v-for="tag in tagList" + :key="tag" + class="gl-display-inline gl-mr-1" + :tag="tag" + :size="size" + /> </div> </template> 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, }; </script> @@ -112,15 +131,28 @@ export default { <div class="note-header"> <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> <span v-safe-html="actionTextHtml"></span> - <template v-if="canSeeDescriptionVersion" #extra-controls> + <template + v-if="canSeeDescriptionVersion || note.outdated_line_change_path" + #extra-controls + > · <gl-button + v-if="canSeeDescriptionVersion" variant="link" :icon="descriptionVersionToggleIcon" data-testid="compare-btn" @click="toggleDescriptionVersion" >{{ __('Compare with previous version') }}</gl-button > + <gl-button + v-if="note.outdated_line_change_path" + :icon="showLines ? 'chevron-up' : 'chevron-down'" + variant="link" + data-testid="outdated-lines-change-btn" + @click="toggleDiff" + > + {{ __('Compare changes') }} + </gl-button> </template> </note-header> </div> @@ -154,6 +186,37 @@ export default { @click="deleteDescriptionVersion" /> </div> + <div + v-if="lines.length && showLines" + class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden" + > + <table + :class="$options.userColorSchemeClass" + class="code js-syntax-highlight" + data-testid="outdated-lines" + > + <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder"> + <td + :class="line.type" + class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!" + > + {{ line.old_line }} + </td> + <td + :class="line.type" + class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!" + > + {{ line.new_line }} + </td> + <td + :class="line.type" + class="line_content gl-display-table-cell!" + v-html="line.rich_text /* eslint-disable-line vue/no-v-html */" + ></td> + </tr> + </table> + </div> + <gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" /> </div> </div> </timeline-entry-item> diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1eb4f6f1b7a..e8057308386 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -55,6 +55,14 @@ class Projects::NotesController < Projects::ApplicationController end end + def outdated_line_change + diff_lines = Rails.cache.fetch(['note', note.id, 'oudated_line_change'], expires_in: 7.days) do + ::MergeRequests::OutdatedDiscussionDiffLinesService.new(project: @project, note: note).execute.to_json + end + + render json: diff_lines + end + private def render_json_with_notes_serializer diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb new file mode 100644 index 00000000000..c451d4e7936 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_groups.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Mixin for all resolver classes for type `Types::GroupType.connection_type`. +module ResolvesGroups + extend ActiveSupport::Concern + include LooksAhead + + def resolve_with_lookahead(**args) + apply_lookahead(resolve_groups(**args)) + end + + private + + # The resolver should implement this method. + def resolve_groups(**args) + raise NotImplementedError + end + + def preloads + { + contacts: [:contacts], + container_repositories_count: [:container_repositories], + custom_emoji: [:custom_emoji], + full_path: [:route], + organizations: [:organizations], + path: [:route], + dependency_proxy_blob_count: [:dependency_proxy_blobs], + dependency_proxy_blobs: [:dependency_proxy_blobs], + dependency_proxy_image_count: [:dependency_proxy_manifests], + dependency_proxy_image_ttl_policy: [:dependency_proxy_image_ttl_policy], + dependency_proxy_setting: [:dependency_proxy_setting] + } + end +end diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb index b090fdc49d4..abd3bf9e6e0 100644 --- a/app/graphql/resolvers/groups_resolver.rb +++ b/app/graphql/resolvers/groups_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class GroupsResolver < BaseResolver + include ResolvesGroups + type Types::GroupType, null: true argument :include_parent_descendants, GraphQL::Types::Boolean, @@ -19,16 +21,12 @@ module Resolvers alias_method :parent, :object - def resolve(**args) - return [] unless parent.present? - - find_groups(args) - end - private # rubocop: disable CodeReuse/ActiveRecord - def find_groups(args) + def resolve_groups(args) + return Group.none unless parent.present? + GroupsFinder .new(context[:current_user], args.merge(parent: parent)) .execute diff --git a/app/graphql/resolvers/users/groups_resolver.rb b/app/graphql/resolvers/users/groups_resolver.rb index 0899b08e19c..0c1cdc70163 100644 --- a/app/graphql/resolvers/users/groups_resolver.rb +++ b/app/graphql/resolvers/users/groups_resolver.rb @@ -3,8 +3,8 @@ module Resolvers module Users class GroupsResolver < BaseResolver + include ResolvesGroups include Gitlab::Graphql::Authorize::AuthorizeResource - include LooksAhead type Types::GroupType.connection_type, null: true @@ -23,19 +23,14 @@ module Resolvers Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute end - def resolve_with_lookahead(**args) - return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml) - - apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute) + def ready?(**args) + Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml) end private - def preloads - { - path: [:route], - full_path: [:route] - } + def resolve_groups(**args) + Groups::UserGroupsFinder.new(current_user, object, args).execute end end end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index b8f382681f3..4a20d84f2ab 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -34,6 +34,7 @@ module Types null: true, method: :project_creation_level_str, description: 'Permission level required to create projects in the group.' + field :subgroup_creation_level, type: GraphQL::Types::String, null: true, @@ -44,6 +45,7 @@ module Types type: GraphQL::Types::Boolean, null: true, description: 'Indicates if all users in this group are required to set up two-factor authentication.' + field :two_factor_grace_period, type: GraphQL::Types::Int, null: true, @@ -225,11 +227,11 @@ module Types end def dependency_proxy_image_count - group.dependency_proxy_manifests.count + group.dependency_proxy_manifests.size end def dependency_proxy_blob_count - group.dependency_proxy_blobs.count + group.dependency_proxy_blobs.size end def dependency_proxy_total_size diff --git a/app/models/group.rb b/app/models/group.rb index 15eb2ee81a6..2dd20300ad2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -56,6 +56,9 @@ class Group < Namespace has_many :boards has_many :badges, class_name: 'GroupBadge' + has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group + has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group + has_many :cluster_groups, class_name: 'Clusters::Group' has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster' @@ -757,14 +760,6 @@ class Group < Namespace Timelog.in_group(self) end - def organizations - ::CustomerRelations::Organization.where(group_id: self.id) - end - - def contacts - ::CustomerRelations::Contact.where(group_id: self.id) - end - def dependency_proxy_image_ttl_policy super || build_dependency_proxy_image_ttl_policy end diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 27d7dac2182..8e1b675d051 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -33,7 +33,7 @@ module Ci end def runner_variables - variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables + variables.sort_and_expand_all(keep_undefined: true).to_runner_variables end def refspecs diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 8308e954c06..8f189f14dea 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -51,6 +51,10 @@ class NoteEntity < API::Entities::Note SystemNoteHelper.system_note_icon_name(note) end + expose :outdated_line_change_path, if: -> (note, _) { note.system? && note.change_position&.line_range && Feature.enabled?(:display_outdated_line_diff, note.project, default_enabled: :yaml) } do |note| + outdated_line_change_namespace_project_note_path(namespace_id: note.project.namespace, project_id: note.project, id: note) + end + expose :is_noteable_author do |note| note.noteable_author?(request.noteable) end diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb new file mode 100644 index 00000000000..de06f18dd82 --- /dev/null +++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module MergeRequests + class OutdatedDiscussionDiffLinesService + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :note + + OVERFLOW_LINES_COUNT = 2 + + def initialize(project:, note:) + @project = project + @note = note + end + + def execute + end_position = position.line_range["end"] + diff_line_index = diff_lines.find_index { |l| l.new_line == end_position["new_line"] || l.old_line == end_position["old_line"] } + initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max + last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min + + prev_lines = [] + + diff_lines[initial_line_index..last_line_index].each do |line| + if line.meta? + prev_lines.clear + else + prev_lines << line + end + end + + prev_lines + end + + private + + def position + note.change_position + end + + def repository + project.repository + end + + def diff_file + position.diff_file(repository) + end + + def diff_lines + strong_memoize(:diff_lines) do + diff_file.highlighted_diff_lines + end + end + end +end |