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 @@
-
-
-
- {{ $options.i18n.newLabelList }}
-
+
+
+
-
-
-
-
{{ $options.i18n.formDescription }}
-
-
-
-
-
-
{{ $options.i18n.noLabelSelected }}
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $options.i18n.cancel }}
- {{ $options.i18n.add }}
-
-
-
+
+
+ {{ label.title }}
+
+
+
+
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 @@
+
+
+
+
+
+
+ {{ $options.i18n.newList }}
+
+
+
+
+
+
+
+
{{ formDescription }}
+
+
+
+
+ {{ $options.i18n.noneSelected }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $options.i18n.cancel }}
+ {{ $options.i18n.add }}
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
{{ formattedFileSize }}
+
{{ file.name }}
+
{{ $options.i18n.REMOVE_FILE_TEXT }}
+
+
+
+
+
+
+
+
+
+
+ {{ $options.i18n.NEW_BRANCH_IN_FORK }}
+
+
+
+
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