Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
257b14287f
commit
7173270eb4
|
@ -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.
|
|
@ -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.
|
|
@ -48,6 +48,7 @@ AllCops:
|
|||
|
||||
Metrics/ParameterLists:
|
||||
Exclude:
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/356771
|
||||
- 'app/components/**/*'
|
||||
|
||||
Cop/AvoidKeywordArgumentsInSidekiqWorkers:
|
||||
|
|
|
@ -107,11 +107,6 @@ Lint/MixedRegexpCaptureTypes:
|
|||
Lint/RedundantCopDisableDirective:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
Lint/SelfAssignment:
|
||||
Exclude:
|
||||
- 'spec/lib/gitlab/search_context/builder_spec.rb'
|
||||
|
||||
# Offense count: 3
|
||||
Lint/StructNewOverride:
|
||||
Exclude:
|
||||
|
@ -125,11 +120,6 @@ Lint/StructNewOverride:
|
|||
Lint/UselessMethodDefinition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 14
|
||||
# Configuration parameters: IgnoredMethods.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 25
|
||||
|
||||
# Offense count: 11
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: lowercase, uppercase
|
||||
|
@ -333,22 +323,6 @@ Rails/CreateTableWithTimestamps:
|
|||
Rails/FilePath:
|
||||
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
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
|
|
|
@ -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'
|
|
@ -1,11 +1,9 @@
|
|||
import produce from 'immer';
|
||||
import VueApollo from 'vue-apollo';
|
||||
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';
|
||||
|
||||
const resolvers = {
|
||||
...workItemResolvers,
|
||||
Mutation: {
|
||||
updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => {
|
||||
const sourceData = cache.readQuery({ query: getIssueStateQuery });
|
||||
|
@ -14,7 +12,6 @@ const resolvers = {
|
|||
});
|
||||
cache.writeQuery({ query: getIssueStateQuery, data });
|
||||
},
|
||||
...workItemResolvers.Mutation,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,3 @@ export const i18n = {
|
|||
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.'),
|
||||
};
|
||||
|
||||
export const widgetTypes = {
|
||||
title: 'TITLE',
|
||||
};
|
||||
|
|
|
@ -1,45 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import workItemQuery from './work_item.query.graphql';
|
||||
import { resolvers } from './resolvers';
|
||||
import typeDefs from './typedefs.graphql';
|
||||
|
||||
export function createApolloProvider() {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const defaultClient = createDefaultClient(resolvers, {
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const defaultClient = createDefaultClient();
|
||||
|
||||
return new VueApollo({
|
||||
defaultClient,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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!
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
fragment WidgetBase on LocalWorkItemWidget {
|
||||
type
|
||||
}
|
|
@ -128,10 +128,6 @@ export default {
|
|||
id,
|
||||
title,
|
||||
workItemType,
|
||||
widgets: {
|
||||
__typename: 'LocalWorkItemWidgetConnection',
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -11,6 +11,6 @@ class Dashboard::ApplicationController < ApplicationController
|
|||
private
|
||||
|
||||
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
|
||||
|
|
|
@ -268,7 +268,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
project
|
||||
.all_pipelines
|
||||
.includes(builds: :tags, user: :status)
|
||||
.find_by!(id: params[:id])
|
||||
.find(params[:id])
|
||||
.present(current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,11 @@ module Routing
|
|||
class MaskHelper
|
||||
QUERY_PARAMS_TO_NOT_MASK = %w[
|
||||
scope
|
||||
severity
|
||||
sortBy
|
||||
sortDesc
|
||||
state
|
||||
tab
|
||||
].freeze
|
||||
|
||||
def initialize(request_object, group, project)
|
||||
|
|
|
@ -546,8 +546,8 @@ class Project < ApplicationRecord
|
|||
.or(arel_table[:storage_version].eq(nil)))
|
||||
end
|
||||
|
||||
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
|
||||
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_asc, -> { reorder(self.arel_table['updated_at'].asc) }
|
||||
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_asc, -> { reorder(self.arel_table['star_count'].asc) }
|
||||
# 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
|
||||
reorder('project_statistics.storage_size DESC, projects.id DESC')
|
||||
when 'latest_activity_desc'
|
||||
reorder(self.arel_table['last_activity_at'].desc)
|
||||
sorted_by_updated_desc
|
||||
when 'latest_activity_asc'
|
||||
reorder(self.arel_table['last_activity_at'].asc)
|
||||
sorted_by_updated_asc
|
||||
when 'stars_desc'
|
||||
sorted_by_stars_desc
|
||||
when 'stars_asc'
|
||||
|
@ -1404,7 +1404,7 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def last_activity_date
|
||||
[last_activity_at, last_repository_updated_at, updated_at].compact.max
|
||||
updated_at
|
||||
end
|
||||
|
||||
def project_id
|
||||
|
|
|
@ -11,7 +11,7 @@ module DeployTokenMethods
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
|
|
@ -91,6 +91,8 @@ Example response:
|
|||
|
||||
## 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
|
||||
update both epics to succeed.
|
||||
|
||||
|
@ -205,3 +207,120 @@ Example response:
|
|||
"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"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -404,7 +404,7 @@ click_link _('UI testing docs')
|
|||
|
||||
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')
|
||||
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_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'), options: ['Last updated', 'Created date', 'Due date'] # assert an exact list of options
|
||||
expect(page).to have_select _('Sort by'), selected: 'Updated date' # assert the option is selected
|
||||
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_text _('Some paragraph text.')
|
||||
|
|
|
@ -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)
|
||||
|
||||
- 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.
|
||||
- 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:
|
||||
|
|
|
@ -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
|
||||
project, the following information is listed:
|
||||
|
||||
- Name.
|
||||
- Namespace.
|
||||
- Description.
|
||||
- Size, updated every 15 minutes at most.
|
||||
- Name
|
||||
- Namespace
|
||||
- Description
|
||||
- Size, updated every 15 minutes at most
|
||||
|
||||
Projects can be edited or deleted.
|
||||
|
||||
The list of projects can be sorted by:
|
||||
|
||||
- Name.
|
||||
- Last created.
|
||||
- Oldest created.
|
||||
- Last updated.
|
||||
- Oldest updated.
|
||||
- Owner.
|
||||
- Updated date
|
||||
- Last created
|
||||
- Name
|
||||
- Most stars
|
||||
- Oldest created
|
||||
- Oldest updated
|
||||
- Largest repository
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
|
|
|
@ -6,20 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Sorting and ordering issue lists **(FREE)**
|
||||
|
||||
You can sort a list of issues several ways, including by:
|
||||
|
||||
- [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)
|
||||
|
||||
You can sort a list of issues several ways.
|
||||
The available sorting options can change based on the context of the list.
|
||||
|
||||
## 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).
|
||||
|
||||
## 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
|
||||
update. Issues changed the most recently are first.
|
||||
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 shown first.
|
||||
|
||||
## Manual sorting
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ To search for a requirement:
|
|||
You can also sort the requirements list by:
|
||||
|
||||
- Created date
|
||||
- Last updated
|
||||
- Updated date
|
||||
|
||||
## Allow requirements to be satisfied from a CI job
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
|
@ -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,
|
||||
through **Trending**, best rated with **Most stars**, or **All** of them.
|
||||
|
||||
You can also sort them by **Name**, **Last created**, **Oldest created**, **Last updated**,
|
||||
**Oldest updated**, **Owner**, and choose to hide or show **archived projects**:
|
||||
You can also sort them by:
|
||||
|
||||
![sort projects](img/sort_projects.png)
|
||||
- Name
|
||||
- Created date
|
||||
- Updated date
|
||||
- Owner
|
||||
|
||||
You can also choose to hide or show archived projects.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -200,7 +200,7 @@ module API
|
|||
get ":id/user_agent_detail" do
|
||||
authenticated_as_admin!
|
||||
|
||||
snippet = Snippet.find_by_id!(params[:id])
|
||||
snippet = Snippet.find(params[:id])
|
||||
|
||||
break not_found!('UserAgentDetail') unless snippet.user_agent_detail
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ RSpec.describe FinderMethods do
|
|||
authorized_project.add_developer(user)
|
||||
end
|
||||
|
||||
# rubocop:disable Rails/FindById
|
||||
describe '#find_by!' do
|
||||
it 'returns the project if the user has access' do
|
||||
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)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/FindById
|
||||
|
||||
describe '#find' do
|
||||
it 'returns the project if the user has access' do
|
||||
|
|
|
@ -93,11 +93,11 @@ RSpec.describe FinderWithCrossProjectAccess do
|
|||
it 'checks the accessibility of the subject directly' do
|
||||
expect_access_check_on_result
|
||||
|
||||
finder.find_by!(id: result.id)
|
||||
finder.find(result.id)
|
||||
end
|
||||
|
||||
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))
|
||||
.to eq(false)
|
||||
|
|
|
@ -22,13 +22,14 @@ describe('WorkItemTitle component', () => {
|
|||
const findItemTitle = () => wrapper.findComponent(ItemTitle);
|
||||
|
||||
const createComponent = ({ loading = false, mutationHandler = mutationSuccessHandler } = {}) => {
|
||||
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemTitle, {
|
||||
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
|
||||
propsData: {
|
||||
loading,
|
||||
workItemId: workItemQueryResponse.workItem.id,
|
||||
workItemTitle: workItemQueryResponse.workItem.title,
|
||||
workItemType: workItemQueryResponse.workItem.workItemType.name,
|
||||
workItemId: id,
|
||||
workItemTitle: title,
|
||||
workItemType: workItemType.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -61,7 +62,7 @@ describe('WorkItemTitle component', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
||||
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', () => {
|
||||
createComponent();
|
||||
|
||||
findItemTitle().vm.$emit('title-changed', workItemQueryResponse.workItem.title);
|
||||
findItemTitle().vm.$emit('title-changed', workItemQueryResponse.data.workItem.title);
|
||||
|
||||
expect(mutationSuccessHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
export const workItemQueryResponse = {
|
||||
workItem: {
|
||||
__typename: 'WorkItem',
|
||||
id: '1',
|
||||
title: 'Test',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'work-item-type-1',
|
||||
name: 'Task',
|
||||
},
|
||||
widgets: {
|
||||
__typename: 'LocalWorkItemWidgetConnection',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'LocalTitleWidget',
|
||||
type: 'TITLE',
|
||||
contentText: 'Test',
|
||||
},
|
||||
],
|
||||
data: {
|
||||
workItem: {
|
||||
__typename: 'WorkItem',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
title: 'Test',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -24,27 +16,16 @@ export const workItemQueryResponse = {
|
|||
export const updateWorkItemMutationResponse = {
|
||||
data: {
|
||||
workItemUpdate: {
|
||||
__typename: 'LocalUpdateWorkItemPayload',
|
||||
__typename: 'WorkItemUpdatePayload',
|
||||
workItem: {
|
||||
__typename: 'LocalWorkItem',
|
||||
id: '1',
|
||||
__typename: 'WorkItem',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
title: 'Updated title',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'work-item-type-1',
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
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 = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: '1',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
workItemTypes: {
|
||||
nodes: [
|
||||
{ id: 'work-item-1', name: 'Issue' },
|
||||
{ id: 'work-item-2', name: 'Incident' },
|
||||
{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
|
||||
{ id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -70,11 +51,11 @@ export const createWorkItemMutationResponse = {
|
|||
__typename: 'WorkItemCreatePayload',
|
||||
workItem: {
|
||||
__typename: 'WorkItem',
|
||||
id: '1',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
title: 'Updated title',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'work-item-type-1',
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CreateWorkItem from '~/work_items/pages/create_work_item.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 createWorkItemMutation from '~/work_items/graphql/create_work_item.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,
|
||||
mutationHandler = createWorkItemSuccessHandler,
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo(
|
||||
[
|
||||
[projectWorkItemTypesQuery, queryHandler],
|
||||
[createWorkItemMutation, mutationHandler],
|
||||
[createWorkItemFromTaskMutation, mutationHandler],
|
||||
],
|
||||
resolvers,
|
||||
);
|
||||
fakeApollo = createMockApollo([
|
||||
[projectWorkItemTypesQuery, queryHandler],
|
||||
[createWorkItemMutation, mutationHandler],
|
||||
[createWorkItemFromTaskMutation, mutationHandler],
|
||||
]);
|
||||
wrapper = shallowMount(CreateWorkItem, {
|
||||
apolloProvider: fakeApollo,
|
||||
data() {
|
||||
|
|
|
@ -16,14 +16,14 @@ describe('WorkItemDetail component', () => {
|
|||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const successHandler = jest.fn().mockResolvedValue({ data: workItemQueryResponse });
|
||||
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
|
||||
|
||||
const createComponent = ({
|
||||
workItemId = workItemQueryResponse.workItem.id,
|
||||
workItemId = workItemQueryResponse.data.workItem.id,
|
||||
handler = successHandler,
|
||||
subscriptionHandler = initialSubscriptionHandler,
|
||||
} = {}) => {
|
||||
|
@ -93,7 +93,7 @@ describe('WorkItemDetail component', () => {
|
|||
createComponent();
|
||||
|
||||
expect(initialSubscriptionHandler).toHaveBeenCalledWith({
|
||||
issuableId: workItemQueryResponse.workItem.id,
|
||||
issuableId: workItemQueryResponse.data.workItem.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Work items router', () => {
|
|||
it('renders work item on `/1` route', async () => {
|
||||
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 () => {
|
||||
|
|
|
@ -180,7 +180,7 @@ RSpec.describe ::Routing::PseudonymizationHelper do
|
|||
end
|
||||
|
||||
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
|
||||
double(:Request,
|
||||
path_parameters: {
|
||||
|
@ -189,11 +189,11 @@ RSpec.describe ::Routing::PseudonymizationHelper do
|
|||
},
|
||||
protocol: 'http',
|
||||
host: 'localhost',
|
||||
query_string: 'author_username=root&scope=all&state=opened')
|
||||
query_string: 'author_username=root&scope=all&state=opened&tab=2')
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@ RSpec.describe Gitlab::SearchContext::Builder, type: :controller do
|
|||
def be_search_context(project: nil, group: nil, snippets: [], ref: nil)
|
||||
group = project ? project.group : group
|
||||
snippets.compact!
|
||||
ref = ref
|
||||
|
||||
have_attributes(
|
||||
project: project,
|
||||
|
|
|
@ -1191,29 +1191,8 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
|
||||
describe 'last_activity_date' do
|
||||
it 'returns the creation date of the project\'s last event if present' do
|
||||
new_event = create(:event, :closed, project: project, created_at: Time.current)
|
||||
|
||||
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)
|
||||
it 'returns the project\'s last update date' do
|
||||
expect(project.last_activity_date).to be_like_time(project.updated_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1690,15 +1669,27 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
|
||||
describe '.sort_by_attribute' do
|
||||
it 'reorders the input relation by start count desc' do
|
||||
project1 = create(:project, star_count: 2)
|
||||
project2 = create(:project, star_count: 1)
|
||||
project3 = create(:project)
|
||||
let_it_be(:project1) { create(:project, star_count: 2, updated_at: 1.minute.ago) }
|
||||
let_it_be(:project2) { create(:project, star_count: 1) }
|
||||
let_it_be(:project3) { create(:project, updated_at: 2.minutes.ago) }
|
||||
|
||||
it 'reorders the input relation by start count desc' do
|
||||
projects = described_class.sort_by_attribute(:stars_desc)
|
||||
|
||||
expect(projects).to eq([project1, project2, project3])
|
||||
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
|
||||
|
||||
describe '.with_shared_runners' do
|
||||
|
|
Loading…
Reference in New Issue