From 1631d8a2e0eef291f1d0b9486ee35ed6b52a176a Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 24 Feb 2021 21:11:16 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 1 - .../diffs/components/settings_dropdown.vue | 40 +++-- app/assets/javascripts/diffs/i18n.js | 1 + app/assets/javascripts/issue.js | 11 +- .../pages/projects/compare/show/index.js | 3 + .../pipelines_list/pipelines_actions.vue | 1 + .../pipelines_list/pipelines_artifacts.vue | 2 + .../compare/components/revision_dropdown.vue | 2 +- .../components/related_issuable_input.vue | 1 + .../labels_select_vue/dropdown_value.vue | 5 +- .../labels_select_vue/labels_select_root.vue | 6 + .../projects/compare_controller.rb | 2 +- app/helpers/compare_helper.rb | 16 ++ app/views/projects/compare/_form.html.haml | 28 ---- app/views/projects/compare/index.html.haml | 15 +- app/views/projects/compare/show.html.haml | 3 +- .../300853-convert-compare-show-to-vue.yml | 5 + ...5-performance-pipeline-table-dropdowns.yml | 5 + .../bugfix-linked-issue-autocomplete.yml | 5 + ...gham-add-branch-to-dast-profile-322526.yml | 5 + ...lace-bold-label-in-diffs-dropdown-menu.yml | 5 + ...3053451_add_branch_name_to_dast_profile.rb | 23 +++ db/schema_migrations/20210223053451 | 1 + db/structure.sql | 2 + doc/development/README.md | 1 + doc/development/appsec/index.md | 32 ++++ doc/development/database/index.md | 1 + .../database/setting_multiple_values.md | 59 ++++--- doc/development/fe_guide/vuex.md | 11 +- doc/development/migration_style_guide.md | 7 +- .../img/comment-on-any-diff-line.png | Bin 33199 -> 0 bytes .../img/comment-on-any-diff-line_v13_10.png | Bin 0 -> 21304 bytes .../reviewing_and_managing_merge_requests.md | 26 ++-- locale/gitlab.pot | 12 -- package.json | 2 +- .../user_toggles_whitespace_changes_spec.rb | 4 +- .../projects/merge_request_button_spec.rb | 6 +- .../components/settings_dropdown_spec.js | 147 +++++++----------- spec/frontend/issue_spec.js | 133 ++++++++-------- .../pipelines/pipelines_actions_spec.js | 6 +- .../pipelines/pipelines_artifacts_spec.js | 17 +- .../pipelines/pipelines_table_row_spec.js | 11 +- .../labels_select_vue/dropdown_value_spec.js | 44 +++--- .../sidebar/labels_select_vue/mock_data.js | 1 + yarn.lock | 2 +- 45 files changed, 386 insertions(+), 324 deletions(-) delete mode 100644 app/views/projects/compare/_form.html.haml create mode 100644 changelogs/unreleased/300853-convert-compare-show-to-vue.yml create mode 100644 changelogs/unreleased/321845-performance-pipeline-table-dropdowns.yml create mode 100644 changelogs/unreleased/bugfix-linked-issue-autocomplete.yml create mode 100644 changelogs/unreleased/philipcunningham-add-branch-to-dast-profile-322526.yml create mode 100644 changelogs/unreleased/tor-defect-out-of-place-bold-label-in-diffs-dropdown-menu.yml create mode 100644 db/migrate/20210223053451_add_branch_name_to_dast_profile.rb create mode 100644 db/schema_migrations/20210223053451 create mode 100644 doc/development/appsec/index.md delete mode 100644 doc/user/project/merge_requests/img/comment-on-any-diff-line.png create mode 100644 doc/user/project/merge_requests/img/comment-on-any-diff-line_v13_10.png diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index d928c645a6b..ac4763103b2 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -298,7 +298,6 @@ rules: - <<: *if-not-canonical-namespace when: never - - <<: *if-master-refs - changes: *ci-build-images-patterns - changes: *code-qa-patterns diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue index acba0c5c8af..b1f8e583c1f 100644 --- a/app/assets/javascripts/diffs/components/settings_dropdown.vue +++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue @@ -26,6 +26,9 @@ export default { toggleFileByFile() { this.setFileByFile({ fileByFile: !this.viewDiffsFileByFile }); }, + toggleWhitespace(updatedSetting) { + this.setShowWhitespace({ showWhitespace: updatedSetting, pushState: true }); + }, }, }; @@ -80,26 +83,21 @@ export default { -
- -
-
- - {{ $options.i18n.fileByFile }} - -
+ + {{ $options.i18n.whitespace }} + + + {{ $options.i18n.fileByFile }} + diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js index 2a061876937..b2354af1eec 100644 --- a/app/assets/javascripts/diffs/i18n.js +++ b/app/assets/javascripts/diffs/i18n.js @@ -21,5 +21,6 @@ export const DIFF_FILE = { }; export const SETTINGS_DROPDOWN = { + whitespace: __('Show whitespace changes'), fileByFile: __('Show one file at a time'), }; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 3f25682ab8b..8922c53564b 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -6,6 +6,9 @@ import axios from './lib/utils/axios_utils'; import { addDelimiter } from './lib/utils/text_utility'; import { __ } from './locale'; +// TODO: Update all references of "issuable_vue_app:change" https://gitlab.com/gitlab-org/gitlab/-/issues/322760 +export const EVENT_ISSUABLE_VUE_APP_CHANGE = 'issuable_vue_app:change'; + export default class Issue { constructor() { if ($('.js-alert-moved-from-service-desk-warning').length) { @@ -23,9 +26,13 @@ export default class Issue { } // Listen to state changes in the Vue app - document.addEventListener('issuable_vue_app:change', (event) => { + this.issuableVueAppChangeHandler = (event) => this.updateTopState(event.detail.isClosed, event.detail.data); - }); + document.addEventListener(EVENT_ISSUABLE_VUE_APP_CHANGE, this.issuableVueAppChangeHandler); + } + + dispose() { + document.removeEventListener(EVENT_ISSUABLE_VUE_APP_CHANGE, this.issuableVueAppChangeHandler); } /** diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js index f1cf9caa28b..549e596cb8d 100644 --- a/app/assets/javascripts/pages/projects/compare/show/index.js +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -1,6 +1,9 @@ import Diff from '~/diff'; import GpgBadges from '~/gpg_badges'; import initChangesDropdown from '~/init_changes_dropdown'; +import initCompareSelector from '~/projects/compare'; + +initCompareSelector(); document.addEventListener('DOMContentLoaded', () => { new Diff(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue index 6890cbb9bed..b94f1a42039 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -82,6 +82,7 @@ export default { :loading="isLoading" data-testid="pipelines-manual-actions-dropdown" right + lazy icon="play" > signatures_namespace_project_compare_index_path } do - .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown - .input-group.inline-input-group - %span.input-group-prepend - .input-group-text - = s_("CompareBranches|Source") - = hidden_field_tag :to, params[:to] - = button_tag type: 'button', title: params[:to], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do - .dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag") - = sprite_icon('chevron-down', css_class: 'float-right') - = render 'shared/ref_dropdown' - .compare-ellipsis.inline ... - .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown - .input-group.inline-input-group - %span.input-group-prepend - .input-group-text - = s_("CompareBranches|Target") - = hidden_field_tag :from, params[:from] - = button_tag type: 'button', title: params[:from], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag") - = sprite_icon('chevron-down', css_class: 'float-right') - = render 'shared/ref_dropdown' -   - = button_tag s_("CompareBranches|Compare"), class: "btn gl-button btn-success commits-compare-btn" - - if @merge_request.present? - = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'gl-ml-3 btn' - - elsif create_mr_button? - = link_to _("Create merge request"), create_mr_path, class: 'gl-ml-3 btn gl-button' diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 39d4a3b2eb2..e3ab184ec6f 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -13,17 +13,4 @@ = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: ''.html_safe, b_close: ''.html_safe } .prepend-top-20 - - if Feature.enabled?(:compare_repo_dropdown) - #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), - refs_project_path: refs_project_path(@project), - params_from: params[:from], params_to: params[:to], - project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', - create_mr_path: create_mr_button? ? create_mr_path : '', - project_to: { id: @project.id, name: @project.full_path }.to_json, - projects_from: target_projects(@project).map { |project| { id:project.id, name: project.full_path } }.to_json } } - - else - #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), - refs_project_path: refs_project_path(@project), - params_from: params[:from], params_to: params[:to], - project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', - create_mr_path: create_mr_button? ? create_mr_path : '' } } + #js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) } diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 51cf95dc84b..9e9c271e7be 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -2,7 +2,8 @@ - page_title "#{params[:from]}...#{params[:to]}" .sub-header-block.no-bottom-space - = render "form" + .js-signature-container{ data: { 'signatures-path' => signatures_namespace_project_compare_index_path } } + #js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) } - if @commits.present? = render "projects/commits/commit_list" diff --git a/changelogs/unreleased/300853-convert-compare-show-to-vue.yml b/changelogs/unreleased/300853-convert-compare-show-to-vue.yml new file mode 100644 index 00000000000..2926ef2682b --- /dev/null +++ b/changelogs/unreleased/300853-convert-compare-show-to-vue.yml @@ -0,0 +1,5 @@ +--- +title: Restyle the repository compare show page +merge_request: 53523 +author: +type: changed diff --git a/changelogs/unreleased/321845-performance-pipeline-table-dropdowns.yml b/changelogs/unreleased/321845-performance-pipeline-table-dropdowns.yml new file mode 100644 index 00000000000..0fac7a05f7a --- /dev/null +++ b/changelogs/unreleased/321845-performance-pipeline-table-dropdowns.yml @@ -0,0 +1,5 @@ +--- +title: Reduce elements in Pipeline page dropdowns with lazy +merge_request: 54674 +author: +type: changed diff --git a/changelogs/unreleased/bugfix-linked-issue-autocomplete.yml b/changelogs/unreleased/bugfix-linked-issue-autocomplete.yml new file mode 100644 index 00000000000..20b36d33bbd --- /dev/null +++ b/changelogs/unreleased/bugfix-linked-issue-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Improve Linked Issues Usability +merge_request: 50879 +author: Andrew Minion +type: changed diff --git a/changelogs/unreleased/philipcunningham-add-branch-to-dast-profile-322526.yml b/changelogs/unreleased/philipcunningham-add-branch-to-dast-profile-322526.yml new file mode 100644 index 00000000000..4ad204b7cd1 --- /dev/null +++ b/changelogs/unreleased/philipcunningham-add-branch-to-dast-profile-322526.yml @@ -0,0 +1,5 @@ +--- +title: Add branch_name to dast_profiles table +merge_request: 54891 +author: +type: added diff --git a/changelogs/unreleased/tor-defect-out-of-place-bold-label-in-diffs-dropdown-menu.yml b/changelogs/unreleased/tor-defect-out-of-place-bold-label-in-diffs-dropdown-menu.yml new file mode 100644 index 00000000000..8525204ee7e --- /dev/null +++ b/changelogs/unreleased/tor-defect-out-of-place-bold-label-in-diffs-dropdown-menu.yml @@ -0,0 +1,5 @@ +--- +title: Fix bold text mismatch in MR ⚙ menu +merge_request: 54531 +author: +type: fixed diff --git a/db/migrate/20210223053451_add_branch_name_to_dast_profile.rb b/db/migrate/20210223053451_add_branch_name_to_dast_profile.rb new file mode 100644 index 00000000000..311e809103f --- /dev/null +++ b/db/migrate/20210223053451_add_branch_name_to_dast_profile.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AddBranchNameToDastProfile < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + with_lock_retries do + add_column :dast_profiles, :branch_name, :text + end + + add_text_limit :dast_profiles, :branch_name, 255 + end + + def down + with_lock_retries do + remove_column :dast_profiles, :branch_name + end + end +end diff --git a/db/schema_migrations/20210223053451 b/db/schema_migrations/20210223053451 new file mode 100644 index 00000000000..ad40bb0fa71 --- /dev/null +++ b/db/schema_migrations/20210223053451 @@ -0,0 +1 @@ +1266bf92f23a42d96778bf546534882f03d2388f22640e4cfaa2a9a1aad19093 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b829666c1d3..c3b46a15098 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11634,7 +11634,9 @@ CREATE TABLE dast_profiles ( updated_at timestamp with time zone NOT NULL, name text NOT NULL, description text NOT NULL, + branch_name text, CONSTRAINT check_5fcf73bf61 CHECK ((char_length(name) <= 255)), + CONSTRAINT check_6c9d775949 CHECK ((char_length(branch_name) <= 255)), CONSTRAINT check_c34e505c24 CHECK ((char_length(description) <= 255)) ); diff --git a/doc/development/README.md b/doc/development/README.md index 3d5335feb11..c25782dbf84 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -287,6 +287,7 @@ See [database guidelines](database/index.md). ## Domain-specific guides - [CI/CD development documentation](cicd/index.md) +- [AppSec development documentation](appsec/index.md) ## Other Development guides diff --git a/doc/development/appsec/index.md b/doc/development/appsec/index.md new file mode 100644 index 00000000000..e8ce885e75d --- /dev/null +++ b/doc/development/appsec/index.md @@ -0,0 +1,32 @@ +--- +stage: Secure, Protect +group: all +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: index, dev, reference +--- + +# Application Security development documentation + +Development guides that are specific to the stages that work on Application Security features are listed here. + +Please go to [Application Security](../../user/application_security/index.md) if you are looking for documentation on how to use those features. + +## Namespaces + +Application Security code in the Rails monolith is organized into the following namespaces, which generally follows +the feature categories in the [Secure](https://about.gitlab.com/stages-devops-lifecycle/secure/) and [Protect](https://about.gitlab.com/stages-devops-lifecycle/protect/) stages. + +- `AppSec`: shared code. + - `AppSec::ContainerScanning`: Container Scanning code. + - `AppSec::Dast`: DAST code. + - `AppSec::DependencyScanning`: Dependency Scanning code. + - `AppSec::Fuzzing::Api`: API Fuzzing code. + - `AppSec::Fuzzing::Coverage`: Coverage Fuzzing code. + - `AppSec::Fuzzing`: Shared fuzzing code. + - `AppSec::LicenseCompliance`: License Compliance code. + - `AppSec::Sast`: SAST code. + - `AppSec::SecretDetection`: Secret Detection code. + - `AppSec::VulnMgmt`: Vulnerability Management code. + +Most AppSec code does not conform to these namespace guidelines. When developing, make an effort +to move existing code into the appropriate namespace whenever possible. diff --git a/doc/development/database/index.md b/doc/development/database/index.md index 367ef455898..870ae1542bd 100644 --- a/doc/development/database/index.md +++ b/doc/development/database/index.md @@ -69,3 +69,4 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Miscellaneous - [Maintenance operations](maintenance_operations.md) +- [Update multiple database objects](setting_multiple_values.md) diff --git a/doc/development/database/setting_multiple_values.md b/doc/development/database/setting_multiple_values.md index 54870380047..0f23aae9f79 100644 --- a/doc/development/database/setting_multiple_values.md +++ b/doc/development/database/setting_multiple_values.md @@ -4,24 +4,22 @@ group: Database info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Setting Multiple Values +# Update multiple database objects > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32921) in GitLab 13.5. -There's often a need to update multiple objects with new values for one -or more columns. One method of doing this is using `Relation#update_all`: +You can update multiple database objects with new values for one or more columns. +One method is to use `Relation#update_all`: ```ruby user.issues.open.update_all(due_date: 7.days.from_now) # (1) user.issues.update_all('relative_position = relative_position + 1') # (2) ``` -But what do you do if you cannot express the update as either a static value (1) -or as a calculation (2)? - -Thankfully we can use `UPDATE FROM` to express the need to update multiple rows -with distinct values in a single query. One can either use a temporary table, or -a Common Table Expression (CTE), and then use that as the source of the updates: +If you cannot express the update as either a static value (1) or as a calculation (2), +use `UPDATE FROM` to express the need to update multiple rows with distinct values +in a single query. Create a temporary table, or a Common Table Expression (CTE), +and use it as the source of the updates: ```sql with updates(obj_id, new_title, new_weight) as ( @@ -34,23 +32,22 @@ update issues where id = obj_id ``` -The bad news: there is no way to express this in ActiveRecord or even dropping -down to ARel. The `UpdateManager` does not support `update from`, so this -is not expressible. - -The good news: we supply an abstraction to help you generate these kinds of -updates, called `Gitlab::Database::BulkUpdate`. This constructs queries such as the -above, and uses binding parameters to avoid SQL injection. +You can't express this in ActiveRecord, or by dropping down to [Arel](https://api.rubyonrails.org/v6.1.0/classes/Arel.html), +because the `UpdateManager` does not support `update from`. However, we supply +an abstraction to help you generate these kinds of updates: `Gitlab::Database::BulkUpdate`. +This abstraction constructs queries like the previous example, and uses +binding parameters to avoid SQL injection. ## Usage -To use this, we need: +To use `Gitlab::Database::BulkUpdate`, we need: -- the list of columns to update -- a mapping from object/ID to the new values to set for that object -- a way to determine the table for each object +- The list of columns to update. +- A mapping from the object (or ID) to the new values to set for that object. +- A way to determine the table for each object. -For example, we can express the query above as: +For example, we can express the example query in a way that determines the +table by calling `object.class.table_name`: ```ruby issue_a = Issue.find(..) @@ -63,10 +60,7 @@ issue_b = Issue.find(..) }) ``` -Here the table can be determined automatically, from calling -`object.class.table_name`, so we don't need to provide anything. - -We can even pass heterogeneous sets of objects, if the updates all make sense +You can even pass heterogeneous sets of objects, if the updates all make sense for them: ```ruby @@ -82,8 +76,8 @@ merge_request = MergeRequest.find(..) }) ``` -If your objects do not return the correct model class (perhaps because they are -part of a union), then we need to specify this explicitly in a block: +If your objects do not return the correct model class, such as if they are part +of a union, then specify the model class explicitly in a block: ```ruby bazzes = params @@ -103,7 +97,10 @@ end ## Caveats -Note that this is a **very low level** tool, and operates on the raw column -values. Enumerations and state fields must be translated into their underlying -representations, for example, and nested associations are not supported. No -validations or hooks are called. +This tool is **very low level**, and operates directly on the raw column +values. You should consider these issues if you implement it: + +- Enumerations and state fields must be translated into their underlying + representations. +- Nested associations are not supported. +- No validations or hooks are called. diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index cc1d9ccab77..d44ab64ae5d 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -440,12 +440,11 @@ components, we need to include the store and provide the correct state: //component_spec.js import Vue from 'vue'; import Vuex from 'vuex'; -import { mount, createLocalVue } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { createStore } from './store'; import Component from './component.vue' -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('component', () => { let store; @@ -455,7 +454,6 @@ describe('component', () => { store = createStore(); wrapper = mount(Component, { - localVue, store, }); }; @@ -483,6 +481,11 @@ describe('component', () => { }); ``` +Some test files may still use the +[deprecated `createLocalVue` function](https://gitlab.com/gitlab-org/gitlab/-/issues/220482) +from `@vue/test-utils` and `localVue.use(Vuex)`. This is unnecessary, and should be +avoided or removed when possible. + ### Two way data binding When storing form data in Vuex, it is sometimes necessary to update the value stored. The store diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index e1205346585..759b19db36f 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -605,9 +605,10 @@ When adding a foreign-key constraint to an existing column in a non-empty table, we have to employ `add_concurrent_foreign_key` and `add_concurrent_index` instead of `add_reference`. -For an empty table (such as a fresh one), it is recommended to use -`add_reference` in a single-transaction migration, combining it with other -operations that don't require `disable_ddl_transaction!`. +If you have a new or empty table that doesn't reference a +[high-traffic table](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/rubocop-migrations.yml#L3), +we recommend that you use `add_reference` in a single-transaction migration. You can +combine it with other operations that don't require `disable_ddl_transaction!`. You can read more about adding [foreign key constraints to an existing column](database/add_foreign_key_to_existing_column.md). diff --git a/doc/user/project/merge_requests/img/comment-on-any-diff-line.png b/doc/user/project/merge_requests/img/comment-on-any-diff-line.png deleted file mode 100644 index cff5acb98ef16d6e1380a792128d1de412ce4f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33199 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

OzofPAd7S^i% zdy!4UTUQZQuSZ(-veEtuQ~yM@Rnwz-5(mHh=$u+Yqif`sB=OQ@s|}-nhP5hSq*Vr? z-+A_@?Q!eu!y`NAaTJf3g@M#mpo+5>qNvv`W#i<_lt|@&cThzPSn*~^)XsHbTHJDg zfi$QCJysxDbqDx;4P$Gvnl*SRrVfmG6d05>^*_q+f+Psz9eeSZX!-g3a5$aZ>CF*( z>MbMxLZwrH&>y{SdJ7VWhyNJ1z`F~#uYHML7mA0btM+pZf^N-vtY%FY4 z!l)lVeiU>vv*1^il>RUHYfFgA%Ejd;KP#)7n;VN8CyRrVB`Z50A0I0l2P+2$^Xmv^ zXAgT9V|QkIXX<}5`ENdw=FX;0)<0dW9qd2;!`Ilv;g^dL71ck2{^$6&p62e>|0~Jf z`M=D1HIVh69#(c1HrD?e%*EQ`{{j1_=ijh@+4XO6g8wk%SGIOHx7CrfwllYPepOAF zlbuKKUt<1`p8r+!Kakr0i{#G`Wi`LR6*AN$qHfAj6>P{*F-`2 zDXZfQ2Zx6D&lespBa09Y4iQdSL0w9ef&GONE~%g(FS8k2QU#5V;h)5!=YT??FE1~v ztE(L=kEpy77Z(>MC5OfRduI@+g@pxdb|N%1G$AjK&~-d9KVQMd`QhP#wjdpj;Y&<* zc6?4QJPR*n)C?RWw*)hn7$YYNw`gE;%JcKfFIRVLDb>)lbf36*DRv%DZ=YvsI3x}s zYb)z-LK5zgG5Df#5lP9|G8!*d3Tl$_xWY2>ydp-LdV_<5QEBPT&CPOF_Ed7JcXxMo zHg;ez*f}ghOJ12yQNu1U#NES_Q`6w)=El_9msngeAS}Yc{wJ1zq=m&mOlFpup{az4 zRb*ynbaeF6(vs{K0bEsmbbg8J>+7J%sNcVTPfblJi%E0q7&$vR`v-?qR#w7bFkK~e zBYnfdf&yVfi?Pu$MkO69Gb=_$#^mJW(75=7gan_!;Mmw$GJoIR-d;8}eG*U48U{^H3Xxs5jD>+mbcD6P zui8&5BM$(kAR|Rsuz|Z1E-r3KNim^yGBq`|w_gAf64D2K6Nlh1YI$LNqrj}JEI&U# za^H66upkQ)i_glE7&g6x5?@^HQ}*}wLtB; z_|r+r)``F@0#Cz$M^EMTNKg8GFHB4k(3h5$wz;uECe3@**G_Ej#LL6y6A*OXSV7`e z1#rt6axjiTgCq6)t!`khnZ9_fz-$I^-_;cxS%=(=^+Q>ZiJgmL+xLK>UW3I2;eo|R zC7G1U`o+AoIiK;be;Hf^{>;e8xQX}6PHQA~`Nhu8zM2xba(Mgv5pF0s`u8_sOlcLP z;6G2VUYwqM+*(a0d=nd9zJLC}?%JLIcRwPrRWy8}x2~o;$g_I<#G!Pfb>U7XZmxHn zJ&f8|l(HaClA+<4{S*%FBivU>F?ILllWd{bqjvm|Sj`=;*b>QbGrIbu_g`@szLbmO z_+!hy2P&&j9VMYRzJCvaJVicuxIHjZp!Z{EFFo#N9lP1|o0+h(uEb`WhobvF9S|H( zy{sQ9nV8QZ!Y&^{z76$9{?GUC#-R?~l%dJlH_T8?d_ZJZveb)V^fUP9g>SQlTWH|3 zHL>;}6I2OkXxQqmmn7j$78CSz@X>ARvdP|n6x#>e(;PW!yR=kE7RH5;jgKoH(8~C- zq~7UJzwh48g&AYqYD-T3CqpwM>1OIHVMw$ZS%-aFI)y-My7%9f1;xz%k_A>PLEO3Y z8YI>+XNI23)S-GTFzam%V}{51wupFhjq(Nh?ipWBL2kr1BXajD5b$s}a#zj$2Q&YU zeEH1ZzF~xrBP+~a=NW0^d?Owrjh5Qo6(dH)4 zFx>UW3)Sh4w5g^tb{7TOGK}a-dHyjd%{H4zRBRB@Z;{Ri{bK+TJD^jB>T{2}K*Ht< z99<%Y-vJkJ10fk)3dbp4JkrRKEr1SpQ2T(|gR{zOk)vn|h!;&v52YOpkR%L4QQ9S& zRoK$$p+Mc2eOHl(u84)Z!-YTfwysLS5MEqmN&H)|z@d^se(nwqiHU!FnL8Obyn9a<8 zzVE?K|B8;`N^SX0Moh#6fWpDos;4!vCXnij_-dsFx|fCWNta}|pJGqVFZKH1pQ%Yn z!N(Y+I5AF>Z=mHrz_XdE=(Kd}^f<~=);~kuLeDu8v_(mGH#go1@{K2ihswk&nZf-4 zYQ-2)QbBTl8#A=00E;EVdG*M&M`9)b?L)@uJ+5T)oHTcRXy2iO$6va7Ziz`|P_hEm zpF|lF1-YB!emT*eY{{RY#qf?Ob~H7HVfoq)z80!Zr$6#u5Q4 zg|C)Mcf7ASj({u|5s#O3S9+THN4NLmnAjSlm8%qiD3lgA19rG63(519a7^z$R-V|8 zFoNAiLHc(Jezb=9u{bvjwgs#qBg8--!ZFSTVgRbv+!W}+JMd#~qYobJYZ-6@+wlkw z>TsVKmDLgLWKpQK)FLY*Z^A`qV&3%R9VSdhQES!JLz#qiggRQ4yl^-k`XJ0vg4N?# zk=lQZ1EpE)3T0#>XT>w<%8^ZewI>eFuL+?{8x5j^+En0 zgOJ(B5Ht-7Y7ef70iPwz(3k0OP%cx)h+%NMR2X_rA-bsucJrbyV4Q~W;6plyBXQB< zaqnOM0PZX;>5;GQy-4^2so+QQ%<)ap2<FS@-}Qg}gu5u*QR}a+1(Hjrj8pDoj8F zJ9N2i^xDO|nZz`2691KBgZYFiy|97ujGHFwtUmGZmLJ%5?aR4kn8^zzOMh<_O+0rI zD~j9dKV%P+LFr^P$_U0*2NpY-em>_w1=ziu-Jlr5uQ$PzHqg3n-FR^-;t6Yq^b2^T z6K`%ND0m58I4MvB!vdfuGW6O?r5L((BVo5}&BotA2Pg2MgD%ZmKBRLP1&Uf^rC3c1pEPoSoCURBX7Gtf8&QmU#WlT82f!ZrbjyP@HkGrA3Ap}f%5eKsZhU7S;& z>2E$N`xifIT}|6lUf#LVG%bA@GNTDHa*oJM@D2_0ok-H*s*ZmY9xx~?gVaNX*GvCY z`T#18S4UW_(CAxa_TzY$87WkmsT&(yOM1cb0s3Z$&OQ9H9SF9WF%$743E}i#cM{*p z$-uiqM_F~>EW!o&shQ5mE>#c*iH*nPu9{icloAADfk^ucc0WM z14UVJ=RKvb=JxAro15A=f<%AZEw`BI_OVg#M0+7DuZyl-3Sy2E7Snh-)pCQLflx^5v8uFPQusA?Wb-7#dcZNRq)Pfk1!hn$H!Ybz)0p+0$Yl9AN!yA&?Mr+lM+@(`iVu2iC z@mu-TD(Nxk(w2=}2;c>MY9sP_qiF4f7Vg{OWvK{43sXeGvlPkB*LH|+b!N9VKI}PC zmxmepk_*<;f&9-bXQRNu9Mtj;sk-VJ1>BO;NbAyt)`i&Sg~?dCp6`2CRDHhvxX|lT{JHYevsf^Pr z=mX%o$gbY)+To>3{Ek#p6CkxAXw6t~`#ZD4&!_ai34eyLEly($M7z~q1wS)8uP80Y zthIIj%6&$fIOD6T@(g=kOKkQ&ZhyG9-dz_8P1ayVB&P<*r!-R_03JcFY=k6VexF#n zS202H0gRDdQX8WM4xpBLupXqE399)zt7CknCMW+x-)OLOPW;|>kNG;K`kcIt=0xyP zW4{`_b=Z>X@pvPpu)S7#_ITu_I^lLP%6iuMh-*~1PjGj8v9$X4m-F2Nom2aP6}`n~ z-RTC(QOic=ijG)%NcZRppWw4VJ=DwFSDer5-sVgpCGyeYk;3cXYDPNYolzy`+;@7u zkGl9*5`!f;SbTysvf2j5CI;{rl#;iL{z1H=^M&>;3MbyAzW+5-UQ+|T(VCkxzi=C(S4HM^Z);tkg%2?! zxN8UIj%;-UMzaLA*qgtc`I0$$x3#hX#b=X$aMNRkdJ_}W$4Cy`Rq%Jc&t-5JNHvNL z)BCX3)`~lS=Co(dKwEn({P?AOGWwcBJVQPZhtwok7SV2)z)86EmIhjYK$RM+_0g)* zkC?Lemc*p3MiM}MvOJ;t_Y*wk57(7ii-x5unwut$RK^I7R^cu3x?l#HzE7sieHP(f z`BwQL43@mIUxa^y^K?E4K38ExOdZjyrA6P4as(Fs9VR}u+5CUJR)-x zTVBRmVd!C?)O9Tg-H!5b0!f9XNc%gN^CsrQp|9GJSGpH%8T*RNO`cO5^eB#CLRZef zvcg>HxF-j{yg1cvM^?13*C$eozi<~e)QWXk&&CUkipZSDs?jiG z#oxM(!LBwbXU5Ip56w)MN0V$r1-!ShKf8K3md=1N>JntwlNT<2h4k)?n)ygj4}21= zrPpdma?^#xbx&GQCd^4>_udC_0N)}A2y9L_0z*-U^(pr^i5foV5!2ary8&H_CXs&; zgG8eOOsc#^FPA@9Wv<-XEZLeH?s8%!j}b``=^Og-NqN~JjzW-~t;MRO7MX$z=?T$0 ze{8ybb2kIkmpA{GK-d3xwnz8J`Ku!jH<3SFFCuu zR!R17PL0RiTLxlX={U~JYu=DovOR3HFV4*}jgmeaL0le*Qn5+tZZFqlEL8`koEGwgFFGfjBc z_+Av%8#g{t3aoiO?L@<2vyb!aOV*@dMhORYhzF>m+#bRu06o)J-5N%83J3A@q%g!K zywyc+_)(1q^id(1ZXGrPDm+l5ykGe*c-ba00)>9h zOh+d?X&kx{PTAnc!HPfL-)Bymo99#^rcGM>px4W7()ED~UOHR-YWnV7XuD_BAAVE+ zB^F;il6`6iQfC-eZ*y*vKQ|Uk&x&$85LJ}L4rt^srVNh;XaATjUAt{tb1rV!JYn~! z;c2+!8#J^KEG(&lJCeuXw7^y67_Vbm8$JBYwIQA$E>4b14snhN)CHxAQE zZm`bdX(K!6fCK&UP2L1tGB^LkhKB%bhOG*C-bl86gEZ~ycDZcXm>i3$L~rwOH<#+y z*fJEyL|?0pR1$qV*hD=?u?@D2B0n#>Micm9TW&2yWW@))9qM$5OJ-n%Yok|tGNxqA zOW_VaT_7mTM8W`K$wJKkiFol{a0|)V<*)pI{Cq?7>((*T!#T9-RcEZh7B6PhXwm?aEBHm!iZF<_4btF?RzCsn6q%d%YD(Gc_7)T zo;;(ekKwv4tn!I0r@{YdN0?aKW$zzu8IxtOw!!u#NTXST}Up@iwQ zd#V-S!vGLC)Q2=kI^K1Q`|7!t>?&GP$bW{Mh8MitD7ecV;^ud^@pgUVp=f zaOR=~`Wk%2M@njw;uMoPi3oZ)gQFl%jxTg+N$+$2^*xtAi~UavyH8x2XL|1`XQjmt z5gsYXThC>OPav!d{-=E)FT3whxU0{;>bq}r!$*4IKAIasz(lohVV~;E(gOLUbVOfZ zWn!--IYVfW73(lP2Xw3~a!_ zo11Mlo6mv2r_5M4u#QKm{_SCFQefK^HK;BGD`w9r@1YsEU_ z#G*c^VX~q=z7KjWj+ZkHU@mr68<@6uNXTIl$*QG49|^U}j*3mh)1RL_fI5t@2>k_3 z6_p@wp}+JB_>FV$%lOKHgp<%s;q9+vboUhHX59M^nE_<5a^k4iaMK#!>0Y47 z?8x?SG8lSgkBk9!C0*@kzm#kIKfb4B$UmO4p@&vNRXk*bB};M?J|X9@no4-ur*>Z1 z|Ad@RTlVf~H1h}!Xxd$<67@sW#2^QTSAD2?zRBMfNFWH{8FI(j?9k_;jKs@|#%`v3 zLpj3?FuHG-_s6Be!i(Y*Ir>jXg9QhtOnv_s z`3hVD$(PRbL97jUXsIOAitlWLd-ti^^ zW>!Jbd2s1{H>@_W%%N-8$j;2HL~0)HBJjN~x`kD<7nInx254B zD`R*gTYX?wSR`wM!zOKF5k1u5PdLjybgA;M;1+l%M^2pR?+63OMKc?e*BTe_9ylbk zEAw2U{}uBm5v9zZ0>Vz!yj-(FpM*5%qafsxrgOgJ-o;PTFXiq5-0~)l=i>ZnN2Fs@ z`$-v`1?Rcj=HJzAl$7hyEi;&iwvj~M4Wl!22iajdBi6>1eggKZ6cfHqY9qjBr}$Hy zqHstEcah1fp8bZ@w`Mx^T>ZhHFaEY_{|Z&v>^&&hpf_2V3DAB1+?bnZIQ8_B(m|qH zejH18V**}z-u|W4^-CA+Y_ao?*WiPWjB&GF$s%3T;{h3qPn+{6!M8h<`g_)mYrQ|` z8s2GWpu_4uGOT(|y^XxnnzCd41GuTaq55;c@8z^=0||Nnt`axrzl^JG1T6YXU%HeZ zwDn!(e&%2G%ua>~D{Gi%M|%p!c60<7JLadn)vT@XOlo$5huYR&G?b~RgDDVTb%LDU z-6{o!8s<&<>V5^Rh}%|CL)V746Z=he=4hMSOhFp0k*d@PAaMj6%0MIpO2ZG^RBQxh zarhq=tLqY5-ygKI5Xhacb#)EcT+}=YZdakM&ql5awjUN`?w_|tK2x4&#YOY8miGNOJY!g`XseO|`D`grgj*L_A6)BAPSC zMJT4O7;d)&SXFRA=oM?HD?Y9p*Jj1Scz3D#p_I6X2SNPBb?4{t3ogJ)rc1B2U6z!b zkY9{M3*63*#Oj6h6MxCT-mk<| zs))NXBF?*e;{M_(R|fI&<6bpEpUTsv7>^V5ppxJX%0Wj&MqCR0jhh!-2L;nmK}`wx z#nEhz`H2qUsKnG(u|sP5q)M3wfHTP+a|fwSa0mcWR2FShb{$?chH!ROwj%HSm zvh3ehzkaZlpa`t&-YYm&*X%uFtky^TG*0!stYZ-h&&ktZ-aR7Mhz~=KPHqkm&bUnv zmi|+HJjx_v7>ZfY-NRERAq!6Yje5DkP*3-#T^No|A{V`hA@2DXBer12&K}qqgfosi zWby&Vm-q_+x${3ioDRj);iu{H+s4;SM}UTlr@Y0pN3;zMSf=wjM0rG0E?dy}p(P;% z<`uw~*q#WK##S)?Qd5w=a3EAh4V6ar**~6%auf*Q6hl%ZW#QMW_(_@k__;eOBn#aS zagh$`gotS}BD#fz2#*)md|D$Y&nrJZ@)EScc%CUW^rzOHPxSH zqS$V9#Q~s!9FWe0$A{{BR(~Js3Z_O95#a z5f1$>7fUY8{X99aC#RN#>GIyi9T&7Dw?COAjP4$3b@Thr5BnQ=aBNQ>zO7xvwi@L3 zhW?k-U;cBY8gs4OP#IX{S;usL<^CQ8IJoNR>h$(f@d4UQm+J-k40ZSrL1%{f@K-%_ zodZhQ7*)3S6M~WE-Z&vti^7?2hsw3vd-kvjkUaSD)1>6WLW^yuMm_)OWe<4Q?(e znP9bPb*Ry!)b(C4>GDAVKyF~do~1<4*5kCpS9_CfBsKUe2Dyy-+Ge^~pma0KQZ3J` z566kIZs&DXu>Mi#tXW6r-E#>LYufLv*9?*aP=EOS)>j}wh)WjNBWiti!4v|qT!=6# zJe5DvmqA)cFi{48Cp^G~p4;C`7j4&(WjXP|NkkxbYl3b8frt%FF!^zYqnPUqU=l>vb4BP0(1(S8<5ea63HF% z1TOT%{#QnRtSfE!#qh`wq9snUuTY-@=iHmrM=~&h4gL z^+Ih$>L4bTItVYR;xC^E79K}8<+zO93&{={mQ#s22bQ9bg(Aib?iKegyt$!&nwq<` z@UV)!qGyIIXJn=?LF@g85{D`Yx7Sab8hf#aQ%$Ct{`#JW^M5R-&l;~pt3V8Y>Hd-5 zJ@X+~F6q=a0!-j^i37`S*Iq)!DvsT*Cg^|H6e#Hbq9^XV`#4d_e&EHkj3WUH7`Ga{ z1CEi1y`bA&8b{Oa&{-1d0Y=xW1m#F`+)QNGf%AS^ym6R+ee*7cSe+s1{SsicX5ZqN zfB36GKx-Qh8c8K|bOK6OZ=M}cV2TehW50l>odV`tZ&r4ouJy+)Tr#(DZs_~ObFV;@{&V$;Q-u7stxhEV;x;k0&aMEf1c-; zI8+y`Fb8)ROXWS9VL-pwxQ=R4FrEm?K(6(laG|v{K5*=cn;UuE42Br6+!o9jH&w`; z>%g7rLD5R*$^=NH8b$(mz_J0p96ndZ`~{X)0rrnYcpo&NqM=i-EU2K6;h_efoStqm zU$rh`D4he%p>c$8JA=b5|Mq3zqg#$SI5L~L+(|}1(5L&8-v+GtziR=0x#iS@_ec); z^1H7Umc9C@1U*=MP++m&zj`w4qdbv7c=jsu4ZkIqt{s42ZdQYOxs%PTpUxUQE*t+m zBa4tT72`) zQz!km1F!s$?H%3x7hhfh(LbKvkk3+0CtUSix^0pKJgVM|B))~;TWcn~G$B&JmzbD} zq{JYRKVKrlO;X9g=CmU{3=|C-dn5EBqDd5ao#f5Zlx=jWzjpShnCFj=Ycw5B7|(n- zNqjrgXn$kD;@ic#wXYiLQ5#=pvk>X)J&0P#gwGD3(1hiTa`+P;)vVYKZ7 zQ#;KpT#Z6HPrNif!|3^}>0qX%0-t|w%N8GKRVWDwB_ACZ^^p=6t9RJVl4|Z$=-Epo zJh^Fya#5Z7qTBekCu8O3ew+I|j%R_k8q-Kd=I#o1X87Sr*YPEh(PAbU?dy-@2mEqn zcsrCAL}tk!of2AdB@wO2Ga}OOAx`WyTA0Y7n_nsxdU@}%2Y%k{7M_;z@_wiJ%!e6b zIZMHU8S~vdEr@VI$@OZQfhGFv3({P*!`RerTHZpLGRP+J`CzjF-`hKWR;|M>CqPU6 z{o@0fm%W(O_M&QqR z05fXTY!E%82J$>#X&7fKGjArzctel>LFPqPJm-%1Wg)88IR0$)cfU5Nh(SsQ*=i^L zjY0En&vMjG^-n{z%vl$bZ#wm(j_=H8v`aIv#Vkm^ri-iORxrGAykHq!36u}t#EJ-H z-3B|OT(VTqw7_NJ&oV{gn%GB%YA69$<|}EdsksC+>i>YO_j-xr_iw z&r6MT?40%!9^vGhN>-a)QCmF&AxV%qyb_?8Iu8qj zF&?Hkan1t|;3ZO}4H0fZ>hxsdzwidh2!-Qh=@_zSPE+2)LD}d(%ajQS*?(q>sL zBBVz0LKQap23mc7m@6NVhx@4aX4Wti5yDa3?}P_R>FJBFs$`JJUU`21O#YR9uG%uv z(tDl0=c9-8)r4+F66P^;LJb!!%&%pFpoKZO%dsdSTYGlu7z%nDrI2qot-_9VQ&DEk zR}ltwa4{%ra$PxZp@%Y%f=>ydtN347nEA0^ON_Ams+}p~gkQ$945~%AHxC$A_&qlMXGQiSiK@`Uiw?!&H zR;U^i=qYb)!&N9$&oVxvfUld8z+UxrN5%{>!U@e%p{mfRjA(D7H8f-v4GY#)?nO;C zHJ>7TAgk>qh?Q|*LJkHz=Rv=QY<{V}?kcO5D?n6%bU*L|Uv9VbbHA#%jTq#gR+Y7%m3rzcj8dE+Ed17U@s-Z%Y3wUu8DY3Z)$m-lSKD3F84huT)-xuCGYq z=@+_*6;&lp7a-iU_ce^ftU;x4!&O5CTU|_i+zzQt_ydIe@KFOME}7dhA(P7(5j#A_ z3w^svp_MmpKSrXG{f&~M9meXv@W*6DSs-GgLYcMM^>VtflVcekicwJVs}=jgN1%dG zFNj>fgZ|6M1y-`Z&VUWg_kbhO_polC>kofn6kW;d*RqjvDl?~GbS$;k%P*CeTCIRn zuv8&o7E@XN*Q>h={z+PoJk=%iSo($!pi%@~7K#sld+&ve^JO>#M0khPqw=Qyw&n!2 zmL%}+Te%cO+5Ep4kQ6hBO4K@R$)Y|D(K;@^d}$ zXAw^&pL4PL-`cS2d&`C~vUmc^CJCo`d>sSJnGkj8A`?}7Tl(2k6osi^0pSiR{~KOfbu=PN#t@7--n(`#|3 z@AFRS8(5JbHDsex*igVQ5CP)*q88wznu7-I&QUwl;S+he8=c-W+xxvI`AtrBT}_w- z>ab9-hjT&)rrSf4epzU0qHD;_%S~b>fVmymINrV_zBX#fVXs@qFJ1e=1utXo@_ap< z&_o>0?H4B#|BMxba&?qX9&cl$Bb(bfb;u`IX_YE$bk^wZ!pz&WPS*U|wxgU(pX{bM zyBuQnhMg9Y6>THUKasvzgDk~f6xk*o_Sua^VxISITq3Frm1;|Q*3T2oK~q+eb5-}Z ztVc5o&}z;XWFOMzy*|!n$o*G2g3p4>ha4`hL!xEd|#YR zXNtf|iy`57;)ZJ}UY`pbzA0#aZ;t03N(udft`tHEajs9KZ0)sB&uE+ndOehc;z1@z ztxJeV72noZ76dka_Ev6=^-0G}M{44Q-rM&!g=zjRT%;Q=xx0##&|_~bU)TpiR=?mu zM7W{d>=2kvPRn~(lVMK8GjlOu@9od-?Dbw*zwXl1NKnh_02pf)T_tc+q!f*g&dEWE zlDNmAJSYkgG89oB0}m5c?{gQPG`qkcO-8!SBBhk$iRu!hiV*}E@hA)T=yH0BrJo~} zJXXl<*UO8zV@n&7GeSOgz(qUOhAfcbI)R12mcI)yn?Hyl*MZBkgO)4n0tsx?y--py`D@@h~ndF#aUs^`L>8ok8liz68?>{%xP$ z7nR>|3|DI4uDEOPtCnJNF3OPIb>}$bH*=Xo3>%bNINU>!Cu-1JN(|K<%(0^OD7(#1 z82}xW(829!@Z?;e19_qrO;fzg5142|iF~o3h(AF{F}?moL<7L{L(Zks(OX-q7!6Uw zbeF^RH?Y{)=Z8|U7gtY!nlSJVpF~6?twtxvQVtZHit$ZC^Q5(Dk8lqL(cPbm?aJ`%yA=SC5IE}F}cRG6f&0XPHDpecv$QU zk`42DYtiiaql0U67gNMI~(0wgS<(Et;lCB`Fp?MU%_ZWxh`{#3*m(#({v@IG`prN zjqOxWd?Mb-o-J^u+3Li!zAty93=V{hgJQm6&3NI;86cnC`on0sS%EXPIBT+xQxM=r z5>J-VNt*4X6Vz1&$zq8zVpDqi9gmtX{EJhpQGJQI9hwuMRlb`z)EO{9EMBuwD^PE;aGnD=cHoa z(kElNp6D0I&z;-4e8v7x`gNJ2WE91-bW-}e{Hh5n)qKiKHL4Q0sYijtv`*sm^sAJ0 zqf!`_VIsp59|~9rWHU4Rjhx<{w)h-pE-m?9$^DK#b^dn!aeZy+&SG7z+v9Qn^fRzy z%3nALcHJ^Er6WR@-(<`R^e%16cg&sRVyJua`&N^#b^0MEWU$i>gU7HsMEJ1qMo@vR zlLbi(ka|5r`n|vqf%z(GQ_i&CoCI3*aqfQMd|h}N$kq?qh>+3Uc$|8fzxgB)O+X|J zBeAx7x+QB7hCunspJ#T5z$)j^{FO6x<8(3n$l*>G4lk zL!jEng@qdZF7LbLqs5NK@)%x*wGS*e5zh?Ih*B&!;a$%S zE{IRFnCD04%B50MxQJ?hElkP8z3TZWpQ}DnUN-4Zd?pUP% zGtk}7&s%{Ma@#xU@9J>fs*Sgc8d+Xr|0>EZ>bc=FxWyd{JMIzwZVi3wc^RywEqc}u zI;aPLd>_`!f3yiBwdt*%3en%HHU9CrC5>Zb=7s7vGedKQt^`-q&|vtE9kueLR6}?s zk3WDdIzJ(8wrY?&L91)`E`AL(!1=8Eg0hxhiNL##O0kXlO|bJ)3HaRO=+b?e{QgJ5 z`3-kVq?*Bb*?qIyFE9x{DS&0=JzxV5E1uPJHMK1eus!>+=M)N4fX|FttRlG+q-_=jDx`F>>vaa`0Uf9kfJvkjCCIZW!Du$g z8xH!u0UV%4$T{^RkyVMk~L1`fe=&8)a71uU@fL73d^ZwJlHdL_a zpwC#L)H6ikzy(z_Z1m6^4gBN=c|_Qa6oXO2*P}#13KgY%CF!0W``0V$dN0*KH4Yde zPYw>3ke15)HgiqUcefC>aicMqOt{X+wN@~1AWDtdZ}3!7s1%3lk@%LLe-|p4@(VjU zQ|5UJCE21@YQdrxUjWySmGv2yh0XJWv_zTYW7le1OB>Q)mrMe5stWiCD?aXHlTe-x5=VLc0usR8!MqSvKjYEzsa zGB>ZZ@UNMbN=*-|(OSBOe1ailRXl$k=9sDq+K6vQbbgvBBIAsT!! zIP^4;w>6IXn)ypW+#r)tZj(J44(PN@Vp`w$$ca)t6yV|2${j}8i9 zYl3t|;b08n6ijAf5-zyz%IJmpsF|zGB$d{Hsg0BJL=ajFSL1|9-ZrBGXc~<88OQ2K z0=sqE!Vb)JX3UUBzgqc4rv$q)^%(LR-xQIcTuUi7OMRb4p-dp6<;qA;E6pFH;=?eF z6O^G|Hm8)}u1{|Y0vm_q#4G_VpVbSd8W+P-QevL3o;547;%IuzbrT9ukgs_W1aM(* zacR0``wxRYm#J83qBFnl`-Q!i68Ha7DNY#E!_AD7-hjOj{T^l9)_Ua2@4*n9&KCLv zBH#x?HeyOe)qq{$Mx|i)LvT>O!w*mwy6<@WCYuTySzXO(b>|%L+}f$|u5&Wk&hi;D zQW2RUe+EvxTUd1hMEHD^*@rU|bk7f*@M9Jg`74S#vZpK;Gq9^&PMBbNU7VKn)dGGP z{o9B(@C&chN9UhIQE=M|vhunUjzMGfMuhRci}h-I>SaYT0K0kV+PHCkXCfUebV>ne zhzMgrSIvNQW25%$#v^`Yy(&C?`1>NZ&7{<;h?t=)$v!VCRZ(LNLir*zIGPy*)zZR> zBxz7sfh2){M;<@f_xKAoYN?1a=Y%~2Z5&iKF?8yLwl{eA(<-#ApfZYYi~RyAS&UfJ zN1?wU)`lS>^E7CTAodyf3ZVy-5P6twMHvuI=foHs9<{$OyzTq9+njZj3 zf4FAlfw{Wii5mxWs6W!@waJ;sTRrd)p&Jo$tM<16vn(bQU2oy=y+vCUdh2p^)VjIR zD4}=IapZz7M16F|0}map==$rtaAsXV#l~xBwQ&?TC$m&vuQg>ntL#iYf8pkdC^OYVsp4dU5&dw-f@YlAvwN7jc?LXuxq$We3uJowI&K%eH0r51m%n|0z_?P)?KhNJ z;L6^WK-Yu!PJ!TAtD(12u#6@HGJq`lPurA|)2|7~0-&kkh96e*1={q<;*0UecomGV z>v{>tiI6VkX4dEleF{^%lq7*4G5~btnF3k4$Y2FurjZo!it>LhOhV|6XL#%`6qyKI zTHNL>PSvXlT4;~|GIlc)ekcc_Z24B`vRKgiaZkf+>Ly$T30ha95$Foh7~%gK#{$%+ z5W`ohiN;apfCW2;dV{ZpeU@zR-y*f|vYxIx$UG{9-*WhnD9 zC>nd7*r51ji|;7ZoK@_FjWp}h&1W+=Xpo|&z0A6!Ut-|``%8{HAbJr>>O|QlMuxV{&5*f&1qy=3={XnF_Gy) zX1-UCWU~@w{9Pm3$7^EAe3MXDA8Cem3A2^}Crqb-wtx3ehK8$FLg z%jh*iaht&8WxbK3Zi?6eXi{Y3cLaweffeYixKA8S=dgPZ9ShDiC@7 z%gL19n^-SBxu`CO;R`|#1zp47p>_5aW#a;;Ag6A~ke@J0$IRXNF_5C0r#jO5N60}< z!ZK*T0Ruc0H|AZKQ*0nl6dTf&u@(O=udC$}%eQUaC*zpl>*kasm6|p-+rScSKI>+1 z^!v#7kOo{R>0rEU&?+%e$IK+O`6|0)_ zy-XE(e*97K#$ujJX`1SGt?O=M%DNdg*)w=Orm}-;}iJbY{kNmef zj5L-9J7OecNY0{Dn5kggi43r-H1kGeidMVvborRQynM#S1%r{N`+mXDAr>p&LU^P7 zaYEGa610|EtqulJKmiVB8IOKfogZzbFNmE?+y-e$a->qVRvVnHrjob0Ac(vv&fCi5 zr30;N3pT0^IcrQG>2=>?gbL@)%nK!H54ii+Xrmmu%u|R)j?rhv=U%aFwuT7*F&=vF zQJUTA({)f}(dm+i2oaMA>|#<1K2@@p$5Ybfi#_>xN?w0f^y^hPIiyFY}OA3}QHu9G@?Ny$DlpR5G ztSP7GC1bO=Czdso(VYE!2~ReME_|NhayfVWBa2sds zO=^hiOb)(z$ckf{8#Hi-1H|#YE|doQV2DE{bi-^4z{C$G$!&P#uYmWkf?hBMvxoJN z@~AM~|5wPBM?>9z|B|Jwl`L74vZP2v8fHSw6EU)HnaIA&ld&|$gX|%DkukQ(UKm4~ zQHiXXF*M0CiDASvVUSOTuYTwE$8)~t{Lb&3`_KL3eeb#VoO@sQy}f!XV6aGy9c;qQ zR4~fpFd&9bk#y3D7|t2kk0*RTo-HAekZFaSbVYMk{rc(1KZbRkIUU$Ag+AYA9;e_w zfs^`~`8DbObba~FgOBt9-a0tH*Ib61j#D{B|xFlsYf@yW}%ba`?_nXnpI}hDV{+9oV1&(yv7#l~%Bm3ka*QKv0g>jLCBaVTfl6Xee_$JX>nr6@y z7NU~1&YJk03pQoy}477!6+3}WjF!?V>QzkSMNK)b<78_5#=#)vCBud#lO zo{?kjt=CY^z)I*^yQ`8ym!rBv`qvGpI5J2TA0Y(`HK^#S*12eOF#haQWdxU(w_kz9 z0dU?vV~f`xUI4k6jSv1Ya1HnTsnb{4xoqJz{~%OV*V%c!6aA;;35>PcgUIc_H|~`W z%7y*3NK$5BzGZDl(-Di6-1YZK=-nmF%+cM*)Fez9%D%xKUBi%|71YtSq&7sd;H88M z2LQ!1u-eBO^l*-uWb`J`*t)Z>Lb#Tje-dlfgGK8r9YCOMsVq*_}xMN$7x{A z?R<}_+A~PTdkK>K4K}W7>WfWCJo6C8j@mXq94_qNZv4Ki)es)Ua)F{I;)tI%3)7wo zej-p0A{$&@ z!(}3t1g&v8kDtZ0YnGa|Lg9X_It(lZsAc%5zBZeL`{|pjuPCo`AK|3L@0KUUw#Dj! z;Z>#eNS;xRR*738CF-#2)(dk_4HPsSl0Wu~@xyWIMVBr~a!Sky=;NE)P zvnbIh=`j?3;dZeWH|K3Z(=$A?Nl68Mi2ZvDbYy}xB%8Rf@i^v1e(_@9Z;&J#<3=lC zg!5e9|3+vipH{FO5A)kc6tC{Ly=5YdgqxW}=USR1dmqjw$QrG}z&_s1Gh!^A`H=&%xp(2%6?63@9dn11||P(1Wb8rwO~yF(O>- zkvW7jH}aair$5`Pi*k;!4>DR68o!@8*ZuJ~ltElUWZd6(!;mV|*p{73ZaIJ?UA_kI zXSE9casHlR9*b?GUHY?5v_fMMVd=ul!5E{FFF^>eFB)87-bU~>5Ho(4gxT|0G2>Y& zK7!Qi3a_1v@kt@0E+cRZ7S}x`BQ)@=%@FmOgqG>_c=@Kpk5sr0!mfVWr-a~R52wev z2L=Y?iMX{Wl23?`SyPh~$E8ex+(Ry01LXss1~?Ua-MOj%sQ%qEKVlJ=bY0$cinWq8 z{)LYtN7@T63yIwFCqwoRxf8(3a*~@AmNbW@6bq~_d}XuEZ1kbT;(cRh%UI7#+C+fc zPW=WB-i;aF~$*<^syH;=u9$ zme;od!;eF4PX-(IZYa$RA-92qLjz(|X&H-|ra4*YrE4^aoagFbEqX|Km)n>q{zPlH zgYaMX)F<;RmYqktL8U!Kx^@NKJ*nfOPqZUDs*eve2gwz#RXkGw!Zj7|CHj)LeO|tO zK&dGC^*f(tz45MAuW;*w!)BREC%bAV8u{S9o|A#zrRb;hKw^+wz{%PTpK_>tVoHsY z9HYtUN=)A{(Me0e8$3*B)<=1UY80El`^$}H3zh$|^b+Y5p+a3*dzZEQdSLVSR^c-} zy|li%kE*_Hr_==4(vNEMI5t9!Swh#Rp!nK4(6-?5fMRNfo)4HTw>?Q(E9Ww4BYWk^^wi*sQt$bZnz$~WQguV zJ|rRo`>^|4iJ&?te>vnSQBmE@NY{jLO21lu)uO)Lj9(6(H!S;xq6z#%4b1Us3@`o1 zVEfI?^1Lf=(r!+OZ(rOWco}Jkq|W`$a(eahrr0O ziiyRzr4bvpM&hqEla$`o<`*yS0QE$L!ojM%j+S_fEd@=^wML2UzgduhFa^fcL3?|D z&e}K6gl7FP>Cm9gfVK2p=(eUYd&%$tAT!nj|0b%*aZii28RCD)+KY*5go_7u7E~ne z+FD{MD(6R=wekuu@H{)WF~3`L#2A#@3-w#yeHc23RSk1aS;4H}&F}k_Q zpdEMEdq8RS70RLlt(cH-6PQV`c7bKCAkyD5ivFT+xq{ckJQW7CSTAqUXWRE@DYVTT zXuh)F-Gc2y2|4mM2V~@^3jBSxQnyR&+$*tk>e8Y{zSr%aDMww!cM^#jo#D{v6DQ_H z@6&l)y&lFgO>n#WHlQDdWw{`7Zj!r!K*tF0)@Q->n~*LHi`-3`TM!q?$f7JpBxx_TuL=kn4TYAw%^#tqrM zN%2?>?T?Z}ah)#4?XGIf!BB+(;HmmA{T-`D;N9O+Iby(g1oM41>U*cFdd}4Ry*n_s zSap7&5A@%4_FrZ9zv`Y0XKz>jEhu%r3O)08QAg*u_;BF(Tk`B@7mcgtv`NMcAZKwcJQOhT3@_pm-kdB58g`|6Fk0g(VkpV$L(;RSNm^8 z=CapJ`nS(ieUg3kOX8r-^!=&!m&^BK$g6h=+FH$tmd`itbWVlefBu9>kCw%i8&*<> z&xtaD&i)kWf?r*@sePr;55wf*cGp3+vZ4cP|DF*>&y5N9bphrw?+uG4%iEZgN}FCC z5*;qQx|zVV3@la*NsP^N_+r(k%5>o<*ooWNi0K!2blLLzR=$Ge+UkvMMc-wz-0*Y3 zH35&S9t2b+8H^Kb?5A+~bkjqZu~p0CS8Zz55K|86K~0@Tm5_*&=S}=CZMLy71w_U8CD(CU;-lp6bPQ$@&_5Cy$L1^VwpG6OrCtZf2M z@I|yrM@rNd=lujq8tw4dB*nslOy=l-vfXkGsSW$eQ8#n=Vyu-#(aX>Ft`DzYCA3$s zjhQo)r}aDDDBBqPIFNWHvKFU%EGGu4 z-7&|JnuzEmOs6g`^toV~9-x-$PE+t%BoSNX`IX-5aJ;eZ7dxeMHKnYf)6@yNPtNFt8k;z&~$%Kj%NEm%sC>lM& z?~Joyi+BeaSVGcc0Z8>ase$@U{Qsmvi;Y`k6b%Mno_X-GL-+eK$1ium z)xm0Pf=Ik|WT$9OR2|j2qru2??>3eR4wotiwOr2eeG=bR)2^Alg4v|BI+=hnm!}sWN#IMhm+*L zhsrt5L@4VF8;rxxsn5UBTW3$I*KHx#pn=K{hps|8t6NmN?ja~8S*>Xfo$<+LkF9~! zhR_yu*-@-paUsl_o$H488MUtYys`mCs=M1lSqu@^Dx+9ur<%x+pTl*2)<;)?I|Nb8 z_Kimj6%G?aE&+W*zbjsnlGrda(GZZU?Ro9dI-P0}PC{w1QSH=@C?tzMoA`EPH2Rc* zIWjcY(#!2Z!&Wql@$K8&Z!^{FaqqORnxBOa@)n!lAgndbWS4FE5_>=9TL$W^&@;86 zGXfll3Jf#M^Z7@;R7cQrF3#4qJ9MlcYs*$Vz^}c34IKNOFYDC*C;J2CzsHo< cn4LqaR|v;j9*9@}p&q [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/13950) in GitLab 11.5. In a merge request, you can leave comments in any part of the file being changed. -In the Merge Request Diff UI, click the **{comment}** **comment** icon in the gutter -to expand the diff lines and leave a comment, just as you would for a changed line. +In the Merge Request Diff UI, you can: -![Comment on any diff file line](img/comment-on-any-diff-line.png) +- **Comment on a single line**: Click the **{comment}** **comment** icon in the + gutter to expand the diff lines and display a comment box. +- [**Comment on multiple lines**](#commenting-on-multiple-lines). ### Commenting on multiple lines > - [Introduced](https://gitlab.com/gitlab-org/ux-research/-/issues/870) in GitLab 13.2. +> - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875) click-and-drag features in GitLab 13.8. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299121) in GitLab 13.9. -GitLab provides a way to select which lines of code a comment refers to. After starting a comment -a dropdown selector is shown to select the first line that this comment refers to. -The last line is the line that the comment icon was initially clicked on. +When commenting on a diff, you can select which lines of code your comment refers +to by either: -New comments default to single line comments by having the first and last lines -the same. Selecting a different starting line turns this into a multiline comment. +![Comment on any diff file line](img/comment-on-any-diff-line_v13_10.png) -![Multiline comment selection highlighted](img/multiline-comment-highlighted.png) +- Clicking and dragging the **{comment}** **comment** icon in the gutter to highlight + lines in the diff. GitLab expands the diff lines and displays a comment box. +- After starting a comment by clicking the **{comment}** **comment** icon in the + gutter, select the first line number your comment refers to in the **Commenting on lines** + select box. New comments default to single-line comments, unless you select + a different starting line. -Once a multiline comment is saved the lines of code pertaining to that comment are listed directly -above it. +Multiline comments display the comment's line numbers above the body of the comment: ![Multiline comment selection displayed above comment](img/multiline-comment-saved.png) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f1838bc3dc6..e0a624b2113 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7559,15 +7559,6 @@ msgstr "" msgid "CompareBranches|%{source_branch} and %{target_branch} are the same." msgstr "" -msgid "CompareBranches|Compare" -msgstr "" - -msgid "CompareBranches|Source" -msgstr "" - -msgid "CompareBranches|Target" -msgstr "" - msgid "CompareBranches|There isn't anything to compare." msgstr "" @@ -26722,9 +26713,6 @@ msgstr "" msgid "Select branch" msgstr "" -msgid "Select branch/tag" -msgstr "" - msgid "Select due date" msgstr "" diff --git a/package.json b/package.json index 15dd7546b37..3d370c9259e 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@gitlab/eslint-plugin": "8.1.0", - "@gitlab/stylelint-config": "^2.2.0", + "@gitlab/stylelint-config": "2.2.0", "@testing-library/dom": "^7.16.2", "@vue/test-utils": "1.1.2", "acorn": "^6.3.0", diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb index 05f4c16ef60..b72ac071ecb 100644 --- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb +++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb @@ -21,13 +21,13 @@ RSpec.describe 'Merge request > User toggles whitespace changes', :js do describe 'clicking "Hide whitespace changes" button' do it 'toggles the "Hide whitespace changes" button' do - find('#show-whitespace').click + find('[data-testid="show-whitespace"]').click visit diffs_project_merge_request_path(project, merge_request) find('.js-show-diff-settings').click - expect(find('#show-whitespace')).not_to be_checked + expect(find('[data-testid="show-whitespace"]')).not_to be_checked end end end diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 2a486e6e5e1..9547ba8a390 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Merge Request button' do project.add_developer(user) end - it 'shows Create merge request button' do + it 'shows Create merge request button', :js do href = project_new_merge_request_path( project, merge_request: { @@ -83,7 +83,7 @@ RSpec.describe 'Merge Request button' do end context 'on own fork of project' do - it 'shows Create merge request button' do + it 'shows Create merge request button', :js do href = project_new_merge_request_path( forked_project, merge_request: { @@ -120,7 +120,7 @@ RSpec.describe 'Merge Request button' do let(:fork_url) { project_compare_path(forked_project, from: 'master', to: 'feature') } end - it 'shows the correct merge request button when viewing across forks' do + it 'shows the correct merge request button when viewing across forks', :js do sign_in(user) project.add_developer(user) diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 33f965f47df..feac88cb802 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -1,79 +1,66 @@ -import { mount, createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; +import { mount } from '@vue/test-utils'; + +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; import eventHub from '~/diffs/event_hub'; -import diffModule from '~/diffs/store/modules'; -const localVue = createLocalVue(); -localVue.use(Vuex); +import createDiffsStore from '../create_diffs_store'; describe('Diff settings dropdown component', () => { let wrapper; let vm; - let actions; + let store; function createComponent(extendStore = () => {}) { - const store = new Vuex.Store({ - modules: { - diffs: { - namespaced: true, - actions, - state: diffModule().state, - getters: diffModule().getters, - }, - }, - }); + store = createDiffsStore(); extendStore(store); - wrapper = mount(SettingsDropdown, { - localVue, - store, - }); + wrapper = extendedWrapper( + mount(SettingsDropdown, { + store, + }), + ); vm = wrapper.vm; } function getFileByFileCheckbox(vueWrapper) { - return vueWrapper.find('[data-testid="file-by-file"]'); + return vueWrapper.findByTestId('file-by-file'); + } + + function setup({ storeUpdater } = {}) { + createComponent(storeUpdater); + jest.spyOn(store, 'dispatch').mockImplementation(() => {}); } beforeEach(() => { - actions = { - setInlineDiffViewType: jest.fn(), - setParallelDiffViewType: jest.fn(), - setRenderTreeList: jest.fn(), - setShowWhitespace: jest.fn(), - setFileByFile: jest.fn(), - }; + setup(); }); afterEach(() => { + store.dispatch.mockRestore(); wrapper.destroy(); }); describe('tree view buttons', () => { it('list view button dispatches setRenderTreeList with false', () => { - createComponent(); - wrapper.find('.js-list-view').trigger('click'); - expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false); + expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', false); }); it('tree view button dispatches setRenderTreeList with true', () => { - createComponent(); - wrapper.find('.js-tree-view').trigger('click'); - expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true); + expect(store.dispatch).toHaveBeenCalledWith('diffs/setRenderTreeList', true); }); it('sets list button as selected when renderTreeList is false', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - renderTreeList: false, - }); + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { renderTreeList: false }), }); expect(wrapper.find('.js-list-view').classes('selected')).toBe(true); @@ -81,10 +68,8 @@ describe('Diff settings dropdown component', () => { }); it('sets tree button as selected when renderTreeList is true', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - renderTreeList: true, - }); + setup({ + storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { renderTreeList: true }), }); expect(wrapper.find('.js-list-view').classes('selected')).toBe(false); @@ -94,10 +79,9 @@ describe('Diff settings dropdown component', () => { describe('compare changes', () => { it('sets inline button as selected', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - diffViewType: INLINE_DIFF_VIEW_TYPE, - }); + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { diffViewType: INLINE_DIFF_VIEW_TYPE }), }); expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(true); @@ -105,10 +89,9 @@ describe('Diff settings dropdown component', () => { }); it('sets parallel button as selected', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - diffViewType: PARALLEL_DIFF_VIEW_TYPE, - }); + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { diffViewType: PARALLEL_DIFF_VIEW_TYPE }), }); expect(wrapper.find('.js-inline-diff-button').classes('selected')).toBe(false); @@ -116,53 +99,49 @@ describe('Diff settings dropdown component', () => { }); it('calls setInlineDiffViewType when clicking inline button', () => { - createComponent(); - wrapper.find('.js-inline-diff-button').trigger('click'); - expect(actions.setInlineDiffViewType).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith('diffs/setInlineDiffViewType', expect.anything()); }); it('calls setParallelDiffViewType when clicking parallel button', () => { - createComponent(); - wrapper.find('.js-parallel-diff-button').trigger('click'); - expect(actions.setParallelDiffViewType).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/setParallelDiffViewType', + expect.anything(), + ); }); }); describe('whitespace toggle', () => { it('does not set as checked when showWhitespace is false', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - showWhitespace: false, - }); + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { showWhitespace: false }), }); - expect(wrapper.find('#show-whitespace').element.checked).toBe(false); + expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(false); }); it('sets as checked when showWhitespace is true', () => { - createComponent((store) => { - Object.assign(store.state.diffs, { - showWhitespace: true, - }); + setup({ + storeUpdater: (origStore) => Object.assign(origStore.state.diffs, { showWhitespace: true }), }); - expect(wrapper.find('#show-whitespace').element.checked).toBe(true); + expect(wrapper.findByTestId('show-whitespace').element.checked).toBe(true); }); - it('calls setShowWhitespace on change', () => { - createComponent(); + it('calls setShowWhitespace on change', async () => { + const checkbox = wrapper.findByTestId('show-whitespace'); + const { checked } = checkbox.element; - const checkbox = wrapper.find('#show-whitespace'); + checkbox.trigger('click'); - checkbox.element.checked = true; - checkbox.trigger('change'); + await vm.$nextTick(); - expect(actions.setShowWhitespace).toHaveBeenCalledWith(expect.anything(), { - showWhitespace: true, + expect(store.dispatch).toHaveBeenCalledWith('diffs/setShowWhitespace', { + showWhitespace: !checked, pushState: true, }); }); @@ -179,15 +158,12 @@ describe('Diff settings dropdown component', () => { ${false} | ${false} `( 'sets the checkbox to { checked: $checked } if the fileByFile setting is $fileByFile', - async ({ fileByFile, checked }) => { - createComponent((store) => { - Object.assign(store.state.diffs, { - viewDiffsFileByFile: fileByFile, - }); + ({ fileByFile, checked }) => { + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { viewDiffsFileByFile: fileByFile }), }); - await vm.$nextTick(); - expect(getFileByFileCheckbox(wrapper).element.checked).toBe(checked); }, ); @@ -199,19 +175,16 @@ describe('Diff settings dropdown component', () => { `( 'when the file by file setting starts as $start, toggling the checkbox should call setFileByFile with $setting', async ({ start, setting }) => { - createComponent((store) => { - Object.assign(store.state.diffs, { - viewDiffsFileByFile: start, - }); + setup({ + storeUpdater: (origStore) => + Object.assign(origStore.state.diffs, { viewDiffsFileByFile: start }), }); - await vm.$nextTick(); - getFileByFileCheckbox(wrapper).trigger('click'); await vm.$nextTick(); - expect(actions.setFileByFile).toHaveBeenLastCalledWith(expect.anything(), { + expect(store.dispatch).toHaveBeenCalledWith('diffs/setFileByFile', { fileByFile: setting, }); }, diff --git a/spec/frontend/issue_spec.js b/spec/frontend/issue_spec.js index fb6caef41e2..19f7e6bd5a9 100644 --- a/spec/frontend/issue_spec.js +++ b/spec/frontend/issue_spec.js @@ -1,91 +1,94 @@ +import { getByText } from '@testing-library/dom'; import MockAdapter from 'axios-mock-adapter'; -import $ from 'jquery'; -import Issue from '~/issue'; +import Issue, { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issue'; import axios from '~/lib/utils/axios_utils'; -import '~/lib/utils/text_utility'; describe('Issue', () => { - let $boxClosed; - let $boxOpen; let testContext; + let mock; - beforeEach(() => { - testContext = {}; + beforeAll(() => { + preloadFixtures('issues/closed-issue.html'); + preloadFixtures('issues/open-issue.html'); }); - preloadFixtures('issues/closed-issue.html'); - preloadFixtures('issues/open-issue.html'); + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/related_branches$/).reply(200, {}); - function expectVisibility($element, shouldBeVisible) { - if (shouldBeVisible) { - expect($element).not.toHaveClass('hidden'); - } else { - expect($element).toHaveClass('hidden'); - } - } + testContext = {}; + testContext.issue = new Issue(); + }); - function expectIssueState(isIssueOpen) { - expectVisibility($boxClosed, !isIssueOpen); - expectVisibility($boxOpen, isIssueOpen); - } + afterEach(() => { + mock.restore(); + testContext.issue.dispose(); + }); - function findElements() { - $boxClosed = $('div.status-box-issue-closed'); + const getIssueCounter = () => document.querySelector('.issue_counter'); + const getOpenStatusBox = () => + getByText(document, (_, el) => el.textContent.match(/Open/), { + selector: '.status-box-open', + }); + const getClosedStatusBox = () => + getByText(document, (_, el) => el.textContent.match(/Closed/), { + selector: '.status-box-issue-closed', + }); - expect($boxClosed).toExist(); - expect($boxClosed).toHaveText('Closed'); - - $boxOpen = $('div.status-box-open'); - - expect($boxOpen).toExist(); - expect($boxOpen).toHaveText('Open'); - } - - [true, false].forEach((isIssueInitiallyOpen) => { - describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, () => { - const action = isIssueInitiallyOpen ? 'close' : 'reopen'; - let mock; - - function setup() { - testContext.issue = new Issue(); - expectIssueState(isIssueInitiallyOpen); - - testContext.$projectIssuesCounter = $('.issue_counter').first(); - testContext.$projectIssuesCounter.text('1,001'); + describe.each` + desc | isIssueInitiallyOpen | expectedCounterText + ${'with an initially open issue'} | ${true} | ${'1,000'} + ${'with an initially closed issue'} | ${false} | ${'1,002'} + `('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => { + beforeEach(() => { + if (isIssueInitiallyOpen) { + loadFixtures('issues/open-issue.html'); + } else { + loadFixtures('issues/closed-issue.html'); } + testContext.issueCounter = getIssueCounter(); + testContext.statusBoxClosed = getClosedStatusBox(); + testContext.statusBoxOpen = getOpenStatusBox(); + + testContext.issueCounter.textContent = '1,001'; + }); + + it(`has the proper visible status box when ${isIssueInitiallyOpen ? 'open' : 'closed'}`, () => { + if (isIssueInitiallyOpen) { + expect(testContext.statusBoxClosed).toHaveClass('hidden'); + expect(testContext.statusBoxOpen).not.toHaveClass('hidden'); + } else { + expect(testContext.statusBoxClosed).not.toHaveClass('hidden'); + expect(testContext.statusBoxOpen).toHaveClass('hidden'); + } + }); + + describe('when vue app triggers change', () => { beforeEach(() => { - if (isIssueInitiallyOpen) { - loadFixtures('issues/open-issue.html'); - } else { - loadFixtures('issues/closed-issue.html'); - } - - mock = new MockAdapter(axios); - mock.onGet(/(.*)\/related_branches$/).reply(200, {}); - jest.spyOn(axios, 'get'); - - findElements(isIssueInitiallyOpen); - }); - - afterEach(() => { - mock.restore(); - $('div.flash-alert').remove(); - }); - - it(`${action}s the issue on dispatch of issuable_vue_app:change event`, () => { - setup(); - document.dispatchEvent( - new CustomEvent('issuable_vue_app:change', { + new CustomEvent(EVENT_ISSUABLE_VUE_APP_CHANGE, { detail: { data: { id: 1 }, isClosed: isIssueInitiallyOpen, }, }), ); + }); - expectIssueState(!isIssueInitiallyOpen); + it('displays correct status box', () => { + if (isIssueInitiallyOpen) { + expect(testContext.statusBoxClosed).not.toHaveClass('hidden'); + expect(testContext.statusBoxOpen).toHaveClass('hidden'); + } else { + expect(testContext.statusBoxClosed).toHaveClass('hidden'); + expect(testContext.statusBoxOpen).not.toHaveClass('hidden'); + } + }); + + it('updates issueCounter text', () => { + expect(testContext.issueCounter).toBeVisible(); + expect(testContext.issueCounter).toHaveText(expectedCounterText); }); }); }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index 1e6c9e50a7e..a481b1cb850 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -1,5 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; @@ -63,10 +63,6 @@ describe('Pipelines Actions dropdown', () => { }); describe('on click', () => { - beforeEach(() => { - createComponent({ actions: mockActions }, mount); - }); - it('makes a request and toggles the loading state', async () => { mock.onPost(mockActions.path).reply(200); diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index f077833ae16..d4a2db08d97 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -1,24 +1,27 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; describe('Pipelines Artifacts dropdown', () => { let wrapper; const createComponent = () => { - wrapper = mount(PipelineArtifacts, { + wrapper = shallowMount(PipelineArtifacts, { propsData: { artifacts: [ { - name: 'artifact', + name: 'job my-artifact', path: '/download/path', }, { - name: 'artifact two', + name: 'job-2 my-artifact-2', path: '/download/path-two', }, ], }, + stubs: { + GlSprintf, + }, }); }; @@ -39,8 +42,8 @@ describe('Pipelines Artifacts dropdown', () => { }); it('should render a link with the provided path', () => { - expect(findFirstGlDropdownItem().find('a').attributes('href')).toEqual('/download/path'); + expect(findFirstGlDropdownItem().attributes('href')).toBe('/download/path'); - expect(findFirstGlDropdownItem().text()).toContain('artifact'); + expect(findFirstGlDropdownItem().text()).toBe('Download job my-artifact artifact'); }); }); diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js index ff997912384..56da636bb3d 100644 --- a/spec/frontend/pipelines/pipelines_table_row_spec.js +++ b/spec/frontend/pipelines/pipelines_table_row_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; import PipelinesTableRowComponent from '~/pipelines/components/pipelines_list/pipelines_table_row.vue'; import eventHub from '~/pipelines/event_hub'; @@ -181,10 +182,16 @@ describe('Pipelines Table Row', () => { expect(wrapper.find('.js-pipelines-retry-button').attributes('title')).toMatch('Retry'); expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true); expect(wrapper.find('.js-pipelines-cancel-button').attributes('title')).toMatch('Cancel'); + }); - const actionsMenu = wrapper.find('[data-testid="pipelines-manual-actions-dropdown"]'); + it('should render the manual actions', async () => { + const manualActions = wrapper.find('[data-testid="pipelines-manual-actions-dropdown"]'); - expect(actionsMenu.text()).toContain(scheduledJobAction.name); + // Click on the dropdown and wait for `lazy` dropdown items + manualActions.find('.dropdown-toggle').trigger('click'); + await waitForPromises(); + + expect(manualActions.text()).toContain(scheduledJobAction.name); }); it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js index 0d1d6ebcfe5..c90e63313b2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js @@ -11,32 +11,31 @@ import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); -const createComponent = (initialState = mockConfig, slots = {}) => { - const store = new Vuex.Store(labelsSelectModule()); - - store.dispatch('setInitialState', initialState); - - return shallowMount(DropdownValue, { - localVue, - store, - slots, - }); -}; - describe('DropdownValue', () => { let wrapper; - beforeEach(() => { - wrapper = createComponent(); - }); + const createComponent = (initialState = {}, slots = {}) => { + const store = new Vuex.Store(labelsSelectModule()); + + store.dispatch('setInitialState', { ...mockConfig, ...initialState }); + + wrapper = shallowMount(DropdownValue, { + localVue, + store, + slots, + }); + }; afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('methods', () => { describe('labelFilterUrl', () => { it('returns a label filter URL based on provided label param', () => { + createComponent(); + expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe( '/gitlab-org/my-project/issues?label_name[]=Foo%20Label', ); @@ -44,6 +43,10 @@ describe('DropdownValue', () => { }); describe('scopedLabel', () => { + beforeEach(() => { + createComponent(); + }); + it('returns `true` when provided label param is a scoped label', () => { expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true); }); @@ -56,28 +59,29 @@ describe('DropdownValue', () => { describe('template', () => { it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => { + createComponent(); + expect(wrapper.attributes('class')).toContain('has-labels'); }); it('renders element containing `None` when `selectedLabels` is empty', () => { - const wrapperNoLabels = createComponent( + createComponent( { - ...mockConfig, selectedLabels: [], }, { default: 'None', }, ); - const noneEl = wrapperNoLabels.find('span.text-secondary'); + const noneEl = wrapper.find('span.text-secondary'); expect(noneEl.exists()).toBe(true); expect(noneEl.text()).toBe('None'); - - wrapperNoLabels.destroy(); }); it('renders labels when `selectedLabels` is not empty', () => { + createComponent(); + expect(wrapper.findAll(GlLabel).length).toBe(2); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js index 85a14226585..f293b8422e7 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js @@ -47,6 +47,7 @@ export const mockConfig = { labelsFetchPath: '/gitlab-org/my-project/-/labels.json', labelsManagePath: '/gitlab-org/my-project/-/labels', labelsFilterBasePath: '/gitlab-org/my-project/issues', + labelsFilterParam: 'label_name', }; export const mockSuggestedColors = { diff --git a/yarn.lock b/yarn.lock index 2d08a5bdbce..24db1e176d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -889,7 +889,7 @@ resolved "https://registry.yarnpkg.com/@gitlab/favicon-overlay/-/favicon-overlay-2.0.0.tgz#2f32d0b6a4d5b8ac44e2927083d9ab478a78c984" integrity sha512-GNcORxXJ98LVGzOT9dDYKfbheqH6lNgPDD72lyXRnQIH7CjgGyos8i17aSBPq1f4s3zF3PyedFiAR4YEZbva2Q== -"@gitlab/stylelint-config@^2.2.0": +"@gitlab/stylelint-config@2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@gitlab/stylelint-config/-/stylelint-config-2.2.0.tgz#f0139c8bd29525b51ee9f16d26b66283bd2be5bb" integrity sha512-yLBwRu/geN7nGzoOtF6VV2Fbjhcu2w3PwVnJ5/6wX3MILLO7Wh8zzurIjjSnls8124WUoD7n51Catrjl0hyqDw==