Merge branch '25426-group-dashboard-ui' into 'master'
Resolve "Group dashboard UI" Closes #25426 See merge request !11098
This commit is contained in:
commit
4b1c49171d
|
@ -160,9 +160,6 @@ import initSettingsPanels from './settings_panels';
|
||||||
case 'admin:projects:index':
|
case 'admin:projects:index':
|
||||||
new ProjectsList();
|
new ProjectsList();
|
||||||
break;
|
break;
|
||||||
case 'dashboard:groups:index':
|
|
||||||
new GroupsList();
|
|
||||||
break;
|
|
||||||
case 'explore:groups:index':
|
case 'explore:groups:index':
|
||||||
new GroupsList();
|
new GroupsList();
|
||||||
|
|
||||||
|
|
|
@ -8,39 +8,87 @@ export default class FilterableList {
|
||||||
this.filterForm = form;
|
this.filterForm = form;
|
||||||
this.listFilterElement = filter;
|
this.listFilterElement = filter;
|
||||||
this.listHolderElement = holder;
|
this.listHolderElement = holder;
|
||||||
|
this.isBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterEndpoint() {
|
||||||
|
return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPagePath() {
|
||||||
|
return this.getFilterEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
initSearch() {
|
initSearch() {
|
||||||
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
|
// Wrap to prevent passing event arguments to .filterResults;
|
||||||
|
this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500);
|
||||||
|
|
||||||
this.listFilterElement.removeEventListener('input', this.debounceFilter);
|
this.unbindEvents();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterInput() {
|
||||||
|
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() {
|
||||||
this.listFilterElement.addEventListener('input', this.debounceFilter);
|
this.listFilterElement.addEventListener('input', this.debounceFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterResults() {
|
unbindEvents() {
|
||||||
const form = this.filterForm;
|
this.listFilterElement.removeEventListener('input', this.debounceFilter);
|
||||||
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
|
}
|
||||||
|
|
||||||
|
filterResults(queryData) {
|
||||||
|
if (this.isBusy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$(this.listHolderElement).fadeTo(250, 0.5);
|
$(this.listHolderElement).fadeTo(250, 0.5);
|
||||||
|
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
url: form.getAttribute('action'),
|
url: this.getFilterEndpoint(),
|
||||||
data: $(form).serialize(),
|
data: queryData,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
context: this,
|
context: this,
|
||||||
complete() {
|
complete: this.onFilterComplete,
|
||||||
$(this.listHolderElement).fadeTo(250, 1);
|
beforeSend: () => {
|
||||||
|
this.isBusy = true;
|
||||||
},
|
},
|
||||||
success(data) {
|
success: (response, textStatus, xhr) => {
|
||||||
this.listHolderElement.innerHTML = data.html;
|
this.onFilterSuccess(response, xhr, queryData);
|
||||||
|
|
||||||
// Change url so if user reload a page - search results are saved
|
|
||||||
return window.history.replaceState({
|
|
||||||
page: filterUrl,
|
|
||||||
|
|
||||||
}, document.title, filterUrl);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFilterSuccess(response, xhr, queryData) {
|
||||||
|
if (response.html) {
|
||||||
|
this.listHolderElement.innerHTML = response.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change url so if user reload a page - search results are saved
|
||||||
|
const currentPath = this.getPagePath(queryData);
|
||||||
|
|
||||||
|
return window.history.replaceState({
|
||||||
|
page: currentPath,
|
||||||
|
}, document.title, currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterComplete() {
|
||||||
|
this.isBusy = false;
|
||||||
|
$(this.listHolderElement).fadeTo(250, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
groups: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
baseGroup: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul class="content-list group-list-tree">
|
||||||
|
<group-item
|
||||||
|
v-for="(group, index) in groups"
|
||||||
|
:key="index"
|
||||||
|
:group="group"
|
||||||
|
:base-group="baseGroup"
|
||||||
|
:collection="groups"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
|
@ -0,0 +1,220 @@
|
||||||
|
<script>
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
baseGroup: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClickRowGroup(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Skip for buttons
|
||||||
|
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
|
||||||
|
if (this.group.hasSubgroups) {
|
||||||
|
eventHub.$emit('toggleSubGroups', this.group);
|
||||||
|
} else {
|
||||||
|
window.location.href = this.group.webUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLeaveGroup(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
|
||||||
|
this.leaveGroup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leaveGroup() {
|
||||||
|
eventHub.$emit('leaveGroup', this.group, this.collection);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
groupDomId() {
|
||||||
|
return `group-${this.group.id}`;
|
||||||
|
},
|
||||||
|
rowClass() {
|
||||||
|
return {
|
||||||
|
'group-row': true,
|
||||||
|
'is-open': this.group.isOpen,
|
||||||
|
'has-subgroups': this.group.hasSubgroups,
|
||||||
|
'no-description': !this.group.description,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
visibilityIcon() {
|
||||||
|
return {
|
||||||
|
fa: true,
|
||||||
|
'fa-globe': this.group.visibility === 'public',
|
||||||
|
'fa-shield': this.group.visibility === 'internal',
|
||||||
|
'fa-lock': this.group.visibility === 'private',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fullPath() {
|
||||||
|
let fullPath = '';
|
||||||
|
|
||||||
|
if (this.group.isOrphan) {
|
||||||
|
// check if current group is baseGroup
|
||||||
|
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
|
||||||
|
// Remove baseGroup prefix from our current group.fullName. e.g:
|
||||||
|
// baseGroup.fullName: `level1`
|
||||||
|
// group.fullName: `level1 / level2 / level3`
|
||||||
|
// Result: `level2 / level3`
|
||||||
|
const gfn = this.group.fullName;
|
||||||
|
const bfn = this.baseGroup.fullName;
|
||||||
|
const length = bfn.length;
|
||||||
|
const start = gfn.indexOf(bfn);
|
||||||
|
const extraPrefixChars = 3;
|
||||||
|
|
||||||
|
fullPath = gfn.substr(start + length + extraPrefixChars);
|
||||||
|
} else {
|
||||||
|
fullPath = this.group.fullName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fullPath = this.group.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
},
|
||||||
|
hasGroups() {
|
||||||
|
return Object.keys(this.group.subGroups).length > 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
@click.stop="onClickRowGroup"
|
||||||
|
:id="groupDomId"
|
||||||
|
:class="rowClass"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="group-row-contents">
|
||||||
|
<div
|
||||||
|
class="controls">
|
||||||
|
<a
|
||||||
|
v-if="group.canEdit"
|
||||||
|
class="edit-group btn"
|
||||||
|
:href="group.editPath">
|
||||||
|
<i
|
||||||
|
class="fa fa-cogs"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
@click="onLeaveGroup"
|
||||||
|
:href="group.leavePath"
|
||||||
|
class="leave-group btn"
|
||||||
|
title="Leave this group">
|
||||||
|
<i
|
||||||
|
class="fa fa-sign-out"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="stats">
|
||||||
|
<span
|
||||||
|
class="number-projects">
|
||||||
|
<i
|
||||||
|
class="fa fa-bookmark"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
{{group.numberProjects}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="number-users">
|
||||||
|
<i
|
||||||
|
class="fa fa-users"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
{{group.numberUsers}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="group-visibility">
|
||||||
|
<i
|
||||||
|
:class="visibilityIcon"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="folder-toggle-wrap">
|
||||||
|
<span
|
||||||
|
class="folder-caret"
|
||||||
|
v-if="group.hasSubgroups">
|
||||||
|
<i
|
||||||
|
v-if="group.isOpen"
|
||||||
|
class="fa fa-caret-down"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
<i
|
||||||
|
v-if="!group.isOpen"
|
||||||
|
class="fa fa-caret-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
<span class="folder-icon">
|
||||||
|
<i
|
||||||
|
v-if="group.isOpen"
|
||||||
|
class="fa fa-folder-open"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
<i
|
||||||
|
v-if="!group.isOpen"
|
||||||
|
class="fa fa-folder"
|
||||||
|
aria-hidden="true">
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="avatar-container s40 hidden-xs">
|
||||||
|
<a
|
||||||
|
:href="group.webUrl">
|
||||||
|
<img
|
||||||
|
class="avatar s40"
|
||||||
|
:src="group.avatarUrl"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="title">
|
||||||
|
<a
|
||||||
|
:href="group.webUrl">{{fullPath}}</a>
|
||||||
|
<template v-if="group.permissions.humanGroupAccess">
|
||||||
|
as
|
||||||
|
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="description">{{group.description}}</div>
|
||||||
|
</div>
|
||||||
|
<group-folder
|
||||||
|
v-if="group.isOpen && hasGroups"
|
||||||
|
:groups="group.subGroups"
|
||||||
|
:baseGroup="group"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</template>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
groups: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
pageInfo: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
tablePagination,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
change(page) {
|
||||||
|
const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
|
||||||
|
const sortParam = gl.utils.getParameterByName('sort');
|
||||||
|
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="groups-list-tree-container">
|
||||||
|
<group-folder
|
||||||
|
:groups="groups"
|
||||||
|
/>
|
||||||
|
<table-pagination
|
||||||
|
:change="change"
|
||||||
|
:pageInfo="pageInfo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default new Vue();
|
|
@ -0,0 +1,87 @@
|
||||||
|
import FilterableList from '~/filterable_list';
|
||||||
|
import eventHub from './event_hub';
|
||||||
|
|
||||||
|
export default class GroupFilterableList extends FilterableList {
|
||||||
|
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
|
||||||
|
super(form, filter, holder);
|
||||||
|
this.form = form;
|
||||||
|
this.filterEndpoint = filterEndpoint;
|
||||||
|
this.pagePath = pagePath;
|
||||||
|
this.$dropdown = $('.js-group-filter-dropdown-wrap');
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterEndpoint() {
|
||||||
|
return this.filterEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPagePath(queryData) {
|
||||||
|
const params = queryData ? $.param(queryData) : '';
|
||||||
|
const queryString = params ? `?${params}` : '';
|
||||||
|
return `${this.pagePath}${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
super.bindEvents();
|
||||||
|
|
||||||
|
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
|
||||||
|
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
|
||||||
|
|
||||||
|
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
|
||||||
|
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $form = $(this.form);
|
||||||
|
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
|
||||||
|
const queryData = {};
|
||||||
|
|
||||||
|
if (filterGroupsParam) {
|
||||||
|
queryData.filter_groups = filterGroupsParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filterResults(queryData);
|
||||||
|
this.setDefaultFilterOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultFilterOption() {
|
||||||
|
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
|
||||||
|
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOptionClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const queryData = {};
|
||||||
|
const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
|
||||||
|
|
||||||
|
if (sortParam) {
|
||||||
|
queryData.sort = sortParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filterResults(queryData);
|
||||||
|
|
||||||
|
// Active selected option
|
||||||
|
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
|
||||||
|
|
||||||
|
// Clear current value on search form
|
||||||
|
this.form.querySelector('[name="filter_groups"]').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterSuccess(data, xhr, queryData) {
|
||||||
|
super.onFilterSuccess(data, xhr, queryData);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/* global Flash */
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
import GroupFilterableList from './groups_filterable_list';
|
||||||
|
import GroupsComponent from './components/groups.vue';
|
||||||
|
import GroupFolder from './components/group_folder.vue';
|
||||||
|
import GroupItem from './components/group_item.vue';
|
||||||
|
import GroupsStore from './stores/groups_store';
|
||||||
|
import GroupsService from './services/groups_service';
|
||||||
|
import eventHub from './event_hub';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const el = document.getElementById('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);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Vue({
|
||||||
|
el,
|
||||||
|
data() {
|
||||||
|
this.store = new GroupsStore();
|
||||||
|
this.service = new GroupsService(el.dataset.endpoint);
|
||||||
|
|
||||||
|
return {
|
||||||
|
store: this.store,
|
||||||
|
isLoading: true,
|
||||||
|
state: this.store.state,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isEmpty() {
|
||||||
|
return Object.keys(this.state.groups).length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchGroups(parentGroup) {
|
||||||
|
let parentId = null;
|
||||||
|
let getGroups = null;
|
||||||
|
let page = null;
|
||||||
|
let sort = null;
|
||||||
|
let pageParam = null;
|
||||||
|
let sortParam = null;
|
||||||
|
let filterGroups = null;
|
||||||
|
let filterGroupsParam = null;
|
||||||
|
|
||||||
|
if (parentGroup) {
|
||||||
|
parentId = parentGroup.id;
|
||||||
|
} else {
|
||||||
|
this.isLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageParam = gl.utils.getParameterByName('page');
|
||||||
|
if (pageParam) {
|
||||||
|
page = pageParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterGroupsParam = gl.utils.getParameterByName('filter_groups');
|
||||||
|
if (filterGroupsParam) {
|
||||||
|
filterGroups = filterGroupsParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortParam = gl.utils.getParameterByName('sort');
|
||||||
|
if (sortParam) {
|
||||||
|
sort = sortParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
|
||||||
|
getGroups
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((response) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.updateGroups(response, parentGroup);
|
||||||
|
})
|
||||||
|
.catch(this.handleErrorResponse);
|
||||||
|
|
||||||
|
return getGroups;
|
||||||
|
},
|
||||||
|
fetchPage(page, filterGroups, sort) {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
return this.service
|
||||||
|
.getGroups(null, page, filterGroups, sort)
|
||||||
|
.then((response) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
$.scrollTo(0);
|
||||||
|
|
||||||
|
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
|
||||||
|
window.history.replaceState({
|
||||||
|
page: currentPath,
|
||||||
|
}, document.title, currentPath);
|
||||||
|
|
||||||
|
this.updateGroups(response.json());
|
||||||
|
this.updatePagination(response.headers);
|
||||||
|
})
|
||||||
|
.catch(this.handleErrorResponse);
|
||||||
|
},
|
||||||
|
toggleSubGroups(parentGroup = null) {
|
||||||
|
if (!parentGroup.isOpen) {
|
||||||
|
this.store.resetGroups(parentGroup);
|
||||||
|
this.fetchGroups(parentGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.toggleSubGroups(parentGroup);
|
||||||
|
},
|
||||||
|
leaveGroup(group, collection) {
|
||||||
|
this.service.leaveGroup(group.leavePath)
|
||||||
|
.then((response) => {
|
||||||
|
$.scrollTo(0);
|
||||||
|
|
||||||
|
this.store.removeGroup(group, collection);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Flash(response.json().notice, 'notice');
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
let message = 'An error occurred. Please try again.';
|
||||||
|
|
||||||
|
if (response.status === 403) {
|
||||||
|
message = 'Failed to leave the group. Please make sure you are not the only owner';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Flash(message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateGroups(groups, parentGroup) {
|
||||||
|
this.store.setGroups(groups, parentGroup);
|
||||||
|
},
|
||||||
|
updatePagination(headers) {
|
||||||
|
this.store.storePagination(headers);
|
||||||
|
},
|
||||||
|
handleErrorResponse() {
|
||||||
|
this.isLoading = false;
|
||||||
|
$.scrollTo(0);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Flash('An error occurred. Please try again.');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
eventHub.$on('fetchPage', this.fetchPage);
|
||||||
|
eventHub.$on('toggleSubGroups', this.toggleSubGroups);
|
||||||
|
eventHub.$on('leaveGroup', this.leaveGroup);
|
||||||
|
eventHub.$on('updateGroups', this.updateGroups);
|
||||||
|
eventHub.$on('updatePagination', this.updatePagination);
|
||||||
|
},
|
||||||
|
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');
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
form,
|
||||||
|
filter,
|
||||||
|
holder,
|
||||||
|
filterEndpoint: el.dataset.endpoint,
|
||||||
|
pagePath: el.dataset.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
groupFilterList = new GroupFilterableList(opts);
|
||||||
|
groupFilterList.initSearch();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchGroups()
|
||||||
|
.then((response) => {
|
||||||
|
this.updatePagination(response.headers);
|
||||||
|
this.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(this.handleErrorResponse);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
eventHub.$off('fetchPage', this.fetchPage);
|
||||||
|
eventHub.$off('toggleSubGroups', this.toggleSubGroups);
|
||||||
|
eventHub.$off('leaveGroup', this.leaveGroup);
|
||||||
|
eventHub.$off('updateGroups', this.updateGroups);
|
||||||
|
eventHub.$off('updatePagination', this.updatePagination);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueResource from 'vue-resource';
|
||||||
|
|
||||||
|
Vue.use(VueResource);
|
||||||
|
|
||||||
|
export default class GroupsService {
|
||||||
|
constructor(endpoint) {
|
||||||
|
this.groups = Vue.resource(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups(parentId, page, filterGroups, sort) {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
data.parent_id = parentId;
|
||||||
|
} else {
|
||||||
|
// Do not send the following param for sub groups
|
||||||
|
if (page) {
|
||||||
|
data.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterGroups) {
|
||||||
|
data.filter_groups = filterGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
data.sort = sort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.groups.get(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
leaveGroup(endpoint) {
|
||||||
|
return Vue.http.delete(endpoint);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default class GroupsStore {
|
||||||
|
constructor() {
|
||||||
|
this.state = {};
|
||||||
|
this.state.groups = {};
|
||||||
|
this.state.pageInfo = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroups(rawGroups, parent) {
|
||||||
|
const parentGroup = parent;
|
||||||
|
const tree = this.buildTree(rawGroups, parentGroup);
|
||||||
|
|
||||||
|
if (parentGroup) {
|
||||||
|
parentGroup.subGroups = tree;
|
||||||
|
} else {
|
||||||
|
this.state.groups = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
resetGroups(parent) {
|
||||||
|
const parentGroup = parent;
|
||||||
|
parentGroup.subGroups = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
storePagination(pagination = {}) {
|
||||||
|
let paginationInfo;
|
||||||
|
|
||||||
|
if (Object.keys(pagination).length) {
|
||||||
|
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
|
||||||
|
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
|
||||||
|
} else {
|
||||||
|
paginationInfo = pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.pageInfo = paginationInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTree(rawGroups, parentGroup) {
|
||||||
|
const groups = this.decorateGroups(rawGroups);
|
||||||
|
const tree = {};
|
||||||
|
const mappedGroups = {};
|
||||||
|
const orphans = [];
|
||||||
|
|
||||||
|
// Map groups to an object
|
||||||
|
groups.map((group) => {
|
||||||
|
mappedGroups[group.id] = group;
|
||||||
|
mappedGroups[group.id].subGroups = {};
|
||||||
|
return group;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(mappedGroups).map((key) => {
|
||||||
|
const currentGroup = mappedGroups[key];
|
||||||
|
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
|
||||||
|
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
|
||||||
|
tree[currentGroup.id] = currentGroup;
|
||||||
|
} else {
|
||||||
|
// Means the groups hast no direct parent.
|
||||||
|
// Save for later processing, we will add them to its corresponding base group
|
||||||
|
orphans.push(currentGroup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the group is at the root level, add it to first level elements array.
|
||||||
|
tree[currentGroup.id] = currentGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hopefully this array will be empty for most cases
|
||||||
|
if (orphans.length) {
|
||||||
|
orphans.map((orphan) => {
|
||||||
|
let found = false;
|
||||||
|
const currentOrphan = orphan;
|
||||||
|
|
||||||
|
Object.keys(tree).map((key) => {
|
||||||
|
const group = tree[key];
|
||||||
|
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
|
||||||
|
group.subGroups[currentOrphan.id] = currentOrphan;
|
||||||
|
group.isOpen = true;
|
||||||
|
currentOrphan.isOrphan = true;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
currentOrphan.isOrphan = true;
|
||||||
|
tree[currentOrphan.id] = currentOrphan;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orphan;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
decorateGroups(rawGroups) {
|
||||||
|
this.groups = rawGroups.map(this.decorateGroup);
|
||||||
|
return this.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
decorateGroup(rawGroup) {
|
||||||
|
return {
|
||||||
|
id: rawGroup.id,
|
||||||
|
fullName: rawGroup.full_name,
|
||||||
|
fullPath: rawGroup.full_path,
|
||||||
|
avatarUrl: rawGroup.avatar_url,
|
||||||
|
name: rawGroup.name,
|
||||||
|
hasSubgroups: rawGroup.has_subgroups,
|
||||||
|
canEdit: rawGroup.can_edit,
|
||||||
|
description: rawGroup.description,
|
||||||
|
webUrl: rawGroup.web_url,
|
||||||
|
parentId: rawGroup.parent_id,
|
||||||
|
visibility: rawGroup.visibility,
|
||||||
|
leavePath: rawGroup.leave_path,
|
||||||
|
editPath: rawGroup.edit_path,
|
||||||
|
isOpen: false,
|
||||||
|
isOrphan: false,
|
||||||
|
numberProjects: rawGroup.number_projects_with_delimiter,
|
||||||
|
numberUsers: rawGroup.number_users_with_delimiter,
|
||||||
|
permissions: {
|
||||||
|
humanGroupAccess: rawGroup.permissions.human_group_access,
|
||||||
|
},
|
||||||
|
subGroups: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
removeGroup(group, collection) {
|
||||||
|
Vue.delete(collection, group.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
toggleSubGroups(toggleGroup) {
|
||||||
|
const group = toggleGroup;
|
||||||
|
group.isOpen = !group.isOpen;
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
|
@ -167,8 +167,8 @@
|
||||||
if the name does not exist this function will return `null`
|
if the name does not exist this function will return `null`
|
||||||
otherwise it will return the value of the param key provided
|
otherwise it will return the value of the param key provided
|
||||||
*/
|
*/
|
||||||
w.gl.utils.getParameterByName = (name) => {
|
w.gl.utils.getParameterByName = (name, parseUrl) => {
|
||||||
const url = window.location.href;
|
const url = parseUrl || window.location.href;
|
||||||
name = name.replace(/[[\]]/g, '\\$&');
|
name = name.replace(/[[\]]/g, '\\$&');
|
||||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||||
const results = regex.exec(url);
|
const results = regex.exec(url);
|
||||||
|
|
|
@ -284,9 +284,7 @@ export default {
|
||||||
|
|
||||||
<table-pagination
|
<table-pagination
|
||||||
v-if="shouldRenderPagination"
|
v-if="shouldRenderPagination"
|
||||||
:pagenum="pagenum"
|
|
||||||
:change="change"
|
:change="change"
|
||||||
:count="state.count.all"
|
|
||||||
:pageInfo="state.pageInfo"
|
:pageInfo="state.pageInfo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -264,3 +264,103 @@ ul.controls {
|
||||||
ul.indent-list {
|
ul.indent-list {
|
||||||
padding: 10px 0 0 30px;
|
padding: 10px 0 0 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Specific styles for tree list
|
||||||
|
.group-list-tree {
|
||||||
|
.folder-toggle-wrap {
|
||||||
|
float: left;
|
||||||
|
line-height: $list-text-height;
|
||||||
|
font-size: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: $gl-font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-caret,
|
||||||
|
.folder-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-caret {
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .group-row:not(.has-subgroups) {
|
||||||
|
.folder-caret .fa {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-list li:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-tree {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
bottom: 0;
|
||||||
|
left: -16px;
|
||||||
|
border-left: 2px solid $border-white-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 10px;
|
||||||
|
height: 0;
|
||||||
|
border-top: 2px solid $border-white-normal;
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child::before {
|
||||||
|
background: $white-light;
|
||||||
|
height: auto;
|
||||||
|
top: 30px;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-row-contents {
|
||||||
|
padding: 10px 10px 8px;
|
||||||
|
border-top: solid 1px transparent;
|
||||||
|
border-bottom: solid 1px $white-normal;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $row-hover-border;
|
||||||
|
background-color: $row-hover;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.js-groups-list-holder {
|
||||||
|
.groups-list-loading {
|
||||||
|
font-size: 34px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -52,9 +52,14 @@ module MembershipActions
|
||||||
"You left the \"#{membershipable.human_name}\" #{source_type}."
|
"You left the \"#{membershipable.human_name}\" #{source_type}."
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
|
||||||
|
redirect_to redirect_path, notice: notice
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to redirect_path, notice: notice
|
format.json { render json: { notice: notice } }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
class Dashboard::GroupsController < Dashboard::ApplicationController
|
class Dashboard::GroupsController < Dashboard::ApplicationController
|
||||||
def index
|
def index
|
||||||
@group_members = current_user.group_members.includes(source: :route).joins(:group)
|
@groups =
|
||||||
@group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present?
|
if params[:parent_id] && Group.supports_nested_groups?
|
||||||
@group_members = @group_members.merge(Group.sort(@sort = params[:sort]))
|
parent = Group.find_by(id: params[:parent_id])
|
||||||
@group_members = @group_members.page(params[:page])
|
|
||||||
|
if can?(current_user, :read_group, parent)
|
||||||
|
GroupsFinder.new(current_user, parent: parent).execute
|
||||||
|
else
|
||||||
|
Group.none
|
||||||
|
end
|
||||||
|
else
|
||||||
|
current_user.groups
|
||||||
|
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|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.json do
|
format.json do
|
||||||
render json: {
|
render json: GroupSerializer
|
||||||
html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members })
|
.new(current_user: @current_user)
|
||||||
}
|
.with_pagination(request, response)
|
||||||
|
.represent(@groups)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ module GroupsHelper
|
||||||
group = Group.find_by_full_path(group)
|
group = Group.find_by_full_path(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
group.try(:avatar_url) || image_path('no_group_avatar.png')
|
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_title(group, name = nil, url = nil)
|
def group_title(group, name = nil, url = nil)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
class GroupEntity < Grape::Entity
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include RequestAwareEntity
|
||||||
|
include MembersHelper
|
||||||
|
include GroupsHelper
|
||||||
|
|
||||||
|
expose :id, :name, :path, :description, :visibility
|
||||||
|
expose :web_url
|
||||||
|
expose :full_name, :full_path
|
||||||
|
expose :parent_id
|
||||||
|
expose :created_at, :updated_at
|
||||||
|
|
||||||
|
expose :permissions do
|
||||||
|
expose :human_group_access do |group, options|
|
||||||
|
group.group_members.find_by(user_id: request.current_user)&.human_access
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :edit_path do |group|
|
||||||
|
edit_group_path(group)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :leave_path do |group|
|
||||||
|
leave_group_group_members_path(group)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :can_edit do |group|
|
||||||
|
can?(request.current_user, :admin_group, group)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :has_subgroups do |group|
|
||||||
|
GroupsFinder.new(request.current_user, parent: group).execute.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :number_projects_with_delimiter do |group|
|
||||||
|
number_with_delimiter(GroupProjectsFinder.new(group: group, current_user: request.current_user).execute.count)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :number_users_with_delimiter do |group|
|
||||||
|
number_with_delimiter(group.users.count)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :avatar_url do |group|
|
||||||
|
group_icon(group)
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -1,6 +1,9 @@
|
||||||
.js-groups-list-holder
|
.js-groups-list-holder
|
||||||
%ul.content-list
|
#dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } }
|
||||||
- @group_members.each do |group_member|
|
.groups-list-loading
|
||||||
= render 'shared/groups/group', group: group_member.group, group_member: group_member
|
= icon('spinner spin', 'v-show' => 'isLoading')
|
||||||
|
%template{ 'v-if' => '!isLoading && isEmpty' }
|
||||||
= paginate @group_members, theme: 'gitlab'
|
%div{ 'v-cloak' => true }
|
||||||
|
= render 'empty_state'
|
||||||
|
%template{ 'v-else-if' => '!isLoading && !isEmpty' }
|
||||||
|
%groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' }
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
- header_title "Groups", dashboard_groups_path
|
- header_title "Groups", dashboard_groups_path
|
||||||
= render 'dashboard/groups_head'
|
= render 'dashboard/groups_head'
|
||||||
|
|
||||||
- if @group_members.empty?
|
= webpack_bundle_tag 'common_vue'
|
||||||
|
= webpack_bundle_tag 'groups'
|
||||||
|
|
||||||
|
- if @groups.empty?
|
||||||
= render 'empty_state'
|
= render 'empty_state'
|
||||||
- else
|
- else
|
||||||
= render 'groups'
|
= render 'groups'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
.dropdown.inline
|
.dropdown.inline.js-group-filter-dropdown-wrap
|
||||||
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||||
%span.light
|
%span.dropdown-label
|
||||||
- if @sort.present?
|
- if @sort.present?
|
||||||
= sort_options_hash[@sort]
|
= sort_options_hash[@sort]
|
||||||
- else
|
- else
|
||||||
= sort_title_recently_created
|
= sort_title_recently_created
|
||||||
= icon('chevron-down')
|
= icon('chevron-down')
|
||||||
%ul.dropdown-menu.dropdown-menu-align-right
|
%ul.dropdown-menu.dropdown-menu-align-right
|
||||||
%li
|
%li
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Update Dashboard Groups UI with better support for subgroups
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -40,6 +40,7 @@ var config = {
|
||||||
filtered_search: './filtered_search/filtered_search_bundle.js',
|
filtered_search: './filtered_search/filtered_search_bundle.js',
|
||||||
graphs: './graphs/graphs_bundle.js',
|
graphs: './graphs/graphs_bundle.js',
|
||||||
group: './group.js',
|
group: './group.js',
|
||||||
|
groups: './groups/index.js',
|
||||||
groups_list: './groups_list.js',
|
groups_list: './groups_list.js',
|
||||||
issue_show: './issue_show/index.js',
|
issue_show: './issue_show/index.js',
|
||||||
integrations: './integrations',
|
integrations: './integrations',
|
||||||
|
@ -155,6 +156,7 @@ var config = {
|
||||||
'environments',
|
'environments',
|
||||||
'environments_folder',
|
'environments_folder',
|
||||||
'filtered_search',
|
'filtered_search',
|
||||||
|
'groups',
|
||||||
'issue_show',
|
'issue_show',
|
||||||
'merge_conflicts',
|
'merge_conflicts',
|
||||||
'notebook_viewer',
|
'notebook_viewer',
|
||||||
|
|
|
@ -124,6 +124,13 @@ describe Groups::GroupMembersController do
|
||||||
expect(response).to redirect_to(dashboard_groups_path)
|
expect(response).to redirect_to(dashboard_groups_path)
|
||||||
expect(group.users).not_to include user
|
expect(group.users).not_to include user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'supports json request' do
|
||||||
|
delete :leave, group_id: group, format: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response['notice']).to eq "You left the \"#{group.name}\" group."
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'and is an owner' do
|
context 'and is an owner' do
|
||||||
|
|
|
@ -6,40 +6,124 @@ describe 'Dashboard Groups page', js: true, feature: true do
|
||||||
let!(:nested_group) { create(:group, :nested) }
|
let!(:nested_group) { create(:group, :nested) }
|
||||||
let!(:another_group) { create(:group) }
|
let!(:another_group) { create(:group) }
|
||||||
|
|
||||||
before do
|
it 'shows groups user is member of' do
|
||||||
group.add_owner(user)
|
group.add_owner(user)
|
||||||
nested_group.add_owner(user)
|
nested_group.add_owner(user)
|
||||||
|
|
||||||
login_as(user)
|
login_as(user)
|
||||||
|
|
||||||
visit dashboard_groups_path
|
visit dashboard_groups_path
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows groups user is member of' do
|
|
||||||
expect(page).to have_content(group.full_name)
|
|
||||||
expect(page).to have_content(nested_group.full_name)
|
|
||||||
expect(page).not_to have_content(another_group.full_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters groups' do
|
|
||||||
fill_in 'filter_groups', with: group.name
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
expect(page).to have_content(group.full_name)
|
|
||||||
expect(page).not_to have_content(nested_group.full_name)
|
|
||||||
expect(page).not_to have_content(another_group.full_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets search when user cleans the input' do
|
|
||||||
fill_in 'filter_groups', with: group.name
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
fill_in 'filter_groups', with: ""
|
|
||||||
wait_for_requests
|
|
||||||
|
|
||||||
expect(page).to have_content(group.full_name)
|
expect(page).to have_content(group.full_name)
|
||||||
expect(page).to have_content(nested_group.full_name)
|
expect(page).to have_content(nested_group.full_name)
|
||||||
expect(page).not_to have_content(another_group.full_name)
|
expect(page).not_to have_content(another_group.full_name)
|
||||||
expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
|
end
|
||||||
|
|
||||||
|
describe 'when filtering groups' do
|
||||||
|
before do
|
||||||
|
group.add_owner(user)
|
||||||
|
nested_group.add_owner(user)
|
||||||
|
|
||||||
|
login_as(user)
|
||||||
|
|
||||||
|
visit dashboard_groups_path
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters groups' do
|
||||||
|
fill_in 'filter_groups', with: group.name
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_content(group.full_name)
|
||||||
|
expect(page).not_to have_content(nested_group.full_name)
|
||||||
|
expect(page).not_to have_content(another_group.full_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets search when user cleans the input' do
|
||||||
|
fill_in 'filter_groups', with: group.name
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
fill_in 'filter_groups', with: ""
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_content(group.full_name)
|
||||||
|
expect(page).to have_content(nested_group.full_name)
|
||||||
|
expect(page).not_to have_content(another_group.full_name)
|
||||||
|
expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'group with subgroups' do
|
||||||
|
let!(:subgroup) { create(:group, :public, parent: group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.add_owner(user)
|
||||||
|
subgroup.add_owner(user)
|
||||||
|
|
||||||
|
login_as(user)
|
||||||
|
|
||||||
|
visit dashboard_groups_path
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows subgroups inside of its parent group' do
|
||||||
|
expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2)
|
||||||
|
expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can toggle parent group' do
|
||||||
|
# Expanded by default
|
||||||
|
expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
|
||||||
|
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
|
||||||
|
|
||||||
|
# Collapse
|
||||||
|
find("#group-#{group.id}").trigger('click')
|
||||||
|
|
||||||
|
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down")
|
||||||
|
expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
|
||||||
|
expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
|
||||||
|
|
||||||
|
# Expand
|
||||||
|
find("#group-#{group.id}").trigger('click')
|
||||||
|
|
||||||
|
expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
|
||||||
|
expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
|
||||||
|
expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when using pagination' do
|
||||||
|
let(:group2) { create(:group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.add_owner(user)
|
||||||
|
group2.add_owner(user)
|
||||||
|
|
||||||
|
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
|
||||||
|
|
||||||
|
login_as(user)
|
||||||
|
visit dashboard_groups_path
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows pagination' do
|
||||||
|
expect(page).to have_selector('.gl-pagination')
|
||||||
|
expect(page).to have_selector('.gl-pagination .page', count: 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'loads results for next page' do
|
||||||
|
# Check first page
|
||||||
|
expect(page).to have_content(group2.full_name)
|
||||||
|
expect(page).to have_selector("#group-#{group2.id}")
|
||||||
|
expect(page).not_to have_content(group.full_name)
|
||||||
|
expect(page).not_to have_selector("#group-#{group.id}")
|
||||||
|
|
||||||
|
# Go to next page
|
||||||
|
find(".gl-pagination .page:not(.active) a").trigger('click')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
# Check second page
|
||||||
|
expect(page).to have_content(group.full_name)
|
||||||
|
expect(page).to have_selector("#group-#{group.id}")
|
||||||
|
expect(page).not_to have_content(group2.full_name)
|
||||||
|
expect(page).not_to have_selector("#group-#{group2.id}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import groupItemComponent from '~/groups/components/group_item.vue';
|
||||||
|
import GroupsStore from '~/groups/stores/groups_store';
|
||||||
|
import { group1 } from './mock_data';
|
||||||
|
|
||||||
|
describe('Groups Component', () => {
|
||||||
|
let GroupItemComponent;
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
let group;
|
||||||
|
|
||||||
|
describe('group with default data', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
GroupItemComponent = Vue.extend(groupItemComponent);
|
||||||
|
store = new GroupsStore();
|
||||||
|
group = store.decorateGroup(group1);
|
||||||
|
|
||||||
|
component = new GroupItemComponent({
|
||||||
|
propsData: {
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the group item correctly', () => {
|
||||||
|
expect(component.$el.classList.contains('group-row')).toBe(true);
|
||||||
|
expect(component.$el.classList.contains('.no-description')).toBe(false);
|
||||||
|
expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects);
|
||||||
|
expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers);
|
||||||
|
expect(component.$el.querySelector('.group-visibility')).toBeDefined();
|
||||||
|
expect(component.$el.querySelector('.avatar-container')).toBeDefined();
|
||||||
|
expect(component.$el.querySelector('.title').textContent).toContain(group.name);
|
||||||
|
expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess);
|
||||||
|
expect(component.$el.querySelector('.description').textContent).toContain(group.description);
|
||||||
|
expect(component.$el.querySelector('.edit-group')).toBeDefined();
|
||||||
|
expect(component.$el.querySelector('.leave-group')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('group without description', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
GroupItemComponent = Vue.extend(groupItemComponent);
|
||||||
|
store = new GroupsStore();
|
||||||
|
group1.description = '';
|
||||||
|
group = store.decorateGroup(group1);
|
||||||
|
|
||||||
|
component = new GroupItemComponent({
|
||||||
|
propsData: {
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render group item correctly', () => {
|
||||||
|
expect(component.$el.querySelector('.description').textContent).toBe('');
|
||||||
|
expect(component.$el.classList.contains('.no-description')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('user has not access to group', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
GroupItemComponent = Vue.extend(groupItemComponent);
|
||||||
|
store = new GroupsStore();
|
||||||
|
group1.permissions.human_group_access = null;
|
||||||
|
group = store.decorateGroup(group1);
|
||||||
|
|
||||||
|
component = new GroupItemComponent({
|
||||||
|
propsData: {
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display access type', () => {
|
||||||
|
expect(component.$el.querySelector('.access-type')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import groupFolderComponent from '~/groups/components/group_folder.vue';
|
||||||
|
import groupItemComponent from '~/groups/components/group_item.vue';
|
||||||
|
import groupsComponent from '~/groups/components/groups.vue';
|
||||||
|
import GroupsStore from '~/groups/stores/groups_store';
|
||||||
|
import { groupsData } from './mock_data';
|
||||||
|
|
||||||
|
describe('Groups Component', () => {
|
||||||
|
let GroupsComponent;
|
||||||
|
let store;
|
||||||
|
let component;
|
||||||
|
let groups;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
Vue.component('group-folder', groupFolderComponent);
|
||||||
|
Vue.component('group-item', groupItemComponent);
|
||||||
|
|
||||||
|
store = new GroupsStore();
|
||||||
|
groups = store.setGroups(groupsData.groups);
|
||||||
|
|
||||||
|
store.storePagination(groupsData.pagination);
|
||||||
|
|
||||||
|
GroupsComponent = Vue.extend(groupsComponent);
|
||||||
|
|
||||||
|
component = new GroupsComponent({
|
||||||
|
propsData: {
|
||||||
|
groups: store.state.groups,
|
||||||
|
pageInfo: store.state.pageInfo,
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with data', () => {
|
||||||
|
it('should render a list of groups', () => {
|
||||||
|
expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true);
|
||||||
|
expect(component.$el.querySelector('#group-12')).toBeDefined();
|
||||||
|
expect(component.$el.querySelector('#group-1119')).toBeDefined();
|
||||||
|
expect(component.$el.querySelector('#group-1120')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render group and its subgroup', () => {
|
||||||
|
const lists = component.$el.querySelectorAll('.group-list-tree');
|
||||||
|
|
||||||
|
expect(lists.length).toBe(3); // one parent and two subgroups
|
||||||
|
|
||||||
|
expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true);
|
||||||
|
expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true);
|
||||||
|
|
||||||
|
expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove prefix of parent group', () => {
|
||||||
|
expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,110 @@
|
||||||
|
const group1 = {
|
||||||
|
id: '12',
|
||||||
|
name: 'level1',
|
||||||
|
path: 'level1',
|
||||||
|
description: 'foo',
|
||||||
|
visibility: 'public',
|
||||||
|
avatar_url: null,
|
||||||
|
web_url: 'http://localhost:3000/groups/level1',
|
||||||
|
full_name: 'level1',
|
||||||
|
full_path: 'level1',
|
||||||
|
parent_id: null,
|
||||||
|
created_at: '2017-05-15T19:01:23.670Z',
|
||||||
|
updated_at: '2017-05-15T19:01:23.670Z',
|
||||||
|
number_projects_with_delimiter: '1',
|
||||||
|
number_users_with_delimiter: '1',
|
||||||
|
has_subgroups: true,
|
||||||
|
permissions: {
|
||||||
|
human_group_access: 'Master',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This group has no direct parent, should be placed as subgroup of group1
|
||||||
|
const group14 = {
|
||||||
|
id: 1128,
|
||||||
|
name: 'level4',
|
||||||
|
path: 'level4',
|
||||||
|
description: 'foo',
|
||||||
|
visibility: 'public',
|
||||||
|
avatar_url: null,
|
||||||
|
web_url: 'http://localhost:3000/groups/level1/level2/level3/level4',
|
||||||
|
full_name: 'level1 / level2 / level3 / level4',
|
||||||
|
full_path: 'level1/level2/level3/level4',
|
||||||
|
parent_id: 1127,
|
||||||
|
created_at: '2017-05-15T19:02:01.645Z',
|
||||||
|
updated_at: '2017-05-15T19:02:01.645Z',
|
||||||
|
number_projects_with_delimiter: '1',
|
||||||
|
number_users_with_delimiter: '1',
|
||||||
|
has_subgroups: true,
|
||||||
|
permissions: {
|
||||||
|
human_group_access: 'Master',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const group2 = {
|
||||||
|
id: 1119,
|
||||||
|
name: 'devops',
|
||||||
|
path: 'devops',
|
||||||
|
description: 'foo',
|
||||||
|
visibility: 'public',
|
||||||
|
avatar_url: null,
|
||||||
|
web_url: 'http://localhost:3000/groups/devops',
|
||||||
|
full_name: 'devops',
|
||||||
|
full_path: 'devops',
|
||||||
|
parent_id: null,
|
||||||
|
created_at: '2017-05-11T19:35:09.635Z',
|
||||||
|
updated_at: '2017-05-11T19:35:09.635Z',
|
||||||
|
number_projects_with_delimiter: '1',
|
||||||
|
number_users_with_delimiter: '1',
|
||||||
|
has_subgroups: true,
|
||||||
|
permissions: {
|
||||||
|
human_group_access: 'Master',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const group21 = {
|
||||||
|
id: 1120,
|
||||||
|
name: 'chef',
|
||||||
|
path: 'chef',
|
||||||
|
description: 'foo',
|
||||||
|
visibility: 'public',
|
||||||
|
avatar_url: null,
|
||||||
|
web_url: 'http://localhost:3000/groups/devops/chef',
|
||||||
|
full_name: 'devops / chef',
|
||||||
|
full_path: 'devops/chef',
|
||||||
|
parent_id: 1119,
|
||||||
|
created_at: '2017-05-11T19:51:04.060Z',
|
||||||
|
updated_at: '2017-05-11T19:51:04.060Z',
|
||||||
|
number_projects_with_delimiter: '1',
|
||||||
|
number_users_with_delimiter: '1',
|
||||||
|
has_subgroups: true,
|
||||||
|
permissions: {
|
||||||
|
human_group_access: 'Master',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupsData = {
|
||||||
|
groups: [group1, group14, group2, group21],
|
||||||
|
pagination: {
|
||||||
|
Date: 'Mon, 22 May 2017 22:31:52 GMT',
|
||||||
|
'X-Prev-Page': '1',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'X-Total': '31',
|
||||||
|
'Transfer-Encoding': 'chunked',
|
||||||
|
'X-Runtime': '0.611144',
|
||||||
|
'X-Xss-Protection': '1; mode=block',
|
||||||
|
'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09',
|
||||||
|
'X-Ua-Compatible': 'IE=edge',
|
||||||
|
'X-Per-Page': '20',
|
||||||
|
Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"',
|
||||||
|
'X-Next-Page': '',
|
||||||
|
Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"',
|
||||||
|
'X-Frame-Options': 'DENY',
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Cache-Control': 'max-age=0, private, must-revalidate',
|
||||||
|
'X-Total-Pages': '2',
|
||||||
|
'X-Page': '2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { groupsData, group1 };
|
|
@ -150,6 +150,14 @@ import '~/lib/utils/common_utils';
|
||||||
const value = gl.utils.getParameterByName('fakeParameter');
|
const value = gl.utils.getParameterByName('fakeParameter');
|
||||||
expect(value).toBe(null);
|
expect(value).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return valid paramentes if URL is provided', () => {
|
||||||
|
let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar');
|
||||||
|
expect(value).toBe('bar');
|
||||||
|
|
||||||
|
value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu');
|
||||||
|
expect(value).toBe('canchu');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('gl.utils.normalizedHeaders', () => {
|
describe('gl.utils.normalizedHeaders', () => {
|
||||||
|
|
|
@ -51,7 +51,6 @@ if (process.env.BABEL_ENV === 'coverage') {
|
||||||
'./environments/environments_bundle.js',
|
'./environments/environments_bundle.js',
|
||||||
'./filtered_search/filtered_search_bundle.js',
|
'./filtered_search/filtered_search_bundle.js',
|
||||||
'./graphs/graphs_bundle.js',
|
'./graphs/graphs_bundle.js',
|
||||||
'./issuable/issuable_bundle.js',
|
|
||||||
'./issuable/time_tracking/time_tracking_bundle.js',
|
'./issuable/time_tracking/time_tracking_bundle.js',
|
||||||
'./main.js',
|
'./main.js',
|
||||||
'./merge_conflicts/merge_conflicts_bundle.js',
|
'./merge_conflicts/merge_conflicts_bundle.js',
|
||||||
|
|
Loading…
Reference in New Issue