diff options
14 files changed, 246 insertions, 21 deletions
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue index cfd6b21fa66..baefac572f1 100644 --- a/app/assets/javascripts/boards/components/board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -27,7 +27,13 @@ export default { }, computed: { urlParams() { - const { authorUsername, labelName, assigneeUsername, search } = this.filterParams; + const { + authorUsername, + labelName, + assigneeUsername, + search, + milestoneTitle, + } = this.filterParams; let notParams = {}; if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { @@ -36,6 +42,7 @@ export default { 'not[label_name][]': this.filterParams.not.labelName, 'not[author_username]': this.filterParams.not.authorUsername, 'not[assignee_username]': this.filterParams.not.assigneeUsername, + 'not[milestone_title]': this.filterParams.not.milestoneTitle, }, undefined, ); @@ -46,6 +53,7 @@ export default { author_username: authorUsername, 'label_name[]': labelName, assignee_username: assigneeUsername, + milestone_title: milestoneTitle, search, }; }, @@ -64,7 +72,13 @@ export default { this.performSearch(); }, getFilteredSearchValue() { - const { authorUsername, labelName, assigneeUsername, search } = this.filterParams; + const { + authorUsername, + labelName, + assigneeUsername, + search, + milestoneTitle, + } = this.filterParams; const filteredSearchValue = []; if (authorUsername) { @@ -90,6 +104,13 @@ export default { ); } + if (milestoneTitle) { + filteredSearchValue.push({ + type: 'milestone_title', + value: { data: milestoneTitle, operator: '=' }, + }); + } + if (this.filterParams['not[authorUsername]']) { filteredSearchValue.push({ type: 'author_username', @@ -97,6 +118,13 @@ export default { }); } + if (this.filterParams['not[milestoneTitle]']) { + filteredSearchValue.push({ + type: 'milestone_title', + value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' }, + }); + } + if (this.filterParams['not[assigneeUsername]']) { filteredSearchValue.push({ type: 'assignee_username', @@ -143,6 +171,9 @@ export default { case 'label_name': labels.push(filter.value.data); break; + case 'milestone_title': + filterParams.milestoneTitle = filter.value.data; + break; case 'filtered-search-term': if (filter.value.data) plainText.push(filter.value.data); break; diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue index d8dac17d326..22099f695ee 100644 --- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue @@ -1,4 +1,5 @@ <script> +import { mapActions } from 'vuex'; import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; import issueBoardFilters from '~/boards/issue_board_filters'; import { TYPE_USER } from '~/graphql_shared/constants'; @@ -6,6 +7,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; export default { i18n: { @@ -13,6 +15,7 @@ export default { label: __('Label'), author: __('Author'), assignee: __('Assignee'), + milestone: __('Milestone'), is: __('is'), isNot: __('is not'), }, @@ -29,7 +32,7 @@ export default { }, computed: { tokens() { - const { label, is, isNot, author, assignee } = this.$options.i18n; + const { label, is, isNot, author, assignee, milestone } = this.$options.i18n; const { fetchAuthors, fetchLabels } = issueBoardFilters( this.$apollo, this.fullPath, @@ -77,10 +80,21 @@ export default { fetchAuthors, preloadedAuthors: this.preloadedAuthors(), }, + { + type: 'milestone_title', + title: milestone, + icon: 'clock', + symbol: '%', + token: MilestoneToken, + unique: true, + defaultMilestones: [], // todo: https://gitlab.com/gitlab-org/gitlab/-/issues/337044#note_640010094 + fetchMilestones: this.fetchMilestones, + }, ]; }, }, methods: { + ...mapActions(['fetchMilestones']), preloadedAuthors() { return gon?.current_user_id ? [ diff --git a/app/assets/javascripts/content_editor/components/content_editor_error.vue b/app/assets/javascripts/content_editor/components/content_editor_error.vue new file mode 100644 index 00000000000..031ea92a7e9 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/content_editor_error.vue @@ -0,0 +1,31 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import EditorStateObserver from './editor_state_observer.vue'; + +export default { + components: { + GlAlert, + EditorStateObserver, + }, + data() { + return { + error: null, + }; + }, + methods: { + displayError({ error }) { + this.error = error; + }, + dismissError() { + this.error = null; + }, + }, +}; +</script> +<template> + <editor-state-observer @error="displayError"> + <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError"> + {{ error }} + </gl-alert> + </editor-state-observer> +</template> diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue index acdca67bff7..2eeb0719096 100644 --- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue +++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue @@ -5,6 +5,9 @@ export const tiptapToComponentMap = { update: 'docUpdate', selectionUpdate: 'selectionUpdate', transaction: 'transaction', + focus: 'focus', + blur: 'blur', + error: 'error', }; const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName]; diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 6563094ef72..17d35212a0d 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -275,7 +275,6 @@ export default { avatar_url: gon.current_user_avatar_url, }); } - const tokens = [ { type: TOKEN_TYPE_AUTHOR, diff --git a/db/migrate/20210803110920_add_unique_index_to_vulnerability_flags_table.rb b/db/migrate/20210803110920_add_unique_index_to_vulnerability_flags_table.rb new file mode 100644 index 00000000000..38d72496484 --- /dev/null +++ b/db/migrate/20210803110920_add_unique_index_to_vulnerability_flags_table.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddUniqueIndexToVulnerabilityFlagsTable < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + INDEX_NAME = 'index_vulnerability_flags_on_unique_columns' + + disable_ddl_transaction! + + def up + add_concurrent_index :vulnerability_flags, [:vulnerability_occurrence_id, :flag_type, :origin], name: INDEX_NAME, unique: true + end + + def down + remove_concurrent_index_by_name :vulnerability_flags, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210803110920 b/db/schema_migrations/20210803110920 new file mode 100644 index 00000000000..69ba671ea7b --- /dev/null +++ b/db/schema_migrations/20210803110920 @@ -0,0 +1 @@ +529cf86e09b5aa9015b604e73827cb21e92ced401f30dfb281115a506596bd4e
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 688cd0f1452..93b4d62bfea 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25450,6 +25450,8 @@ CREATE INDEX index_vulnerability_findings_remediations_on_remediation_id ON vuln CREATE UNIQUE INDEX index_vulnerability_findings_remediations_on_unique_keys ON vulnerability_findings_remediations USING btree (vulnerability_occurrence_id, vulnerability_remediation_id); +CREATE UNIQUE INDEX index_vulnerability_flags_on_unique_columns ON vulnerability_flags USING btree (vulnerability_occurrence_id, flag_type, origin); + CREATE INDEX index_vulnerability_flags_on_vulnerability_occurrence_id ON vulnerability_flags USING btree (vulnerability_occurrence_id); CREATE INDEX index_vulnerability_historical_statistics_on_date_and_id ON vulnerability_historical_statistics USING btree (date, id); diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index df75b3cf716..6675439e430 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -2,6 +2,42 @@ namespace :gitlab do namespace :gitaly do + desc 'Installs gitaly for running tests within gitlab-development-kit' + task :test_install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args| + inside_gdk = Rails.env.test? && File.exist?(Rails.root.join('../GDK_ROOT')) + + if ENV['FORCE_GITALY_INSTALL'] || !inside_gdk + Rake::Task["gitlab:gitaly:install"].invoke(*args) + + next + end + + gdk_gitaly_dir = ENV.fetch('GDK_GITALY', Rails.root.join('../gitaly')) + + # Our test setup expects a git repo, so clone rather than copy + version = Gitlab::GitalyClient.expected_server_version + checkout_or_clone_version(version: version, repo: gdk_gitaly_dir, target_dir: args.dir, clone_opts: %w[--depth 1]) + + # We assume the GDK gitaly already compiled binaries + build_dir = File.join(gdk_gitaly_dir, '_build') + FileUtils.cp_r(build_dir, args.dir) + + # We assume the GDK gitaly already ran bundle install + bundle_dir = File.join(gdk_gitaly_dir, 'ruby', '.bundle') + FileUtils.cp_r(bundle_dir, File.join(args.dir, 'ruby')) + + # For completeness we copy this for gitaly's make target + ruby_bundle_file = File.join(gdk_gitaly_dir, '.ruby-bundle') + FileUtils.cp_r(ruby_bundle_file, args.dir) + + gitaly_binary = File.join(build_dir, 'bin', 'gitaly') + warn_gitaly_out_of_date!(gitaly_binary, version) + rescue Errno::ENOENT => e + puts "Could not copy files, did you run `gdk update`? Error: #{e.message}" + + raise + end + desc 'GitLab | Gitaly | Install or upgrade gitaly' task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab @@ -41,5 +77,24 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]") _, status = Gitlab::Popen.popen(%w[which gmake]) status == 0 ? 'gmake' : 'make' end + + def warn_gitaly_out_of_date!(gitaly_binary, expected_version) + binary_version, exit_status = Gitlab::Popen.popen(%W[#{gitaly_binary} -version]) + + raise "Failed to run `#{gitaly_binary} -version`" unless exit_status == 0 + + binary_version = binary_version.strip + + # See help for `git describe` for format + git_describe_sha = /g([a-f0-9]{5,40})\z/ + match = binary_version.match(git_describe_sha) + + # Just skip if the version does not have a sha component + return unless match + + return if expected_version.start_with?(match[1]) + + puts "WARNING: #{binary_version.strip} does not exactly match repository version #{expected_version}" + end end end diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index 6ac5d16e5a3..01dba83dd91 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -115,6 +115,7 @@ describe('BoardFilteredSearch', () => { { type: 'author_username', value: { data: 'root', operator: '=' } }, { type: 'label_name', value: { data: 'label', operator: '=' } }, { type: 'label_name', value: { data: 'label2', operator: '=' } }, + { type: 'milestone_title', value: { data: 'New Milestone', operator: '=' } }, ]; jest.spyOn(urlUtility, 'updateHistory'); findFilteredSearch().vm.$emit('onFilter', mockFilters); @@ -122,7 +123,8 @@ describe('BoardFilteredSearch', () => { expect(urlUtility.updateHistory).toHaveBeenCalledWith({ title: '', replace: true, - url: 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2', + url: + 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone', }); }); }); diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js index 0e3cf59901e..b6de46f8db8 100644 --- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js +++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js @@ -1,16 +1,16 @@ import { shallowMount } from '@vue/test-utils'; import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue'; -import { BoardType } from '~/boards/constants'; import issueBoardFilters from '~/boards/issue_board_filters'; import { mockTokens } from '../mock_data'; +jest.mock('~/boards/issue_board_filters'); + describe('IssueBoardFilter', () => { let wrapper; - const createComponent = ({ initialFilterParams = {} } = {}) => { + const createComponent = () => { wrapper = shallowMount(IssueBoardFilteredSpec, { - provide: { initialFilterParams }, props: { fullPath: '', boardType: '' }, }); }; @@ -20,7 +20,17 @@ describe('IssueBoardFilter', () => { }); describe('default', () => { + let fetchAuthorsSpy; + let fetchLabelsSpy; beforeEach(() => { + fetchAuthorsSpy = jest.fn(); + fetchLabelsSpy = jest.fn(); + + issueBoardFilters.mockReturnValue({ + fetchAuthors: fetchAuthorsSpy, + fetchLabels: fetchLabelsSpy, + }); + createComponent(); }); @@ -28,17 +38,10 @@ describe('IssueBoardFilter', () => { expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true); }); - it.each([[BoardType.group], [BoardType.project]])( - 'when boardType is %s we pass the correct tokens to BoardFilteredSearch', - (boardType) => { - const { fetchAuthors, fetchLabels } = issueBoardFilters({}, '', boardType); + it('passes the correct tokens to BoardFilteredSearch', () => { + const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones); - const tokens = mockTokens(fetchLabels, fetchAuthors); - - expect(wrapper.find(BoardFilteredSearch).props('tokens').toString()).toBe( - tokens.toString(), - ); - }, - ); + expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens); + }); }); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 420f5aa293b..d1d00786ad3 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -8,6 +8,7 @@ import boardsStore from '~/boards/stores/boards_store'; import { __ } from '~/locale'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; export const boardObj = { id: 1, @@ -542,7 +543,7 @@ export const mockMoveData = { ...mockMoveIssueParams, }; -export const mockTokens = (fetchLabels, fetchAuthors) => [ +export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ { icon: 'labels', title: __('Label'), @@ -568,6 +569,7 @@ export const mockTokens = (fetchLabels, fetchAuthors) => [ token: AuthorToken, unique: true, fetchAuthors, + preloadedAuthors: [], }, { icon: 'user', @@ -580,5 +582,16 @@ export const mockTokens = (fetchLabels, fetchAuthors) => [ token: AuthorToken, unique: true, fetchAuthors, + preloadedAuthors: [], + }, + { + icon: 'clock', + title: __('Milestone'), + symbol: '%', + type: 'milestone_title', + token: MilestoneToken, + unique: true, + defaultMilestones: [], + fetchMilestones, }, ]; diff --git a/spec/frontend/content_editor/components/content_editor_error_spec.js b/spec/frontend/content_editor/components/content_editor_error_spec.js new file mode 100644 index 00000000000..8723fb5a338 --- /dev/null +++ b/spec/frontend/content_editor/components/content_editor_error_spec.js @@ -0,0 +1,54 @@ +import { GlAlert } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ContentEditorError from '~/content_editor/components/content_editor_error.vue'; +import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; +import { createTestEditor, emitEditorEvent } from '../test_utils'; + +describe('content_editor/components/content_editor_error', () => { + let wrapper; + let tiptapEditor; + + const findErrorAlert = () => wrapper.findComponent(GlAlert); + + const createWrapper = async () => { + tiptapEditor = createTestEditor(); + + wrapper = shallowMountExtended(ContentEditorError, { + provide: { + tiptapEditor, + }, + stubs: { + EditorStateObserver, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders error when content editor emits an error event', async () => { + const error = 'error message'; + + createWrapper(); + + await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } }); + + expect(findErrorAlert().text()).toBe(error); + }); + + it('allows dismissing the error', async () => { + const error = 'error message'; + + createWrapper(); + + await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } }); + + findErrorAlert().vm.$emit('dismiss'); + + await nextTick(); + + expect(findErrorAlert().exists()).toBe(false); + }); +}); diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 5d27a47709f..eca0716f484 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -158,7 +158,7 @@ module TestEnv component_timed_setup('Gitaly', install_dir: gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:install", + task: "gitlab:gitaly:test_install", task_args: [gitaly_dir, repos_path, gitaly_url].compact) do Gitlab::SetupHelper::Gitaly.create_configuration( gitaly_dir, |