diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-15 03:07:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-15 03:07:23 +0300 |
commit | afd8f58f2d0d42d21496fe4652c1664add9b68b7 (patch) | |
tree | 38741f62cac6fafb42d30632596db951ec955850 /app | |
parent | beabc7d164276a8bb35c2b497a0c4dc0dc824e3c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 191 insertions, 62 deletions
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js index e5b3b8a397c..08177cd0eac 100644 --- a/app/assets/javascripts/deprecated_notes.js +++ b/app/assets/javascripts/deprecated_notes.js @@ -25,7 +25,7 @@ import syntaxHighlight from '~/syntax_highlight'; import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue'; import * as constants from '~/notes/constants'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; -import { COMMENT_FORM } from '~/notes/i18n'; +import { COMMENT_FORM, UPDATE_COMMENT_FORM } from '~/notes/i18n'; import Autosave from './autosave'; import loadAwardsHandler from './awards_handler'; import { defaultAutocompleteConfig } from './gfm_auto_complete'; @@ -708,11 +708,16 @@ export default class Notes { }); } - updateNoteError() { - createAlert({ - message: __( - 'Your comment could not be updated! Please check your network connection and try again.', - ), + updateNoteError(error, $editingNote) { + const serverErrorMessage = error?.response?.data?.errors; + + const alertMessage = serverErrorMessage + ? sprintf(UPDATE_COMMENT_FORM.error, { reason: serverErrorMessage }, false) + : UPDATE_COMMENT_FORM.defaultError; + + return this.addAlert({ + message: alertMessage, + parent: $editingNote.get(0), }); } @@ -794,6 +799,8 @@ export default class Notes { const $note = $target.closest('.note'); const $currentlyEditing = $('.note.is-editing:visible'); + this.clearAlertWrapper(); + if ($currentlyEditing.length) { const isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); @@ -1860,14 +1867,14 @@ export default class Notes { // Submission successful! render final note element this.updateNote(data, $editingNote); }) - .catch(() => { + .catch((error) => { + $editingNote.addClass('is-editing fade-in-full').removeClass('being-posted fade-in-half'); // Submission failed, revert back to original note - $noteBodyText.html(escape(cachedNoteBodyText)); - $editingNote.removeClass('being-posted fade-in'); + $noteBodyText.html(cachedNoteBodyText); $editingNote.find('.gl-spinner').remove(); // Show Flash message about failure - this.updateNoteError(); + this.updateNoteError(error, $editingNote); }); return $closeBtn.text($closeBtn.data('originalText')); diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue index df50a30abb7..ff48bfceb29 100644 --- a/app/assets/javascripts/issuable/components/related_issuable_item.vue +++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue @@ -248,7 +248,7 @@ export default { size="small" :disabled="removeDisabled" class="js-issue-item-remove-button gl-mr-2" - data-qa-selector="remove_related_issue_button" + data-testid="remove_related_issue_button" :title="__('Remove')" :aria-label="__('Remove')" @click="onRemoveRequest" diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 229bc473db7..a36b0c46927 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -340,7 +340,6 @@ export default { class="gl-sm-display-none! w-100" block :text="dropdownText" - data-qa-selector="issue_actions_dropdown" data-testid="mobile-dropdown" :loading="isToggleStateButtonLoading" > @@ -364,7 +363,7 @@ export default { </gl-dropdown-item> <gl-dropdown-item v-if="showToggleIssueStateButton" - :data-qa-selector="`mobile_${qaSelector}`" + :data-testid="`mobile_${qaSelector}`" @click="toggleIssueState" > {{ buttonText }} @@ -434,7 +433,7 @@ export default { class="gl-display-none gl-sm-display-inline-flex!" :data-qa-selector="qaSelector" :loading="isToggleStateButtonLoading" - data-testid="toggle-button" + data-testid="toggle-issue-state-button" @click="toggleIssueState" > {{ buttonText }} @@ -447,7 +446,6 @@ export default { class="gl-display-none gl-sm-display-inline-flex!" icon="ellipsis_v" category="tertiary" - data-qa-selector="issue_actions_ellipsis_dropdown" :text="dropdownText" :text-sr-only="true" :title="dropdownText" @@ -510,7 +508,7 @@ export default { <gl-dropdown-item v-gl-modal="$options.deleteModalId" variant="danger" - data-qa-selector="delete_issue_button" + data-testid="delete_issue_button" @click="track('click_dropdown')" > {{ deleteButtonText }} diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue index 2d2ef327018..c464f48d574 100644 --- a/app/assets/javascripts/issues/show/components/title.vue +++ b/app/assets/javascripts/issues/show/components/title.vue @@ -60,7 +60,6 @@ export default { 'issue-realtime-trigger-pulse': pulseAnimation, }" class="title gl-font-size-h-display" - data-qa-selector="title_content" data-testid="issue-title" dir="auto" ></h1> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 5929e419247..dd135eaee3b 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -18,6 +18,7 @@ import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import { renderMarkdown } from '../utils'; +import { UPDATE_COMMENT_FORM } from '../i18n'; import { getStartLineNumber, getEndLineNumber, @@ -113,6 +114,7 @@ export default { isResolving: false, commentLineStart: {}, resolveAsThread: true, + oldContent: this.note.note_html, }; }, computed: { @@ -293,7 +295,7 @@ export default { updateSuccess() { this.isEditing = false; this.isRequesting = false; - this.oldContent = null; + this.oldContent = this.note.note_html; renderGFM(this.$refs.noteBody.$el); this.$emit('updateSuccess'); }, @@ -341,7 +343,6 @@ export default { // https://gitlab.com/gitlab-org/gitlab/-/issues/298827 if (!isEmpty(position)) data.note.note.position = JSON.stringify(position); this.isRequesting = true; - this.oldContent = this.note.note_html; // eslint-disable-next-line vue/no-mutating-props this.note.note_html = renderMarkdown(noteText); @@ -350,8 +351,8 @@ export default { this.updateSuccess(); callback(); }) - .catch((response) => { - if (response.status === HTTP_STATUS_GONE) { + .catch((e) => { + if (e.status === HTTP_STATUS_GONE) { this.removeNote(this.note); this.updateSuccess(); callback(); @@ -360,17 +361,22 @@ export default { this.isEditing = true; this.setSelectedCommentPositionHover(); this.$nextTick(() => { - this.handleUpdateError(response); // The 'response' parameter is being used in JH, don't remove it - this.recoverNoteContent(noteText); + this.handleUpdateError(e); // The 'e' parameter is being used in JH, don't remove it + this.recoverNoteContent(); callback(); }); } }); }, - handleUpdateError() { - const msg = __('Something went wrong while editing your comment. Please try again.'); + handleUpdateError(e) { + const serverErrorMessage = e?.response?.data?.errors; + + const alertMessage = serverErrorMessage + ? sprintf(UPDATE_COMMENT_FORM.error, { reason: serverErrorMessage.toLowerCase() }, false) + : UPDATE_COMMENT_FORM.defaultError; + createAlert({ - message: msg, + message: alertMessage, parent: this.$el, }); }, @@ -391,22 +397,14 @@ export default { }); if (!confirmed) return; } - if (this.oldContent) { - // eslint-disable-next-line vue/no-mutating-props - this.note.note_html = this.oldContent; - this.oldContent = null; - } + this.recoverNoteContent(); this.isEditing = false; this.$emit('cancelForm'); }), - recoverNoteContent(noteText) { - // we need to do this to prevent noteForm inconsistent content warning - // this is something we intentionally do so we need to recover the content - // eslint-disable-next-line vue/no-mutating-props - this.note.note = noteText; - const { noteBody } = this.$refs; - if (noteBody) { - noteBody.note.note = noteText; + recoverNoteContent() { + if (this.oldContent) { + // eslint-disable-next-line vue/no-mutating-props + this.note.note_html = this.oldContent; } }, getLineClasses(lineNumber) { diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js index 633466a5fd3..c25ca6b586d 100644 --- a/app/assets/javascripts/notes/i18n.js +++ b/app/assets/javascripts/notes/i18n.js @@ -55,3 +55,8 @@ export const EDITED_TEXT = { actionWithAuthor: __('%{actionText} %{actionDetail} %{timeago} by %{author}'), actionWithoutAuthor: __('%{actionText} %{actionDetail}'), }; + +export const UPDATE_COMMENT_FORM = { + error: __('Your comment could not be updated because %{reason}.'), + defaultError: __('Something went wrong while editing your comment. Please try again.'), +}; diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue index 7ecc39a56e7..b3033ddf3b6 100644 --- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue +++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue @@ -218,7 +218,7 @@ export default { type="submit" size="small" class="gl-mr-2" - data-qa-selector="add_issue_button" + data-testid="add_issue_button" > {{ __('Add') }} </gl-button> diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue index 1846b9cf8f4..f92c81a7eb2 100644 --- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue +++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -217,7 +217,7 @@ export default { :aria-label="inputPlaceholder" type="text" class="gl-w-full gl-border-none gl-outline-0" - data-qa-selector="add_issue_field" + data-testid="add_issue_field" autocomplete="off" @input="onInput" @focus="onFocus" diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue index 24b350c7f18..f672acda062 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_block.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue @@ -220,7 +220,6 @@ export default { <gl-button v-if="canAdmin" size="small" - data-qa-selector="related_issues_plus_button" data-testid="related-issues-plus-button" :aria-label="addIssuableButtonText" class="gl-ml-3" diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue index 63452f3eace..8d26917f749 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_list.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue @@ -104,7 +104,7 @@ export default { {{ heading }} </h4> <div class="related-issues-token-body" :class="{ 'sortable-container': canReorder }"> - <div v-if="isFetching" class="gl-mb-2" data-qa-selector="related_issues_loading_placeholder"> + <div v-if="isFetching" class="gl-mb-2" data-testid="related_issues_loading_placeholder"> <gl-loading-icon ref="loadingIcon" size="sm" @@ -146,7 +146,7 @@ export default { :locked-message="issue.lockedMessage" :work-item-type="issue.type" event-namespace="relatedIssue" - data-qa-selector="related_issuable_content" + data-testid="related_issuable_content" class="gl-mx-n2" @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)" /> diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 1680e42e5e4..2653748861b 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -157,7 +157,6 @@ export default { :data-track-action="tracking.event" :data-track-label="tracking.label" :data-track-property="tracking.property" - data-qa-selector="edit_link" @keyup.esc="toggle" @click="toggle" > diff --git a/app/components/diffs/base_component.rb b/app/components/diffs/base_component.rb index f5bc59cb314..9e1347d1e84 100644 --- a/app/components/diffs/base_component.rb +++ b/app/components/diffs/base_component.rb @@ -2,8 +2,6 @@ module Diffs class BaseComponent < ViewComponent::Base - warn_on_deprecated_slot_setter - # To make converting the partials to components easier, # we delegate all missing methods to the helpers, # where they probably are. diff --git a/app/components/layouts/horizontal_section_component.rb b/app/components/layouts/horizontal_section_component.rb index caeaa1782c0..48c960f17d9 100644 --- a/app/components/layouts/horizontal_section_component.rb +++ b/app/components/layouts/horizontal_section_component.rb @@ -2,8 +2,6 @@ module Layouts class HorizontalSectionComponent < ViewComponent::Base - warn_on_deprecated_slot_setter - # @param [Boolean] border # @param [Hash] options def initialize(border: true, options: {}) diff --git a/app/components/pajamas/component.rb b/app/components/pajamas/component.rb index a7b45ffd7fd..3b1826a646c 100644 --- a/app/components/pajamas/component.rb +++ b/app/components/pajamas/component.rb @@ -2,8 +2,6 @@ module Pajamas class Component < ViewComponent::Base - warn_on_deprecated_slot_setter - private # Filter a given a value against a list of allowed values diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 030b438afc7..7b2cf131fce 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -80,10 +80,16 @@ module NotesActions return end - prepare_notes_for_rendering([@note]) - respond_to do |format| - format.json { render json: note_json(@note) } + format.json do + if @note.errors.present? + render json: { errors: @note.errors.full_messages.to_sentence }, status: :unprocessable_entity + else + prepare_notes_for_rendering([@note]) + render json: note_json(@note) + end + end + format.html { redirect_back_or_default } end end diff --git a/app/finders/groups/environment_scopes_finder.rb b/app/finders/groups/environment_scopes_finder.rb new file mode 100644 index 00000000000..886be7881ee --- /dev/null +++ b/app/finders/groups/environment_scopes_finder.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Groups::EnvironmentsScopesFinder +# +# Arguments: +# group +# params: +# search: string +# +module Groups + class EnvironmentScopesFinder + DEFAULT_ENVIRONMENT_SCOPES_LIMIT = 100 + + def initialize(group:, params: {}) + @group = group + @params = params + end + + EnvironmentScope = Struct.new(:name) + + def execute + variables = group.variables + variables = by_name(variables) + variables = by_search(variables) + variables = variables.limit(DEFAULT_ENVIRONMENT_SCOPES_LIMIT) + environment_scope_names = variables.environment_scope_names + environment_scope_names.map { |environment_scope| EnvironmentScope.new(environment_scope) } + end + + private + + attr_reader :group, :params + + def by_name(group_variables) + if params[:name].present? + group_variables.by_environment_scope(params[:name]) + else + group_variables + end + end + + def by_search(group_variables) + if params[:search].present? + group_variables.for_environment_scope_like(params[:search]) + else + group_variables + end + end + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 879c94ae4d4..63f7616884f 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -103,13 +103,11 @@ class GroupsFinder < UnionFinder end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def by_search(groups) return groups unless params[:search].present? groups.search(params[:search], include_parents: params[:parent].blank?) end - # rubocop: enable CodeReuse/ActiveRecord def owned_groups current_user&.owned_groups || Group.none diff --git a/app/graphql/resolvers/group_environment_scopes_resolver.rb b/app/graphql/resolvers/group_environment_scopes_resolver.rb new file mode 100644 index 00000000000..61ccb2eefbb --- /dev/null +++ b/app/graphql/resolvers/group_environment_scopes_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + class GroupEnvironmentScopesResolver < BaseResolver + type Types::Ci::GroupEnvironmentScopeType.connection_type, null: true + + alias_method :group, :object + + argument :name, GraphQL::Types::String, + required: false, + description: 'Name of the environment scope.' + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search query for environment scope name.' + + def resolve(**args) + return unless group.present? + + ::Groups::EnvironmentScopesFinder.new(group: group, params: args).execute + end + end +end diff --git a/app/graphql/types/ci/group_environment_scope_connection_type.rb b/app/graphql/types/ci/group_environment_scope_connection_type.rb new file mode 100644 index 00000000000..ddbc00d3870 --- /dev/null +++ b/app/graphql/types/ci/group_environment_scope_connection_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class GroupEnvironmentScopeConnectionType < GraphQL::Types::Relay::BaseConnection + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/group_environment_scope_type.rb b/app/graphql/types/ci/group_environment_scope_type.rb new file mode 100644 index 00000000000..3a3a5a3f59f --- /dev/null +++ b/app/graphql/types/ci/group_environment_scope_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class GroupEnvironmentScopeType < BaseObject + graphql_name 'CiGroupEnvironmentScope' + description 'Ci/CD environment scope for a group.' + + connection_type_class(Types::Ci::GroupEnvironmentScopeConnectionType) + + field :name, GraphQL::Types::String, + null: true, + description: 'Scope name defininig the enviromnments that can use the variable.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 24326c131ce..5fd6ee948d3 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -83,6 +83,12 @@ module Types description: 'Merge requests for projects in this group.', resolver: Resolvers::GroupMergeRequestsResolver + field :environment_scopes, + Types::Ci::GroupEnvironmentScopeType.connection_type, + description: 'Environment scopes of the group.', + null: true, + resolver: Resolvers::GroupEnvironmentScopesResolver + field :milestones, description: 'Milestones of the group.', extras: [:lookahead], diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index f04f0d27e51..5522a01758f 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -23,6 +23,19 @@ module Ci scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) } scope :for_groups, ->(group_ids) { where(group_id: group_ids) } + scope :for_environment_scope_like, -> (query) do + top_level = 'LOWER(ci_group_variables.environment_scope) LIKE LOWER(?) || \'%\'' + search_like = "%#{sanitize_sql_like(query)}%" + + where(top_level, search_like) + end + + scope :environment_scope_names, -> do + group(:environment_scope) + .order(:environment_scope) + .pluck(:environment_scope) + end + self.limit_name = 'group_ci_variables' self.limit_scope = :group diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index ff9f39ce0be..52940281018 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -9,6 +9,8 @@ module Notes note.assign_attributes(params) + return note unless note.valid? + track_note_edit_usage_for_issues(note) if note.for_issue? track_note_edit_usage_for_merge_requests(note) if note.for_merge_request? @@ -23,11 +25,7 @@ module Notes note.note = content end - if note.note_changed? - note.assign_attributes(last_edited_at: Time.current, updated_by: current_user) - note.check_for_spam(action: :update, user: current_user) unless only_commands - end - + update_note(note, only_commands) note.save unless only_commands || note.for_personal_snippet? @@ -56,6 +54,13 @@ module Notes private + def update_note(note, only_commands) + return unless note.note_changed? + + note.assign_attributes(last_edited_at: Time.current, updated_by: current_user) + note.check_for_spam(action: :update, user: current_user) unless only_commands + end + def delete_note(note, message) # We must add the error after we call #save because errors are reset # when #save is called diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 09162e6a349..ee1ca364b07 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -46,7 +46,7 @@ .js-sidebar-milestone-widget-root{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } } - if in_group_context_with_iterations - .block.gl-collapse-empty{ data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }< + .block.gl-collapse-empty{ data: { testid: 'iteration_container' } }< = render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type - if issuable_sidebar[:show_crm_contacts] diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index 72081856da6..40a71aa53dc 100644 --- a/app/views/shared/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -2,6 +2,7 @@ = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do = hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_type, '', class: 'js-form-target-type' + .flash-container = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do = render 'shared/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', qa_selector: 'edit_note_field', placeholder: _("Write a comment or drag your files hereā¦") = render 'shared/notes/hints' |