Commit a2bebc74 authored by Clement Ho's avatar Clement Ho

Merge branch 'multiple_assignees_review' into multiple-assignees-issue-board-sidebar

parents d14e6e6a 973f7588
......@@ -97,7 +97,7 @@
.avatar-counter {
background-color: $gray-darkest;
color: $white-light;
border: 1px solid $avatar-counter-border-color;
border: 1px solid $border-color;
border-radius: 1em;
font-family: $regular_font;
font-size: 9px;
......
......@@ -439,7 +439,6 @@ $help-shortcut-header-color: #333;
$issues-today-bg: #f3fff2;
$issues-today-border: #e1e8d5;
$compare-display-color: #888;
$avatar-counter-border-color: #e6e6e6;
/*
* jQuery UI
......
......@@ -81,10 +81,14 @@ class Issue < ActiveRecord::Base
end
def hook_attrs
assignee_ids = self.assignee_ids
attrs = {
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate
human_time_estimate: human_time_estimate,
assignee_ids: assignee_ids,
assignee_id: assignee_ids.first # This key is deprecated
}
attributes.merge!(attrs)
......
......@@ -76,6 +76,14 @@ Example response:
"avatar_url" : null,
"username" : "root"
}],
"assignee" : {
"state" : "active",
"id" : 1,
"name" : "Administrator",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root"
},
"updated_at" : "2016-01-04T15:31:51.081Z",
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
......@@ -91,6 +99,8 @@ Example response:
]
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## List group issues
Get a list of a group's issues.
......@@ -158,6 +168,14 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -172,6 +190,8 @@ Example response:
]
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## List project issues
Get a list of a project's issues.
......@@ -239,6 +259,14 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -253,6 +281,8 @@ Example response:
]
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Single issue
Get a single project issue.
......@@ -305,6 +335,14 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
}],
"assignee" : {
"avatar_url" : null,
"web_url" : "https://gitlab.example.com/lennie",
"state" : "active",
"username" : "lennie",
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
......@@ -319,6 +357,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## New issue
Creates a new project issue.
......@@ -357,6 +397,7 @@ Example response:
"title" : "Issues with auth",
"state" : "opened",
"assignees" : [],
"assignee" : null,
"labels" : [
"bug"
],
......@@ -380,6 +421,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Edit issue
Updates an existing project issue. This call is also used to mark an issue as
......@@ -432,6 +475,7 @@ Example response:
],
"id" : 85,
"assignees" : [],
"assignee" : null,
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
......@@ -442,6 +486,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Delete an issue
Only for admins and project owners. Soft deletes the issue in question.
......@@ -504,6 +550,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
}],
"assignee": {
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
},
"author": {
"name": "Kris Steuber",
"username": "solon.cremin",
......@@ -519,6 +573,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications.
......@@ -560,6 +616,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
}],
"assignee": {
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
},
"author": {
"name": "Kris Steuber",
"username": "solon.cremin",
......@@ -575,6 +639,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications
......@@ -664,6 +730,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/francisca"
}],
"assignee": {
"name": "Jarret O'Keefe",
"username": "francisca",
"id": 14,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
"web_url": "https://gitlab.example.com/francisca"
},
"author": {
"name": "Maxie Medhurst",
"username": "craig_rutherford",
......@@ -688,6 +762,8 @@ Example response:
}
```
**Note**: `assignee` column is deprecated, it shows the first assignee only.
## Set a time estimate for an issue
Sets an estimated time of work for this issue.
......
......@@ -233,6 +233,7 @@ X-Gitlab-Event: Issue Hook
"id": 301,
"title": "New API: create/update/delete file",
"assignee_ids": [51],
"assignee_id": 51,
"author_id": 51,
"project_id": 14,
"created_at": "2013-12-03T17:15:43Z",
......@@ -251,6 +252,11 @@ X-Gitlab-Event: Issue Hook
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}],
"assignee": {
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"labels": [{
"id": 206,
"title": "API",
......@@ -265,6 +271,9 @@ X-Gitlab-Event: Issue Hook
}]
}
```
**Note**: `assignee` and `assignee_id` keys are deprecated and now show the first assignee only.
### Comment events
Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
......@@ -545,6 +554,7 @@ X-Gitlab-Event: Note Hook
"id": 92,
"title": "test",
"assignee_ids": [],
"assignee_id": null,
"author_id": 1,
"project_id": 5,
"created_at": "2015-04-12 14:53:17 UTC",
......@@ -559,6 +569,8 @@ X-Gitlab-Event: Note Hook
}
```
**Note**: `assignee_id` field is deprecated and now shows the first assignee only.
#### Comment on code snippet
**Request header**:
......
......@@ -6,6 +6,7 @@ module API
version 'v3', using: :path do
helpers ::API::V3::Helpers
helpers ::API::Helpers::CommonHelpers
mount ::API::V3::AwardEmoji
mount ::API::V3::Boards
......@@ -81,6 +82,7 @@ module API
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
helpers ::SentryHelper
helpers ::API::Helpers
helpers ::API::Helpers::CommonHelpers
# Keep in alphabetical order
mount ::API::AccessRequests
......
......@@ -281,6 +281,10 @@ module API
expose :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic
expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
issue.assignees.first
end
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
......
module API
module Helpers
module CommonHelpers
def convert_parameters_from_legacy_format(params)
if params[:assignee_id].present?
params[:assignee_ids] = [params.delete(:assignee_id)]
end
params
end
end
end
end
......@@ -31,7 +31,8 @@ module API
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The ID of a user to assign issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
......@@ -132,6 +133,8 @@ module API
issue_params = declared_params(include_missing: false)
issue_params = convert_parameters_from_legacy_format(issue_params)
issue = ::Issues::CreateService.new(user_project,
current_user,
issue_params.merge(request: request, api: true)).execute
......@@ -156,7 +159,7 @@ module API
desc: 'Date time when the issue was updated. Available only for admins and project owners.'
optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
use :issue_params
at_least_one_of :title, :description, :assignee_ids, :milestone_id,
at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
:labels, :created_at, :due_date, :confidential, :state_event,
:weight
end
......@@ -171,6 +174,8 @@ module API
update_params = declared_params(include_missing: false).merge(request: request, api: true)
update_params = convert_parameters_from_legacy_format(update_params)
issue = ::Issues::UpdateService.new(user_project,
current_user,
update_params).execute(issue)
......
......@@ -14,14 +14,6 @@ module API
authorize! access_level, merge_request
merge_request
end
def convert_parameters_from_legacy_format(params)
if params[:assignee_id].present?
params[:assignee_ids] = [params.delete(:assignee_id)]
end
params
end
end
end
end
......@@ -48,6 +48,18 @@
"additionalProperties": false
}
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
......
......@@ -40,6 +40,7 @@ describe('Issue card component', () => {
iid: 1,
confidential: false,
labels: [list.label],
assignees: [],
});
component = new Vue({
......@@ -92,12 +93,12 @@ describe('Issue card component', () => {
it('renders confidential icon', (done) => {
component.issue.confidential = true;
setTimeout(() => {
Vue.nextTick(() => {
expect(
component.$el.querySelector('.confidential-icon'),
).not.toBeNull();
done();
}, 0);
});
});
it('renders issue ID with #', () => {
......@@ -109,34 +110,32 @@ describe('Issue card component', () => {
describe('assignee', () => {
it('does not render assignee', () => {
expect(
component.$el.querySelector('.card-assignee'),
component.$el.querySelector('.card-assignee .avatar'),
).toBeNull();
});
describe('exists', () => {
beforeEach((done) => {
component.issue.assignee = user;
component.issue.assignees = [user];
setTimeout(() => {
done();
}, 0);
Vue.nextTick(() => done());
});
it('renders assignee', () => {
expect(
component.$el.querySelector('.card-assignee'),
component.$el.querySelector('.card-assignee .avatar'),
).not.toBeNull();
});
it('sets title', () => {
expect(
component.$el.querySelector('.card-assignee').getAttribute('title'),
component.$el.querySelector('.card-assignee a').getAttribute('title'),
).toContain(`Assigned to ${user.name}`);
});
it('sets users path', () => {
expect(
component.$el.querySelector('.card-assignee').getAttribute('href'),
component.$el.querySelector('.card-assignee a').getAttribute('href'),
).toBe('/test');
});
......@@ -148,6 +147,76 @@ describe('Issue card component', () => {
});
});
describe('multiple assignees', () => {
beforeEach((done) => {
component.issue.assignees = [
user,
new ListUser({
id: 2,
name: 'user2',
username: 'user2',
avatar: 'test_image',
}),
new ListUser({
id: 3,
name: 'user3',
username: 'user3',
avatar: 'test_image',
}),
new ListUser({
id: 4,
name: 'user4',
username: 'user4',
avatar: 'test_image',
})];
Vue.nextTick(() => done());
});
it('renders all four assignees', () => {
expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4);
});
describe('more than four assignees', () => {
beforeEach((done) => {
component.issue.assignees.push(new ListUser({
id: 5,
name: 'user5',
username: 'user5',
avatar: 'test_image',
}));
Vue.nextTick(() => done());
});
it('renders more avatar counter', () => {
expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2');
});
it('renders three assignees', () => {
expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3);
});
it('renders 99+ avatar counter', (done) => {
for(let i = 5; i < 104; i++) {
const u = new ListUser({
id: i,
name: 'name',
username: 'username',
avatar: 'test_image',
});
component.issue.assignees.push(u);
}
Vue.nextTick(() => {
expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+');
done();
});
});
});
})
describe('labels', () => {
it('does not render any', () => {
expect(
......@@ -159,9 +228,7 @@ describe('Issue card component', () => {
beforeEach((done) => {
component.issue.addLabel(label1);
setTimeout(() => {
done();
}, 0);
Vue.nextTick(() => done());
});
it('does not render list label', () => {
......
......@@ -27,7 +27,8 @@ describe('Issue model', () => {
title: 'test',
color: 'red',
description: 'testing'
}]
}],
assignees: [],
});
});
......@@ -90,7 +91,8 @@ describe('Issue model', () => {
iid: 1,
confidential: false,
relative_position: 1,
labels: []
labels: [],
assignees: [],
});
expect(relativePositionIssue.position).toBe(1);
......
......@@ -94,7 +94,8 @@ describe('List model', () => {
title: 'Testing',
iid: 1,
confidential: false,
labels: [list.label, listDup.label]
labels: [list.label, listDup.label],
assignees: [],
});
list.issues.push(issue);
......
......@@ -21,12 +21,14 @@ describe('Modal store', () => {
iid: 1,
confidential: false,
labels: [],
assignees: [],
});
issue2 = new ListIssue({
title: 'Testing',
iid: 2,
confidential: false,
labels: [],
assignees: [],
});
Store.store.issues.push(issue);
Store.store.issues.push(issue2);
......
......@@ -730,6 +730,11 @@ describe Issue, models: true do
expect(attrs_hash).to include(:human_total_time_spent)
expect(attrs_hash).to include('time_estimate')
end
it 'includes assignee_ids and deprecated assignee_id' do
expect(attrs_hash).to include(:assignee_id)
expect(attrs_hash).to include(:assignee_ids)
end
end
describe '#check_for_spam' do
......
......@@ -789,6 +789,7 @@ describe API::Issues, api: true do
expect(json_response['labels']).to eq(issue.label_names)
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignees']).to be_a Array
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to be_nil
......@@ -861,9 +862,22 @@ describe API::Issues, api: true do
end
describe "POST /projects/:id/issues" do
context 'support for deprecated assignee_id' do
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3
title: 'new issue', assignee_id: user2.id
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
end
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
assignee_ids: [user2.id]
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
......@@ -871,6 +885,8 @@ describe API::Issues, api: true do
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
it 'creates a new confidential project issue' do
......@@ -1160,6 +1176,46 @@ describe API::Issues, api: true do
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_id: 0
expect(response).to have_http_status(200)
expect(json_response['assignee']).to be_nil
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_id: user2.id
expect(response).to have_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
it 'removes assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [0]
expect(response).to have_http_status(200)
expect(json_response['assignees']).to be_empty
end
it 'updates an issue with new assignee' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id]
expect(response).to have_http_status(200)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
end
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
......
......@@ -731,7 +731,7 @@ describe API::V3::Issues, api: true do
describe "POST /projects/:id/issues" do
it 'creates a new project issue' do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3
title: 'new issue', labels: 'label, label2', weight: 3, assignee_id: assignee.id
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
......@@ -739,6 +739,7 @@ describe API::V3::Issues, api: true do
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(assignee.name)
end
it 'creates a new confidential project issue' do
......@@ -1135,6 +1136,22 @@ describe API::V3::Issues, api: true do
end
end
describe 'PUT /projects/:id/issues/:issue_id to update assignee' do
it 'updates an issue with no assignee' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0
expect(response).to have_http_status(200)
expect(json_response['assignee']).to eq(nil)
end
it 'updates an issue with assignee' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id
expect(response).to have_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), weight: 5
......
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