Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/cop/ignored_columns.yml1
-rw-r--r--.rubocop_todo/gitlab/namespaced_class.yml1
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml6
-rw-r--r--.rubocop_todo/lint/redundant_cop_disable_directive.yml2
-rw-r--r--.rubocop_todo/rails/pluck.yml2
-rw-r--r--.rubocop_todo/rspec/feature_category.yml3
-rw-r--r--.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml11
-rw-r--r--.rubocop_todo/style/inline_disable_annotation.yml19
-rw-r--r--.rubocop_todo/style/symbol_proc.yml1
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue124
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js171
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js9
-rw-r--r--app/assets/javascripts/content_editor/services/data_source_factory.js213
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js4
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/service/archived_projects_service.js2
-rw-r--r--app/assets/stylesheets/vendors/atwho.scss41
-rw-r--r--app/models/batched_git_ref_updates/deletion.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb17
-rw-r--r--app/models/concerns/ignorable_columns.rb19
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb6
-rw-r--r--app/services/bulk_imports/batched_relation_export_service.rb14
-rw-r--r--app/services/ci/components/fetch_service.rb2
-rw-r--r--db/migrate/20231205201701_remove_geo_primary_deprecated_workers_job_instances.rb24
-rw-r--r--db/schema_migrations/202312052017011
-rw-r--r--doc/ci/components/index.md59
-rw-r--r--doc/development/runner_fleet_dashboard.md6
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js14
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js188
-rw-r--r--spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap256
-rw-r--r--spec/frontend/content_editor/services/autocomplete_mock_data.js967
-rw-r--r--spec/frontend/content_editor/services/data_source_factory_spec.js202
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js8
-rw-r--r--spec/frontend/groups/service/archived_projects_service_spec.js2
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb83
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb2
-rw-r--r--spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb6
-rw-r--r--spec/models/ci/catalog/components_project_spec.rb1
-rw-r--r--spec/models/concerns/ignorable_columns_spec.rb18
-rw-r--r--spec/policies/protected_branch_access_policy_spec.rb10
-rw-r--r--spec/policies/protected_branch_policy_spec.rb40
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb34
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/policies/protected_branches.rb21
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb11
49 files changed, 2139 insertions, 543 deletions
diff --git a/.rubocop_todo/cop/ignored_columns.yml b/.rubocop_todo/cop/ignored_columns.yml
index a791fcd0d6d..d15bcb497d9 100644
--- a/.rubocop_todo/cop/ignored_columns.yml
+++ b/.rubocop_todo/cop/ignored_columns.yml
@@ -1,7 +1,6 @@
---
Cop/IgnoredColumns:
Exclude:
- - 'app/models/loose_foreign_keys/deleted_record.rb'
- 'ee/lib/ee/gitlab/background_migration/create_vulnerability_links.rb'
- 'ee/lib/ee/gitlab/background_migration/migrate_vulnerabilities_feedback_to_vulnerabilities_state_transition.rb'
- 'spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb'
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 19f31b94a6a..f1f86122067 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -1028,7 +1028,6 @@ Gitlab/NamespacedClass:
- 'ee/app/workers/elastic_namespace_rollout_worker.rb'
- 'ee/app/workers/elastic_remove_expired_namespace_subscriptions_from_index_cron_worker.rb'
- 'ee/app/workers/elastic_wiki_indexer_worker.rb'
- - 'ee/app/workers/geo_repository_destroy_worker.rb'
- 'ee/app/workers/group_saml_group_sync_worker.rb'
- 'ee/app/workers/historical_data_worker.rb'
- 'ee/app/workers/import_software_licenses_worker.rb'
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index c40f51cf04e..a1aacb908e6 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -375,7 +375,6 @@ Gitlab/StrongMemoizeAttr:
- 'ee/app/services/vulnerability_feedback/create_service.rb'
- 'ee/app/services/vulnerability_feedback/destroy_service.rb'
- 'ee/app/workers/auth/saml_group_sync_worker.rb'
- - 'ee/app/workers/geo/repository_cleanup_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/app/workers/status_page/publish_worker.rb'
- 'ee/lib/api/analytics/project_deployment_frequency.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index a68124c649b..471aed8912f 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -1083,16 +1083,10 @@ Layout/LineLength:
- 'ee/app/workers/elastic_commit_indexer_worker.rb'
- 'ee/app/workers/elastic_delete_project_worker.rb'
- 'ee/app/workers/elastic_namespace_rollout_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_scheduler_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_worker.rb'
- 'ee/app/workers/geo/destroy_worker.rb'
- - 'ee/app/workers/geo/repositories_clean_up_worker.rb'
- - 'ee/app/workers/geo/repository_shard_sync_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/app/workers/geo/secondary/registry_consistency_worker.rb'
- 'ee/app/workers/geo/verification_worker.rb'
- - 'ee/app/workers/geo_repository_destroy_worker.rb'
- 'ee/app/workers/repository_update_mirror_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_namespace_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_worker.rb'
diff --git a/.rubocop_todo/lint/redundant_cop_disable_directive.yml b/.rubocop_todo/lint/redundant_cop_disable_directive.yml
index 453954e9bd1..1f452c24c15 100644
--- a/.rubocop_todo/lint/redundant_cop_disable_directive.yml
+++ b/.rubocop_todo/lint/redundant_cop_disable_directive.yml
@@ -114,8 +114,6 @@ Lint/RedundantCopDisableDirective:
- 'ee/app/services/ee/search_service.rb'
- 'ee/app/services/security/token_revocation_service.rb'
- 'ee/app/workers/ee/issuable_export_csv_worker.rb'
- - 'ee/app/workers/geo/repository_shard_sync_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
- 'ee/app/workers/scan_security_report_secrets_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_worker.rb'
- 'ee/db/geo/migrate/20210504143244_add_verification_to_merge_request_diff_registry.rb'
diff --git a/.rubocop_todo/rails/pluck.yml b/.rubocop_todo/rails/pluck.yml
index 6ed8c935f82..7a25f45bfc4 100644
--- a/.rubocop_todo/rails/pluck.yml
+++ b/.rubocop_todo/rails/pluck.yml
@@ -21,8 +21,6 @@ Rails/Pluck:
- 'ee/app/models/boards/epic_list.rb'
- 'ee/app/models/concerns/geo/verification_state.rb'
- 'ee/app/services/concerns/incident_management/oncall_rotations/shared_rotation_logic.rb'
- - 'ee/app/workers/geo/repository_shard_sync_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/lib/banzai/filter/references/iteration_reference_filter.rb'
- 'ee/lib/ee/gitlab/auth/ldap/person.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index 20f9cd77113..c956d05f6f0 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -1173,7 +1173,6 @@ RSpec/FeatureCategory:
- 'ee/spec/policies/note_policy_spec.rb'
- 'ee/spec/policies/path_lock_policy_spec.rb'
- 'ee/spec/policies/project_snippet_policy_spec.rb'
- - 'ee/spec/policies/protected_branch_policy_spec.rb'
- 'ee/spec/policies/requirements_management/requirement_policy_spec.rb'
- 'ee/spec/policies/saml_provider_policy_spec.rb'
- 'ee/spec/policies/security/finding_policy_spec.rb'
@@ -4827,8 +4826,6 @@ RSpec/FeatureCategory:
- 'spec/policies/project_member_policy_spec.rb'
- 'spec/policies/project_snippet_policy_spec.rb'
- 'spec/policies/project_statistics_policy_spec.rb'
- - 'spec/policies/protected_branch_access_policy_spec.rb'
- - 'spec/policies/protected_branch_policy_spec.rb'
- 'spec/policies/release_policy_spec.rb'
- 'spec/policies/system_hook_policy_spec.rb'
- 'spec/policies/terraform/state_policy_spec.rb'
diff --git a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
index 0c8affcd964..45c060ebe03 100644
--- a/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
+++ b/.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml
@@ -344,8 +344,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/epics/new_epic_issue_worker.rb'
- 'ee/app/workers/epics/update_cached_metadata_worker.rb'
- 'ee/app/workers/epics/update_epics_dates_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_scheduler_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_worker.rb'
- 'ee/app/workers/geo/batch_event_create_worker.rb'
- 'ee/app/workers/geo/container_repository_sync_worker.rb'
- 'ee/app/workers/geo/create_repository_updated_event_worker.rb'
@@ -353,17 +351,9 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/geo/event_worker.rb'
- 'ee/app/workers/geo/file_removal_worker.rb'
- 'ee/app/workers/geo/hashed_storage_attachments_migration_worker.rb'
- - 'ee/app/workers/geo/hashed_storage_migration_worker.rb'
- 'ee/app/workers/geo/metrics_update_worker.rb'
- - 'ee/app/workers/geo/project_sync_worker.rb'
- 'ee/app/workers/geo/prune_event_log_worker.rb'
- - 'ee/app/workers/geo/rename_repository_worker.rb'
- - 'ee/app/workers/geo/repositories_clean_up_worker.rb'
- - 'ee/app/workers/geo/repository_cleanup_worker.rb'
- - 'ee/app/workers/geo/repository_verification/primary/single_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/single_worker.rb'
- 'ee/app/workers/geo/reverification_batch_worker.rb'
- - 'ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- 'ee/app/workers/geo/secondary/registry_consistency_worker.rb'
- 'ee/app/workers/geo/secondary_usage_data_cron_worker.rb'
@@ -374,7 +364,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/geo/verification_state_backfill_worker.rb'
- 'ee/app/workers/geo/verification_timeout_worker.rb'
- 'ee/app/workers/geo/verification_worker.rb'
- - 'ee/app/workers/geo_repository_destroy_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/schedule_refresh_seats_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/trials/apply_trial_worker.rb'
- 'ee/app/workers/group_saml_group_sync_worker.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index 55a3320cedd..f3d93810758 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -1696,33 +1696,15 @@ Style/InlineDisableAnnotation:
- 'ee/app/workers/emails/abandoned_trial_emails_cron_worker.rb'
- 'ee/app/workers/epics/new_epic_issue_worker.rb'
- 'ee/app/workers/epics/update_epics_dates_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_scheduler_worker.rb'
- - 'ee/app/workers/geo/batch/project_registry_worker.rb'
- 'ee/app/workers/geo/bulk_mark_pending_batch_worker.rb'
- 'ee/app/workers/geo/bulk_mark_verification_pending_batch_worker.rb'
- 'ee/app/workers/geo/container_repository_sync_worker.rb'
- 'ee/app/workers/geo/event_worker.rb'
- 'ee/app/workers/geo/file_removal_worker.rb'
- 'ee/app/workers/geo/hashed_storage_attachments_migration_worker.rb'
- - 'ee/app/workers/geo/hashed_storage_migration_worker.rb'
- 'ee/app/workers/geo/metrics_update_worker.rb'
- 'ee/app/workers/geo/prune_event_log_worker.rb'
- - 'ee/app/workers/geo/rename_repository_worker.rb'
- - 'ee/app/workers/geo/repositories_clean_up_worker.rb'
- - 'ee/app/workers/geo/repository_cleanup_worker.rb'
- - 'ee/app/workers/geo/repository_shard_sync_worker.rb'
- - 'ee/app/workers/geo/repository_sync_worker.rb'
- - 'ee/app/workers/geo/repository_verification/primary/batch_worker.rb'
- - 'ee/app/workers/geo/repository_verification/primary/shard_worker.rb'
- - 'ee/app/workers/geo/repository_verification/primary/single_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/scheduler_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/shard_worker.rb'
- - 'ee/app/workers/geo/repository_verification/secondary/single_worker.rb'
- - 'ee/app/workers/geo/scheduler/per_shard_scheduler_worker.rb'
- - 'ee/app/workers/geo/scheduler/primary/per_shard_scheduler_worker.rb'
- - 'ee/app/workers/geo/scheduler/primary/scheduler_worker.rb'
- 'ee/app/workers/geo/scheduler/scheduler_worker.rb'
- - 'ee/app/workers/geo/scheduler/secondary/per_shard_scheduler_worker.rb'
- 'ee/app/workers/geo/scheduler/secondary/scheduler_worker.rb'
- 'ee/app/workers/geo/secondary/registry_consistency_worker.rb'
- 'ee/app/workers/geo/secondary_usage_data_cron_worker.rb'
@@ -1730,7 +1712,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/workers/geo/sync_timeout_cron_worker.rb'
- 'ee/app/workers/geo/verification_cron_worker.rb'
- 'ee/app/workers/geo/verification_state_backfill_worker.rb'
- - 'ee/app/workers/geo_repository_destroy_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/add_on_purchases/schedule_bulk_refresh_user_assignments_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/notify_seats_exceeded_batch_worker.rb'
- 'ee/app/workers/gitlab_subscriptions/refresh_seats_worker.rb'
diff --git a/.rubocop_todo/style/symbol_proc.yml b/.rubocop_todo/style/symbol_proc.yml
index be1319c10bb..1808bb7fbb6 100644
--- a/.rubocop_todo/style/symbol_proc.yml
+++ b/.rubocop_todo/style/symbol_proc.yml
@@ -80,7 +80,6 @@ Style/SymbolProc:
- 'ee/app/services/security/scanned_resources_counting_service.rb'
- 'ee/app/services/timebox_report_service.rb'
- 'ee/app/services/vulnerabilities/historical_statistics/deletion_service.rb'
- - 'ee/app/workers/geo/batch/project_registry_worker.rb'
- 'ee/app/workers/geo/sync_timeout_cron_worker.rb'
- 'ee/app/workers/geo/verification_cron_worker.rb'
- 'ee/lib/api/entities/pending_member.rb'
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 2e9388c1e20..a48245f732d 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -6,6 +6,7 @@ import { VARIANT_DANGER } from '~/alert';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { CONTENT_EDITOR_READY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -157,6 +158,7 @@ export default {
enableAutocomplete,
autocompleteDataSources,
codeSuggestionsConfig,
+ sidebarMediator: SidebarMediator.singleton,
tiptapOptions: {
autofocus,
editable,
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index b34ebe85eb4..dcca76b0786 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -1,12 +1,17 @@
<script>
-import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
- GlAvatarLabeled,
+ GlAvatar,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
+
props: {
char: {
type: String,
@@ -38,6 +43,12 @@ export default {
required: false,
default: false,
},
+
+ query: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -90,20 +101,30 @@ export default {
isEmoji() {
return this.nodeType === 'emoji';
},
+
+ shouldSelectFirstItem() {
+ return this.items.length && this.query;
+ },
},
watch: {
items() {
- this.selectedIndex = -1;
+ this.selectedIndex = this.shouldSelectFirstItem ? 0 : -1;
},
selectedIndex() {
this.scrollIntoView();
},
},
+ mounted() {
+ if (this.shouldSelectFirstItem) {
+ this.selectedIndex = 0;
+ }
+ },
+
methods: {
getText(item) {
- if (this.isEmoji) return item.e;
+ if (this.isEmoji) return item.emoji.e;
switch (this.isReference && this.nodeProps.referenceType) {
case 'user':
@@ -133,10 +154,10 @@ export default {
if (this.isEmoji) {
Object.assign(props, {
- name: item.name,
- unicodeVersion: item.u,
- title: item.d,
- moji: item.e,
+ name: item.emoji.name,
+ unicodeVersion: item.emoji.u,
+ title: item.emoji.d,
+ moji: item.emoji.e,
});
}
@@ -173,7 +194,7 @@ export default {
return true;
}
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' || event.key === 'Tab') {
this.enterHandler();
return true;
}
@@ -194,7 +215,7 @@ export default {
},
scrollIntoView() {
- this.$refs.dropdownItems[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
+ this.$refs.dropdownItems?.[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
},
selectItem(index) {
@@ -211,7 +232,17 @@ export default {
avatarSubLabel(item) {
return item.count ? `${item.name} (${item.count})` : item.name;
},
+
+ highlight(text) {
+ return this.query
+ ? String(text).replace(
+ new RegExp(this.query, 'i'),
+ (match) => `<strong class="gl-text-body!">${match}</strong>`,
+ )
+ : text;
+ },
},
+ safeHtmlConfig: { ALLOWED_TAGS: ['strong'] },
};
</script>
@@ -238,29 +269,45 @@ export default {
@click="selectItem(index)"
>
<div class="gl-new-dropdown-item-text-wrapper">
- <gl-avatar-labeled
- v-if="isUser"
- :label="item.username"
- :sub-label="avatarSubLabel(item)"
- :src="item.avatar_url"
- :entity-name="item.username"
- :shape="item.type === 'Group' ? 'rect' : 'circle'"
- :size="32"
- />
+ <span v-if="isUser" class="gl-flex">
+ <gl-avatar
+ :src="item.avatar_url"
+ :entity-name="item.username"
+ :size="24"
+ :shape="item.type === 'Group' ? 'rect' : 'circle'"
+ class="gl-vertical-align-middle gl-mx-2"
+ />
+ <span class="gl-vertical-align-middle">
+ <span v-safe-html:safeHtmlConfig="highlight(item.username)"></span>
+ <small
+ v-safe-html:safeHtmlConfig="highlight(avatarSubLabel(item))"
+ class="gl-text-gray-500"
+ ></small>
+ </span>
+ </span>
<span v-if="isIssue || isMergeRequest">
- <small>{{ item.iid }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.iid)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isVulnerability || isSnippet">
- <small>{{ item.id }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.id)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isEpic">
- <small>{{ item.reference }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.reference)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isMilestone">
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isLabel" class="gl-display-flex">
<span
@@ -268,20 +315,31 @@ export default {
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ backgroundColor: item.color }"
></span>
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<div v-if="isCommand">
<div class="gl-mb-1">
- <span class="gl-font-weight-bold">/{{ item.name }}</span>
- <em class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</em>
+ /<span v-safe-html:safeHtmlConfig="highlight(item.name)"></span>
+ <span class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</span>
</div>
- <small class="gl-text-gray-500"> {{ item.description }} </small>
+ <em
+ v-safe-html:safeHtmlConfig="highlight(item.description)"
+ class="gl-text-gray-500 gl-font-sm"
+ ></em>
</div>
<div v-if="isEmoji" class="gl-display-flex gl-align-items-center">
- <div class="gl-pr-4 gl-font-lg">{{ item.e }}</div>
+ <div class="gl-pr-4 gl-font-lg">
+ <gl-emoji
+ :key="item.emoji.e"
+ :data-name="item.emoji.name"
+ :title="item.emoji.d"
+ :data-unicode-version="item.emoji.u"
+ :data-fallback-src="item.emoji.src"
+ >{{ item.emoji.e }}</gl-emoji
+ >
+ </div>
<div class="gl-flex-grow-1">
- {{ item.name }}<br />
- <small>{{ item.d }}</small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.fieldValue)"></span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index be6ecb6cafd..96e03dfe598 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -46,7 +46,7 @@ export default Node.create({
title: node.attrs.title,
'data-unicode-version': node.attrs.unicodeVersion,
},
- node.attrs.moji,
+ node.attrs.moji || '',
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f7ff2fd6647..d309210404a 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -3,29 +3,20 @@ import { VueRenderer } from '@tiptap/vue-2';
import tippy from 'tippy.js';
import Suggestion from '@tiptap/suggestion';
import { PluginKey } from '@tiptap/pm/state';
-import { isFunction, uniqueId, memoize } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { uniqueId } from 'lodash';
import SuggestionsDropdown from '../components/suggestions_dropdown.vue';
-function find(haystack, needle) {
- return String(haystack).toLocaleLowerCase().includes(String(needle).toLocaleLowerCase());
-}
-
function createSuggestionPlugin({
editor,
char,
- dataSource,
- search,
- limit = 15,
+ limit = 5,
nodeType,
- nodeProps = {},
+ referenceType,
+ cache = true,
insertionMap = {},
+ serializer,
+ autocompleteHelper,
}) {
- const fetchData = memoize(
- isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
- );
-
return Suggestion({
editor,
char,
@@ -42,16 +33,17 @@ function createSuggestionPlugin({
.run();
},
- async items({ query }) {
- if (!dataSource) return [];
-
- try {
- const items = await fetchData();
-
- return items.filter(search(query)).slice(0, limit);
- } catch {
- return [];
- }
+ async items({ query, editor: tiptapEditor }) {
+ const slice = tiptapEditor.state.doc.slice(0, tiptapEditor.state.selection.to);
+ const markdownLine = serializer.serialize({ doc: slice.content }).split('\n').pop();
+
+ return autocompleteHelper
+ .getDataSource(referenceType, {
+ command: markdownLine.match(/\/\w+/)?.[0],
+ cache,
+ limit,
+ })
+ .search(query);
},
render: () => {
@@ -76,7 +68,7 @@ function createSuggestionPlugin({
...props,
char,
nodeType,
- nodeProps,
+ nodeProps: { referenceType },
loading: true,
},
editor: props.editor,
@@ -132,101 +124,38 @@ export default Node.create({
addOptions() {
return {
- autocompleteDataSources: {},
+ autocompleteHelper: {},
+ serializer: null,
};
},
addProseMirrorPlugins() {
- return [
- createSuggestionPlugin({
- editor: this.editor,
- char: '@',
- dataSource: this.options.autocompleteDataSources.members,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'user',
- },
- search: (query) => ({ name, username }) => find(name, query) || find(username, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '#',
- dataSource: this.options.autocompleteDataSources.issues,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'issue',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '$',
- dataSource: this.options.autocompleteDataSources.snippets,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'snippet',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '~',
- dataSource: this.options.autocompleteDataSources.labels,
- nodeType: 'referenceLabel',
- nodeProps: {
- referenceType: 'label',
- },
- search: (query) => ({ title }) => find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '&',
- dataSource: this.options.autocompleteDataSources.epics,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'epic',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '[vulnerability:',
- dataSource: this.options.autocompleteDataSources.vulnerabilities,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'vulnerability',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '!',
- dataSource: this.options.autocompleteDataSources.mergeRequests,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'merge_request',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '%',
- dataSource: this.options.autocompleteDataSources.milestones,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'milestone',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
+ const { serializer, autocompleteHelper } = this.options;
+
+ const createPlugin = (char, nodeType, referenceType, options = {}) =>
createSuggestionPlugin({
editor: this.editor,
- char: '/',
- dataSource: this.options.autocompleteDataSources.commands,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'command',
- },
- search: (query) => ({ name }) => find(name, query),
+ char,
+ nodeType,
+ referenceType,
+ serializer,
+ autocompleteHelper,
+ ...options,
+ });
+
+ return [
+ createPlugin('@', 'reference', 'user', { limit: 10 }),
+ createPlugin('#', 'reference', 'issue'),
+ createPlugin('$', 'reference', 'snippet'),
+ createPlugin('~', 'referenceLabel', 'label', { limit: 20 }),
+ createPlugin('&', 'reference', 'epic'),
+ createPlugin('!', 'reference', 'merge_request'),
+ createPlugin('[vulnerability:', 'reference', 'vulnerability'),
+ createPlugin('%', 'reference', 'milestone'),
+ createPlugin(':', 'emoji', 'emoji'),
+ createPlugin('/', 'reference', 'command', {
+ cache: false,
+ limit: 100,
insertionMap: {
'/label': '~',
'/unlabel': '~',
@@ -241,18 +170,6 @@ export default Node.create({
'/milestone': '%',
},
}),
- createSuggestionPlugin({
- editor: this.editor,
- char: ':',
- dataSource: () => getAllEmoji(),
- nodeType: 'emoji',
- search: (query) => ({ d, name }) => find(d, query) || find(name, query),
- limit: 10,
- }),
];
},
-
- onCreate() {
- initEmojiMap();
- },
});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 51e41ceefaf..5c48c0b1d43 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -70,6 +70,7 @@ import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
import AssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
+import DataSourceFactory from './data_source_factory';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -86,6 +87,7 @@ export const createContentEditor = ({
drawioEnabled = false,
enableAutocomplete,
autocompleteDataSources = {},
+ sidebarMediator = {},
codeSuggestionsConfig = {},
} = {}) => {
if (!isFunction(renderMarkdown)) {
@@ -95,6 +97,10 @@ export const createContentEditor = ({
const eventHub = eventHubFactory();
const assetResolver = new AssetResolver({ renderMarkdown });
const serializer = new MarkdownSerializer({ serializerConfig });
+ const autocompleteHelper = new DataSourceFactory({
+ dataSourceUrls: autocompleteDataSources,
+ sidebarMediator,
+ });
const deserializer = window.gon?.features?.preserveUnchangedMarkdown
? createRemarkMarkdownDeserializer()
: createGlApiMarkdownDeserializer({
@@ -166,7 +172,8 @@ export const createContentEditor = ({
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
- if (enableAutocomplete) allExtensions.push(Suggestions.configure({ autocompleteDataSources }));
+ if (enableAutocomplete)
+ allExtensions.push(Suggestions.configure({ autocompleteHelper, serializer }));
if (drawioEnabled) allExtensions.push(DrawioDiagram.configure({ uploadsPath, assetResolver }));
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
diff --git a/app/assets/javascripts/content_editor/services/data_source_factory.js b/app/assets/javascripts/content_editor/services/data_source_factory.js
new file mode 100644
index 00000000000..a0f0e106f1d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/data_source_factory.js
@@ -0,0 +1,213 @@
+import { identity, memoize, throttle } from 'lodash';
+import { sprintf, __ } from '~/locale';
+import { initEmojiMap, getAllEmoji, searchEmoji } from '~/emoji';
+import { parsePikadayDate } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+
+export function defaultSorter(searchFields) {
+ return (items, query) => {
+ if (!query) return items;
+
+ const sortOrdersMap = new WeakMap();
+
+ items.forEach((item) => {
+ const sortOrders = searchFields.map((searchField) => {
+ const haystack = String(item[searchField]).toLocaleLowerCase();
+ const needle = query.toLocaleLowerCase();
+
+ const i = haystack.indexOf(needle);
+ if (i < 0) return i;
+ return Number.MAX_SAFE_INTEGER - i;
+ });
+
+ sortOrdersMap.set(item, Math.max(...sortOrders));
+ });
+
+ return items.sort((a, b) => sortOrdersMap.get(b) - sortOrdersMap.get(a));
+ };
+}
+
+export function customSorter(sorter) {
+ return (items) => items.sort(sorter);
+}
+
+const milestonesMap = new WeakMap();
+
+function parseMilestone(milestone) {
+ if (!milestone.title) {
+ return milestone;
+ }
+
+ const dueDate = milestone.due_date ? parsePikadayDate(milestone.due_date) : null;
+ const expired = dueDate ? Date.now() > dueDate.getTime() : false;
+
+ return {
+ id: milestone.iid,
+ title: expired
+ ? sprintf(__('%{milestone} (expired)'), {
+ milestone: milestone.title,
+ })
+ : milestone.title,
+ expired,
+ dueDate,
+ };
+}
+
+function mapMilestone(milestone) {
+ if (!milestonesMap.has(milestone)) {
+ milestonesMap.set(milestone, parseMilestone(milestone));
+ }
+
+ return milestonesMap.get(milestone);
+}
+
+function sortMilestones(milestoneA, milestoneB) {
+ const mappedA = mapMilestone(milestoneA);
+ const mappedB = mapMilestone(milestoneB);
+
+ // Move all expired milestones to the bottom.
+ if (milestoneA.expired) return 1;
+ if (milestoneB.expired) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!milestoneA.dueDate) return 1;
+ if (!milestoneB.dueDate) return -1;
+
+ return mappedA.dueDate - mappedB.dueDate;
+}
+
+export function createDataSource({
+ source,
+ searchFields,
+ filter,
+ mapper = identity,
+ sorter = defaultSorter(searchFields),
+ cache = true,
+ limit = 15,
+}) {
+ const fetchData = source ? async () => (await axios.get(source)).data : () => [];
+ let items = [];
+
+ const sync = async function sync() {
+ try {
+ items = await fetchData();
+ } catch {
+ items = [];
+ }
+ };
+
+ const init = memoize(sync);
+ const throttledSync = throttle(sync, 5000);
+
+ return {
+ search: async (query) => {
+ await init();
+ if (!cache) throttledSync();
+
+ let results = items.map(mapper);
+ if (filter) results = filter(items, query);
+
+ if (query) {
+ results = results.filter((item) => {
+ if (!searchFields.length) return true;
+ return searchFields.some((field) =>
+ String(item[field]).toLocaleLowerCase().includes(query.toLocaleLowerCase()),
+ );
+ });
+ }
+
+ return sorter(results, query).slice(0, limit);
+ },
+ };
+}
+
+export default class DataSourceFactory {
+ constructor({ dataSourceUrls, sidebarMediator }) {
+ this.dataSourceUrls = dataSourceUrls;
+ this.sidebarMediator = sidebarMediator;
+
+ initEmojiMap();
+ }
+
+ getDataSource = memoize(
+ (referenceType, config = {}) => {
+ const sources = {
+ user: this.dataSourceUrls.members,
+ issue: this.dataSourceUrls.issues,
+ snippet: this.dataSourceUrls.snippets,
+ label: this.dataSourceUrls.labels,
+ epic: this.dataSourceUrls.epics,
+ milestone: this.dataSourceUrls.milestones,
+ merge_request: this.dataSourceUrls.mergeRequests,
+ vulnerability: this.dataSourceUrls.vulnerabilities,
+ command: this.dataSourceUrls.commands,
+ };
+
+ const searchFields = {
+ user: ['username', 'name'],
+ issue: ['iid', 'title'],
+ snippet: ['id', 'title'],
+ label: ['title'],
+ epic: ['iid', 'title'],
+ vulnerability: ['id', 'title'],
+ merge_request: ['iid', 'title'],
+ milestone: ['title', 'iid'],
+ command: ['name'],
+ emoji: [],
+ };
+
+ const filters = {
+ label: (items) =>
+ items.filter((item) => {
+ if (config.command === '/unlabel') return item.set;
+ if (config.command === '/label') return !item.set;
+
+ return true;
+ }),
+ user: (items) =>
+ items.filter((item) => {
+ const assigned = this.sidebarMediator?.store?.assignees.some(
+ (assignee) => assignee.username === item.username,
+ );
+ const assignedReviewer = this.sidebarMediator?.store?.reviewers.some(
+ (reviewer) => reviewer.username === item.username,
+ );
+
+ if (config.command === '/assign') return !assigned;
+ if (config.command === '/assign_reviewer') return !assignedReviewer;
+ if (config.command === '/unassign') return assigned;
+ if (config.command === '/unassign_reviewer') return assignedReviewer;
+
+ return true;
+ }),
+ emoji: (_, query) =>
+ query
+ ? searchEmoji(query)
+ : getAllEmoji().map((emoji) => ({ emoji, fieldValue: emoji.name })),
+ };
+
+ const sorters = {
+ milestone: customSorter(sortMilestones),
+ default: defaultSorter(searchFields[referenceType]),
+ // do not sort emoji
+ emoji: customSorter(() => 0),
+ };
+
+ const mappers = {
+ milestone: mapMilestone,
+ default: identity,
+ };
+
+ return createDataSource({
+ source: sources[referenceType],
+ searchFields: searchFields[referenceType],
+ mapper: mappers[referenceType] || mappers.default,
+ sorter: sorters[referenceType] || sorters.default,
+ filter: filters[referenceType],
+ cache: config.cache,
+ limit: config.limit,
+ });
+ },
+ (referenceType, config) => JSON.stringify({ referenceType, config }),
+ );
+}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index e116f64f927..be76ce2c28b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -82,8 +82,8 @@ export function membersBeforeSave(members) {
const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase();
const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : '';
- const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
- const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
+ const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2"/>`;
+ const txtAvatar = `<div class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2">${autoCompleteAvatar}</div>`;
const avatarIcon = member.mentionsDisabled
? spriteIcon('notifications-off', 's16 vertical-align-middle gl-ml-2')
: '';
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index b831ae7b9d6..80dd1d36734 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -21,7 +21,7 @@ export const createRouter = () => {
const router = new VueRouter({
routes,
mode: 'history',
- base: '/',
+ base: gon.relative_url_root || '/',
});
return router;
diff --git a/app/assets/javascripts/groups/service/archived_projects_service.js b/app/assets/javascripts/groups/service/archived_projects_service.js
index b9d48cc660e..7558b8d6713 100644
--- a/app/assets/javascripts/groups/service/archived_projects_service.js
+++ b/app/assets/javascripts/groups/service/archived_projects_service.js
@@ -32,7 +32,7 @@ export default class ArchivedProjectsService {
markdown_description: project.description_html,
visibility: project.visibility,
avatar_url: project.avatar_url,
- relative_path: `/${project.path_with_namespace}`,
+ relative_path: `${gon.relative_url_root}/${project.path_with_namespace}`,
edit_path: null,
leave_path: null,
can_edit: false,
diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss
index cf7dc79c5f5..e77e09bb85d 100644
--- a/app/assets/stylesheets/vendors/atwho.scss
+++ b/app/assets/stylesheets/vendors/atwho.scss
@@ -1,27 +1,24 @@
.atwho-view {
overflow-y: auto;
overflow-x: hidden;
- max-width: calc(100% - 6px);
+ min-width: $gl-new-dropdown-min-width;
+ max-width: $gl-new-dropdown-max-width;
+
@include gl-border-b-1;
@include gl-border-b-solid;
@include gl-border-b-gray-100;
@include gl-rounded-lg;
@include gl-shadow-md;
- .name,
- small.aliases,
- small.params {
- float: left;
- }
- small.aliases,
- small.params {
- padding: 2px 5px;
+ small {
+ @include gl-font-sm;
}
small.description {
- float: right;
- padding: 3px 5px;
+ display: block;
+ width: auto;
+ @include gl-mt-2;
}
.avatar-inline {
@@ -42,24 +39,22 @@
}
}
- ul > li {
- @include clearfix;
- white-space: nowrap;
- }
-
// TODO: fallback to global style
.atwho-view-ul {
- @include gl-p-2;
+ @include gl-py-2;
max-height: $gl-max-dropdown-max-height;
li {
- @include gl-px-3;
- padding-top: $gl-padding-6;
- padding-bottom: $gl-padding-6;
border: 0;
- @include gl-rounded-base;
+ padding: $gl-padding-6;
+
+ @include gl-my-2;
+ @include gl-mx-3;
+ @include gl-rounded-small;
+ @include gl-line-height-normal;
&.cur {
+ @include gl-focus;
background-color: $gray-darker;
color: $gl-text-color;
@@ -78,10 +73,6 @@
align-items: center;
}
- .center {
- line-height: 14px;
- }
-
strong {
color: $gl-text-color;
}
diff --git a/app/models/batched_git_ref_updates/deletion.rb b/app/models/batched_git_ref_updates/deletion.rb
index 61bba8aeba9..fdab19e6f78 100644
--- a/app/models/batched_git_ref_updates/deletion.rb
+++ b/app/models/batched_git_ref_updates/deletion.rb
@@ -15,7 +15,7 @@ module BatchedGitRefUpdates
# This column must be ignored otherwise Rails will cache the default value and `bulk_insert!` will start saving
# incorrect partition_id.
- ignore_column :partition_id, remove_with: '3000.0', remove_after: '3000-01-01'
+ ignore_column :partition_id, remove_never: true
belongs_to :project, inverse_of: :to_be_deleted_git_refs
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
index 02593d41bc2..794cb70c126 100644
--- a/app/models/ci/catalog/components_project.rb
+++ b/app/models/ci/catalog/components_project.rb
@@ -45,6 +45,8 @@ module Ci
end
def fetch_component(component_name)
+ return ComponentData.new unless component_name.index('/').nil?
+
path = simple_template_path(component_name)
content = fetch_content(path)
@@ -53,11 +55,6 @@ module Ci
content = fetch_content(path)
end
- if content.nil?
- path = legacy_template_path(component_name)
- content = fetch_content(path)
- end
-
ComponentData.new(content: content, path: path)
end
@@ -71,9 +68,6 @@ module Ci
# A simple template consists of a single file
def simple_template_path(component_name)
- # TODO: Extract this line and move to fetch_content once we remove legacy fetching
- return unless component_name.index('/').nil?
-
File.join(TEMPLATES_DIR, "#{component_name}.yml")
end
@@ -81,15 +75,8 @@ module Ci
# Given a path like "my-org/sub-group/the-project/templates/component"
# returns the entry point path: "templates/component/template.yml".
def complex_template_path(component_name)
- # TODO: Extract this line and move to fetch_content once we remove legacy fetching
- return unless component_name.index('/').nil?
-
File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE)
end
-
- def legacy_template_path(component_name)
- File.join(component_name, TEMPLATE_FILE).delete_prefix('/')
- end
end
end
end
diff --git a/app/models/concerns/ignorable_columns.rb b/app/models/concerns/ignorable_columns.rb
index 249d0b99494..fb114eed400 100644
--- a/app/models/concerns/ignorable_columns.rb
+++ b/app/models/concerns/ignorable_columns.rb
@@ -3,13 +3,11 @@
module IgnorableColumns
extend ActiveSupport::Concern
- ColumnIgnore = Struct.new(:remove_after, :remove_with) do
+ ColumnIgnore = Struct.new(:remove_after, :remove_with, :remove_never) do
def safe_to_remove?
- Date.today > remove_after
- end
+ return false if remove_never
- def to_s
- "(#{remove_after}, #{remove_with})"
+ Date.today > remove_after
end
end
@@ -17,14 +15,17 @@ module IgnorableColumns
# Ignore database columns in a model
#
# Indicate the earliest date and release we can stop ignoring the column with +remove_after+ (a date string) and +remove_with+ (a release)
- def ignore_columns(*columns, remove_after:, remove_with:)
- raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless Gitlab::Regex.utc_date_regex.match?(remove_after)
- raise ArgumentError, 'Please indicate in which release we can stop ignoring columns with remove_with, example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_with
+ def ignore_columns(*columns, remove_after: nil, remove_with: nil, remove_never: false)
+ unless remove_never
+ raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_after && Gitlab::Regex.utc_date_regex.match?(remove_after)
+ raise ArgumentError, 'Please indicate in which release we can stop ignoring columns with remove_with, example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_with
+ end
self.ignored_columns += columns.flatten # rubocop:disable Cop/IgnoredColumns
columns.flatten.each do |column|
- self.ignored_columns_details[column.to_sym] = ColumnIgnore.new(Date.parse(remove_after), remove_with)
+ remove_after_date = remove_after ? Date.parse(remove_after) : nil
+ self.ignored_columns_details[column.to_sym] = ColumnIgnore.new(remove_after_date, remove_with, remove_never)
end
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index 6af80686ec2..beafd9b7d4b 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -6,9 +6,13 @@ class LooseForeignKeys::DeletedRecord < Gitlab::Database::SharedModel
PARTITION_DURATION = 1.day
include PartitionedTable
+ include IgnorableColumns
self.primary_key = :id
- self.ignored_columns = %i[partition]
+
+ # This column must be ignored otherwise Rails will cache the default value and `bulk_insert!` will start saving
+ # incorrect partition.
+ ignore_column :partition, remove_never: true
partitioned_by :partition, strategy: :sliding_list,
next_partition_if: -> (active_partition) do
diff --git a/app/services/bulk_imports/batched_relation_export_service.rb b/app/services/bulk_imports/batched_relation_export_service.rb
index 9c3b237fad7..e239a6daa4c 100644
--- a/app/services/bulk_imports/batched_relation_export_service.rb
+++ b/app/services/bulk_imports/batched_relation_export_service.rb
@@ -65,19 +65,27 @@ module BulkImports
)
end
+ # rubocop:disable Cop/InBatches
+ # rubocop:disable CodeReuse/ActiveRecord
def enqueue_batch_exports
- resolved_relation.each_batch(of: BATCH_SIZE) do |batch, batch_number|
+ batch_number = 0
+
+ resolved_relation.in_batches(of: BATCH_SIZE) do |batch|
+ batch_number += 1
+
batch_id = find_or_create_batch(batch_number).id
- ids = batch.pluck(batch.model.primary_key) # rubocop:disable CodeReuse/ActiveRecord
+ ids = batch.pluck(batch.model.primary_key)
Gitlab::Cache::Import::Caching.set_add(self.class.cache_key(export.id, batch_id), ids, timeout: CACHE_DURATION)
RelationBatchExportWorker.perform_async(user.id, batch_id)
end
end
+ # rubocop:enable Cop/InBatches
def find_or_create_batch(batch_number)
- export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord
+ export.batches.find_or_create_by!(batch_number: batch_number)
end
+ # rubocop:enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb
index 4f09d47b530..f83c6e30cbb 100644
--- a/app/services/ci/components/fetch_service.rb
+++ b/app/services/ci/components/fetch_service.rb
@@ -24,7 +24,7 @@ module Ci
component_path = component_path_class.new(address: address)
result = component_path.fetch_content!(current_user: current_user)
- if result
+ if result&.content
ServiceResponse.success(payload: {
content: result.content,
path: result.path,
diff --git a/db/migrate/20231205201701_remove_geo_primary_deprecated_workers_job_instances.rb b/db/migrate/20231205201701_remove_geo_primary_deprecated_workers_job_instances.rb
new file mode 100644
index 00000000000..a0eab0f9fcb
--- /dev/null
+++ b/db/migrate/20231205201701_remove_geo_primary_deprecated_workers_job_instances.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class RemoveGeoPrimaryDeprecatedWorkersJobInstances < Gitlab::Database::Migration[2.2]
+ DEPRECATED_JOB_CLASSES = %w[
+ Geo::RepositoryVerification::Primary::BatchWorker
+ Geo::RepositoryVerification::Primary::ShardWorker
+ Geo::RepositoryVerification::Primary::SingleWorker
+ Geo::RepositoryVerification::Secondary::SingleWorker
+ Geo::Scheduler::Primary::PerShardSchedulerWorker
+ Geo::Scheduler::Primary::SchedulerWorker
+ ]
+
+ disable_ddl_transaction!
+
+ milestone '16.7'
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+end
diff --git a/db/schema_migrations/20231205201701 b/db/schema_migrations/20231205201701
new file mode 100644
index 00000000000..5288dfd03f3
--- /dev/null
+++ b/db/schema_migrations/20231205201701
@@ -0,0 +1 @@
+f0f89526db19621991482cc7fce3d95d8762d020d06ecd3bf96c587cca71d015 \ No newline at end of file
diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md
index 6c63cf2109a..6f1365561dc 100644
--- a/doc/ci/components/index.md
+++ b/doc/ci/components/index.md
@@ -92,65 +92,6 @@ In this example:
- The `all-scans` component configuration is defined in a single file.
- The `secret-detection` component configuration contains multiple files in a directory.
-#### Component configurations saved in any directory (deprecated)
-
-WARNING:
-Saving components through the following directory structure is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/415855) and should be avoided.
-
-Components configurations can be saved through the following directory structure, containing:
-
-- `template.yml`: The component configuration, one file per component. If there is
- only one component, this file can be in the root of the project. If there are multiple
- components, each file must be in a separate subdirectory.
-- `README.md`: A documentation file explaining the details of all the components in the repository.
-
-For example, if the project is on GitLab.com, named `my-project`, and in a personal
-namespace named `my-namespace`:
-
-- Containing a single component and a simple pipeline to test the component, then
- the file structure might be:
-
- ```plaintext
- ├── template.yml
- ├── README.md
- └── .gitlab-ci.yml
- ```
-
- This component is referenced with the path `gitlab.com/my-namespace/my-project@<version>`.
-
-- Containing one default component and multiple sub-components, then the file structure
- might be:
-
- ```plaintext
- ├── template.yml
- ├── README.md
- ├── .gitlab-ci.yml
- ├── unit/
- │ └── template.yml
- └── integration/
- └── template.yml
- ```
-
- These components are identified by these paths:
-
- - `gitlab.com/my-namespace/my-project`
- - `gitlab.com/my-namespace/my-project/unit`
- - `gitlab.com/my-namespace/my-project/integration`
-
-It is possible to have a components repository with no default component, by having
-no `template.yml` in the root directory.
-
-**Additional notes:**
-
-Nesting of components is not possible. For example:
-
-```plaintext
-├── unit/
-│ └── template.yml
-│ └── another_folder/
-│ └── nested_template.yml
-```
-
## Use a component
You can use a component in a CI/CD configuration with the `include: component` keyword.
diff --git a/doc/development/runner_fleet_dashboard.md b/doc/development/runner_fleet_dashboard.md
index 474952b9de6..70499e5a087 100644
--- a/doc/development/runner_fleet_dashboard.md
+++ b/doc/development/runner_fleet_dashboard.md
@@ -6,11 +6,11 @@ info: >-
this page, see
https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Runner Fleet Dashboard **(ULTIMATE BETA)**
+# Runner Fleet Dashboard **(ULTIMATE EXPERIMENT)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/424495) in GitLab 16.6 behind several [feature flags](#enable-feature-flags).
-This feature is in [BETA](../policy/experiment-beta-support.md).
+This feature is an [Experiment](../policy/experiment-beta-support.md).
To join the list of users testing this feature, contact us in
[epic 11180](https://gitlab.com/groups/gitlab-org/-/epics/11180).
@@ -48,7 +48,7 @@ for some customers to try this feature.
To test the Runner Fleet Dashboard as part of the early adopters program, you must:
-- Run GitLab 16.6 or above.
+- Run GitLab 16.7 or above.
- Have an [Ultimate license](https://about.gitlab.com/pricing/).
- Be able to run ClickHouse database. We recommend using [ClickHouse Cloud](https://clickhouse.cloud/).
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 816c9458201..bbc0203344c 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,6 +1,8 @@
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { EditorContent, Editor } from '@tiptap/vue-2';
import { nextTick } from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
@@ -16,11 +18,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
-jest.mock('~/emoji');
-
describe('ContentEditor', () => {
let wrapper;
let renderMarkdown;
+ let mock;
const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
@@ -32,6 +33,7 @@ describe('ContentEditor', () => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
+ markdownDocsPath: '/docs/markdown',
uploadsPath,
markdown,
autofocus,
@@ -49,9 +51,17 @@ describe('ContentEditor', () => {
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ // ignore /-/emojis requests
+ mock.onGet().reply(200, []);
+
renderMarkdown = jest.fn();
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('triggers initialized event', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index ee3ad59bf9a..b17a1b5fc11 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,5 +1,6 @@
-import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -14,11 +15,17 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
command: jest.fn(),
...propsData,
},
+ stubs: ['gl-emoji'],
}),
);
};
- const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' };
+ const exampleUser = {
+ username: 'root',
+ avatar_url: 'root_avatar.png',
+ type: 'User',
+ name: 'Administrator',
+ };
const exampleIssue = { iid: 123, title: 'Test Issue' };
const exampleMergeRequest = { iid: 224, title: 'Test MR' };
const exampleMilestone1 = { iid: 21, title: '13' };
@@ -61,11 +68,14 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
title: 'Project creation QueryRecorder logs',
};
const exampleEmoji = {
- c: 'people',
- e: '😃',
- d: 'smiling face with open mouth',
- u: '6.0',
- name: 'smiley',
+ emoji: {
+ c: 'people',
+ e: '😃',
+ d: 'smiling face with open mouth',
+ u: '6.0',
+ name: 'smiley',
+ },
+ fieldValue: 'smiley',
};
const insertedEmojiProps = {
@@ -95,6 +105,68 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(loading);
});
+ it('selects first item if query is not empty and items are available', async () => {
+ buildWrapper({
+ propsData: {
+ char: '@',
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'member',
+ },
+ items: [exampleUser],
+ query: 'ro',
+ },
+ });
+
+ await nextTick();
+
+ expect(
+ wrapper.findByTestId('content-editor-suggestions-dropdown').find('li').classes(),
+ ).toContain('focused');
+ });
+
+ describe('when query is defined', () => {
+ it.each`
+ nodeType | referenceType | reference | query | expectedHTML
+ ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'<strong class="gl-text-body!">r</strong>oot'}
+ ${'reference'} | ${'user'} | ${exampleUser} | ${'r'} | ${'Administ<strong class="gl-text-body!">r</strong>ator'}
+ ${'reference'} | ${'issue'} | ${exampleIssue} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> Issue'}
+ ${'reference'} | ${'issue'} | ${exampleIssue} | ${'12'} | ${'<strong class="gl-text-body!">12</strong>3'}
+ ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'test'} | ${'<strong class="gl-text-body!">Test</strong> MR'}
+ ${'reference'} | ${'merge_request'} | ${exampleMergeRequest} | ${'22'} | ${'<strong class="gl-text-body!">22</strong>4'}
+ ${'reference'} | ${'epic'} | ${exampleEpic} | ${'rem'} | ${'❓ <strong class="gl-text-body!">Rem</strong>ote Development | Solution validation'}
+ ${'reference'} | ${'epic'} | ${exampleEpic} | ${'88'} | ${'gitlab-org&amp;<strong class="gl-text-body!">88</strong>84'}
+ ${'reference'} | ${'milestone'} | ${exampleMilestone1} | ${'1'} | ${'<strong class="gl-text-body!">1</strong>3'}
+ ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'<strong class="gl-text-body!">due</strong>'}
+ ${'reference'} | ${'command'} | ${exampleCommand} | ${'due'} | ${'Set <strong class="gl-text-body!">due</strong> date'}
+ ${'reference'} | ${'label'} | ${exampleLabel1} | ${'c'} | ${'<strong class="gl-text-body!">C</strong>reate'}
+ ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'network'} | ${'System procs <strong class="gl-text-body!">network</strong> activity'}
+ ${'reference'} | ${'vulnerability'} | ${exampleVulnerability} | ${'85'} | ${'60<strong class="gl-text-body!">85</strong>0147'}
+ ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'project'} | ${'<strong class="gl-text-body!">Project</strong> creation QueryRecorder logs'}
+ ${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'242'} | ${'<strong class="gl-text-body!">242</strong>0859'}
+ ${'emoji'} | ${'emoji'} | ${exampleEmoji} | ${'sm'} | ${'<strong class="gl-text-body!">sm</strong>iley'}
+ `(
+ 'highlights query as bolded in $referenceType text',
+ ({ nodeType, referenceType, reference, query, expectedHTML }) => {
+ buildWrapper({
+ propsData: {
+ char: '@',
+ nodeType,
+ nodeProps: {
+ referenceType,
+ },
+ items: [reference],
+ query,
+ },
+ });
+
+ expect(wrapper.findByTestId('content-editor-suggestions-dropdown').html()).toContain(
+ expectedHTML,
+ );
+ },
+ );
+ });
+
describe('on item select', () => {
it.each`
nodeType | referenceType | char | reference | insertedText | insertedProps
@@ -146,7 +218,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
});
describe('rendering user references', () => {
- it('displays avatar labeled component', () => {
+ it('displays avatar component', () => {
buildWrapper({
propsData: {
char: '@',
@@ -157,13 +229,11 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- expect(wrapper.findComponent(GlAvatarLabeled).attributes()).toEqual(
- expect.objectContaining({
- label: exampleUser.username,
- shape: 'circle',
- src: exampleUser.avatar_url,
- }),
- );
+ expect(wrapper.findComponent(GlAvatar).attributes()).toMatchObject({
+ entityname: exampleUser.username,
+ shape: 'circle',
+ src: exampleUser.avatar_url,
+ });
});
describe.each`
@@ -273,20 +343,46 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
it('displays emoji', () => {
const testEmojis = [
{
- c: 'people',
- e: '😄',
- d: 'smiling face with open mouth and smiling eyes',
- u: '6.0',
- name: 'smile',
+ emoji: {
+ c: 'people',
+ e: '😄',
+ d: 'smiling face with open mouth and smiling eyes',
+ u: '6.0',
+ name: 'smile',
+ },
+ fieldValue: 'smile',
+ },
+ {
+ emoji: {
+ c: 'people',
+ e: '😸',
+ d: 'grinning cat face with smiling eyes',
+ u: '6.0',
+ name: 'smile_cat',
+ },
+ fieldValue: 'smile_cat',
+ },
+ {
+ emoji: {
+ c: 'people',
+ e: '😃',
+ d: 'smiling face with open mouth',
+ u: '6.0',
+ name: 'smiley',
+ },
+ fieldValue: 'smiley',
},
{
- c: 'people',
- e: '😸',
- d: 'grinning cat face with smiling eyes',
- u: '6.0',
- name: 'smile_cat',
+ emoji: {
+ c: 'custom',
+ e: null,
+ d: 'party-parrot',
+ u: 'custom',
+ name: 'party-parrot',
+ src: 'https://cultofthepartyparrot.com/parrots/hd/parrot.gif',
+ },
+ fieldValue: 'party-parrot',
},
- { c: 'people', e: '😃', d: 'smiling face with open mouth', u: '6.0', name: 'smiley' },
];
buildWrapper({
@@ -298,11 +394,41 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- testEmojis.forEach((testEmoji) => {
- expect(wrapper.text()).toContain(testEmoji.e);
- expect(wrapper.text()).toContain(testEmoji.d);
- expect(wrapper.text()).toContain(testEmoji.name);
- });
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(0).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smile"
+ data-unicode-version="6.0"
+ title="smiling face with open mouth and smiling eyes"
+ >
+ 😄
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(1).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smile_cat"
+ data-unicode-version="6.0"
+ title="grinning cat face with smiling eyes"
+ >
+ 😸
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(2).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-name="smiley"
+ data-unicode-version="6.0"
+ title="smiling face with open mouth"
+ >
+ 😃
+ </gl-emoji-stub>
+ `);
+ expect(wrapper.findAllComponents('gl-emoji-stub').at(3).html()).toMatchInlineSnapshot(`
+ <gl-emoji-stub
+ data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif"
+ data-name="party-parrot"
+ data-unicode-version="custom"
+ title="party-parrot"
+ />
+ `);
});
});
});
diff --git a/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap
new file mode 100644
index 00000000000..2d16c6b1a2f
--- /dev/null
+++ b/spec/frontend/content_editor/services/__snapshots__/data_source_factory_spec.js.snap
@@ -0,0 +1,256 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DataSourceFactory filters items based on command "/assign" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+ "jashkenas",
+ "twitter",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/assign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+ "jashkenas",
+ "twitter",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/label" for reference type "label" and command 1`] = `
+Array [
+ "Bronce",
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "Frontier",
+ "Grand Am",
+ "Onesync",
+ "Phone",
+ "Pynefunc",
+ "Trinix",
+ "Trounswood",
+ "group::knowledge",
+ "scoped label",
+ "type::one",
+ "type::two",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/reassign" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/reassign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "florida.schoen",
+ "root",
+ "all",
+ "errol",
+ "evelynn_olson",
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+ "Commit451",
+ "flightjs",
+ "gitlab-instance-ade037f9",
+ "gitlab-org",
+ "gnuwget",
+ "h5bp",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/relabel" for reference type "label" and command 1`] = `
+Array [
+ "Amsche",
+ "Brioffe",
+ "Bronce",
+ "Bryncefunc",
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "Frontier",
+ "Ghost",
+ "Grand Am",
+ "Onesync",
+ "Phone",
+ "Pynefunc",
+ "Trinix",
+ "Trounswood",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unassign" for reference type "user" and command 1`] = `
+Array [
+ "errol",
+ "evelynn_olson",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unassign_reviewer" for reference type "user" and command 1`] = `
+Array [
+ "lakeesha.batz",
+ "laurene_blick",
+ "myrtis",
+ "patty",
+]
+`;
+
+exports[`DataSourceFactory filters items based on command "/unlabel" for reference type "label" and command 1`] = `
+Array [
+ "Amsche",
+ "Brioffe",
+ "Bryncefunc",
+ "Ghost",
+]
+`;
+
+exports[`DataSourceFactory for reference type "command", searches for "re" correctly 1`] = `
+Array [
+ "relabel",
+ "remove_milestone",
+ "remove_estimate",
+ "remove_time_spent",
+ "relate",
+ "remove_epic",
+ "reassign",
+ "create_merge_request",
+]
+`;
+
+exports[`DataSourceFactory for reference type "epic", searches for "n" correctly 1`] = `
+Array [
+ "Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.",
+ "Minus eius ut omnis quos sunt dicta ex ipsum.",
+ "Quae nostrum possimus rerum aliquam pariatur a eos aut id.",
+ "Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.",
+ "Doloremque a quisquam qui culpa numquam doloribus similique iure enim.",
+]
+`;
+
+exports[`DataSourceFactory for reference type "issue", searches for "q" correctly 1`] = `
+Array [
+ "Quasi id et et nihil sint autem.",
+ "Eaque omnis eius quas necessitatibus hic ut et corrupti.",
+ "Aut quisquam magnam eos distinctio incidunt perferendis fugit.",
+ "Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.",
+ "Nesciunt quia molestiae in aliquam amet et dolorem.",
+ "Porro tempore qui qui culpa saepe et nam quos.",
+ "Sed sint a est consequatur quae quasi autem debitis alias.",
+ "Molestiae minima maxime optio nihil quam eveniet dolor.",
+ "Et laboriosam aut ratione voluptatem quasi recusandae.",
+ "Et molestiae delectus voluptates velit vero illo aut rerum quo et.",
+]
+`;
+
+exports[`DataSourceFactory for reference type "label", searches for "c" correctly 1`] = `
+Array [
+ "Contour",
+ "Corolla",
+ "Cygsync",
+ "scoped label",
+ "Amsche",
+ "Bronce",
+ "Bryncefunc",
+ "Onesync",
+ "Pynefunc",
+]
+`;
+
+exports[`DataSourceFactory for reference type "merge_request", searches for "n" correctly 1`] = `
+Array [
+ "Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.",
+ "Optio nemo qui dolorem sit ipsum qui saepe.",
+ "Draft: Alunny/publish lib",
+ "Draft: Fix event current target",
+ "Draft: Resolve \\"hgvbbvnnb\\"",
+ "Autem eaque et sed provident enim corrupti molestiae.",
+ "Always call registry's trigger method from withRegistration",
+]
+`;
+
+exports[`DataSourceFactory for reference type "milestone", searches for "16" correctly 1`] = `
+Array [
+ "16.7",
+ "16.8",
+ "16.9",
+ "16.10",
+ "16.11",
+ "16.0 (expired)",
+ "16.1 (expired)",
+ "16.2 (expired)",
+ "16.3 (expired)",
+ "16.4 (expired)",
+ "16.5 (expired)",
+ "16.6 (expired)",
+]
+`;
+
+exports[`DataSourceFactory for reference type "snippet", searches for "s" correctly 1`] = `
+Array [
+ "ss",
+ "test snippet",
+ "another test snippet",
+]
+`;
+
+exports[`DataSourceFactory for reference type "user", searches for "r" correctly 1`] = `
+Array [
+ "root",
+ "errol",
+ "lakeesha.batz",
+ "myrtis",
+ "florida.schoen",
+ "laurene_blick",
+ "all",
+ "twitter",
+ "gitlab-org",
+ "evelynn_olson",
+]
+`;
+
+exports[`DataSourceFactory for reference type "vulnerability", searches for "cross" correctly 1`] = `
+Array [
+ "Cross Site Scripting (Persistent)",
+ "Cross Site Scripting (Persistent)",
+ "Cross Site Scripting (Persistent)",
+]
+`;
diff --git a/spec/frontend/content_editor/services/autocomplete_mock_data.js b/spec/frontend/content_editor/services/autocomplete_mock_data.js
new file mode 100644
index 00000000000..c1bf2a6ae5b
--- /dev/null
+++ b/spec/frontend/content_editor/services/autocomplete_mock_data.js
@@ -0,0 +1,967 @@
+export const MOCK_MEMBERS = [
+ {
+ type: 'User',
+ username: 'florida.schoen',
+ name: 'Anglea Durgan',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/ac82b5615d3308ecbcacedad361af8e7?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'root',
+ name: 'Administrator',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ username: 'all',
+ name: 'All Project and Group Members',
+ count: 8,
+ },
+ {
+ type: 'User',
+ username: 'errol',
+ name: "Linnie O'Connell",
+ avatar_url:
+ 'https://www.gravatar.com/avatar/d3d9a468a9884eb217fad5ca5b2b9bd7?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'evelynn_olson',
+ name: 'Dimple Dare',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/bc1e51ee3512c2b4442f51732d655107?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'lakeesha.batz',
+ name: 'Larae Veum',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e5605cb9bbb1a28640d65f25f256e541?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'laurene_blick',
+ name: 'Evelina Murray',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/389768eef61b7b2d125c64ee01c240fb?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'myrtis',
+ name: 'Fernanda Adams',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/719d5569bd31d4a70e350b4205fa2cb5?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'User',
+ username: 'patty',
+ name: 'Emily Toy',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/dca2077b662338808459dc11e70d6688?s=80\u0026d=identicon',
+ availability: null,
+ },
+ {
+ type: 'Group',
+ username: 'Commit451',
+ name: 'Commit451',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'flightjs',
+ name: 'Flightjs',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gitlab-instance-ade037f9',
+ name: 'GitLab Instance',
+ avatar_url: null,
+ count: 1,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gitlab-org',
+ name: 'Gitlab Org',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'gnuwget',
+ name: 'Gnuwget',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'h5bp',
+ name: 'H5bp',
+ avatar_url: null,
+ count: 4,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'jashkenas',
+ name: 'Jashkenas',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+ {
+ type: 'Group',
+ username: 'twitter',
+ name: 'Twitter',
+ avatar_url: null,
+ count: 5,
+ mentionsDisabled: null,
+ },
+];
+
+export const MOCK_ASSIGNEES = MOCK_MEMBERS.filter(
+ ({ username }) => username === 'errol' || username === 'evelynn_olson',
+);
+
+export const MOCK_REVIEWERS = MOCK_MEMBERS.filter(
+ ({ username }) =>
+ username === 'lakeesha.batz' ||
+ username === 'laurene_blick' ||
+ username === 'myrtis' ||
+ username === 'patty',
+);
+
+export const MOCK_ISSUES = [
+ {
+ iid: 31,
+ title: 'rdfhdfj',
+ id: null,
+ },
+ {
+ iid: 30,
+ title: 'incident1',
+ id: null,
+ },
+ {
+ iid: 29,
+ title: 'example feature rollout',
+ id: null,
+ },
+ {
+ iid: 28,
+ title: 'sagasg',
+ id: null,
+ },
+ {
+ iid: 26,
+ title: 'Quasi id et et nihil sint autem.',
+ id: null,
+ },
+ {
+ iid: 25,
+ title: 'Dolorem quisquam cupiditate consequatur perspiciatis sequi eligendi ullam.',
+ id: null,
+ },
+ {
+ iid: 24,
+ title: 'Et molestiae delectus voluptates velit vero illo aut rerum quo et.',
+ id: null,
+ },
+ {
+ iid: 23,
+ title: 'Nesciunt quia molestiae in aliquam amet et dolorem.',
+ id: null,
+ },
+ {
+ iid: 22,
+ title: 'Sint asperiores unde vel autem delectus ullam dolor nihil et.',
+ id: null,
+ },
+ {
+ iid: 21,
+ title: 'Eaque omnis eius quas necessitatibus hic ut et corrupti.',
+ id: null,
+ },
+ {
+ iid: 20,
+ title: 'Porro tempore qui qui culpa saepe et nam quos.',
+ id: null,
+ },
+ {
+ iid: 19,
+ title: 'Molestiae minima maxime optio nihil quam eveniet dolor.',
+ id: null,
+ },
+ {
+ iid: 18,
+ title: 'Sed sint a est consequatur quae quasi autem debitis alias.',
+ id: null,
+ },
+ {
+ iid: 6,
+ title: 'Et laboriosam aut ratione voluptatem quasi recusandae.',
+ id: null,
+ },
+ {
+ iid: 2,
+ title: 'Aut quisquam magnam eos distinctio incidunt perferendis fugit.',
+ id: null,
+ },
+];
+
+export const MOCK_EPICS = [
+ {
+ iid: 6,
+ title: 'sgs',
+ reference: 'flightjs\u00266',
+ },
+ {
+ iid: 5,
+ title: 'Doloremque a quisquam qui culpa numquam doloribus similique iure enim.',
+ reference: 'flightjs\u00265',
+ },
+ {
+ iid: 4,
+ title: 'Minus eius ut omnis quos sunt dicta ex ipsum.',
+ reference: 'flightjs\u00264',
+ },
+ {
+ iid: 3,
+ title: 'Quae nostrum possimus rerum aliquam pariatur a eos aut id.',
+ reference: 'flightjs\u00263',
+ },
+ {
+ iid: 2,
+ title: 'Nobis quidem aspernatur reprehenderit sunt ut ipsum tempora sapiente sed iste.',
+ reference: 'flightjs\u00262',
+ },
+ {
+ iid: 1,
+ title: 'Dicta incidunt vel dignissimos sint sit esse est quibusdam quidem consequatur.',
+ reference: 'flightjs\u00261',
+ },
+];
+
+export const MOCK_MERGE_REQUESTS = [
+ {
+ iid: 12,
+ title: "Always call registry's trigger method from withRegistration",
+ id: null,
+ },
+ {
+ iid: 11,
+ title: 'Draft: Alunny/publish lib',
+ id: null,
+ },
+ {
+ iid: 10,
+ title: 'Draft: Resolve "hgvbbvnnb"',
+ id: null,
+ },
+ {
+ iid: 9,
+ title: 'Draft: Fix event current target',
+ id: null,
+ },
+ {
+ iid: 3,
+ title: 'Autem eaque et sed provident enim corrupti molestiae.',
+ id: null,
+ },
+ {
+ iid: 2,
+ title: 'Blanditiis maxime voluptatem ut pariatur vel autem vero non quod libero.',
+ id: null,
+ },
+ {
+ iid: 1,
+ title: 'Optio nemo qui dolorem sit ipsum qui saepe.',
+ id: null,
+ },
+];
+
+export const MOCK_SNIPPETS = [
+ {
+ id: 24,
+ title: 'ss',
+ },
+ {
+ id: 22,
+ title: 'another test snippet',
+ },
+ {
+ id: 21,
+ title: 'test snippet',
+ },
+];
+
+export const MOCK_LABELS = [
+ {
+ title: 'Amsche',
+ color: '#9964cf',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Brioffe',
+ color: '#203e13',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Bronce',
+ color: '#c0b7f2',
+ type: 'GroupLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Bryncefunc',
+ color: '#8baa5e',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Contour',
+ color: '#8cf3a3',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Corolla',
+ color: '#0384f3',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Cygsync',
+ color: '#1308c3',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Frontier',
+ color: '#85db43',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Ghost',
+ color: '#df1bc4',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ set: true,
+ },
+ {
+ title: 'Grand Am',
+ color: '#a1d7ee',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Onesync',
+ color: '#a73ba0',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Phone',
+ color: '#63dceb',
+ type: 'GroupLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'Pynefunc',
+ color: '#974b19',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Trinix',
+ color: '#2c894f',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'Trounswood',
+ color: '#ad0370',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'group::knowledge',
+ color: '#8fbc8f',
+ type: 'ProjectLabel',
+ textColor: '#1F1E24',
+ },
+ {
+ title: 'scoped label',
+ color: '#6699cc',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'type::one',
+ color: '#9400d3',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+ {
+ title: 'type::two',
+ color: '#013220',
+ type: 'ProjectLabel',
+ textColor: '#FFFFFF',
+ },
+];
+
+export const MOCK_MILESTONES = [
+ {
+ iid: 65,
+ title: '15.0',
+ due_date: '2022-05-17',
+ id: null,
+ },
+ {
+ iid: 73,
+ title: '15.1',
+ due_date: '2022-06-17',
+ id: null,
+ },
+ {
+ iid: 74,
+ title: '15.2',
+ due_date: '2022-07-17',
+ id: null,
+ },
+ {
+ iid: 75,
+ title: '15.3',
+ due_date: '2022-08-17',
+ id: null,
+ },
+ {
+ iid: 76,
+ title: '15.4',
+ due_date: '2022-09-17',
+ id: null,
+ },
+ {
+ iid: 77,
+ title: '15.5',
+ due_date: '2022-10-17',
+ id: null,
+ },
+ {
+ iid: 81,
+ title: '15.6',
+ due_date: '2022-11-17',
+ id: null,
+ },
+ {
+ iid: 82,
+ title: '15.7',
+ due_date: '2022-12-17',
+ id: null,
+ },
+ {
+ iid: 83,
+ title: '15.8',
+ due_date: '2023-01-17',
+ id: null,
+ },
+ {
+ iid: 84,
+ title: '15.9',
+ due_date: '2023-02-17',
+ id: null,
+ },
+ {
+ iid: 85,
+ title: '15.10',
+ due_date: '2023-03-17',
+ id: null,
+ },
+ {
+ iid: 86,
+ title: '15.11',
+ due_date: '2023-04-17',
+ id: null,
+ },
+ {
+ iid: 80,
+ title: '16.0',
+ due_date: '2023-05-17',
+ id: null,
+ },
+ {
+ iid: 88,
+ title: '16.1',
+ due_date: '2023-06-17',
+ id: null,
+ },
+ {
+ iid: 89,
+ title: '16.2',
+ due_date: '2023-07-17',
+ id: null,
+ },
+ {
+ iid: 90,
+ title: '16.3',
+ due_date: '2023-08-17',
+ id: null,
+ },
+ {
+ iid: 91,
+ title: '16.4',
+ due_date: '2023-09-17',
+ id: null,
+ },
+ {
+ iid: 92,
+ title: '16.5',
+ due_date: '2023-10-17',
+ id: null,
+ },
+ {
+ iid: 93,
+ title: '16.6',
+ due_date: '2023-11-10',
+ id: null,
+ },
+ {
+ iid: 95,
+ title: '16.7',
+ due_date: '2023-12-15',
+ id: null,
+ },
+ {
+ iid: 94,
+ title: '16.8',
+ due_date: '2024-01-12',
+ id: null,
+ },
+ {
+ iid: 96,
+ title: '16.9',
+ due_date: '2024-02-09',
+ id: null,
+ },
+ {
+ iid: 97,
+ title: '16.10',
+ due_date: '2024-03-15',
+ id: null,
+ },
+ {
+ iid: 98,
+ title: '16.11',
+ due_date: '2024-04-12',
+ id: null,
+ },
+ {
+ iid: 87,
+ title: '17.0',
+ due_date: '2024-05-10',
+ id: null,
+ },
+ {
+ iid: 48,
+ title: 'Next 1-3 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 24,
+ title: 'Awaiting further demand',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 14,
+ title: 'Backlog',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 11,
+ title: 'Next 4-7 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 10,
+ title: 'Next 3-4 releases',
+ due_date: null,
+ id: null,
+ },
+ {
+ iid: 6,
+ title: 'Next 7-13 releases',
+ due_date: null,
+ id: null,
+ },
+];
+
+export const MOCK_VULNERABILITIES = [
+ {
+ id: 99499903,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+ {
+ id: 99495085,
+ title: 'Possible SQL injection',
+ },
+ {
+ id: 99490610,
+ title: 'GitLab Runner Authentication Token',
+ },
+ {
+ id: 99288920,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+ {
+ id: 99258720,
+ title: 'Cross Site Scripting (Persistent)',
+ },
+];
+
+export const MOCK_COMMANDS = [
+ {
+ name: 'due',
+ aliases: [],
+ description: 'Set due date',
+ warning: '',
+ icon: '',
+ params: ['\u003cin 2 days | this Friday | December 31st\u003e'],
+ },
+ {
+ name: 'duplicate',
+ aliases: [],
+ description: 'Mark this issue as a duplicate of another issue',
+ warning: '',
+ icon: '',
+ params: ['#issue'],
+ },
+ {
+ name: 'clone',
+ aliases: [],
+ description: 'Clone this issue',
+ warning: '',
+ icon: '',
+ params: ['path/to/project [--with_notes]'],
+ },
+ {
+ name: 'move',
+ aliases: [],
+ description: 'Move this issue to another project.',
+ warning: '',
+ icon: '',
+ params: ['path/to/project'],
+ },
+ {
+ name: 'create_merge_request',
+ aliases: [],
+ description: 'Create a merge request',
+ warning: '',
+ icon: '',
+ params: ['\u003cbranch name\u003e'],
+ },
+ {
+ name: 'zoom',
+ aliases: [],
+ description: 'Add Zoom meeting',
+ warning: '',
+ icon: '',
+ params: ['\u003cZoom URL\u003e'],
+ },
+ {
+ name: 'promote_to_incident',
+ aliases: [],
+ description: 'Promote issue to incident',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'close',
+ aliases: [],
+ description: 'Close this issue',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'title',
+ aliases: [],
+ description: 'Change title',
+ warning: '',
+ icon: '',
+ params: ['\u003cNew title\u003e'],
+ },
+ {
+ name: 'label',
+ aliases: ['labels'],
+ description: 'Add labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'unlabel',
+ aliases: ['remove_label'],
+ description: 'Remove all or specific labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'relabel',
+ aliases: [],
+ description: 'Replace all labels',
+ warning: '',
+ icon: '',
+ params: ['~label1 ~"label 2"'],
+ },
+ {
+ name: 'todo',
+ aliases: [],
+ description: 'Add a to do',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'unsubscribe',
+ aliases: [],
+ description: 'Unsubscribe',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'award',
+ aliases: [],
+ description: 'Toggle emoji award',
+ warning: '',
+ icon: '',
+ params: [':emoji:'],
+ },
+ {
+ name: 'shrug',
+ aliases: [],
+ description: 'Append the comment with ¯\\_(ツ)_/¯',
+ warning: '',
+ icon: '',
+ params: ['\u003cComment\u003e'],
+ },
+ {
+ name: 'tableflip',
+ aliases: [],
+ description: 'Append the comment with (╯°□°)╯︵ ┻━┻',
+ warning: '',
+ icon: '',
+ params: ['\u003cComment\u003e'],
+ },
+ {
+ name: 'confidential',
+ aliases: [],
+ description: 'Make issue confidential',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'assign',
+ aliases: [],
+ description: 'Assign',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'unassign',
+ aliases: [],
+ description: 'Remove all or specific assignees',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'milestone',
+ aliases: [],
+ description: 'Set milestone',
+ warning: '',
+ icon: '',
+ params: ['%"milestone"'],
+ },
+ {
+ name: 'remove_milestone',
+ aliases: [],
+ description: 'Remove milestone',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'copy_metadata',
+ aliases: [],
+ description: 'Copy labels and milestone from other issue or merge request in this project',
+ warning: '',
+ icon: '',
+ params: ['#issue | !merge_request'],
+ },
+ {
+ name: 'estimate',
+ aliases: ['estimate_time'],
+ description: 'Set time estimate',
+ warning: '',
+ icon: '',
+ params: ['\u003c1w 3d 2h 14m\u003e'],
+ },
+ {
+ name: 'spend',
+ aliases: ['spent', 'spend_time'],
+ description: 'Add or subtract spent time',
+ warning: '',
+ icon: '',
+ params: ['\u003ctime(1h30m | -1h30m)\u003e \u003cdate(YYYY-MM-DD)\u003e'],
+ },
+ {
+ name: 'remove_estimate',
+ aliases: ['remove_time_estimate'],
+ description: 'Remove time estimate',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'remove_time_spent',
+ aliases: [],
+ description: 'Remove spent time',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'lock',
+ aliases: [],
+ description: 'Lock the discussion',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'cc',
+ aliases: [],
+ description: 'CC',
+ warning: '',
+ icon: '',
+ params: ['@user'],
+ },
+ {
+ name: 'relate',
+ aliases: [],
+ description: 'Mark this issue as related to another issue',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'unlink',
+ aliases: [],
+ description: 'Remove link with another issue',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'epic',
+ aliases: [],
+ description: 'Add to epic',
+ warning: '',
+ icon: '',
+ params: ['\u003c\u0026epic | group\u0026epic | Epic URL\u003e'],
+ },
+ {
+ name: 'remove_epic',
+ aliases: [],
+ description: 'Remove from epic',
+ warning: '',
+ icon: '',
+ params: [],
+ },
+ {
+ name: 'promote',
+ aliases: [],
+ description: 'Promote issue to an epic',
+ warning: '',
+ icon: 'confidential',
+ params: [],
+ },
+ {
+ name: 'iteration',
+ aliases: [],
+ description: 'Set iteration',
+ warning: '',
+ icon: '',
+ params: ['*iteration:"iteration name" | *iteration:\u003cID\u003e'],
+ },
+ {
+ name: 'health_status',
+ aliases: [],
+ description: 'Set health status',
+ warning: '',
+ icon: '',
+ params: ['\u003con_track|needs_attention|at_risk\u003e'],
+ },
+ {
+ name: 'reassign',
+ aliases: [],
+ description: 'Change assignees',
+ warning: '',
+ icon: '',
+ params: ['@user1 @user2'],
+ },
+ {
+ name: 'weight',
+ aliases: [],
+ description: 'Set weight',
+ warning: '',
+ icon: '',
+ params: ['0, 1, 2, …'],
+ },
+ {
+ name: 'blocks',
+ aliases: [],
+ description: 'Specifies that this issue blocks other issues',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+ {
+ name: 'blocked_by',
+ aliases: [],
+ description: 'Mark this issue as blocked by other issues',
+ warning: '',
+ icon: '',
+ params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
+ },
+];
diff --git a/spec/frontend/content_editor/services/data_source_factory_spec.js b/spec/frontend/content_editor/services/data_source_factory_spec.js
new file mode 100644
index 00000000000..d540f11711d
--- /dev/null
+++ b/spec/frontend/content_editor/services/data_source_factory_spec.js
@@ -0,0 +1,202 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import DataSourceFactory, {
+ defaultSorter,
+ customSorter,
+ createDataSource,
+} from '~/content_editor/services/data_source_factory';
+import {
+ MOCK_MEMBERS,
+ MOCK_COMMANDS,
+ MOCK_EPICS,
+ MOCK_ISSUES,
+ MOCK_LABELS,
+ MOCK_MILESTONES,
+ MOCK_SNIPPETS,
+ MOCK_VULNERABILITIES,
+ MOCK_MERGE_REQUESTS,
+ MOCK_ASSIGNEES,
+ MOCK_REVIEWERS,
+} from './autocomplete_mock_data';
+
+jest.mock('~/emoji');
+
+describe('defaultSorter', () => {
+ it('returns items as is if query is empty', () => {
+ const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }];
+ const sorter = defaultSorter(['name']);
+ expect(sorter(items, '')).toEqual(items);
+ });
+
+ it('sorts items based on query match', () => {
+ const items = [{ name: 'abc' }, { name: 'bcd' }, { name: 'cde' }];
+ const sorter = defaultSorter(['name']);
+ expect(sorter(items, 'b')).toEqual([{ name: 'bcd' }, { name: 'abc' }, { name: 'cde' }]);
+ });
+
+ it('sorts items based on query match in multiple fields', () => {
+ const items = [
+ { name: 'wabc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ];
+ const sorter = defaultSorter(['name', 'description']);
+ expect(sorter(items, 'w')).toEqual([
+ { name: 'wabc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ]);
+ });
+});
+
+describe('customSorter', () => {
+ it('sorts items based on custom sorter function', () => {
+ const items = [3, 1, 2];
+ const sorter = customSorter((a, b) => a - b);
+ expect(sorter(items)).toEqual([1, 2, 3]);
+ });
+});
+
+describe('createDataSource', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('fetches data from source and filters based on query', async () => {
+ const data = [
+ { name: 'abc', description: 'xyz' },
+ { name: 'bcd', description: 'wxy' },
+ { name: 'cde', description: 'vwx' },
+ ];
+ mock.onGet('/source').reply(200, data);
+
+ const dataSource = createDataSource({
+ source: '/source',
+ searchFields: ['name', 'description'],
+ });
+
+ const results = await dataSource.search('b');
+ expect(results).toEqual([
+ { name: 'bcd', description: 'wxy' },
+ { name: 'abc', description: 'xyz' },
+ ]);
+ });
+
+ it('handles source fetch errors', async () => {
+ mock.onGet('/source').reply(500);
+
+ const dataSource = createDataSource({
+ source: '/source',
+ searchFields: ['name', 'description'],
+ sorter: (items) => items,
+ });
+
+ const results = await dataSource.search('b');
+ expect(results).toEqual([]);
+ });
+});
+
+describe('DataSourceFactory', () => {
+ let mock;
+ let autocompleteHelper;
+ let dateNowOld;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ const dataSourceUrls = {
+ members: '/members',
+ issues: '/issues',
+ snippets: '/snippets',
+ labels: '/labels',
+ epics: '/epics',
+ milestones: '/milestones',
+ mergeRequests: '/mergeRequests',
+ vulnerabilities: '/vulnerabilities',
+ commands: '/commands',
+ };
+
+ mock.onGet('/members').reply(200, MOCK_MEMBERS);
+ mock.onGet('/issues').reply(200, MOCK_ISSUES);
+ mock.onGet('/snippets').reply(200, MOCK_SNIPPETS);
+ mock.onGet('/labels').reply(200, MOCK_LABELS);
+ mock.onGet('/epics').reply(200, MOCK_EPICS);
+ mock.onGet('/milestones').reply(200, MOCK_MILESTONES);
+ mock.onGet('/mergeRequests').reply(200, MOCK_MERGE_REQUESTS);
+ mock.onGet('/vulnerabilities').reply(200, MOCK_VULNERABILITIES);
+ mock.onGet('/commands').reply(200, MOCK_COMMANDS);
+
+ const sidebarMediator = {
+ store: {
+ assignees: MOCK_ASSIGNEES,
+ reviewers: MOCK_REVIEWERS,
+ },
+ };
+
+ autocompleteHelper = new DataSourceFactory({
+ dataSourceUrls,
+ sidebarMediator,
+ });
+
+ dateNowOld = Date.now();
+
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2023-11-14').getTime());
+ });
+
+ afterEach(() => {
+ mock.restore();
+
+ jest.spyOn(Date, 'now').mockImplementation(() => dateNowOld);
+ });
+
+ it.each`
+ referenceType | query
+ ${'user'} | ${'r'}
+ ${'issue'} | ${'q'}
+ ${'snippet'} | ${'s'}
+ ${'label'} | ${'c'}
+ ${'epic'} | ${'n'}
+ ${'milestone'} | ${'16'}
+ ${'merge_request'} | ${'n'}
+ ${'vulnerability'} | ${'cross'}
+ ${'command'} | ${'re'}
+ `(
+ 'for reference type "$referenceType", searches for "$query" correctly',
+ async ({ referenceType, query }) => {
+ const dataSource = autocompleteHelper.getDataSource(referenceType);
+ const results = await dataSource.search(query);
+
+ expect(
+ results.map(({ title, name, username }) => username || name || title),
+ ).toMatchSnapshot();
+ },
+ );
+
+ it.each`
+ referenceType | command
+ ${'label'} | ${'/label'}
+ ${'label'} | ${'/unlabel'}
+ ${'label'} | ${'/relabel'}
+ ${'user'} | ${'/assign'}
+ ${'user'} | ${'/reassign'}
+ ${'user'} | ${'/unassign'}
+ ${'user'} | ${'/assign_reviewer'}
+ ${'user'} | ${'/unassign_reviewer'}
+ ${'user'} | ${'/reassign_reviewer'}
+ `(
+ 'filters items based on command "$command" for reference type "$referenceType" and command',
+ async ({ referenceType, command }) => {
+ const dataSource = autocompleteHelper.getDataSource(referenceType, { command });
+ const results = await dataSource.search();
+
+ expect(
+ results.map(({ username, name, title }) => username || name || title),
+ ).toMatchSnapshot();
+ },
+ );
+});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 6cea75036bc..2d7841771a1 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -547,7 +547,7 @@ describe('GfmAutoComplete', () => {
expect(membersBeforeSave([{ ...mockGroup, avatar_url: null }])).toEqual([
{
username: 'my-group',
- avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>',
+ avatarTag: '<div class="avatar rect-avatar avatar-inline s24 gl-mr-2">M</div>',
title: 'My Group (2)',
search: 'MyGroup my-group',
icon: '',
@@ -560,7 +560,7 @@ describe('GfmAutoComplete', () => {
{
username: 'my-group',
avatarTag:
- '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
+ '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline s24 gl-mr-2"/>',
title: 'My Group (2)',
search: 'MyGroup my-group',
icon: '',
@@ -573,7 +573,7 @@ describe('GfmAutoComplete', () => {
{
username: 'my-group',
avatarTag:
- '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
+ '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline s24 gl-mr-2"/>',
title: 'My Group',
search: 'MyGroup my-group',
icon:
@@ -591,7 +591,7 @@ describe('GfmAutoComplete', () => {
{
username: 'my-user',
avatarTag:
- '<img src="./users.jpg" alt="my-user" class="avatar avatar-inline center s26"/>',
+ '<img src="./users.jpg" alt="my-user" class="avatar avatar-inline s24 gl-mr-2"/>',
title: 'My User',
search: 'MyUser my-user',
icon: '',
diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js
index 6bc46e4799c..988fb5553ba 100644
--- a/spec/frontend/groups/service/archived_projects_service_spec.js
+++ b/spec/frontend/groups/service/archived_projects_service_spec.js
@@ -30,7 +30,7 @@ describe('ArchivedProjectsService', () => {
markdown_description: project.description_html,
visibility: project.visibility,
avatar_url: project.avatar_url,
- relative_path: `/${project.path_with_namespace}`,
+ relative_path: `${gon.relative_url_root}/${project.path_with_namespace}`,
edit_path: null,
leave_path: null,
can_edit: false,
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
index d48ad85de74..6bc6f04309d 100644
--- a/spec/lib/gitlab/ci/components/instance_path_spec.rb
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -181,88 +181,5 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
end
end
end
-
- # All the following tests are for deprecated code and will be removed
- # in https://gitlab.com/gitlab-org/gitlab/-/issues/415855
- context 'when the project does not contain a templates directory' do
- let(:project_path) { project.full_path }
- let(:address) { "acme.com/#{project_path}/component@#{version}" }
-
- let_it_be(:project) do
- create(
- :project, :custom_repo,
- files: {
- 'component/template.yml' => 'image: alpine'
- }
- )
- end
-
- before do
- project.add_developer(user)
- end
-
- it 'fetches the component content', :aggregate_failures do
- result = path.fetch_content!(current_user: user)
- expect(result.content).to eq('image: alpine')
- expect(result.path).to eq('component/template.yml')
- expect(path.host).to eq(current_host)
- expect(path.project).to eq(project)
- expect(path.sha).to eq(project.commit('master').id)
- end
-
- context 'when project path is nested under a subgroup' do
- let_it_be(:group) { create(:group, :nested) }
- let_it_be(:project) do
- create(
- :project, :custom_repo,
- files: {
- 'component/template.yml' => 'image: alpine'
- },
- group: group
- )
- end
-
- it 'fetches the component content', :aggregate_failures do
- result = path.fetch_content!(current_user: user)
- expect(result.content).to eq('image: alpine')
- expect(result.path).to eq('component/template.yml')
- expect(path.host).to eq(current_host)
- expect(path.project).to eq(project)
- expect(path.sha).to eq(project.commit('master').id)
- end
- end
-
- context 'when current GitLab instance is installed on a relative URL' do
- let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" }
- let(:current_host) { 'acme.com/gitlab/' }
-
- it 'fetches the component content', :aggregate_failures do
- result = path.fetch_content!(current_user: user)
- expect(result.content).to eq('image: alpine')
- expect(result.path).to eq('component/template.yml')
- expect(path.host).to eq(current_host)
- expect(path.project).to eq(project)
- expect(path.sha).to eq(project.commit('master').id)
- end
- end
-
- context 'when version does not exist' do
- let(:version) { 'non-existent' }
-
- it 'returns nil', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to be_nil
- expect(path.host).to eq(current_host)
- expect(path.project).to eq(project)
- expect(path.sha).to be_nil
- end
- end
-
- context 'when user does not have permissions' do
- it 'raises an error when fetching the content' do
- expect { path.fetch_content!(current_user: build(:user)) }
- .to raise_error(Gitlab::Access::AccessDeniedError)
- end
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 68cdf56f198..4684495fa26 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -410,7 +410,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
let(:other_project_files) do
{
- '/component-x/template.yml' => <<~YAML
+ '/templates/component-x/template.yml' => <<~YAML
component_x_job:
script: echo Component X
YAML
diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
index fa7645d581c..56899924b60 100644
--- a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
+++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
@@ -53,11 +53,11 @@ RSpec.describe Gitlab::Database::ObsoleteIgnoredColumns, feature_category: :data
expect(subject.execute).to eq(
[
['Testing::A', {
- 'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0'),
- 'also_unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-02-01'), '12.1')
+ 'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0', false),
+ 'also_unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-02-01'), '12.1', false)
}],
['Testing::B', {
- 'other' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0')
+ 'other' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0', false)
}]
])
end
diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb
index 79e1a113e47..5f739c244a5 100644
--- a/spec/models/ci/catalog/components_project_spec.rb
+++ b/spec/models/ci/catalog/components_project_spec.rb
@@ -97,6 +97,7 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo
'dast' | 'image: alpine_2' | 'templates/dast/template.yml'
'template' | 'image: alpine_3' | 'templates/template.yml'
'blank-yaml' | '' | 'templates/blank-yaml.yml'
+ 'non/exist' | nil | nil
end
with_them do
diff --git a/spec/models/concerns/ignorable_columns_spec.rb b/spec/models/concerns/ignorable_columns_spec.rb
index 339f06f9c45..44dc0bb6da6 100644
--- a/spec/models/concerns/ignorable_columns_spec.rb
+++ b/spec/models/concerns/ignorable_columns_spec.rb
@@ -27,6 +27,12 @@ RSpec.describe IgnorableColumns do
expect { subject.ignore_columns(:name, remove_after: nil, remove_with: 12.6) }.to raise_error(ArgumentError, /Please indicate/)
end
+ it 'allows setting remove_never: true and not setting other remove options' do
+ expect do
+ subject.ignore_columns(%i[name created_at], remove_never: true)
+ end.to change { subject.ignored_columns }.from([]).to(%w[name created_at])
+ end
+
it 'requires remove_after attribute to be set' do
expect { subject.ignore_columns(:name, remove_after: "not a date", remove_with: 12.6) }.to raise_error(ArgumentError, /Please indicate/)
end
@@ -73,9 +79,11 @@ RSpec.describe IgnorableColumns do
end
describe IgnorableColumns::ColumnIgnore do
- subject { described_class.new(remove_after, remove_with) }
+ subject { described_class.new(remove_after, remove_with, remove_never) }
+ let(:remove_after) { nil }
let(:remove_with) { double }
+ let(:remove_never) { false }
describe '#safe_to_remove?' do
context 'after remove_after date has passed' do
@@ -93,6 +101,14 @@ RSpec.describe IgnorableColumns do
expect(subject.safe_to_remove?).to be_falsey
end
end
+
+ context 'with remove_never: true' do
+ let(:remove_never) { true }
+
+ it 'is false' do
+ expect(subject.safe_to_remove?).to be_falsey
+ end
+ end
end
end
end
diff --git a/spec/policies/protected_branch_access_policy_spec.rb b/spec/policies/protected_branch_access_policy_spec.rb
index 68a130d666a..6725cde0cb1 100644
--- a/spec/policies/protected_branch_access_policy_spec.rb
+++ b/spec/policies/protected_branch_access_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranchAccessPolicy do
+RSpec.describe ProtectedBranchAccessPolicy, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:protected_branch_access) { create(:protected_branch_merge_access_level) }
let(:project) { protected_branch_access.protected_branch.project }
@@ -14,9 +14,7 @@ RSpec.describe ProtectedBranchAccessPolicy do
project.add_maintainer(user)
end
- it 'can be read' do
- is_expected.to be_allowed(:read_protected_branch)
- end
+ it_behaves_like 'allows protected branch crud'
end
context 'as guests' do
@@ -24,8 +22,6 @@ RSpec.describe ProtectedBranchAccessPolicy do
project.add_guest(user)
end
- it 'can not be read' do
- is_expected.to be_disallowed(:read_protected_branch)
- end
+ it_behaves_like 'disallows protected branch crud'
end
end
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
index d676de14735..d2040a0d334 100644
--- a/spec/policies/protected_branch_policy_spec.rb
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranchPolicy do
+RSpec.describe ProtectedBranchPolicy, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:name) { 'feature' }
let(:protected_branch) { create(:protected_branch, name: name) }
@@ -10,47 +10,27 @@ RSpec.describe ProtectedBranchPolicy do
subject { described_class.new(user, protected_branch) }
- context 'as maintainers' do
+ context 'as a maintainer' do
before do
project.add_maintainer(user)
end
- it 'can be read' do
- is_expected.to be_allowed(:read_protected_branch)
- end
-
- it 'can be created' do
- is_expected.to be_allowed(:create_protected_branch)
- end
+ it_behaves_like 'allows protected branch crud'
+ end
- it 'can be updated' do
- is_expected.to be_allowed(:update_protected_branch)
+ context 'as a developer' do
+ before do
+ project.add_developer(user)
end
- it 'can be destroyed' do
- is_expected.to be_allowed(:destroy_protected_branch)
- end
+ it_behaves_like 'disallows protected branch crud'
end
- context 'as guests' do
+ context 'as a guest' do
before do
project.add_guest(user)
end
- it 'can be read' do
- is_expected.to be_disallowed(:read_protected_branch)
- end
-
- it 'can be created' do
- is_expected.to be_disallowed(:create_protected_branch)
- end
-
- it 'can be updated' do
- is_expected.to be_disallowed(:update_protected_branch)
- end
-
- it 'cannot be destroyed' do
- is_expected.to be_disallowed(:destroy_protected_branch)
- end
+ it_behaves_like 'disallows protected branch crud'
end
end
diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb
index 21b7df19f4a..82be795e997 100644
--- a/spec/services/ci/components/fetch_service_spec.rb
+++ b/spec/services/ci/components/fetch_service_spec.rb
@@ -21,14 +21,15 @@ RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composi
project = create(
:project, :custom_repo,
files: {
- 'template.yml' => content,
- 'my-component/template.yml' => content,
- 'my-dir/my-component/template.yml' => content
+ 'templates/first-component.yml' => content,
+ 'templates/second-component/template.yml' => content
}
)
project.repository.add_tag(project.creator, 'v0.1', project.repository.commit.sha)
+ create(:release, project: project, tag: 'v0.1', sha: project.repository.commit.sha)
+
project
end
@@ -119,32 +120,27 @@ RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composi
context 'when address points to an external component' do
let(:address) { "#{current_host}/#{component_path}@#{version}" }
- context 'when component path is the full path to a project' do
- let(:component_path) { project.full_path }
- let(:component_yaml_path) { 'template.yml' }
+ context 'when component path points to a template file in a project' do
+ let(:component_path) { "#{project.full_path}/first-component" }
it_behaves_like 'an external component'
end
- context 'when component path points to a directory in a project' do
- let(:component_path) { "#{project.full_path}/my-component" }
- let(:component_yaml_path) { 'my-component/template.yml' }
+ context 'when component path points to a template directory in a project' do
+ let(:component_path) { "#{project.full_path}/second-component" }
it_behaves_like 'an external component'
end
- context 'when component path points to a nested directory in a project' do
- let(:component_path) { "#{project.full_path}/my-dir/my-component" }
- let(:component_yaml_path) { 'my-dir/my-component/template.yml' }
+ context 'when the project exists but the component does not' do
+ let(:component_path) { "#{project.full_path}/unknown-component" }
+ let(:version) { '~latest' }
- it_behaves_like 'an external component'
+ it 'returns a content not found error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
end
end
end
-
- def stub_project_blob(ref, path, content)
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content)
- end
- end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 19e55c22df8..7dea50ba270 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1757,7 +1757,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:sha) do
components_project.repository.create_file(
user,
- 'my-component/template.yml',
+ 'templates/my-component/template.yml',
template,
message: 'Add my first CI component',
branch_name: 'master'
@@ -1894,7 +1894,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
let(:sha) do
components_project.repository.create_file(
user,
- 'my-component/template.yml',
+ 'templates/my-component/template.yml',
template,
message: 'Add my first CI component',
branch_name: 'master'
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 6bfb60c3f34..0a71658c9e7 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -570,7 +570,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
type_in_content_editor '/assign'
expect(find(suggestions_dropdown)).to have_text('/assign')
- send_keys [:arrow_down, :enter]
+ send_keys :enter
expect(page).to have_text('/assign @')
end
@@ -579,7 +579,7 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
type_in_content_editor '/label'
expect(find(suggestions_dropdown)).to have_text('/label')
- send_keys [:arrow_down, :enter]
+ send_keys :enter
expect(page).to have_text('/label ~')
end
@@ -588,10 +588,23 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
type_in_content_editor '/milestone'
expect(find(suggestions_dropdown)).to have_text('/milestone')
- send_keys [:arrow_down, :enter]
+ send_keys :enter
expect(page).to have_text('/milestone %')
end
+
+ it 'scrolls selected item into view when navigating with keyboard' do
+ type_in_content_editor '/'
+
+ expect(find(suggestions_dropdown)).to have_text('label')
+
+ expect(dropdown_scroll_top).to be 0
+
+ send_keys :arrow_up
+
+ expect(dropdown_scroll_top).to be > 100
+ end
+
end
it 'shows suggestions for members with descriptions' do
@@ -603,7 +616,18 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
type_in_content_editor 'bc'
- send_keys [:arrow_down, :enter]
+ send_keys :enter
+
+ expect(page).not_to have_css(suggestions_dropdown)
+ expect(page).to have_text('@abc123')
+ end
+
+ it 'allows selecting element with tab key' do
+ type_in_content_editor '@abc'
+
+ expect(find(suggestions_dropdown)).to have_text('abc123')
+
+ send_keys :tab
expect(page).not_to have_css(suggestions_dropdown)
expect(page).to have_text('@abc123')
@@ -701,11 +725,11 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
expect(find(suggestions_dropdown)).to have_text('😃 smiley')
expect(find(suggestions_dropdown)).to have_text('😸 smile_cat')
- send_keys [:arrow_down, :enter]
+ send_keys :enter
expect(page).not_to have_css(suggestions_dropdown)
- expect(page).to have_text('😃')
+ expect(page).to have_text('😄')
end
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
@@ -718,18 +742,6 @@ RSpec.shared_examples 'edits content using the content editor' do |params = {
expect(page).not_to have_css(suggestions_dropdown)
end
- it 'scrolls selected item into view when navigating with keyboard' do
- type_in_content_editor ':'
-
- expect(find(suggestions_dropdown)).to have_text('grinning')
-
- expect(dropdown_scroll_top).to be 0
-
- send_keys :arrow_up
-
- expect(dropdown_scroll_top).to be > 100
- end
-
def dropdown_scroll_top
evaluate_script("document.querySelector('#{suggestions_dropdown}').scrollTop")
end
diff --git a/spec/support/shared_examples/policies/protected_branches.rb b/spec/support/shared_examples/policies/protected_branches.rb
new file mode 100644
index 00000000000..39652434acb
--- /dev/null
+++ b/spec/support/shared_examples/policies/protected_branches.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'allows protected branch crud' do
+ it { is_expected.to be_allowed(:read_protected_branch) }
+ it { is_expected.to be_allowed(:create_protected_branch) }
+ it { is_expected.to be_allowed(:update_protected_branch) }
+ it { is_expected.to be_allowed(:destroy_protected_branch) }
+end
+
+RSpec.shared_examples 'disallows protected branch crud' do
+ it { is_expected.not_to be_allowed(:read_protected_branch) }
+ it { is_expected.not_to be_allowed(:create_protected_branch) }
+ it { is_expected.not_to be_allowed(:update_protected_branch) }
+ it { is_expected.not_to be_allowed(:destroy_protected_branch) }
+end
+
+RSpec.shared_examples 'disallows protected branch changes' do
+ it { is_expected.not_to be_allowed(:create_protected_branch) }
+ it { is_expected.not_to be_allowed(:update_protected_branch) }
+ it { is_expected.not_to be_allowed(:destroy_protected_branch) }
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 2db8cebe58c..136fa6b4aa0 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -235,30 +235,19 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'ExternalServiceReactiveCachingWorker' => 3,
'FileHookWorker' => false,
'FlushCounterIncrementsWorker' => 3,
- 'Geo::Batch::ProjectRegistrySchedulerWorker' => 3,
- 'Geo::Batch::ProjectRegistryWorker' => 3,
'Geo::ContainerRepositorySyncWorker' => 1,
'Geo::DestroyWorker' => 3,
'Geo::EventWorker' => 3,
'Geo::FileRemovalWorker' => 3,
- 'Geo::ProjectSyncWorker' => 1,
- 'Geo::RenameRepositoryWorker' => 3,
- 'Geo::RepositoryCleanupWorker' => 3,
- 'Geo::RepositoryShardSyncWorker' => false,
- 'Geo::RepositoryVerification::Primary::ShardWorker' => false,
- 'Geo::RepositoryVerification::Primary::SingleWorker' => false,
- 'Geo::RepositoryVerification::Secondary::SingleWorker' => false,
'Geo::ReverificationBatchWorker' => 0,
'Geo::BulkMarkPendingBatchWorker' => 0,
'Geo::BulkMarkVerificationPendingBatchWorker' => 0,
- 'Geo::Scheduler::Primary::SchedulerWorker' => false,
'Geo::Scheduler::SchedulerWorker' => false,
'Geo::Scheduler::Secondary::SchedulerWorker' => false,
'Geo::VerificationBatchWorker' => 0,
'Geo::VerificationStateBackfillWorker' => false,
'Geo::VerificationTimeoutWorker' => false,
'Geo::VerificationWorker' => 3,
- 'GeoRepositoryDestroyWorker' => 3,
'Gitlab::BitbucketImport::AdvanceStageWorker' => 3,
'Gitlab::BitbucketImport::Stage::FinishImportWorker' => 3,
'Gitlab::BitbucketImport::Stage::ImportIssuesWorker' => 3,