diff --git a/Gemfile b/Gemfile index ac343c234e9..08295f7ac01 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,7 @@ gem 'responders', '~> 3.0' gem 'sprockets', '~> 3.7.0' -gem 'view_component', '~> 2.82.0' +gem 'view_component', '~> 3.1.0' # Supported DBs gem 'pg', '~> 1.5.3' diff --git a/Gemfile.checksum b/Gemfile.checksum index 638081e93c3..94b48126b42 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -673,7 +673,7 @@ {"name":"validates_hostname","version":"1.0.11","platform":"ruby","checksum":"d506bae0342ec14c920eb319e057fc1886c321a59b85b4b6e966ee4b88fab8c3"}, {"name":"version_gem","version":"1.1.0","platform":"ruby","checksum":"6b009518020db57f51ec7b410213fae2bf692baea9f1b51770db97fbc93d9a80"}, {"name":"version_sorter","version":"2.3.0","platform":"ruby","checksum":"2147f2a1a3804fbb8f60d268b7d7c1ec717e6dd727ffe2c165b4e05e82efe1da"}, -{"name":"view_component","version":"2.82.0","platform":"ruby","checksum":"c8b69561e19d8f9c4ba8c16df7f3bf2a5f0bba439dd25a597948e118f6bc9baf"}, +{"name":"view_component","version":"3.1.0","platform":"ruby","checksum":"ead1ba472aca31fa9811e513da9b803b45ef555b717574d7bf0dda39ae880e54"}, {"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"}, {"name":"vmstat","version":"2.3.0","platform":"ruby","checksum":"ab5446a3e3bd0a9cdb9d9ac69a0bbd119c4f161d945a0846a519dd7018af656d"}, {"name":"warden","version":"1.2.9","platform":"ruby","checksum":"46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0"}, diff --git a/Gemfile.lock b/Gemfile.lock index 1464b07af2e..236b24cd245 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1608,7 +1608,7 @@ GEM activesupport (>= 3.0) version_gem (1.1.0) version_sorter (2.3.0) - view_component (2.82.0) + view_component (3.1.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) @@ -1963,7 +1963,7 @@ DEPENDENCIES valid_email (~> 0.1) validates_hostname (~> 1.0.11) version_sorter (~> 2.3) - view_component (~> 2.82.0) + view_component (~> 3.1.0) vmstat (~> 2.3.0) warning (~> 1.3.0) webauthn (~> 3.0) 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 { {{ 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 { {{ 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" > 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') }} 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 {