Resolve "Improve handling of projects shared with a group"
This commit is contained in:
parent
53fae9ad84
commit
5b74a1aebc
35 changed files with 593 additions and 249 deletions
|
@ -2,14 +2,15 @@
|
|||
/* global Flash */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { HIDDEN_CLASS } from '~/lib/utils/constants';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
import eventHub from '../event_hub';
|
||||
import { COMMON_STR } from '../constants';
|
||||
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
|
||||
import groupsComponent from './groups.vue';
|
||||
|
||||
export default {
|
||||
|
@ -19,6 +20,16 @@ export default {
|
|||
groupsComponent,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
containerId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
@ -56,31 +67,28 @@ export default {
|
|||
? COMMON_STR.GROUP_SEARCH_EMPTY
|
||||
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||
|
||||
eventHub.$on('fetchPage', this.fetchPage);
|
||||
eventHub.$on('toggleChildren', this.toggleChildren);
|
||||
eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal);
|
||||
eventHub.$on('updatePagination', this.updatePagination);
|
||||
eventHub.$on('updateGroups', this.updateGroups);
|
||||
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
|
||||
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
|
||||
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAllGroups();
|
||||
|
||||
if (this.containerId) {
|
||||
this.containerEl = document.getElementById(this.containerId);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('fetchPage', this.fetchPage);
|
||||
eventHub.$off('toggleChildren', this.toggleChildren);
|
||||
eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal);
|
||||
eventHub.$off('updatePagination', this.updatePagination);
|
||||
eventHub.$off('updateGroups', this.updateGroups);
|
||||
eventHub.$off(`${this.action}fetchPage`, this.fetchPage);
|
||||
eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren);
|
||||
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
|
||||
},
|
||||
methods: {
|
||||
fetchGroups({
|
||||
parentId,
|
||||
page,
|
||||
filterGroupsBy,
|
||||
sortBy,
|
||||
archived,
|
||||
updatePagination,
|
||||
}) {
|
||||
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
|
||||
return this.service
|
||||
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||
.then(res => {
|
||||
|
@ -165,13 +173,13 @@ export default {
|
|||
}
|
||||
},
|
||||
showLeaveGroupModal(group, parentGroup) {
|
||||
const { fullName } = group;
|
||||
this.targetGroup = group;
|
||||
this.targetParentGroup = parentGroup;
|
||||
this.showModal = true;
|
||||
this.groupLeaveConfirmationMessage = s__(
|
||||
`GroupsTree|Are you sure you want to leave the "${
|
||||
group.fullName
|
||||
}" group?`,
|
||||
this.groupLeaveConfirmationMessage = sprintf(
|
||||
s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
|
||||
{ fullName },
|
||||
);
|
||||
},
|
||||
hideLeaveGroupModal() {
|
||||
|
@ -197,16 +205,35 @@ export default {
|
|||
this.targetGroup.isBeingRemoved = false;
|
||||
});
|
||||
},
|
||||
showEmptyState() {
|
||||
const { containerEl } = this;
|
||||
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||
const emptyStateEl = containerEl.querySelector('.empty-state');
|
||||
|
||||
if (contentListEl) {
|
||||
contentListEl.remove();
|
||||
}
|
||||
|
||||
if (emptyStateEl) {
|
||||
emptyStateEl.classList.remove(HIDDEN_CLASS);
|
||||
}
|
||||
},
|
||||
updatePagination(headers) {
|
||||
this.store.setPaginationInfo(headers);
|
||||
},
|
||||
updateGroups(groups, fromSearch) {
|
||||
this.isSearchEmpty = groups ? groups.length === 0 : false;
|
||||
const hasGroups = groups && groups.length > 0;
|
||||
this.isSearchEmpty = !hasGroups;
|
||||
|
||||
if (fromSearch) {
|
||||
this.store.setSearchedGroups(groups);
|
||||
} else {
|
||||
this.store.setGroups(groups);
|
||||
}
|
||||
|
||||
if (this.action && !hasGroups && !fromSearch) {
|
||||
this.showEmptyState();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -226,6 +253,7 @@ export default {
|
|||
:search-empty="isSearchEmpty"
|
||||
:search-empty-message="searchEmptyMessage"
|
||||
:page-info="pageInfo"
|
||||
:action="action"
|
||||
/>
|
||||
<deprecated-modal
|
||||
v-show="showModal"
|
||||
|
|
|
@ -11,8 +11,12 @@ export default {
|
|||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => ([]),
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -37,6 +41,7 @@ export default {
|
|||
:key="index"
|
||||
:group="group"
|
||||
:parent-group="parentGroup"
|
||||
:action="action"
|
||||
/>
|
||||
<li
|
||||
v-if="hasMoreChildren"
|
||||
|
|
|
@ -30,6 +30,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
groupDomId() {
|
||||
|
@ -56,10 +61,12 @@ export default {
|
|||
methods: {
|
||||
onClickRowGroup(e) {
|
||||
const NO_EXPAND_CLS = 'no-expand';
|
||||
if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
|
||||
e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
|
||||
const targetClasses = e.target.classList;
|
||||
const parentElClasses = e.target.parentElement.classList;
|
||||
|
||||
if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) {
|
||||
if (this.hasChildren) {
|
||||
eventHub.$emit('toggleChildren', this.group);
|
||||
eventHub.$emit(`${this.action}toggleChildren`, this.group);
|
||||
} else {
|
||||
visitUrl(this.group.relativePath);
|
||||
}
|
||||
|
@ -158,6 +165,7 @@ export default {
|
|||
v-if="group.isOpen && hasChildren"
|
||||
:parent-group="group"
|
||||
:groups="group.children"
|
||||
:action="action"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -1,39 +1,44 @@
|
|||
<script>
|
||||
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
tablePagination,
|
||||
export default {
|
||||
components: {
|
||||
tablePagination,
|
||||
},
|
||||
props: {
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
searchEmpty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchEmptyMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
change(page) {
|
||||
const filterGroupsParam = getParameterByName('filter_groups');
|
||||
const sortParam = getParameterByName('sort');
|
||||
const archivedParam = getParameterByName('archived');
|
||||
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
|
||||
},
|
||||
searchEmpty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
searchEmptyMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
change(page) {
|
||||
const filterGroupsParam = getParameterByName('filter_groups');
|
||||
const sortParam = getParameterByName('sort');
|
||||
const archivedParam = getParameterByName('archived');
|
||||
eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -47,6 +52,7 @@
|
|||
<group-folder
|
||||
v-if="!searchEmpty"
|
||||
:groups="groups"
|
||||
:action="action"
|
||||
/>
|
||||
<table-pagination
|
||||
v-if="!searchEmpty"
|
||||
|
|
|
@ -21,6 +21,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
leaveBtnTitle() {
|
||||
|
@ -32,7 +37,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onLeaveGroup() {
|
||||
eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup);
|
||||
eventHub.$emit(`${this.action}showLeaveGroupModal`, this.group, this.parentGroup);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,13 +2,23 @@ import { __, s__ } from '../locale';
|
|||
|
||||
export const MAX_CHILDREN_COUNT = 20;
|
||||
|
||||
export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects';
|
||||
export const ACTIVE_TAB_SHARED = 'shared';
|
||||
export const ACTIVE_TAB_ARCHIVED = 'archived';
|
||||
|
||||
export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder';
|
||||
export const GROUPS_FILTER_FORM_CLASS = '.js-group-filter-form';
|
||||
export const CONTENT_LIST_CLASS = '.content-list';
|
||||
|
||||
export const COMMON_STR = {
|
||||
FAILURE: __('An error occurred. Please try again.'),
|
||||
LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'),
|
||||
LEAVE_FORBIDDEN: s__(
|
||||
'GroupsTree|Failed to leave the group. Please make sure you are not the only owner.',
|
||||
),
|
||||
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
|
||||
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
|
||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
|
||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
|
||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
|
||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
|
||||
};
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
|
@ -17,8 +27,12 @@ export const ITEM_TYPE = {
|
|||
};
|
||||
|
||||
export const GROUP_VISIBILITY_TYPE = {
|
||||
public: __('Public - The group and any public projects can be viewed without any authentication.'),
|
||||
internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'),
|
||||
public: __(
|
||||
'Public - The group and any public projects can be viewed without any authentication.',
|
||||
),
|
||||
internal: __(
|
||||
'Internal - The group and any internal projects can be viewed by any logged in user.',
|
||||
),
|
||||
private: __('Private - The group and its projects can only be viewed by members.'),
|
||||
};
|
||||
|
||||
|
|
|
@ -4,13 +4,23 @@ import eventHub from './event_hub';
|
|||
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
|
||||
|
||||
export default class GroupFilterableList extends FilterableList {
|
||||
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
|
||||
constructor({
|
||||
form,
|
||||
filter,
|
||||
holder,
|
||||
filterEndpoint,
|
||||
pagePath,
|
||||
dropdownSel,
|
||||
filterInputField,
|
||||
action,
|
||||
}) {
|
||||
super(form, filter, holder, filterInputField);
|
||||
this.form = form;
|
||||
this.filterEndpoint = filterEndpoint;
|
||||
this.pagePath = pagePath;
|
||||
this.filterInputField = filterInputField;
|
||||
this.$dropdown = $(dropdownSel);
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
getFilterEndpoint() {
|
||||
|
@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList {
|
|||
getPagePath(queryData) {
|
||||
const params = queryData ? $.param(queryData) : '';
|
||||
const queryString = params ? `?${params}` : '';
|
||||
return `${this.pagePath}${queryString}`;
|
||||
const path = this.pagePath || window.location.pathname;
|
||||
return `${path}${queryString}`;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
super.bindEvents();
|
||||
|
||||
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
|
||||
this.onFilterOptionClickWrapper = this.onOptionClick.bind(this);
|
||||
|
||||
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
|
||||
this.$dropdown.on('click', 'a', this.onFilterOptionClickWrapper);
|
||||
}
|
||||
|
||||
onFilterInput() {
|
||||
|
@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList {
|
|||
}
|
||||
|
||||
setDefaultFilterOption() {
|
||||
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
|
||||
const defaultOption = $.trim(
|
||||
this.$dropdown
|
||||
.find('.dropdown-menu li.js-filter-sort-order a')
|
||||
.first()
|
||||
.text(),
|
||||
);
|
||||
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
||||
}
|
||||
|
||||
|
@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList {
|
|||
// Get type of option selected from dropdown
|
||||
const currentTargetClassList = e.currentTarget.parentElement.classList;
|
||||
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
|
||||
const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects');
|
||||
const isOptionFilterByArchivedProjects = currentTargetClassList.contains(
|
||||
'js-filter-archived-projects',
|
||||
);
|
||||
|
||||
// Get option query param, also preserve currently applied query param
|
||||
const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
|
||||
const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
|
||||
const sortParam = getParameterByName(
|
||||
'sort',
|
||||
isOptionFilterBySort ? e.currentTarget.href : window.location.href,
|
||||
);
|
||||
const archivedParam = getParameterByName(
|
||||
'archived',
|
||||
isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
|
||||
);
|
||||
|
||||
if (sortParam) {
|
||||
queryData.sort = sortParam;
|
||||
|
@ -86,7 +110,9 @@ export default class GroupFilterableList extends FilterableList {
|
|||
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
|
||||
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
|
||||
} else if (isOptionFilterByArchivedProjects) {
|
||||
this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active');
|
||||
this.$dropdown
|
||||
.find('.dropdown-menu li.js-filter-archived-projects a')
|
||||
.removeClass('is-active');
|
||||
}
|
||||
|
||||
$(e.target).addClass('is-active');
|
||||
|
@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList {
|
|||
onFilterSuccess(res, queryData) {
|
||||
const currentPath = this.getPagePath(queryData);
|
||||
|
||||
window.history.replaceState({
|
||||
page: currentPath,
|
||||
}, document.title, currentPath);
|
||||
window.history.replaceState(
|
||||
{
|
||||
page: currentPath,
|
||||
},
|
||||
document.title,
|
||||
currentPath,
|
||||
);
|
||||
|
||||
eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
|
||||
eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
|
||||
eventHub.$emit(
|
||||
`${this.action}updateGroups`,
|
||||
res.data,
|
||||
Object.prototype.hasOwnProperty.call(queryData, this.filterInputField),
|
||||
);
|
||||
eventHub.$emit(`${this.action}updatePagination`, normalizeHeaders(res.headers));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,26 @@ import GroupsService from './service/groups_service';
|
|||
import groupsApp from './components/app.vue';
|
||||
import groupFolderComponent from './components/group_folder.vue';
|
||||
import groupItemComponent from './components/group_item.vue';
|
||||
import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-groups-tree');
|
||||
export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||
const containerEl = document.getElementById(containerId);
|
||||
let dataEl;
|
||||
|
||||
// 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) {
|
||||
if (!containerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = action ? containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS) : containerEl;
|
||||
|
||||
if (action) {
|
||||
dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||
}
|
||||
|
||||
Vue.component('group-folder', groupFolderComponent);
|
||||
Vue.component('group-item', groupItemComponent);
|
||||
|
||||
|
@ -29,20 +37,26 @@ export default () => {
|
|||
groupsApp,
|
||||
},
|
||||
data() {
|
||||
const { dataset } = this.$options.el;
|
||||
const { dataset } = dataEl || this.$options.el;
|
||||
const hideProjects = dataset.hideProjects === 'true';
|
||||
const service = new GroupsService(endpoint || dataset.endpoint);
|
||||
const store = new GroupsStore(hideProjects);
|
||||
const service = new GroupsService(dataset.endpoint);
|
||||
|
||||
return {
|
||||
action,
|
||||
store,
|
||||
service,
|
||||
hideProjects,
|
||||
loading: true,
|
||||
containerId,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
const { dataset } = this.$options.el;
|
||||
if (this.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dataset } = dataEl || this.$options.el;
|
||||
let groupFilterList = null;
|
||||
const form = document.querySelector(dataset.formSel);
|
||||
const filter = document.querySelector(dataset.filterSel);
|
||||
|
@ -52,10 +66,11 @@ export default () => {
|
|||
form,
|
||||
filter,
|
||||
holder,
|
||||
filterEndpoint: dataset.endpoint,
|
||||
filterEndpoint: endpoint || dataset.endpoint,
|
||||
pagePath: dataset.path,
|
||||
dropdownSel: dataset.dropdownSel,
|
||||
filterInputField: 'filter',
|
||||
action: this.action,
|
||||
};
|
||||
|
||||
groupFilterList = new GroupFilterableList(opts);
|
||||
|
@ -64,9 +79,11 @@ export default () => {
|
|||
render(createElement) {
|
||||
return createElement('groups-app', {
|
||||
props: {
|
||||
action: this.action,
|
||||
store: this.store,
|
||||
service: this.service,
|
||||
hideProjects: this.hideProjects,
|
||||
containerId: this.containerId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -47,9 +47,9 @@ export function removeParamQueryString(url, param) {
|
|||
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
|
||||
}
|
||||
|
||||
export function removeParams(params) {
|
||||
export function removeParams(params, source = window.location.href) {
|
||||
const url = document.createElement('a');
|
||||
url.href = window.location.href;
|
||||
url.href = source;
|
||||
|
||||
params.forEach(param => {
|
||||
url.search = removeParamQueryString(url.search, param);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import initGroupsList from '~/groups';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initGroupsList);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initGroupsList();
|
||||
});
|
||||
|
|
136
app/assets/javascripts/pages/groups/show/group_tabs.js
Normal file
136
app/assets/javascripts/pages/groups/show/group_tabs.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import $ from 'jquery';
|
||||
import { removeParams } from '~/lib/utils/url_utility';
|
||||
import createGroupTree from '~/groups';
|
||||
import {
|
||||
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
ACTIVE_TAB_SHARED,
|
||||
ACTIVE_TAB_ARCHIVED,
|
||||
CONTENT_LIST_CLASS,
|
||||
GROUPS_LIST_HOLDER_CLASS,
|
||||
GROUPS_FILTER_FORM_CLASS,
|
||||
} from '~/groups/constants';
|
||||
import UserTabs from '~/pages/users/user_tabs';
|
||||
import GroupFilterableList from '~/groups/groups_filterable_list';
|
||||
|
||||
export default class GroupTabs extends UserTabs {
|
||||
constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) {
|
||||
super({ defaultAction, action, parentEl });
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$parentEl
|
||||
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
|
||||
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
|
||||
}
|
||||
|
||||
tabShown(event) {
|
||||
const $target = $(event.target);
|
||||
const action = $target.data('action') || $target.data('targetSection');
|
||||
const source = $target.attr('href') || $target.data('targetPath');
|
||||
|
||||
document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source;
|
||||
|
||||
this.setTab(action);
|
||||
return this.setCurrentAction(source);
|
||||
}
|
||||
|
||||
setTab(action) {
|
||||
const loadableActions = [
|
||||
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
ACTIVE_TAB_SHARED,
|
||||
ACTIVE_TAB_ARCHIVED,
|
||||
];
|
||||
this.enableSearchBar(action);
|
||||
this.action = action;
|
||||
|
||||
if (this.loaded[action]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadableActions.includes(action)) {
|
||||
this.cleanFilterState();
|
||||
this.loadTab(action);
|
||||
}
|
||||
}
|
||||
|
||||
loadTab(action) {
|
||||
const elId = `js-groups-${action}-tree`;
|
||||
const endpoint = this.getEndpoint(action);
|
||||
|
||||
this.toggleLoading(true);
|
||||
|
||||
createGroupTree(elId, endpoint, action);
|
||||
this.loaded[action] = true;
|
||||
|
||||
this.toggleLoading(false);
|
||||
}
|
||||
|
||||
getEndpoint(action) {
|
||||
const { endpointsDefault, endpointsShared } = this.$parentEl.data();
|
||||
let endpoint;
|
||||
|
||||
switch (action) {
|
||||
case ACTIVE_TAB_ARCHIVED:
|
||||
endpoint = `${endpointsDefault}?archived=only`;
|
||||
break;
|
||||
case ACTIVE_TAB_SHARED:
|
||||
endpoint = endpointsShared;
|
||||
break;
|
||||
default:
|
||||
// ACTIVE_TAB_SUBGROUPS_AND_PROJECTS
|
||||
endpoint = endpointsDefault;
|
||||
break;
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
enableSearchBar(action) {
|
||||
const containerEl = document.getElementById(action);
|
||||
const form = document.querySelector(GROUPS_FILTER_FORM_CLASS);
|
||||
const filter = form.querySelector('.js-groups-list-filter');
|
||||
const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS);
|
||||
const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||
const endpoint = this.getEndpoint(action);
|
||||
|
||||
if (!dataEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dataset } = dataEl;
|
||||
const opts = {
|
||||
form,
|
||||
filter,
|
||||
holder,
|
||||
filterEndpoint: endpoint || dataset.endpoint,
|
||||
pagePath: null,
|
||||
dropdownSel: '.js-group-filter-dropdown-wrap',
|
||||
filterInputField: 'filter',
|
||||
action,
|
||||
};
|
||||
|
||||
if (!this.loaded[action]) {
|
||||
const filterableList = new GroupFilterableList(opts);
|
||||
filterableList.initSearch();
|
||||
}
|
||||
}
|
||||
|
||||
cleanFilterState() {
|
||||
const values = Object.values(this.loaded);
|
||||
const loadedTabs = values.filter(e => e === true);
|
||||
|
||||
if (!loadedTabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = removeParams(['page'], window.location.search);
|
||||
|
||||
window.history.replaceState(
|
||||
{
|
||||
url: newState,
|
||||
},
|
||||
document.title,
|
||||
newState,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,22 @@
|
|||
/* eslint-disable no-new */
|
||||
|
||||
import { getPagePath } from '~/lib/utils/common_utils';
|
||||
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
|
||||
import NewGroupChild from '~/groups/new_group_child';
|
||||
import notificationsDropdown from '~/notifications_dropdown';
|
||||
import NotificationsForm from '~/notifications_form';
|
||||
import ProjectsList from '~/projects_list';
|
||||
import ShortcutsNavigation from '~/shortcuts_navigation';
|
||||
import initGroupsList from '~/groups';
|
||||
import GroupTabs from './group_tabs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
|
||||
const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
|
||||
const paths = window.location.pathname.split('/');
|
||||
const subpath = paths[paths.length - 1];
|
||||
const action = loadableActions.includes(subpath) ? subpath : getPagePath(1);
|
||||
|
||||
new GroupTabs({ parentEl: '.groups-listing', action });
|
||||
new ShortcutsNavigation();
|
||||
new NotificationsForm();
|
||||
notificationsDropdown();
|
||||
|
@ -17,6 +25,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (newGroupChildWrapper) {
|
||||
new NewGroupChild(newGroupChildWrapper);
|
||||
}
|
||||
|
||||
initGroupsList();
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
}
|
||||
|
||||
.dashboard .side .card .card-header .input-group {
|
||||
|
||||
.form-control {
|
||||
height: 42px;
|
||||
}
|
||||
|
@ -30,14 +29,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.group-nav-container .group-search,
|
||||
.group-nav-container .nav-controls {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: $gl-padding-top 0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: $gl-padding-top 0 0;
|
||||
|
||||
.group-filter-form {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
margin-right: $gl-padding-8;
|
||||
}
|
||||
|
||||
.dropdown-menu-right {
|
||||
|
@ -136,6 +136,10 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
width: 100%;
|
||||
max-width: inherit;
|
||||
|
@ -145,38 +149,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.groups-empty-state {
|
||||
padding: 50px 100px;
|
||||
overflow: hidden;
|
||||
.group-nav-container .group-search {
|
||||
padding: $gl-padding 0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
float: right;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
float: none;
|
||||
display: block;
|
||||
width: 250px;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
margin-left: -125px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-content {
|
||||
float: left;
|
||||
width: 460px;
|
||||
margin-top: 120px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
float: none;
|
||||
margin-top: 60px;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.groups-listing {
|
||||
.group-list-tree .group-row:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,7 +258,7 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -346,7 +326,7 @@
|
|||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 0;
|
||||
|
|
|
@ -17,7 +17,7 @@ class GroupsController < Groups::ApplicationController
|
|||
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
|
||||
before_action :event_filter, only: [:activity]
|
||||
|
||||
before_action :user_actions, only: [:show, :subgroups]
|
||||
before_action :user_actions, only: [:show]
|
||||
|
||||
skip_cross_project_access_check :index, :new, :create, :edit, :update,
|
||||
:destroy, :projects
|
||||
|
@ -53,11 +53,7 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@has_children = GroupDescendantsFinder.new(current_user: current_user,
|
||||
parent_group: @group,
|
||||
params: params).has_children?
|
||||
end
|
||||
format.html
|
||||
|
||||
format.atom do
|
||||
load_events
|
||||
|
|
|
@ -134,7 +134,7 @@ class GroupDescendantsFinder
|
|||
end
|
||||
|
||||
def direct_child_projects
|
||||
GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
|
||||
GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params, options: { only_owned: true })
|
||||
.execute
|
||||
end
|
||||
|
||||
|
|
8
app/views/groups/_archived_projects.html.haml
Normal file
8
app/views/groups/_archived_projects.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
#js-groups-archived-tree
|
||||
.empty-state.text-center.hidden
|
||||
%p= _("There are no archived projects yet")
|
||||
|
||||
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
|
||||
.js-groups-list-holder
|
||||
.loading-container.text-center
|
||||
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
|
|
@ -1,4 +0,0 @@
|
|||
.js-groups-list-holder
|
||||
#js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
|
||||
.loading-container.text-center
|
||||
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
|
8
app/views/groups/_shared_projects.html.haml
Normal file
8
app/views/groups/_shared_projects.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
#js-groups-shared-tree
|
||||
.empty-state.text-center.hidden
|
||||
%p= _("There are no projects shared with this group yet")
|
||||
|
||||
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
|
||||
.js-groups-list-holder
|
||||
.loading-container.text-center
|
||||
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
|
8
app/views/groups/_subgroups_and_projects.html.haml
Normal file
8
app/views/groups/_subgroups_and_projects.html.haml
Normal file
|
@ -0,0 +1,8 @@
|
|||
#js-groups-subgroups_and_projects-tree
|
||||
.empty-state.hidden
|
||||
= render "shared/groups/empty_state"
|
||||
|
||||
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
|
||||
.js-groups-list-holder
|
||||
.loading-container.text-center
|
||||
= icon('spinner spin 2x', class: 'loading-animation prepend-top-20')
|
|
@ -7,11 +7,10 @@
|
|||
|
||||
= render 'groups/home_panel'
|
||||
|
||||
.groups-header{ class: container_class }
|
||||
.group-nav-container
|
||||
.nav-controls.clearfix
|
||||
.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
|
||||
.top-area.group-nav-container
|
||||
.group-search
|
||||
= render "shared/groups/search_form"
|
||||
= render "shared/groups/dropdown", show_archive_options: true
|
||||
- if can? current_user, :create_projects, @group
|
||||
- new_project_label = _("New project")
|
||||
- new_subgroup_label = _("New subgroup")
|
||||
|
@ -39,7 +38,29 @@
|
|||
- else
|
||||
= link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
|
||||
|
||||
- if params[:filter].blank? && !@has_children
|
||||
= render "shared/groups/empty_state"
|
||||
- else
|
||||
= render "children", children: @children, group: @group
|
||||
.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
.fade-left= icon('angle-left')
|
||||
.fade-right= icon('angle-right')
|
||||
%ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
|
||||
%li.js-subgroups_and_projects-tab
|
||||
= link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
|
||||
= _("Subgroups and projects")
|
||||
%li.js-shared-tab
|
||||
= link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
|
||||
= _("Shared projects")
|
||||
%li.js-archived-tab
|
||||
= link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
|
||||
= _("Archived projects")
|
||||
|
||||
.nav-controls
|
||||
= render "shared/groups/dropdown"
|
||||
|
||||
.tab-content
|
||||
#subgroups_and_projects.tab-pane
|
||||
= render "subgroups_and_projects", group: @group
|
||||
|
||||
#shared.tab-pane
|
||||
= render "shared_projects", group: @group
|
||||
|
||||
#archived.tab-pane
|
||||
= render "archived_projects", group: @group
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
.col-sm-12
|
||||
= form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do
|
||||
.form-group
|
||||
= label_tag :link_group_id, "Select a group to share with", class: "label-bold"
|
||||
= label_tag :link_group_id, _("Select a group to invite"), class: "label-bold"
|
||||
= groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp", required: true)
|
||||
.form-group
|
||||
= label_tag :link_group_access, "Max access level", class: "label-bold"
|
||||
= label_tag :link_group_access, _("Max access level"), class: "label-bold"
|
||||
.select-wrapper
|
||||
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
|
||||
= icon('chevron-down')
|
||||
.form-text.text-muted.append-bottom-10
|
||||
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
|
||||
= link_to _("Read more"), help_page_path("user/permissions"), class: "vlink"
|
||||
about role permissions
|
||||
.form-group
|
||||
= label_tag :expires_at, 'Access expiration date', class: 'label-bold'
|
||||
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
|
||||
.clearable-input
|
||||
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Expiration date', id: 'expires_at_groups'
|
||||
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: _('Expiration date'), id: 'expires_at_groups'
|
||||
%i.clear-icon.js-clear-input
|
||||
= submit_tag "Share", class: "btn btn-create"
|
||||
= submit_tag _("Invite"), class: "btn btn-create"
|
|
@ -6,9 +6,9 @@
|
|||
Project members
|
||||
- if can?(current_user, :admin_project_member, @project)
|
||||
%p
|
||||
You can add a new member to
|
||||
You can invite a new member to
|
||||
%strong= @project.name
|
||||
or share it with another group.
|
||||
or invite another group.
|
||||
- else
|
||||
%p
|
||||
Members can be added by project
|
||||
|
@ -19,16 +19,16 @@
|
|||
- if can?(current_user, :admin_project_member, @project)
|
||||
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
||||
%li.nav-tab{ role: 'presentation' }
|
||||
%a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
|
||||
%a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
|
||||
- if @project.allowed_to_share_with_group?
|
||||
%li.nav-tab{ role: 'presentation' }
|
||||
%a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
|
||||
%a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
|
||||
|
||||
.tab-content.gitlab-tab-content
|
||||
.tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
|
||||
= render 'projects/project_members/new_project_member', tab_title: 'Add member'
|
||||
.tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' }
|
||||
= render 'projects/project_members/new_shared_group', tab_title: 'Share with group'
|
||||
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
|
||||
= render 'projects/project_members/new_project_member', tab_title: 'Invite member'
|
||||
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
|
||||
= render 'projects/project_members/new_project_group', tab_title: 'Invite group'
|
||||
|
||||
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
|
||||
.clearfix
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
.groups-empty-state.qa-groups-empty-state
|
||||
= custom_icon("icon_empty_groups")
|
||||
.group-empty-state.row.align-items-center.justify-content-center.qa-groups-empty-state
|
||||
.icon.text-center.order-md-2
|
||||
= custom_icon("icon_empty_groups")
|
||||
|
||||
.text-content
|
||||
.text-content.m-0.order-md-1
|
||||
%h4= s_("GroupsEmptyState|A group is a collection of several projects.")
|
||||
%p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
|
||||
%p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
= form_tag request.path, method: :get, class: 'group-filter-form append-right-10', id: 'group-filter-form' do |f|
|
||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Filter by name...'), class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
|
||||
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
|
||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Overhaul listing of projects in the group overview page
|
||||
merge_request: 20262
|
||||
author:
|
||||
type: added
|
|
@ -14,6 +14,9 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
get :projects, as: :projects_group
|
||||
get :activity, as: :activity_group
|
||||
put :transfer, as: :transfer_group
|
||||
# TODO: Remove as part of refactor in https://gitlab.com/gitlab-org/gitlab-ce/issues/49693
|
||||
get 'shared', action: :show, as: :group_shared
|
||||
get 'archived', action: :show, as: :group_archived
|
||||
end
|
||||
|
||||
get '/', action: :show, as: :group_canonical
|
||||
|
|
|
@ -295,6 +295,9 @@ msgstr ""
|
|||
msgid "Access denied! Please verify you can add deploy keys to this repository."
|
||||
msgstr ""
|
||||
|
||||
msgid "Access expiration date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -604,6 +607,9 @@ msgstr ""
|
|||
msgid "Archived project! Repository and other project resources are read-only"
|
||||
msgstr ""
|
||||
|
||||
msgid "Archived projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete this pipeline schedule?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2622,6 +2628,9 @@ msgstr ""
|
|||
msgid "Expand sidebar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2985,6 +2994,9 @@ msgstr ""
|
|||
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Are you sure you want to leave the \"%{fullName}\" group?"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Create a project in this group."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2997,19 +3009,19 @@ msgstr ""
|
|||
msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Filter by name..."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Leave this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Loading groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Sorry, no groups matched your search"
|
||||
msgid "GroupsTree|No groups matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Sorry, no groups or projects matched your search"
|
||||
msgid "GroupsTree|No groups or projects matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Search by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Health Check"
|
||||
|
@ -3245,6 +3257,9 @@ msgstr ""
|
|||
msgid "Introducing Cycle Analytics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invite"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issue Boards"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3582,6 +3597,9 @@ msgstr ""
|
|||
msgid "Markdown enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Max access level"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum git storage failures"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5072,6 +5090,9 @@ msgstr ""
|
|||
msgid "Select Archive Format"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a group to invite"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a namespace to fork the project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5171,6 +5192,9 @@ msgstr ""
|
|||
msgid "Shared Runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shared projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sherlock Transactions"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5449,6 +5473,9 @@ msgstr ""
|
|||
msgid "Subgroups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subgroups and projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Submit as spam"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5691,6 +5718,9 @@ msgstr ""
|
|||
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no archived projects yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no issues to show"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5700,6 +5730,9 @@ msgstr ""
|
|||
msgid "There are no merge requests to show"
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no projects shared with this group yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "There are problems accessing Git storage: "
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ module QA
|
|||
def self.included(base)
|
||||
base.view 'app/views/shared/groups/_search_form.html.haml' do
|
||||
element :groups_filter, 'search_field_tag :filter'
|
||||
element :groups_filter_placeholder, 'Filter by name...'
|
||||
element :groups_filter_placeholder, 'Search by name'
|
||||
end
|
||||
|
||||
base.view 'app/views/shared/groups/_empty_state.html.haml' do
|
||||
|
@ -27,7 +27,7 @@ module QA
|
|||
page.has_css?(element_selector_css(:groups_list_tree_container))
|
||||
end
|
||||
|
||||
fill_in 'Filter by name...', with: name
|
||||
fill_in 'Search by name', with: name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,11 @@ module QA
|
|||
class Groups < Page::Base
|
||||
include Page::Component::GroupsFilter
|
||||
|
||||
view 'app/views/shared/groups/_search_form.html.haml' do
|
||||
element :groups_filter, 'search_field_tag :filter'
|
||||
element :groups_filter_placeholder, 'Search by name'
|
||||
end
|
||||
|
||||
view 'app/views/dashboard/_groups_head.html.haml' do
|
||||
element :new_group_button, 'link_to _("New group")'
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module QA
|
|||
end
|
||||
|
||||
view 'app/assets/javascripts/groups/constants.js' do
|
||||
element :no_result_text, 'Sorry, no groups or projects matched your search'
|
||||
element :no_result_text, 'No groups or projects matched your search'
|
||||
end
|
||||
|
||||
def go_to_subgroup(name)
|
||||
|
@ -30,7 +30,7 @@ module QA
|
|||
def has_subgroup?(name)
|
||||
filter_by_name(name)
|
||||
|
||||
page.has_text?(/#{name}|Sorry, no groups or projects matched your search/, wait: 60)
|
||||
page.has_text?(/#{name}|No groups or projects matched your search/, wait: 60)
|
||||
|
||||
page.has_text?(name, wait: 0)
|
||||
end
|
||||
|
|
|
@ -38,14 +38,6 @@ describe GroupsController do
|
|||
project
|
||||
end
|
||||
|
||||
context 'as html' do
|
||||
it 'assigns whether or not a group has children' do
|
||||
get :show, id: group.to_param
|
||||
|
||||
expect(assigns(:has_children)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'as atom' do
|
||||
it 'assigns events for all the projects in the group' do
|
||||
create(:event, project: project)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Project > Members > Share with Group', :js do
|
||||
describe 'Project > Members > Invite group', :js do
|
||||
include Select2Helper
|
||||
include ActionView::Helpers::DateHelper
|
||||
|
||||
|
@ -8,17 +8,17 @@ describe 'Project > Members > Share with Group', :js do
|
|||
|
||||
describe 'Share with group lock' do
|
||||
shared_examples 'the project can be shared with groups' do
|
||||
it 'the "Share with group" tab exists' do
|
||||
it 'the "Invite group" tab exists' do
|
||||
visit project_settings_members_path(project)
|
||||
expect(page).to have_selector('#share-with-group-tab')
|
||||
expect(page).to have_selector('#invite-group-tab')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'the project cannot be shared with groups' do
|
||||
it 'the "Share with group" tab does not exist' do
|
||||
it 'the "Invite group" tab does not exist' do
|
||||
visit project_settings_members_path(project)
|
||||
expect(page).to have_selector('#add-member-tab')
|
||||
expect(page).not_to have_selector('#share-with-group-tab')
|
||||
expect(page).to have_selector('#invite-member-tab')
|
||||
expect(page).not_to have_selector('#invite-group-tab')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -31,13 +31,13 @@ describe 'Project > Members > Share with Group', :js do
|
|||
sign_in(maintainer)
|
||||
end
|
||||
|
||||
context 'when the group has "Share with group lock" disabled' do
|
||||
context 'when the group has "Invite group lock" disabled' do
|
||||
it_behaves_like 'the project can be shared with groups'
|
||||
|
||||
it 'the project can be shared with another group' do
|
||||
visit project_settings_members_path(project)
|
||||
|
||||
click_on 'share-with-group-tab'
|
||||
click_on 'invite-group-tab'
|
||||
|
||||
select2 group_to_share_with.id, from: '#link_group_id'
|
||||
page.find('body').click
|
||||
|
@ -49,7 +49,7 @@ describe 'Project > Members > Share with Group', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the group has "Share with group lock" enabled' do
|
||||
context 'when the group has "Invite group lock" enabled' do
|
||||
before do
|
||||
project.namespace.update_column(:share_with_group_lock, true)
|
||||
end
|
||||
|
@ -69,12 +69,12 @@ describe 'Project > Members > Share with Group', :js do
|
|||
sign_in(maintainer)
|
||||
end
|
||||
|
||||
context 'when the root_group has "Share with group lock" disabled' do
|
||||
context 'when the subgroup has "Share with group lock" disabled' do
|
||||
context 'when the root_group has "Invite group lock" disabled' do
|
||||
context 'when the subgroup has "Invite group lock" disabled' do
|
||||
it_behaves_like 'the project can be shared with groups'
|
||||
end
|
||||
|
||||
context 'when the subgroup has "Share with group lock" enabled' do
|
||||
context 'when the subgroup has "Invite group lock" enabled' do
|
||||
before do
|
||||
subgroup.update_column(:share_with_group_lock, true)
|
||||
end
|
||||
|
@ -83,16 +83,16 @@ describe 'Project > Members > Share with Group', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the root_group has "Share with group lock" enabled' do
|
||||
context 'when the root_group has "Invite group lock" enabled' do
|
||||
before do
|
||||
root_group.update_column(:share_with_group_lock, true)
|
||||
end
|
||||
|
||||
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
|
||||
context 'when the subgroup has "Invite group lock" disabled (parent overridden)' do
|
||||
it_behaves_like 'the project can be shared with groups'
|
||||
end
|
||||
|
||||
context 'when the subgroup has "Share with group lock" enabled' do
|
||||
context 'when the subgroup has "Invite group lock" enabled' do
|
||||
before do
|
||||
subgroup.update_column(:share_with_group_lock, true)
|
||||
end
|
||||
|
@ -117,12 +117,12 @@ describe 'Project > Members > Share with Group', :js do
|
|||
|
||||
visit project_settings_members_path(project)
|
||||
|
||||
click_on 'share-with-group-tab'
|
||||
click_on 'invite-group-tab'
|
||||
|
||||
select2 group.id, from: '#link_group_id'
|
||||
|
||||
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
|
||||
click_on 'share-with-group-tab'
|
||||
click_on 'invite-group-tab'
|
||||
find('.btn-create').click
|
||||
end
|
||||
|
||||
|
@ -150,7 +150,7 @@ describe 'Project > Members > Share with Group', :js do
|
|||
|
||||
visit project_settings_members_path(project)
|
||||
|
||||
click_link 'Share with group'
|
||||
click_link 'Invite group'
|
||||
|
||||
find('.ajax-groups-select.select2-container')
|
||||
|
||||
|
@ -183,7 +183,7 @@ describe 'Project > Members > Share with Group', :js do
|
|||
it 'the groups dropdown does not show ancestors', :nested_groups do
|
||||
visit project_settings_members_path(project)
|
||||
|
||||
click_on 'share-with-group-tab'
|
||||
click_on 'invite-group-tab'
|
||||
click_link 'Search for a group'
|
||||
|
||||
page.within '.select2-drop' do
|
|
@ -26,13 +26,13 @@ describe 'Projects > Settings > User manages group links' do
|
|||
end
|
||||
end
|
||||
|
||||
it 'shares a project with a group', :js do
|
||||
click_link('Share with group')
|
||||
it 'invites a group to a project', :js do
|
||||
click_link('Invite group')
|
||||
|
||||
select2(group_market.id, from: '#link_group_id')
|
||||
select('Maintainer', from: 'link_group_access')
|
||||
|
||||
click_button('Share')
|
||||
click_button('Invite')
|
||||
|
||||
page.within('.project-members-groups') do
|
||||
expect(page).to have_content('Market')
|
||||
|
|
|
@ -108,6 +108,15 @@ describe GroupDescendantsFinder do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not include projects shared with the group' do
|
||||
project = create(:project, namespace: group)
|
||||
other_project = create(:project)
|
||||
other_project.project_group_links.create(group: group,
|
||||
group_access: ProjectGroupLink::MASTER)
|
||||
|
||||
expect(finder.execute).to contain_exactly(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nested groups', :nested_groups do
|
||||
|
|
|
@ -9,9 +9,14 @@ import GroupsStore from '~/groups/store/groups_store';
|
|||
import GroupsService from '~/groups/service/groups_service';
|
||||
|
||||
import {
|
||||
mockEndpoint, mockGroups, mockSearchedGroups,
|
||||
mockRawPageInfo, mockParentGroupItem, mockRawChildren,
|
||||
mockChildren, mockPageInfo,
|
||||
mockEndpoint,
|
||||
mockGroups,
|
||||
mockSearchedGroups,
|
||||
mockRawPageInfo,
|
||||
mockParentGroupItem,
|
||||
mockRawChildren,
|
||||
mockChildren,
|
||||
mockPageInfo,
|
||||
} from '../mock_data';
|
||||
|
||||
const createComponent = (hideProjects = false) => {
|
||||
|
@ -28,22 +33,23 @@ const createComponent = (hideProjects = false) => {
|
|||
});
|
||||
};
|
||||
|
||||
const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
|
||||
if (failed) {
|
||||
reject(data);
|
||||
} else {
|
||||
resolve({
|
||||
json() {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const returnServicePromise = (data, failed) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (failed) {
|
||||
reject(data);
|
||||
} else {
|
||||
resolve({
|
||||
json() {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach((done) => {
|
||||
beforeEach(done => {
|
||||
Vue.component('group-folder', groupFolderComponent);
|
||||
Vue.component('group-item', groupItemComponent);
|
||||
|
||||
|
@ -94,7 +100,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('fetchGroups', () => {
|
||||
it('should call `getGroups` with all the params provided', (done) => {
|
||||
it('should call `getGroups` with all the params provided', done => {
|
||||
spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups));
|
||||
|
||||
vm.fetchGroups({
|
||||
|
@ -110,8 +116,10 @@ describe('AppComponent', () => {
|
|||
}, 0);
|
||||
});
|
||||
|
||||
it('should set headers to store for building pagination info when called with `updatePagination`', (done) => {
|
||||
spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise({ headers: mockRawPageInfo }));
|
||||
it('should set headers to store for building pagination info when called with `updatePagination`', done => {
|
||||
spyOn(vm.service, 'getGroups').and.returnValue(
|
||||
returnServicePromise({ headers: mockRawPageInfo }),
|
||||
);
|
||||
spyOn(vm, 'updatePagination');
|
||||
|
||||
vm.fetchGroups({ updatePagination: true });
|
||||
|
@ -122,7 +130,7 @@ describe('AppComponent', () => {
|
|||
}, 0);
|
||||
});
|
||||
|
||||
it('should show flash error when request fails', (done) => {
|
||||
it('should show flash error when request fails', done => {
|
||||
spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true));
|
||||
spyOn($, 'scrollTo');
|
||||
spyOn(window, 'Flash');
|
||||
|
@ -138,7 +146,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('fetchAllGroups', () => {
|
||||
it('should fetch default set of groups', (done) => {
|
||||
it('should fetch default set of groups', done => {
|
||||
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
|
||||
spyOn(vm, 'updatePagination').and.callThrough();
|
||||
spyOn(vm, 'updateGroups').and.callThrough();
|
||||
|
@ -153,7 +161,7 @@ describe('AppComponent', () => {
|
|||
}, 0);
|
||||
});
|
||||
|
||||
it('should fetch matching set of groups when app is loaded with search query', (done) => {
|
||||
it('should fetch matching set of groups when app is loaded with search query', done => {
|
||||
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups));
|
||||
spyOn(vm, 'updateGroups').and.callThrough();
|
||||
|
||||
|
@ -173,7 +181,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('fetchPage', () => {
|
||||
it('should fetch groups for provided page details and update window state', (done) => {
|
||||
it('should fetch groups for provided page details and update window state', done => {
|
||||
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
|
||||
spyOn(vm, 'updateGroups').and.callThrough();
|
||||
const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough();
|
||||
|
@ -193,9 +201,13 @@ describe('AppComponent', () => {
|
|||
expect(vm.isLoading).toBe(false);
|
||||
expect($.scrollTo).toHaveBeenCalledWith(0);
|
||||
expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
|
||||
expect(window.history.replaceState).toHaveBeenCalledWith({
|
||||
page: jasmine.any(String),
|
||||
}, jasmine.any(String), jasmine.any(String));
|
||||
expect(window.history.replaceState).toHaveBeenCalledWith(
|
||||
{
|
||||
page: jasmine.any(String),
|
||||
},
|
||||
jasmine.any(String),
|
||||
jasmine.any(String),
|
||||
);
|
||||
expect(vm.updateGroups).toHaveBeenCalled();
|
||||
done();
|
||||
}, 0);
|
||||
|
@ -211,7 +223,7 @@ describe('AppComponent', () => {
|
|||
groupItem.isChildrenLoading = false;
|
||||
});
|
||||
|
||||
it('should fetch children of given group and expand it if group is collapsed and children are not loaded', (done) => {
|
||||
it('should fetch children of given group and expand it if group is collapsed and children are not loaded', done => {
|
||||
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren));
|
||||
spyOn(vm.store, 'setGroupChildren');
|
||||
|
||||
|
@ -244,7 +256,7 @@ describe('AppComponent', () => {
|
|||
expect(groupItem.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should set `isChildrenLoading` back to `false` if load request fails', (done) => {
|
||||
it('should set `isChildrenLoading` back to `false` if load request fails', done => {
|
||||
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true));
|
||||
|
||||
vm.toggleChildren(groupItem);
|
||||
|
@ -272,7 +284,9 @@ describe('AppComponent', () => {
|
|||
expect(vm.groupLeaveConfirmationMessage).toBe('');
|
||||
vm.showLeaveGroupModal(group, mockParentGroupItem);
|
||||
expect(vm.showModal).toBe(true);
|
||||
expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`);
|
||||
expect(vm.groupLeaveConfirmationMessage).toBe(
|
||||
`Are you sure you want to leave the "${group.fullName}" group?`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -299,7 +313,7 @@ describe('AppComponent', () => {
|
|||
vm.targetParentGroup = groupItem;
|
||||
});
|
||||
|
||||
it('hides modal confirmation leave group and remove group item from tree', (done) => {
|
||||
it('hides modal confirmation leave group and remove group item from tree', done => {
|
||||
const notice = `You left the "${childGroupItem.fullName}" group.`;
|
||||
spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice }));
|
||||
spyOn(vm.store, 'removeGroup').and.callThrough();
|
||||
|
@ -318,9 +332,11 @@ describe('AppComponent', () => {
|
|||
}, 0);
|
||||
});
|
||||
|
||||
it('should show error flash message if request failed to leave group', (done) => {
|
||||
it('should show error flash message if request failed to leave group', done => {
|
||||
const message = 'An error occurred. Please try again.';
|
||||
spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 500 }, true));
|
||||
spyOn(vm.service, 'leaveGroup').and.returnValue(
|
||||
returnServicePromise({ status: 500 }, true),
|
||||
);
|
||||
spyOn(vm.store, 'removeGroup').and.callThrough();
|
||||
spyOn(window, 'Flash');
|
||||
|
||||
|
@ -335,9 +351,11 @@ describe('AppComponent', () => {
|
|||
}, 0);
|
||||
});
|
||||
|
||||
it('should show appropriate error flash message if request forbids to leave group', (done) => {
|
||||
it('should show appropriate error flash message if request forbids to leave group', done => {
|
||||
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
|
||||
spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 403 }, true));
|
||||
spyOn(vm.service, 'leaveGroup').and.returnValue(
|
||||
returnServicePromise({ status: 403 }, true),
|
||||
);
|
||||
spyOn(vm.store, 'removeGroup').and.callThrough();
|
||||
spyOn(window, 'Flash');
|
||||
|
||||
|
@ -388,7 +406,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('created', () => {
|
||||
it('should bind event listeners on eventHub', (done) => {
|
||||
it('should bind event listeners on eventHub', done => {
|
||||
spyOn(eventHub, '$on');
|
||||
|
||||
const newVm = createComponent();
|
||||
|
@ -405,21 +423,21 @@ describe('AppComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', (done) => {
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', done => {
|
||||
const newVm = createComponent();
|
||||
newVm.$mount();
|
||||
Vue.nextTick(() => {
|
||||
expect(newVm.searchEmptyMessage).toBe('Sorry, no groups or projects matched your search');
|
||||
expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search');
|
||||
newVm.$destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', (done) => {
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', done => {
|
||||
const newVm = createComponent(true);
|
||||
newVm.$mount();
|
||||
Vue.nextTick(() => {
|
||||
expect(newVm.searchEmptyMessage).toBe('Sorry, no groups matched your search');
|
||||
expect(newVm.searchEmptyMessage).toBe('No groups matched your search');
|
||||
newVm.$destroy();
|
||||
done();
|
||||
});
|
||||
|
@ -427,7 +445,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
|
||||
describe('beforeDestroy', () => {
|
||||
it('should unbind event listeners on eventHub', (done) => {
|
||||
it('should unbind event listeners on eventHub', done => {
|
||||
spyOn(eventHub, '$off');
|
||||
|
||||
const newVm = createComponent();
|
||||
|
@ -454,7 +472,7 @@ describe('AppComponent', () => {
|
|||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('should render loading icon', (done) => {
|
||||
it('should render loading icon', done => {
|
||||
vm.isLoading = true;
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
|
||||
|
@ -463,7 +481,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render groups tree', (done) => {
|
||||
it('should render groups tree', done => {
|
||||
vm.store.state.groups = [mockParentGroupItem];
|
||||
vm.isLoading = false;
|
||||
vm.store.state.pageInfo = mockPageInfo;
|
||||
|
@ -473,7 +491,7 @@ describe('AppComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders modal confirmation dialog', (done) => {
|
||||
it('renders modal confirmation dialog', done => {
|
||||
vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
|
||||
vm.showModal = true;
|
||||
Vue.nextTick(() => {
|
||||
|
|
Loading…
Reference in a new issue