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 @@
groups_path: "/api/:version/groups.json"
group_path: "/api/:version/groups/:id.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) ->
url = Api.buildUrl(Api.group_path)
......@@ -44,6 +46,35 @@
).done (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) ->
url = gon.relative_url_root + url if gon.relative_url_root?
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 {
.alert, .progress {
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
select2_tag(id, opts)
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 = {})
css_class = ''
css_class << 'multiselect ' if opts[:multiple]
......
......@@ -4,14 +4,20 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
.append-bottom-20
.pull-right
.project-issuable-filter
.controls
.pull-left
- 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
.hidden-xs.pull-left
= link_to issues_dashboard_url(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/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"
- 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/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"
- 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
.oneline
List all milestones from all projects you have access to.
.milestones
......
......@@ -4,16 +4,19 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block
.pull-right
.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) do
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
%i.fa.fa-rss
%div
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block
Only issues from
%strong #{@group.name}
group are listed here.
......
- page_title "Merge Requests"
- 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
%div
Only merge requests from
%strong #{@group.name}
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
.prepend-top-default
= render 'shared/merge_requests'
- page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
= render 'shared/milestones_filter'
.gray-content-block
.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
.oneline
= render 'shared/milestones_filter'
.gray-content-block
Only milestones from
%strong #{@group.name}
group are listed here.
.milestones
%ul.content-list
- if @milestones.blank?
......
- page_title "Milestones"
= render "header_title"
= render 'shared/milestones_filter'
.gray-content-block
.pull-right
- if can? current_user, :admin_milestone, @project
.project-issuable-filter
.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
%i.fa.fa-plus
New Milestone
.oneline
= render 'shared/milestones_filter'
.gray-content-block
Milestone allows you to group issues and set due date for it
.milestones
......
- 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
## List project groups
## List groups
Get a list of groups. (As user: my groups, as admin: all groups)
......@@ -21,6 +21,70 @@ GET /groups
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
Get all details of a group.
......
......@@ -65,6 +65,18 @@ module API
DestroyGroupService.new(group, current_user).execute
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
#
# Parameters:
......
......@@ -10,6 +10,8 @@ describe API::API, api: true do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
let!(:group2) { create(:group) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
before do
group1.add_owner(user1)
......@@ -67,7 +69,7 @@ describe API::API, api: true do
it "should return any existing group" do
get api("/groups/#{group2.id}", admin)
expect(response.status).to eq(200)
json_response['name'] == group2.name
expect(json_response['name']).to eq(group2.name)
end
it "should not return a non existing group" do
......@@ -80,7 +82,7 @@ describe API::API, api: true do
it 'should return any existing group' do
get api("/groups/#{group1.path}", admin)
expect(response.status).to eq(200)
json_response['name'] == group2.name
expect(json_response['name']).to eq(group1.name)
end
it 'should not return a non existing group' do
......@@ -95,6 +97,59 @@ describe API::API, api: true do
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
context "when authenticated as user without group permissions" 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