diff --git a/Gemfile.lock b/Gemfile.lock index 9a22dc0562a..d47cec9f06d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1277,7 +1277,7 @@ GEM validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) - validate_url (1.0.8) + validate_url (1.0.13) activemodel (>= 3.0.0) public_suffix validates_hostname (1.0.11) diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue index 7818d45507e..ce4590bd755 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column.vue @@ -1,38 +1,23 @@ + diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue new file mode 100644 index 00000000000..99699943e02 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue @@ -0,0 +1,122 @@ + + + diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index f5f2bd6b74c..e84f1b3359a 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -157,8 +157,8 @@ export default { }, }) .then(({ data }) => { - if (data?.boardListCreate?.errors.length) { - commit(types.CREATE_LIST_FAILURE); + if (data.boardListCreate?.errors.length) { + commit(types.CREATE_LIST_FAILURE, data.boardListCreate.errors[0]); } else { const list = data.boardListCreate?.list; dispatch('addList', list); diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 99a3f3c8f86..96e7812fbe2 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -60,8 +60,11 @@ export default { state.filterParams = filterParams; }, - [mutationTypes.CREATE_LIST_FAILURE]: (state) => { - state.error = s__('Boards|An error occurred while creating the list. Please try again.'); + [mutationTypes.CREATE_LIST_FAILURE]: ( + state, + error = s__('Boards|An error occurred while creating the list. Please try again.'), + ) => { + state.error = error; }, [mutationTypes.RECEIVE_LABELS_REQUEST]: (state) => { diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js index 3ad95fb1318..3e09b1796b1 100644 --- a/app/assets/javascripts/pages/dashboard/issues/index.js +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -4,13 +4,11 @@ import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import projectSelect from '~/project_select'; -document.addEventListener('DOMContentLoaded', () => { - initFilteredSearch({ - page: FILTERED_SEARCH.ISSUES, - filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, - useDefaultState: true, - }); - - projectSelect(); - initManualOrdering(); +initFilteredSearch({ + page: FILTERED_SEARCH.ISSUES, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, + useDefaultState: true, }); + +projectSelect(); +initManualOrdering(); diff --git a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js index ba4b271f09e..a8698e10c57 100644 --- a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js +++ b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js @@ -1,13 +1,11 @@ import IntegrationSettingsForm from '~/integrations/integration_settings_form'; import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; -document.addEventListener('DOMContentLoaded', () => { - const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring'); - const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); - integrationSettingsForm.init(); +const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring'); +const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); +integrationSettingsForm.init(); - if (prometheusSettingsWrapper) { - const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); - prometheusMetrics.loadActiveMetrics(); - } -}); +if (prometheusSettingsWrapper) { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); +} diff --git a/app/assets/javascripts/pages/projects/blob/edit/index.js b/app/assets/javascripts/pages/projects/blob/edit/index.js index 189053f3ed7..ed416610173 100644 --- a/app/assets/javascripts/pages/projects/blob/edit/index.js +++ b/app/assets/javascripts/pages/projects/blob/edit/index.js @@ -1,3 +1,3 @@ import initBlobBundle from '~/blob_edit/blob_bundle'; -document.addEventListener('DOMContentLoaded', initBlobBundle); +initBlobBundle(); diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 61ff1c95a38..10bac6d60c2 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -7,61 +7,59 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import '~/sourcegraph/load'; -document.addEventListener('DOMContentLoaded', () => { - new BlobViewer(); // eslint-disable-line no-new - initBlob(); +new BlobViewer(); // eslint-disable-line no-new +initBlob(); - const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); - const statusLink = document.querySelector('.commit-actions .ci-status-link'); - if (statusLink) { - statusLink.remove(); - // eslint-disable-next-line no-new - new Vue({ - el: CommitPipelineStatusEl, - components: { - commitPipelineStatus, - }, - render(createElement) { - return createElement('commit-pipeline-status', { - props: { - endpoint: CommitPipelineStatusEl.dataset.endpoint, - }, - }); - }, - }); - } +const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); +const statusLink = document.querySelector('.commit-actions .ci-status-link'); +if (statusLink) { + statusLink.remove(); + // eslint-disable-next-line no-new + new Vue({ + el: CommitPipelineStatusEl, + components: { + commitPipelineStatus, + }, + render(createElement) { + return createElement('commit-pipeline-status', { + props: { + endpoint: CommitPipelineStatusEl.dataset.endpoint, + }, + }); + }, + }); +} - initWebIdeLink({ el: document.getElementById('js-blob-web-ide-link') }); +initWebIdeLink({ el: document.getElementById('js-blob-web-ide-link') }); - GpgBadges.fetch(); +GpgBadges.fetch(); - const codeNavEl = document.getElementById('js-code-navigation'); +const codeNavEl = document.getElementById('js-code-navigation'); - if (codeNavEl) { - const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset; +if (codeNavEl) { + const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset; - // eslint-disable-next-line promise/catch-or-return - import('~/code_navigation').then((m) => - m.default({ - blobs: [{ path: blobPath, codeNavigationPath }], - definitionPathPrefix, - }), - ); - } + // eslint-disable-next-line promise/catch-or-return + import('~/code_navigation').then((m) => + m.default({ + blobs: [{ path: blobPath, codeNavigationPath }], + definitionPathPrefix, + }), + ); +} - const successPipelineEl = document.querySelector('.js-success-pipeline-modal'); +const successPipelineEl = document.querySelector('.js-success-pipeline-modal'); - if (successPipelineEl) { - // eslint-disable-next-line no-new - new Vue({ - el: successPipelineEl, - render(createElement) { - return createElement(PipelineTourSuccessModal, { - props: { - ...successPipelineEl.dataset, - }, - }); - }, - }); - } -}); +if (successPipelineEl) { + // eslint-disable-next-line no-new + new Vue({ + el: successPipelineEl, + render(createElement) { + return createElement(PipelineTourSuccessModal, { + props: { + ...successPipelineEl.dataset, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index b22287a0093..58ceb524360 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -15,9 +15,7 @@ function initUserProfile(action) { }); } -document.addEventListener('DOMContentLoaded', () => { - const page = $('body').attr('data-page'); - const action = page.split(':')[1]; - initUserProfile(action); - new UserCallout(); // eslint-disable-line no-new -}); +const page = $('body').attr('data-page'); +const action = page.split(':')[1]; +initUserProfile(action); +new UserCallout(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue new file mode 100644 index 00000000000..4cdfc5e947a --- /dev/null +++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue @@ -0,0 +1,218 @@ + + diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 349297c776c..290a9af859b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -196,8 +196,8 @@ class MergeRequest < ApplicationRecord end event :mark_as_unchecked do - transition [:preparing, :can_be_merged, :checking, :unchecked] => :unchecked - transition [:cannot_be_merged, :cannot_be_merged_rechecking, :cannot_be_merged_recheck] => :cannot_be_merged_recheck + transition [:preparing, :can_be_merged, :checking] => :unchecked + transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck end event :mark_as_checking do @@ -326,7 +326,7 @@ class MergeRequest < ApplicationRecord scope :preload_approved_by_users, -> { preload(:approved_by_users) } scope :preload_metrics, -> (relation) { preload(metrics: relation) } scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) } - scope :preload_latest_diff_comment, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) } + scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) } scope :with_web_entity_associations, -> { preload(:author, :target_project) } scope :with_auto_merge_enabled, -> do diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 32b1079df1a..89af2b772e1 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -75,7 +75,7 @@ module MergeRequests commit_ids = @commits.map(&:id) merge_requests = @project.merge_requests.opened .preload_project_and_latest_diff - .preload_latest_diff_comment + .preload_latest_diff_commit .where(target_branch: @push.branch_name).to_a .select(&:diff_head_commit) .select do |merge_request| diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 4bc7823faaa..ae0d8bba9d4 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -205,6 +205,7 @@ module MergeRequests new_assignees = merge_request.assignees - old_assignees merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees) + merge_request_activity_counter.track_assignees_changed_action(user: current_user) end def handle_reviewers_change(merge_request, old_reviewers) @@ -216,6 +217,7 @@ module MergeRequests new_reviewers = merge_request.reviewers - old_reviewers merge_request_activity_counter.track_users_review_requested(users: new_reviewers) + merge_request_activity_counter.track_reviewers_changed_action(user: current_user) end def create_branch_change_note(issuable, branch_type, event_type, old_branch, new_branch) diff --git a/app/views/projects/blame/_age_map_legend.html.haml b/app/views/projects/blame/_age_map_legend.html.haml deleted file mode 100644 index 533dc20ffb3..00000000000 --- a/app/views/projects/blame/_age_map_legend.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%span.left-label Newer -%span.legend-box.legend-box-0 -%span.legend-box.legend-box-1 -%span.legend-box.legend-box-2 -%span.legend-box.legend-box-3 -%span.legend-box.legend-box-4 -%span.legend-box.legend-box-5 -%span.legend-box.legend-box-6 -%span.legend-box.legend-box-7 -%span.legend-box.legend-box-8 -%span.legend-box.legend-box-9 -%span.right-label Older diff --git a/app/views/projects/blame/_blame_group.html.haml b/app/views/projects/blame/_blame_group.html.haml deleted file mode 100644 index e9967814833..00000000000 --- a/app/views/projects/blame/_blame_group.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -%tr - %td.blame-commit{ class: commit_data.age_map_class } - .commit - = commit_data.author_avatar - .commit-row-title - %span.item-title.str-truncated-100 - = commit_data.commit_link - %span - = commit_data.project_blame_link -   - .light - = commit_data.commit_author_link - = _('committed') - #{commit_data.time_ago_tooltip} - %td.line-numbers - - line_count = blame_group[:lines].count - - (current_line...(current_line + line_count)).each do |i| - %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } - = link_icon - = i - \ - %td.lines - %pre.code.highlight - %code - - blame_group[:lines].each do |line| - #{line} diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 2f3d0660caa..049920e55c5 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -6,18 +6,56 @@ .file-holder = render "projects/blob/header", blob: @blob, blame: true + .file-blame-legend - = render 'age_map_legend' + %span.left-label Newer + %span.legend-box.legend-box-0 + %span.legend-box.legend-box-1 + %span.legend-box.legend-box-2 + %span.legend-box.legend-box-3 + %span.legend-box.legend-box-4 + %span.legend-box.legend-box-5 + %span.legend-box.legend-box-6 + %span.legend-box.legend-box-7 + %span.legend-box.legend-box-8 + %span.legend-box.legend-box-9 + %span.right-label Older + .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 - @blame.groups.each do |blame_group| - commit_data = @blame.commit_data(blame_group[:commit]) + - line_count = blame_group[:lines].count - = render 'blame_group', - blame_group: blame_group, - current_line: current_line, - link_icon: link_icon, - commit_data: commit_data + %tr + %td.blame-commit{ class: commit_data.age_map_class } + .commit + = commit_data.author_avatar - - current_line += blame_group[:lines].count + .commit-row-title + %span.item-title.str-truncated-100 + = commit_data.commit_link + %span + = commit_data.project_blame_link +   + + .light + = commit_data.commit_author_link + = _('committed') + #{commit_data.time_ago_tooltip} + + %td.line-numbers + - (current_line...(current_line + line_count)).each do |i| + %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i } + = link_icon + = i + \ + + %td.lines + %pre.code.highlight + %code + - blame_group[:lines].each do |line| + #{line} + + - current_line += line_count diff --git a/app/views/shared/_confirm_fork_modal.html.haml b/app/views/shared/_confirm_fork_modal.html.haml index 1390d821899..b692dffce37 100644 --- a/app/views/shared/_confirm_fork_modal.html.haml +++ b/app/views/shared/_confirm_fork_modal.html.haml @@ -8,5 +8,5 @@ .modal-body.p-3 %p= _("You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.") % { tag_start: '', tag_end: ''} .modal-footer - = link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal" - = link_to _('Fork project'), fork_path, class: 'btn btn-success', data: { qa_selector: 'fork_project_button' }, method: :post + = link_to _('Cancel'), '#', class: "gl-button btn btn-default btn-cancel", "data-dismiss" => "modal" + = link_to _('Fork project'), fork_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'fork_project_button' }, method: :post diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index b327b6f7ee8..3817ff8a56d 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,7 +1,7 @@ - if any_projects?(@projects) .project-item-select-holder.btn-group.gl-ml-auto.gl-mr-auto.gl-py-3.gl-relative.gl-display-flex.gl-overflow-hidden - %a.btn.gl-button.btn-success.new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" } + %a.btn.gl-button.btn-confirm.new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" } = loading_icon(color: 'light') = project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled] - %button.btn.dropdown-toggle.btn-success.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') } + %button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') } = sprite_icon('chevron-down') diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml index aa9e9a34c90..f524747dea0 100644 --- a/app/views/shared/_recaptcha_form.html.haml +++ b/app/views/shared/_recaptcha_form.html.haml @@ -20,4 +20,4 @@ - if has_submit .row-content-block.footer-block - = f.submit _("Submit %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'btn btn-success' + = f.submit _("Submit %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm' diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml index 1d3bc1d6959..506954c53ca 100644 --- a/app/views/shared/groups/_empty_state.html.haml +++ b/app/views/shared/groups/_empty_state.html.haml @@ -9,5 +9,5 @@ - if invite_group_members?(@group) = link_to _('Invite your team'), group_group_members_path(@group), - class: 'gl-button btn btn-success-secondary', + class: 'gl-button btn btn-confirm-secondary', data: { track_event: 'click_invite_team_group_empty_state', track_label: 'invite_team_group_empty_state' } diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index c0c009f2a86..32b1af50a01 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -4,7 +4,6 @@ - stars = true unless local_assigns[:stars] == false - forks = true unless local_assigns[:forks] == false - merge_requests = true unless local_assigns[:merge_requests] == false -- issues = true unless local_assigns[:issues] == false - pipeline_status = true unless local_assigns[:pipeline_status] == false - skip_namespace = false unless local_assigns[:skip_namespace] == true - user = local_assigns[:user] @@ -41,7 +40,7 @@ = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, use_creator_avatar: use_creator_avatar, forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests, - issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode + issues: project.issues_enabled?, pipeline_status: pipeline_status, compact_mode: compact_mode = paginate_collection(projects, remote: remote) unless skip_pagination - else - if @contributed_projects diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml index 5fd22665633..d91e3c73c49 100644 --- a/app/views/shared/wikis/_form.html.haml +++ b/app/views/shared/wikis/_form.html.haml @@ -70,10 +70,10 @@ .form-actions - if @page && @page.persisted? - = f.submit _("Save changes"), class: 'btn gl-button btn-success qa-save-changes-button js-wiki-btn-submit', disabled: 'true' + = f.submit _("Save changes"), class: 'btn gl-button btn-confirm qa-save-changes-button js-wiki-btn-submit', disabled: 'true' .float-right = link_to _("Cancel"), wiki_page_path(@wiki, @page), class: 'btn gl-button btn-cancel btn-default' - else - = f.submit s_("Wiki|Create page"), class: 'btn-success gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true' + = f.submit s_("Wiki|Create page"), class: 'btn-confirm gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true' .float-right = link_to _("Cancel"), wiki_path(@wiki), class: 'btn gl-button btn-cancel btn-default' diff --git a/app/views/shared/wikis/_main_links.html.haml b/app/views/shared/wikis/_main_links.html.haml index 8568c36559a..02794950895 100644 --- a/app/views/shared/wikis/_main_links.html.haml +++ b/app/views/shared/wikis/_main_links.html.haml @@ -2,5 +2,5 @@ = link_to wiki_page_path(@wiki, @page, action: :history), class: "btn gl-button", role: "button", data: { qa_selector: 'page_history_button' } do = s_("Wiki|Page history") - if can?(current_user, :create_wiki, @wiki.container) - = link_to wiki_path(@wiki, action: :new), class: "btn gl-button btn-success btn-inverted", role: "button", data: { qa_selector: 'new_page_button' } do + = link_to wiki_path(@wiki, action: :new), class: "btn gl-button btn-confirm-secondary", role: "button", data: { qa_selector: 'new_page_button' } do = s_("Wiki|New page") diff --git a/changelogs/unreleased/292824-track-assignee-reviewer-changes.yml b/changelogs/unreleased/292824-track-assignee-reviewer-changes.yml new file mode 100644 index 00000000000..fd404fc80d5 --- /dev/null +++ b/changelogs/unreleased/292824-track-assignee-reviewer-changes.yml @@ -0,0 +1,5 @@ +--- +title: Add tracking to merge request assignees/reviewers changes +merge_request: 55486 +author: +type: other diff --git a/changelogs/unreleased/321593-hide-issue-counts-in-project-list-where-issues-disabled.yml b/changelogs/unreleased/321593-hide-issue-counts-in-project-list-where-issues-disabled.yml new file mode 100644 index 00000000000..7d53db8fa13 --- /dev/null +++ b/changelogs/unreleased/321593-hide-issue-counts-in-project-list-where-issues-disabled.yml @@ -0,0 +1,5 @@ +--- +title: Hide issue count and link in project list for projects with disabled issues +merge_request: 54275 +author: Simon Stieger @sim0 +type: fixed diff --git a/changelogs/unreleased/blame-performance.yml b/changelogs/unreleased/blame-performance.yml new file mode 100644 index 00000000000..ef19b47c412 --- /dev/null +++ b/changelogs/unreleased/blame-performance.yml @@ -0,0 +1,5 @@ +--- +title: Refactor blame view +merge_request: 55488 +author: +type: performance diff --git a/changelogs/unreleased/btn-confirm-shared-groups.yml b/changelogs/unreleased/btn-confirm-shared-groups.yml new file mode 100644 index 00000000000..0f97ca6e9c8 --- /dev/null +++ b/changelogs/unreleased/btn-confirm-shared-groups.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in shared/groups directory +merge_request: 55302 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/btn-confirm-shared-wikis.yml b/changelogs/unreleased/btn-confirm-shared-wikis.yml new file mode 100644 index 00000000000..fd9fefefc6c --- /dev/null +++ b/changelogs/unreleased/btn-confirm-shared-wikis.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in shared/wikis directory +merge_request: 55316 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/btn-confirm-shared.yml b/changelogs/unreleased/btn-confirm-shared.yml new file mode 100644 index 00000000000..9b5fc864289 --- /dev/null +++ b/changelogs/unreleased/btn-confirm-shared.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in shared directory +merge_request: 55317 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/test_update_merge_request_worker_performance_2.yml b/changelogs/unreleased/test_update_merge_request_worker_performance_2.yml new file mode 100644 index 00000000000..1a307918e27 --- /dev/null +++ b/changelogs/unreleased/test_update_merge_request_worker_performance_2.yml @@ -0,0 +1,5 @@ +--- +title: Remove unneeded transitions on MR for mark_as_unchecked event +merge_request: 53537 +author: +type: performance diff --git a/changelogs/unreleased/update-validate-url-gem.yml b/changelogs/unreleased/update-validate-url-gem.yml new file mode 100644 index 00000000000..9500d70b11f --- /dev/null +++ b/changelogs/unreleased/update-validate-url-gem.yml @@ -0,0 +1,5 @@ +--- +title: Update validate_url gem +merge_request: 55706 +author: +type: fixed diff --git a/config/feature_flags/development/usage_data_i_code_review_user_assignees_changed.yml b/config/feature_flags/development/usage_data_i_code_review_user_assignees_changed.yml new file mode 100644 index 00000000000..ac7edd86e5f --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_assignees_changed.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_i_code_review_user_assignees_changed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +rollout_issue_url: +milestone: '13.10' +type: development +group: group::code review +default_enabled: true diff --git a/config/feature_flags/development/usage_data_i_code_review_user_reviewers_changed.yml b/config/feature_flags/development/usage_data_i_code_review_user_reviewers_changed.yml new file mode 100644 index 00000000000..10084b70ee8 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_reviewers_changed.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_i_code_review_user_reviewers_changed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +rollout_issue_url: +milestone: '13.10' +type: development +group: group::code review +default_enabled: true diff --git a/config/metrics/counts_28d/20210302114145_i_code_review_user_assignees_changed_monthly.yml b/config/metrics/counts_28d/20210302114145_i_code_review_user_assignees_changed_monthly.yml new file mode 100644 index 00000000000..83d06db1fb3 --- /dev/null +++ b/config/metrics/counts_28d/20210302114145_i_code_review_user_assignees_changed_monthly.yml @@ -0,0 +1,20 @@ +--- +key_path: redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly +description: Count of unique users per month who changed assignees of a MR +product_section: dev +product_stage: create +product_group: group::code review +product_category: code_review +value_type: number +status: implemented +milestone: "13.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +time_frame: 28d +data_source: redis_hll +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20210302114219_i_code_review_user_reviewers_changed_monthly.yml b/config/metrics/counts_28d/20210302114219_i_code_review_user_reviewers_changed_monthly.yml new file mode 100644 index 00000000000..9cf18201916 --- /dev/null +++ b/config/metrics/counts_28d/20210302114219_i_code_review_user_reviewers_changed_monthly.yml @@ -0,0 +1,20 @@ +--- +key_path: redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly +description: Count of unique users per month who changed reviewers of a MR +product_section: dev +product_stage: create +product_group: group::code review +product_category: code_review +value_type: number +status: implemented +milestone: "13.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +time_frame: 28d +data_source: redis_hll +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20210302114202_i_code_review_user_assignees_changed_weekly.yml b/config/metrics/counts_7d/20210302114202_i_code_review_user_assignees_changed_weekly.yml new file mode 100644 index 00000000000..334114dd64c --- /dev/null +++ b/config/metrics/counts_7d/20210302114202_i_code_review_user_assignees_changed_weekly.yml @@ -0,0 +1,20 @@ +--- +key_path: redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly +description: Count of unique users per week who changed assignees of a MR +product_section: dev +product_stage: create +product_group: group::code review +product_category: code_review +value_type: number +status: implemented +milestone: "13.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +time_frame: 7d +data_source: redis_hll +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20210302114235_i_code_review_user_reviewers_changed_weekly.yml b/config/metrics/counts_7d/20210302114235_i_code_review_user_reviewers_changed_weekly.yml new file mode 100644 index 00000000000..a70d0b6204d --- /dev/null +++ b/config/metrics/counts_7d/20210302114235_i_code_review_user_reviewers_changed_weekly.yml @@ -0,0 +1,20 @@ +--- +key_path: redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly +description: Count of unique users per week who changed reviewers of a MR +product_section: dev +product_stage: create +product_group: group::code review +product_category: code_review +value_type: number +status: implemented +milestone: "13.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486 +time_frame: 7d +data_source: redis_hll +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 16ab43eff1a..778048983c2 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -13004,6 +13004,46 @@ Missing description | `tier` | | | `skip_validation` | true | +## `redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly` + +Count of unique users per month who changed assignees of a MR + +| field | value | +| --- | --- | +| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_assignees_changed_monthly`** | +| `product_section` | dev | +| `product_stage` | create | +| `product_group` | `group::code review` | +| `product_category` | `code_review` | +| `value_type` | number | +| `status` | implemented | +| `milestone` | 13.10 | +| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) | +| `time_frame` | 28d | +| `data_source` | Redis_hll | +| `distribution` | ce, ee | +| `tier` | free, premium, ultimate | + +## `redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly` + +Count of unique users per week who changed assignees of a MR + +| field | value | +| --- | --- | +| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_assignees_changed_weekly`** | +| `product_section` | dev | +| `product_stage` | create | +| `product_group` | `group::code review` | +| `product_category` | `code_review` | +| `value_type` | number | +| `status` | implemented | +| `milestone` | 13.10 | +| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) | +| `time_frame` | 7d | +| `data_source` | Redis_hll | +| `distribution` | ce, ee | +| `tier` | free, premium, ultimate | + ## `redis_hll_counters.code_review.i_code_review_user_close_mr_monthly` Count of unique users per week|month who closed a MR @@ -13692,6 +13732,46 @@ Missing description | `tier` | | | `skip_validation` | true | +## `redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly` + +Count of unique users per month who changed reviewers of a MR + +| field | value | +| --- | --- | +| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_reviewers_changed_monthly`** | +| `product_section` | dev | +| `product_stage` | create | +| `product_group` | `group::code review` | +| `product_category` | `code_review` | +| `value_type` | number | +| `status` | implemented | +| `milestone` | 13.10 | +| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) | +| `time_frame` | 28d | +| `data_source` | Redis_hll | +| `distribution` | ce, ee | +| `tier` | free, premium, ultimate | + +## `redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly` + +Count of unique users per week who changed reviewers of a MR + +| field | value | +| --- | --- | +| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_reviewers_changed_weekly`** | +| `product_section` | dev | +| `product_stage` | create | +| `product_group` | `group::code review` | +| `product_category` | `code_review` | +| `value_type` | number | +| `status` | implemented | +| `milestone` | 13.10 | +| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55486) | +| `time_frame` | 7d | +| `data_source` | Redis_hll | +| `distribution` | ce, ee | +| `tier` | free, premium, ultimate | + ## `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly` Count of unique users per week|month with diffs viewed file by file diff --git a/lefthook.yml b/lefthook.yml index 8791cf61231..9284b872e7f 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -5,36 +5,36 @@ pre-push: run: bundle exec danger dry_run eslint: tags: frontend style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "*.{js,vue}" run: yarn run lint:eslint {files} haml-lint: tags: view haml style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "*.html.haml" run: bundle exec haml-lint --config .haml-lint.yml {files} markdownlint: tags: documentation style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "doc/*.md" run: yarn markdownlint {files} stylelint: tags: stylesheet css style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "*.scss{,.css}" run: yarn stylelint -q {files} prettier: tags: frontend style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "*.{js,vue,graphql}" run: yarn run prettier --check {files} rubocop: tags: backend style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "*.rb" run: bundle exec rubocop --parallel --force-exclusion {files} vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters tags: documentation style - files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD + files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: "doc/*.md" run: if command -v vale 2> /dev/null; then vale --config .vale.ini --minAlertLevel error {files}; else echo "Vale not found. Install Vale"; fi diff --git a/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml b/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml index f19de850490..72c3fb4fc8d 100644 --- a/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml +++ b/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml @@ -46,7 +46,9 @@ 'i_code_review_user_mr_discussion_locked', 'i_code_review_user_mr_discussion_unlocked', 'i_code_review_user_time_estimate_changed', - 'i_code_review_user_time_spent_changed' + 'i_code_review_user_time_spent_changed', + 'i_code_review_user_assignees_changed', + 'i_code_review_user_reviewers_changed' ] - name: code_review_category_monthly_active_users operator: OR @@ -86,7 +88,9 @@ 'i_code_review_user_mr_discussion_locked', 'i_code_review_user_mr_discussion_unlocked', 'i_code_review_user_time_estimate_changed', - 'i_code_review_user_time_spent_changed' + 'i_code_review_user_time_spent_changed', + 'i_code_review_user_assignees_changed', + 'i_code_review_user_reviewers_changed' ] - name: code_review_extension_category_monthly_active_users operator: OR diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml index 83a932456f9..5a7084f346e 100644 --- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml @@ -184,3 +184,13 @@ category: code_review aggregation: weekly feature_flag: usage_data_i_code_review_user_time_spent_changed +- name: i_code_review_user_assignees_changed + redis_slot: code_review + category: code_review + aggregation: weekly + feature_flag: usage_data_i_code_review_user_assignees_changed +- name: i_code_review_user_reviewers_changed + redis_slot: code_review + category: code_review + aggregation: weekly + feature_flag: usage_data_i_code_review_user_reviewers_changed diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb index daa4c34ae1f..25f985f9953 100644 --- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb @@ -39,6 +39,8 @@ module Gitlab MR_DISCUSSION_UNLOCKED_ACTION = 'i_code_review_user_mr_discussion_unlocked' MR_TIME_ESTIMATE_CHANGED_ACTION = 'i_code_review_user_time_estimate_changed' MR_TIME_SPENT_CHANGED_ACTION = 'i_code_review_user_time_spent_changed' + MR_ASSIGNEES_CHANGED_ACTION = 'i_code_review_user_assignees_changed' + MR_REVIEWERS_CHANGED_ACTION = 'i_code_review_user_reviewers_changed' class << self def track_mr_diffs_action(merge_request:) @@ -173,6 +175,14 @@ module Gitlab track_unique_action_by_user(MR_TIME_SPENT_CHANGED_ACTION, user) end + def track_assignees_changed_action(user:) + track_unique_action_by_user(MR_ASSIGNEES_CHANGED_ACTION, user) + end + + def track_reviewers_changed_action(user:) + track_unique_action_by_user(MR_REVIEWERS_CHANGED_ACTION, user) + end + private def track_unique_action_by_merge_request(action, merge_request) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e7916e963f7..0f9160ca50e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1330,7 +1330,7 @@ msgstr "" msgid "A job artifact is an archive of files and directories saved by a job when it finishes." msgstr "" -msgid "A label list displays all issues with the selected label." +msgid "A label list displays issues with the selected label." msgstr "" msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies." @@ -12053,6 +12053,9 @@ msgstr "" msgid "Error uploading file" msgstr "" +msgid "Error uploading file. Please try again." +msgstr "" + msgid "Error uploading file: %{stripped}" msgstr "" @@ -20298,7 +20301,7 @@ msgstr "" msgid "New label" msgstr "" -msgid "New label list" +msgid "New list" msgstr "" msgid "New merge request" @@ -20520,9 +20523,6 @@ msgstr "" msgid "No label" msgstr "" -msgid "No label selected" -msgstr "" - msgid "No labels with such name or description" msgstr "" @@ -25023,6 +25023,9 @@ msgstr "" msgid "Remove due date" msgstr "" +msgid "Remove file" +msgstr "" + msgid "Remove fork relationship" msgstr "" @@ -28386,6 +28389,9 @@ msgstr "" msgid "Start a new merge request" msgstr "" +msgid "Start a new merge request with these changes" +msgstr "" + msgid "Start a review" msgstr "" diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js new file mode 100644 index 00000000000..3702f55f17b --- /dev/null +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -0,0 +1,166 @@ +import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; +import defaultState from '~/boards/stores/state'; +import { mockLabelList } from '../mock_data'; + +Vue.use(Vuex); + +describe('Board card layout', () => { + let wrapper; + + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { + return new Vuex.Store({ + state: { + ...defaultState, + ...state, + }, + actions, + getters, + }); + }; + + const mountComponent = ({ + loading = false, + formDescription = '', + searchLabel = '', + searchPlaceholder = '', + selectedId, + actions, + slots, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardAddNewColumnForm, { + stubs: { + GlFormGroup: true, + }, + propsData: { + loading, + formDescription, + searchLabel, + searchPlaceholder, + selectedId, + }, + slots, + store: createStore({ + actions: { + setAddColumnFormVisibility: jest.fn(), + ...actions, + }, + }), + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); + const findSearchInput = () => wrapper.find(GlSearchBoxByType); + const findSearchLabel = () => wrapper.find(GlFormGroup); + const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); + const submitButton = () => wrapper.findByTestId('addNewColumnButton'); + + it('shows form title & search input', () => { + mountComponent(); + + expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); + expect(findSearchInput().exists()).toBe(true); + }); + + it('clicking cancel hides the form', () => { + const setAddColumnFormVisibility = jest.fn(); + mountComponent({ + actions: { + setAddColumnFormVisibility, + }, + }); + + cancelButton().vm.$emit('click'); + + expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); + }); + + it('sets placeholder and description from props', () => { + const props = { + formDescription: 'Some description of a list', + }; + + mountComponent(props); + + expect(wrapper.html()).toHaveText(props.formDescription); + }); + + describe('items', () => { + const mountWithItems = (loading) => + mountComponent({ + loading, + slots: { + items: '
Some kind of list
', + }, + }); + + it('hides items slot and shows skeleton while loading', () => { + mountWithItems(true); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.find('.item-slot').exists()).toBe(false); + }); + + it('shows items slot and hides skeleton while not loading', () => { + mountWithItems(false); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); + expect(wrapper.find('.item-slot').exists()).toBe(true); + }); + }); + + describe('search box', () => { + it('sets label and placeholder text from props', () => { + const props = { + searchLabel: 'Some items', + searchPlaceholder: 'Search for an item', + }; + + mountComponent(props); + + expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel); + expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder); + }); + + it('emits filter event on input', () => { + mountComponent(); + + const searchText = 'some text'; + + findSearchInput().vm.$emit('input', searchText); + + expect(wrapper.emitted('filter-items')).toEqual([[searchText]]); + }); + }); + + describe('Add list button', () => { + it('is disabled if no item is selected', () => { + mountComponent(); + + expect(submitButton().props('disabled')).toBe(true); + }); + + it('emits add-list event on click', async () => { + mountComponent({ + selectedId: mockLabelList.label.id, + }); + + await nextTick(); + + submitButton().vm.$emit('click'); + + expect(wrapper.emitted('add-list')).toEqual([[]]); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index 84b6d7abb1e..60584eaf6cf 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -1,9 +1,9 @@ -import { GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import defaultState from '~/boards/stores/state'; import { mockLabelList } from '../mock_data'; @@ -11,7 +11,6 @@ Vue.use(Vuex); describe('Board card layout', () => { let wrapper; - let shouldUseGraphQL; const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { return new Vuex.Store({ @@ -25,19 +24,16 @@ describe('Board card layout', () => { }; const mountComponent = ({ - selectedLabelId, + selectedId, labels = [], getListByLabelId = jest.fn(), actions = {}, } = {}) => { wrapper = extendedWrapper( shallowMount(BoardAddNewColumn, { - stubs: { - GlFormGroup: true, - }, data() { return { - selectedLabelId, + selectedId, }; }, store: createStore({ @@ -47,12 +43,13 @@ describe('Board card layout', () => { ...actions, }, getters: { - shouldUseGraphQL: () => shouldUseGraphQL, + shouldUseGraphQL: () => true, getListByLabelId: () => getListByLabelId, }, state: { labels, labelsLoading: false, + isEpicBoard: false, }, }), provide: { @@ -64,65 +61,32 @@ describe('Board card layout', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; - }); - - const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); - const findSearchInput = () => wrapper.find(GlSearchBoxByType); - const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); - const submitButton = () => wrapper.findByTestId('addNewColumnButton'); - - beforeEach(() => { - shouldUseGraphQL = true; - }); - - it('shows form title & search input', () => { - mountComponent(); - - expect(formTitle()).toEqual(BoardAddNewColumn.i18n.newLabelList); - expect(findSearchInput().exists()).toBe(true); - }); - - it('clicking cancel hides the form', () => { - const setAddColumnFormVisibility = jest.fn(); - mountComponent({ - actions: { - setAddColumnFormVisibility, - }, - }); - - cancelButton().vm.$emit('click'); - - expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); }); describe('Add list button', () => { - it('is disabled if no item is selected', () => { - mountComponent(); - - expect(submitButton().props('disabled')).toBe(true); - }); - - it('adds a new list on click', async () => { - const labelId = mockLabelList.label.id; + it('calls addList', async () => { + const getListByLabelId = jest.fn().mockReturnValue(null); const highlightList = jest.fn(); const createList = jest.fn(); mountComponent({ labels: [mockLabelList.label], - selectedLabelId: labelId, + selectedId: mockLabelList.label.id, + getListByLabelId, actions: { createList, highlightList, }, }); + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + await nextTick(); - submitButton().vm.$emit('click'); - expect(highlightList).not.toHaveBeenCalled(); - expect(createList).toHaveBeenCalledWith(expect.anything(), { labelId }); + expect(createList).toHaveBeenCalledWith(expect.anything(), { + labelId: mockLabelList.label.id, + }); }); it('highlights existing list if trying to re-add', async () => { @@ -132,7 +96,7 @@ describe('Board card layout', () => { mountComponent({ labels: [mockLabelList.label], - selectedLabelId: mockLabelList.label.id, + selectedId: mockLabelList.label.id, getListByLabelId, actions: { createList, @@ -140,9 +104,9 @@ describe('Board card layout', () => { }, }); - await nextTick(); + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); - submitButton().vm.$emit('click'); + await nextTick(); expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id); expect(createList).not.toHaveBeenCalled(); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 9e1b5018cc1..377c606ac92 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -293,7 +293,7 @@ describe('createIssueList', () => { data: { boardListCreate: { list: {}, - errors: [{ foo: 'bar' }], + errors: ['foo'], }, }, }), @@ -301,7 +301,7 @@ describe('createIssueList', () => { await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true }); - expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE); + expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE, 'foo'); }); it('highlights list and does not re-query if it already exists', async () => { diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js new file mode 100644 index 00000000000..6e3cbad558d --- /dev/null +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -0,0 +1,193 @@ +import { GlModal, GlFormInput, GlFormTextarea, GlToggle, GlAlert } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { visitUrl } from '~/lib/utils/url_utility'; +import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + joinPaths: () => '/new_upload', +})); + +const initialProps = { + modalId: 'upload-blob', + commitMessage: 'Upload New File', + targetBranch: 'master', + origionalBranch: 'master', + canPushCode: true, + path: 'new_upload', +}; + +describe('UploadBlobModal', () => { + let wrapper; + let mock; + + const mockEvent = { preventDefault: jest.fn() }; + + const createComponent = (props) => { + wrapper = shallowMount(UploadBlobModal, { + propsData: { + ...initialProps, + ...props, + }, + mocks: { + $route: { + params: { + path: '', + }, + }, + }, + }); + }; + + const findModal = () => wrapper.find(GlModal); + const findAlert = () => wrapper.find(GlAlert); + const findCommitMessage = () => wrapper.find(GlFormTextarea); + const findBranchName = () => wrapper.find(GlFormInput); + const findMrToggle = () => wrapper.find(GlToggle); + const findUploadDropzone = () => wrapper.find(UploadDropzone); + const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled; + const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled; + const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + canPushCode | displayBranchName | displayForkedBranchMessage + ${true} | ${true} | ${false} + ${false} | ${false} | ${true} + `( + 'canPushCode = $canPushCode', + ({ canPushCode, displayBranchName, displayForkedBranchMessage }) => { + beforeEach(() => { + createComponent({ canPushCode }); + }); + + it('displays the modal', () => { + expect(findModal().exists()).toBe(true); + }); + + it('includes the upload dropzone', () => { + expect(findUploadDropzone().exists()).toBe(true); + }); + + it('includes the commit message', () => { + expect(findCommitMessage().exists()).toBe(true); + }); + + it('displays the disabled upload button', () => { + expect(actionButtonDisabledState()).toBe(true); + }); + + it('displays the enabled cancel button', () => { + expect(cancelButtonDisabledState()).toBe(false); + }); + + it('does not display the MR toggle', () => { + expect(findMrToggle().exists()).toBe(false); + }); + + it(`${ + displayForkedBranchMessage ? 'displays' : 'does not display' + } the forked branch message`, () => { + expect(findAlert().exists()).toBe(displayForkedBranchMessage); + }); + + it(`${displayBranchName ? 'displays' : 'does not display'} the branch name`, () => { + expect(findBranchName().exists()).toBe(displayBranchName); + }); + + if (canPushCode) { + describe('when changing the branch name', () => { + it('displays the MR toggle', async () => { + wrapper.setData({ target: 'Not master' }); + + await wrapper.vm.$nextTick(); + + expect(findMrToggle().exists()).toBe(true); + }); + }); + } + + describe('completed form', () => { + beforeEach(() => { + wrapper.setData({ + file: { type: 'jpg' }, + filePreviewURL: 'http://file.com?format=jpg', + }); + }); + + it('enables the upload button when the form is completed', () => { + expect(actionButtonDisabledState()).toBe(false); + }); + + describe('form submission', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + + findModal().vm.$emit('primary', mockEvent); + }); + + afterEach(() => { + mock.restore(); + }); + + it('disables the upload button', () => { + expect(actionButtonDisabledState()).toBe(true); + }); + + it('sets the upload button to loading', () => { + expect(actionButtonLoadingState()).toBe(true); + }); + }); + + describe('successful response', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' }); + + findModal().vm.$emit('primary', mockEvent); + + await waitForPromises(); + }); + + it('redirects to the uploaded file', () => { + expect(visitUrl).toHaveBeenCalled(); + }); + + afterEach(() => { + mock.restore(); + }); + }); + + describe('error response', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + mock.onPost(initialProps.path).timeout(); + + findModal().vm.$emit('primary', mockEvent); + + await waitForPromises(); + }); + + it('creates a flash error', () => { + expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.'); + }); + + afterEach(() => { + mock.restore(); + }); + }); + }); + }, + ); +}); diff --git a/spec/lib/gitlab/git/push_spec.rb b/spec/lib/gitlab/git/push_spec.rb index 8ba43b2967c..68cef558f6f 100644 --- a/spec/lib/gitlab/git/push_spec.rb +++ b/spec/lib/gitlab/git/push_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Gitlab::Git::Push do it { is_expected.to be_force_push } end - context 'when called muiltiple times' do + context 'when called mulitiple times' do it 'does not make make multiple calls to the force push check' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).once diff --git a/spec/lib/gitlab/kroki_spec.rb b/spec/lib/gitlab/kroki_spec.rb index cb3d41528f5..31d3edd158b 100644 --- a/spec/lib/gitlab/kroki_spec.rb +++ b/spec/lib/gitlab/kroki_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Kroki do describe '.formats' do def default_formats - %w[bytefield c4plantuml ditaa erd graphviz nomnoml plantuml svgbob umlet vega vegalite wavedrow].freeze + %w[bytefield c4plantuml ditaa erd graphviz nomnoml plantuml svgbob umlet vega vegalite wavedrom].freeze end subject { described_class.formats(Gitlab::CurrentSettings) } diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index d740e19ae7b..717c3b8eca4 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -316,4 +316,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl let(:action) { described_class::MR_TIME_SPENT_CHANGED_ACTION } end end + + describe '.track_assignees_changed_action' do + subject { described_class.track_assignees_changed_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_ASSIGNEES_CHANGED_ACTION } + end + end + + describe '.track_reviewers_changed_action' do + subject { described_class.track_reviewers_changed_action(user: user) } + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_REVIEWERS_CHANGED_ACTION } + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 176667c9361..f22ee7b1686 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4105,6 +4105,72 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '#mark_as_unchecked' do + subject { create(:merge_request, source_project: project, merge_status: merge_status) } + + shared_examples 'for an invalid state transition' do + it 'is not a valid state transition' do + expect { subject.mark_as_unchecked! }.to raise_error(StateMachines::InvalidTransition) + end + end + + shared_examples 'for an valid state transition' do + it 'is a valid state transition' do + expect { subject.mark_as_unchecked! } + .to change { subject.merge_status } + .from(merge_status.to_s) + .to(expected_merge_status) + end + end + + context 'when the status is unchecked' do + let(:merge_status) { :unchecked } + + include_examples 'for an invalid state transition' + end + + context 'when the status is checking' do + let(:merge_status) { :checking } + let(:expected_merge_status) { 'unchecked' } + + include_examples 'for an valid state transition' + end + + context 'when the status is preparing' do + let(:merge_status) { :preparing } + let(:expected_merge_status) { 'unchecked' } + + include_examples 'for an valid state transition' + end + + context 'when the status is can_be_merged' do + let(:merge_status) { :can_be_merged } + let(:expected_merge_status) { 'unchecked' } + + include_examples 'for an valid state transition' + end + + context 'when the status is cannot_be_merged_recheck' do + let(:merge_status) { :cannot_be_merged_recheck } + + include_examples 'for an invalid state transition' + end + + context 'when the status is cannot_be_merged' do + let(:merge_status) { :cannot_be_merged } + let(:expected_merge_status) { 'cannot_be_merged_recheck' } + + include_examples 'for an valid state transition' + end + + context 'when the status is cannot_be_merged' do + let(:merge_status) { :cannot_be_merged } + let(:expected_merge_status) { 'cannot_be_merged_recheck' } + + include_examples 'for an valid state transition' + end + end + describe 'transition to cannot_be_merged' do let(:notification_service) { double(:notification_service) } let(:todo_service) { double(:todo_service) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 30d94a2e948..86007c9cc72 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -186,6 +186,54 @@ RSpec.describe MergeRequests::UpdateService, :mailer do MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) end + + context 'assignees' do + context 'when assignees changed' do + it 'tracks assignees changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_assignees_changed_action).once.with(user: user) + + opts[:assignees] = [user2] + + MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + end + + context 'when assignees did not change' do + it 'does not track assignees changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .not_to receive(:track_assignees_changed_action) + + opts[:assignees] = merge_request.assignees + + MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + end + end + + context 'reviewers' do + context 'when reviewers changed' do + it 'tracks reviewers changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_reviewers_changed_action).once.with(user: user) + + opts[:reviewers] = [user2] + + MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + end + + context 'when reviewers did not change' do + it 'does not track reviewers changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .not_to receive(:track_reviewers_changed_action) + + opts[:reviewers] = merge_request.reviewers + + MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + end + end end context 'updating milestone' do