Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-12-08 19:43:20 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-12-08 19:43:20 +0300
commit038c9a65fbe814927e9c9fa12f1bda660249e8b0 (patch)
treebe87583c14bc960651ee489b7e86c6bcfbda2f47
parent033947de90163aeadf0b1ae2c0f5be1b8529088b (diff)
parent15925290eec4f5616e12ef3169794f85150e3270 (diff)
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
-rw-r--r--app/assets/javascripts/api.js.coffee31
-rw-r--r--app/assets/javascripts/project_select.js.coffee39
-rw-r--r--app/assets/stylesheets/framework/common.scss13
-rw-r--r--app/helpers/selects_helper.rb13
-rw-r--r--app/views/dashboard/issues.html.haml20
-rw-r--r--app/views/dashboard/merge_requests.html.haml12
-rw-r--r--app/views/dashboard/milestones/index.html.haml10
-rw-r--r--app/views/groups/issues.html.haml27
-rw-r--r--app/views/groups/merge_requests.html.haml19
-rw-r--r--app/views/groups/milestones/index.html.haml24
-rw-r--r--app/views/projects/milestones/index.html.haml15
-rw-r--r--app/views/shared/_new_project_item_select.html.haml20
-rw-r--r--doc/api/groups.md70
-rw-r--r--lib/api/groups.rb12
-rw-r--r--spec/requests/api/groups_spec.rb59
15 files changed, 331 insertions, 53 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 9e5d594c861..746fa3cea87 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -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)
diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee
new file mode 100644
index 00000000000..0ae274f3363
--- /dev/null
+++ b/app/assets/javascripts/project_select.js.coffee
@@ -0,0 +1,39 @@
+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"
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d2f491daf78..7eabd9c99f7 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -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;
+ }
+}
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 7e175d0de8a..05386d790ca 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -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]
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index cd602e897b7..2d3da01178a 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -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")
+.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
- .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/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'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index d1f332fa0d3..c5a5ec21f78 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,6 +1,14 @@
- 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'
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 635251e2374..bec1692a4de 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,12 +1,14 @@
- 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
- .oneline
- List all milestones from all projects you have access to.
+ List all milestones from all projects you have access to.
.milestones
%ul.content-list
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 08d97e418a3..90ade1e1680 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -4,21 +4,24 @@
- if current_user
= 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
- .pull-right
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do
- %i.fa.fa-rss
- %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.
+ 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
= render 'shared/issues'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 425ad8331bf..f662f5a8c17 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,13 +1,18 @@
- 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.
+ 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'
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 84ec77c6188..b221d3a89a4 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,18 +1,22 @@
- page_title "Milestones"
- 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
- - 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
- New Milestone
+ Only milestones from
+ %strong #{@group.name}
+ group are listed here.
- .oneline
- Only milestones from
- %strong #{@group.name}
- group are listed here.
.milestones
%ul.content-list
- if @milestones.blank?
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index a207385bd43..114b06457a5 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,15 +1,18 @@
- 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
- 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
%ul.content-list
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
new file mode 100644
index 00000000000..c4431d66927
--- /dev/null
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -0,0 +1,20 @@
+- 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()
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 0b9f6406d8d..808675d8605 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,6 +1,6 @@
# 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.
@@ -186,7 +250,7 @@ To get more (up to 100), pass the following as an argument to the API call:
/groups?per_page=100
```
-And to switch pages add:
+And to switch pages add:
```
/groups?per_page=100&page=2
-``` \ No newline at end of file
+```
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 024aeec2e14..1a14d870a4a 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -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:
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 13cced81875..4cfa49d1566 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -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