From 5bce197b617f2542430db7aecec321cf1619de72 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 4 May 2017 23:45:02 +0300 Subject: [PATCH 01/88] Serialize groups as json for Dashboard::GroupsController Signed-off-by: Dmitriy Zaporozhets --- .../dashboard/groups_controller.rb | 27 ++++++++++++++----- app/serializers/group_entity.rb | 16 +++++++++++ app/serializers/group_serializer.rb | 19 +++++++++++++ app/views/dashboard/groups/_groups.html.haml | 5 ++-- app/views/dashboard/groups/index.html.haml | 2 +- 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 app/serializers/group_entity.rb create mode 100644 app/serializers/group_serializer.rb diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index d03265e9f20..b339344dd40 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,16 +1,29 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index - @group_members = current_user.group_members.includes(source: :route).joins(:group) - @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? - @group_members = @group_members.merge(Group.sort(@sort = params[:sort])) - @group_members = @group_members.page(params[:page]) + @groups = if params[:parent_id] + parent = Group.find(params[:parent_id]) + + if parent.users_with_parents.find_by(id: current_user) + Group.where(id: parent.children) + else + Group.none + end + else + Group.joins(:group_members).merge(current_user.group_members) + end + + @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? + @groups = @groups.includes(:route) + @groups = @groups.sort(@sort = params[:sort]) + @groups = @groups.page(params[:page]) respond_to do |format| format.html format.json do - render json: { - html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) - } + render json: GroupSerializer + .new(current_user: @current_user) + .with_pagination(request, response) + .represent(@groups) end end end diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb new file mode 100644 index 00000000000..33f1fbff31d --- /dev/null +++ b/app/serializers/group_entity.rb @@ -0,0 +1,16 @@ +class GroupEntity < Grape::Entity + include RequestAwareEntity + + expose :id, :name, :path, :description, :visibility + expose :avatar_url + expose :web_url + expose :full_name, :full_path + expose :parent_id + expose :created_at, :updated_at + + expose :permissions do + expose :group_access do |group, options| + group.group_members.find_by(user_id: request.current_user)&.access_level + end + end +end diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb new file mode 100644 index 00000000000..26e8566828b --- /dev/null +++ b/app/serializers/group_serializer.rb @@ -0,0 +1,19 @@ +class GroupSerializer < BaseSerializer + entity GroupEntity + + def with_pagination(request, response) + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } + end + + def paginated? + @paginator.present? + end + + def represent(resource, opts = {}) + if paginated? + super(@paginator.paginate(resource), opts) + else + super(resource, opts) + end + end +end diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 6c3bf1a2b3b..d33ee450b29 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,6 +1,7 @@ .js-groups-list-holder %ul.content-list - - @group_members.each do |group_member| + - @groups.each do |group| + - group_member = group.group_members.find_by(user_id: current_user) = render 'shared/groups/group', group: group_member.group, group_member: group_member - = paginate @group_members, theme: 'gitlab' + = paginate @groups, theme: 'gitlab' diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 73ab2c95ff9..0d35d11fb63 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,7 +2,7 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -- if @group_members.empty? +- if @groups.empty? = render 'empty_state' - else = render 'groups' From 1dc2b4693e4a58c94e556ae219ae6200044f95dc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 19:29:56 -0500 Subject: [PATCH 02/88] =?UTF-8?q?Add=20=E2=80=9Cgroups=E2=80=9D=20JS=20bun?= =?UTF-8?q?dle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/groups/index.js | 3 +++ app/views/dashboard/groups/index.html.haml | 2 ++ config/webpack.config.js | 1 + 3 files changed, 6 insertions(+) create mode 100644 app/assets/javascripts/groups/index.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js new file mode 100644 index 00000000000..c0e083ef635 --- /dev/null +++ b/app/assets/javascripts/groups/index.js @@ -0,0 +1,3 @@ +$(() => { + // Groups bundle +}); \ No newline at end of file diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 0d35d11fb63..bf1013c685b 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,6 +2,8 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' += page_specific_javascript_bundle_tag('groups') + - if @groups.empty? = render 'empty_state' - else diff --git a/config/webpack.config.js b/config/webpack.config.js index 0ec9e48845e..966b1e2283e 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -32,6 +32,7 @@ var config = { filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', group: './group.js', + groups: './groups/index.js', groups_list: './groups_list.js', issuable: './issuable/issuable_bundle.js', issue_show: './issue_show/index.js', From d67ab685350005b83a12988845b7fb87c613b472 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 20:49:07 -0500 Subject: [PATCH 03/88] Set Groups Vue app for Dashboard page --- app/assets/javascripts/groups/index.js | 12 ++++++++++-- app/assets/javascripts/groups/stores/groups_store.js | 5 +++++ app/views/dashboard/groups/index.html.haml | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/groups/stores/groups_store.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index c0e083ef635..934e3b8e580 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,3 +1,11 @@ +import Vue from 'vue'; +import GroupsStore from './stores/groups_store'; + $(() => { - // Groups bundle -}); \ No newline at end of file + const groupsStore = new GroupsStore(); + + const GroupsApp = new Vue({ + el: document.querySelector('.js-groups-list-holder'), + data: groupsStore, + }); +}); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js new file mode 100644 index 00000000000..f62f419ac1b --- /dev/null +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -0,0 +1,5 @@ +export default class GroupsStore { + constructor() { + this.groups = []; + } +} diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index bf1013c685b..af9f9b1b363 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,6 +2,7 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' += page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('groups') - if @groups.empty? From 265736052906fefc5ff57c3958158b1f563c2a9e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 21:34:56 -0500 Subject: [PATCH 04/88] Add GroupsService to fetch data from server --- app/assets/javascripts/groups/index.js | 17 ++++++++++++++++- .../groups/services/groups_service.js | 14 ++++++++++++++ app/views/dashboard/groups/_groups.html.haml | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/groups/services/groups_service.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 934e3b8e580..f9fe3f7f341 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,11 +1,26 @@ +/* eslint-disable no-unused-vars */ + import Vue from 'vue'; import GroupsStore from './stores/groups_store'; +import GroupsService from './services/groups_service'; $(() => { + const appEl = document.querySelector('.js-groups-list-holder'); + const groupsStore = new GroupsStore(); + const groupsService = new GroupsService(appEl.dataset.endpoint); const GroupsApp = new Vue({ - el: document.querySelector('.js-groups-list-holder'), + el: appEl, data: groupsStore, + mounted() { + groupsService.getGroups() + .then((response) => { + this.groups = response.json(); + }) + .catch(() => { + // TODO: Handle error + }); + }, }); }); diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js new file mode 100644 index 00000000000..4c5ce87f396 --- /dev/null +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class GroupsService { + constructor(endpoint) { + this.groups = Vue.resource(endpoint); + } + + getGroups() { + return this.groups.get(); + } +} diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index d33ee450b29..9e19b6bc347 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,4 +1,4 @@ -.js-groups-list-holder +.js-groups-list-holder{ data: { endpoint: dashboard_groups_path(format: :json) } } %ul.content-list - @groups.each do |group| - group_member = group.group_members.find_by(user_id: current_user) From 5e0e3971b8c396c03375404c98874d9c18221668 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 May 2017 23:15:04 -0500 Subject: [PATCH 05/88] List groups with basic details - Adds Groups component - Adds GroupItem component --- .../groups/components/group_item.vue | 21 +++++++++ .../javascripts/groups/components/groups.vue | 45 +++++++++++++++++++ app/assets/javascripts/groups/index.js | 20 +++------ .../javascripts/groups/stores/groups_store.js | 11 ++++- app/views/dashboard/groups/_groups.html.haml | 8 +--- 5 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/groups/components/group_item.vue create mode 100644 app/assets/javascripts/groups/components/groups.vue diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue new file mode 100644 index 00000000000..2db9672547a --- /dev/null +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue new file mode 100644 index 00000000000..341855b9915 --- /dev/null +++ b/app/assets/javascripts/groups/components/groups.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index f9fe3f7f341..883e9cb4187 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,26 +1,16 @@ /* eslint-disable no-unused-vars */ import Vue from 'vue'; -import GroupsStore from './stores/groups_store'; -import GroupsService from './services/groups_service'; +import GroupsComponent from './components/groups.vue' $(() => { - const appEl = document.querySelector('.js-groups-list-holder'); - - const groupsStore = new GroupsStore(); - const groupsService = new GroupsService(appEl.dataset.endpoint); + const appEl = document.querySelector('#dashboard-group-app'); const GroupsApp = new Vue({ el: appEl, - data: groupsStore, - mounted() { - groupsService.getGroups() - .then((response) => { - this.groups = response.json(); - }) - .catch(() => { - // TODO: Handle error - }); + components: { + 'groups-component': GroupsComponent }, + render: createElement => createElement('groups-component'), }); }); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index f62f419ac1b..0e1820b7335 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,5 +1,14 @@ export default class GroupsStore { constructor() { - this.groups = []; + this.state = {}; + this.state.groups = []; + + return this; + } + + setGroups(groups) { + this.state.groups = groups; + + return groups; } } diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 9e19b6bc347..e7a5fa8ba32 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,7 +1 @@ -.js-groups-list-holder{ data: { endpoint: dashboard_groups_path(format: :json) } } - %ul.content-list - - @groups.each do |group| - - group_member = group.group_members.find_by(user_id: current_user) - = render 'shared/groups/group', group: group_member.group, group_member: group_member - - = paginate @groups, theme: 'gitlab' +.js-groups-list-holder#dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json) } } From 4b488d72a263b56b9701dde818ca6b77b038ca89 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 8 May 2017 15:22:26 -0500 Subject: [PATCH 06/88] Prepare groups components for subgroups --- .../groups/components/group_item.vue | 39 ++++++++++++++----- .../javascripts/groups/components/groups.vue | 18 +++++++-- app/assets/javascripts/groups/event_hub.js | 3 ++ app/assets/javascripts/groups/index.js | 4 +- .../javascripts/groups/stores/groups_store.js | 19 ++++++++- 5 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 app/assets/javascripts/groups/event_hub.js diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 2db9672547a..98f1d516cb2 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,21 +1,42 @@ diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 341855b9915..26ed0990ef5 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -2,6 +2,7 @@ import GroupsStore from '../stores/groups_store'; import GroupsService from '../services/groups_service'; import GroupItem from '../components/group_item.vue'; +import eventHub from '../event_hub'; export default { components: { @@ -14,7 +15,7 @@ export default { return { store, state: store.state, - } + }; }, created() { @@ -22,6 +23,8 @@ export default { this.service = new GroupsService(appEl.dataset.endpoint); this.fetchGroups(); + + eventHub.$on('toggleSubGroups', this.toggleSubGroups); }, methods: { @@ -34,12 +37,21 @@ export default { // TODO: Handler error }); }, - } + toggleSubGroups(group) { + GroupsStore.toggleSubGroups(group); + }, + }, }; diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/groups/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 883e9cb4187..81385dfd981 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import Vue from 'vue'; -import GroupsComponent from './components/groups.vue' +import GroupsComponent from './components/groups.vue'; $(() => { const appEl = document.querySelector('#dashboard-group-app'); @@ -9,7 +9,7 @@ $(() => { const GroupsApp = new Vue({ el: appEl, components: { - 'groups-component': GroupsComponent + 'groups-component': GroupsComponent, }, render: createElement => createElement('groups-component'), }); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 0e1820b7335..2b74447c7f9 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -7,8 +7,25 @@ export default class GroupsStore { } setGroups(groups) { - this.state.groups = groups; + this.state.groups = this.decorateGroups(groups); return groups; } + + decorateGroups(rawGroups) { + this.groups = rawGroups.map(GroupsStore.decorateGroup); + return this.groups; + } + + static decorateGroup(rawGroup) { + const group = rawGroup; + group.isOpen = false; + return group; + } + + static toggleSubGroups(toggleGroup) { + const group = toggleGroup; + group.isOpen = !group.isOpen; + return group; + } } From 4c3753387b9bb46d0d257c90720c6e27a258c37a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 9 May 2017 18:10:19 -0500 Subject: [PATCH 07/88] Set tree structure for groups --- .../groups/components/group_folder.vue | 18 ++++++++ .../groups/components/group_item.vue | 43 +++++++++---------- .../javascripts/groups/components/groups.vue | 19 +++----- app/assets/javascripts/groups/index.js | 9 ++-- 4 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/groups/components/group_folder.vue diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue new file mode 100644 index 00000000000..5485da58ec5 --- /dev/null +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 98f1d516cb2..627eae1cd0d 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -10,6 +10,10 @@ export default { }, methods: { toggleSubGroups() { + if (this.group.subGroups && !this.group.subGroups.length) { + return; + } + eventHub.$emit('toggleSubGroups', this.group); }, }, @@ -17,26 +21,21 @@ export default { diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 26ed0990ef5..797fca9bd49 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,14 +1,9 @@ diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 81385dfd981..8a7c01161a1 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -2,15 +2,18 @@ import Vue from 'vue'; import GroupsComponent from './components/groups.vue'; +import GroupFolder from './components/group_folder.vue'; +import GroupItem from './components/group_item.vue'; $(() => { const appEl = document.querySelector('#dashboard-group-app'); + Vue.component('groups-component', GroupsComponent); + Vue.component('group-folder', GroupFolder); + Vue.component('group-item', GroupItem); + const GroupsApp = new Vue({ el: appEl, - components: { - 'groups-component': GroupsComponent, - }, render: createElement => createElement('groups-component'), }); }); From 3b6ff7fcaf4443b518770f97e437631197297980 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 10 May 2017 03:06:51 -0500 Subject: [PATCH 08/88] Add support to filter by name to Group list --- app/assets/javascripts/dispatcher.js | 6 --- app/assets/javascripts/filterable_list.js | 5 ++- .../groups/components/group_folder.vue | 2 - .../javascripts/groups/components/groups.vue | 42 +++---------------- app/assets/javascripts/groups/index.js | 39 ++++++++++++++++- app/assets/javascripts/groups_list.js | 10 ++--- app/views/dashboard/groups/_groups.html.haml | 4 +- 7 files changed, 52 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7e469153106..e4c60ef1188 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -40,7 +40,6 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import Group from './group'; import GroupName from './group_name'; -import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; @@ -148,12 +147,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'admin:projects:index': new ProjectsList(); break; - case 'dashboard:groups:index': - new GroupsList(); - break; case 'explore:groups:index': - new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) break; const exploreGroupsLanding = new Landing( diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index aaaeb9bddb1..e6d6400ca86 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -4,7 +4,8 @@ */ export default class FilterableList { - constructor(form, filter, holder) { + constructor(form, filter, holder, store) { + this.store = store; this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; @@ -33,7 +34,7 @@ export default class FilterableList { $(this.listHolderElement).fadeTo(250, 1); }, success(data) { - this.listHolderElement.innerHTML = data.html; + this.store.setGroups(data); // Change url so if user reload a page - search results are saved return window.history.replaceState({ diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index 5485da58ec5..75b20111ca1 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -1,6 +1,4 @@ diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 2ee7ec96dfe..45cc483b06e 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -2,7 +2,7 @@ export default { props: { groups: { - type: Array, + type: Object, required: true, }, }, diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index f8ba11caccc..be89955c101 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,5 @@ import FilterableList from '~/filterable_list'; - export default class GroupFilterableList extends FilterableList { constructor(form, filter, holder, store) { super(form, filter, holder); diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index d136e64850f..5b94f7b762d 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -31,19 +31,24 @@ $(() => { }; }, methods: { - fetchGroups() { - service.getGroups() + fetchGroups(parentGroup) { + let parentId = null; + + if (parentGroup) { + parentId = parentGroup.id; + } + + service.getGroups(parentId) .then((response) => { - store.setGroups(response.json()); + store.setGroups(response.json(), parentGroup); }) .catch(() => { // TODO: Handler error }); }, - toggleSubGroups(group) { - GroupsStore.toggleSubGroups(group); - - this.fetchGroups(); + toggleSubGroups(parentGroup = null) { + GroupsStore.toggleSubGroups(parentGroup); + this.fetchGroups(parentGroup); }, }, created() { diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index 4c5ce87f396..d92e93d5fa9 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -8,7 +8,15 @@ export default class GroupsService { this.groups = Vue.resource(endpoint); } - getGroups() { - return this.groups.get(); + getGroups(parentId) { + let data = {}; + + if (parentId) { + data = { + parent_id: parentId, + }; + } + + return this.groups.get(data); } } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index c0ef4f776e0..bf43441bd71 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,15 +1,48 @@ export default class GroupsStore { constructor() { this.state = {}; - this.state.groups = []; + this.state.groups = {}; return this; } - setGroups(groups) { - this.state.groups = this.decorateGroups(groups); + setGroups(rawGroups, parent = null) { + const parentGroup = parent; - return groups; + if (parentGroup) { + parentGroup.subGroups = this.buildTree(rawGroups); + } else { + this.state.groups = this.buildTree(rawGroups); + } + + return rawGroups; + } + + buildTree(rawGroups) { + const groups = this.decorateGroups(rawGroups); + const tree = {}; + const mappedGroups = {}; + + // Map groups to an object + for (let i = 0, len = groups.length; i < len; i += 1) { + const group = groups[i]; + mappedGroups[group.id] = group; + mappedGroups[group.id].subGroups = {}; + } + + Object.keys(mappedGroups).forEach((key) => { + const currentGroup = mappedGroups[key]; + // If the group is not at the root level, add it to its parent array of subGroups. + if (currentGroup.parentId) { + mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; + mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + } else { + // If the group is at the root level, add it to first level elements array. + tree[currentGroup.id] = currentGroup; + } + }); + + return tree; } decorateGroups(rawGroups) { @@ -19,12 +52,14 @@ export default class GroupsStore { static decorateGroup(rawGroup) { return { - fullName: rawGroup.name, + id: rawGroup.id, + fullName: rawGroup.full_name, description: rawGroup.description, webUrl: rawGroup.web_url, - parentId: rawGroup.parentId, - hasSubgroups: !!rawGroup.parent_id, + parentId: rawGroup.parent_id, + expandable: true, isOpen: false, + subGroups: {}, }; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 72d73b89a2a..8c7e5591d79 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -111,3 +111,9 @@ height: 50px; } } + + +.list-group .list-group { + margin-top: 10px; + margin-bottom: 0; +} From d55a9d4c6426b3c611def994525f065c8d12b514 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 12 May 2017 03:43:39 -0500 Subject: [PATCH 14/88] Fix tree generation when filtering by name --- app/assets/javascripts/groups/stores/groups_store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index bf43441bd71..1bfd8745423 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -33,7 +33,8 @@ export default class GroupsStore { Object.keys(mappedGroups).forEach((key) => { const currentGroup = mappedGroups[key]; // If the group is not at the root level, add it to its parent array of subGroups. - if (currentGroup.parentId) { + const parentGroup = mappedGroups[currentGroup['parentId']]; + if (currentGroup.parentId && parentGroup) { mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups } else { From db098f22e1ef871095d416757ee330e1e807524a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 15 May 2017 16:32:09 -0500 Subject: [PATCH 15/88] Stop event propagation to prevent multiple ajax calls --- app/assets/javascripts/groups/components/group_item.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 07fa648b237..3689804d576 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -18,7 +18,7 @@ export default { From 5bd52eaefb11e3568b3e78d21efd0f1dabf328b8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 17:04:25 -0500 Subject: [PATCH 52/88] Use object destructuring when passing more than 3 params to follow style guide --- app/assets/javascripts/groups/groups_filterable_list.js | 2 +- app/assets/javascripts/groups/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 9a8797caf36..d1347dc5a6e 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,7 +1,7 @@ import FilterableList from '~/filterable_list'; export default class GroupFilterableList extends FilterableList { - constructor(form, filter, holder, store) { + constructor({ form, filter, holder, store }) { super(form, filter, holder); this.store = store; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 4d0f415bfc2..29dd6709421 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -78,7 +78,7 @@ $(() => { created() { let groupFilterList = null; - groupFilterList = new GroupFilterableList(form, filter, holder, store); + groupFilterList = new GroupFilterableList({ form, filter, holder, store }); groupFilterList.initSearch(); this.fetchGroups() From a161384b91e6f06f2afa41f9e0038cc3129f12b1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 18:29:11 -0500 Subject: [PATCH 53/88] Declare store and service inside Vue app --- app/assets/javascripts/groups/index.js | 61 ++++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 29dd6709421..26b172f3e94 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - import Vue from 'vue'; import GroupFilterableList from './groups_filterable_list'; import GroupsComponent from './components/groups.vue'; @@ -9,25 +7,22 @@ import GroupsStore from './stores/groups_store'; import GroupsService from './services/groups_service'; import eventHub from './event_hub'; -$(() => { - const appEl = document.querySelector('#dashboard-group-app'); - const form = document.querySelector('form#group-filter-form'); - const filter = document.querySelector('.js-groups-list-filter'); - const holder = document.querySelector('.js-groups-list-holder'); - - const store = new GroupsStore(); - const service = new GroupsService(appEl.dataset.endpoint); +document.addEventListener('DOMContentLoaded', () => { + const el = document.querySelector('#dashboard-group-app'); Vue.component('groups-component', GroupsComponent); Vue.component('group-folder', GroupFolder); Vue.component('group-item', GroupItem); - const GroupsApp = new Vue({ - el: appEl, + return new Vue({ + el, data() { + this.store = new GroupsStore(); + this.service = new GroupsService(el.dataset.endpoint); + return { - store, - state: store.state, + store: this.store, + state: this.store.state, }; }, methods: { @@ -47,9 +42,9 @@ $(() => { page = pageParam; } - getGroups = service.getGroups(parentId, page); + getGroups = this.service.getGroups(parentId, page); getGroups.then((response) => { - store.setGroups(response.json(), parentGroup); + this.store.setGroups(response.json(), parentGroup); }) .catch(() => { // TODO: Handle error @@ -59,14 +54,14 @@ $(() => { }, toggleSubGroups(parentGroup = null) { if (!parentGroup.isOpen) { - store.resetGroups(parentGroup); + this.store.resetGroups(parentGroup); this.fetchGroups(parentGroup); } GroupsStore.toggleSubGroups(parentGroup); }, leaveGroup(endpoint) { - service.leaveGroup(endpoint) + this.service.leaveGroup(endpoint) .then(() => { // TODO: Refresh? }) @@ -75,22 +70,32 @@ $(() => { }); }, }, - created() { + beforeMount() { let groupFilterList = null; + const form = document.querySelector('form#group-filter-form'); + const filter = document.querySelector('.js-groups-list-filter'); + const holder = document.querySelector('.js-groups-list-holder'); - groupFilterList = new GroupFilterableList({ form, filter, holder, store }); + const options = { + form, + filter, + holder, + store: this.store, + }; + groupFilterList = new GroupFilterableList(options); groupFilterList.initSearch(); - this.fetchGroups() - .then((response) => { - store.storePagination(response.headers); - }) - .catch(() => { - // TODO: Handle error - }); - eventHub.$on('toggleSubGroups', this.toggleSubGroups); eventHub.$on('leaveGroup', this.leaveGroup); }, + mounted() { + this.fetchGroups() + .then((response) => { + this.store.storePagination(response.headers); + }) + .catch(() => { + // TODO: Handle error + }); + }, }); }); From abdd18922bb99e4d85266ff6e9995599466e3ca4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 19:45:05 -0500 Subject: [PATCH 54/88] Restore accidentally deleted code --- app/assets/javascripts/groups/components/group_item.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 3c92d6fd1eb..769ef05add6 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -84,6 +84,9 @@ export default { return fullPath; }, + hasGroups() { + return Object.keys(this.group.subGroups).length > 0; + }, }, }; @@ -158,6 +161,6 @@ export default { {{group.description}} - + From 1c1c08020b10257494a570d2f3ed2dec13796b0a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:00:34 -0500 Subject: [PATCH 55/88] Remove duplicated line --- app/assets/javascripts/groups/services/groups_service.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index 86e911b73b9..ddebd1af47f 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -8,7 +8,6 @@ Vue.use(VueResource); export default class GroupsService { constructor(endpoint) { this.groups = Vue.resource(endpoint); - this.groups = Vue.resource(endpoint); } getGroups(parentId, page) { From 56a279be975147a800d6cd4879346def058ac4fa Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:25:23 -0500 Subject: [PATCH 56/88] Move eslint disable rule close to offending line to conform styleguide --- app/assets/javascripts/groups/services/groups_service.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index ddebd1af47f..8d67c0244f3 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -1,5 +1,3 @@ -/* eslint-disable class-methods-use-this */ - import Vue from 'vue'; import VueResource from 'vue-resource'; @@ -23,6 +21,7 @@ export default class GroupsService { return this.groups.get(data); } + // eslint-disable-next-line class-methods-use-this leaveGroup(endpoint) { return Vue.http.delete(endpoint); } From 9c71b78145c649740815be7ee4f714d4451892a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:27:37 -0500 Subject: [PATCH 57/88] Fix JS error when filtering by option --- app/assets/javascripts/groups/components/group_item.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 769ef05add6..2013469f9e6 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -63,7 +63,7 @@ export default { if (this.group.isOrphan) { // check if current group is baseGroup - if (this.baseGroup) { + if (Object.keys(this.baseGroup).length > 0) { // Remove baseGroup prefix from our current group.fullName. e.g: // baseGroup.fullName: `level1` // group.fullName: `level1 / level2 / level3` From bf65c49c699f6f009c0a919a189499e2aca82118 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:39:44 -0500 Subject: [PATCH 58/88] Remove unnecesary return --- app/assets/javascripts/groups/stores/groups_store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index fc925172c2f..eaad30638ac 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -4,8 +4,6 @@ export default class GroupsStore { this.state = {}; this.state.groups = {}; this.state.pageInfo = {}; - - return this; } setGroups(rawGroups, parent = null) { From 0a1b196643110380ec1fa672bfd829fb79a691a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:52:43 -0500 Subject: [PATCH 59/88] Remove default null value for parent param --- app/assets/javascripts/groups/stores/groups_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index eaad30638ac..d343b61f378 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -6,7 +6,7 @@ export default class GroupsStore { this.state.pageInfo = {}; } - setGroups(rawGroups, parent = null) { + setGroups(rawGroups, parent) { const parentGroup = parent; const tree = this.buildTree(rawGroups, parentGroup); From af6e4ae900896931503073e2f74dd497ccf6a60d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:01:23 -0500 Subject: [PATCH 60/88] Use map to iterate arrays --- app/assets/javascripts/groups/stores/groups_store.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index d343b61f378..06ef3b268e3 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -44,11 +44,11 @@ export default class GroupsStore { const orphans = []; // Map groups to an object - for (let i = 0, len = groups.length; i < len; i += 1) { - const group = groups[i]; + groups.map((group) => { mappedGroups[group.id] = group; mappedGroups[group.id].subGroups = {}; - } + return group; + }); Object.keys(mappedGroups).map((key) => { const currentGroup = mappedGroups[key]; From 7feda1a354274c4672a95f965b59de3c40118ccb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:09:29 -0500 Subject: [PATCH 61/88] Use webpack_bundle_tag instead of deprecated page_specific_javascript_bundle_tag --- app/views/dashboard/groups/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index af9f9b1b363..f9b45a539a1 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,8 +2,8 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -= page_specific_javascript_bundle_tag('common_vue') -= page_specific_javascript_bundle_tag('groups') += webpack_bundle_tag 'common_vue' += webpack_bundle_tag 'groups' - if @groups.empty? = render 'empty_state' From 24b456e6a3e978965715d53243407d059e485bdb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:18:11 -0500 Subject: [PATCH 62/88] Move line to a better place --- app/assets/javascripts/groups/stores/groups_store.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 06ef3b268e3..b6bc2a25776 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -52,10 +52,9 @@ export default class GroupsStore { Object.keys(mappedGroups).map((key) => { const currentGroup = mappedGroups[key]; - // If the group is not at the root level, add it to its parent array of subGroups. - const findParentGroup = mappedGroups[currentGroup.parentId]; - if (currentGroup.parentId) { + // If the group is not at the root level, add it to its parent array of subGroups. + const findParentGroup = mappedGroups[currentGroup.parentId]; if (findParentGroup) { mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups From 76543bce90b51f99f7185d7e9f1cc82f975c6d60 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:37:56 -0500 Subject: [PATCH 63/88] Fix karma tests --- spec/javascripts/groups/mock_data.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index fdb809018cf..98f2aa52135 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -11,8 +11,8 @@ const group1 = { parent_id: null, created_at: '2017-05-15T19:01:23.670Z', updated_at: '2017-05-15T19:01:23.670Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, @@ -33,8 +33,8 @@ const group14 = { parent_id: 1127, created_at: '2017-05-15T19:02:01.645Z', updated_at: '2017-05-15T19:02:01.645Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 30, @@ -54,8 +54,8 @@ const group2 = { parent_id: null, created_at: '2017-05-11T19:35:09.635Z', updated_at: '2017-05-11T19:35:09.635Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, @@ -75,8 +75,8 @@ const group21 = { parent_id: 1119, created_at: '2017-05-11T19:51:04.060Z', updated_at: '2017-05-11T19:51:04.060Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, From 0880094455f3b654733088a18070c17e45f87e74 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 1 Jun 2017 13:40:22 -0500 Subject: [PATCH 64/88] Make GroupsStore.store method non-static --- app/assets/javascripts/groups/stores/groups_store.js | 6 +++--- spec/javascripts/groups/group_item_spec.js | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index b6bc2a25776..4f31efb67a7 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,4 +1,3 @@ -/* eslint-disable class-methods-use-this */ export default class GroupsStore { constructor() { this.state = {}; @@ -104,11 +103,11 @@ export default class GroupsStore { } decorateGroups(rawGroups) { - this.groups = rawGroups.map(GroupsStore.decorateGroup); + this.groups = rawGroups.map(this.decorateGroup); return this.groups; } - static decorateGroup(rawGroup) { + decorateGroup(rawGroup) { return { id: rawGroup.id, fullName: rawGroup.full_name, @@ -130,6 +129,7 @@ export default class GroupsStore { }; } + // eslint-disable-next-line class-methods-use-this static toggleSubGroups(toggleGroup) { const group = toggleGroup; group.isOpen = !group.isOpen; diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js index ed441242085..609c45250f1 100644 --- a/spec/javascripts/groups/group_item_spec.js +++ b/spec/javascripts/groups/group_item_spec.js @@ -6,11 +6,13 @@ import { group1 } from './mock_data'; describe('Groups Component', () => { let GroupItemComponent; let component; + let store; let group; beforeEach((done) => { GroupItemComponent = Vue.extend(groupItemComponent); - group = GroupsStore.decorateGroup(group1); + store = new GroupsStore(); + group = store.decorateGroup(group1); component = new GroupItemComponent({ propsData: { From 4d141cb30dfcad94db89bdc08f4ea907dc2f8bdf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 1 Jun 2017 16:47:39 -0500 Subject: [PATCH 65/88] Use eventHub to update groups from GroupFilterableList class --- .../groups/groups_filterable_list.js | 13 ++++--- app/assets/javascripts/groups/index.js | 39 +++++++++++++------ .../groups/services/groups_service.js | 14 +++++-- .../javascripts/groups/stores/groups_store.js | 2 + 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index d1347dc5a6e..f3c1b8ab545 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,10 +1,10 @@ import FilterableList from '~/filterable_list'; +import eventHub from './event_hub'; export default class GroupFilterableList extends FilterableList { - constructor({ form, filter, holder, store }) { + constructor(form, filter, holder) { super(form, filter, holder); - this.store = store; this.$dropdown = $('.js-group-filter-dropdown-wrap'); } @@ -41,15 +41,16 @@ export default class GroupFilterableList extends FilterableList { onFilterSuccess(data, xhr) { super.onFilterSuccess(data); - - this.store.setGroups(data); - this.store.storePagination({ + const paginationData = { 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), 'X-Page': xhr.getResponseHeader('X-Page'), 'X-Total': xhr.getResponseHeader('X-Total'), 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), - }); + }; + + eventHub.$emit('updateGroups', data); + eventHub.$emit('updatePagination', paginationData); } } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 26b172f3e94..21a1c71ca77 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -10,11 +10,18 @@ import eventHub from './event_hub'; document.addEventListener('DOMContentLoaded', () => { const el = document.querySelector('#dashboard-group-app'); + // Don't do anything if element doesn't exist (No groups) + // This is for when the user enters directly to the page via URL + if (!el) { + return; + } + Vue.component('groups-component', GroupsComponent); Vue.component('group-folder', GroupFolder); Vue.component('group-item', GroupItem); - return new Vue({ + // eslint-disable-next-line no-new + new Vue({ el, data() { this.store = new GroupsStore(); @@ -31,20 +38,26 @@ document.addEventListener('DOMContentLoaded', () => { let getGroups = null; let page = null; let pageParam = null; + let filterGroups = null; + let filterGroupsParam = null; if (parentGroup) { parentId = parentGroup.id; } pageParam = gl.utils.getParameterByName('page'); - if (pageParam) { page = pageParam; } - getGroups = this.service.getGroups(parentId, page); + filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + if (filterGroupsParam) { + filterGroups = filterGroupsParam; + } + + getGroups = this.service.getGroups(parentId, page, filterGroups); getGroups.then((response) => { - this.store.setGroups(response.json(), parentGroup); + eventHub.$emit('updateGroups', response.json(), parentGroup); }) .catch(() => { // TODO: Handle error @@ -69,6 +82,12 @@ document.addEventListener('DOMContentLoaded', () => { // TODO: Handle error }); }, + updateGroups(groups, parentGroup) { + this.store.setGroups(groups, parentGroup); + }, + updatePagination(headers) { + this.store.storePagination(headers); + }, }, beforeMount() { let groupFilterList = null; @@ -76,22 +95,18 @@ document.addEventListener('DOMContentLoaded', () => { const filter = document.querySelector('.js-groups-list-filter'); const holder = document.querySelector('.js-groups-list-holder'); - const options = { - form, - filter, - holder, - store: this.store, - }; - groupFilterList = new GroupFilterableList(options); + groupFilterList = new GroupFilterableList(form, filter, holder); groupFilterList.initSearch(); eventHub.$on('toggleSubGroups', this.toggleSubGroups); eventHub.$on('leaveGroup', this.leaveGroup); + eventHub.$on('updateGroups', this.updateGroups); + eventHub.$on('updatePagination', this.updatePagination); }, mounted() { this.fetchGroups() .then((response) => { - this.store.storePagination(response.headers); + eventHub.$emit('updatePagination', response.headers); }) .catch(() => { // TODO: Handle error diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index 8d67c0244f3..b893a38a494 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -8,14 +8,20 @@ export default class GroupsService { this.groups = Vue.resource(endpoint); } - getGroups(parentId, page) { + getGroups(parentId, page, filterGroups) { const data = {}; if (parentId) { data.parent_id = parentId; - // Do not send this param for sub groups - } else if (page) { - data.page = page; + } else { + // Do not send the following param for sub groups + if (page) { + data.page = page; + } + + if (filterGroups) { + data.filter_groups = filterGroups; + } } return this.groups.get(data); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 4f31efb67a7..d8353a92881 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -18,6 +18,7 @@ export default class GroupsStore { return tree; } + // eslint-disable-next-line class-methods-use-this resetGroups(parent) { const parentGroup = parent; parentGroup.subGroups = {}; @@ -107,6 +108,7 @@ export default class GroupsStore { return this.groups; } + // eslint-disable-next-line class-methods-use-this decorateGroup(rawGroup) { return { id: rawGroup.id, From 323a326c73f4aabf37bf79f8e42350c128983c2d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 6 Jun 2017 00:06:08 -0500 Subject: [PATCH 66/88] Improve pagination when searching or filtering [ci skip] --- app/assets/javascripts/filterable_list.js | 62 ++++++++++++------- .../javascripts/groups/components/groups.vue | 12 ++-- .../groups/groups_filterable_list.js | 57 +++++++++++++---- app/assets/javascripts/groups/index.js | 35 +++++++++-- .../groups/services/groups_service.js | 6 +- .../javascripts/lib/utils/common_utils.js | 4 +- app/views/dashboard/groups/_groups.html.haml | 2 +- 7 files changed, 130 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 17c39cc7bbb..139206cc185 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -8,7 +8,15 @@ export default class FilterableList { this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; - this.filterUrl = `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + this.isBusy = false; + } + + getFilterEndpoint() { + return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + } + + getPagePath() { + return this.getFilterEndpoint(); } initSearch() { @@ -20,9 +28,19 @@ export default class FilterableList { } onFilterInput() { - const url = this.filterForm.getAttribute('action'); - const data = $(this.filterForm).serialize(); - this.filterResults(url, data, 'filter-input'); + const $form = $(this.filterForm); + const queryData = {}; + const filterGroupsParam = $form.find('[name="filter_groups"]').val(); + + if (filterGroupsParam) { + queryData.filter_groups = filterGroupsParam; + } + + this.filterResults(queryData); + + if (this.setDefaultFilterOption) { + this.setDefaultFilterOption(); + } } bindEvents() { @@ -33,42 +51,44 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(url, data, comingFrom) { - const endpoint = url || this.filterForm.getAttribute('action'); - const additionalData = data || $(this.filterForm).serialize(); + filterResults(queryData) { + if (this.isBusy) { + return false; + } $(this.listHolderElement).fadeTo(250, 0.5); return $.ajax({ - url: endpoint, - data: additionalData, + url: this.getFilterEndpoint(), + data: queryData, type: 'GET', dataType: 'json', context: this, complete: this.onFilterComplete, + beforeSend: () => { + this.isBusy = true; + }, success: (response, textStatus, xhr) => { - if (this.preOnFilterSuccess) { - this.preOnFilterSuccess(comingFrom); - } - - this.onFilterSuccess(response, xhr); + this.onFilterSuccess(response, xhr, queryData); }, }); } - onFilterSuccess(data) { - if (data.html) { - this.listHolderElement.innerHTML = data.html; + onFilterSuccess(response, xhr, queryData) { + if (response.html) { + this.listHolderElement.innerHTML = response.html; } - // Change url so if user reload a page - search results are saved - return window.history.replaceState({ - page: this.filterUrl, + // Change url so if user reload a page - search results are saved + const currentPath = this.getPagePath(queryData); - }, document.title, this.filterUrl); + return window.history.replaceState({ + page: currentPath, + }, document.title, currentPath); } onFilterComplete() { + this.isBusy = false; $(this.listHolderElement).fadeTo(250, 1); } } diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 6ddf54275d9..759b68c8bb4 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,5 +1,6 @@