summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-03-08 18:08:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-03-08 18:08:14 +0000
commit9885b7e33ece32ac3bfbf077bb2dc53c459dbc0e (patch)
treebcbabc83286e8852df225b2117b28d4dc8678a7f
parent684838d4bea13af1dac9c2f32b99985bf0f9f8e2 (diff)
downloadgitlab-ce-9885b7e33ece32ac3bfbf077bb2dc53c459dbc0e.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml2
-rw-r--r--.rubocop.yml4
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum18
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/search/store/actions.js5
-rw-r--r--app/assets/javascripts/search/store/utils.js29
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue61
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql17
-rw-r--r--app/models/note.rb5
-rw-r--r--config/feature_flags/development/user_search_simple_query_string.yml8
-rw-r--r--doc/user/search/advanced_search.md6
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb5
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js170
-rw-r--r--spec/frontend/search/mock_data.js7
-rw-r--r--spec/frontend/search/store/utils_spec.js22
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js98
-rw-r--r--yarn.lock10
22 files changed, 350 insertions, 144 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 41b15e2edd3..62907acfd57 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -227,6 +227,8 @@ _ee:super-sidebar-nav:
QA_SUPER_SIDEBAR_ENABLED: "true"
QA_ALLURE_RESULTS_DIRECTORY: tmp/allure-results-super-sidebar
GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav=enabled
+ RSPEC_REPORT_OPTS: "--format documentation"
+ SKIP_REPORT_IN_ISSUES: "true"
allow_failure: true
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
diff --git a/.rubocop.yml b/.rubocop.yml
index 3489c2e2534..8f9a02bbadd 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -30,6 +30,7 @@ inherit_mode:
merge:
- Include
- Exclude
+ - AllowedPatterns
AllCops:
# Target the current Ruby version. For example, "2.7" or "3.0".
@@ -97,6 +98,9 @@ InternalAffairs/DeprecateCopHelper:
Include:
- spec/rubocop/**/*.rb
+Layout/LineLength:
+ AllowedPatterns: ['^RSpec\.describe\s.*\sdo']
+
Lint/LastKeywordArgument:
Safe: false
diff --git a/Gemfile b/Gemfile
index dd8f0245d88..a87cf586a1a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -514,7 +514,7 @@ gem 'kas-grpc', '~> 0.0.2'
gem 'grpc', '~> 1.42.0'
-gem 'google-protobuf', '~> 3.22'
+gem 'google-protobuf', '~> 3.22', '>= 3.22.1'
gem 'toml-rb', '~> 2.2.0'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 9bb151adb66..579849182bc 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -235,15 +235,15 @@
{"name":"google-cloud-env","version":"1.6.0","platform":"ruby","checksum":"6179acb946975892c7908748df5722a4ebadfc8cf5bb7b0d8d933ca67183fa15"},
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
{"name":"google-cloud-storage","version":"1.44.0","platform":"ruby","checksum":"299a1e055c9277c8120f7c10d21d37e4d8c17c7b963350c0e0bff7e9d9a570ea"},
-{"name":"google-protobuf","version":"3.22.0","platform":"arm64-darwin","checksum":"e47680f8cf46d5e0bf573052047276260d785caf7b586719af407767e96e535c"},
-{"name":"google-protobuf","version":"3.22.0","platform":"java","checksum":"24e55a0665113f60af35e27dd4c1fcc9b08d16f9da7605456634e505c95890b3"},
-{"name":"google-protobuf","version":"3.22.0","platform":"ruby","checksum":"58db86f65c686ef4b9389569faa176bada384850751675f0c3736ef76cdcae90"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x64-mingw-ucrt","checksum":"43ce8f98fdfa06a81397577bb502230d545178afd592ef5e7d2daddacdda8eb6"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x64-mingw32","checksum":"8255654f5a4a7fff0d430357fa9fe2f23db16eb17ed1067b46a1aab7fd9fcbab"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x86-linux","checksum":"2794f32ecfbeec0fd7baa4a61292fa220f5aad07b656ba656ecf134b1f7e8425"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x86-mingw32","checksum":"a037a5ed2d0a1faa556466c34f7177fee1f3aee8282c2195aed804fead65c65d"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x86_64-darwin","checksum":"bff2987e4bf1a934a4555aea2020df18b557393b188c849fb9deacec9ed0f3d9"},
-{"name":"google-protobuf","version":"3.22.0","platform":"x86_64-linux","checksum":"a0ea6aa03602e9e4f11c1506a114777204aade2e04adc2051945536c35d88bfe"},
+{"name":"google-protobuf","version":"3.22.1","platform":"arm64-darwin","checksum":"bd904af849bd1f143c4d9eb74e0622f9efca7ed20719b00baa77fcb1a5fc2aa7"},
+{"name":"google-protobuf","version":"3.22.1","platform":"java","checksum":"268359af055709c5796b1c726f839ef0fe356702c7abdb136367eb37cf9662e3"},
+{"name":"google-protobuf","version":"3.22.1","platform":"ruby","checksum":"2fc99aa4f0c2fdd33baf7c7fafb045118d1775893ca8823254658429a23ba0f2"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x64-mingw-ucrt","checksum":"351cb7743ea748156fea56ace3f5613ce5a5f1c4c59b18ff28930ef7c53de5f6"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x64-mingw32","checksum":"3808b954ee3240ef97d019037149dde2afa877585420b30f7ac7be37cee7bb4e"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x86-linux","checksum":"6d8ee9928dbe564916b37ede264b1b3492915b818a0daf9c8b4b572eda7baa30"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x86-mingw32","checksum":"a8ab1fda2a208dc238f16b2796d5a3383e6a273f8ef88a23ca90e0c2846594ee"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x86_64-darwin","checksum":"a425ce701b8c4f9aeb7cac0e7f1b36496ff8f6bf86dc7fe9d06af534d25e45d8"},
+{"name":"google-protobuf","version":"3.22.1","platform":"x86_64-linux","checksum":"a62f7d472c346cea6c6141c70b1caba5b7e75319c56cf44a24b62bad6c1adf10"},
{"name":"googleapis-common-protos-types","version":"1.3.0","platform":"ruby","checksum":"c5411f3197cc3e02547ded1858303b1f830b4dc89c588c142ad6c8a231050671"},
{"name":"googleauth","version":"1.3.0","platform":"ruby","checksum":"51dd7362353cf1e90a2d01e1fb94321ae3926c776d4dc4a79db65230217ffcc2"},
{"name":"gpgme","version":"2.0.22","platform":"ruby","checksum":"7c6904952afdd0bf2c7c3ed6de98a5143f86c6b7390dbcd9d7012bddfa3ec862"},
diff --git a/Gemfile.lock b/Gemfile.lock
index dd8b77c40cc..89c13820b12 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -664,7 +664,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- google-protobuf (3.22.0)
+ google-protobuf (3.22.1)
googleapis-common-protos-types (1.3.0)
google-protobuf (~> 3.14)
googleauth (1.3.0)
@@ -1704,7 +1704,7 @@ DEPENDENCIES
google-apis-serviceusage_v1 (~> 0.28.0)
google-apis-sqladmin_v1beta4 (~> 0.41.0)
google-cloud-storage (~> 1.44.0)
- google-protobuf (~> 3.22)
+ google-protobuf (~> 3.22, >= 3.22.1)
gpgme (~> 2.0.22)
grape (~> 1.5.2)
grape-entity (~> 0.10.0)
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 3af2b7892be..cd89234a231 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -13,6 +13,7 @@ import {
mergeById,
isSidebarDirty,
getAggregationsUrl,
+ prepareSearchAggregations,
} from './utils';
export const fetchGroups = ({ commit }, search) => {
@@ -135,12 +136,12 @@ export const fetchSidebarCount = ({ commit, state }) => {
return Promise.all(promises);
};
-export const fetchLanguageAggregation = ({ commit }) => {
+export const fetchLanguageAggregation = ({ commit, state }) => {
commit(types.REQUEST_AGGREGATIONS);
return axios
.get(getAggregationsUrl())
.then(({ data }) => {
- commit(types.RECEIVE_AGGREGATIONS_SUCCESS, data);
+ commit(types.RECEIVE_AGGREGATIONS_SUCCESS, prepareSearchAggregations(state, data));
})
.catch((e) => {
logError(e);
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index 1e6619ca6d5..8e484e69646 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -1,7 +1,8 @@
-import { isEqual } from 'lodash';
+import { isEqual, orderBy } from 'lodash';
import AccessorUtilities from '~/lib/utils/accessor';
import { formatNumber } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
+import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
import {
MAX_FREQUENT_ITEMS,
MAX_FREQUENCY,
@@ -9,6 +10,8 @@ import {
NUMBER_FORMATING_OPTIONS,
} from './constants';
+const LANGUAGE_AGGREGATION_NAME = languageFilterData.filterParam;
+
function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]]));
}
@@ -117,3 +120,27 @@ export const getAggregationsUrl = () => {
currentUrl.pathname = joinPaths('/search', 'aggregations');
return currentUrl.toString();
};
+
+const sortLanguages = (state, entries) => {
+ const queriedLanguages = state.query?.[LANGUAGE_AGGREGATION_NAME] || [];
+
+ if (!Array.isArray(queriedLanguages) || !queriedLanguages.length) {
+ return entries;
+ }
+
+ const queriedLanguagesSet = new Set(queriedLanguages);
+
+ return orderBy(entries, [({ key }) => queriedLanguagesSet.has(key), 'count'], ['desc', 'desc']);
+};
+
+export const prepareSearchAggregations = (state, aggregationData) =>
+ aggregationData.map((item) => {
+ if (item?.name === LANGUAGE_AGGREGATION_NAME) {
+ return {
+ ...item,
+ buckets: sortLanguages(state, item.buckets),
+ };
+ }
+
+ return item;
+ });
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 3225d3f6e49..1d3319dbc90 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -87,6 +87,9 @@ export default {
hasAdminPermission() {
return this.note.userPermissions.adminNote;
},
+ hasAwardEmojiPermission() {
+ return this.note.userPermissions.awardEmoji;
+ },
},
methods: {
showReplyForm() {
@@ -159,10 +162,13 @@ export default {
</note-header>
<div class="gl-display-inline-flex">
<note-actions
+ :show-award-emoji="hasAwardEmojiPermission"
:show-reply="showReply"
:show-edit="hasAdminPermission"
+ :note-id="note.id"
@startReplying="showReplyForm"
@startEditing="startEditing"
+ @error="($event) => $emit('error', $event)"
/>
<!-- v-if condition should be moved to "delete" dropdown item as soon as we implement copying the link -->
<gl-dropdown
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index c17e855e527..6bea7953698 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -1,7 +1,10 @@
<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { __, s__ } from '~/locale';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import addAwardEmojiMutation from '../../graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
export default {
name: 'WorkItemNoteActions',
@@ -10,11 +13,14 @@ export default {
},
components: {
GlButton,
+ GlIcon,
ReplyButton,
+ EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
showReply: {
type: Boolean,
@@ -24,12 +30,63 @@ export default {
type: Boolean,
required: true,
},
+ noteId: {
+ type: String,
+ required: true,
+ },
+ showAwardEmoji: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ async setAwardEmoji(name) {
+ try {
+ const {
+ data: {
+ awardEmojiAdd: { errors = [] },
+ },
+ } = await this.$apollo.mutate({
+ mutation: addAwardEmojiMutation,
+ variables: {
+ awardableId: this.noteId,
+ name,
+ },
+ });
+
+ if (errors.length > 0) {
+ throw new Error(errors[0].message);
+ }
+ } catch (error) {
+ this.$emit('error', s__('WorkItem|Failed to award emoji'));
+ Sentry.captureException(error);
+ }
+ },
},
};
</script>
<template>
<div class="note-actions">
+ <emoji-picker
+ v-if="showAwardEmoji && glFeatures.workItemsMvc2"
+ toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
+ data-testid="note-emoji-button"
+ @click="setAwardEmoji"
+ >
+ <template #button-content>
+ <gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" />
+ <gl-icon
+ class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!"
+ name="smiley"
+ />
+ <gl-icon
+ class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!"
+ name="smile"
+ />
+ </template>
+ </emoji-picker>
<reply-button v-if="showReply" ref="replyButton" @startReplying="$emit('startReplying')" />
<gl-button
v-if="showEdit"
diff --git a/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql
new file mode 100644
index 00000000000..dc51c53428b
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql
@@ -0,0 +1,17 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+mutation workItemNoteAddAwardEmoji($awardableId: AwardableID!, $name: String!) {
+ awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) {
+ awardEmoji {
+ name
+ description
+ unicode
+ emoji
+ unicodeVersion
+ user {
+ ...User
+ }
+ }
+ errors
+ }
+}
diff --git a/app/models/note.rb b/app/models/note.rb
index 54e7389136f..b9b884b88c5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -172,7 +172,6 @@ class Note < ApplicationRecord
project: [:project_members, :namespace, { group: [:group_members] }])
end
scope :with_metadata, -> { includes(:system_note_metadata) }
- scope :with_web_entity_associations, -> { preload(:project, :author, :noteable) }
scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) }
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
@@ -291,6 +290,10 @@ class Note < ApplicationRecord
def cherry_picked_merge_requests(shas)
where(noteable_type: 'MergeRequest', commit_id: shas).select(:noteable_id)
end
+
+ def with_web_entity_associations
+ preload(:project, :author, :noteable)
+ end
end
# rubocop: disable CodeReuse/ServiceClass
diff --git a/config/feature_flags/development/user_search_simple_query_string.yml b/config/feature_flags/development/user_search_simple_query_string.yml
deleted file mode 100644
index 78230c0fef9..00000000000
--- a/config/feature_flags/development/user_search_simple_query_string.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: user_search_simple_query_string
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110623
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391955
-milestone: '15.10'
-type: development
-group: group::global search
-default_enabled: false
diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md
index ac4c3c62746..5273ea47f58 100644
--- a/doc/user/search/advanced_search.md
+++ b/doc/user/search/advanced_search.md
@@ -55,12 +55,6 @@ Advanced search uses [Elasticsearch syntax](https://www.elastic.co/guide/en/elas
### Refining user search
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/388409) in GitLab 15.10 [with a flag](../../administration/feature_flags.md) named `user_search_simple_query_string`. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `user_search_simple_query_string`.
-On GitLab.com, this feature is available.
-
In user search, a [fuzzy query](https://www.elastic.co/guide/en/elasticsearch/reference/7.2/query-dsl-fuzzy-query.html) is used by default. You can refine your search with [Elasticsearch syntax](#syntax).
### Code search
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0d921596def..48797d7a384 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -29713,6 +29713,9 @@ msgstr ""
msgid "OnDemandScans|Only project owners and maintainers can select runner tags."
msgstr ""
+msgid "OnDemandScans|Pre-scan verification for %{profile_name} completed with"
+msgstr ""
+
msgid "OnDemandScans|Repeats"
msgstr ""
@@ -49001,6 +49004,9 @@ msgstr ""
msgid "WorkItem|Existing task"
msgstr ""
+msgid "WorkItem|Failed to award emoji"
+msgstr ""
+
msgid "WorkItem|Health status"
msgstr ""
diff --git a/package.json b/package.json
index a993fcafa15..c4d738429d3 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.24.0",
- "@gitlab/ui": "56.2.0",
+ "@gitlab/ui": "56.2.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230223005157",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
index a10e95a860c..ecf8717be87 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
module QA
- # https://github.com/gitlab-qa-github/import-test <- project under test
- #
- RSpec.describe 'Manage', product_group: :import do
+ RSpec.describe 'Manage', product_group: :import, quarantine: {
+ type: :stale,
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/394994'
+ } do
describe 'GitHub import' do
include_context 'with github import'
diff --git a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb
index b5a8df15ddc..6cb777ba7f4 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', product_group: :import do
+ RSpec.describe 'Manage', product_group: :import, quarantine: {
+ type: :stale,
+ issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/394994'
+ } do
describe 'GitHub import' do
include_context 'with github import'
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 46740958090..47e09bb38bc 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,9 +1,14 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
+import {
+ RICH_BLOB_VIEWER_TITLE,
+ SIMPLE_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+} from '~/blob/components/constants';
import TableContents from '~/blob/components/table_contents.vue';
import { Blob } from './mock_data';
@@ -11,12 +16,26 @@ import { Blob } from './mock_data';
describe('Blob Header Default Actions', () => {
let wrapper;
- function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
- const method = shouldMount ? mount : shallowMount;
- const blobHash = 'foo-bar';
- wrapper = method.call(this, BlobHeader, {
+ const defaultProvide = {
+ blobHash: 'foo-bar',
+ };
+
+ const findDefaultActions = () => wrapper.findComponent(DefaultActions);
+ const findTableContents = () => wrapper.findComponent(TableContents);
+ const findViewSwitcher = () => wrapper.findComponent(ViewerSwitcher);
+ const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
+ const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE);
+ const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE);
+
+ function createComponent({
+ blobProps = {},
+ options = {},
+ propsData = {},
+ mountFn = shallowMount,
+ } = {}) {
+ wrapper = mountFn(BlobHeader, {
provide: {
- blobHash,
+ ...defaultProvide,
},
propsData: {
blob: { ...Blob, ...blobProps },
@@ -26,143 +45,123 @@ describe('Blob Header Default Actions', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
- const findDefaultActions = () => wrapper.findComponent(DefaultActions);
-
- const slots = {
- prepend: 'Foo Prepend',
- actions: 'Actions Bar',
- };
-
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
- it('renders all components', () => {
- createComponent();
- expect(wrapper.findComponent(TableContents).exists()).toBe(true);
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true);
- expect(findDefaultActions().exists()).toBe(true);
- expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true);
+ describe('default render', () => {
+ it.each`
+ findComponent | componentName
+ ${findTableContents} | ${'TableContents'}
+ ${findViewSwitcher} | ${'ViewSwitcher'}
+ ${findDefaultActions} | ${'DefaultActions'}
+ ${findBlobFilePath} | ${'BlobFilePath'}
+ `('renders $componentName component by default', ({ findComponent }) => {
+ createComponent();
+
+ expect(findComponent().exists()).toBe(true);
+ });
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
- richViewer: null,
+ blobProps: {
+ richViewer: null,
+ },
});
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideViewerSwitcher: true,
},
- );
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ });
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideDefaultActions: true,
},
- );
- expect(wrapper.findComponent(DefaultActions).exists()).toBe(false);
+ });
+ expect(findDefaultActions().exists()).toBe(false);
});
- Object.keys(slots).forEach((slot) => {
- it('renders the slots', () => {
- const slotContent = slots[slot];
- createComponent(
- {},
- {
- scopedSlots: {
- [slot]: `<span>${slotContent}</span>`,
- },
+ it.each`
+ slotContent | key
+ ${'Foo Prepend'} | ${'prepend'}
+ ${'Actions Bar'} | ${'actions'}
+ `('renders the slot $key', ({ key, slotContent }) => {
+ createComponent({
+ options: {
+ scopedSlots: {
+ [key]: `<span>${slotContent}</span>`,
},
- {},
- true,
- );
- expect(wrapper.text()).toContain(slotContent);
+ },
+ mountFn: mount,
});
+ expect(wrapper.text()).toContain(slotContent);
});
it('passes information about render error down to default actions', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hasRenderError: true,
},
- );
+ });
expect(findDefaultActions().props('hasRenderError')).toBe(true);
});
it('passes the correct isBinary value to default actions when viewing a binary file', () => {
- createComponent({}, {}, { isBinary: true });
+ createComponent({ propsData: { isBinary: true } });
expect(findDefaultActions().props('isBinary')).toBe(true);
});
});
describe('functionality', () => {
- const newViewer = 'Foo Bar';
- const activeViewerType = 'Alpha Beta';
-
const factory = (hideViewerSwitcher = false) => {
- createComponent(
- {},
- {},
- {
- activeViewerType,
+ createComponent({
+ propsData: {
+ activeViewerType: SIMPLE_BLOB_VIEWER,
hideViewerSwitcher,
},
- );
+ mountFn: mountExtended,
+ });
};
- it('by default sets viewer data based on activeViewerType', () => {
+ it('shows the correctly selected view by default', () => {
factory();
- expect(wrapper.vm.viewer).toBe(activeViewerType);
+
+ expect(findViewSwitcher().exists()).toBe(true);
+ expect(findRichTextEditorBtn().props().selected).toBe(false);
+ expect(findSimpleTextEditorBtn().props().selected).toBe(true);
});
- it('sets viewer to null if the viewer switcher should be hidden', () => {
+ it('Does not show the viewer switcher should be hidden', () => {
factory(true);
- expect(wrapper.vm.viewer).toBe(null);
+
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('watches the changes in viewer data and emits event when the change is registered', async () => {
factory();
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
- });
-
- it('does not emit event if the switcher is not rendered', async () => {
- factory(true);
-
- expect(wrapper.vm.showViewerSwitcher).toBe(false);
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
+ await findRichTextEditorBtn().trigger('click');
- await nextTick();
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('viewer-changed')).toBeDefined();
});
it('sets different icons depending on the blob file type', async () => {
factory();
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('document');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('document');
+
await wrapper.setProps({
blob: {
...Blob,
@@ -172,7 +171,8 @@ describe('Blob Header Default Actions', () => {
},
},
});
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('table');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('table');
});
});
});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fb9c0a93907..0aa4f0e1c84 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -653,3 +653,10 @@ export const TEST_FILTER_DATA = {
JSON: { label: 'JSON', value: 'JSON', count: 15 },
},
};
+
+export const SMALL_MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: TEST_RAW_BUCKETS,
+ },
+];
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index 8c4a17f0a5d..dfe4e801f11 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -7,6 +7,7 @@ import {
isSidebarDirty,
formatSearchResultCount,
getAggregationsUrl,
+ prepareSearchAggregations,
} from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
@@ -15,6 +16,9 @@ import {
MOCK_INFLATED_DATA,
FRESH_STORED_DATA,
STALE_STORED_DATA,
+ MOCK_AGGREGATIONS,
+ SMALL_MOCK_AGGREGATIONS,
+ TEST_RAW_BUCKETS,
} from '../mock_data';
const PREV_TIME = new Date().getTime() - 1;
@@ -266,4 +270,22 @@ describe('Global Search Store Utils', () => {
expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`);
});
});
+
+ const TEST_LANGUAGE_QUERY = ['Markdown', 'JSON'];
+ const TEST_EXPECTED_ORDERED_BUCKETS = [
+ TEST_RAW_BUCKETS.find((x) => x.key === 'Markdown'),
+ TEST_RAW_BUCKETS.find((x) => x.key === 'JSON'),
+ ...TEST_RAW_BUCKETS.filter((x) => !TEST_LANGUAGE_QUERY.includes(x.key)),
+ ];
+
+ describe('prepareSearchAggregations', () => {
+ it.each`
+ description | query | data | result
+ ${'has no query'} | ${undefined} | ${MOCK_AGGREGATIONS} | ${MOCK_AGGREGATIONS}
+ ${'has query'} | ${{ language: TEST_LANGUAGE_QUERY }} | ${SMALL_MOCK_AGGREGATIONS} | ${[{ ...SMALL_MOCK_AGGREGATIONS[0], buckets: TEST_EXPECTED_ORDERED_BUCKETS }]}
+ ${'has bad query'} | ${{ language: ['sdf', 'wrt'] }} | ${SMALL_MOCK_AGGREGATIONS} | ${SMALL_MOCK_AGGREGATIONS}
+ `('$description', ({ query, data, result }) => {
+ expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index d85cd46c1c3..b293127b6af 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,52 +1,116 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import EmojiPicker from '~/emoji/components/picker.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+
+Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
+ const noteId = '1';
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+ const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+
+ const addEmojiMutationResolver = jest.fn().mockResolvedValue({
+ data: {
+ errors: [],
+ },
+ });
+
+ const EmojiPickerStub = {
+ props: EmojiPicker.props,
+ template: '<div></div>',
+ };
- const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
+ noteId,
+ showAwardEmoji,
+ },
+ provide: {
+ glFeatures: {
+ workItemsMvc2: true,
+ },
},
+ stubs: {
+ EmojiPicker: EmojiPickerStub,
+ },
+ apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
});
};
- describe('Default', () => {
- it('Should show the reply button by default', () => {
+ describe('reply button', () => {
+ it('is visible by default', () => {
createComponent();
+
expect(findReplyButton().exists()).toBe(true);
});
- });
- describe('When the reply button needs to be hidden', () => {
- it('Should show the reply button by default', () => {
+ it('is hidden when showReply false', () => {
createComponent({ showReply: false });
+
expect(findReplyButton().exists()).toBe(false);
});
});
- it('shows edit button when `showEdit` prop is true', () => {
- createComponent();
+ describe('edit button', () => {
+ it('is visible when `showEdit` prop is true', () => {
+ createComponent();
- expect(findEditButton().exists()).toBe(true);
- });
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
- it('does not show edit button when `showEdit` prop is false', () => {
- createComponent({ showEdit: false });
+ it('emits `startEditing` event when clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
- expect(findEditButton().exists()).toBe(false);
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
});
- it('emits `startEditing` event when edit button is clicked', () => {
- createComponent();
- findEditButton().vm.$emit('click');
+ describe('emoji picker', () => {
+ it('is visible when `showAwardEmoji` prop is true', () => {
+ createComponent();
+
+ expect(findEmojiButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showAwardEmoji` prop is false', () => {
+ createComponent({ showAwardEmoji: false });
- expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ expect(findEmojiButton().exists()).toBe(false);
+ });
+
+ it('commits mutation on click', async () => {
+ const awardName = 'carrot';
+
+ createComponent();
+
+ findEmojiButton().vm.$emit('click', awardName);
+
+ await waitForPromises();
+
+ expect(findEmojiButton().emitted('errors')).toEqual(undefined);
+ expect(addEmojiMutationResolver).toHaveBeenCalledWith({
+ awardableId: noteId,
+ name: awardName,
+ });
+ });
});
});
diff --git a/yarn.lock b/yarn.lock
index 7adc8295ebb..1dc075a3e62 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1226,14 +1226,14 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.24.0.tgz#bc8265919aa04b06cd08be91637471bad195936d"
integrity sha512-R4s5qJUFUIbPflknpw1aI/PchiNq65vY7LVsJZnQkY+vi+AgmsETdut/AdferbGWmeWMU0q2wuVu9phE8lDUgA==
-"@gitlab/ui@56.2.0":
- version "56.2.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-56.2.0.tgz#7163dbb161b995fe69b5c4436c85fd3dbd127f12"
- integrity sha512-dhhtgK2oq69zVkMnKj8SS/zqVy3VBdT2kT+Y+iTPxtHLmqr8SRgUvG0Yf247h/Inza5Facaa5qzweCXEK062Gw==
+"@gitlab/ui@56.2.1":
+ version "56.2.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-56.2.1.tgz#ca503f5b70a912c98da6bb30f9e22b6fff710d72"
+ integrity sha512-WeTNZT+Bo3jZ2gv7efuTyu4fm5Ks6KbpBLaJmyFtDMe38+o0RpgRgMjc+Zh0nLP3RuejhBUPkYsK9WxhdVmiog==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"
- dompurify "^2.4.4"
+ dompurify "^2.4.5"
echarts "^5.3.2"
iframe-resizer "^4.3.2"
lodash "^4.17.20"