Commit 801ced25 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent ed9c54b5
......@@ -124,7 +124,7 @@ export default {
},
{
attrs: {
href: this.newBlobPath,
href: `${this.newBlobPath}${this.currentPath}`,
class: 'qa-new-file-option',
},
text: __('New file'),
......
......@@ -7,6 +7,7 @@ import TreeActionLink from './components/tree_action_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { parseBoolean } from '../lib/utils/common_utils';
import { webIDEUrl } from '../lib/utils/url_utility';
import { __ } from '../locale';
......@@ -42,8 +43,15 @@ export default function setupVueRepositoryList() {
forkNewBlobPath,
forkNewDirectoryPath,
forkUploadBlobPath,
uploadPath,
newDirPath,
} = breadcrumbEl.dataset;
router.afterEach(({ params: { pathMatch = '/' } }) => {
updateFormAction('.js-upload-blob-form', uploadPath, pathMatch);
updateFormAction('.js-create-dir-form', newDirPath, pathMatch);
});
// eslint-disable-next-line no-new
new Vue({
el: breadcrumbEl,
......
// eslint-disable-next-line import/prefer-default-export
export const updateElementsVisibility = (selector, isVisible) => {
document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible));
};
export const updateFormAction = (selector, basePath, path) => {
const form = document.querySelector(selector);
if (form) {
form.action = `${basePath}${path}`;
}
};
# frozen_string_literal: true
module Mutations
module Issues
class Base < BaseMutation
include Mutations::ResolvesProject
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: "The project the issue to mutate is in"
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the issue to mutate"
field :issue,
Types::IssueType,
null: true,
description: "The issue after mutation"
authorize :update_issue
private
def find_object(project_path:, iid:)
project = resolve_project(full_path: project_path)
resolver = Resolvers::IssuesResolver
.single.new(object: project, context: context)
resolver.resolve(iid: iid)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Issues
class SetDueDate < Base
graphql_name 'IssueSetDueDate'
argument :due_date,
Types::TimeType,
required: true,
description: 'The desired due date for the issue'
def resolve(project_path:, iid:, due_date:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
::Issues::UpdateService.new(project, current_user, due_date: due_date)
.execute(issue)
{
issue: issue,
errors: issue.errors.full_messages
}
end
end
end
end
......@@ -9,6 +9,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
......
......@@ -158,7 +158,9 @@ module TreeHelper
def breadcrumb_data_attributes
attrs = {
can_collaborate: can_collaborate_with_project?(@project).to_s,
new_blob_path: project_new_blob_path(@project, @id),
new_blob_path: project_new_blob_path(@project, @ref),
upload_path: project_create_blob_path(@project, @ref),
new_dir_path: project_create_dir_path(@project, @ref),
new_branch_path: new_project_branch_path(@project),
new_tag_path: new_project_tag_path(@project),
can_edit_tree: can_edit_tree?.to_s
......
---
title: Add GraphQL mutation for changing due date of an issue
merge_request: 20577
author:
type: added
---
title: Suppress progress on pulling image on Code Quality of Auto DevOps
merge_request: 20604
author: Takuya Noguchi
type: other
---
title: Add migrations for 'soft-delete for groups' feature
merge_request: 20276
author:
type: added
# frozen_string_literal: true
class AddGroupDeletionSchedules < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
create_table :group_deletion_schedules, id: false do |t|
t.references :group,
foreign_key: { on_delete: :cascade, to_table: :namespaces },
default: nil,
index: false,
primary_key: true
t.references :user,
index: true,
foreign_key: { on_delete: :nullify },
null: false
t.date :marked_for_deletion_on,
index: true,
null: false
end
end
def down
drop_table :group_deletion_schedules
end
end
......@@ -1892,6 +1892,13 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do
t.index ["key", "value"], name: "index_group_custom_attributes_on_key_and_value"
end
create_table "group_deletion_schedules", primary_key: "group_id", id: :bigint, default: nil, force: :cascade do |t|
t.bigint "user_id", null: false
t.date "marked_for_deletion_on", null: false
t.index ["marked_for_deletion_on"], name: "index_group_deletion_schedules_on_marked_for_deletion_on"
t.index ["user_id"], name: "index_group_deletion_schedules_on_user_id"
end
create_table "group_group_links", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
......@@ -4413,6 +4420,8 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "grafana_integrations", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "group_deletion_schedules", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "group_deletion_schedules", "users", on_delete: :nullify
add_foreign_key "group_group_links", "namespaces", column: "shared_group_id", on_delete: :cascade
add_foreign_key "group_group_links", "namespaces", column: "shared_with_group_id", on_delete: :cascade
add_foreign_key "identities", "saml_providers", name: "fk_aade90f0fc", on_delete: :cascade
......
......@@ -23,7 +23,7 @@ No matter how you use GitLab, we have documentation for you.
| Essential Documentation | Essential Documentation |
|:-------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------|
| [**User Documentation**](user/index.md)<br/>Discover features and concepts for GitLab users. | [**Administrator documentation**](administration/index.md)<br/>Everything GitLab self-managed administrators need to know. |
| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have resources to get you started. |
| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have the resources to get you started. |
| [**Building an integration with GitLab?**](#building-an-integration-with-gitlab)<br/>Consult our automation and integration documentation. | [**Coming to GitLab from another platform?**](#coming-to-gitlab-from-another-platform)<br/>Consult our handy guides. |
| [**Install GitLab**](https://about.gitlab.com/install/)<br/>Installation options for different platforms. | [**Customers**](subscriptions/index.md)<br/>Information for new and existing customers. |
| [**Update GitLab**](update/README.md)<br/>Update your GitLab self-managed instance to the latest version. | [**GitLab Releases**](https://about.gitlab.com/releases/)<br/>What's new in GitLab. |
......@@ -412,7 +412,7 @@ Learn more about using Git, and using Git with GitLab:
| Topic | Description |
|:----------------------------------------------------------------------------|:---------------------------------------------------------------------------|
| [Git](topics/git/index.md) | Getting started with Git, branching strategies, Git LFS, and advanced use. |
| [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF describing the most used Git operations. |
| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF describing the most used Git operations. |
| [GitLab Flow](topics/gitlab_flow.md) | Explore the best of Git with the GitLab Flow strategy. |
<div align="right">
......
......@@ -2518,6 +2518,51 @@ type IssuePermissions {
updateIssue: Boolean!
}
"""
Autogenerated input type of IssueSetDueDate
"""
input IssueSetDueDateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The desired due date for the issue
"""
dueDate: Time!
"""
The iid of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of IssueSetDueDate
"""
type IssueSetDueDatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
"""
Values for sorting issues
"""
......@@ -3511,6 +3556,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload
......
......@@ -14112,6 +14112,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issueSetDueDate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "IssueSetDueDateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IssueSetDueDatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequestSetAssignees",
"description": null,
......@@ -14958,6 +14985,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "IssueSetDueDatePayload",
"description": "Autogenerated return type of IssueSetDueDate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issue",
"description": "The issue after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "IssueSetDueDateInput",
"description": "Autogenerated input type of IssueSetDueDate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the issue to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the issue to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "dueDate",
"description": "The desired due date for the issue",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MergeRequestSetLabelsPayload",
......
......@@ -375,6 +375,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
### IssueSetDueDatePayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `issue` | Issue | The issue after mutation |
### Label
| Name | Type | Description |
......
......@@ -7,6 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable"
script:
- |
if ! docker info &>/dev/null; then
......@@ -14,11 +15,12 @@ code_quality:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- docker pull --quiet "$CODE_QUALITY_IMAGE"
- docker run
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" /code
"$CODE_QUALITY_IMAGE" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
......
......@@ -32,18 +32,11 @@ module Gitlab
class Connection < GraphQL::Relay::BaseConnection
include Gitlab::Utils::StrongMemoize
# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
include Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection
def cursor_from_node(node)
return legacy_cursor_from_node(node) if use_legacy_pagination?
encoded_json_from_ordering(node)
end
def sliced_nodes
return legacy_sliced_nodes if use_legacy_pagination?
@sliced_nodes ||=
begin
OrderInfo.validate_ordering(ordered_nodes, order_list)
......
# frozen_string_literal: true
# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
module Gitlab
module Graphql
module Connections
module Keyset
module LegacyKeysetConnection
def legacy_cursor_from_node(node)
encode(node[legacy_order_field].to_s)
end
# rubocop: disable CodeReuse/ActiveRecord
def legacy_sliced_nodes
@sliced_nodes ||=
begin
sliced = nodes
sliced = sliced.where(legacy_before_slice) if before.present?
sliced = sliced.where(legacy_after_slice) if after.present?
sliced
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
def use_legacy_pagination?
strong_memoize(:feature_disabled) do
Feature.disabled?(:graphql_keyset_pagination, default_enabled: true)
end
end
def legacy_before_slice
if legacy_sort_direction == :asc
arel_table[legacy_order_field].lt(decode(before))
else
arel_table[legacy_order_field].gt(decode(before))
end
end
def legacy_after_slice
if legacy_sort_direction == :asc
arel_table[legacy_order_field].gt(decode(after))
else
arel_table[legacy_order_field].lt(decode(after))
end
end
def legacy_order_info
@legacy_order_info ||= nodes.order_values.first
end
def legacy_order_field
@legacy_order_field ||= legacy_order_info&.expr&.name || nodes.primary_key
end
def legacy_sort_direction
@legacy_order_direction ||= legacy_order_info&.direction || :desc
end
end
end
end
end
end
import { setHTMLFixture } from '../../helpers/fixtures';
import { updateElementsVisibility } from '~/repository/utils/dom';
import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom';
describe('updateElementsVisibility', () => {
it('adds hidden class', () => {
......@@ -18,3 +18,13 @@ describe('updateElementsVisibility', () => {
expect(document.querySelector('.js-test').classList).not.toContain('hidden');
});
});
describe('updateFormAction', () => {
it('updates form action', () => {
setHTMLFixture('<form class="js-test" action="/"></form>');
updateFormAction('.js-test', '/gitlab/create', '/test');
expect(document.querySelector('.js-test').action).toBe('http://localhost/gitlab/create/test');
});
});
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Issues::SetDueDate do
let(:issue) { create(:issue) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:due_date) { 2.days.since }
let(:mutated_issue) { subject[:issue] }
subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, due_date: due_date) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the issue' do
before do
issue.project.add_developer(user)
end
it 'returns the issue with updated due date' do
expect(mutated_issue).to eq(issue)
expect(mutated_issue.due_date).to eq(Date.today + 2.days)
expect(subject[:errors]).to be_empty
end
context 'when passing incorrect due date value' do
let(:due_date) { 'test' }
it 'does not update due date' do
expect(mutated_issue.due_date).to eq(issue.due_date)
end
end
end
end
end
# frozen_string_literal: true
# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
require 'spec_helper'
describe Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection do
describe 'old keyset_connection' do
let(:described_class) { Gitlab::Graphql::Connections::Keyset::Connection }
let(:nodes) { Project.all.order(id: :asc) }
let(:arguments) { {} }
subject(:connection) do
described_class.new(nodes, arguments, max_page_size: 3)
end
before do
stub_feature_flags(graphql_keyset_pagination: false)
end
def encoded_property(value)
Base64Bp.urlsafe_encode64(value.to_s, padding: false)
end
describe '#cursor_from_nodes' do
let(:project) { create(:project) }
it 'returns an encoded ID' do
expect(connection.cursor_from_node(project))
.to eq(encoded_property(project.id))
end
context 'when an order was specified' do
let(:nodes) { Project.order(:updated_at) }
it 'returns the encoded value of the order' do
expect(connection.cursor_from_node(project))
.to eq(encoded_property(project.updated_at))
end
end
end
describe '#sliced_nodes' do
let(:projects) { create_list(:project, 4) }
context 'when before is passed' do
let(:arguments) { { before: encoded_property(projects[1].id) } }
it 'only returns the project before the selected one' do
expect(subject.sliced_nodes).to contain_exactly(projects.first)
end
context 'when the sort order is descending' do
let(:nodes) { Project.all.order(id: :desc) }
it 'returns the correct nodes' do
expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
end
end
end
context 'when after is passed' do
let(:arguments) { { after: encoded_property(projects[1].id) } }
it 'only returns the project before the selected one' do
expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
end
context 'when the sort order is descending' do
let(:nodes) { Project.all.order(id: :desc) }
it 'returns the correct nodes' do
expect(subject.sliced_nodes).to contain_exactly(projects.first)
end
end
end
context 'when both before and after are passed' do
let(:arguments) do
{
after: encoded_property(projects[1].id),
before: encoded_property(projects[3].id)
}
end
it 'returns the expected set' do
expect(subject.sliced_nodes).to contain_exactly(projects[2])
end
end
end
describe '#paged_nodes' do
let!(:projects) { create_list(:project, 5) }
it 'returns the collection limited to max page size' do
expect(subject.paged_nodes.size).to eq(3)
end
it 'is a loaded memoized array' do
expect(subject.paged_nodes).to be_an(Array)
expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
end
context 'when `first` is passed' do
let(:arguments) { { first: 2 } }
it 'returns only the first elements' do
expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second)
end
end
context 'when `last` is passed' do
let(:arguments) { { last: 2 } }
it 'returns only the last elements' do
expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4])
end
end
context 'when both are passed' do
let(:arguments) { { first: 2, last: 2 } }
it 'raises an error' do
expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting Due Date of an issue' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:issue) { create(:issue) }
let(:project) { issue.project }
let(:input) { { due_date: 2.days.since } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: issue.iid.to_s
}
graphql_mutation(:issue_set_due_date, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
issue {
iid
dueDate
}
QL
)
end
def mutation_response
graphql_mutation_response(:issue_set_due_date)
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the issue' do
error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => error))
end
it 'updates the issue due date' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['issue']['dueDate']).to eq(2.days.since.to_date.to_s)
end
context 'when passing due date without a date value' do
let(:input) { { due_date: 'test' } }
it 'returns internal server error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to include(a_hash_including('message' => 'Internal server error'))
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment