Commit 038c9a65 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ui/dashboard-new-issue' into 'master'

UI: Add "New X" buttons to dashboard and group issue, MR and milestone indexes

# To do

- [x] Use searchable dropdown since dashboard/group can have a lot of projects. Use select2?

## Before

![Screen_Shot_2015-12-07_at_17.26.52](/uploads/22c6d6df10414f9e3e35d6cea3870486/Screen_Shot_2015-12-07_at_17.26.52.png)

## After

![Screen_Shot_2015-12-07_at_17.26.33](/uploads/02d082490ed6c83c66f052a5b601b5be/Screen_Shot_2015-12-07_at_17.26.33.png)

As you can see, for milestones, groups are listed as well as we can now easily create group milestones.

Fixes #3544 and https://dev.gitlab.org/gitlab/gitlabhq/issues/2581

See merge request !1968
parents 033947de 15925290
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
groups_path: "/api/:version/groups.json" groups_path: "/api/:version/groups.json"
group_path: "/api/:version/groups/:id.json" group_path: "/api/:version/groups/:id.json"
namespaces_path: "/api/:version/namespaces.json" namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
group: (group_id, callback) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path) url = Api.buildUrl(Api.group_path)
...@@ -44,6 +46,35 @@ ...@@ -44,6 +46,35 @@
).done (namespaces) -> ).done (namespaces) ->
callback(namespaces) callback(namespaces)
# Return projects list. Filtered by query
projects: (query, callback) ->
url = Api.buildUrl(Api.projects_path)
$.ajax(
url: url
data:
private_token: gon.api_token
search: query
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
url = url.replace(':id', group_id)
$.ajax(
url: url
data:
private_token: gon.api_token
search: query
per_page: 20
dataType: "json"
).done (projects) ->
callback(projects)
buildUrl: (url) -> buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root? url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version) return url.replace(':version', gon.api_version)
class @ProjectSelect
constructor: ->
$('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups')
placeholder = "Search for project"
placeholder += " or group" if @includeGroups
$(select).select2
placeholder: placeholder
minimumInputLength: 0
query: (query) =>
finalCallback = (projects) ->
data = { results: projects }
query.callback(data)
if @includeGroups
projectsCallback = (projects) ->
groupsCallback = (groups) ->
data = groups.concat(projects)
finalCallback(data)
Api.groups query.term, false, groupsCallback
else
projectsCallback = finalCallback
if @groupId
Api.groupProjects @groupId, query.term, projectsCallback
else
Api.projects query.term, projectsCallback
id: (project) ->
project.web_url
text: (project) ->
project.name_with_namespace || project.name
dropdownCssClass: "ajax-project-dropdown"
...@@ -437,3 +437,16 @@ table { ...@@ -437,3 +437,16 @@ table {
.alert, .progress { .alert, .progress {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
} }
.new-project-item-select-holder {
display: inline-block;
position: relative;
.new-project-item-select {
position: absolute;
top: 0;
right: 0;
width: 250px !important;
visibility: hidden;
}
}
...@@ -48,6 +48,19 @@ module SelectsHelper ...@@ -48,6 +48,19 @@ module SelectsHelper
select2_tag(id, opts) select2_tag(id, opts)
end end
def project_select_tag(id, opts = {})
opts[:class] ||= ''
opts[:class] << ' ajax-project-select'
unless opts.delete(:scope) == :all
if @group
opts['data-group-id'] = @group.id
end
end
hidden_field_tag(id, opts[:selected], opts)
end
def select2_tag(id, opts = {}) def select2_tag(id, opts = {})
css_class = '' css_class = ''
css_class << 'multiselect ' if opts[:multiple] css_class << 'multiselect ' if opts[:multiple]
......
...@@ -4,14 +4,20 @@ ...@@ -4,14 +4,20 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
.project-issuable-filter
.controls
.pull-left
- if current_user
.hidden-xs.pull-left
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
%i.fa.fa-rss
.append-bottom-20 = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
.pull-right
- if current_user
.hidden-xs.pull-left.prepend-top-20
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do
%i.fa.fa-rss
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
= render 'shared/issues' .gray-content-block.second-block
List all issues from all projects you have access to.
.prepend-top-default
= render 'shared/issues'
- page_title "Merge Requests" - page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
.append-bottom-20 .project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests'
.gray-content-block.second-block
List all merge requests from all projects you have access to.
.prepend-top-default
= render 'shared/merge_requests'
- page_title "Milestones" - page_title "Milestones"
- header_title "Milestones", dashboard_milestones_path - header_title "Milestones", dashboard_milestones_path
.project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
= render 'shared/milestones_filter' = render 'shared/milestones_filter'
.gray-content-block .gray-content-block
.oneline List all milestones from all projects you have access to.
List all milestones from all projects you have access to.
.milestones .milestones
%ul.content-list %ul.content-list
......
...@@ -4,21 +4,24 @@ ...@@ -4,21 +4,24 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
.project-issuable-filter
.controls
.pull-left
- if current_user
.hidden-xs.pull-left
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
%i.fa.fa-rss
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block .gray-content-block.second-block
.pull-right Only issues from
- if current_user %strong #{@group.name}
.hidden-xs.pull-left group are listed here.
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do - if current_user
%i.fa.fa-rss To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
%div
Only issues from
%strong #{@group.name}
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/issues' = render 'shared/issues'
- page_title "Merge Requests" - page_title "Merge Requests"
- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) - header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
= render 'shared/issuable/filter', type: :merge_requests .project-issuable-filter
.controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
.gray-content-block.second-block .gray-content-block.second-block
%div Only merge requests from
Only merge requests from %strong #{@group.name}
%strong #{@group.name} group are listed here.
group are listed here. - if current_user
- if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/merge_requests' = render 'shared/merge_requests'
- page_title "Milestones" - page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group)) - header_title group_title(@group, "Milestones", group_milestones_path(@group))
= render 'shared/milestones_filter' .project-issuable-filter
.controls
- if can?(current_user, :admin_milestones, @group)
.pull-right
%span.pull-right.hidden-xs
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
= icon('plus')
New Milestone
= render 'shared/milestones_filter'
.gray-content-block .gray-content-block
- if can?(current_user, :admin_milestones, @group) Only milestones from
.pull-right %strong #{@group.name}
%span.pull-right.hidden-xs group are listed here.
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
New Milestone
.oneline
Only milestones from
%strong #{@group.name}
group are listed here.
.milestones .milestones
%ul.content-list %ul.content-list
- if @milestones.blank? - if @milestones.blank?
......
- page_title "Milestones" - page_title "Milestones"
= render "header_title" = render "header_title"
= render 'shared/milestones_filter'
.gray-content-block
.pull-right .project-issuable-filter
- if can? current_user, :admin_milestone, @project .controls
- if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
%i.fa.fa-plus %i.fa.fa-plus
New Milestone New Milestone
.oneline
Milestone allows you to group issues and set due date for it = render 'shared/milestones_filter'
.gray-content-block
Milestone allows you to group issues and set due date for it
.milestones .milestones
%ul.content-list %ul.content-list
......
- if @projects.any?
.prepend-left-10.new-project-item-select-holder
= project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] }
%a.btn.btn-new.new-project-item-select-button
= icon('plus')
= local_assigns[:label]
%b.caret
:javascript
$('.new-project-item-select-button').on('click', function() {
$('.new-project-item-select').select2('open');
});
var relativePath = '#{local_assigns[:path]}';
$('.new-project-item-select').on('click', function() {
window.location = $(this).val() + '/' + relativePath;
});
new ProjectSelect()
# Groups # Groups
## List project groups ## List groups
Get a list of groups. (As user: my groups, as admin: all groups) Get a list of groups. (As user: my groups, as admin: all groups)
...@@ -21,6 +21,70 @@ GET /groups ...@@ -21,6 +21,70 @@ GET /groups
You can search for groups by name or path, see below. You can search for groups by name or path, see below.
## List a group's projects
Get a list of projects in this group.
```
GET /groups/:id/projects
```
Parameters:
- `archived` (optional) - if passed, limit by archived status
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
```json
[
{
"id": 4,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 0,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
"tag_list": [
"example",
"disapora client"
],
"owner": {
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13: 46: 02Z"
},
"name": "Diaspora Client",
"name_with_namespace": "Diaspora / Diaspora Client",
"path": "diaspora-client",
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
}
]
```
## Details of a group ## Details of a group
Get all details of a group. Get all details of a group.
...@@ -186,7 +250,7 @@ To get more (up to 100), pass the following as an argument to the API call: ...@@ -186,7 +250,7 @@ To get more (up to 100), pass the following as an argument to the API call:
/groups?per_page=100 /groups?per_page=100
``` ```
And to switch pages add: And to switch pages add:
``` ```
/groups?per_page=100&page=2 /groups?per_page=100&page=2
``` ```
\ No newline at end of file
...@@ -65,6 +65,18 @@ module API ...@@ -65,6 +65,18 @@ module API
DestroyGroupService.new(group, current_user).execute DestroyGroupService.new(group, current_user).execute
end end
# Get a list of projects in this group
#
# Example Request:
# GET /groups/:id/projects
get ":id/projects" do
group = find_group(params[:id])
projects = group.projects
projects = filter_projects(projects)
projects = paginate projects
present projects, with: Entities::Project
end
# Transfer a project to the Group namespace # Transfer a project to the Group namespace
# #
# Parameters: # Parameters:
......
...@@ -10,6 +10,8 @@ describe API::API, api: true do ...@@ -10,6 +10,8 @@ describe API::API, api: true do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
let!(:group2) { create(:group) } let!(:group2) { create(:group) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
before do before do
group1.add_owner(user1) group1.add_owner(user1)
...@@ -67,7 +69,7 @@ describe API::API, api: true do ...@@ -67,7 +69,7 @@ describe API::API, api: true do
it "should return any existing group" do it "should return any existing group" do
get api("/groups/#{group2.id}", admin) get api("/groups/#{group2.id}", admin)
expect(response.status).to eq(200) expect(response.status).to eq(200)
json_response['name'] == group2.name expect(json_response['name']).to eq(group2.name)
end end
it "should not return a non existing group" do it "should not return a non existing group" do
...@@ -80,7 +82,7 @@ describe API::API, api: true do ...@@ -80,7 +82,7 @@ describe API::API, api: true do
it 'should return any existing group' do it 'should return any existing group' do
get api("/groups/#{group1.path}", admin) get api("/groups/#{group1.path}", admin)
expect(response.status).to eq(200) expect(response.status).to eq(200)
json_response['name'] == group2.name expect(json_response['name']).to eq(group1.name)
end end
it 'should not return a non existing group' do it 'should not return a non existing group' do
...@@ -95,6 +97,59 @@ describe API::API, api: true do ...@@ -95,6 +97,59 @@ describe API::API, api: true do
end end
end end
describe "GET /groups/:id/projects" do
context "when authenticated as user" do
it "should return the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
expect(response.status).to eq(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project1.name)
end
it "should not return a non existing group" do
get api("/groups/1328/projects", user1)
expect(response.status).to eq(404)
end
it "should not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
expect(response.status).to eq(403)
end
end
context "when authenticated as admin" do
it "should return any existing group" do
get api("/groups/#{group2.id}/projects", admin)
expect(response.status).to eq(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
it "should not return a non existing group" do
get api("/groups/1328/projects", admin)
expect(response.status).to eq(404)
end
end
context 'when using group path in URL' do
it 'should return any existing group' do
get api("/groups/#{group1.path}/projects", admin)
expect(response.status).to eq(200)
expect(json_response.first['name']).to eq(project1.name)
end
it 'should not return a non existing group' do
get api('/groups/unknown/projects', admin)
expect(response.status).to eq(404)
end
it 'should not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
expect(response.status).to eq(403)
end
end
end
describe "POST /groups" do describe "POST /groups" do
context "when authenticated as user without group permissions" do context "when authenticated as user without group permissions" do
it "should not create group" do it "should not create group" do
......
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