diff options
20 files changed, 227 insertions, 51 deletions
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 9ed286826cc..f857e618d89 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -142,11 +142,25 @@ export const stripHtml = (string, replace = '') => { }; /** - * Converts snake_case string to camelCase + * Converts a snake_cased string to camelCase. + * Leading and trailing underscores are ignored. * - * @param {*} string + * @param {String} string The snake_cased string to convert + * @returns {String} A camelCased version of the string + * + * @example + * + * // returns "aSnakeCasedString" + * convertToCamelCase('a_snake_cased_string') + * + * // returns "_leadingUnderscore" + * convertToCamelCase('_leading_underscore') + * + * // returns "trailingUnderscore_" + * convertToCamelCase('trailing_underscore_') */ -export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); +export const convertToCamelCase = string => + string.replace(/([a-z0-9])_([a-z0-9])/gi, (match, p1, p2) => `${p1}${p2.toUpperCase()}`); /** * Converts camelCase string to snake_case diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index f0d3f3f8c1d..0bc2a5ce2eb 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -20,10 +20,10 @@ export default { }, computed: { editLink() { - return this.release.Links?.editUrl; + return this.release._links?.editUrl; }, selfLink() { - return this.release.Links?.self; + return this.release._links?.self; }, }, }; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 4b838b1383e..a748c669ee8 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -994,11 +994,6 @@ $ide-commit-header-height: 48px; } .ide-context-header { - .ide-merge-requests-dropdown.dropdown-menu { - width: 385px; - max-height: initial; - } - .avatar-container { flex: 0 0 auto; margin-right: 0; diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 664e0955535..ae77af32b5b 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -15,6 +15,15 @@ module Resolvers argument :label_name, GraphQL::STRING_TYPE.to_list_type, required: false, description: 'Labels applied to this issue' + argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Milestones applied to this issue' + argument :assignee_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of a user assigned to the issues' + argument :assignee_id, GraphQL::STRING_TYPE, + required: false, + description: 'ID of a user assigned to the issues, "none" and "any" values supported' argument :created_before, Types::TimeType, required: false, description: 'Issues created before this date' diff --git a/app/models/project.rb b/app/models/project.rb index 6ff5016be03..0f61d32eb8d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -780,7 +780,7 @@ class Project < ApplicationRecord end def repository - @repository ||= Repository.new(full_path, self, disk_path: disk_path) + @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path) end def cleanup @@ -1411,8 +1411,8 @@ class Project < ApplicationRecord # Expires various caches before a project is renamed. def expire_caches_before_rename(old_path) - repo = Repository.new(old_path, self) - wiki = Repository.new("#{old_path}.wiki", self) + repo = Repository.new(old_path, self, shard: repository_storage) + wiki = Repository.new("#{old_path}.wiki", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI) if repo.exists? repo.before_delete diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 1abde5196de..7529047a73a 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -170,7 +170,7 @@ class ProjectWiki end def repository - @repository ||= Repository.new(full_path, @project, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI) + @repository ||= Repository.new(full_path, @project, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI) end def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index a53850bb068..beb65ff26fd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -22,7 +22,7 @@ class Repository include Gitlab::RepositoryCacheAdapter - attr_accessor :full_path, :disk_path, :container, :repo_type + attr_accessor :full_path, :shard, :disk_path, :container, :repo_type delegate :ref_name_for_sha, to: :raw_repository delegate :bundle_to_disk, to: :raw_repository @@ -65,8 +65,9 @@ class Repository xcode_config: :xcode_project? }.freeze - def initialize(full_path, container, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT) + def initialize(full_path, container, shard:, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT) @full_path = full_path + @shard = shard @disk_path = disk_path || full_path @container = container @commit_cache = {} @@ -95,7 +96,7 @@ class Repository def path_to_repo @path_to_repo ||= begin - storage = Gitlab.config.repositories.storages[container.repository_storage] + storage = Gitlab.config.repositories.storages[shard] File.expand_path( File.join(storage.legacy_disk_path, disk_path + '.git') @@ -1181,7 +1182,7 @@ class Repository end def initialize_raw_repository - Gitlab::Git::Repository.new(container.repository_storage, + Gitlab::Git::Repository.new(shard, disk_path + '.git', repo_type.identifier_for_container(container), container.full_path) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8f162f17ac5..8bba79bd944 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -261,7 +261,7 @@ class Snippet < ApplicationRecord end def repository - @repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET) + @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET) end def storage diff --git a/changelogs/unreleased/197227-milestone-tab-async.yml b/changelogs/unreleased/197227-milestone-tab-async.yml new file mode 100644 index 00000000000..c2aa3dd4485 --- /dev/null +++ b/changelogs/unreleased/197227-milestone-tab-async.yml @@ -0,0 +1,5 @@ +--- +title: Search issues in GraphQL API by milestone title and assignees +merge_request: 25794 +author: +type: added diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 816512c83bb..5cf48ab901d 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -5404,6 +5404,16 @@ type Project { """ issue( """ + ID of a user assigned to the issues, "none" and "any" values supported + """ + assigneeId: String + + """ + Username of a user assigned to the issues + """ + assigneeUsername: String + + """ Issues closed after this date """ closedAfter: Time @@ -5439,6 +5449,11 @@ type Project { labelName: [String] """ + Milestones applied to this issue + """ + milestoneTitle: [String] + + """ Search query for finding issues by title or description """ search: String @@ -5474,6 +5489,16 @@ type Project { after: String """ + ID of a user assigned to the issues, "none" and "any" values supported + """ + assigneeId: String + + """ + Username of a user assigned to the issues + """ + assigneeUsername: String + + """ Returns the elements in the list that come before the specified cursor. """ before: String @@ -5524,6 +5549,11 @@ type Project { last: Int """ + Milestones applied to this issue + """ + milestoneTitle: [String] + + """ Search query for finding issues by title or description """ search: String diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 2053bdb9404..ba716f82630 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -750,6 +750,40 @@ "defaultValue": null }, { + "name": "milestoneTitle", + "description": "Milestones applied to this issue", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "assigneeUsername", + "description": "Username of a user assigned to the issues", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "assigneeId", + "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { "name": "createdBefore", "description": "Issues created before this date", "type": { @@ -895,6 +929,40 @@ "defaultValue": null }, { + "name": "milestoneTitle", + "description": "Milestones applied to this issue", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "assigneeUsername", + "description": "Username of a user assigned to the issues", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "assigneeId", + "description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { "name": "createdBefore", "description": "Issues created before this date", "type": { diff --git a/doc/api/group_import_export.md b/doc/api/group_import_export.md index c97a753d298..09bc9810615 100644 --- a/doc/api/group_import_export.md +++ b/doc/api/group_import_export.md @@ -1,19 +1,19 @@ # Group Import/Export API -> Introduced in GitLab 12.8 as an experimental feature. May change in future releases. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8 as an experimental feature. May change in future releases. -Group Import/Export functionality allows to export group structure and import it at a new location. -Used in combination with [Project Import/Export](project_import_export.md) it allows you to preserve connections with group level relations -(e.g. a connection between a project issue and group epic). +Group Import/Export allows you to export group structure and import it to a new location. +When used with [Project Import/Export](project_import_export.md), you can preserve connections with +group-level relationships, such as connections between project issues and group epics. -Group Export includes: +Group exports include the following: -1. Group Milestones -1. Group Boards -1. Group Labels -1. Group Badges -1. Group Members -1. Sub-groups (each sub-group includes all data above) +- Group milestones +- Group boards +- Group labels +- Group badges +- Group members +- Sub-groups. Each sub-group includes all data above ## Schedule new export @@ -58,7 +58,11 @@ ls *export.tar.gz 2020-12-05_22-11-148_namespace_export.tar.gz ``` -Time spent on exporting a group may vary depending on a size of the group. Export download endpoint will return exported archive once it is available. 404 is returned otherwise. +Time spent on exporting a group may vary depending on a size of the group. This endpoint +returns either: + +- The exported archive (when available) +- A 404 message ## Import a file @@ -81,3 +85,12 @@ by `@`. For example: ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "name=imported-group" --form "path=imported-group" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/groups/import ``` + +## Important notes + +Note the following: + +- To preserve group-level relationships from imported projects, run Group Import/Export first, + to allow project imports into the desired group structure. +- Imported groups are given a `private` visibility level, unless imported into a parent group. +- If imported into a parent group, subgroups will inherit a similar level of visibility, unless otherwise restricted. diff --git a/doc/development/README.md b/doc/development/README.md index 039b53d7adf..d1ad9216596 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -22,7 +22,7 @@ description: 'Learn how to contribute to GitLab.' - [Code review guidelines](code_review.md) for reviewing code and having code reviewed - [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed -- [Secure coding guidlines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) +- [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) - [Pipelines for the GitLab project](pipelines.md) Complementary reads: diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index cf71d436a15..460bb6d25df 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -222,6 +222,7 @@ requirements. 1. Regressions and bugs are covered with tests that reduce the risk of the issue happening again. 1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed. +1. [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) have been followed. 1. [Documented](../documentation/index.md) in the `/doc` directory. 1. [Changelog entry added](../changelog.md), if necessary. 1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed. diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 716078ed1d1..1a4023b6ded 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -12,6 +12,7 @@ See also: - [Project import/export API](../../../api/project_import_export.md) - [Project import/export administration rake tasks](../../../administration/raketasks/project_import_export.md) **(CORE ONLY)** +- [Group import/export API](../../../api/group_import_export.md) To set up a project import/export: diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 803b3629524..dc8f6c64136 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -94,8 +94,27 @@ describe('text_utility', () => { }); describe('convertToCamelCase', () => { - it('converts snake_case string to camelCase string', () => { - expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase'); + it.each` + txt | result + ${'a_snake_cased_string'} | ${'aSnakeCasedString'} + ${'_leading_underscore'} | ${'_leadingUnderscore'} + ${'__leading_underscores'} | ${'__leadingUnderscores'} + ${'trailing_underscore_'} | ${'trailingUnderscore_'} + ${'trailing_underscores__'} | ${'trailingUnderscores__'} + `('converts string "$txt" to "$result"', ({ txt, result }) => { + expect(textUtils.convertToCamelCase(txt)).toBe(result); + }); + + it.each` + txt + ${'__withoutMiddleUnderscores__'} + ${''} + ${'with spaces'} + ${'with\nnew\r\nlines'} + ${'_'} + ${'___'} + `('does not modify string "$txt"', ({ txt }) => { + expect(textUtils.convertToCamelCase(txt)).toBe(txt); }); }); diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index 78adad13f69..44f6f63fa79 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -37,13 +37,13 @@ describe('Release block header', () => { const link = findHeaderLink(); expect(link.text()).toBe(release.name); - expect(link.attributes('href')).toBe(release.Links.self); + expect(link.attributes('href')).toBe(release._links.self); }); }); describe('when _links.self is missing', () => { beforeEach(() => { - factory({ Links: { self: null } }); + factory({ _links: { self: null } }); }); it('renders the title as text', () => { diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 5d365b77560..ff88e3193bc 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -63,7 +63,7 @@ describe('Release block', () => { it('renders an edit button that links to the "Edit release" page', () => { expect(editButton().exists()).toBe(true); - expect(editButton().attributes('href')).toBe(release.Links.editUrl); + expect(editButton().attributes('href')).toBe(release._links.editUrl); }); it('renders release name', () => { @@ -150,8 +150,8 @@ describe('Release block', () => { }); }); - it("does not render an edit button if release.Links.editUrl isn't a string", () => { - delete release.Links; + it("does not render an edit button if release._links.editUrl isn't a string", () => { + delete release._links; return factory(release).then(() => { expect(editButton().exists()).toBe(false); diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 3fbb7280465..7cfef9b4cc7 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -8,11 +8,13 @@ describe Resolvers::IssuesResolver do let(:current_user) { create(:user) } context "with a project" do - let_it_be(:project) { create(:project) } - let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) } - let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) } - let_it_be(:label1) { create(:label, project: project) } - let_it_be(:label2) { create(:label, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:assignee) { create(:user) } + let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) } + let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) } + let_it_be(:label1) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project) } before do project.add_developer(current_user) @@ -31,6 +33,26 @@ describe Resolvers::IssuesResolver do expect(resolve_issues(state: 'closed')).to contain_exactly(issue2) end + it 'filters by milestone' do + expect(resolve_issues(milestone_title: milestone.title)).to contain_exactly(issue1) + end + + it 'filters by assignee_username' do + expect(resolve_issues(assignee_username: assignee.username)).to contain_exactly(issue2) + end + + it 'filters by assignee_id' do + expect(resolve_issues(assignee_id: assignee.id)).to contain_exactly(issue2) + end + + it 'filters by any assignee' do + expect(resolve_issues(assignee_id: IssuableFinder::FILTER_ANY)).to contain_exactly(issue2) + end + + it 'filters by no assignee' do + expect(resolve_issues(assignee_id: IssuableFinder::FILTER_NONE)).to contain_exactly(issue1) + end + it 'filters by labels' do expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2) expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6c90a1b5614..9b1c724f0c2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1791,21 +1791,19 @@ describe Project do let(:project) { create(:project, :repository) } let(:repo) { double(:repo, exists?: true) } let(:wiki) { double(:wiki, exists?: true) } - let(:design) { double(:wiki, exists?: false) } it 'expires the caches of the repository and wiki' do + # In EE, there are design repositories as well + allow(Repository).to receive(:new).and_call_original + allow(Repository).to receive(:new) - .with('foo', project) + .with('foo', project, shard: project.repository_storage) .and_return(repo) allow(Repository).to receive(:new) - .with('foo.wiki', project) + .with('foo.wiki', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::WIKI) .and_return(wiki) - allow(Repository).to receive(:new) - .with('foo.design', project) - .and_return(design) - expect(repo).to receive(:before_delete) expect(wiki).to receive(:before_delete) |