diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index b2ab53f42c1..c5214b9e10b 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -312,6 +312,8 @@
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/background_migration{,_spec}.rb"
- "{,ee/,jh/}spec/support/helpers/database/**/*"
- "lib/gitlab/markdown_cache/active_record/**/*"
+ - "lib/api/admin/batched_background_migrations.rb"
+ - "spec/requests/api/admin/batched_background_migrations_spec.rb"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/,jh/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
# Gitaly has interactions with background migrations: https://gitlab.com/gitlab-org/gitlab/-/issues/336538
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index b79402ce5bf..79212670488 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -252,7 +252,6 @@ Gitlab/NamespacedClass:
- 'app/models/notification_setting.rb'
- 'app/models/oauth_access_grant.rb'
- 'app/models/oauth_access_token.rb'
- - 'app/models/onboarding_progress.rb'
- 'app/models/out_of_context_discussion.rb'
- 'app/models/pages_deployment.rb'
- 'app/models/pages_domain.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 5fcc55bb0b0..6d2ea48e48c 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -5394,7 +5394,6 @@ Layout/LineLength:
- 'spec/models/namespace_statistics_spec.rb'
- 'spec/models/note_spec.rb'
- 'spec/models/notification_setting_spec.rb'
- - 'spec/models/onboarding_progress_spec.rb'
- 'spec/models/packages/composer/cache_file_spec.rb'
- 'spec/models/packages/composer/metadatum_spec.rb'
- 'spec/models/packages/conan/metadatum_spec.rb'
diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml
index c88fdf5a3b0..9359939514b 100644
--- a/.rubocop_todo/layout/space_in_lambda_literal.yml
+++ b/.rubocop_todo/layout/space_in_lambda_literal.yml
@@ -96,7 +96,6 @@ Layout/SpaceInLambdaLiteral:
- 'app/models/namespace_statistics.rb'
- 'app/models/note.rb'
- 'app/models/note_diff_file.rb'
- - 'app/models/onboarding_progress.rb'
- 'app/models/operations/feature_flags/user_list.rb'
- 'app/models/packages/build_info.rb'
- 'app/models/packages/maven/metadatum.rb'
diff --git a/.rubocop_todo/rails/where_exists.yml b/.rubocop_todo/rails/where_exists.yml
index 00ff82d137e..77722549722 100644
--- a/.rubocop_todo/rails/where_exists.yml
+++ b/.rubocop_todo/rails/where_exists.yml
@@ -19,7 +19,6 @@ Rails/WhereExists:
- 'app/models/lfs_object.rb'
- 'app/models/merge_request_diff.rb'
- 'app/models/namespace.rb'
- - 'app/models/onboarding_progress.rb'
- 'app/models/project.rb'
- 'app/models/protected_branch/push_access_level.rb'
- 'app/services/projects/transfer_service.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index 98fd3c52fd9..97480a0e0ba 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -2466,7 +2466,6 @@ RSpec/ContextWording:
- 'spec/models/note_spec.rb'
- 'spec/models/notification_recipient_spec.rb'
- 'spec/models/notification_setting_spec.rb'
- - 'spec/models/onboarding_progress_spec.rb'
- 'spec/models/operations/feature_flag_spec.rb'
- 'spec/models/packages/conan/file_metadatum_spec.rb'
- 'spec/models/packages/debian/file_metadatum_spec.rb'
diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml
index 525e2c31797..f733af601ec 100644
--- a/.rubocop_todo/style/lambda.yml
+++ b/.rubocop_todo/style/lambda.yml
@@ -77,7 +77,7 @@ Style/Lambda:
- 'app/models/note.rb'
- 'app/models/note_diff_file.rb'
- 'app/models/notification_setting.rb'
- - 'app/models/onboarding_progress.rb'
+ - 'app/models/onboarding/progress.rb'
- 'app/models/operations/feature_flags/user_list.rb'
- 'app/models/packages/package.rb'
- 'app/models/packages/package_file.rb'
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 05d6f468d23..659c447e861 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,6 +1,9 @@
-
+
{{ message }}
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 252f69f7a5d..41c3771bf41 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -1,11 +1,6 @@
-
-
-
+
+
+
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 75d8581890f..514ab9699bc 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -1,5 +1,3 @@
-import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
-
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub }) {
@@ -20,14 +18,19 @@ export class ContentEditor {
}
get changed() {
- return this._pristineDoc?.eq(this.tiptapEditor.state.doc);
+ if (!this._pristineDoc) {
+ return !this.empty;
+ }
+
+ return !this._pristineDoc.eq(this.tiptapEditor.state.doc);
}
get empty() {
- const doc = this.tiptapEditor?.state.doc;
+ return this.tiptapEditor.isEmpty;
+ }
- // Makes sure the document has more than one empty paragraph
- return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0);
+ get editable() {
+ return this.tiptapEditor.isEditable;
}
dispose() {
@@ -55,24 +58,22 @@ export class ContentEditor {
return this._assetResolver.renderDiagram(code, language);
}
+ setEditable(editable = true) {
+ this._tiptapEditor.setOptions({
+ editable,
+ });
+ }
+
async setSerializedContent(serializedContent) {
- const { _tiptapEditor: editor, _eventHub: eventHub } = this;
+ const { _tiptapEditor: editor } = this;
const { doc, tr } = editor.state;
- try {
- eventHub.$emit(LOADING_CONTENT_EVENT);
- const { document } = await this.deserialize(serializedContent);
+ const { document } = await this.deserialize(serializedContent);
- if (document) {
- this._pristineDoc = document;
- tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', true);
- editor.view.dispatch(tr);
- }
-
- eventHub.$emit(LOADING_SUCCESS_EVENT);
- } catch (e) {
- eventHub.$emit(LOADING_ERROR_EVENT, e);
- throw e;
+ if (document) {
+ this._pristineDoc = document;
+ tr.replaceWith(0, doc.content.size, document).setMeta('preventUpdate', true);
+ editor.view.dispatch(tr);
}
}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
index ec21d8c84e0..6c481beefe1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
@@ -1,5 +1,72 @@
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+
+import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
+
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
import initCheckFormState from './check_form_state';
+function initTargetBranchSelector() {
+ const targetBranch = document.querySelector('.js-target-branch');
+ const { selected, fieldName, refsUrl } = targetBranch?.dataset ?? {};
+ const formField = document.querySelector(`input[name="${fieldName}"]`);
+
+ if (targetBranch && refsUrl && formField) {
+ /* eslint-disable-next-line no-new */
+ new GitLabDropdown(targetBranch, {
+ selectable: true,
+ filterable: true,
+ filterRemote: Boolean(refsUrl),
+ filterInput: 'input[type="search"]',
+ data(term, callback) {
+ const params = {
+ search: term,
+ };
+
+ axios
+ .get(refsUrl, {
+ params,
+ })
+ .then(({ data }) => {
+ callback(data);
+ })
+ .catch(() =>
+ createFlash({
+ message: __('Error fetching branches'),
+ }),
+ );
+ },
+ renderRow(branch) {
+ const item = document.createElement('li');
+ const link = document.createElement('a');
+
+ link.setAttribute('href', '#');
+ link.dataset.branch = branch;
+ link.classList.toggle('is-active', branch === selected);
+ link.textContent = branch;
+
+ item.appendChild(link);
+
+ return item;
+ },
+ id(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked({ $el, e }) {
+ e.preventDefault();
+
+ const branchName = $el[0].dataset.branch;
+
+ formField.setAttribute('value', branchName);
+ },
+ });
+ }
+}
+
initMergeRequest();
initCheckFormState();
+initTargetBranchSelector();
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 9d7d9e376cf..3f4ab7319a0 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -5,7 +5,6 @@ import {
GlLink,
GlButton,
GlSprintf,
- GlAlert,
GlFormGroup,
GlFormInput,
GlFormSelect,
@@ -59,14 +58,6 @@ export default {
label: s__('WikiPage|Content'),
placeholder: s__('WikiPage|Write your content or drag files here…'),
},
- contentEditor: {
- renderFailed: {
- message: s__(
- 'WikiPage|An error occurred while trying to render the content editor. Please try again later.',
- ),
- primaryAction: s__('WikiPage|Retry'),
- },
- },
linksHelpText: s__(
'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
),
@@ -88,7 +79,6 @@ export default {
{ text: s__('Wiki Page|Rich text'), value: 'richText' },
],
components: {
- GlAlert,
GlIcon,
GlForm,
GlFormGroup,
@@ -115,14 +105,12 @@ export default {
content: this.pageInfo.content || '',
commitMessage: '',
isDirty: false,
- contentEditorRenderFailed: false,
contentEditorEmpty: false,
switchEditingControlDisabled: false,
};
},
computed: {
noContent() {
- if (this.isContentEditorActive) return this.contentEditorEmpty;
return !this.content.trim();
},
csrfToken() {
@@ -145,11 +133,6 @@ export default {
linkExample() {
return MARKDOWN_LINK_TEXT[this.format];
},
- toggleEditingModeButtonText() {
- return this.isContentEditorActive
- ? this.$options.i18n.editSourceButtonText
- : this.$options.i18n.editRichTextButtonText;
- },
submitButtonText() {
return this.pageInfo.persisted
? this.$options.i18n.submitButton.existingPage
@@ -177,7 +160,7 @@ export default {
return !this.isContentEditorActive;
},
disableSubmitButton() {
- return this.noContent || !this.title || this.contentEditorRenderFailed;
+ return this.noContent || !this.title;
},
isContentEditorActive() {
return this.isMarkdownFormat && this.useContentEditor;
@@ -201,23 +184,14 @@ export default {
.then(({ data }) => data.body);
},
- toggleEditingMode(editingMode) {
+ setEditingMode(editingMode) {
this.editingMode = editingMode;
- if (!this.useContentEditor && this.contentEditor) {
- this.content = this.contentEditor.getSerializedContent();
- }
- },
-
- setEditingMode(value) {
- this.editingMode = value;
},
async handleFormSubmit(e) {
e.preventDefault();
if (this.useContentEditor) {
- this.content = this.contentEditor.getSerializedContent();
-
this.trackFormSubmit();
}
@@ -235,30 +209,10 @@ export default {
this.isDirty = true;
},
- async loadInitialContent(contentEditor) {
- this.contentEditor = contentEditor;
-
- try {
- await this.contentEditor.setSerializedContent(this.content);
- this.trackContentEditorLoaded();
- } catch (e) {
- this.contentEditorRenderFailed = true;
- }
- },
-
- async retryInitContentEditor() {
- try {
- this.contentEditorRenderFailed = false;
- await this.contentEditor.setSerializedContent(this.content);
- } catch (e) {
- this.contentEditorRenderFailed = true;
- }
- },
-
- handleContentEditorChange({ empty }) {
+ handleContentEditorChange({ empty, markdown, changed }) {
this.contentEditorEmpty = empty;
- // TODO: Implement a precise mechanism to detect changes in the Content
- this.isDirty = true;
+ this.isDirty = changed;
+ this.content = markdown;
},
onPageUnload(event) {
@@ -320,17 +274,6 @@ export default {
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
>
-
- {{ $options.i18n.contentEditor.renderFailed.message }}
-
-
-
nil)
.distinct('ci_builds_metadata.expanded_environment_name')
.limit(100)
@@ -977,17 +977,17 @@ module Ci
end
# With multi-project and parent-child pipelines
- def all_pipelines_in_hierarchy
+ def upstream_and_all_downstreams
object_hierarchy.all_objects
end
# With only parent-child pipelines
- def self_and_ancestors
+ def self_and_project_ancestors
object_hierarchy(project_condition: :same).base_and_ancestors
end
# With only parent-child pipelines
- def self_and_descendants
+ def self_and_project_descendants
object_hierarchy(project_condition: :same).base_and_descendants
end
@@ -996,8 +996,8 @@ module Ci
object_hierarchy(project_condition: :same).descendants
end
- def self_and_descendants_complete?
- self_and_descendants.all?(&:complete?)
+ def self_and_project_descendants_complete?
+ self_and_project_descendants.all?(&:complete?)
end
# Follow the parent-child relationships and return the top-level parent
@@ -1061,8 +1061,8 @@ module Ci
latest_report_builds(Ci::JobArtifact.of_report_type(:test)).preload(:project, :metadata)
end
- def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
- builds_in_self_and_descendants.with_artifacts(reports_scope)
+ def latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
+ builds_in_self_and_project_descendants.with_artifacts(reports_scope)
end
def builds_with_coverage
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 6c3754d84d0..3cbc4c50444 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -359,14 +359,6 @@ module Ci
runner_projects.limit(2).count(:all) > 1
end
- def assigned_to_group?
- runner_namespaces.any?
- end
-
- def assigned_to_project?
- runner_projects.any?
- end
-
def match_build_if_online?(build)
active? && online? && matches_build?(build)
end
diff --git a/app/models/concerns/integrations/has_web_hook.rb b/app/models/concerns/integrations/has_web_hook.rb
index e9fcb0d151d..e6ca6cc7938 100644
--- a/app/models/concerns/integrations/has_web_hook.rb
+++ b/app/models/concerns/integrations/has_web_hook.rb
@@ -5,6 +5,7 @@ module Integrations
extend ActiveSupport::Concern
included do
+ after_save :update_web_hook!, if: :activated?
has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 65fb62a814f..eccb004b503 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -43,6 +43,33 @@ module Sortable
}
end
+ def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
+ reversed_direction = direction == :asc ? :desc : :asc
+
+ # rubocop: disable GitlabSecurity/PublicSend
+ order = ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: attribute_name,
+ column_expression: column,
+ order_expression: column.send(direction).send(nullable),
+ reversed_order_expression: column.send(reversed_direction).send(nullable),
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true,
+ nullable: nullable
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table['id'].desc
+ )
+ ]
+ )
+ # rubocop: enable GitlabSecurity/PublicSend
+
+ order.apply_cursor_conditions(scope).reorder(order)
+ end
+
private
def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
diff --git a/app/models/environment.rb b/app/models/environment.rb
index bc1b2daf3db..4aab4ab8170 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -84,7 +84,7 @@ class Environment < ApplicationRecord
# Search environments which have names like the given query.
# Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
scope :for_name_like, -> (query, limit: 5) do
- where(arel_table[:name].matches("#{sanitize_sql_like query}%")).limit(limit)
+ where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit)
end
scope :for_project, -> (project) { where(project_id: project) }
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 43b2c7899a1..d06d0a99948 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,7 +100,7 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
+ pipeline.environments_in_self_and_project_descendants.includes(:project).available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index c19e06f055e..7a48e71b934 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -8,8 +8,6 @@ module Integrations
include ReactivelyCached
extend Gitlab::Utils::Override
- after_save :ensure_ssl_verification
-
ENDPOINT = "https://buildkite.com"
field :project_url,
@@ -50,12 +48,6 @@ module Integrations
self.properties = properties.except('enable_ssl_verification') # Remove unused key
end
- def ensure_ssl_verification
- return unless service_hook
-
- update_web_hook!
- end
-
override :hook_url
def hook_url
"#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1de2075b456..153747c75df 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -254,32 +254,6 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
- def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
- reversed_direction = direction == :asc ? :desc : :asc
-
- # rubocop: disable GitlabSecurity/PublicSend
- order = ::Gitlab::Pagination::Keyset::Order.build(
- [
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: attribute_name,
- column_expression: column,
- order_expression: column.send(direction).send(nullable),
- reversed_order_expression: column.send(reversed_direction).send(nullable),
- order_direction: direction,
- distinct: false,
- add_to_projections: true,
- nullable: nullable
- ),
- ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: arel_table['id'].desc
- )
- ])
- # rubocop: enable GitlabSecurity/PublicSend
-
- order.apply_cursor_conditions(scope).order(order)
- end
-
override :order_upvotes_desc
def order_upvotes_desc
reorder(upvotes_count: :desc)
diff --git a/app/models/member.rb b/app/models/member.rb
index 186fcd8759f..c5351d5447b 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -184,14 +184,85 @@ class Member < ApplicationRecord
unscoped.from(distinct_members, :members)
end
- scope :order_name_asc, -> { left_join_users.reorder(User.arel_table[:name].asc.nulls_last) }
- scope :order_name_desc, -> { left_join_users.reorder(User.arel_table[:name].desc.nulls_last) }
- scope :order_recent_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].desc.nulls_last) }
- scope :order_oldest_sign_in, -> { left_join_users.reorder(User.arel_table[:last_sign_in_at].asc.nulls_last) }
- scope :order_recent_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].desc.nulls_last) }
- scope :order_oldest_last_activity, -> { left_join_users.reorder(User.arel_table[:last_activity_on].asc.nulls_first) }
- scope :order_recent_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].desc.nulls_last) }
- scope :order_oldest_created_user, -> { left_join_users.reorder(User.arel_table[:created_at].asc.nulls_first) }
+ scope :order_name_asc, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_full_name',
+ column: User.arel_table[:name],
+ direction: :asc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_name_desc, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_full_name',
+ column: User.arel_table[:name],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_sign_in, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_sign_in_at',
+ column: User.arel_table[:last_sign_in_at],
+ direction: :asc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_recent_sign_in, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_sign_in_at',
+ column: User.arel_table[:last_sign_in_at],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_last_activity, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_activity_on',
+ column: User.arel_table[:last_activity_on],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+
+ scope :order_recent_last_activity, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_last_activity_on',
+ column: User.arel_table[:last_activity_on],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
+
+ scope :order_oldest_created_user, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_created_at',
+ column: User.arel_table[:created_at],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+
+ scope :order_recent_created_user, -> do
+ build_keyset_order_on_joined_column(
+ scope: left_join_users,
+ attribute_name: 'member_user_created_at',
+ column: User.arel_table[:created_at],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 18459805883..d155a295481 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1466,7 +1466,7 @@ class MergeRequest < ApplicationRecord
end
def environments_in_head_pipeline(deployment_status: nil)
- actual_head_pipeline&.environments_in_self_and_descendants(deployment_status: deployment_status) || Environment.none
+ actual_head_pipeline&.environments_in_self_and_project_descendants(deployment_status: deployment_status) || Environment.none
end
def fetch_ref!
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4f0639a08a0..17d4c6d27e6 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -61,7 +61,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
has_many :pending_builds, class_name: 'Ci::PendingBuild'
- has_one :onboarding_progress
+ has_one :onboarding_progress, class_name: 'Onboarding::Progress'
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
diff --git a/app/models/onboarding/progress.rb b/app/models/onboarding/progress.rb
new file mode 100644
index 00000000000..ecc78418256
--- /dev/null
+++ b/app/models/onboarding/progress.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module Onboarding
+ class Progress < ApplicationRecord
+ self.table_name = 'onboarding_progresses'
+
+ belongs_to :namespace, optional: false
+
+ validate :namespace_is_root_namespace
+
+ ACTIONS = [
+ :git_pull,
+ :git_write,
+ :merge_request_created,
+ :pipeline_created,
+ :user_added,
+ :trial_started,
+ :subscription_created,
+ :required_mr_approvals_enabled,
+ :code_owners_enabled,
+ :scoped_label_created,
+ :security_scan_enabled,
+ :issue_created,
+ :issue_auto_closed,
+ :repository_imported,
+ :repository_mirrored,
+ :secure_dependency_scanning_run,
+ :secure_container_scanning_run,
+ :secure_dast_run,
+ :secure_secret_detection_run,
+ :secure_coverage_fuzzing_run,
+ :secure_api_fuzzing_run,
+ :secure_cluster_image_scanning_run,
+ :license_scanning_run
+ ].freeze
+
+ scope :incomplete_actions, ->(actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
+ end
+
+ scope :completed_actions, ->(actions) do
+ Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
+ end
+
+ scope :completed_actions_with_latest_in_range, ->(actions, range) do
+ actions = Array(actions)
+ if actions.size == 1
+ where(column_name(actions[0]) => range)
+ else
+ action_columns = actions.map { |action| arel_table[column_name(action)] }
+ completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
+ end
+ end
+
+ class << self
+ def onboard(namespace)
+ return unless root_namespace?(namespace)
+
+ create(namespace: namespace)
+ end
+
+ def onboarding?(namespace)
+ where(namespace: namespace).any?
+ end
+
+ def register(namespace, actions)
+ actions = Array(actions)
+ return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty?
+
+ onboarding_progress = find_by(namespace: namespace)
+ return unless onboarding_progress
+
+ now = Time.current
+ nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? }
+ return if nil_actions.empty?
+
+ updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) }
+ onboarding_progress.update!(updates)
+ end
+
+ def completed?(namespace, action)
+ return unless root_namespace?(namespace) && ACTIONS.include?(action)
+
+ action_column = column_name(action)
+ where(namespace: namespace).where.not(action_column => nil).exists?
+ end
+
+ def not_completed?(namespace_id, action)
+ return unless ACTIONS.include?(action)
+
+ action_column = column_name(action)
+ exists?(namespace_id: namespace_id, action_column => nil)
+ end
+
+ def column_name(action)
+ :"#{action}_at"
+ end
+
+ private
+
+ def root_namespace?(namespace)
+ namespace&.root?
+ end
+ end
+
+ def number_of_completed_actions
+ attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
+ end
+
+ private
+
+ def namespace_is_root_namespace
+ return unless namespace
+
+ errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
+ end
+ end
+end
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
deleted file mode 100644
index e5851c5cfc5..00000000000
--- a/app/models/onboarding_progress.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-# frozen_string_literal: true
-
-class OnboardingProgress < ApplicationRecord
- belongs_to :namespace, optional: false
-
- validate :namespace_is_root_namespace
-
- ACTIONS = [
- :git_pull,
- :git_write,
- :merge_request_created,
- :pipeline_created,
- :user_added,
- :trial_started,
- :subscription_created,
- :required_mr_approvals_enabled,
- :code_owners_enabled,
- :scoped_label_created,
- :security_scan_enabled,
- :issue_created,
- :issue_auto_closed,
- :repository_imported,
- :repository_mirrored,
- :secure_dependency_scanning_run,
- :secure_container_scanning_run,
- :secure_dast_run,
- :secure_secret_detection_run,
- :secure_coverage_fuzzing_run,
- :secure_api_fuzzing_run,
- :secure_cluster_image_scanning_run,
- :license_scanning_run
- ].freeze
-
- scope :incomplete_actions, -> (actions) do
- Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
- end
-
- scope :completed_actions, -> (actions) do
- Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
- end
-
- scope :completed_actions_with_latest_in_range, -> (actions, range) do
- actions = Array(actions)
- if actions.size == 1
- where(column_name(actions[0]) => range)
- else
- action_columns = actions.map { |action| arel_table[column_name(action)] }
- completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
- end
- end
-
- class << self
- def onboard(namespace)
- return unless root_namespace?(namespace)
-
- create(namespace: namespace)
- end
-
- def onboarding?(namespace)
- where(namespace: namespace).any?
- end
-
- def register(namespace, actions)
- actions = Array(actions)
- return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty?
-
- onboarding_progress = find_by(namespace: namespace)
- return unless onboarding_progress
-
- now = Time.current
- nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? }
- return if nil_actions.empty?
-
- updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) }
- onboarding_progress.update!(updates)
- end
-
- def completed?(namespace, action)
- return unless root_namespace?(namespace) && ACTIONS.include?(action)
-
- action_column = column_name(action)
- where(namespace: namespace).where.not(action_column => nil).exists?
- end
-
- def not_completed?(namespace_id, action)
- return unless ACTIONS.include?(action)
-
- action_column = column_name(action)
- where(namespace_id: namespace_id).where(action_column => nil).exists?
- end
-
- def column_name(action)
- :"#{action}_at"
- end
-
- private
-
- def root_namespace?(namespace)
- namespace && namespace.root?
- end
- end
-
- def number_of_completed_actions
- attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
- end
-
- private
-
- def namespace_is_root_namespace
- return unless namespace
-
- errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent?
- end
-end
diff --git a/app/models/project.rb b/app/models/project.rb
index 78ec17acc01..0be24252fa8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1170,7 +1170,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
return unless latest_pipeline
- latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name)
end
def latest_successful_build_for_sha(job_name, sha)
@@ -1179,7 +1179,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
return unless latest_pipeline
- latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
+ latest_pipeline.build_with_artifacts_in_self_and_project_descendants(job_name)
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index b38b3b93353..c68d990ae2e 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -137,7 +137,7 @@ module Ci
return false unless @bridge.triggers_child_pipeline?
# only applies to parent-child pipelines not multi-project
- ancestors_of_new_child = @bridge.pipeline.self_and_ancestors
+ ancestors_of_new_child = @bridge.pipeline.self_and_project_ancestors
ancestors_of_new_child.count > MAX_NESTED_CHILDREN
end
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
index bf2355c447a..15597eb7209 100644
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ b/app/services/ci/expire_pipeline_cache_service.rb
@@ -86,7 +86,7 @@ module Ci
etag_paths << path
end
- pipeline.all_pipelines_in_hierarchy.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord
+ pipeline.upstream_and_all_downstreams.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord
etag_paths << project_pipeline_path(relative_pipeline.project, relative_pipeline)
etag_paths << graphql_pipeline_path(relative_pipeline)
etag_paths << graphql_pipeline_sha_path(relative_pipeline.sha)
diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb
index 81f26e84ef8..8beecb79fd9 100644
--- a/app/services/ci/generate_coverage_reports_service.rb
+++ b/app/services/ci/generate_coverage_reports_service.rb
@@ -43,7 +43,7 @@ module Ci
end
def last_update_timestamp(pipeline_hierarchy)
- pipeline_hierarchy&.self_and_descendants&.maximum(:updated_at)
+ pipeline_hierarchy&.self_and_project_descendants&.maximum(:updated_at)
end
end
end
diff --git a/app/services/concerns/ci/downstream_pipeline_helpers.rb b/app/services/concerns/ci/downstream_pipeline_helpers.rb
index 39c0adb6e4e..557bfe2534b 100644
--- a/app/services/concerns/ci/downstream_pipeline_helpers.rb
+++ b/app/services/concerns/ci/downstream_pipeline_helpers.rb
@@ -5,7 +5,7 @@ module Ci
def log_downstream_pipeline_creation(downstream_pipeline)
return unless downstream_pipeline&.persisted?
- hierarchy_size = downstream_pipeline.all_pipelines_in_hierarchy.count
+ hierarchy_size = downstream_pipeline.upstream_and_all_downstreams.count
root_pipeline = downstream_pipeline.upstream_root
::Gitlab::AppLogger.info(
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 35716f7742a..d508865ef32 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -39,7 +39,7 @@ module Groups
if @group.save
@group.add_owner(current_user)
Integration.create_from_active_default_integrations(@group, :group_id)
- OnboardingProgress.onboard(@group)
+ Onboarding::Progress.onboard(@group)
end
end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index c139b2e11dd..1ce7e4cae16 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -89,7 +89,7 @@ module Namespaces
end
def groups_for_track
- onboarding_progress_scope = OnboardingProgress
+ onboarding_progress_scope = Onboarding::Progress
.completed_actions_with_latest_in_range(completed_actions, range)
.incomplete_actions(incomplete_actions)
diff --git a/app/services/onboarding_progress_service.rb b/app/services/onboarding_progress_service.rb
index 6d44c0a61ea..7ba7fbc5b7f 100644
--- a/app/services/onboarding_progress_service.rb
+++ b/app/services/onboarding_progress_service.rb
@@ -9,7 +9,7 @@ class OnboardingProgressService
end
def execute(action:)
- return unless OnboardingProgress.not_completed?(namespace_id, action)
+ return unless Onboarding::Progress.not_completed?(namespace_id, action)
Namespaces::OnboardingProgressWorker.perform_async(namespace_id, action)
end
@@ -26,6 +26,6 @@ class OnboardingProgressService
def execute(action:)
return unless @namespace
- OnboardingProgress.register(@namespace, action)
+ Onboarding::Progress.register(@namespace, action)
end
end
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 8ab002f755f..634e927f891 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -31,10 +31,14 @@
- if issuable.merged?
%code= target_title
- unless issuable.new_record? || issuable.merged?
- %span.dropdown.gl-ml-2.d-inline-block
- = form.hidden_field(:target_branch,
- { class: 'target_branch js-target-branch-select ref-name mw-xl',
- data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
+ .merge-request-select.dropdown.gl-w-auto
+ = form.hidden_field :target_branch
+ = dropdown_toggle form.object.target_branch.presence || _("Select branch"), { toggle: "dropdown", 'field-name': "#{form.object_name}[target_branch]", 'refs-url': refs_project_path(@project, sort: 'updated_desc', find: 'branches'), selected: form.object.target_branch, default_text: _("Select branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
+ .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.target_branch.ref-name.git-revision-dropdown
+ = dropdown_title(_("Select branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
- if source_level < target_level
= render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mb-4' }) do |c|
diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
index 127eb3b6f44..53bed0fa9da 100644
--- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
@@ -20,7 +20,7 @@ module Ci
return unless pipeline
pipeline.root_ancestor.try do |root_ancestor_pipeline|
- next unless root_ancestor_pipeline.self_and_descendants_complete?
+ next unless root_ancestor_pipeline.self_and_project_descendants_complete?
Ci::PipelineArtifacts::CoverageReportService.new(root_ancestor_pipeline).execute
end
diff --git a/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml b/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml
index c0963ea33c3..559189d759b 100644
--- a/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml
+++ b/data/deprecations/14-5-certificate-based-integration-with-kubernetes-saas.yml
@@ -5,7 +5,7 @@
removal_date: "2023-02-22" # the date of the milestone release when this feature is planned to be removed
breaking_change: true
body: |
- The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. The integrations are expected to be switched off completely on GitLab SaaS around 2022 November 22.
+ The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace.
For a more robust, secure, forthcoming, and reliable integration with Kubernetes, we recommend you use the
[agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. [How do I migrate?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html)
diff --git a/db/docs/onboarding_progresses.yml b/db/docs/onboarding_progresses.yml
index 4166b934570..80b70fe0b1f 100644
--- a/db/docs/onboarding_progresses.yml
+++ b/db/docs/onboarding_progresses.yml
@@ -1,7 +1,7 @@
---
table_name: onboarding_progresses
classes:
-- OnboardingProgress
+- Onboarding::Progress
feature_categories:
- onboarding
description: TODO
diff --git a/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb b/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb
new file mode 100644
index 00000000000..d8b7cd9be28
--- /dev/null
+++ b/db/migrate/20220901131828_add_environments_project_name_lower_pattern_ops_index.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddEnvironmentsProjectNameLowerPatternOpsIndex < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_environments_on_project_name_varchar_pattern_ops'
+
+ def up
+ add_concurrent_index :environments, 'project_id, lower(name) varchar_pattern_ops', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :environments, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20220901131828 b/db/schema_migrations/20220901131828
new file mode 100644
index 00000000000..b41fef9415b
--- /dev/null
+++ b/db/schema_migrations/20220901131828
@@ -0,0 +1 @@
+c32756c482bdda948f911d0405d2373673041c57ebc514cfc5f172ba6fda9185
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 37b410e7018..ea00b4049cd 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -28560,6 +28560,8 @@ CREATE INDEX index_environments_on_project_id_and_tier ON environments USING btr
CREATE INDEX index_environments_on_project_id_state_environment_type ON environments USING btree (project_id, state, environment_type);
+CREATE INDEX index_environments_on_project_name_varchar_pattern_ops ON environments USING btree (project_id, lower((name)::text) varchar_pattern_ops);
+
CREATE INDEX index_environments_on_state_and_auto_delete_at ON environments USING btree (auto_delete_at) WHERE ((auto_delete_at IS NOT NULL) AND ((state)::text = 'stopped'::text));
CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING btree (state, auto_stop_at) WHERE ((auto_stop_at IS NOT NULL) AND ((state)::text = 'available'::text));
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index bcffccf7140..a13a0ece465 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12682,6 +12682,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| `accessLevels` | [`[AccessLevelEnum!]`](#accesslevelenum) | Filter members by the given access levels. |
| `relations` | [`[GroupMemberRelation!]`](#groupmemberrelation) | Filter members by the given member relations. |
| `search` | [`String`](#string) | Search query. |
+| `sort` | [`MemberSort`](#membersort) | sort query. |
##### `Group.issues`
@@ -16597,6 +16598,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| `relations` | [`[ProjectMemberRelation!]`](#projectmemberrelation) | Filter members by the given member relations. |
| `search` | [`String`](#string) | Search query. |
+| `sort` | [`MemberSort`](#membersort) | sort query. |
##### `Project.release`
@@ -20318,6 +20320,25 @@ Possible identifier types for a measurement.
| `PROJECTS` | Project count. |
| `USERS` | User count. |
+### `MemberSort`
+
+Values for sorting members.
+
+| Value | Description |
+| ----- | ----------- |
+| `ACCESS_LEVEL_ASC` | Access level ascending order. |
+| `ACCESS_LEVEL_DESC` | Access level descending order. |
+| `CREATED_ASC` | Created at ascending order. |
+| `CREATED_DESC` | Created at descending order. |
+| `UPDATED_ASC` | Updated at ascending order. |
+| `UPDATED_DESC` | Updated at descending order. |
+| `USER_FULL_NAME_ASC` | User's full name ascending order. |
+| `USER_FULL_NAME_DESC` | User's full name descending order. |
+| `created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. |
+| `created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. |
+| `updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
+| `updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
+
### `MergeRequestNewState`
New state to apply to a merge request.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index ad565fbc3e7..8c39f39132e 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -1783,7 +1783,7 @@ WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
-The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace. The integrations are expected to be switched off completely on GitLab SaaS around 2022 November 22.
+The certificate-based integration with Kubernetes will be [deprecated and removed](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). As a GitLab SaaS customer, on new namespaces, you will no longer be able to integrate GitLab and your cluster using the certificate-based approach as of GitLab 15.0. The integration for current users will be enabled per namespace.
For a more robust, secure, forthcoming, and reliable integration with Kubernetes, we recommend you use the
[agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. [How do I migrate?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html)
diff --git a/lib/gitlab/ci/reports/coverage_report_generator.rb b/lib/gitlab/ci/reports/coverage_report_generator.rb
index 5625707431a..88b3b14d5c9 100644
--- a/lib/gitlab/ci/reports/coverage_report_generator.rb
+++ b/lib/gitlab/ci/reports/coverage_report_generator.rb
@@ -35,7 +35,7 @@ module Gitlab
private
def report_builds
- @pipeline.latest_report_builds_in_self_and_descendants(::Ci::JobArtifact.of_report_type(:coverage))
+ @pipeline.latest_report_builds_in_self_and_project_descendants(::Ci::JobArtifact.of_report_type(:coverage))
end
end
end
diff --git a/lib/learn_gitlab/onboarding.rb b/lib/learn_gitlab/onboarding.rb
index 54af01a21fe..4215221cdf1 100644
--- a/lib/learn_gitlab/onboarding.rb
+++ b/lib/learn_gitlab/onboarding.rb
@@ -39,13 +39,13 @@ module LearnGitlab
def onboarding_progress
strong_memoize(:onboarding_progress) do
- OnboardingProgress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord
+ ::Onboarding::Progress.find_by(namespace: namespace) # rubocop: disable CodeReuse/ActiveRecord
end
end
def action_columns
strong_memoize(:action_columns) do
- tracked_actions.map { |action_key| OnboardingProgress.column_name(action_key) }
+ tracked_actions.map { |action_key| ::Onboarding::Progress.column_name(action_key) }
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 621500f4d87..8f0bb16fb74 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4354,6 +4354,9 @@ msgstr ""
msgid "An error occurred while trying to generate the report. Please try again later."
msgstr ""
+msgid "An error occurred while trying to render the content editor. Please try again."
+msgstr ""
+
msgid "An error occurred while trying to run a new pipeline for this merge request."
msgstr ""
@@ -15152,6 +15155,9 @@ msgstr ""
msgid "Error deleting project. Check logs for error details."
msgstr ""
+msgid "Error fetching branches"
+msgstr ""
+
msgid "Error fetching burnup chart data"
msgstr ""
@@ -44476,9 +44482,6 @@ msgstr ""
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs."
msgstr ""
-msgid "WikiPage|An error occurred while trying to render the content editor. Please try again later."
-msgstr ""
-
msgid "WikiPage|Cancel"
msgstr ""
@@ -44503,9 +44506,6 @@ msgstr ""
msgid "WikiPage|Page title"
msgstr ""
-msgid "WikiPage|Retry"
-msgstr ""
-
msgid "WikiPage|Save changes"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb
index 29e590976d2..ca934337fbb 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb
@@ -2,7 +2,11 @@
module QA
RSpec.describe 'Manage' do
- describe 'Project owner permissions', :reliable do
+ describe 'Project owner permissions', :reliable, quarantine: {
+ only: { subdomain: %i[staging staging-canary] },
+ type: :investigating,
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/373038'
+ } do
let!(:owner) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
end
diff --git a/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb
index 9569fe22b1f..8261461e8aa 100644
--- a/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb
+++ b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb
@@ -5,8 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationHookLogsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:service_hook) { create(:service_hook) }
- let(:integration) { create(:drone_ci_integration, project: project, service_hook: service_hook) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
let(:log) { create(:web_hook_log, web_hook: integration.service_hook) }
let(:log_params) do
{
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 448587c937a..da62acb1fda 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -55,11 +55,11 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:namespace) { project.namespace }
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
send_request
end
- subject { OnboardingProgress.completed?(namespace, :git_pull) }
+ subject { Onboarding::Progress.completed?(namespace, :git_pull) }
it { is_expected.to be(true) }
end
diff --git a/spec/factories/onboarding_progresses.rb b/spec/factories/onboarding/progresses.rb
similarity index 53%
rename from spec/factories/onboarding_progresses.rb
rename to spec/factories/onboarding/progresses.rb
index e39bad91b19..15f58b482d3 100644
--- a/spec/factories/onboarding_progresses.rb
+++ b/spec/factories/onboarding/progresses.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :onboarding_progress do
+ factory :onboarding_progress, class: 'Onboarding::Progress' do
namespace
end
end
diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb
index 0b4b9d7452a..4ac25ea7ae0 100644
--- a/spec/features/merge_request/user_edits_merge_request_spec.rb
+++ b/spec/features/merge_request/user_edits_merge_request_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'User edits a merge request', :js do
- include Select2Helper
-
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -89,7 +87,12 @@ RSpec.describe 'User edits a merge request', :js do
it 'allows user to change target branch' do
expect(page).to have_content('From master into feature')
- select2('merge-test', from: '#merge_request_target_branch')
+ first('.js-target-branch').click
+
+ wait_for_requests
+
+ first('.js-target-branch-dropdown a', text: 'merge-test').click
+
click_button('Save changes')
expect(page).to have_content("requested to merge #{merge_request.source_branch} into merge-test")
@@ -101,7 +104,7 @@ RSpec.describe 'User edits a merge request', :js do
it 'does not allow user to change target branch' do
expect(page).to have_content('From master into feature')
- expect(page).not_to have_selector('.select2-container')
+ expect(page).not_to have_selector('.js-target-branch.js-compare-dropdown')
end
end
end
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index 12484cb13c6..ee9ead8f8a7 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -51,6 +51,16 @@ describe('content_editor/components/content_editor_alert', () => {
},
);
+ it('does not show primary action by default', async () => {
+ const message = 'error message';
+
+ createWrapper();
+ eventHub.$emit(ALERT_EVENT, { message });
+ await nextTick();
+
+ expect(findErrorAlert().attributes().primaryButtonText).toBeUndefined();
+ });
+
it('allows dismissing the error', async () => {
const message = 'error message';
@@ -62,4 +72,19 @@ describe('content_editor/components/content_editor_alert', () => {
expect(findErrorAlert().exists()).toBe(false);
});
+
+ it('allows dismissing the error with a primary action button', async () => {
+ const message = 'error message';
+ const actionLabel = 'Retry';
+ const action = jest.fn();
+
+ createWrapper();
+ eventHub.$emit(ALERT_EVENT, { message, action, actionLabel });
+ await nextTick();
+ findErrorAlert().vm.$emit('primaryAction');
+ await nextTick();
+
+ expect(action).toHaveBeenCalled();
+ expect(findErrorAlert().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 4c87ccca85b..ae52cb05eaf 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,4 +1,6 @@
-import { EditorContent } from '@tiptap/vue-2';
+import { GlAlert } from '@gitlab/ui';
+import { EditorContent, Editor } from '@tiptap/vue-2';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
@@ -10,112 +12,205 @@ import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
-import { emitEditorEvent } from '../test_utils';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
- let contentEditor;
let renderMarkdown;
const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
- const createWrapper = (propsData = {}) => {
- renderMarkdown = jest.fn();
-
+ const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
+ const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
+ const createWrapper = ({ markdown } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
- ...propsData,
+ markdown,
},
stubs: {
EditorStateObserver,
ContentEditorProvider,
- },
- listeners: {
- initialized(editor) {
- contentEditor = editor;
- },
+ ContentEditorAlert,
},
});
};
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- it('triggers initialized event and provides contentEditor instance as event data', () => {
+ it('triggers initialized event', () => {
createWrapper();
- expect(contentEditor).not.toBe(false);
+ expect(wrapper.emitted('initialized')).toHaveLength(1);
});
- it('renders EditorContent component and provides tiptapEditor instance', () => {
- createWrapper();
+ it('renders EditorContent component and provides tiptapEditor instance', async () => {
+ const markdown = 'hello world';
+
+ createWrapper({ markdown });
+
+ renderMarkdown.mockResolvedValueOnce(markdown);
+
+ await nextTick();
const editorContent = findEditorContent();
- expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor);
+ expect(editorContent.props().editor).toBeInstanceOf(Editor);
expect(editorContent.classes()).toContain('md');
});
- it('renders ContentEditorProvider component', () => {
- createWrapper();
+ it('renders ContentEditorProvider component', async () => {
+ await createWrapper();
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it('renders top toolbar component', () => {
- createWrapper();
+ it('renders top toolbar component', async () => {
+ await createWrapper();
expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
});
- it('adds is-focused class when focus event is emitted', async () => {
- createWrapper();
+ describe('when setting initial content', () => {
+ it('displays loading indicator', async () => {
+ createWrapper();
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
+ await nextTick();
- expect(findEditorElement().classes()).toContain('is-focused');
+ expect(findLoadingIndicator().exists()).toBe(true);
+ });
+
+ it('emits loading event', async () => {
+ createWrapper();
+
+ await nextTick();
+
+ expect(wrapper.emitted('loading')).toHaveLength(1);
+ });
+
+ describe('succeeds', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockResolvedValueOnce('hello world');
+
+ createWrapper({ markddown: 'hello world' });
+ await nextTick();
+ });
+
+ it('hides loading indicator', async () => {
+ await nextTick();
+ expect(findLoadingIndicator().exists()).toBe(false);
+ });
+
+ it('emits loadingSuccess event', () => {
+ expect(wrapper.emitted('loadingSuccess')).toHaveLength(1);
+ });
+ });
+
+ describe('fails', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockRejectedValueOnce(new Error());
+
+ createWrapper({ markddown: 'hello world' });
+ await nextTick();
+ });
+
+ it('sets the content editor as read only when loading content fails', async () => {
+ await nextTick();
+
+ expect(findEditorContent().props().editor.isEditable).toBe(false);
+ });
+
+ it('hides loading indicator', async () => {
+ await nextTick();
+
+ expect(findLoadingIndicator().exists()).toBe(false);
+ });
+
+ it('emits loadingError event', () => {
+ expect(wrapper.emitted('loadingError')).toHaveLength(1);
+ });
+
+ it('displays error alert indicating that the content editor failed to load', () => {
+ expect(findContentEditorAlert().text()).toContain(
+ 'An error occurred while trying to render the content editor. Please try again.',
+ );
+ });
+
+ describe('when clicking the retry button in the loading error alert and loading succeeds', () => {
+ beforeEach(async () => {
+ renderMarkdown.mockResolvedValueOnce('hello markdown');
+ await wrapper.findComponent(GlAlert).vm.$emit('primaryAction');
+ });
+
+ it('hides the loading error alert', () => {
+ expect(findContentEditorAlert().text()).toBe('');
+ });
+
+ it('sets the content editor as writable', async () => {
+ await nextTick();
+
+ expect(findEditorContent().props().editor.isEditable).toBe(true);
+ });
+ });
+ });
});
- it('removes is-focused class when blur event is emitted', async () => {
- createWrapper();
+ describe('when focused event is emitted', () => {
+ beforeEach(async () => {
+ createWrapper();
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' });
+ findEditorStateObserver().vm.$emit('focus');
- expect(findEditorElement().classes()).not.toContain('is-focused');
+ await nextTick();
+ });
+
+ it('adds is-focused class when focus event is emitted', () => {
+ expect(findEditorElement().classes()).toContain('is-focused');
+ });
+
+ it('removes is-focused class when blur event is emitted', async () => {
+ findEditorStateObserver().vm.$emit('blur');
+
+ await nextTick();
+
+ expect(findEditorElement().classes()).not.toContain('is-focused');
+ });
});
- it('emits change event when document is updated', async () => {
- createWrapper();
+ describe('when editorStateObserver emits docUpdate event', () => {
+ it('emits change event with the latest markdown', async () => {
+ const markdown = 'Loaded content';
- await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' });
+ renderMarkdown.mockResolvedValueOnce(markdown);
- expect(wrapper.emitted('change')).toEqual([
- [
- {
- empty: contentEditor.empty,
- },
- ],
- ]);
- });
+ createWrapper({ markdown: 'initial content' });
- it('renders content_editor_alert component', () => {
- createWrapper();
+ await nextTick();
+ await waitForPromises();
- expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true);
- });
+ findEditorStateObserver().vm.$emit('docUpdate');
- it('renders loading indicator component', () => {
- createWrapper();
-
- expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true);
+ expect(wrapper.emitted('change')).toEqual([
+ [
+ {
+ markdown,
+ changed: false,
+ empty: false,
+ },
+ ],
+ ]);
+ });
});
it.each`
@@ -129,17 +224,4 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(component).exists()).toBe(true);
});
-
- it.each`
- event
- ${'loading'}
- ${'loadingSuccess'}
- ${'loadingError'}
- `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => {
- createWrapper();
-
- findEditorStateObserver().vm.$emit(event);
-
- expect(wrapper.emitted(event)).toHaveLength(1);
- });
});
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
index 51a594a606b..e8c2d8c8793 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -4,12 +4,7 @@ import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
- ALERT_EVENT,
-} from '~/content_editor/constants';
+import { ALERT_EVENT } from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
@@ -18,9 +13,6 @@ describe('content_editor/components/editor_state_observer', () => {
let onDocUpdateListener;
let onSelectionUpdateListener;
let onTransactionListener;
- let onLoadingContentListener;
- let onLoadingSuccessListener;
- let onLoadingErrorListener;
let onAlertListener;
let eventHub;
@@ -38,9 +30,6 @@ describe('content_editor/components/editor_state_observer', () => {
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
[ALERT_EVENT]: onAlertListener,
- [LOADING_CONTENT_EVENT]: onLoadingContentListener,
- [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener,
- [LOADING_ERROR_EVENT]: onLoadingErrorListener,
},
});
};
@@ -50,9 +39,6 @@ describe('content_editor/components/editor_state_observer', () => {
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
onAlertListener = jest.fn();
- onLoadingSuccessListener = jest.fn();
- onLoadingContentListener = jest.fn();
- onLoadingErrorListener = jest.fn();
buildEditor();
});
@@ -81,11 +67,8 @@ describe('content_editor/components/editor_state_observer', () => {
});
it.each`
- event | listener
- ${ALERT_EVENT} | ${() => onAlertListener}
- ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener}
- ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener}
- ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener}
+ event | listener
+ ${ALERT_EVENT} | ${() => onAlertListener}
`('listens to $event event in the eventBus object', ({ event, listener }) => {
const args = {};
@@ -114,9 +97,6 @@ describe('content_editor/components/editor_state_observer', () => {
it.each`
event
${ALERT_EVENT}
- ${LOADING_CONTENT_EVENT}
- ${LOADING_SUCCESS_EVENT}
- ${LOADING_ERROR_EVENT}
`('removes $event event hook from eventHub', ({ event }) => {
jest.spyOn(eventHub, '$off');
jest.spyOn(eventHub, '$on');
diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js
index e4fb09b70a4..0065103d01b 100644
--- a/spec/frontend/content_editor/components/loading_indicator_spec.js
+++ b/spec/frontend/content_editor/components/loading_indicator_spec.js
@@ -1,18 +1,10 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
-import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
describe('content_editor/components/loading_indicator', () => {
let wrapper;
- const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createWrapper = () => {
@@ -24,48 +16,12 @@ describe('content_editor/components/loading_indicator', () => {
});
describe('when loading content', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
-
- await nextTick();
});
it('displays loading indicator', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
-
- describe('when loading content succeeds', () => {
- beforeEach(async () => {
- createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
- await nextTick();
- findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
-
- describe('when loading content fails', () => {
- const error = 'error';
-
- beforeEach(async () => {
- createWrapper();
-
- findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT);
- await nextTick();
- findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error);
- await nextTick();
- });
-
- it('hides loading indicator', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index a3553e612ca..6175cbdd3d4 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -1,8 +1,3 @@
-import {
- LOADING_CONTENT_EVENT,
- LOADING_SUCCESS_EVENT,
- LOADING_ERROR_EVENT,
-} from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
@@ -14,6 +9,7 @@ describe('content_editor/services/content_editor', () => {
let eventHub;
let doc;
let p;
+ const testMarkdown = '**bold text**';
beforeEach(() => {
const tiptapEditor = createTestEditor();
@@ -36,6 +32,9 @@ describe('content_editor/services/content_editor', () => {
});
});
+ const testDoc = () => doc(p('document'));
+ const testEmptyDoc = () => doc();
+
describe('.dispose', () => {
it('destroys the tiptapEditor', () => {
expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled();
@@ -46,51 +45,77 @@ describe('content_editor/services/content_editor', () => {
});
});
+ describe('empty', () => {
+ it('returns true when tiptapEditor is empty', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testEmptyDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ expect(contentEditor.empty).toBe(true);
+ });
+
+ it('returns false when tiptapEditor is not empty', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ expect(contentEditor.empty).toBe(false);
+ });
+ });
+
+ describe('editable', () => {
+ it('returns true when tiptapEditor is editable', async () => {
+ contentEditor.setEditable(true);
+
+ expect(contentEditor.editable).toBe(true);
+ });
+
+ it('returns false when tiptapEditor is readonly', async () => {
+ contentEditor.setEditable(false);
+
+ expect(contentEditor.editable).toBe(false);
+ });
+ });
+
+ describe('changed', () => {
+ it('returns true when the initial document changes', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ contentEditor.tiptapEditor.commands.insertContent(' new content');
+
+ expect(contentEditor.changed).toBe(true);
+ });
+
+ it('returns false when the initial document hasn’t changed', async () => {
+ deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() });
+
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ expect(contentEditor.changed).toBe(false);
+ });
+
+ it('returns false when an initial document is not set and the document is empty', () => {
+ expect(contentEditor.changed).toBe(false);
+ });
+
+ it('returns true when an initial document is not set and the document is not empty', () => {
+ contentEditor.tiptapEditor.commands.insertContent('new content');
+
+ expect(contentEditor.changed).toBe(true);
+ });
+ });
+
describe('when setSerializedContent succeeds', () => {
- let document;
- const languages = ['javascript'];
- const testMarkdown = '**bold text**';
-
- beforeEach(() => {
- document = doc(p('document'));
- deserializer.deserialize.mockResolvedValueOnce({ document, languages });
- });
-
- it('emits loadingContent and loadingSuccess event in the eventHub', () => {
- let loadingContentEmitted = false;
-
- eventHub.$on(LOADING_CONTENT_EVENT, () => {
- loadingContentEmitted = true;
- });
- eventHub.$on(LOADING_SUCCESS_EVENT, () => {
- expect(loadingContentEmitted).toBe(true);
- });
-
- contentEditor.setSerializedContent(testMarkdown);
- });
-
it('sets the deserialized document in the tiptap editor object', async () => {
+ const document = testDoc();
+
+ deserializer.deserialize.mockResolvedValueOnce({ document });
+
await contentEditor.setSerializedContent(testMarkdown);
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
});
-
- describe('when setSerializedContent fails', () => {
- const error = 'error';
-
- beforeEach(() => {
- deserializer.deserialize.mockRejectedValueOnce(error);
- });
-
- it('emits loadingError event', async () => {
- eventHub.$on(LOADING_ERROR_EVENT, (e) => {
- expect(e).toBe('error');
- });
-
- await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual(
- error,
- );
- });
- });
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 204c48f8de1..36a926990f2 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -302,19 +302,15 @@ describe('WikiForm', () => {
});
it.each`
- format | enabled | action
+ format | exists | action
${'markdown'} | ${true} | ${'displays'}
${'rdoc'} | ${false} | ${'hides'}
${'asciidoc'} | ${false} | ${'hides'}
${'org'} | ${false} | ${'hides'}
- `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => {
+ `('$action toggle editing mode button when format is $format', async ({ format, exists }) => {
await setFormat(format);
- expect(findToggleEditingModeButton().exists()).toBe(enabled);
- });
-
- it('displays toggle editing mode button', () => {
- expect(findToggleEditingModeButton().exists()).toBe(true);
+ expect(findToggleEditingModeButton().exists()).toBe(exists);
});
describe('when content editor is not active', () => {
@@ -351,15 +347,8 @@ describe('WikiForm', () => {
});
describe('when content editor is active', () => {
- let mockContentEditor;
-
beforeEach(() => {
createWrapper();
- mockContentEditor = {
- getSerializedContent: jest.fn(),
- setSerializedContent: jest.fn(),
- };
-
findToggleEditingModeButton().vm.$emit('input', 'richText');
});
@@ -368,14 +357,7 @@ describe('WikiForm', () => {
});
describe('when clicking the toggle editing mode button', () => {
- const contentEditorFakeSerializedContent = 'fake content';
-
beforeEach(async () => {
- mockContentEditor.getSerializedContent.mockReturnValueOnce(
- contentEditorFakeSerializedContent,
- );
-
- findContentEditor().vm.$emit('initialized', mockContentEditor);
await findToggleEditingModeButton().vm.$emit('input', 'source');
await nextTick();
});
@@ -387,10 +369,6 @@ describe('WikiForm', () => {
it('displays the classic editor', () => {
expect(findClassicEditor().exists()).toBe(true);
});
-
- it('updates the classic editor content field', () => {
- expect(findContent().element.value).toBe(contentEditorFakeSerializedContent);
- });
});
describe('when content editor is loading', () => {
@@ -480,8 +458,14 @@ describe('WikiForm', () => {
});
describe('when wiki content is updated', () => {
+ const updatedMarkdown = 'hello **world**';
+
beforeEach(() => {
- findContentEditor().vm.$emit('change', { empty: false });
+ findContentEditor().vm.$emit('change', {
+ empty: false,
+ changed: true,
+ markdown: updatedMarkdown,
+ });
});
it('sets before unload warning', () => {
@@ -512,16 +496,8 @@ describe('WikiForm', () => {
});
});
- it('updates content from content editor on form submit', async () => {
- // old value
- expect(findContent().element.value).toBe(' My page content ');
-
- // wait for content editor to load
- await waitForPromises();
-
- await triggerFormSubmit();
-
- expect(findContent().element.value).toBe('hello **world**');
+ it('sets content field to the content editor updated markdown', async () => {
+ expect(findContent().element.value).toBe(updatedMarkdown);
});
});
});
diff --git a/spec/frontend_integration/content_editor/content_editor_integration_spec.js b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
index 12cd6dcad83..7781a463fd6 100644
--- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js
+++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
@@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { ContentEditor } from '~/content_editor';
+import waitForPromises from 'helpers/wait_for_promises';
/**
* This spec exercises some workflows in the Content Editor without mocking
@@ -10,32 +11,34 @@ import { ContentEditor } from '~/content_editor';
describe('content_editor', () => {
let wrapper;
let renderMarkdown;
- let contentEditorService;
- const buildWrapper = () => {
- renderMarkdown = jest.fn();
+ const buildWrapper = ({ markdown = '' } = {}) => {
wrapper = mountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath: '/',
- },
- listeners: {
- initialized(contentEditor) {
- contentEditorService = contentEditor;
- },
+ markdown,
},
});
};
+ const waitUntilContentIsLoaded = async () => {
+ await waitForPromises();
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ });
+
describe('when loading initial content', () => {
describe('when the initial content is empty', () => {
it('still hides the loading indicator', async () => {
- buildWrapper();
-
renderMarkdown.mockResolvedValue('');
- await contentEditorService.setSerializedContent('');
- await nextTick();
+ buildWrapper();
+
+ await waitUntilContentIsLoaded();
expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false);
});
@@ -44,14 +47,13 @@ describe('content_editor', () => {
describe('when the initial content is not empty', () => {
const initialContent = 'bold text
';
beforeEach(async () => {
- buildWrapper();
-
renderMarkdown.mockResolvedValue(initialContent);
- await contentEditorService.setSerializedContent('**bold text**');
- await nextTick();
+ buildWrapper();
+
+ await waitUntilContentIsLoaded();
});
- it('hides the loading indicator', async () => {
+ it('hides the loading indicator', () => {
expect(wrapper.findByTestId('content-editor-loading-indicator').exists()).toBe(false);
});
@@ -70,27 +72,29 @@ describe('content_editor', () => {
});
it('processes and renders footnote ids alongside the footnote definition', async () => {
- buildWrapper();
-
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
This reference tag is a mix of letters and numbers [^footnote].
[^footnote]: This is another footnote.
- `);
- await nextTick();
+ `,
+ });
+
+ await waitUntilContentIsLoaded();
expect(wrapper.text()).toContain('footnote: This is another footnote');
});
it('processes and displays reference definitions', async () => {
- buildWrapper();
-
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
[GitLab][gitlab]
[gitlab]: https://gitlab.com
- `);
- await nextTick();
+ `,
+ });
+
+ await waitUntilContentIsLoaded();
expect(wrapper.find('pre').text()).toContain('[gitlab]: https://gitlab.com');
});
@@ -99,9 +103,7 @@ This reference tag is a mix of letters and numbers [^footnote].
it('renders table of contents', async () => {
jest.useFakeTimers();
- buildWrapper();
-
- renderMarkdown.mockResolvedValue(`
+ renderMarkdown.mockResolvedValueOnce(`
@@ -112,16 +114,17 @@ This reference tag is a mix of letters and numbers [^footnote].
`);
- await contentEditorService.setSerializedContent(`
+ buildWrapper({
+ markdown: `
[TOC]
# Heading 1
## Heading 2
- `);
+ `,
+ });
- await nextTick();
- jest.runAllTimers();
+ await waitUntilContentIsLoaded();
expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 1');
expect(wrapper.findByTestId('table-of-contents').text()).toContain('Heading 2');
diff --git a/spec/graphql/resolvers/group_members_resolver_spec.rb b/spec/graphql/resolvers/group_members_resolver_spec.rb
index bd0b4870062..d860b87875e 100644
--- a/spec/graphql/resolvers/group_members_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_members_resolver_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Resolvers::GroupMembersResolver do
+RSpec.describe 'Resolvers::GroupMembersResolver' do
include GraphqlHelpers
+ let(:described_class) { Resolvers::GroupMembersResolver }
+
specify do
expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type)
end
diff --git a/spec/graphql/resolvers/project_members_resolver_spec.rb b/spec/graphql/resolvers/project_members_resolver_spec.rb
index 2f4145b3215..c38cb3d157b 100644
--- a/spec/graphql/resolvers/project_members_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_members_resolver_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Resolvers::ProjectMembersResolver do
+RSpec.describe 'Resolvers::ProjectMembersResolver' do
include GraphqlHelpers
+ let(:described_class) { Resolvers::ProjectMembersResolver }
+
it_behaves_like 'querying members with a group' do
let_it_be(:project) { create(:project, group: group_1) }
let_it_be(:resource_member) { create(:project_member, user: user_1, project: project) }
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
index 7c9dfd6b5be..68f847e80b0 100644
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe LearnGitlabHelper do
allow(learn_gitlab).to receive(:project).and_return(project)
end
- OnboardingProgress.onboard(namespace)
- OnboardingProgress.register(namespace, :git_write)
+ Onboarding::Progress.onboard(namespace)
+ Onboarding::Progress.register(namespace, :git_write)
end
describe '#learn_gitlab_enabled?' do
@@ -37,7 +37,7 @@ RSpec.describe LearnGitlabHelper do
with_them do
before do
- allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
+ allow(Onboarding::Progress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(LearnGitlab::Project, user).to receive(:available?).and_return(learn_gitlab_available)
end
diff --git a/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb b/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb
index 3a5864e1832..b25ce4ea9da 100644
--- a/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb
+++ b/spec/lib/gitlab/web_hooks/rate_limiter_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::WebHooks::RateLimiter, :clean_gitlab_redis_rate_limiting
let_it_be(:plan) { create(:default_plan) }
let_it_be_with_reload(:project_hook) { create(:project_hook) }
let_it_be_with_reload(:system_hook) { create(:system_hook) }
- let_it_be_with_reload(:integration_hook) { create(:service_hook) }
+ let_it_be_with_reload(:integration_hook) { create(:jenkins_integration).service_hook }
let_it_be(:limit) { 1 }
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/learn_gitlab/onboarding_spec.rb b/spec/lib/learn_gitlab/onboarding_spec.rb
index 3e22ce59091..97926f8a612 100644
--- a/spec/lib/learn_gitlab/onboarding_spec.rb
+++ b/spec/lib/learn_gitlab/onboarding_spec.rb
@@ -13,11 +13,11 @@ RSpec.describe LearnGitlab::Onboarding do
*described_class::ACTION_ISSUE_IDS.keys,
*described_class::ACTION_PATHS,
:security_scan_enabled
- ].map { |key| OnboardingProgress.column_name(key) }
+ ].map { |key| ::Onboarding::Progress.column_name(key) }
end
before do
- expect(OnboardingProgress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress)
+ expect(::Onboarding::Progress).to receive(:find_by).with(namespace: namespace).and_return(onboarding_progress)
end
subject { described_class.new(namespace).completed_percentage }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 85a30fe02cc..84ca8ac47fe 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3629,8 +3629,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#environments_in_self_and_descendants' do
- subject { pipeline.environments_in_self_and_descendants }
+ describe '#environments_in_self_and_project_descendants' do
+ subject { pipeline.environments_in_self_and_project_descendants }
context 'when pipeline is not child nor parent' do
let_it_be(:pipeline) { create(:ci_pipeline, :created) }
@@ -4036,13 +4036,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#self_and_descendants_complete?' do
+ describe '#self_and_project_descendants_complete?' do
let_it_be(:pipeline) { create(:ci_pipeline, :success) }
let_it_be(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) }
let_it_be_with_reload(:grandchild_pipeline) { create(:ci_pipeline, :success, child_of: child_pipeline) }
context 'when all pipelines in the hierarchy is complete' do
- it { expect(pipeline.self_and_descendants_complete?).to be(true) }
+ it { expect(pipeline.self_and_project_descendants_complete?).to be(true) }
end
context 'when a pipeline in the hierarchy is not complete' do
@@ -4050,12 +4050,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
grandchild_pipeline.update!(status: :running)
end
- it { expect(pipeline.self_and_descendants_complete?).to be(false) }
+ it { expect(pipeline.self_and_project_descendants_complete?).to be(false) }
end
end
- describe '#builds_in_self_and_descendants' do
- subject(:builds) { pipeline.builds_in_self_and_descendants }
+ describe '#builds_in_self_and_project_descendants' do
+ subject(:builds) { pipeline.builds_in_self_and_project_descendants }
let(:pipeline) { create(:ci_pipeline) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
@@ -4087,7 +4087,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#build_with_artifacts_in_self_and_descendants' do
+ describe '#build_with_artifacts_in_self_and_project_descendants' do
let_it_be(:pipeline) { create(:ci_pipeline) }
let!(:build) { create(:ci_build, name: 'test', pipeline: pipeline) }
@@ -4095,14 +4095,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let!(:child_build) { create(:ci_build, :artifacts, name: 'test', pipeline: child_pipeline) }
it 'returns the build with a given name, having artifacts' do
- expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(child_build)
+ expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(child_build)
end
context 'when same job name is present in both parent and child pipeline' do
let!(:build) { create(:ci_build, :artifacts, name: 'test', pipeline: pipeline) }
it 'returns the job in the parent pipeline' do
- expect(pipeline.build_with_artifacts_in_self_and_descendants('test')).to eq(build)
+ expect(pipeline.build_with_artifacts_in_self_and_project_descendants('test')).to eq(build)
end
end
end
@@ -4183,7 +4183,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#latest_report_builds_in_self_and_descendants' do
+ describe '#latest_report_builds_in_self_and_project_descendants' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
let_it_be(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
@@ -4193,21 +4193,21 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
child_build = create(:ci_build, :coverage_reports, pipeline: child_pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(parent_build, child_build, grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(parent_build, child_build, grandchild_build)
end
it 'filters builds by scope' do
create(:ci_build, :test_reports, pipeline: pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants(Ci::JobArtifact.of_report_type(:codequality))).to contain_exactly(grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants(Ci::JobArtifact.of_report_type(:codequality))).to contain_exactly(grandchild_build)
end
it 'only returns builds that are not retried' do
create(:ci_build, :codequality_reports, :retried, pipeline: grandchild_pipeline)
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
- expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(grandchild_build)
+ expect(pipeline.latest_report_builds_in_self_and_project_descendants).to contain_exactly(grandchild_build)
end
end
@@ -4967,8 +4967,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe '#self_and_ancestors' do
- subject(:self_and_ancestors) { pipeline.self_and_ancestors }
+ describe '#self_and_project_ancestors' do
+ subject(:self_and_project_ancestors) { pipeline.self_and_project_ancestors }
context 'when pipeline is child' do
let(:pipeline) { create(:ci_pipeline, :created) }
@@ -4981,7 +4981,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'returns parent and self' do
- expect(self_and_ancestors).to contain_exactly(parent, pipeline)
+ expect(self_and_project_ancestors).to contain_exactly(parent, pipeline)
end
end
@@ -4995,7 +4995,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'returns only self' do
- expect(self_and_ancestors).to contain_exactly(pipeline)
+ expect(self_and_project_ancestors).to contain_exactly(pipeline)
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ae8748f8ae3..70fa942d8b8 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1326,54 +1326,6 @@ RSpec.describe Ci::Runner do
end
end
- describe '#assigned_to_group?' do
- subject { runner.assigned_to_group? }
-
- context 'when project runner' do
- let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- let(:project) { create(:project) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when shared runner' do
- let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when group runner' do
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
-
- it { is_expected.to be_truthy }
- end
- end
-
- describe '#assigned_to_project?' do
- subject { runner.assigned_to_project? }
-
- context 'when group runner' do
- let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
- let(:group) { create(:group) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when shared runner' do
- let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when project runner' do
- let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- let(:project) { create(:project) }
-
- it { is_expected.to be_truthy }
- end
- end
-
describe '#pick_build!' do
let(:build) { create(:ci_build) }
let(:runner) { create(:ci_runner) }
diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index c43d969eec8..905fee075ad 100644
--- a/spec/models/integrations/drone_ci_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -115,7 +115,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
it_behaves_like Integrations::HasWebHook do
include_context :drone_ci_integration
- let(:drone_url) { 'https://cloud.drone.io' }
let(:integration) { drone }
let(:hook_url) { "#{drone_url}/hook?owner=#{project.namespace.full_path}&name=#{project.path}&access_token=#{token}" }
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding/progress_spec.rb
similarity index 82%
rename from spec/models/onboarding_progress_spec.rb
rename to spec/models/onboarding/progress_spec.rb
index 9688dd01c71..9d91af2487a 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding/progress_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OnboardingProgress do
+RSpec.describe Onboarding::Progress do
let(:namespace) { create(:namespace) }
let(:action) { :subscription_created }
@@ -34,7 +34,9 @@ RSpec.describe OnboardingProgress do
subject { described_class.incomplete_actions(actions) }
let!(:no_actions_completed) { create(:onboarding_progress) }
- let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
+ let!(:one_action_completed_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => Time.current)
+ end
context 'when given one action' do
let(:actions) { action }
@@ -52,8 +54,13 @@ RSpec.describe OnboardingProgress do
describe '.completed_actions' do
subject { described_class.completed_actions(actions) }
- let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
- let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) }
+ let!(:one_action_completed_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => Time.current)
+ end
+
+ let!(:both_actions_completed) do
+ create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current)
+ end
context 'when given one action' do
let(:actions) { action }
@@ -69,12 +76,23 @@ RSpec.describe OnboardingProgress do
end
describe '.completed_actions_with_latest_in_range' do
- subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) }
+ subject do
+ described_class.completed_actions_with_latest_in_range(actions,
+ 1.day.ago.beginning_of_day..1.day.ago.end_of_day)
+ end
+
+ let!(:one_action_completed_in_range_one_action_incompleted) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day)
+ end
- let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) }
let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) }
- let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) }
- let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) }
+ let!(:both_actions_completed_latest_action_out_of_range) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current)
+ end
+
+ let!(:both_actions_completed_latest_action_in_range) do
+ create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day)
+ end
context 'when given one action' do
let(:actions) { :git_write }
@@ -147,7 +165,9 @@ RSpec.describe OnboardingProgress do
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).to be_nil
register_action
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).not_to be_nil
- expect { described_class.register(namespace, action) }.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
+ expect do
+ described_class.register(namespace, action)
+ end.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
end
context 'when the action does not exist' do
@@ -209,7 +229,9 @@ RSpec.describe OnboardingProgress do
context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action1) }.from(false)
- expect { described_class.register(namespace, action) }.not_to change { described_class.completed?(namespace, action2) }.from(false)
+ expect do
+ described_class.register(namespace, action)
+ end.not_to change { described_class.completed?(namespace, action2) }.from(false)
end
end
end
@@ -270,21 +292,23 @@ RSpec.describe OnboardingProgress do
end
describe '#number_of_completed_actions' do
- subject { build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions }
+ subject do
+ build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions
+ end
- context '0 completed actions' do
+ context 'with 0 completed actions' do
let(:actions) { [:created_at, :updated_at] }
it { is_expected.to eq(0) }
end
- context '1 completed action' do
+ context 'with 1 completed action' do
let(:actions) { [:created_at, :subscription_created_at] }
it { is_expected.to eq(1) }
end
- context '2 completed actions' do
+ context 'with 2 completed actions' do
let(:actions) { [:subscription_created_at, :git_write_at] }
it { is_expected.to eq(2) }
diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb
index b0c9826b137..4c581f31ccb 100644
--- a/spec/services/environments/stop_service_spec.rb
+++ b/spec/services/environments/stop_service_spec.rb
@@ -193,7 +193,7 @@ RSpec.describe Environments::StopService do
end
it 'has active environment at first' do
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
context 'when user is a developer' do
@@ -203,7 +203,7 @@ RSpec.describe Environments::StopService do
it 'stops the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_stopping
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_stopping
end
context 'when pipeline is a branch pipeline for merge request' do
@@ -218,7 +218,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
@@ -244,7 +244,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
@@ -268,7 +268,7 @@ RSpec.describe Environments::StopService do
it 'does not stop the active environment' do
subject
- expect(pipeline.environments_in_self_and_descendants.first).to be_available
+ expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
end
end
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0cfde9ef434..0a8164c9ca3 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
it 'adds an onboarding progress record' do
- expect { subject }.to change(OnboardingProgress, :count).from(0).to(1)
+ expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1)
end
context 'with before_commit callback' do
@@ -108,7 +108,7 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
it 'does not add an onboarding progress record' do
- expect { subject }.not_to change(OnboardingProgress, :count).from(0)
+ expect { subject }.not_to change(Onboarding::Progress, :count).from(0)
end
it_behaves_like 'has sync-ed traversal_ids'
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index fe9f3ddc14d..25696ca209e 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
case source
when Project
source.add_maintainer(user)
- OnboardingProgress.onboard(source.namespace)
+ Onboarding::Progress.onboard(source.namespace)
when Group
source.add_owner(user)
- OnboardingProgress.onboard(source)
+ Onboarding::Progress.onboard(source)
end
end
@@ -59,7 +59,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds a user to members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
context 'when user_id is passed as an integer' do
@@ -68,7 +68,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates member' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -78,7 +78,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include(member, user_invited_by_id)
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -88,7 +88,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'successfully creates members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include(member, user_invited_by_id)
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -98,7 +98,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds a user to members' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include member
- expect(OnboardingProgress.completed?(source, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source, :user_added)).to be(true)
end
it 'triggers a members added event' do
@@ -119,7 +119,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to be_present
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -130,7 +130,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to be_present
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -141,7 +141,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to include("#{member.username}: Access level is not included in the list")
expect(source.users).not_to include member
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -153,7 +153,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'allows already invited members to be re-invited by email and updates the member access' do
expect(execute_service[:status]).to eq(:success)
expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
@@ -170,7 +170,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'does not update the member' do
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member")
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
end
@@ -178,7 +178,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
it 'adds the member' do
expect(execute_service[:status]).to eq(:success)
expect(source.users).to include project_bot
- expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(true)
+ expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(true)
end
end
end
diff --git a/spec/services/onboarding_progress_service_spec.rb b/spec/services/onboarding_progress_service_spec.rb
index ef4f4f0d822..6ff6b03d808 100644
--- a/spec/services/onboarding_progress_service_spec.rb
+++ b/spec/services/onboarding_progress_service_spec.rb
@@ -19,12 +19,12 @@ RSpec.describe OnboardingProgressService do
context 'when onboarded' do
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
end
context 'when action is already completed' do
before do
- OnboardingProgress.register(namespace, action)
+ Onboarding::Progress.register(namespace, action)
end
it 'does not schedule a worker' do
@@ -52,13 +52,13 @@ RSpec.describe OnboardingProgressService do
context 'when the namespace is a root' do
before do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
end
it 'registers a namespace onboarding progress action for the given namespace' do
execute_service
- expect(OnboardingProgress.completed?(namespace, :subscription_created)).to eq(true)
+ expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to eq(true)
end
end
@@ -66,13 +66,13 @@ RSpec.describe OnboardingProgressService do
let(:group) { create(:group, :nested) }
before do
- OnboardingProgress.onboard(group)
+ Onboarding::Progress.onboard(group)
end
it 'does not register a namespace onboarding progress action' do
execute_service
- expect(OnboardingProgress.completed?(group, :subscription_created)).to be(nil)
+ expect(Onboarding::Progress.completed?(group, :subscription_created)).to be(nil)
end
end
@@ -82,7 +82,7 @@ RSpec.describe OnboardingProgressService do
it 'does not register a namespace onboarding progress action' do
execute_service
- expect(OnboardingProgress.completed?(namespace, :subscription_created)).to be(nil)
+ expect(Onboarding::Progress.completed?(namespace, :subscription_created)).to be(nil)
end
end
end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index f0c50e20a72..5cba8baa829 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -52,6 +52,15 @@ RSpec.shared_examples 'querying members with a group' do
expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member)
end
+ context 'with sort options' do
+ let(:args) { { sort: 'name_asc' } }
+
+ it 'searches users by user name' do
+ # the order is important here
+ expect(subject.items).to eq([root_group_member, resource_member, group_1_member])
+ end
+ end
+
context 'with search' do
context 'when the search term matches a user' do
let(:args) { { search: 'test' } }
diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
index 64b2dcb8d36..2f693edeb53 100644
--- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
@@ -7,6 +7,30 @@ RSpec.shared_examples Integrations::HasWebHook do
it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:service_id) }
end
+ describe 'callbacks' do
+ it 'calls #update_web_hook! when enabled' do
+ expect(integration).to receive(:update_web_hook!)
+
+ integration.active = true
+ integration.save!
+ end
+
+ it 'does not call #update_web_hook! when disabled' do
+ expect(integration).not_to receive(:update_web_hook!)
+
+ integration.active = false
+ integration.save!
+ end
+
+ it 'does not call #update_web_hook! when validation fails' do
+ expect(integration).not_to receive(:update_web_hook!)
+
+ integration.active = true
+ integration.project = nil
+ expect(integration.save).to be(false)
+ end
+ end
+
describe '#hook_url' do
it 'returns a string' do
expect(integration.hook_url).to be_a(String)
diff --git a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
index 53116815ce7..0a896d864b7 100644
--- a/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
+++ b/spec/workers/namespaces/onboarding_issue_created_worker_spec.rb
@@ -19,11 +19,11 @@ RSpec.describe Namespaces::OnboardingIssueCreatedWorker, '#perform' do
let(:job_args) { [namespace.id] }
it 'sets the onboarding progress action' do
- OnboardingProgress.onboard(namespace)
+ Onboarding::Progress.onboard(namespace)
subject
- expect(OnboardingProgress.completed?(namespace, :issue_created)).to eq(true)
+ expect(Onboarding::Progress.completed?(namespace, :issue_created)).to eq(true)
end
end
end