summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-13 09:07:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-13 09:07:54 +0000
commit2705a15deaef07b1a38a53b9539d02f8ad499ce3 (patch)
tree33d51226e6c70efe71e5e531bb8073cb145bdc22
parent1a397155d654096edc0a900e2b69f6913bc26eb4 (diff)
downloadgitlab-ce-2705a15deaef07b1a38a53b9539d02f8ad499ce3.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--.rubocop_todo/naming/rescued_exceptions_variable_name.yml222
-rw-r--r--CONTRIBUTING.md13
-rw-r--r--app/assets/javascripts/crm/contacts/components/contacts_root.vue2
-rw-r--r--app/assets/javascripts/crm/organizations/components/organizations_root.vue2
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue99
-rw-r--r--app/assets/javascripts/issues/issue.js4
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue16
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js8
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue14
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue37
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue8
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/helpers/badges_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb20
-rw-r--r--app/helpers/routing/projects_helper.rb16
-rw-r--r--app/presenters/issue_presenter.rb16
-rw-r--r--app/serializers/issue_board_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb2
-rw-r--r--app/serializers/linked_issue_entity.rb2
-rw-r--r--app/services/members/creator_service.rb8
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml7
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/_status_box.html.haml13
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml17
-rw-r--r--config/feature_flags/development/contacts_autocomplete.yml2
-rw-r--r--config/feature_flags/development/customer_relations.yml2
-rw-r--r--doc/ci/pipelines/cicd_minutes.md23
-rw-r--r--doc/install/index.md6
-rw-r--r--doc/user/application_security/dast/checks/16.6.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md18
-rw-r--r--doc/user/crm/index.md18
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md2
-rw-r--r--doc/user/project/import/cvs.md4
-rw-r--r--doc/user/project/merge_requests/code_quality.md2
-rw-r--r--doc/user/project/merge_requests/reviews/index.md2
-rw-r--r--doc/user/shortcuts.md2
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/fixtures/package_managers/maven/build_install.gradle.erb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb40
-rw-r--r--spec/frontend/crm/contacts_root_spec.js2
-rw-r--r--spec/frontend/crm/organizations_root_spec.js4
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js74
-rw-r--r--spec/frontend/issues/issue_spec.js4
-rw-r--r--spec/frontend/sidebar/components/crm_contacts_spec.js11
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/show/mock_data.js5
-rw-r--r--spec/helpers/badges_helper_spec.rb6
-rw-r--r--spec/helpers/issuables_helper_spec.rb35
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb25
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb16
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb11
-rw-r--r--spec/serializers/issue_board_entity_spec.rb16
-rw-r--r--spec/serializers/issue_entity_spec.rb13
-rw-r--r--spec/serializers/linked_project_issue_entity_spec.rb22
-rw-r--r--spec/services/members/groups/creator_service_spec.rb24
-rw-r--r--spec/services/members/projects/creator_service_spec.rb24
-rw-r--r--spec/support/helpers/trial_status_widget_test_helper.rb9
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb403
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb16
67 files changed, 935 insertions, 487 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index abe3a4faf4d..e4db93d1602 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -11,12 +11,6 @@ Gitlab/PolicyRuleBoolean:
Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb'
-# Offense count: 218
-# Cop supports --auto-correct.
-# Configuration parameters: PreferredName.
-Naming/RescuedExceptionsVariableName:
- Enabled: false
-
# Offense count: 29
# Configuration parameters: MinSize.
Performance/CollectionLiteralInLoop:
diff --git a/.rubocop_todo/naming/rescued_exceptions_variable_name.yml b/.rubocop_todo/naming/rescued_exceptions_variable_name.yml
new file mode 100644
index 00000000000..8d4d2da29ac
--- /dev/null
+++ b/.rubocop_todo/naming/rescued_exceptions_variable_name.yml
@@ -0,0 +1,222 @@
+---
+# Cop supports --auto-correct.
+Naming/RescuedExceptionsVariableName:
+ # Offense count: 269
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'app/controllers/admin/projects_controller.rb'
+ - 'app/controllers/projects/google_cloud/deployments_controller.rb'
+ - 'app/controllers/projects/google_cloud/service_accounts_controller.rb'
+ - 'app/controllers/projects/merge_requests/drafts_controller.rb'
+ - 'app/controllers/projects/milestones_controller.rb'
+ - 'app/controllers/projects/mirrors_controller.rb'
+ - 'app/controllers/projects/repositories_controller.rb'
+ - 'app/controllers/projects_controller.rb'
+ - 'app/finders/repositories/changelog_tag_finder.rb'
+ - 'app/graphql/mutations/issues/move.rb'
+ - 'app/graphql/resolvers/ci/config_resolver.rb'
+ - 'app/graphql/resolvers/environments_resolver.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/application_setting.rb'
+ - 'app/models/blob_viewer/metrics_dashboard_yml.rb'
+ - 'app/models/ci/build.rb'
+ - 'app/models/ci/deleted_object.rb'
+ - 'app/models/clusters/concerns/elasticsearch_client.rb'
+ - 'app/models/concerns/prometheus_adapter.rb'
+ - 'app/models/concerns/repository_storage_movable.rb'
+ - 'app/models/concerns/x509_serial_number_attribute.rb'
+ - 'app/models/integrations/base_issue_tracker.rb'
+ - 'app/models/integrations/discord.rb'
+ - 'app/models/integrations/jenkins.rb'
+ - 'app/models/integrations/jira.rb'
+ - 'app/models/integrations/packagist.rb'
+ - 'app/models/integrations/pipelines_email.rb'
+ - 'app/models/integrations/prometheus.rb'
+ - 'app/models/performance_monitoring/prometheus_dashboard.rb'
+ - 'app/models/personal_access_token.rb'
+ - 'app/models/project.rb'
+ - 'app/models/repository.rb'
+ - 'app/models/snippet_repository.rb'
+ - 'app/models/u2f_registration.rb'
+ - 'app/models/wiki.rb'
+ - 'app/services/branches/delete_service.rb'
+ - 'app/services/branches/validate_new_service.rb'
+ - 'app/services/ci/job_artifacts/create_service.rb'
+ - 'app/services/ci/parse_dotenv_artifact_service.rb'
+ - 'app/services/ci/register_job_service.rb'
+ - 'app/services/ci/stuck_builds/drop_helpers.rb'
+ - 'app/services/clusters/applications/prometheus_update_service.rb'
+ - 'app/services/commits/change_service.rb'
+ - 'app/services/commits/create_service.rb'
+ - 'app/services/dependency_proxy/head_manifest_service.rb'
+ - 'app/services/dependency_proxy/request_token_service.rb'
+ - 'app/services/design_management/copy_design_collection/copy_service.rb'
+ - 'app/services/git/base_hooks_service.rb'
+ - 'app/services/grafana/proxy_service.rb'
+ - 'app/services/groups/update_shared_runners_service.rb'
+ - 'app/services/issues/relative_position_rebalancing_service.rb'
+ - 'app/services/jira/requests/base.rb'
+ - 'app/services/jira_import/start_import_service.rb'
+ - 'app/services/jira_import/users_importer.rb'
+ - 'app/services/lfs/lock_file_service.rb'
+ - 'app/services/lfs/locks_finder_service.rb'
+ - 'app/services/lfs/push_service.rb'
+ - 'app/services/lfs/unlock_file_service.rb'
+ - 'app/services/merge_requests/merge_to_ref_service.rb'
+ - 'app/services/merge_requests/mergeability_check_service.rb'
+ - 'app/services/metrics/dashboard/base_service.rb'
+ - 'app/services/metrics/dashboard/panel_preview_service.rb'
+ - 'app/services/projects/cleanup_service.rb'
+ - 'app/services/projects/destroy_service.rb'
+ - 'app/services/projects/hashed_storage/base_repository_service.rb'
+ - 'app/services/projects/transfer_service.rb'
+ - 'app/services/prometheus/proxy_service.rb'
+ - 'app/services/resource_access_tokens/revoke_service.rb'
+ - 'app/services/tags/create_service.rb'
+ - 'app/services/tags/destroy_service.rb'
+ - 'app/services/users/validate_manual_otp_service.rb'
+ - 'app/services/users/validate_push_otp_service.rb'
+ - 'app/services/verify_pages_domain_service.rb'
+ - 'app/validators/js_regex_validator.rb'
+ - 'app/workers/concerns/limited_capacity/worker.rb'
+ - 'app/workers/gitlab/jira_import/import_issue_worker.rb'
+ - 'app/workers/issuable_export_csv_worker.rb'
+ - 'app/workers/namespaces/root_statistics_worker.rb'
+ - 'app/workers/namespaces/schedule_aggregation_worker.rb'
+ - 'app/workers/packages/go/sync_packages_worker.rb'
+ - 'app/workers/project_destroy_worker.rb'
+ - 'app/workers/project_service_worker.rb'
+ - 'app/workers/projects/git_garbage_collect_worker.rb'
+ - 'app/workers/remove_expired_members_worker.rb'
+ - 'app/workers/users/create_statistics_worker.rb'
+ - 'config/initializers/rspec_profiling.rb'
+ - 'config/initializers/wikicloth_redos_patch.rb'
+ - 'db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb'
+ - 'db/post_migrate/20211206162601_cleanup_after_add_primary_email_to_emails_if_user_confirmed.rb'
+ - 'db/post_migrate/20220318111729_cleanup_after_fixing_issue_when_admin_changed_primary_email.rb'
+ - 'db/post_migrate/20220504083836_cleanup_after_fixing_regression_with_new_users_emails.rb'
+ - 'ee/app/finders/projects/integrations/jira/by_ids_finder.rb'
+ - 'ee/app/graphql/mutations/issues/promote_to_epic.rb'
+ - 'ee/app/graphql/mutations/issues/set_epic.rb'
+ - 'ee/app/helpers/ee/kerberos_spnego_helper.rb'
+ - 'ee/app/models/concerns/geo/replicable_model.rb'
+ - 'ee/app/models/integrations/github.rb'
+ - 'ee/app/services/app_sec/dast/profiles/create_service.rb'
+ - 'ee/app/services/app_sec/dast/profiles/update_service.rb'
+ - 'ee/app/services/app_sec/dast/scans/create_service.rb'
+ - 'ee/app/services/app_sec/dast/site_validations/find_or_create_service.rb'
+ - 'ee/app/services/app_sec/dast/site_validations/revoke_service.rb'
+ - 'ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb'
+ - 'ee/app/services/arkose/user_verification_service.rb'
+ - 'ee/app/services/ci/sync_reports_to_approval_rules_service.rb'
+ - 'ee/app/services/elastic/process_bookkeeping_service.rb'
+ - 'ee/app/services/geo/file_registry_removal_service.rb'
+ - 'ee/app/services/geo/framework_repository_sync_service.rb'
+ - 'ee/app/services/geo/move_repository_service.rb'
+ - 'ee/app/services/geo/repository_base_sync_service.rb'
+ - 'ee/app/services/incident_management/oncall_rotations/create_service.rb'
+ - 'ee/app/services/incident_management/oncall_rotations/edit_service.rb'
+ - 'ee/app/services/namespaces/deactivate_members_over_limit_service.rb'
+ - 'ee/app/services/namespaces/remove_project_group_links_outside_hierarchy_service.rb'
+ - 'ee/app/services/namespaces/update_prevent_sharing_outside_hierarchy_service.rb'
+ - 'ee/app/services/projects/licenses/create_policy_service.rb'
+ - 'ee/app/services/projects/licenses/update_policy_service.rb'
+ - 'ee/app/services/security/ingestion/ingest_report_service.rb'
+ - 'ee/app/services/security/orchestration/assign_service.rb'
+ - 'ee/app/services/security/store_grouped_scans_service.rb'
+ - 'ee/app/services/security/store_scan_service.rb'
+ - 'ee/app/services/security/token_revocation_service.rb'
+ - 'ee/app/services/software_license_policies/create_service.rb'
+ - 'ee/app/services/software_license_policies/update_service.rb'
+ - 'ee/app/workers/adjourned_project_deletion_worker.rb'
+ - 'ee/app/workers/geo/file_removal_worker.rb'
+ - 'ee/app/workers/geo/repositories_clean_up_worker.rb'
+ - 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
+ - 'ee/app/workers/namespaces/free_user_cap_worker.rb'
+ - 'ee/app/workers/refresh_license_compliance_checks_worker.rb'
+ - 'ee/app/workers/repository_update_mirror_worker.rb'
+ - 'ee/app/workers/sync_seat_link_request_worker.rb'
+ - 'ee/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings.rb'
+ - 'ee/lib/elastic/instance_proxy_util.rb'
+ - 'ee/lib/gitlab/audit/auditor.rb'
+ - 'ee/lib/gitlab/auth/smartcard/base.rb'
+ - 'ee/lib/gitlab/ci/parsers/license_compliance/license_scanning.rb'
+ - 'ee/lib/gitlab/elastic/bulk_indexer.rb'
+ - 'ee/lib/gitlab/spdx/catalogue_gateway.rb'
+ - 'ee/lib/tasks/gitlab/seed/metrics.rake'
+ - 'lib/api/environments.rb'
+ - 'lib/api/helpers.rb'
+ - 'lib/api/helpers/label_helpers.rb'
+ - 'lib/api/issues.rb'
+ - 'lib/api/project_milestones.rb'
+ - 'lib/api/projects.rb'
+ - 'lib/api/repositories.rb'
+ - 'lib/api/v3/github.rb'
+ - 'lib/gitaly/server.rb'
+ - 'lib/gitlab/auth/ldap/adapter.rb'
+ - 'lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb'
+ - 'lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp.rb'
+ - 'lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb'
+ - 'lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb'
+ - 'lib/gitlab/ci/config/external/file/artifact.rb'
+ - 'lib/gitlab/ci/pipeline/chain/config/process.rb'
+ - 'lib/gitlab/ci/pipeline/chain/validate/external.rb'
+ - 'lib/gitlab/ci/reports/codequality_reports.rb'
+ - 'lib/gitlab/database/background_migration/batched_job.rb'
+ - 'lib/gitlab/database/background_migration/batched_migration_wrapper.rb'
+ - 'lib/gitlab/database/batch_counter.rb'
+ - 'lib/gitlab/database/load_balancing/load_balancer.rb'
+ - 'lib/gitlab/database/load_balancing/service_discovery.rb'
+ - 'lib/gitlab/database/reindexing/grafana_notifier.rb'
+ - 'lib/gitlab/git/keep_around.rb'
+ - 'lib/gitlab/gitaly_client/call.rb'
+ - 'lib/gitlab/gitaly_client/commit_service.rb'
+ - 'lib/gitlab/gitaly_client/operation_service.rb'
+ - 'lib/gitlab/gitaly_client/ref_service.rb'
+ - 'lib/gitlab/gitaly_client/repository_service.rb'
+ - 'lib/gitlab/hashed_storage/migrator.rb'
+ - 'lib/gitlab/health_checks/base_abstract_check.rb'
+ - 'lib/gitlab/import_export/merge_request_parser.rb'
+ - 'lib/gitlab/instrumentation/redis_interceptor.rb'
+ - 'lib/gitlab/issuables_count_for_state.rb'
+ - 'lib/gitlab/jira_import/issues_importer.rb'
+ - 'lib/gitlab/json.rb'
+ - 'lib/gitlab/jwt_token.rb'
+ - 'lib/gitlab/kubernetes/namespace.rb'
+ - 'lib/gitlab/metrics/dashboard/stages/panel_ids_inserter.rb'
+ - 'lib/gitlab/metrics/rack_middleware.rb'
+ - 'lib/gitlab/middleware/handle_ip_spoof_attack_error.rb'
+ - 'lib/gitlab/prometheus/queries/validate_query.rb'
+ - 'lib/gitlab/prometheus_client.rb'
+ - 'lib/gitlab/sanitizers/exif.rb'
+ - 'lib/gitlab/sidekiq_logging/structured_logger.rb'
+ - 'lib/gitlab/tcp_checker.rb'
+ - 'lib/gitlab/template_parser/parser.rb'
+ - 'lib/gitlab/tracking.rb'
+ - 'lib/gitlab/url_blocker.rb'
+ - 'lib/gitlab/usage/metrics/aggregates/aggregate.rb'
+ - 'lib/gitlab/usage_data.rb'
+ - 'lib/gitlab/utils/usage_data.rb'
+ - 'lib/gitlab/verify/batch_verifier.rb'
+ - 'lib/gitlab/wiki_pages/front_matter_parser.rb'
+ - 'lib/microsoft_teams/notifier.rb'
+ - 'lib/system_check/incoming_email/imap_authentication_check.rb'
+ - 'lib/tasks/gitlab/db/validate_config.rake'
+ - 'lib/tasks/gitlab/setup.rake'
+ - 'lib/tasks/gitlab/storage.rake'
+ - 'lib/tasks/lint.rake'
+ - 'qa/qa/resource/user_gpg.rb'
+ - 'scripts/review_apps/automated_cleanup.rb'
+ - 'scripts/trigger-build.rb'
+ - 'spec/commands/metrics_server/metrics_server_spec.rb'
+ - 'spec/db/docs_spec.rb'
+ - 'spec/lib/bulk_imports/network_error_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/host_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
+ - 'spec/lib/gitlab/error_tracking_spec.rb'
+ - 'spec/lib/gitlab/sanitizers/exception_message_spec.rb'
+ - 'spec/support/capybara.rb'
+ - 'spec/support/helpers/capybara_helpers.rb'
+ - 'tooling/lib/tooling/helm3_client.rb'
+ - 'tooling/lib/tooling/kubernetes_client.rb'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1818bca1e46..ac3fbaddf8f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,9 +1,12 @@
-## Contributor license agreement
+## Contributor License Agreement and Developer Certificate of Origin
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+Contributions to this repository are subject to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11), or the [Individual](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html) or [Corporate](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html) Contributor License Agreement, depending on where the contribution is made and on whose behalf:
+
+- By submitting code contributions as an individual to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Individual Contributor License Agreement](https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html).
+
+- By submitting code contributions on behalf of a corporation to the [`/ee` subdirectory](/ee) of this repository, you agree to the [Corporate Contributor License Agreement](https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html).
+
+- By submitting code contributions as an individual or on behalf of a corporation to any directory in this repository outside of the [`/ee` subdirectory](/ee), you agree to the [Developer Certificate of Origin](https://docs.gitlab.com/ee/legal/developer_certificate_of_origin.html#developer-certificate-of-origin-version-11).
All Documentation content that resides under the [`doc/` directory](/doc) of this
repository is licensed under Creative Commons:
diff --git a/app/assets/javascripts/crm/contacts/components/contacts_root.vue b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
index 17be3800256..9d6f34c73b7 100644
--- a/app/assets/javascripts/crm/contacts/components/contacts_root.vue
+++ b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
@@ -55,7 +55,7 @@ export default {
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName));
},
getIssuesPath(path, value) {
- return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
+ return `${path}?crm_contact_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
diff --git a/app/assets/javascripts/crm/organizations/components/organizations_root.vue b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
index 522e29eb2af..a165dd68603 100644
--- a/app/assets/javascripts/crm/organizations/components/organizations_root.vue
+++ b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
@@ -55,7 +55,7 @@ export default {
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
},
getIssuesPath(path, value) {
- return `${path}?scope=all&state=opened&crm_organization_id=${value}`;
+ return `${path}?crm_organization_id=${value}`;
},
getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
index 8aaf42ce3da..cd4f98ba505 100644
--- a/app/assets/javascripts/issuable/components/status_box.vue
+++ b/app/assets/javascripts/issuable/components/status_box.vue
@@ -1,37 +1,48 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
+import { IssuableType } from '~/issues/constants';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
-export const statusBoxState = Vue.observable({
+export const badgeState = Vue.observable({
state: '',
updateStatus: null,
});
const CLASSES = {
- opened: 'status-box-open',
- merge_request_opened: 'badge-success',
- locked: 'status-box-open',
- merge_request_locked: 'badge-success',
- closed: 'status-box-mr-closed',
- merge_request_closed: 'badge-danger',
- merged: 'badge-info',
+ opened: 'issuable-status-badge-open',
+ locked: 'issuable-status-badge-open',
+ closed: 'issuable-status-badge-closed',
+ merged: 'issuable-status-badge-merged',
+};
+
+const ISSUE_ICONS = {
+ opened: 'issues',
+ locked: 'issues',
+ closed: 'issue-closed',
+};
+
+const MERGE_REQUEST_ICONS = {
+ opened: 'merge-request-open',
+ locked: 'merge-request-open',
+ closed: 'merge-request-close',
+ merged: 'merge',
};
const STATUS = {
- opened: [__('Open'), 'issue-open-m'],
- locked: [__('Open'), 'issue-open-m'],
- closed: [__('Closed'), 'issue-close'],
- merged: [__('Merged'), 'git-merge'],
+ opened: __('Open'),
+ locked: __('Open'),
+ closed: __('Closed'),
+ merged: __('Merged'),
};
export default {
components: {
+ GlBadge,
GlIcon,
},
- mixins: [glFeatureFlagMixin()],
inject: {
query: { default: null },
projectPath: { default: null },
@@ -51,39 +62,41 @@ export default {
},
data() {
if (this.initialState) {
- statusBoxState.state = this.initialState;
+ badgeState.state = this.initialState;
}
- return statusBoxState;
+ return badgeState;
},
computed: {
- isMergeRequest() {
- return this.issuableType === 'merge_request' && this.glFeatures.updatedMrHeader;
+ badgeClass() {
+ return CLASSES[this.state];
},
- statusBoxClass() {
- return [
- CLASSES[`${this.issuableType}_${this.state}`] || CLASSES[this.state],
- {
- 'badge badge-pill gl-badge gl-mr-3': this.isMergeRequest,
- 'issuable-status-box status-box': !this.isMergeRequest,
- },
- ];
+ badgeVariant() {
+ if (this.state === IssuableStates.Opened) {
+ return 'success';
+ } else if (this.state === IssuableStates.Closed) {
+ return this.issuableType === IssuableType.MergeRequest ? 'danger' : 'info';
+ }
+ return 'info';
},
- statusHumanName() {
- return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[0];
+ badgeText() {
+ return STATUS[this.state];
},
- statusIconName() {
- return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[1];
+ badgeIcon() {
+ if (this.issuableType === IssuableType.Issue) {
+ return ISSUE_ICONS[this.state];
+ }
+ return MERGE_REQUEST_ICONS[this.state];
},
},
created() {
- if (!statusBoxState.updateStatus) {
- statusBoxState.updateStatus = this.fetchState;
+ if (!badgeState.updateStatus) {
+ badgeState.updateStatus = this.fetchState;
}
},
beforeDestroy() {
- if (statusBoxState.updateStatus && this.query) {
- statusBoxState.updateStatus = null;
+ if (badgeState.updateStatus && this.query) {
+ badgeState.updateStatus = null;
}
},
methods: {
@@ -97,21 +110,15 @@ export default {
fetchPolicy: fetchPolicies.NO_CACHE,
});
- statusBoxState.state = data?.workspace?.issuable?.state;
+ badgeState.state = data?.workspace?.issuable?.state;
},
},
};
</script>
<template>
- <div :class="statusBoxClass">
- <gl-icon
- v-if="!isMergeRequest"
- :name="statusIconName"
- class="gl-display-block gl-sm-display-none!"
- />
- <span :class="{ 'gl-display-none gl-sm-display-block': !isMergeRequest }">
- {{ statusHumanName }}
- </span>
- </div>
+ <gl-badge class="issuable-status-badge gl-mr-3" :class="badgeClass" :variant="badgeVariant">
+ <gl-icon :name="badgeIcon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index 8e27f547b5c..a9321cf200d 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -49,8 +49,8 @@ export default class Issue {
issueFailMessage = __('Unable to update this issue at this time.'),
) {
if ('id' in data) {
- const isClosedBadge = $('div.status-box-issue-closed');
- const isOpenBadge = $('div.status-box-open');
+ const isClosedBadge = $('.issuable-status-badge-closed');
+ const isOpenBadge = $('.issuable-status-badge-open');
const projectIssuesCounter = $('.issue_counter');
isClosedBadge.toggleClass('hidden', !isClosed);
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 8f8e38c5b9f..65d86d2936c 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import {
@@ -27,6 +27,7 @@ export default {
WorkspaceType,
components: {
GlIcon,
+ GlBadge,
GlIntersectionObserver,
titleComponent,
editedComponent,
@@ -267,7 +268,10 @@ export default {
: '';
},
statusIcon() {
- return this.isClosed ? 'issue-close' : 'issue-open-m';
+ return this.isClosed ? 'issue-closed' : 'issues';
+ },
+ statusVariant() {
+ return this.isClosed ? 'info' : 'success';
},
statusText() {
return IssuableStatusText[this.issuableStatus];
@@ -517,13 +521,9 @@ export default {
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
- <p
- class="issuable-status-box status-box gl-white-space-nowrap gl-my-0"
- :class="[isClosed ? 'status-box-issue-closed' : 'status-box-open']"
+ <gl-badge :icon="statusIcon" :variant="statusVariant" class="gl-mr-2"
+ ><span class="gl-display-none gl-sm-display-block">{{ statusText }}</span></gl-badge
>
- <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
- <span class="gl-display-none d-sm-block">{{ statusText }}</span>
- </p>
<span v-if="isLocked" data-testid="locked" class="issuable-warning-icon">
<gl-icon name="lock" :aria-label="__('Locked')" />
</span>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 3432fe12705..4a15b810d38 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -6,7 +6,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import createFlash from '~/flash';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
@@ -266,7 +266,7 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
- .then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
+ .then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
createFlash({
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 9d184b14878..1fe82d96435 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,6 +1,6 @@
import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { badgeState } from '~/issuable/components/status_box.vue';
import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -85,8 +85,7 @@ export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issue
export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note);
-export const openState = (state) =>
- isInMRPage() ? statusBoxState.state : state.noteableData.state;
+export const openState = (state) => (isInMRPage() ? badgeState.state : state.noteableData.state);
export const getUserData = (state) => state.userData || {};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 1b5d00662dc..48e360ce762 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -20,23 +20,25 @@ export default function initMergeRequestShow() {
initAwardsApp(document.getElementById('js-vue-awards-block'));
const el = document.querySelector('.js-mr-status-box');
+ const { iid, issuableType, projectPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
el,
+ name: 'IssuableStatusBoxRoot',
apolloProvider,
provide: {
query: getStateQuery,
- projectPath: el.dataset.projectPath,
- iid: el.dataset.iid,
+ iid,
+ projectPath,
},
render(h) {
return h(StatusBox, {
props: {
initialState: el.dataset.state,
- issuableType: 'merge_request',
+ issuableType,
},
});
},
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index c94fe19039d..67f36f65b5d 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -2,7 +2,7 @@
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import createFlash from '~/flash';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
@@ -21,6 +21,10 @@ export default {
type: String,
required: true,
},
+ groupIssuesPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -85,6 +89,10 @@ export default {
Boolean,
);
},
+ getIssuesPath(contactId) {
+ const id = getIdFromGraphQLId(contactId);
+ return `${this.groupIssuesPath}?crm_contact_id=${id}`;
+ },
},
};
</script>
@@ -110,8 +118,8 @@ export default {
:key="index"
class="gl-pr-2"
>
- <span :id="`contact_${index}`" class="gl-font-weight-bold"
- >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</span
+ <gl-link :id="`contact_${index}`" :href="getIssuesPath(contact.id)"
+ >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</gl-link
>
<gl-popover
v-if="shouldShowPopover(contact)"
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2a7d967cb61..d76782b0cec 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -218,7 +218,7 @@ function mountCrmContactsComponent() {
if (!el) return;
- const { issueId } = el.dataset;
+ const { issueId, groupIssuesPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
@@ -231,6 +231,7 @@ function mountCrmContactsComponent() {
createElement('crm-contacts', {
props: {
issueId,
+ groupIssuesPath,
},
}),
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index eb07609d5d6..146cf7e11a7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,5 +1,5 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { badgeState } from '~/issuable/components/status_box.vue';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
import {
@@ -221,8 +221,8 @@ export default class MergeRequestStore {
}
updateStatusState(state) {
- if (this.mergeRequestState !== state && statusBoxState.updateStatus) {
- statusBoxState.updateStatus();
+ if (this.mergeRequestState !== state && badgeState.updateStatus) {
+ badgeState.updateStatus();
}
}
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index 479ada6c7c6..649dbd6576b 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -1,14 +1,23 @@
<script>
-import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlBadge,
+ GlButton,
+ GlTooltipDirective,
+ GlAvatarLink,
+ GlAvatarLabeled,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isExternal } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
export default {
components: {
GlIcon,
+ GlBadge,
GlButton,
GlAvatarLink,
GlAvatarLabeled,
@@ -26,6 +35,11 @@ export default {
type: Object,
required: true,
},
+ issuableState: {
+ type: String,
+ required: false,
+ default: '',
+ },
statusBadgeClass: {
type: String,
required: false,
@@ -36,6 +50,11 @@ export default {
required: false,
default: '',
},
+ statusIconClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
blocked: {
type: Boolean,
required: false,
@@ -53,6 +72,9 @@ export default {
},
},
computed: {
+ badgeVariant() {
+ return this.issuableState === IssuableStates.Opened ? 'success' : 'info';
+ },
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
@@ -91,10 +113,15 @@ export default {
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
- <div data-testid="status" class="issuable-status-box status-box" :class="statusBadgeClass">
- <gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" />
- <span class="d-none d-sm-block"><slot name="status-badge"></slot></span>
- </div>
+ <gl-badge
+ data-testid="status"
+ class="issuable-status-badge gl-mr-3"
+ :class="statusBadgeClass"
+ :variant="badgeVariant"
+ >
+ <gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
+ <span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span>
+ </gl-badge>
<div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block">
<div v-if="blocked || confidential" class="gl-display-inline-block">
<div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline">
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 8849af2a52e..c165ee91c59 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
+ statusIconClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
enableEdit: {
type: Boolean,
required: false,
@@ -102,8 +107,10 @@ export default {
<template>
<div class="issuable-show-container" data-qa-selector="issuable_show_container">
<issuable-header
+ :issuable-state="issuable.state"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
+ :status-icon-class="statusIconClass"
:blocked="issuable.blocked"
:confidential="issuable.confidential"
:created-at="issuable.createdAt"
@@ -122,6 +129,7 @@ export default {
:issuable="issuable"
:status-badge-class="statusBadgeClass"
:status-icon="statusIcon"
+ :status-icon-class="statusIconClass"
:enable-edit="enableEdit"
:enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave"
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index bc9975ea17a..e0319952adb 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -26,6 +26,7 @@
.detail-page-header-body {
position: relative;
display: flex;
+ align-items: center;
flex: 1 1;
min-width: 0;
diff --git a/app/helpers/badges_helper.rb b/app/helpers/badges_helper.rb
index a03f7f4097a..26ebe8a6470 100644
--- a/app/helpers/badges_helper.rb
+++ b/app/helpers/badges_helper.rb
@@ -69,6 +69,7 @@ module BadgesHelper
icon_only = options[:icon_only]
variant_class = VARIANT_CLASSES[options.fetch(:variant, :muted)]
size_class = SIZE_CLASSES[options.fetch(:size, :md)]
+ icon_classes = GL_ICON_CLASSES.dup << options.fetch(:icon_classes, nil)
html_options = html_options.merge(
class: [
@@ -85,7 +86,6 @@ module BadgesHelper
end
if options[:icon]
- icon_classes = GL_ICON_CLASSES.dup
icon_classes << "gl-mr-2" unless icon_only
icon = sprite_icon(options[:icon], css_class: icon_classes.join(' '))
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 98eca3785e7..6ecc41151af 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -341,14 +341,20 @@ module IssuablesHelper
end
def state_name_with_icon(issuable)
- if issuable.is_a?(MergeRequest) && issuable.merged?
- [_("Merged"), "git-merge"]
- elsif issuable.is_a?(MergeRequest) && issuable.closed?
- [_("Closed"), "close"]
- elsif issuable.closed?
- [_("Closed"), "mobile-issue-close"]
+ if issuable.is_a?(MergeRequest)
+ if issuable.open?
+ [_("Open"), "merge-request-open"]
+ elsif issuable.merged?
+ [_("Merged"), "merge"]
+ else
+ [_("Closed"), "merge-request-close"]
+ end
else
- [_("Open"), "issue-open-m"]
+ if issuable.open?
+ [_("Open"), "issues"]
+ else
+ [_("Closed"), "issue-closed"]
+ end
end
end
diff --git a/app/helpers/routing/projects_helper.rb b/app/helpers/routing/projects_helper.rb
index 859070d59ec..8c0bd9b1ecc 100644
--- a/app/helpers/routing/projects_helper.rb
+++ b/app/helpers/routing/projects_helper.rb
@@ -35,7 +35,15 @@ module Routing
end
def issue_url(entity, *args)
- project_issue_url(entity.project, entity, *args)
+ if use_work_items_path?(entity)
+ work_item_url(entity, *args)
+ else
+ project_issue_url(entity.project, entity, *args)
+ end
+ end
+
+ def work_item_url(entity, *args)
+ project_work_items_url(entity.project, entity.id, *args)
end
def merge_request_url(entity, *args)
@@ -77,5 +85,11 @@ module Routing
toggle_subscription_project_merge_request_path(entity.project, entity)
end
end
+
+ private
+
+ def use_work_items_path?(issue)
+ issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
+ end
end
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 75f6d749acb..9b4e7e22165 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -4,9 +4,7 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents ::Issue, as: :issue
def issue_path
- return url_builder.build(issue, only_path: true) unless use_work_items_path?
-
- project_work_items_path(issue.project, work_items_path: issue.id)
+ web_path
end
delegator_override :subscribed?
@@ -17,18 +15,6 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
def project_emails_disabled?
issue.project.emails_disabled?
end
-
- def web_url
- return super unless use_work_items_path?
-
- project_work_items_url(issue.project, work_items_path: issue.id)
- end
-
- private
-
- def use_work_items_path?
- issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
- end
end
IssuePresenter.prepend_mod_with('IssuePresenter')
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index 4b9c48f3f7c..bcad28d6aad 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity
end
expose :real_path, if: -> (issue) { issue.project } do |issue|
- project_issue_path(issue.project, issue)
+ Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index fbcfcf84d9b..852a2e62b7d 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -31,7 +31,7 @@ class IssueEntity < IssuableEntity
end
expose :web_url do |issue|
- project_issue_path(issue.project, issue)
+ Gitlab::UrlBuilder.build(issue, only_path: true)
end
expose :current_user do
diff --git a/app/serializers/linked_issue_entity.rb b/app/serializers/linked_issue_entity.rb
index 6ae3f4044db..769e3ed7310 100644
--- a/app/serializers/linked_issue_entity.rb
+++ b/app/serializers/linked_issue_entity.rb
@@ -18,7 +18,7 @@ class LinkedIssueEntity < Grape::Entity
end
expose :path do |link|
- project_issue_path(link.project, link.iid)
+ Gitlab::UrlBuilder.build(link, only_path: true)
end
expose :relation_path
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 321658ac9c5..81986a2883f 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -82,7 +82,13 @@ module Members
if member.request?
approve_request
else
- member.save
+ # Calling #save triggers callbacks even if there is no change on object.
+ # This previously caused an incident due to the hard to predict
+ # behaviour caused by the large number of callbacks.
+ # See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
+ # and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
+ # for details.
+ member.save if member.changed?
end
end
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index f4c7be14759..1ccb8dff7e7 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,9 +1,8 @@
.detail-page-description.py-2
- if Feature.enabled?(:updated_mr_header, @project)
- - state_human_name, _ = state_name_with_icon(@merge_request)
- .badge.badge-pill.gl-badge.gl-mr-3.js-mr-status-box{ class: status_box_class(@merge_request), data: { project_path: @merge_request.project.path_with_namespace, iid: @merge_request.iid, state: @merge_request.state } }>
- = state_human_name
- = merge_request_header(@project, @merge_request)
+ = render 'shared/issuable/status_box', issuable: @merge_request
+ .gl-display-inline.gl-vertical-align-top
+ = merge_request_header(@project, @merge_request)
- else
%h2.title.mb-0{ data: { qa_selector: 'title_content' } }
= markdown_field(@merge_request, :title)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 25d69a8fc1b..0af9b1cf2e3 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -45,7 +45,7 @@
- if issuable_sidebar[:show_crm_contacts]
.block.contact
- #js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id] } }
+ #js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id], group_issues_path: issues_group_path(@project.group) } }
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
index c0e972684d2..fd7e720cf1e 100644
--- a/app/views/shared/issuable/_status_box.html.haml
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -1,6 +1,9 @@
-- state_human_name, state_icon_name = state_name_with_icon(issuable)
+- badge_text = state_name_with_icon(issuable)[0]
+- badge_icon = state_name_with_icon(issuable)[1]
+- badge_variant = issuable.open? ? :success : issuable.merged? ? :info : :danger
+- badge_status_class = issuable.open? ? 'issuable-status-badge-open' : issuable.merged? ? 'issuable-status-badge-merged' : 'issuable-status-badge-closed'
+- badge_classes = "js-mr-status-box issuable-status-badge gl-mr-3 #{badge_status_class}"
-.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(issuable), data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, state: issuable.state } }
- = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
- = state_human_name
+= gl_badge_tag({ variant: badge_variant, icon: badge_icon, icon_classes: 'gl-mr-0!' }, { class: badge_classes, data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, issuable_type: 'merge_request', state: issuable.state } }) do
+ %span.gl-display-none.gl-sm-display-block.gl-ml-2
+ = badge_text
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index eca61819cca..71f49649d33 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -1,17 +1,16 @@
- link = issue_closed_link(@issue, current_user, css_class: 'text-white text-underline')
+- badge_classes = 'issuable-status-badge gl-mr-3'
.detail-page-header
.detail-page-header-body
- .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
- = sprite_icon('issue-close', css_class: 'gl-display-block gl-sm-display-none!')
- .gl-display-none.gl-sm-display-block
+ = gl_badge_tag({ variant: :info, icon: 'issue-closed', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :closed)} #{badge_classes} issuable-status-badge-closed" }) do
+ .gl-display-none.gl-sm-display-block.gl-ml-2
= issue_closed_text(issuable, current_user)
- - if link
- %span.text-white.gl-pl-2.gl-sm-display-none
- = "(#{link})"
- .issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
- = sprite_icon('issue-open-m', css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
+ - if link
+ %span.text-white.gl-pl-2.gl-sm-display-none
+ = "(#{link})"
+ = gl_badge_tag({ variant: :success, icon: 'issues', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :open)} #{badge_classes} issuable-status-badge-open" }) do
+ %span.gl-display-none.gl-sm-display-block.gl-ml-2
= _('Open')
.issuable-meta
diff --git a/config/feature_flags/development/contacts_autocomplete.yml b/config/feature_flags/development/contacts_autocomplete.yml
index 9d6960f6713..2b0908e7343 100644
--- a/config/feature_flags/development/contacts_autocomplete.yml
+++ b/config/feature_flags/development/contacts_autocomplete.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352123
milestone: '14.8'
type: development
group: group::product planning
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/customer_relations.yml b/config/feature_flags/development/customer_relations.yml
index 58783708921..d68e299758a 100644
--- a/config/feature_flags/development/customer_relations.yml
+++ b/config/feature_flags/development/customer_relations.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346082
milestone: '14.3'
type: development
group: group::product planning
-default_enabled: false
+default_enabled: true
diff --git a/doc/ci/pipelines/cicd_minutes.md b/doc/ci/pipelines/cicd_minutes.md
index 55dc03fd0fa..1051b176f06 100644
--- a/doc/ci/pipelines/cicd_minutes.md
+++ b/doc/ci/pipelines/cicd_minutes.md
@@ -200,10 +200,31 @@ Conversely, a shared runner that executes jobs for public projects could have a
### Monthly reset of CI/CD minutes
On the first day of each calendar month, the accumulated usage of CI/CD minutes is reset to `0`
-for all namespaces that use shared runners.
+for all namespaces that use shared runners. This means your full quota is available, and
+calculations start again from `0`.
+
+For example, if you have a monthly quota of `10,000` CI/CD minutes:
+
+- On **1st April**, you have `10,000` minutes.
+- During April, you use only `6,000` of the `10,000` minutes.
+- On **1st May**, the accumulated usage of minutes resets to `0`, and you have `10,000` minutes to use again
+ during May.
Usage data for the previous month is kept to show historical view of the consumption over time.
+### Monthly rollover of purchased CI/CD minutes
+
+If you purchase additional CI/CD minutes and don't use the full amount, the remaining amount rolls over to
+the next month.
+
+For example:
+
+- On **1st April**, you purchase `5,000` CI/CD minutes.
+- During April, you use only `3,000` of the `5,000` minutes.
+- On **1st May**, the remaining `2,000` minutes roll over and are added to your monthly quota.
+
+Additional CI/CD minutes are a one-time purchase and do not renew or refresh each month.
+
## What happens when you exceed the quota
When the quota of CI/CD minutes is used for the current month, GitLab stops
diff --git a/doc/install/index.md b/doc/install/index.md
index deb94031e44..52bc9062adc 100644
--- a/doc/install/index.md
+++ b/doc/install/index.md
@@ -28,10 +28,10 @@ install GitLab:
| Installation method | Description | When to choose |
|----------------------------------------------------------------|-------------|----------------|
| [Linux package](https://docs.gitlab.com/omnibus/installation/) | The official deb/rpm packages (also known as Omnibus GitLab) that contains a bundle of GitLab and the components it depends on, including PostgreSQL, Redis, and Sidekiq. | This method is recommended for getting started. The Linux packages are mature, scalable, and are used today on GitLab.com. If you need additional flexibility and resilience, we recommend deploying GitLab as described in the [reference architecture documentation](../administration/reference_architectures/index.md). |
-| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
+| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, it has some trade-offs that you must be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
| [Docker](docker.md) | The GitLab packages, Dockerized. | Use this method if you're familiar with Docker. |
-| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Useful for unsupported systems like \*BSD.|
-| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#documentation) | The GitLab Environment Toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup will be required based on your specific requirements. |
+| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Can be used for unsupported systems like \*BSD.|
+| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup is required based on your specific requirements. |
| [GitLab Operator](https://docs.gitlab.com/operator/) | The GitLab Operator provides an installation and management method for GitLab following the [Kubernetes Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). | Use the GitLab Operator to run GitLab in an [OpenShift](openshift_and_gitlab/index.md) environment. |
## Install GitLab on cloud providers
diff --git a/doc/user/application_security/dast/checks/16.6.md b/doc/user/application_security/dast/checks/16.6.md
index ddd3a10c5f8..9cbcde669a0 100644
--- a/doc/user/application_security/dast/checks/16.6.md
+++ b/doc/user/application_security/dast/checks/16.6.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Description
-The target website returns AspNet header(s) along with version information of this website. By
+The target website returns AspNet headers along with version information of this website. By
exposing these values attackers may attempt to identify if the target software is vulnerable to known
vulnerabilities. Or catalog known sites running particular versions to exploit in the future when a
vulnerability is identified in the particular version.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 5baa385e0bd..1579e400ef9 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -1220,3 +1220,21 @@ gemnasium-python-dependency_scanning:
before_script:
- pip install setuptools==57.5.0
```
+
+### Dependency Scanning of projects using psycopg2 fails with `pg_config executable not found` error
+
+Scanning a Python project that depends on `psycopg2` can fail with this message:
+
+```plaintext
+Error: pg_config executable not found.
+```
+
+[psycopg2](https://pypi.org/project/psycopg2/) depends on the `libpq-dev` Debian package,
+which is not installed in the `gemnasium-python` Docker image. To work around this error,
+install the `libpq-dev` package in a `before_script`:
+
+```yaml
+gemnasium-python-dependency_scanning:
+ before_script:
+ - apt-get update && apt-get install -y libpq-dev
+```
diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md
index 7fc11add2cd..7a400205e30 100644
--- a/doc/user/crm/index.md
+++ b/doc/user/crm/index.md
@@ -6,12 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Customer relations management (CRM) **(FREE)**
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
-On GitLab.com, this feature is not available.
-
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
> - In GitLab 14.8 and later, you can [create contacts and organizations only in root groups](https://gitlab.com/gitlab-org/gitlab/-/issues/350634).
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/346082) in GitLab 15.0.
+
+FLAG:
+On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
+On GitLab.com, this feature is available.
With customer relations management (CRM) you can create a record of contacts
(individuals) and organizations (companies) and relate them to issues.
@@ -118,7 +119,7 @@ organizations using the GraphQL API.
### View issues linked to a contact
-To view a contact's issues:
+To view a contact's issues, select a contact from the issue sidebar, or:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Customer relations > Contacts**.
@@ -166,11 +167,12 @@ API.
## Autocomplete contacts **(FREE SELF)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.0.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`.
-On GitLab.com, this feature is not available.
+On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`.
+On GitLab.com, this feature is available.
This feature is not ready for production use.
When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index ddd8a2acd09..71431086a92 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -446,7 +446,7 @@ The following table lists group permissions available for each role:
### Subgroup permissions
When you add a member to a subgroup, they inherit the membership and
-permission level from the parent group(s). This model allows access to
+permission level from the parent groups. This model allows access to
nested groups if you have membership in one of its parents.
To learn more, read through the documentation on
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index 82106c9d1a9..935bc01bae7 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -226,7 +226,7 @@ on the running pod.
If you are using a self-managed GitLab instance, you need to configure
Amazon credentials. GitLab uses these credentials to assume an Amazon IAM role to create your cluster.
-Create an IAM user and ensure it has permissions to assume the role(s) that
+Create an IAM user and ensure it has permissions to assume the roles that
your users need to create EKS clusters.
For example, the following policy document allows assuming a role whose name starts with
diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md
index 8bb716d8122..17d717bdc61 100644
--- a/doc/user/project/import/cvs.md
+++ b/doc/user/project/import/cvs.md
@@ -24,8 +24,8 @@ The following list illustrates the main differences between CVS and Git:
whole, or they fail without any changes. In CVS, commits (and other operations)
are not atomic. If an operation on the repository is interrupted in the middle,
the repository can be left in an inconsistent state.
-- **Storage method.** Changes in CVS are per file (changeset), while in Git
- a committed file(s) is stored in its entirety (snapshot). That means it's
+- **Storage method.** Changes in CVS are per file (changeset), while in Git,
+ committed files are stored in their entirety (snapshot). This means it is
very easy in Git to revert or undo a whole change.
- **Revision IDs.** The fact that in CVS changes are per files, the revision ID
is depicted by version numbers, for example `1.4` reflects how many times a
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index dd7b628e51e..8ede06090ec 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -541,7 +541,7 @@ This can be due to multiple reasons:
- If no [degradation or error is detected](https://docs.codeclimate.com/docs/maintainability#section-checks),
nothing is displayed.
- The [`artifacts:expire_in`](../../../ci/yaml/index.md#artifactsexpire_in) CI/CD
- setting can cause the Code Quality artifact(s) to expire faster than desired.
+ setting can cause the Code Quality artifacts to expire faster than desired.
- The widgets use the pipeline of the latest commit to the target branch. If commits are made to the default branch that do not run the code quality job, this may cause the merge request widget to have no base report for comparison.
- If you use the [`REPORT_STDOUT` environment variable](https://gitlab.com/gitlab-org/ci-cd/codequality#environment-variables), no report file is generated and nothing displays in the merge request.
- Large `gl-code-quality-report.json` files (esp. >10 MB) are [known to prevent the report from being displayed](https://gitlab.com/gitlab-org/gitlab/-/issues/2737).
diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md
index f6f648a8bea..eb5a54e6119 100644
--- a/doc/user/project/merge_requests/reviews/index.md
+++ b/doc/user/project/merge_requests/reviews/index.md
@@ -203,7 +203,7 @@ These features are associated with merge requests:
- [Fast-forward merge requests](../methods/index.md#fast-forward-merge):
For a linear Git history and a way to accept merge requests without creating merge commits
- [Find the merge request that introduced a change](../versions.md):
- When viewing the commit details page, GitLab links to the merge request(s) containing that commit.
+ When viewing the commit details page, GitLab links to the merge requests containing that commit.
- [Merge requests versions](../versions.md):
Select and compare the different versions of merge request diffs
- [Resolve conflicts](../conflicts.md):
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
index 4c23e5d33d5..2d753fa042a 100644
--- a/doc/user/shortcuts.md
+++ b/doc/user/shortcuts.md
@@ -148,7 +148,7 @@ These shortcuts are available when editing a file with the [Web IDE](project/web
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd>↓</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>↓</kbd> | Copy line down |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd>↑</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>↑</kbd> | Copy line up [(Linux note)](#linux-shortcuts) |
| <kbd>Command</kbd> + <kbd>U</kbd> | <kbd>Control</kbd> + <kbd>U</kbd> | Cursor undo |
-| <kbd>Command</kbd> + <kbd>Backspace<kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Backspace</kbd> | Delete all left |
+| <kbd>Command</kbd> + <kbd>Backspace</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Backspace</kbd> | Delete all left |
| <kbd>Control</kbd> + <kbd>K</kbd> | | Delete all right |
| <kbd>Shift</kbd> + <kbd>Command</kbd> + <kbd>K</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> | Delete line |
| | <kbd>Control</kbd> + <kbd>Backspace</kbd> | Delete word |
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index c00a0e1bcb4..a6d6cffec17 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -28,6 +28,8 @@ module Gitlab
compare_url(object, **options)
when Group
instance.group_canonical_url(object, **options)
+ when WorkItem
+ instance.work_item_url(object, **options)
when Issue
instance.issue_url(object, **options)
when MergeRequest
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 308b38e8c82..1e4d43ad7e6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5995,6 +5995,9 @@ msgstr ""
msgid "Billing|Free groups on GitLab are limited to %{maxNamespaceSeats} seats"
msgstr ""
+msgid "Billing|From June 22, 2022 (GitLab 15.1), free groups will be limited to 5 members"
+msgstr ""
+
msgid "Billing|Group invite"
msgstr ""
@@ -6031,6 +6034,9 @@ msgstr ""
msgid "Billing|You are about to remove user %{username} from your subscription. If you continue, the user will be removed from the %{namespace} group and all its subgroups and projects. This action can't be undone."
msgstr ""
+msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after June 22, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group."
+msgstr ""
+
msgid "Bitbucket Server Import"
msgstr ""
diff --git a/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb b/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb
index 303a64ad233..31543d30e88 100644
--- a/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb
+++ b/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb
@@ -4,7 +4,6 @@ plugins {
}
repositories {
- jcenter()
maven {
url "<%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven"
name "GitLab"
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
index 812a7bae6c3..0e4aa67162f 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
@@ -1,32 +1,27 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Create', :requires_admin do
+ # This test modifies an instance level setting,
+ # so skipping on live envs to avoid random transient issues
+ RSpec.describe 'Create', :requires_admin, :skip_live_env do
describe 'push after setting the file size limit via admin/application_settings' do
- # Note: The file size limits in this test should be greater than the limits in
- # ee/browser_ui/3_create/repository/push_rules_spec to prevent that test from
- # triggering the limit set in this test (which can happen on Staging where the
- # tests are run in parallel).
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/218620#note_361634705
-
include Support::API
- before(:context) do
- @project = Resource::Project.fabricate_via_api! do |p|
+ let!(:project) do
+ Resource::Project.fabricate_via_api! do |p|
p.name = 'project-test-push-limit'
p.initialize_with_readme = true
end
-
- @api_client = Runtime::API::Client.as_admin
end
after(:context) do
- # need to set the default value after test
- # default value for file size limit is empty
set_file_size_limit(nil)
end
- it 'push successful when the file size is under the limit', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347758' do
+ it(
+ 'push successful when the file size is under the limit',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347758'
+ ) do
set_file_size_limit(5)
retry_on_fail do
@@ -36,7 +31,10 @@ module QA
end
end
- it 'push fails when the file size is above the limit', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347759' do
+ it(
+ 'push fails when the file size is above the limit',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347759'
+ ) do
set_file_size_limit(2)
retry_on_fail do
@@ -46,7 +44,7 @@ module QA
end
def set_file_size_limit(limit)
- request = Runtime::API::Request.new(@api_client, '/application/settings')
+ request = Runtime::API::Request.new(Runtime::API::Client.as_admin, '/application/settings')
response = put request.url, receive_max_input_size: limit
expect(response.code).to eq(200)
@@ -56,13 +54,13 @@ module QA
def push_new_file(file_name, wait_for_push: true)
commit_message = 'Adding a new file'
output = Resource::Repository::Push.fabricate! do |p|
- p.repository_http_uri = @project.repository_http_location.uri
+ p.repository_http_uri = project.repository_http_location.uri
p.file_name = file_name
p.file_content = SecureRandom.random_bytes(3000000)
p.commit_message = commit_message
p.new_branch = false
end
- @project.wait_for_push commit_message
+ project.wait_for_push commit_message
output
end
@@ -77,10 +75,8 @@ module QA
# under a minute, i.e., in fewer than 6 attempts with a 10 second sleep
# between attempts.
# See https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30233#note_188616863
- def retry_on_fail
- Support::Retrier.retry_on_exception(max_attempts: 6, reload_page: nil, sleep_interval: 10) do
- yield
- end
+ def retry_on_fail(&block)
+ Support::Retrier.retry_on_exception(max_attempts: 6, reload_page: false, sleep_interval: 10, &block)
end
end
end
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index b02d94e9cb1..3a6989a00f1 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -105,7 +105,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe('/issues?scope=all&state=opened&crm_contact_id=16');
+ expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16');
});
});
});
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index 231208d938e..1780a5945a6 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -102,9 +102,7 @@ describe('Customer relations organizations root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe(
- '/issues?scope=all&state=opened&crm_organization_id=2',
- );
+ expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2');
});
});
});
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index d3e05939527..728b8958b9b 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -1,65 +1,53 @@
-import { GlSprintf } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusBox from '~/issuable/components/status_box.vue';
let wrapper;
function factory(propsData) {
- wrapper = shallowMount(StatusBox, {
- propsData,
- stubs: { GlSprintf },
- provide: { glFeatures: { updatedMrHeader: true } },
- });
+ wrapper = shallowMount(StatusBox, { propsData, stubs: { GlBadge } });
}
-const testCases = [
- {
- name: 'Open',
- state: 'opened',
- class: 'badge-success',
- },
- {
- name: 'Open',
- state: 'locked',
- class: 'badge-success',
- },
- {
- name: 'Closed',
- state: 'closed',
- class: 'badge-danger',
- },
- {
- name: 'Merged',
- state: 'merged',
- class: 'badge-info',
- },
-];
-
describe('Merge request status box component', () => {
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- testCases.forEach((testCase) => {
- describe(`when merge request is ${testCase.name}`, () => {
- it('renders human readable test', () => {
+ describe.each`
+ issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
+ ${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
+ ${'merge_request'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'danger'} | ${'merge-request-close'}
+ ${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
+ `(
+ 'with issuableType set to "$issuableType" and state set to "$initialState"',
+ ({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
+ beforeEach(() => {
factory({
- initialState: testCase.state,
- issuableType: 'merge_request',
+ initialState,
+ issuableType,
});
+ });
- expect(wrapper.text()).toContain(testCase.name);
+ it(`renders badge with text '${badgeText}'`, () => {
+ expect(findBadge().text()).toBe(badgeText);
});
- it('sets css class', () => {
- factory({
- initialState: testCase.state,
- issuableType: 'merge_request',
- });
+ it(`sets badge css class as '${badgeClass}'`, () => {
+ expect(findBadge().classes()).toContain(badgeClass);
+ });
- expect(wrapper.classes()).toContain(testCase.class);
+ it(`sets badge variant as '${badgeVariant}`, () => {
+ expect(findBadge().props('variant')).toBe(badgeVariant);
});
- });
- });
+
+ it(`sets badge icon as '${badgeIcon}'`, () => {
+ expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
+ });
+ },
+ );
});
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 8a089b372ff..b4f6118ec20 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -24,11 +24,11 @@ describe('Issue', () => {
const getIssueCounter = () => document.querySelector('.issue_counter');
const getOpenStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Open/), {
- selector: '.status-box-open',
+ selector: '.issuable-status-badge-open',
});
const getClosedStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Closed/), {
- selector: '.status-box-issue-closed',
+ selector: '.issuable-status-badge-closed',
});
describe.each`
diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts_spec.js
index 758cff30e2d..6456829258f 100644
--- a/spec/frontend/sidebar/components/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts_spec.js
@@ -33,7 +33,7 @@ describe('Issue crm contacts component', () => {
[issueCrmContactsSubscription, subscriptionHandler],
]);
wrapper = shallowMountExtended(CrmContacts, {
- propsData: { issueId: '123' },
+ propsData: { issueId: '123', groupIssuesPath: '/groups/flightjs/-/issues' },
apolloProvider: fakeApollo,
});
};
@@ -71,8 +71,14 @@ describe('Issue crm contacts component', () => {
await waitForPromises();
expect(wrapper.find('#contact_0').text()).toContain('Someone Important');
+ expect(wrapper.find('#contact_0').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=1',
+ );
expect(wrapper.find('#contact_container_0').text()).toContain('si@gitlab.com');
expect(wrapper.find('#contact_1').text()).toContain('Marty McFly');
+ expect(wrapper.find('#contact_1').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=5',
+ );
});
it('renders correct results after subscription update', async () => {
@@ -83,5 +89,8 @@ describe('Issue crm contacts component', () => {
contact.forEach((property) => {
expect(wrapper.find('#contact_container_0').text()).toContain(property);
});
+ expect(wrapper.find('#contact_0').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=13',
+ );
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 868b7af6da5..02579e8b3c4 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -69,9 +69,11 @@ describe('IssuableHeader', () => {
it('renders issuable status icon and text', () => {
createComponent();
const statusBoxEl = wrapper.findByTestId('status');
+ const statusIconEl = statusBoxEl.findComponent(GlIcon);
expect(statusBoxEl.exists()).toBe(true);
- expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass);
expect(statusBoxEl.text()).toContain('Open');
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index d1eb1366225..8b027f990a2 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => {
const {
statusBadgeClass,
statusIcon,
+ statusIconClass,
enableEdit,
enableAutocomplete,
editFormVisible,
@@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => {
descriptionHelpPath,
taskCompletionStatus,
} = mockIssuableShowProps;
- const { blocked, confidential, createdAt, author } = mockIssuable;
+ const { state, blocked, confidential, createdAt, author } = mockIssuable;
it('renders component container element with class `issuable-show-container`', () => {
expect(wrapper.classes()).toContain('issuable-show-container');
@@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => {
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
+ issuableState: state,
statusBadgeClass,
statusIcon,
+ statusIconClass,
blocked,
confidential,
createdAt,
author,
taskCompletionStatus,
});
- expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
+ expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
true,
);
diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js
index f5f3ed58655..32bb9edfe08 100644
--- a/spec/frontend/vue_shared/issuable/show/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/show/mock_data.js
@@ -36,8 +36,9 @@ export const mockIssuableShowProps = {
enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
- statusBadgeClass: 'status-box-open',
- statusIcon: 'issue-open-m',
+ statusBadgeClass: 'issuable-status-badge-open',
+ statusIcon: 'issues',
+ statusIconClass: 'gl-sm-display-none',
taskCompletionStatus: {
completedCount: 0,
count: 5,
diff --git a/spec/helpers/badges_helper_spec.rb b/spec/helpers/badges_helper_spec.rb
index 5be3b4a737b..8e1f92305da 100644
--- a/spec/helpers/badges_helper_spec.rb
+++ b/spec/helpers/badges_helper_spec.rb
@@ -89,16 +89,16 @@ RSpec.describe BadgesHelper do
end
describe 'icons' do
- let(:spacing_class_regex) { %r{<svg .*class=".*gl-mr-2.*".*>.*</svg>} }
+ let(:spacing_class_regex) { %r{<svg .*class=".*my-icon-class gl-mr-2".*>.*</svg>} }
describe 'with text' do
- subject { helper.gl_badge_tag(label, icon: "question-o") }
+ subject { helper.gl_badge_tag(label, icon: "question-o", icon_classes: 'my-icon-class') }
it 'renders an icon' do
expect(subject).to match(%r{<svg .*#question-o".*>.*</svg>})
end
- it 'adds a spacing class to the icon' do
+ it 'adds a spacing class and any custom classes to the icon' do
expect(subject).to match(spacing_class_regex)
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ee5b0145d13..2244aaee2dc 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -464,6 +464,41 @@ RSpec.describe IssuablesHelper do
end
end
+ describe '#state_name_with_icon' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'for an issue' do
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:issue_closed) { create(:issue, :closed, project: project) }
+
+ it 'returns the correct state name and icon when issue is open' do
+ expect(helper.state_name_with_icon(issue)).to match_array([_('Open'), 'issues'])
+ end
+
+ it 'returns the correct state name and icon when issue is closed' do
+ expect(helper.state_name_with_icon(issue_closed)).to match_array([_('Closed'), 'issue-closed'])
+ end
+ end
+
+ context 'for a merge request' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:merge_request_merged) { create(:merge_request, :merged, source_project: project) }
+ let_it_be(:merge_request_closed) { create(:merge_request, :closed, source_project: project) }
+
+ it 'returns the correct state name and icon when merge request is open' do
+ expect(helper.state_name_with_icon(merge_request)).to match_array([_('Open'), 'merge-request-open'])
+ end
+
+ it 'returns the correct state name and icon when merge request is merged' do
+ expect(helper.state_name_with_icon(merge_request_merged)).to match_array([_('Merged'), 'merge'])
+ end
+
+ it 'returns the correct state name and icon when merge request is closed' do
+ expect(helper.state_name_with_icon(merge_request_closed)).to match_array([_('Closed'), 'merge-request-close'])
+ end
+ end
+ end
+
describe '#issuable_display_type' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index f7762069b68..97ad55d9df9 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -5,31 +5,6 @@ require 'spec_helper'
RSpec.describe MergeRequestsHelper do
include ProjectForksHelper
- describe '#state_name_with_icon' do
- using RSpec::Parameterized::TableSyntax
-
- let(:merge_request) { MergeRequest.new }
-
- where(:state, :expected_name, :expected_icon) do
- :merged? | 'Merged' | 'git-merge'
- :closed? | 'Closed' | 'close'
- :opened? | 'Open' | 'issue-open-m'
- end
-
- with_them do
- before do
- allow(merge_request).to receive(state).and_return(true)
- end
-
- it 'returns name and icon' do
- name, icon = helper.state_name_with_icon(merge_request)
-
- expect(name).to eq(expected_name)
- expect(icon).to eq(expected_icon)
- end
- end
- end
-
describe '#format_mr_branch_names' do
describe 'within the same project' do
let(:merge_request) { create(:merge_request) }
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 8e372ba795b..d4f96f1a37f 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe Gitlab::UrlBuilder do
:group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" }
:commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" }
:issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" }
+ [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.id}" }
+ :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.id}" }
:merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" }
:project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
@@ -57,7 +59,7 @@ RSpec.describe Gitlab::UrlBuilder do
end
with_them do
- let(:object) { build_stubbed(factory) }
+ let(:object) { build_stubbed(*Array(factory)) }
let(:path) { path_generator.call(object) }
it 'returns the full URL' do
@@ -69,6 +71,18 @@ RSpec.describe Gitlab::UrlBuilder do
end
end
+ context 'when work_items feature flag is disabled' do
+ before do
+ stub_feature_flags(work_items: false)
+ end
+
+ it 'returns an issue path for an issue of type task' do
+ task = create(:issue, :task)
+
+ expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/issues/#{task.iid}")
+ end
+ end
+
context 'when passing a compare' do
# NOTE: The Compare requires an actual repository, which isn't available
# with the `build_stubbed` strategy used by the table tests above
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
index d22955718f8..3447ff83ed8 100644
--- a/spec/requests/projects/issue_links_controller_spec.rb
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -24,6 +24,17 @@ RSpec.describe Projects::IssueLinksController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(list_service_response.as_json)
end
+
+ context 'when linked issue is a task' do
+ let(:issue_b) { create :issue, :task, project: project }
+
+ it 'returns a work item path for the linked task' do
+ get namespace_project_issue_links_path(issue_links_params)
+
+ expect(json_response.count).to eq(1)
+ expect(json_response.first).to include('path' => project_work_items_path(issue_b.project, issue_b.id))
+ end
+ end
end
describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do
diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
index 30423ceba6d..b8e2bfeaa3d 100644
--- a/spec/serializers/issue_board_entity_spec.rb
+++ b/spec/serializers/issue_board_entity_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssueBoardEntity do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:project) { create(:project) }
let_it_be(:resource) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -40,4 +42,18 @@ RSpec.describe IssueBoardEntity do
expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority)))
end
+
+ describe 'real_path' do
+ it 'has an issue path' do
+ expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid))
+ end
+
+ context 'when issue is of type task' do
+ let(:resource) { create(:issue, :task, project: project) }
+
+ it 'has a work item path' do
+ expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id))
+ end
+ end
+ end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 76f8cf644c6..6ccb3dbc657 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe IssueEntity do
+ include Gitlab::Routing.url_helpers
+
let(:project) { create(:project) }
let(:resource) { create(:issue, project: project) }
let(:user) { create(:user) }
@@ -11,6 +13,17 @@ RSpec.describe IssueEntity do
subject { described_class.new(resource, request: request).as_json }
+ describe 'web_url' do
+ context 'when issue is of type task' do
+ let(:resource) { create(:issue, :task, project: project) }
+
+ # This was already a path and not a url when the work items change was introduced
+ it 'has a work item path' do
+ expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id))
+ end
+ end
+ end
+
it 'has Issuable attributes' do
expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
:title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb
index 864b5c45599..b28b00bd8e1 100644
--- a/spec/serializers/linked_project_issue_entity_spec.rb
+++ b/spec/serializers/linked_project_issue_entity_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe LinkedProjectIssueEntity do
+ include Gitlab::Routing.url_helpers
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue_link) { create(:issue_link) }
@@ -17,7 +19,25 @@ RSpec.describe LinkedProjectIssueEntity do
issue_link.target.project.add_developer(user)
end
+ subject(:serialized_entity) { entity.as_json }
+
describe 'issue_link_type' do
- it { expect(entity.as_json).to include(link_type: 'relates_to') }
+ it { is_expected.to include(link_type: 'relates_to') }
+ end
+
+ describe 'path' do
+ it 'returns an issue path' do
+ expect(serialized_entity).to include(path: project_issue_path(related_issue.project, related_issue.iid))
+ end
+
+ context 'when related issue is a task' do
+ before do
+ related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task))
+ end
+
+ it 'returns a work items path' do
+ expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id))
+ end
+ end
end
end
diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb
index 4427c4e7d9f..c3ba7c0374d 100644
--- a/spec/services/members/groups/creator_service_spec.rb
+++ b/spec/services/members/groups/creator_service_spec.rb
@@ -3,14 +3,28 @@
require 'spec_helper'
RSpec.describe Members::Groups::CreatorService do
- it_behaves_like 'member creation' do
- let_it_be(:source, reload: true) { create(:group, :public) }
- let_it_be(:member_type) { GroupMember }
- end
-
describe '.access_levels' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
+
+ describe '#execute' do
+ let_it_be(:source, reload: true) { create(:group, :public) }
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'member creation' do
+ let_it_be(:member_type) { GroupMember }
+ end
+
+ context 'authorized projects update' do
+ it 'schedules a single project authorization update job when called multiple times' do
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
+
+ 1.upto(3) do
+ described_class.new(source, user, :maintainer).execute
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb
index 7ba183759bc..7605238c3c5 100644
--- a/spec/services/members/projects/creator_service_spec.rb
+++ b/spec/services/members/projects/creator_service_spec.rb
@@ -3,14 +3,28 @@
require 'spec_helper'
RSpec.describe Members::Projects::CreatorService do
- it_behaves_like 'member creation' do
- let_it_be(:source, reload: true) { create(:project, :public) }
- let_it_be(:member_type) { ProjectMember }
- end
-
describe '.access_levels' do
it 'returns Gitlab::Access.sym_options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
+
+ describe '#execute' do
+ let_it_be(:source, reload: true) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'member creation' do
+ let_it_be(:member_type) { ProjectMember }
+ end
+
+ context 'authorized projects update' do
+ it 'schedules a single project authorization update job when called multiple times' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
+
+ 1.upto(3) do
+ described_class.new(source, user, :maintainer).execute
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/trial_status_widget_test_helper.rb b/spec/support/helpers/trial_status_widget_test_helper.rb
new file mode 100644
index 00000000000..d75620d17ee
--- /dev/null
+++ b/spec/support/helpers/trial_status_widget_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module TrialStatusWidgetTestHelper
+ def purchase_href(group)
+ new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id')
+ end
+end
+
+TrialStatusWidgetTestHelper.prepend_mod
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index a329a6dca91..e293d10964b 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -77,312 +77,309 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name|
end
RSpec.shared_examples_for "member creation" do
- let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
- describe '#execute' do
- it 'returns a Member object', :aggregate_failures do
- member = described_class.new(source, user, :maintainer).execute
-
- expect(member).to be_a member_type
- expect(member).to be_persisted
- end
+ it 'returns a Member object', :aggregate_failures do
+ member = described_class.new(source, user, :maintainer).execute
- context 'when adding a project_bot' do
- let_it_be(:project_bot) { create(:user, :project_bot) }
-
- before_all do
- source.add_owner(user)
- end
+ expect(member).to be_a member_type
+ expect(member).to be_persisted
+ end
- context 'when project_bot is already a member' do
- before do
- source.add_developer(project_bot)
- end
+ context 'when adding a project_bot' do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
- it 'does not update the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ before_all do
+ source.add_owner(user)
+ end
- expect(source.users.reload).to include(project_bot)
- expect(member).to be_persisted
- expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
- expect(member.errors.full_messages).to include(/not authorized to update member/)
- end
+ context 'when project_bot is already a member' do
+ before do
+ source.add_developer(project_bot)
end
- context 'when project_bot is not already a member' do
- it 'adds the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ it 'does not update the member' do
+ member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
- expect(source.users.reload).to include(project_bot)
- expect(member).to be_persisted
- end
+ expect(source.users.reload).to include(project_bot)
+ expect(member).to be_persisted
+ expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
+ expect(member.errors.full_messages).to include(/not authorized to update member/)
end
end
- context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
- it 'sets members.created_by to the given admin current_user' do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ context 'when project_bot is not already a member' do
+ it 'adds the member' do
+ member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
- expect(source.users.reload).to include(user)
- expect(member.created_by).to eq(admin)
end
end
+ end
- context 'when admin mode is disabled' do
- it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
+ it 'sets members.created_by to the given admin current_user' do
+ member = described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(member).not_to be_persisted
- expect(source.users.reload).not_to include(user)
- expect(member.errors.full_messages).to include(/not authorized to create member/)
- end
+ expect(member).to be_persisted
+ expect(source.users.reload).to include(user)
+ expect(member.created_by).to eq(admin)
end
+ end
- it 'sets members.expires_at to the given expires_at' do
- member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
+ context 'when admin mode is disabled' do
+ it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
+ member = described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ expect(member).not_to be_persisted
+ expect(source.users.reload).not_to include(user)
+ expect(member.errors.full_messages).to include(/not authorized to create member/)
end
+ end
- described_class.access_levels.each do |sym_key, int_access_level|
- it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
- expect(source.users).not_to include(user)
+ it 'sets members.expires_at to the given expires_at' do
+ member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
- member = described_class.new(source, user.id, sym_key).execute
+ expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ end
- expect(member.access_level).to eq(int_access_level)
- expect(source.users.reload).to include(user)
- end
+ described_class.access_levels.each do |sym_key, int_access_level|
+ it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
+ expect(source.users).not_to include(user)
+
+ member = described_class.new(source, user.id, sym_key).execute
- it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+
+ it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
+ expect(source.users).not_to include(user)
+
+ member = described_class.new(source, user.id, int_access_level).execute
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'with no current_user' do
+ context 'when called with a known user id' do
+ it 'adds the user as a member' do
expect(source.users).not_to include(user)
- member = described_class.new(source, user.id, int_access_level).execute
+ described_class.new(source, user.id, :maintainer).execute
- expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
- context 'with no current_user' do
- context 'when called with a known user id' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user id' do
+ it 'does not add the user as a member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, user.id, :maintainer).execute
+ described_class.new(source, non_existing_record_id, :maintainer).execute
- expect(source.users.reload).to include(user)
- end
+ expect(source.users.reload).not_to include(user)
end
+ end
- context 'when called with an unknown user id' do
- it 'does not add the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with a user object' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, non_existing_record_id, :maintainer).execute
+ described_class.new(source, user, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- end
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
end
- context 'when called with a user object' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ it 'adds the requester as a member', :aggregate_failures do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+ expect do
described_class.new(source, user, :maintainer).execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(source.users.reload).to include(user)
- end
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
-
- it 'adds the requester as a member', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ context 'when called with a known user email' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
- expect do
- described_class.new(source, user, :maintainer).execute
- end.to raise_error(Gitlab::Access::AccessDeniedError)
+ described_class.new(source, user.email, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
- end
+ expect(source.users.reload).to include(user)
end
+ end
- context 'when called with a known user email' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user email' do
+ it 'creates an invited member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, user.email, :maintainer).execute
+ described_class.new(source, 'user@example.com', :maintainer).execute
- expect(source.users.reload).to include(user)
- end
+ expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
+ end
- context 'when called with an unknown user email' do
- it 'creates an invited member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user email starting with a number' do
+ it 'creates an invited member', :aggregate_failures do
+ email_starting_with_number = "#{user.id}_email@example.com"
- described_class.new(source, 'user@example.com', :maintainer).execute
+ described_class.new(source, email_starting_with_number, :maintainer).execute
- expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
- end
+ expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
+ expect(source.users.reload).not_to include(user)
end
+ end
+ end
- context 'when called with an unknown user email starting with a number' do
- it 'creates an invited member', :aggregate_failures do
- email_starting_with_number = "#{user.id}_email@example.com"
+ context 'when current_user can update member', :enable_admin_mode do
+ it 'creates the member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, email_starting_with_number, :maintainer).execute
+ described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
- expect(source.users.reload).not_to include(user)
- end
- end
+ expect(source.users.reload).to include(user)
end
- context 'when current_user can update member', :enable_admin_mode do
- it 'creates the member' do
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.users.reload).to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
+ end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
+ context 'when current_user cannot update member' do
+ it 'does not create the member', :aggregate_failures do
+ expect(source.users).not_to include(user)
- it 'adds the requester as a member', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ member = described_class.new(source, user, :maintainer, current_user: user).execute
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ expect(source.users.reload).not_to include(user)
+ expect(member).not_to be_persisted
+ end
- expect(source.users.reload).to include(user)
- expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
- end
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
end
- end
- context 'when current_user cannot update member' do
- it 'does not create the member', :aggregate_failures do
+ it 'does not destroy the requester', :aggregate_failures do
expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
- member = described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.users.reload).not_to include(user)
- expect(member).not_to be_persisted
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
end
+ end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
+ context 'when member already exists' do
+ before do
+ source.add_user(user, :developer)
+ end
- it 'does not destroy the requester', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ context 'with no current_user' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.new(source, user, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
- context 'when member already exists' do
- before do
- source.add_user(user, :developer)
- end
-
- context 'with no current_user' do
- it 'updates the member' do
- expect(source.users).to include(user)
+ context 'when current_user can update member', :enable_admin_mode do
+ it 'updates the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
+ end
- context 'when current_user can update member', :enable_admin_mode do
- it 'updates the member' do
- expect(source.users).to include(user)
+ context 'when current_user cannot update member' do
+ it 'does not update the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.new(source, user, :maintainer, current_user: user).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
+ end
+ end
- context 'when current_user cannot update member' do
- it 'does not update the member' do
- expect(source.users).to include(user)
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
- described_class.new(source, user, :maintainer, current_user: user).execute
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
- end
- end
- end
+ member = source.members.last
- context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
- let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
+ end
- it 'creates a member_task with the correct attributes', :aggregate_failures do
- described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
+ context 'with an already existing member' do
+ before do
+ source.add_user(user, :developer)
+ end
- member = source.members.last
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.not_to change(MemberTask, :count)
+
+ member.reset
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project).to eq(task_project)
end
- context 'with an already existing member' do
- before do
- source.add_user(user, :developer)
- end
-
- it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
- member = source.members.find_by(user_id: user.id)
- create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
-
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.not_to change(MemberTask, :count)
-
- member.reset
- expect(member.tasks_to_be_done).to match_array([:code, :ci])
- expect(member.member_task.project).to eq(task_project)
- end
-
- it 'adds tasks to be done if they do not exist', :aggregate_failures do
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.to change(MemberTask, :count).by(1)
-
- member = source.members.find_by(user_id: user.id)
- expect(member.tasks_to_be_done).to match_array([:issues])
- expect(member.member_task.project).to eq(task_project)
- end
+ it 'adds tasks to be done if they do not exist', :aggregate_failures do
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
end
end
end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index b2d208f038a..3f1496a24ce 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -26,14 +26,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed (moved)" if an issue has been moved and closed' do
render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
it 'shows "Closed (moved)" if an issue has been moved and discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
it 'links "moved" to the new issue the original issue was moved to' do
@@ -47,7 +47,7 @@ RSpec.describe 'projects/issues/show' do
render
- expect(rendered).not_to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ expect(rendered).not_to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (moved)')
end
end
@@ -75,7 +75,7 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed (duplicated)" if an issue has been duplicated' do
render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (duplicated)')
+ expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed (duplicated)')
end
it 'links "duplicated" to the new issue the original issue was duplicated to' do
@@ -97,14 +97,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Closed" if an issue has not been moved or duplicated' do
render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed')
+ expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed')
end
it 'shows "Closed" if discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed')
+ expect(rendered).to have_selector('.issuable-status-badge-closed:not(.hidden)', text: 'Closed')
end
end
@@ -117,14 +117,14 @@ RSpec.describe 'projects/issues/show' do
it 'shows "Open" if an issue has been moved' do
render
- expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
+ expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open')
end
it 'shows "Open" if discussion is locked' do
allow(issue).to receive(:discussion_locked).and_return(true)
render
- expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
+ expect(rendered).to have_selector('.issuable-status-badge-open:not(.hidden)', text: 'Open')
end
end