Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-24 15:09:10 +00:00
parent 257b14287f
commit 7173270eb4
36 changed files with 307 additions and 296 deletions

View File

@ -0,0 +1,11 @@
Before raising an issue to the GitLab issue tracker, please read through our guide for finding help to determine the best place to post:
* https://about.gitlab.com/getting-help/
If you are experiencing an issue when using GitLab.com, your first port of call should be the Community Forum. Your issue may have already been reported there by another user. Please check:
* https://forum.gitlab.com/
If you feel that your issue can be categorized as a reproducible bug or a feature proposal, please use one of the issue templates provided and include as much information as possible.
Thank you for helping to make GitLab a better product.

View File

@ -0,0 +1,44 @@
## What does this MR do and why?
_Describe in detail what your merge request does and why._
<!--
Please keep this description updated with any discussion that takes place so
that reviewers can understand your intent. Keeping the description updated is
especially important if they didn't participate in the discussion.
-->
## Screenshots or screen recordings
_These are strongly recommended to assist reviewers and reduce the time to merge your change._
<!--
Please include any relevant screenshots or screen recordings that will assist
reviewers and future readers. If you need help visually verifying the change,
please leave a comment and ping a GitLab reviewer, maintainer, or MR coach.
-->
## How to set up and validate locally
_Numbered steps to set up and validate the change are strongly suggested._
<!--
Example below:
1. Enable the invite modal
```ruby
Feature.enable(:invite_members_group_modal)
```
1. In rails console enable the experiment fully
```ruby
Feature.enable(:member_areas_of_focus)
```
1. Visit any group or project member pages such as `http://127.0.0.1:3000/groups/flightjs/-/group_members`
1. Click the `invite members` button.
-->
## MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
* [ ] I have evaluated the [MR acceptance checklist](https://docs.gitlab.com/ee/development/code_review.html#acceptance-checklist) for this MR.

View File

@ -48,6 +48,7 @@ AllCops:
Metrics/ParameterLists: Metrics/ParameterLists:
Exclude: Exclude:
# See https://gitlab.com/gitlab-org/gitlab/-/issues/356771
- 'app/components/**/*' - 'app/components/**/*'
Cop/AvoidKeywordArgumentsInSidekiqWorkers: Cop/AvoidKeywordArgumentsInSidekiqWorkers:

View File

@ -107,11 +107,6 @@ Lint/MixedRegexpCaptureTypes:
Lint/RedundantCopDisableDirective: Lint/RedundantCopDisableDirective:
Enabled: false Enabled: false
# Offense count: 1
Lint/SelfAssignment:
Exclude:
- 'spec/lib/gitlab/search_context/builder_spec.rb'
# Offense count: 3 # Offense count: 3
Lint/StructNewOverride: Lint/StructNewOverride:
Exclude: Exclude:
@ -125,11 +120,6 @@ Lint/StructNewOverride:
Lint/UselessMethodDefinition: Lint/UselessMethodDefinition:
Enabled: false Enabled: false
# Offense count: 14
# Configuration parameters: IgnoredMethods.
Metrics/PerceivedComplexity:
Max: 25
# Offense count: 11 # Offense count: 11
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: lowercase, uppercase # SupportedStyles: lowercase, uppercase
@ -333,22 +323,6 @@ Rails/CreateTableWithTimestamps:
Rails/FilePath: Rails/FilePath:
Enabled: false Enabled: false
# Offense count: 15
# Cop supports --auto-correct.
Rails/FindById:
Exclude:
- 'app/controllers/projects/pipelines_controller.rb'
- 'app/services/concerns/deploy_token_methods.rb'
- 'ee/app/controllers/ee/groups/group_members_controller.rb'
- 'ee/lib/api/audit_events.rb'
- 'ee/lib/api/merge_request_approval_rules.rb'
- 'ee/lib/ee/api/groups.rb'
- 'ee/lib/ee/api/projects.rb'
- 'ee/spec/finders/audit_log_finder_spec.rb'
- 'lib/api/snippets.rb'
- 'spec/finders/concerns/finder_methods_spec.rb'
- 'spec/finders/concerns/finder_with_cross_project_access_spec.rb'
# Offense count: 354 # Offense count: 354
# Configuration parameters: Include. # Configuration parameters: Include.
# Include: app/models/**/*.rb # Include: app/models/**/*.rb

View File

@ -0,0 +1,24 @@
---
Metrics/PerceivedComplexity:
Exclude:
- 'app/controllers/admin/application_settings_controller.rb'
- 'app/finders/deployments_finder.rb'
- 'app/helpers/button_helper.rb'
- 'app/helpers/nav/top_nav_helper.rb'
- 'app/helpers/submodule_helper.rb'
- 'app/services/projects/create_service.rb'
- 'ee/app/controllers/ee/groups_controller.rb'
- 'ee/lib/elastic/latest/git_class_proxy.rb'
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
- 'lib/banzai/renderer.rb'
- 'lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb'
- 'lib/gitlab/conflict/file.rb'
- 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb'
- 'lib/gitlab/diff/parser.rb'
- 'lib/gitlab/utils/merge_hash.rb'
- 'qa/qa/runtime/browser.rb'
- 'qa/qa/specs/runner.rb'
- 'qa/qa/support/repeater.rb'
- 'rubocop/cop/gitlab/mark_used_feature_flags.rb'
- 'sidekiq_cluster/cli.rb'
- 'spec/support/cycle_analytics_helpers/test_generation.rb'

View File

@ -1,11 +1,9 @@
import produce from 'immer'; import produce from 'immer';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql'; import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import { resolvers as workItemResolvers } from '~/work_items/graphql/resolvers';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
const resolvers = { const resolvers = {
...workItemResolvers,
Mutation: { Mutation: {
updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => { updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => {
const sourceData = cache.readQuery({ query: getIssueStateQuery }); const sourceData = cache.readQuery({ query: getIssueStateQuery });
@ -14,7 +12,6 @@ const resolvers = {
}); });
cache.writeQuery({ query: getIssueStateQuery, data }); cache.writeQuery({ query: getIssueStateQuery, data });
}, },
...workItemResolvers.Mutation,
}, },
}; };

View File

@ -4,7 +4,3 @@ export const i18n = {
fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'), fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'), updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
}; };
export const widgetTypes = {
title: 'TITLE',
};

View File

@ -1,45 +1,11 @@
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import workItemQuery from './work_item.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
export function createApolloProvider() { export function createApolloProvider() {
Vue.use(VueApollo); Vue.use(VueApollo);
const defaultClient = createDefaultClient(resolvers, { const defaultClient = createDefaultClient();
typeDefs,
cacheConfig: {
possibleTypes: {
LocalWorkItemWidget: ['LocalTitleWidget'],
},
},
});
defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
id: 'gid://gitlab/WorkItem/1',
},
data: {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
// eslint-disable-next-line @gitlab/require-i18n-strings
title: 'Test Work Item',
workItemType: {
__typename: 'WorkItemType',
id: 'work-item-type-1',
name: 'Type', // eslint-disable-line @gitlab/require-i18n-strings
},
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [],
},
},
},
});
return new VueApollo({ return new VueApollo({
defaultClient, defaultClient,

View File

@ -1,29 +0,0 @@
import workItemQuery from './work_item.query.graphql';
export const resolvers = {
Mutation: {
localUpdateWorkItem(_, { input }, { cache }) {
const workItem = {
__typename: 'LocalWorkItem',
type: 'FEATURE',
id: input.id,
title: input.title,
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [],
},
};
cache.writeQuery({
query: workItemQuery,
variables: { id: input.id },
data: { localWorkItem: workItem },
});
return {
__typename: 'LocalUpdateWorkItemPayload',
workItem,
};
},
},
};

View File

@ -1,56 +0,0 @@
enum LocalWorkItemType {
FEATURE
}
enum LocalWidgetType {
TITLE
}
interface LocalWorkItemWidget {
type: LocalWidgetType!
}
# Replicating Relay connection type for client schema
type LocalWorkItemWidgetEdge {
cursor: String!
node: LocalWorkItemWidget
}
type LocalWorkItemWidgetConnection {
edges: [LocalWorkItemWidgetEdge]
nodes: [LocalWorkItemWidget]
pageInfo: PageInfo!
}
type LocalWorkItem {
id: ID!
type: LocalWorkItemType!
title: String!
widgets: [LocalWorkItemWidgetConnection]
}
input LocalCreateWorkItemInput {
title: String!
}
input LocalUpdateWorkItemInput {
id: ID!
title: String
}
type LocalCreateWorkItemPayload {
workItem: LocalWorkItem!
}
type LocalUpdateWorkItemPayload {
workItem: LocalWorkItem!
}
extend type Query {
localWorkItem(id: ID!): LocalWorkItem!
}
extend type Mutation {
localCreateWorkItem(input: LocalCreateWorkItemInput!): LocalCreateWorkItemPayload!
localUpdateWorkItem(input: LocalUpdateWorkItemInput!): LocalUpdateWorkItemPayload!
}

View File

@ -1,3 +0,0 @@
fragment WidgetBase on LocalWorkItemWidget {
type
}

View File

@ -128,10 +128,6 @@ export default {
id, id,
title, title,
workItemType, workItemType,
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [],
},
}, },
}, },
}); });

View File

@ -11,6 +11,6 @@ class Dashboard::ApplicationController < ApplicationController
private private
def projects def projects
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived @projects ||= current_user.authorized_projects.sorted_by_updated_desc.non_archived
end end
end end

View File

@ -268,7 +268,7 @@ class Projects::PipelinesController < Projects::ApplicationController
project project
.all_pipelines .all_pipelines
.includes(builds: :tags, user: :status) .includes(builds: :tags, user: :status)
.find_by!(id: params[:id]) .find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
end end
end end

View File

@ -5,7 +5,11 @@ module Routing
class MaskHelper class MaskHelper
QUERY_PARAMS_TO_NOT_MASK = %w[ QUERY_PARAMS_TO_NOT_MASK = %w[
scope scope
severity
sortBy
sortDesc
state state
tab
].freeze ].freeze
def initialize(request_object, group, project) def initialize(request_object, group, project)

View File

@ -546,8 +546,8 @@ class Project < ApplicationRecord
.or(arel_table[:storage_version].eq(nil))) .or(arel_table[:storage_version].eq(nil)))
end end
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) } scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) } scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) } scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name # Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
@ -783,9 +783,9 @@ class Project < ApplicationRecord
# pass a string to avoid AR adding the table name # pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC') reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc' when 'latest_activity_desc'
reorder(self.arel_table['last_activity_at'].desc) sorted_by_updated_desc
when 'latest_activity_asc' when 'latest_activity_asc'
reorder(self.arel_table['last_activity_at'].asc) sorted_by_updated_asc
when 'stars_desc' when 'stars_desc'
sorted_by_stars_desc sorted_by_stars_desc
when 'stars_asc' when 'stars_asc'
@ -1404,7 +1404,7 @@ class Project < ApplicationRecord
end end
def last_activity_date def last_activity_date
[last_activity_at, last_repository_updated_at, updated_at].compact.max updated_at
end end
def project_id def project_id

View File

@ -11,7 +11,7 @@ module DeployTokenMethods
end end
def destroy_deploy_token(entity, params) def destroy_deploy_token(entity, params)
deploy_token = entity.deploy_tokens.find_by_id!(params[:token_id]) deploy_token = entity.deploy_tokens.find(params[:token_id])
deploy_token.destroy deploy_token.destroy
end end

View File

@ -91,6 +91,8 @@ Example response:
## Create a related epic link ## Create a related epic link
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352840) in GitLab 14.10.
Create a two-way relation between two epics. The user must be allowed to Create a two-way relation between two epics. The user must be allowed to
update both epics to succeed. update both epics to succeed.
@ -205,3 +207,120 @@ Example response:
"link_type": "relates_to" "link_type": "relates_to"
} }
``` ```
## Delete a related epic link
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352840) in GitLab 14.10.
Delete a two-way relation between two epics. The user must be allowed to
update both epics to succeed.
```plaintext
DELETE /groups/:id/epics/:epic_iid/related_epics/:related_epic_link_id
```
Supported attributes:
| Attribute | Type | Required | Description |
|--------------------------|----------------|-----------------------------|---------------------------------------|
| `epic_iid` | integer | **{check-circle}** Yes | Internal ID of a group's epic. |
| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `related_epic_link_id` | integer/string | **{check-circle}** Yes | Internal ID of a related epic link. |
Example request:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/26/epics/1/related_epics/1"
```
Example response:
```json
{
"source_epic": {
"id": 21,
"iid": 1,
"color": "#1068bf",
"text_color": "#FFFFFF",
"group_id": 26,
"parent_id": null,
"parent_iid": null,
"title": "Aspernatur recusandae distinctio omnis et qui est iste.",
"description": "some description",
"confidential": false,
"author": {
"id": 15,
"username": "trina",
"name": "Theresia Robel",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/085e28df717e16484cbf6ceca75e9a93?s=80&d=identicon",
"web_url": "http://gitlab.example.com/trina"
},
"start_date": null,
"end_date": null,
"due_date": null,
"state": "opened",
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/1",
"references": {
"short": "&1",
"relative": "&1",
"full": "flightjs&1"
},
"created_at": "2022-01-31T15:10:44.988Z",
"updated_at": "2022-03-16T09:32:35.712Z",
"closed_at": null,
"labels": [],
"upvotes": 0,
"downvotes": 0,
"_links": {
"self": "http://gitlab.example.com/api/v4/groups/26/epics/1",
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/1/issues",
"group": "http://gitlab.example.com/api/v4/groups/26",
"parent": null
}
},
"target_epic": {
"id": 25,
"iid": 5,
"color": "#1068bf",
"text_color": "#FFFFFF",
"group_id": 26,
"parent_id": null,
"parent_iid": null,
"title": "Aut assumenda id nihil distinctio fugiat vel numquam est.",
"description": "some description",
"confidential": false,
"author": {
"id": 3,
"username": "valerie",
"name": "Erika Wolf",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/9ef7666abb101418a4716a8ed4dded80?s=80&d=identicon",
"web_url": "http://gitlab.example.com/valerie"
},
"start_date": null,
"end_date": null,
"due_date": null,
"state": "opened",
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/5",
"references": {
"short": "&5",
"relative": "&5",
"full": "flightjs&5"
},
"created_at": "2022-01-31T15:10:45.080Z",
"updated_at": "2022-03-16T09:32:35.842Z",
"closed_at": null,
"labels": [],
"upvotes": 0,
"downvotes": 0,
"_links": {
"self": "http://gitlab.example.com/api/v4/groups/26/epics/5",
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/5/issues",
"group": "http://gitlab.example.com/api/v4/groups/26",
"parent": null
}
},
"link_type": "relates_to"
}
```

View File

@ -404,7 +404,7 @@ click_link _('UI testing docs')
fill_in _('Search projects'), with: 'gitlab' # fill in text input with text fill_in _('Search projects'), with: 'gitlab' # fill in text input with text
select _('Last updated'), from: 'Sort by' # select an option from a select input select _('Updated date'), from: 'Sort by' # select an option from a select input
check _('Checkbox label') check _('Checkbox label')
uncheck _('Checkbox label') uncheck _('Checkbox label')
@ -465,8 +465,8 @@ expect(page).to have_checked_field _('Checkbox label')
expect(page).to have_unchecked_field _('Radio input label') expect(page).to have_unchecked_field _('Radio input label')
expect(page).to have_select _('Sort by') expect(page).to have_select _('Sort by')
expect(page).to have_select _('Sort by'), selected: 'Last updated' # assert the option is selected expect(page).to have_select _('Sort by'), selected: 'Updated date' # assert the option is selected
expect(page).to have_select _('Sort by'), options: ['Last updated', 'Created date', 'Due date'] # assert an exact list of options expect(page).to have_select _('Sort by'), options: ['Updated date', 'Created date', 'Due date'] # assert an exact list of options
expect(page).to have_select _('Sort by'), with_options: ['Created date', 'Due date'] # assert a partial list of options expect(page).to have_select _('Sort by'), with_options: ['Created date', 'Due date'] # assert a partial list of options
expect(page).to have_text _('Some paragraph text.') expect(page).to have_text _('Some paragraph text.')

View File

@ -106,7 +106,7 @@ sorts by **Created date** by default, with the newest issues listed at the top:
![Jira issues integration enabled](img/open_jira_issues_list_v14_6.png) ![Jira issues integration enabled](img/open_jira_issues_list_v14_6.png)
- To display the most recently updated issues first, select **Last updated**. - To display the most recently updated issues first, select **Updated date**.
- You can [search and filter](#search-and-filter-the-issues-list) the issues list. - You can [search and filter](#search-and-filter-the-issues-list) the issues list.
- In GitLab [versions 13.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/299832), - In GitLab [versions 13.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/299832),
you can select an issue from the list to view it in GitLab: you can select an issue from the list to view it in GitLab:

View File

@ -79,28 +79,29 @@ To access the Projects page:
By default, all projects are listed, in reverse order of when they were last updated. For each By default, all projects are listed, in reverse order of when they were last updated. For each
project, the following information is listed: project, the following information is listed:
- Name. - Name
- Namespace. - Namespace
- Description. - Description
- Size, updated every 15 minutes at most. - Size, updated every 15 minutes at most
Projects can be edited or deleted. Projects can be edited or deleted.
The list of projects can be sorted by: The list of projects can be sorted by:
- Name. - Updated date
- Last created. - Last created
- Oldest created. - Name
- Last updated. - Most stars
- Oldest updated. - Oldest created
- Owner. - Oldest updated
- Largest repository
A user can choose to hide or show archived projects in the list. A user can choose to hide or show archived projects in the list.
In the **Filter by name** field, type the project name you want to find, and GitLab filters In the **Filter by name** field, type the project name you want to find, and GitLab filters
them as you type. them as you type.
Select from the **Namespace** dropdown to filter only projects in that namespace. To filter only projects in that namespace, select from the **Namespace** dropdown list.
You can combine the filter options. For example, to list only public projects with `score` in their name: You can combine the filter options. For example, to list only public projects with `score` in their name:

View File

@ -6,20 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Sorting and ordering issue lists **(FREE)** # Sorting and ordering issue lists **(FREE)**
You can sort a list of issues several ways, including by: You can sort a list of issues several ways.
- [Blocking issues](#sorting-by-blocking-issues)
- [Created date](#sorting-by-created-date)
- [Due date](#sorting-by-due-date)
- [Label priority](#sorting-by-label-priority)
- [Last updated](#sorting-by-last-updated)
- [Manual sorting](#manual-sorting)
- [Milestone due date](#sorting-by-milestone-due-date)
- [Popularity](#sorting-by-popularity)
- [Priority](#sorting-by-priority)
- [Title](#sorting-by-title)
- [Weight](#sorting-by-weight)
The available sorting options can change based on the context of the list. The available sorting options can change based on the context of the list.
## Sorting by blocking issues **(PREMIUM)** ## Sorting by blocking issues **(PREMIUM)**
@ -51,10 +38,10 @@ For more information, see [issue 14523](https://gitlab.com/gitlab-org/gitlab/-/i
To learn how to change label priority, see [Label priority](../labels.md#set-label-priority). To learn how to change label priority, see [Label priority](../labels.md#set-label-priority).
## Sorting by last updated ## Sorting by updated date
When you sort by **Last updated**, the issue list changes to sort by the time of a last When you sort by **Updated date**, the issue list changes to sort by the time of a last
update. Issues changed the most recently are first. update. Issues changed the most recently are shown first.
## Manual sorting ## Manual sorting

View File

@ -127,7 +127,7 @@ To search for a requirement:
You can also sort the requirements list by: You can also sort the requirements list by:
- Created date - Created date
- Last updated - Updated date
## Allow requirements to be satisfied from a CI job ## Allow requirements to be satisfied from a CI job

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -207,10 +207,14 @@ You can also look for the projects you [starred](../project/working_with_project
You can **Explore** all public and internal projects available in GitLab.com, from which you can filter by visibility, You can **Explore** all public and internal projects available in GitLab.com, from which you can filter by visibility,
through **Trending**, best rated with **Most stars**, or **All** of them. through **Trending**, best rated with **Most stars**, or **All** of them.
You can also sort them by **Name**, **Last created**, **Oldest created**, **Last updated**, You can also sort them by:
**Oldest updated**, **Owner**, and choose to hide or show **archived projects**:
![sort projects](img/sort_projects.png) - Name
- Created date
- Updated date
- Owner
You can also choose to hide or show archived projects.
## Groups ## Groups
@ -221,7 +225,7 @@ On the field **Filter by name**, type the group name you want to find, and GitLa
filters them for you as you type. filters them for you as you type.
You can also **Explore** all public and internal groups available in GitLab.com, You can also **Explore** all public and internal groups available in GitLab.com,
and sort them by **Last created**, **Oldest created**, **Last updated**, or **Oldest updated**. and sort them by **Name**, **Last created**, **Oldest created**, or **Updated date**.
## Issue boards ## Issue boards

View File

@ -200,7 +200,7 @@ module API
get ":id/user_agent_detail" do get ":id/user_agent_detail" do
authenticated_as_admin! authenticated_as_admin!
snippet = Snippet.find_by_id!(params[:id]) snippet = Snippet.find(params[:id])
break not_found!('UserAgentDetail') unless snippet.user_agent_detail break not_found!('UserAgentDetail') unless snippet.user_agent_detail

View File

@ -30,6 +30,7 @@ RSpec.describe FinderMethods do
authorized_project.add_developer(user) authorized_project.add_developer(user)
end end
# rubocop:disable Rails/FindById
describe '#find_by!' do describe '#find_by!' do
it 'returns the project if the user has access' do it 'returns the project if the user has access' do
expect(finder.find_by!(id: authorized_project.id)).to eq(authorized_project) expect(finder.find_by!(id: authorized_project.id)).to eq(authorized_project)
@ -53,6 +54,7 @@ RSpec.describe FinderMethods do
finder.find_by!(id: authorized_project.id) finder.find_by!(id: authorized_project.id)
end end
end end
# rubocop:enable Rails/FindById
describe '#find' do describe '#find' do
it 'returns the project if the user has access' do it 'returns the project if the user has access' do

View File

@ -93,11 +93,11 @@ RSpec.describe FinderWithCrossProjectAccess do
it 'checks the accessibility of the subject directly' do it 'checks the accessibility of the subject directly' do
expect_access_check_on_result expect_access_check_on_result
finder.find_by!(id: result.id) finder.find(result.id)
end end
it 're-enables the check after the find failed' do it 're-enables the check after the find failed' do
finder.find_by!(id: non_existing_record_id) rescue ActiveRecord::RecordNotFound finder.find(non_existing_record_id) rescue ActiveRecord::RecordNotFound
expect(finder.instance_variable_get(:@should_skip_cross_project_check)) expect(finder.instance_variable_get(:@should_skip_cross_project_check))
.to eq(false) .to eq(false)

View File

@ -22,13 +22,14 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle); const findItemTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ loading = false, mutationHandler = mutationSuccessHandler } = {}) => { const createComponent = ({ loading = false, mutationHandler = mutationSuccessHandler } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, { wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]), apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: { propsData: {
loading, loading,
workItemId: workItemQueryResponse.workItem.id, workItemId: id,
workItemTitle: workItemQueryResponse.workItem.title, workItemTitle: title,
workItemType: workItemQueryResponse.workItem.workItemType.name, workItemType: workItemType.name,
}, },
}); });
}; };
@ -61,7 +62,7 @@ describe('WorkItemTitle component', () => {
}); });
it('renders title', () => { it('renders title', () => {
expect(findItemTitle().props('title')).toBe(workItemQueryResponse.workItem.title); expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
}); });
}); });
@ -73,13 +74,18 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', title); findItemTitle().vm.$emit('title-changed', title);
expect(mutationSuccessHandler).toHaveBeenCalledWith({ input: { id: '1', title } }); expect(mutationSuccessHandler).toHaveBeenCalledWith({
input: {
id: workItemQueryResponse.data.workItem.id,
title,
},
});
}); });
it('does not call a mutation when the title has not changed', () => { it('does not call a mutation when the title has not changed', () => {
createComponent(); createComponent();
findItemTitle().vm.$emit('title-changed', workItemQueryResponse.workItem.title); findItemTitle().vm.$emit('title-changed', workItemQueryResponse.data.workItem.title);
expect(mutationSuccessHandler).not.toHaveBeenCalled(); expect(mutationSuccessHandler).not.toHaveBeenCalled();
}); });

View File

@ -1,22 +1,14 @@
export const workItemQueryResponse = { export const workItemQueryResponse = {
workItem: { data: {
__typename: 'WorkItem', workItem: {
id: '1', __typename: 'WorkItem',
title: 'Test', id: 'gid://gitlab/WorkItem/1',
workItemType: { title: 'Test',
__typename: 'WorkItemType', workItemType: {
id: 'work-item-type-1', __typename: 'WorkItemType',
name: 'Task', id: 'gid://gitlab/WorkItems::Type/5',
}, name: 'Task',
widgets: { },
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
contentText: 'Test',
},
],
}, },
}, },
}; };
@ -24,27 +16,16 @@ export const workItemQueryResponse = {
export const updateWorkItemMutationResponse = { export const updateWorkItemMutationResponse = {
data: { data: {
workItemUpdate: { workItemUpdate: {
__typename: 'LocalUpdateWorkItemPayload', __typename: 'WorkItemUpdatePayload',
workItem: { workItem: {
__typename: 'LocalWorkItem', __typename: 'WorkItem',
id: '1', id: 'gid://gitlab/WorkItem/1',
title: 'Updated title', title: 'Updated title',
workItemType: { workItemType: {
__typename: 'WorkItemType', __typename: 'WorkItemType',
id: 'work-item-type-1', id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task', name: 'Task',
}, },
widgets: {
__typename: 'LocalWorkItemWidgetConnection',
nodes: [
{
__typename: 'LocalTitleWidget',
type: 'TITLE',
enabled: true,
contentText: 'Updated title',
},
],
},
}, },
}, },
}, },
@ -53,11 +34,11 @@ export const updateWorkItemMutationResponse = {
export const projectWorkItemTypesQueryResponse = { export const projectWorkItemTypesQueryResponse = {
data: { data: {
workspace: { workspace: {
id: '1', id: 'gid://gitlab/WorkItem/1',
workItemTypes: { workItemTypes: {
nodes: [ nodes: [
{ id: 'work-item-1', name: 'Issue' }, { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
{ id: 'work-item-2', name: 'Incident' }, { id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
], ],
}, },
}, },
@ -70,11 +51,11 @@ export const createWorkItemMutationResponse = {
__typename: 'WorkItemCreatePayload', __typename: 'WorkItemCreatePayload',
workItem: { workItem: {
__typename: 'WorkItem', __typename: 'WorkItem',
id: '1', id: 'gid://gitlab/WorkItem/1',
title: 'Updated title', title: 'Updated title',
workItemType: { workItemType: {
__typename: 'WorkItemType', __typename: 'WorkItemType',
id: 'work-item-type-1', id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task', name: 'Task',
}, },
}, },

View File

@ -6,7 +6,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue'; import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql'; import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
@ -46,14 +45,11 @@ describe('Create work item component', () => {
queryHandler = querySuccessHandler, queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler, mutationHandler = createWorkItemSuccessHandler,
} = {}) => { } = {}) => {
fakeApollo = createMockApollo( fakeApollo = createMockApollo([
[ [projectWorkItemTypesQuery, queryHandler],
[projectWorkItemTypesQuery, queryHandler], [createWorkItemMutation, mutationHandler],
[createWorkItemMutation, mutationHandler], [createWorkItemFromTaskMutation, mutationHandler],
[createWorkItemFromTaskMutation, mutationHandler], ]);
],
resolvers,
);
wrapper = shallowMount(CreateWorkItem, { wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
data() { data() {

View File

@ -16,14 +16,14 @@ describe('WorkItemDetail component', () => {
Vue.use(VueApollo); Vue.use(VueApollo);
const successHandler = jest.fn().mockResolvedValue({ data: workItemQueryResponse }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle); const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const createComponent = ({ const createComponent = ({
workItemId = workItemQueryResponse.workItem.id, workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler, handler = successHandler,
subscriptionHandler = initialSubscriptionHandler, subscriptionHandler = initialSubscriptionHandler,
} = {}) => { } = {}) => {
@ -93,7 +93,7 @@ describe('WorkItemDetail component', () => {
createComponent(); createComponent();
expect(initialSubscriptionHandler).toHaveBeenCalledWith({ expect(initialSubscriptionHandler).toHaveBeenCalledWith({
issuableId: workItemQueryResponse.workItem.id, issuableId: workItemQueryResponse.data.workItem.id,
}); });
}); });
}); });

View File

@ -37,7 +37,7 @@ describe('Work items router', () => {
it('renders work item on `/1` route', async () => { it('renders work item on `/1` route', async () => {
await createComponent('/1'); await createComponent('/1');
expect(wrapper.find(WorkItemsRoot).exists()).toBe(true); expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true);
}); });
it('renders create work item page on `/new` route', async () => { it('renders create work item page on `/new` route', async () => {

View File

@ -180,7 +180,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end end
context 'when some query params are not required to be masked' do context 'when some query params are not required to be masked' do
let(:masked_url) { "http://localhost/dashboard/issues?author_username=masked_author_username&scope=all&state=masked_state" } let(:masked_url) { "http://localhost/dashboard/issues?author_username=masked_author_username&scope=all&state=masked_state&tab=2" }
let(:request) do let(:request) do
double(:Request, double(:Request,
path_parameters: { path_parameters: {
@ -189,11 +189,11 @@ RSpec.describe ::Routing::PseudonymizationHelper do
}, },
protocol: 'http', protocol: 'http',
host: 'localhost', host: 'localhost',
query_string: 'author_username=root&scope=all&state=opened') query_string: 'author_username=root&scope=all&state=opened&tab=2')
end end
before do before do
stub_const('Routing::PseudonymizationHelper::MaskHelper::QUERY_PARAMS_TO_NOT_MASK', %w[scope].freeze) stub_const('Routing::PseudonymizationHelper::MaskHelper::QUERY_PARAMS_TO_NOT_MASK', %w[scope tab].freeze)
allow(helper).to receive(:request).and_return(request) allow(helper).to receive(:request).and_return(request)
end end

View File

@ -43,7 +43,6 @@ RSpec.describe Gitlab::SearchContext::Builder, type: :controller do
def be_search_context(project: nil, group: nil, snippets: [], ref: nil) def be_search_context(project: nil, group: nil, snippets: [], ref: nil)
group = project ? project.group : group group = project ? project.group : group
snippets.compact! snippets.compact!
ref = ref
have_attributes( have_attributes(
project: project, project: project,

View File

@ -1191,29 +1191,8 @@ RSpec.describe Project, factory_default: :keep do
end end
describe 'last_activity_date' do describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do it 'returns the project\'s last update date' do
new_event = create(:event, :closed, project: project, created_at: Time.current) expect(project.last_activity_date).to be_like_time(project.updated_at)
project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
it 'returns the project\'s last update date if it has no events' do
expect(project.last_activity_date).to eq(project.updated_at)
end
it 'returns the most recent timestamp' do
project.update!(updated_at: nil,
last_activity_at: timestamp,
last_repository_updated_at: timestamp - 1.hour)
expect(project.last_activity_date).to be_like_time(timestamp)
project.update!(updated_at: timestamp,
last_activity_at: timestamp - 1.hour,
last_repository_updated_at: nil)
expect(project.last_activity_date).to be_like_time(timestamp)
end end
end end
end end
@ -1690,15 +1669,27 @@ RSpec.describe Project, factory_default: :keep do
end end
describe '.sort_by_attribute' do describe '.sort_by_attribute' do
it 'reorders the input relation by start count desc' do let_it_be(:project1) { create(:project, star_count: 2, updated_at: 1.minute.ago) }
project1 = create(:project, star_count: 2) let_it_be(:project2) { create(:project, star_count: 1) }
project2 = create(:project, star_count: 1) let_it_be(:project3) { create(:project, updated_at: 2.minutes.ago) }
project3 = create(:project)
it 'reorders the input relation by start count desc' do
projects = described_class.sort_by_attribute(:stars_desc) projects = described_class.sort_by_attribute(:stars_desc)
expect(projects).to eq([project1, project2, project3]) expect(projects).to eq([project1, project2, project3])
end end
it 'reorders the input relation by last activity desc' do
projects = described_class.sort_by_attribute(:latest_activity_desc)
expect(projects).to eq([project2, project1, project3])
end
it 'reorders the input relation by last activity asc' do
projects = described_class.sort_by_attribute(:latest_activity_asc)
expect(projects).to eq([project3, project1, project2])
end
end end
describe '.with_shared_runners' do describe '.with_shared_runners' do