Merge branch 'master' into sh-security-fix-backports-master
This commit is contained in:
commit
891a9ce8b0
|
@ -7,7 +7,7 @@ class BoardService {
|
||||||
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
|
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
|
||||||
issues: {
|
issues: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
|
url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
|
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
|
||||||
|
@ -16,7 +16,7 @@ class BoardService {
|
||||||
url: `${listsEndpoint}/generate.json`
|
url: `${listsEndpoint}/generate.json`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
|
this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
|
||||||
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
|
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
|
||||||
bulkUpdate: {
|
bulkUpdate: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
@ -3,7 +3,8 @@ import Visibility from 'visibilityjs';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Poll from './lib/utils/poll';
|
import Poll from './lib/utils/poll';
|
||||||
import { s__ } from './locale';
|
import { s__ } from './locale';
|
||||||
import './flash';
|
import initSettingsPanels from './settings_panels';
|
||||||
|
import Flash from './flash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cluster page has 2 separate parts:
|
* Cluster page has 2 separate parts:
|
||||||
|
@ -24,6 +25,8 @@ class ClusterService {
|
||||||
|
|
||||||
export default class Clusters {
|
export default class Clusters {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
initSettingsPanels();
|
||||||
|
|
||||||
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
|
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
|
@ -73,6 +73,7 @@ import initProjectVisibilitySelector from './project_visibility';
|
||||||
import GpgBadges from './gpg_badges';
|
import GpgBadges from './gpg_badges';
|
||||||
import UserFeatureHelper from './helpers/user_feature_helper';
|
import UserFeatureHelper from './helpers/user_feature_helper';
|
||||||
import initChangesDropdown from './init_changes_dropdown';
|
import initChangesDropdown from './init_changes_dropdown';
|
||||||
|
import NewGroupChild from './groups/new_group_child';
|
||||||
import AbuseReports from './abuse_reports';
|
import AbuseReports from './abuse_reports';
|
||||||
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
|
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
|
||||||
import AjaxLoadingSpinner from './ajax_loading_spinner';
|
import AjaxLoadingSpinner from './ajax_loading_spinner';
|
||||||
|
@ -168,9 +169,6 @@ import memberExpirationDate from './member_expiration_date';
|
||||||
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
|
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
|
||||||
filteredSearchManager.setup();
|
filteredSearchManager.setup();
|
||||||
}
|
}
|
||||||
if (page === 'projects:merge_requests:index') {
|
|
||||||
new UserCallout({ setCalloutPerProject: true });
|
|
||||||
}
|
|
||||||
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
|
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
|
||||||
IssuableIndex.init(pagePrefix);
|
IssuableIndex.init(pagePrefix);
|
||||||
|
|
||||||
|
@ -352,7 +350,10 @@ import memberExpirationDate from './member_expiration_date';
|
||||||
case 'projects:show':
|
case 'projects:show':
|
||||||
shortcut_handler = new ShortcutsNavigation();
|
shortcut_handler = new ShortcutsNavigation();
|
||||||
new NotificationsForm();
|
new NotificationsForm();
|
||||||
new UserCallout({ setCalloutPerProject: true });
|
new UserCallout({
|
||||||
|
setCalloutPerProject: true,
|
||||||
|
className: 'js-autodevops-banner',
|
||||||
|
});
|
||||||
|
|
||||||
if ($('#tree-slider').length) new TreeView();
|
if ($('#tree-slider').length) new TreeView();
|
||||||
if ($('.blob-viewer').length) new BlobViewer();
|
if ($('.blob-viewer').length) new BlobViewer();
|
||||||
|
@ -372,9 +373,6 @@ import memberExpirationDate from './member_expiration_date';
|
||||||
case 'projects:pipelines:new':
|
case 'projects:pipelines:new':
|
||||||
new NewBranchForm($('.js-new-pipeline-form'));
|
new NewBranchForm($('.js-new-pipeline-form'));
|
||||||
break;
|
break;
|
||||||
case 'projects:pipelines:index':
|
|
||||||
new UserCallout({ setCalloutPerProject: true });
|
|
||||||
break;
|
|
||||||
case 'projects:pipelines:builds':
|
case 'projects:pipelines:builds':
|
||||||
case 'projects:pipelines:failures':
|
case 'projects:pipelines:failures':
|
||||||
case 'projects:pipelines:show':
|
case 'projects:pipelines:show':
|
||||||
|
@ -395,10 +393,15 @@ import memberExpirationDate from './member_expiration_date';
|
||||||
new gl.Activities();
|
new gl.Activities();
|
||||||
break;
|
break;
|
||||||
case 'groups:show':
|
case 'groups:show':
|
||||||
|
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
|
||||||
shortcut_handler = new ShortcutsNavigation();
|
shortcut_handler = new ShortcutsNavigation();
|
||||||
new NotificationsForm();
|
new NotificationsForm();
|
||||||
new NotificationsDropdown();
|
new NotificationsDropdown();
|
||||||
new ProjectsList();
|
new ProjectsList();
|
||||||
|
|
||||||
|
if (newGroupChildWrapper) {
|
||||||
|
new NewGroupChild(newGroupChildWrapper);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'groups:group_members:index':
|
case 'groups:group_members:index':
|
||||||
memberExpirationDate();
|
memberExpirationDate();
|
||||||
|
@ -432,7 +435,6 @@ import memberExpirationDate from './member_expiration_date';
|
||||||
new TreeView();
|
new TreeView();
|
||||||
new BlobViewer();
|
new BlobViewer();
|
||||||
new NewCommitForm($('.js-create-dir-form'));
|
new NewCommitForm($('.js-create-dir-form'));
|
||||||
new UserCallout({ setCalloutPerProject: true });
|
|
||||||
$('#tree-slider').waitForImages(function() {
|
$('#tree-slider').waitForImages(function() {
|
||||||
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
|
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,10 +6,11 @@ import _ from 'underscore';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class FilterableList {
|
export default class FilterableList {
|
||||||
constructor(form, filter, holder) {
|
constructor(form, filter, holder, filterInputField = 'filter_groups') {
|
||||||
this.filterForm = form;
|
this.filterForm = form;
|
||||||
this.listFilterElement = filter;
|
this.listFilterElement = filter;
|
||||||
this.listHolderElement = holder;
|
this.listHolderElement = holder;
|
||||||
|
this.filterInputField = filterInputField;
|
||||||
this.isBusy = false;
|
this.isBusy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +33,10 @@ export default class FilterableList {
|
||||||
onFilterInput() {
|
onFilterInput() {
|
||||||
const $form = $(this.filterForm);
|
const $form = $(this.filterForm);
|
||||||
const queryData = {};
|
const queryData = {};
|
||||||
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
|
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
|
||||||
|
|
||||||
if (filterGroupsParam) {
|
if (filterGroupsParam) {
|
||||||
queryData.filter_groups = filterGroupsParam;
|
queryData[this.filterInputField] = filterGroupsParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.filterResults(queryData);
|
this.filterResults(queryData);
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
<script>
|
||||||
|
/* global Flash */
|
||||||
|
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||||
|
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||||
|
import { COMMON_STR } from '../constants';
|
||||||
|
|
||||||
|
import groupsComponent from './groups.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
loadingIcon,
|
||||||
|
groupsComponent,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
store: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hideProjects: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
isSearchEmpty: false,
|
||||||
|
searchEmptyMessage: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
groups() {
|
||||||
|
return this.store.getGroups();
|
||||||
|
},
|
||||||
|
pageInfo() {
|
||||||
|
return this.store.getPaginationInfo();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
|
||||||
|
return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||||
|
.then((res) => {
|
||||||
|
if (updatePagination) {
|
||||||
|
this.updatePagination(res.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.catch(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
$.scrollTo(0);
|
||||||
|
|
||||||
|
Flash(COMMON_STR.FAILURE);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchAllGroups() {
|
||||||
|
const page = getParameterByName('page') || null;
|
||||||
|
const sortBy = getParameterByName('sort') || null;
|
||||||
|
const archived = getParameterByName('archived') || null;
|
||||||
|
const filterGroupsBy = getParameterByName('filter') || null;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
this.fetchGroups({
|
||||||
|
page,
|
||||||
|
filterGroupsBy,
|
||||||
|
sortBy,
|
||||||
|
archived,
|
||||||
|
updatePagination: true,
|
||||||
|
}).then((res) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.updateGroups(res, Boolean(filterGroupsBy));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchPage(page, filterGroupsBy, sortBy, archived) {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
this.fetchGroups({
|
||||||
|
page,
|
||||||
|
filterGroupsBy,
|
||||||
|
sortBy,
|
||||||
|
archived,
|
||||||
|
updatePagination: true,
|
||||||
|
}).then((res) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
$.scrollTo(0);
|
||||||
|
|
||||||
|
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
|
||||||
|
window.history.replaceState({
|
||||||
|
page: currentPath,
|
||||||
|
}, document.title, currentPath);
|
||||||
|
|
||||||
|
this.updateGroups(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleChildren(group) {
|
||||||
|
const parentGroup = group;
|
||||||
|
if (!parentGroup.isOpen) {
|
||||||
|
if (parentGroup.children.length === 0) {
|
||||||
|
parentGroup.isChildrenLoading = true;
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
this.fetchGroups({
|
||||||
|
parentId: parentGroup.id,
|
||||||
|
}).then((res) => {
|
||||||
|
this.store.setGroupChildren(parentGroup, res);
|
||||||
|
}).catch(() => {
|
||||||
|
parentGroup.isChildrenLoading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
parentGroup.isOpen = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parentGroup.isOpen = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leaveGroup(group, parentGroup) {
|
||||||
|
const targetGroup = group;
|
||||||
|
targetGroup.isBeingRemoved = true;
|
||||||
|
this.service.leaveGroup(targetGroup.leavePath)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
$.scrollTo(0);
|
||||||
|
this.store.removeGroup(targetGroup, parentGroup);
|
||||||
|
Flash(res.notice, 'notice');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
let message = COMMON_STR.FAILURE;
|
||||||
|
if (err.status === 403) {
|
||||||
|
message = COMMON_STR.LEAVE_FORBIDDEN;
|
||||||
|
}
|
||||||
|
Flash(message);
|
||||||
|
targetGroup.isBeingRemoved = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updatePagination(headers) {
|
||||||
|
this.store.setPaginationInfo(headers);
|
||||||
|
},
|
||||||
|
updateGroups(groups, fromSearch) {
|
||||||
|
this.isSearchEmpty = groups ? groups.length === 0 : false;
|
||||||
|
if (fromSearch) {
|
||||||
|
this.store.setSearchedGroups(groups);
|
||||||
|
} else {
|
||||||
|
this.store.setGroups(groups);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.searchEmptyMessage = this.hideProjects ?
|
||||||
|
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||||
|
|
||||||
|
eventHub.$on('fetchPage', this.fetchPage);
|
||||||
|
eventHub.$on('toggleChildren', this.toggleChildren);
|
||||||
|
eventHub.$on('leaveGroup', this.leaveGroup);
|
||||||
|
eventHub.$on('updatePagination', this.updatePagination);
|
||||||
|
eventHub.$on('updateGroups', this.updateGroups);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchAllGroups();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
eventHub.$off('fetchPage', this.fetchPage);
|
||||||
|
eventHub.$off('toggleChildren', this.toggleChildren);
|
||||||
|
eventHub.$off('leaveGroup', this.leaveGroup);
|
||||||
|
eventHub.$off('updatePagination', this.updatePagination);
|
||||||
|
eventHub.$off('updateGroups', this.updateGroups);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<loading-icon
|
||||||
|
class="loading-animation prepend-top-20"
|
||||||
|
size="2"
|
||||||
|
v-if="isLoading"
|
||||||
|
:label="s__('GroupsTree|Loading groups')"
|
||||||
|
/>
|
||||||
|
<groups-component
|
||||||
|
v-if="!isLoading"
|
||||||
|
:groups="groups"
|
||||||
|
:search-empty="isSearchEmpty"
|
||||||
|
:search-empty-message="searchEmptyMessage"
|
||||||
|
:page-info="pageInfo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,15 +1,27 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { n__ } from '../../locale';
|
||||||
|
import { MAX_CHILDREN_COUNT } from '../constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
groups: {
|
parentGroup: {
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
baseGroup: {
|
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => ([]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasMoreChildren() {
|
||||||
|
return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
|
||||||
|
},
|
||||||
|
moreChildrenStats() {
|
||||||
|
return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -20,8 +32,20 @@ export default {
|
||||||
v-for="(group, index) in groups"
|
v-for="(group, index) in groups"
|
||||||
:key="index"
|
:key="index"
|
||||||
:group="group"
|
:group="group"
|
||||||
:base-group="baseGroup"
|
:parent-group="parentGroup"
|
||||||
:collection="groups"
|
|
||||||
/>
|
/>
|
||||||
|
<li
|
||||||
|
v-if="hasMoreChildren"
|
||||||
|
class="group-row">
|
||||||
|
<a
|
||||||
|
:href="parentGroup.relativePath"
|
||||||
|
class="group-row-contents has-more-items">
|
||||||
|
<i
|
||||||
|
class="fa fa-external-link"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{moreChildrenStats}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,50 +2,29 @@
|
||||||
import identicon from '../../vue_shared/components/identicon.vue';
|
import identicon from '../../vue_shared/components/identicon.vue';
|
||||||
import eventHub from '../event_hub';
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
|
import itemCaret from './item_caret.vue';
|
||||||
|
import itemTypeIcon from './item_type_icon.vue';
|
||||||
|
import itemStats from './item_stats.vue';
|
||||||
|
import itemActions from './item_actions.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
identicon,
|
identicon,
|
||||||
|
itemCaret,
|
||||||
|
itemTypeIcon,
|
||||||
|
itemStats,
|
||||||
|
itemActions,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
parentGroup: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
group: {
|
group: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
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.groupPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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: {
|
computed: {
|
||||||
groupDomId() {
|
groupDomId() {
|
||||||
|
@ -53,51 +32,33 @@ export default {
|
||||||
},
|
},
|
||||||
rowClass() {
|
rowClass() {
|
||||||
return {
|
return {
|
||||||
'group-row': true,
|
|
||||||
'is-open': this.group.isOpen,
|
'is-open': this.group.isOpen,
|
||||||
'has-subgroups': this.group.hasSubgroups,
|
'has-children': this.hasChildren,
|
||||||
'no-description': !this.group.description,
|
'has-description': this.group.description,
|
||||||
|
'being-removed': this.group.isBeingRemoved,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
visibilityIcon() {
|
hasChildren() {
|
||||||
return {
|
return this.group.childrenCount > 0;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
hasAvatar() {
|
hasAvatar() {
|
||||||
return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;
|
return this.group.avatarUrl !== null;
|
||||||
|
},
|
||||||
|
isGroup() {
|
||||||
|
return this.group.type === 'group';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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))) {
|
||||||
|
if (this.hasChildren) {
|
||||||
|
eventHub.$emit('toggleChildren', this.group);
|
||||||
|
} else {
|
||||||
|
gl.utils.visitUrl(this.group.relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -108,98 +69,36 @@ export default {
|
||||||
@click.stop="onClickRowGroup"
|
@click.stop="onClickRowGroup"
|
||||||
:id="groupDomId"
|
:id="groupDomId"
|
||||||
:class="rowClass"
|
:class="rowClass"
|
||||||
|
class="group-row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="group-row-contents">
|
class="group-row-contents">
|
||||||
<div
|
<item-actions
|
||||||
class="controls">
|
v-if="isGroup"
|
||||||
<a
|
:group="group"
|
||||||
v-if="group.canEdit"
|
:parent-group="parentGroup"
|
||||||
class="edit-group btn"
|
/>
|
||||||
:href="group.editPath">
|
<item-stats
|
||||||
<i
|
:item="group"
|
||||||
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
|
<div
|
||||||
class="folder-toggle-wrap">
|
class="folder-toggle-wrap">
|
||||||
<span
|
<item-caret
|
||||||
class="folder-caret"
|
:is-group-open="group.isOpen"
|
||||||
v-if="group.hasSubgroups">
|
/>
|
||||||
<i
|
<item-type-icon
|
||||||
v-if="group.isOpen"
|
:item-type="group.type"
|
||||||
class="fa fa-caret-down"
|
:is-group-open="group.isOpen"
|
||||||
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>
|
||||||
<div
|
<div
|
||||||
class="avatar-container s40 hidden-xs">
|
class="avatar-container s40 hidden-xs"
|
||||||
|
:class="{ 'content-loading': group.isChildrenLoading }"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
:href="group.groupPath">
|
:href="group.relativePath"
|
||||||
|
class="no-expand"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="hasAvatar"
|
v-if="hasAvatar"
|
||||||
class="avatar s40"
|
class="avatar s40"
|
||||||
|
@ -215,19 +114,22 @@ export default {
|
||||||
<div
|
<div
|
||||||
class="title">
|
class="title">
|
||||||
<a
|
<a
|
||||||
:href="group.groupPath">{{fullPath}}</a>
|
:href="group.relativePath"
|
||||||
<template v-if="group.permissions.humanGroupAccess">
|
class="no-expand">{{group.fullName}}</a>
|
||||||
as
|
<span
|
||||||
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
|
v-if="group.permission"
|
||||||
</template>
|
class="access-type"
|
||||||
|
>
|
||||||
|
{{s__('GroupsTreeRole|as')}} {{group.permission}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="description">{{group.description}}</div>
|
class="description">{{group.description}}</div>
|
||||||
</div>
|
</div>
|
||||||
<group-folder
|
<group-folder
|
||||||
v-if="group.isOpen && hasGroups"
|
v-if="group.isOpen && hasChildren"
|
||||||
:groups="group.subGroups"
|
:parent-group="group"
|
||||||
:baseGroup="group"
|
:groups="group.children"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,24 +4,33 @@ import eventHub from '../event_hub';
|
||||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
tablePagination,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
groups: {
|
groups: {
|
||||||
type: Object,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
searchEmpty: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchEmptyMessage: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
tablePagination,
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
change(page) {
|
change(page) {
|
||||||
const filterGroupsParam = getParameterByName('filter_groups');
|
const filterGroupsParam = getParameterByName('filter_groups');
|
||||||
const sortParam = getParameterByName('sort');
|
const sortParam = getParameterByName('sort');
|
||||||
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
|
const archivedParam = getParameterByName('archived');
|
||||||
|
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -29,10 +38,17 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="groups-list-tree-container">
|
<div class="groups-list-tree-container">
|
||||||
|
<div
|
||||||
|
v-if="searchEmpty"
|
||||||
|
class="has-no-search-results">
|
||||||
|
{{searchEmptyMessage}}
|
||||||
|
</div>
|
||||||
<group-folder
|
<group-folder
|
||||||
|
v-if="!searchEmpty"
|
||||||
:groups="groups"
|
:groups="groups"
|
||||||
/>
|
/>
|
||||||
<table-pagination
|
<table-pagination
|
||||||
|
v-if="!searchEmpty"
|
||||||
:change="change"
|
:change="change"
|
||||||
:pageInfo="pageInfo"
|
:pageInfo="pageInfo"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script>
|
||||||
|
import { s__ } from '../../locale';
|
||||||
|
import tooltip from '../../vue_shared/directives/tooltip';
|
||||||
|
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
import { COMMON_STR } from '../constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PopupDialog,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
parentGroup: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogStatus: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
leaveBtnTitle() {
|
||||||
|
return COMMON_STR.LEAVE_BTN_TITLE;
|
||||||
|
},
|
||||||
|
editBtnTitle() {
|
||||||
|
return COMMON_STR.EDIT_BTN_TITLE;
|
||||||
|
},
|
||||||
|
leaveConfirmationMessage() {
|
||||||
|
return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onLeaveGroup() {
|
||||||
|
this.dialogStatus = true;
|
||||||
|
},
|
||||||
|
leaveGroup(leaveConfirmed) {
|
||||||
|
this.dialogStatus = false;
|
||||||
|
if (leaveConfirmed) {
|
||||||
|
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="controls">
|
||||||
|
<a
|
||||||
|
v-tooltip
|
||||||
|
v-if="group.canEdit"
|
||||||
|
:href="group.editPath"
|
||||||
|
:title="editBtnTitle"
|
||||||
|
:aria-label="editBtnTitle"
|
||||||
|
data-container="body"
|
||||||
|
class="edit-group btn no-expand">
|
||||||
|
<i
|
||||||
|
class="fa fa-cogs"
|
||||||
|
aria-hidden="true"/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-tooltip
|
||||||
|
v-if="group.canLeave"
|
||||||
|
@click.prevent="onLeaveGroup"
|
||||||
|
:href="group.leavePath"
|
||||||
|
:title="leaveBtnTitle"
|
||||||
|
:aria-label="leaveBtnTitle"
|
||||||
|
data-container="body"
|
||||||
|
class="leave-group btn no-expand">
|
||||||
|
<i
|
||||||
|
class="fa fa-sign-out"
|
||||||
|
aria-hidden="true"/>
|
||||||
|
</a>
|
||||||
|
<popup-dialog
|
||||||
|
v-show="dialogStatus"
|
||||||
|
:primary-button-label="__('Leave')"
|
||||||
|
kind="warning"
|
||||||
|
:title="__('Are you sure?')"
|
||||||
|
:text="__('Are you sure you want to leave this group?')"
|
||||||
|
:body="leaveConfirmationMessage"
|
||||||
|
@submit="leaveGroup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
isGroupOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconClass() {
|
||||||
|
return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="folder-caret">
|
||||||
|
<i
|
||||||
|
:class="iconClass"
|
||||||
|
class="fa"
|
||||||
|
aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
</template>
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script>
|
||||||
|
import tooltip from '../../vue_shared/directives/tooltip';
|
||||||
|
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibilityIcon() {
|
||||||
|
return VISIBILITY_TYPE_ICON[this.item.visibility];
|
||||||
|
},
|
||||||
|
visibilityTooltip() {
|
||||||
|
if (this.item.type === ITEM_TYPE.GROUP) {
|
||||||
|
return GROUP_VISIBILITY_TYPE[this.item.visibility];
|
||||||
|
}
|
||||||
|
return PROJECT_VISIBILITY_TYPE[this.item.visibility];
|
||||||
|
},
|
||||||
|
isProject() {
|
||||||
|
return this.item.type === ITEM_TYPE.PROJECT;
|
||||||
|
},
|
||||||
|
isGroup() {
|
||||||
|
return this.item.type === ITEM_TYPE.GROUP;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="stats">
|
||||||
|
<span
|
||||||
|
v-tooltip
|
||||||
|
v-if="isGroup"
|
||||||
|
:title="s__('Subgroups')"
|
||||||
|
class="number-subgroups"
|
||||||
|
data-placement="top"
|
||||||
|
data-container="body">
|
||||||
|
<i
|
||||||
|
class="fa fa-folder"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{item.subgroupCount}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-tooltip
|
||||||
|
v-if="isGroup"
|
||||||
|
:title="s__('Projects')"
|
||||||
|
class="number-projects"
|
||||||
|
data-placement="top"
|
||||||
|
data-container="body">
|
||||||
|
<i
|
||||||
|
class="fa fa-bookmark"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{item.projectCount}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-tooltip
|
||||||
|
v-if="isGroup"
|
||||||
|
:title="s__('Members')"
|
||||||
|
class="number-users"
|
||||||
|
data-placement="top"
|
||||||
|
data-container="body">
|
||||||
|
<i
|
||||||
|
class="fa fa-users"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{item.memberCount}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isProject"
|
||||||
|
class="project-stars">
|
||||||
|
<i
|
||||||
|
class="fa fa-star"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{{item.starCount}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-tooltip
|
||||||
|
:title="visibilityTooltip"
|
||||||
|
data-placement="left"
|
||||||
|
data-container="body"
|
||||||
|
class="item-visibility">
|
||||||
|
<i
|
||||||
|
:class="visibilityIcon"
|
||||||
|
class="fa"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script>
|
||||||
|
import { ITEM_TYPE } from '../constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
itemType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isGroupOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconClass() {
|
||||||
|
if (this.itemType === ITEM_TYPE.GROUP) {
|
||||||
|
return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
|
||||||
|
}
|
||||||
|
return 'fa-bookmark';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="item-type-icon">
|
||||||
|
<i
|
||||||
|
:class="iconClass"
|
||||||
|
class="fa"
|
||||||
|
aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
</template>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { __, s__ } from '../locale';
|
||||||
|
|
||||||
|
export const MAX_CHILDREN_COUNT = 20;
|
||||||
|
|
||||||
|
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_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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ITEM_TYPE = {
|
||||||
|
PROJECT: 'project',
|
||||||
|
GROUP: 'group',
|
||||||
|
};
|
||||||
|
|
||||||
|
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.'),
|
||||||
|
private: __('Private - The group and its projects can only be viewed by members.'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_VISIBILITY_TYPE = {
|
||||||
|
public: __('Public - The project can be accessed without any authentication.'),
|
||||||
|
internal: __('Internal - The project can be accessed by any logged in user.'),
|
||||||
|
private: __('Private - Project access must be granted explicitly to each user.'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VISIBILITY_TYPE_ICON = {
|
||||||
|
public: 'fa-globe',
|
||||||
|
internal: 'fa-shield',
|
||||||
|
private: 'fa-lock',
|
||||||
|
};
|
|
@ -3,12 +3,13 @@ import eventHub from './event_hub';
|
||||||
import { getParameterByName } from '../lib/utils/common_utils';
|
import { getParameterByName } from '../lib/utils/common_utils';
|
||||||
|
|
||||||
export default class GroupFilterableList extends FilterableList {
|
export default class GroupFilterableList extends FilterableList {
|
||||||
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
|
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
|
||||||
super(form, filter, holder);
|
super(form, filter, holder, filterInputField);
|
||||||
this.form = form;
|
this.form = form;
|
||||||
this.filterEndpoint = filterEndpoint;
|
this.filterEndpoint = filterEndpoint;
|
||||||
this.pagePath = pagePath;
|
this.pagePath = pagePath;
|
||||||
this.$dropdown = $('.js-group-filter-dropdown-wrap');
|
this.filterInputField = filterInputField;
|
||||||
|
this.$dropdown = $(dropdownSel);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterEndpoint() {
|
getFilterEndpoint() {
|
||||||
|
@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
super.bindEvents();
|
super.bindEvents();
|
||||||
|
|
||||||
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
|
|
||||||
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
|
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
|
||||||
|
|
||||||
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
|
|
||||||
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
|
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormSubmit(e) {
|
onFilterInput() {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const $form = $(this.form);
|
|
||||||
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
|
|
||||||
const queryData = {};
|
const queryData = {};
|
||||||
|
const $form = $(this.form);
|
||||||
|
const archivedParam = getParameterByName('archived', window.location.href);
|
||||||
|
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
|
||||||
|
|
||||||
if (filterGroupsParam) {
|
if (filterGroupsParam) {
|
||||||
queryData.filter_groups = filterGroupsParam;
|
queryData[this.filterInputField] = filterGroupsParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archivedParam) {
|
||||||
|
queryData.archived = archivedParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.filterResults(queryData);
|
this.filterResults(queryData);
|
||||||
|
|
||||||
|
if (this.setDefaultFilterOption) {
|
||||||
this.setDefaultFilterOption();
|
this.setDefaultFilterOption();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setDefaultFilterOption() {
|
setDefaultFilterOption() {
|
||||||
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
|
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
|
||||||
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const queryData = {};
|
const queryData = {};
|
||||||
const sortParam = getParameterByName('sort', e.currentTarget.href);
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
if (sortParam) {
|
if (sortParam) {
|
||||||
queryData.sort = sortParam;
|
queryData.sort = sortParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (archivedParam) {
|
||||||
|
queryData.archived = archivedParam;
|
||||||
|
}
|
||||||
|
|
||||||
this.filterResults(queryData);
|
this.filterResults(queryData);
|
||||||
|
|
||||||
// Active selected option
|
// Active selected option
|
||||||
|
if (isOptionFilterBySort) {
|
||||||
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
$(e.target).addClass('is-active');
|
||||||
|
|
||||||
// Clear current value on search form
|
// Clear current value on search form
|
||||||
this.form.querySelector('[name="filter_groups"]').value = '';
|
this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterSuccess(data, xhr, queryData) {
|
onFilterSuccess(data, xhr, queryData) {
|
||||||
super.onFilterSuccess(data, xhr, queryData);
|
const currentPath = this.getPagePath(queryData);
|
||||||
|
|
||||||
const paginationData = {
|
const paginationData = {
|
||||||
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
|
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
|
||||||
|
@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
|
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
|
||||||
};
|
};
|
||||||
|
|
||||||
eventHub.$emit('updateGroups', data);
|
window.history.replaceState({
|
||||||
|
page: currentPath,
|
||||||
|
}, document.title, currentPath);
|
||||||
|
|
||||||
|
eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
|
||||||
eventHub.$emit('updatePagination', paginationData);
|
eventHub.$emit('updatePagination', paginationData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Flash from '../flash';
|
import Translate from '../vue_shared/translate';
|
||||||
import GroupFilterableList from './groups_filterable_list';
|
import GroupFilterableList from './groups_filterable_list';
|
||||||
import GroupsComponent from './components/groups.vue';
|
import GroupsStore from './store/groups_store';
|
||||||
import GroupFolder from './components/group_folder.vue';
|
import GroupsService from './service/groups_service';
|
||||||
import GroupItem from './components/group_item.vue';
|
|
||||||
import GroupsStore from './stores/groups_store';
|
import groupsApp from './components/app.vue';
|
||||||
import GroupsService from './services/groups_service';
|
import groupFolderComponent from './components/group_folder.vue';
|
||||||
import eventHub from './event_hub';
|
import groupItemComponent from './components/group_item.vue';
|
||||||
import { getParameterByName } from '../lib/utils/common_utils';
|
|
||||||
|
Vue.use(Translate);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const el = document.getElementById('dashboard-group-app');
|
const el = document.getElementById('js-groups-tree');
|
||||||
|
|
||||||
// Don't do anything if element doesn't exist (No groups)
|
// Don't do anything if element doesn't exist (No groups)
|
||||||
// This is for when the user enters directly to the page via URL
|
// This is for when the user enters directly to the page via URL
|
||||||
|
@ -18,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('groups-component', GroupsComponent);
|
Vue.component('group-folder', groupFolderComponent);
|
||||||
Vue.component('group-folder', GroupFolder);
|
Vue.component('group-item', groupItemComponent);
|
||||||
Vue.component('group-item', GroupItem);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new Vue({
|
new Vue({
|
||||||
el,
|
el,
|
||||||
|
components: {
|
||||||
|
groupsApp,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
this.store = new GroupsStore();
|
const dataset = this.$options.el.dataset;
|
||||||
this.service = new GroupsService(el.dataset.endpoint);
|
const hideProjects = dataset.hideProjects === 'true';
|
||||||
|
const store = new GroupsStore(hideProjects);
|
||||||
|
const service = new GroupsService(dataset.endpoint);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store: this.store,
|
store,
|
||||||
isLoading: true,
|
service,
|
||||||
state: this.store.state,
|
hideProjects,
|
||||||
loading: true,
|
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 = getParameterByName('page');
|
|
||||||
if (pageParam) {
|
|
||||||
page = pageParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterGroupsParam = getParameterByName('filter_groups');
|
|
||||||
if (filterGroupsParam) {
|
|
||||||
filterGroups = filterGroupsParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
sortParam = 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);
|
|
||||||
|
|
||||||
return response.json().then((data) => {
|
|
||||||
this.updateGroups(data);
|
|
||||||
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(resp => resp.json())
|
|
||||||
.then((response) => {
|
|
||||||
$.scrollTo(0);
|
|
||||||
|
|
||||||
this.store.removeGroup(group, collection);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-new
|
|
||||||
new Flash(response.notice, 'notice');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
let message = 'An error occurred. Please try again.';
|
|
||||||
|
|
||||||
if (error.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() {
|
beforeMount() {
|
||||||
|
const dataset = this.$options.el.dataset;
|
||||||
let groupFilterList = null;
|
let groupFilterList = null;
|
||||||
const form = document.querySelector('form#group-filter-form');
|
const form = document.querySelector(dataset.formSel);
|
||||||
const filter = document.querySelector('.js-groups-list-filter');
|
const filter = document.querySelector(dataset.filterSel);
|
||||||
const holder = document.querySelector('.js-groups-list-holder');
|
const holder = document.querySelector(dataset.holderSel);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
form,
|
form,
|
||||||
filter,
|
filter,
|
||||||
holder,
|
holder,
|
||||||
filterEndpoint: el.dataset.endpoint,
|
filterEndpoint: dataset.endpoint,
|
||||||
pagePath: el.dataset.path,
|
pagePath: dataset.path,
|
||||||
|
dropdownSel: dataset.dropdownSel,
|
||||||
|
filterInputField: 'filter',
|
||||||
};
|
};
|
||||||
|
|
||||||
groupFilterList = new GroupFilterableList(opts);
|
groupFilterList = new GroupFilterableList(opts);
|
||||||
groupFilterList.initSearch();
|
groupFilterList.initSearch();
|
||||||
},
|
},
|
||||||
mounted() {
|
render(createElement) {
|
||||||
this.fetchGroups()
|
return createElement('groups-app', {
|
||||||
.then((response) => {
|
props: {
|
||||||
this.updatePagination(response.headers);
|
store: this.store,
|
||||||
this.isLoading = false;
|
service: this.service,
|
||||||
})
|
hideProjects: this.hideProjects,
|
||||||
.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,62 @@
|
||||||
|
import DropLab from '../droplab/drop_lab';
|
||||||
|
import ISetter from '../droplab/plugins/input_setter';
|
||||||
|
|
||||||
|
const InputSetter = Object.assign({}, ISetter);
|
||||||
|
|
||||||
|
const NEW_PROJECT = 'new-project';
|
||||||
|
const NEW_SUBGROUP = 'new-subgroup';
|
||||||
|
|
||||||
|
export default class NewGroupChild {
|
||||||
|
constructor(buttonWrapper) {
|
||||||
|
this.buttonWrapper = buttonWrapper;
|
||||||
|
this.newGroupChildButton = this.buttonWrapper.querySelector('.js-new-group-child');
|
||||||
|
this.dropdownToggle = this.buttonWrapper.querySelector('.js-dropdown-toggle');
|
||||||
|
this.dropdownList = this.buttonWrapper.querySelector('.dropdown-menu');
|
||||||
|
|
||||||
|
this.newGroupPath = this.buttonWrapper.dataset.projectPath;
|
||||||
|
this.subgroupPath = this.buttonWrapper.dataset.subgroupPath;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.initDroplab();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
initDroplab() {
|
||||||
|
this.droplab = new DropLab();
|
||||||
|
this.droplab.init(
|
||||||
|
this.dropdownToggle,
|
||||||
|
this.dropdownList,
|
||||||
|
[InputSetter],
|
||||||
|
this.getDroplabConfig(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDroplabConfig() {
|
||||||
|
return {
|
||||||
|
InputSetter: [{
|
||||||
|
input: this.newGroupChildButton,
|
||||||
|
valueAttribute: 'data-value',
|
||||||
|
inputAttribute: 'data-action',
|
||||||
|
}, {
|
||||||
|
input: this.newGroupChildButton,
|
||||||
|
valueAttribute: 'data-text',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.newGroupChildButton
|
||||||
|
.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickNewGroupChildButton(e) {
|
||||||
|
if (e.target.dataset.action === NEW_PROJECT) {
|
||||||
|
gl.utils.visitUrl(this.newGroupPath);
|
||||||
|
} else if (e.target.dataset.action === NEW_SUBGROUP) {
|
||||||
|
gl.utils.visitUrl(this.subgroupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ export default class GroupsService {
|
||||||
this.groups = Vue.resource(endpoint);
|
this.groups = Vue.resource(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroups(parentId, page, filterGroups, sort) {
|
getGroups(parentId, page, filterGroups, sort, archived) {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
|
@ -20,12 +20,16 @@ export default class GroupsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterGroups) {
|
if (filterGroups) {
|
||||||
data.filter_groups = filterGroups;
|
data.filter = filterGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sort) {
|
if (sort) {
|
||||||
data.sort = sort;
|
data.sort = sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (archived) {
|
||||||
|
data.archived = archived;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.groups.get(data);
|
return this.groups.get(data);
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
|
||||||
|
|
||||||
|
export default class GroupsStore {
|
||||||
|
constructor(hideProjects) {
|
||||||
|
this.state = {};
|
||||||
|
this.state.groups = [];
|
||||||
|
this.state.pageInfo = {};
|
||||||
|
this.hideProjects = hideProjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroups(rawGroups) {
|
||||||
|
if (rawGroups && rawGroups.length) {
|
||||||
|
this.state.groups = rawGroups.map(rawGroup => this.formatGroupItem(rawGroup));
|
||||||
|
} else {
|
||||||
|
this.state.groups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchedGroups(rawGroups) {
|
||||||
|
const formatGroups = groups => groups.map((group) => {
|
||||||
|
const formattedGroup = this.formatGroupItem(group);
|
||||||
|
if (formattedGroup.children && formattedGroup.children.length) {
|
||||||
|
formattedGroup.children = formatGroups(formattedGroup.children);
|
||||||
|
}
|
||||||
|
return formattedGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rawGroups && rawGroups.length) {
|
||||||
|
this.state.groups = formatGroups(rawGroups);
|
||||||
|
} else {
|
||||||
|
this.state.groups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupChildren(parentGroup, children) {
|
||||||
|
const updatedParentGroup = parentGroup;
|
||||||
|
updatedParentGroup.children = children.map(rawChild => this.formatGroupItem(rawChild));
|
||||||
|
updatedParentGroup.isOpen = true;
|
||||||
|
updatedParentGroup.isChildrenLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroups() {
|
||||||
|
return this.state.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaginationInfo(pagination = {}) {
|
||||||
|
let paginationInfo;
|
||||||
|
|
||||||
|
if (Object.keys(pagination).length) {
|
||||||
|
const normalizedHeaders = normalizeHeaders(pagination);
|
||||||
|
paginationInfo = parseIntPagination(normalizedHeaders);
|
||||||
|
} else {
|
||||||
|
paginationInfo = pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.pageInfo = paginationInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaginationInfo() {
|
||||||
|
return this.state.pageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatGroupItem(rawGroupItem) {
|
||||||
|
const groupChildren = rawGroupItem.children || [];
|
||||||
|
const groupIsOpen = (groupChildren.length > 0) || false;
|
||||||
|
const childrenCount = this.hideProjects ?
|
||||||
|
rawGroupItem.subgroup_count :
|
||||||
|
rawGroupItem.children_count;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: rawGroupItem.id,
|
||||||
|
name: rawGroupItem.name,
|
||||||
|
fullName: rawGroupItem.full_name,
|
||||||
|
description: rawGroupItem.description,
|
||||||
|
visibility: rawGroupItem.visibility,
|
||||||
|
avatarUrl: rawGroupItem.avatar_url,
|
||||||
|
relativePath: rawGroupItem.relative_path,
|
||||||
|
editPath: rawGroupItem.edit_path,
|
||||||
|
leavePath: rawGroupItem.leave_path,
|
||||||
|
canEdit: rawGroupItem.can_edit,
|
||||||
|
canLeave: rawGroupItem.can_leave,
|
||||||
|
type: rawGroupItem.type,
|
||||||
|
permission: rawGroupItem.permission,
|
||||||
|
children: groupChildren,
|
||||||
|
isOpen: groupIsOpen,
|
||||||
|
isChildrenLoading: false,
|
||||||
|
isBeingRemoved: false,
|
||||||
|
parentId: rawGroupItem.parent_id,
|
||||||
|
childrenCount,
|
||||||
|
projectCount: rawGroupItem.project_count,
|
||||||
|
subgroupCount: rawGroupItem.subgroup_count,
|
||||||
|
memberCount: rawGroupItem.number_users_with_delimiter,
|
||||||
|
starCount: rawGroupItem.star_count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeGroup(group, parentGroup) {
|
||||||
|
const updatedParentGroup = parentGroup;
|
||||||
|
if (updatedParentGroup.children && updatedParentGroup.children.length) {
|
||||||
|
updatedParentGroup.children = parentGroup.children.filter(child => group.id !== child.id);
|
||||||
|
} else {
|
||||||
|
this.state.groups = this.state.groups.filter(child => group.id !== child.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,167 +0,0 @@
|
||||||
import Vue from 'vue';
|
|
||||||
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
|
|
||||||
|
|
||||||
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 = normalizeHeaders(pagination);
|
|
||||||
paginationInfo = 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[`id${group.id}`] = group;
|
|
||||||
mappedGroups[`id${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[`id${currentGroup.parentId}`];
|
|
||||||
if (findParentGroup) {
|
|
||||||
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
|
|
||||||
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
|
|
||||||
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
|
|
||||||
tree[`id${currentGroup.id}`] = currentGroup;
|
|
||||||
} else {
|
|
||||||
// No parent found. We save it for later processing
|
|
||||||
orphans.push(currentGroup);
|
|
||||||
|
|
||||||
// Add to tree to preserve original order
|
|
||||||
tree[`id${currentGroup.id}`] = currentGroup;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the group is at the top level, add it to first level elements array.
|
|
||||||
tree[`id${currentGroup.id}`] = currentGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orphans.length) {
|
|
||||||
orphans.map((orphan) => {
|
|
||||||
let found = false;
|
|
||||||
const currentOrphan = orphan;
|
|
||||||
|
|
||||||
Object.keys(tree).map((key) => {
|
|
||||||
const group = tree[key];
|
|
||||||
|
|
||||||
if (
|
|
||||||
group &&
|
|
||||||
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
|
|
||||||
// Make sure the currently selected orphan is not the same as the group
|
|
||||||
// we are checking here otherwise it will end up in an infinite loop
|
|
||||||
currentOrphan.id !== group.id
|
|
||||||
) {
|
|
||||||
group.subGroups[currentOrphan.id] = currentOrphan;
|
|
||||||
group.isOpen = true;
|
|
||||||
currentOrphan.isOrphan = true;
|
|
||||||
found = true;
|
|
||||||
|
|
||||||
// Delete if group was put at the top level. If not the group will be displayed twice.
|
|
||||||
if (tree[`id${currentOrphan.id}`]) {
|
|
||||||
delete tree[`id${currentOrphan.id}`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
currentOrphan.isOrphan = true;
|
|
||||||
|
|
||||||
tree[`id${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,
|
|
||||||
groupPath: rawGroup.group_path,
|
|
||||||
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, `id${group.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
toggleSubGroups(toggleGroup) {
|
|
||||||
const group = toggleGroup;
|
|
||||||
group.isOpen = !group.isOpen;
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,6 +24,11 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
showInlineEditButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
issuableRef: {
|
issuableRef: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -222,20 +227,25 @@ export default {
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<title-component
|
<title-component
|
||||||
:issuable-ref="issuableRef"
|
:issuable-ref="issuableRef"
|
||||||
|
:can-update="canUpdate"
|
||||||
:title-html="state.titleHtml"
|
:title-html="state.titleHtml"
|
||||||
:title-text="state.titleText" />
|
:title-text="state.titleText"
|
||||||
|
:show-inline-edit-button="showInlineEditButton"
|
||||||
|
/>
|
||||||
<description-component
|
<description-component
|
||||||
v-if="state.descriptionHtml"
|
v-if="state.descriptionHtml"
|
||||||
:can-update="canUpdate"
|
:can-update="canUpdate"
|
||||||
:description-html="state.descriptionHtml"
|
:description-html="state.descriptionHtml"
|
||||||
:description-text="state.descriptionText"
|
:description-text="state.descriptionText"
|
||||||
:updated-at="state.updatedAt"
|
:updated-at="state.updatedAt"
|
||||||
:task-status="state.taskStatus" />
|
:task-status="state.taskStatus"
|
||||||
|
/>
|
||||||
<edited-component
|
<edited-component
|
||||||
v-if="hasUpdated"
|
v-if="hasUpdated"
|
||||||
:updated-at="state.updatedAt"
|
:updated-at="state.updatedAt"
|
||||||
:updated-by-name="state.updatedByName"
|
:updated-by-name="state.updatedByName"
|
||||||
:updated-by-path="state.updatedByPath" />
|
:updated-by-path="state.updatedByPath"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import animateMixin from '../mixins/animate';
|
import animateMixin from '../mixins/animate';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
import tooltip from '../../vue_shared/directives/tooltip';
|
||||||
|
import { spriteIcon } from '../../lib/utils/common_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [animateMixin],
|
mixins: [animateMixin],
|
||||||
|
@ -15,6 +18,11 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
canUpdate: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
titleHtml: {
|
titleHtml: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -23,6 +31,14 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showInlineEditButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
titleHtml() {
|
titleHtml() {
|
||||||
|
@ -30,17 +46,26 @@
|
||||||
this.animateChange();
|
this.animateChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
pencilIcon() {
|
||||||
|
return spriteIcon('pencil', 'link-highlight');
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setPageTitle() {
|
setPageTitle() {
|
||||||
const currentPageTitleScope = this.titleEl.innerText.split('·');
|
const currentPageTitleScope = this.titleEl.innerText.split('·');
|
||||||
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
|
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
|
||||||
this.titleEl.textContent = currentPageTitleScope.join('·');
|
this.titleEl.textContent = currentPageTitleScope.join('·');
|
||||||
},
|
},
|
||||||
|
edit() {
|
||||||
|
eventHub.$emit('open.form');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="title-container">
|
||||||
<h2
|
<h2
|
||||||
class="title"
|
class="title"
|
||||||
:class="{
|
:class="{
|
||||||
|
@ -50,4 +75,17 @@
|
||||||
v-html="titleHtml"
|
v-html="titleHtml"
|
||||||
>
|
>
|
||||||
</h2>
|
</h2>
|
||||||
|
<button
|
||||||
|
v-tooltip
|
||||||
|
v-if="showInlineEditButton && canUpdate"
|
||||||
|
type="button"
|
||||||
|
class="btn-blank btn-edit note-action-button"
|
||||||
|
v-html="pencilIcon"
|
||||||
|
title="Edit title and description"
|
||||||
|
data-placement="bottom"
|
||||||
|
data-container="body"
|
||||||
|
@click="edit"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -403,7 +403,11 @@ export const setCiStatusFavicon = (pageUrl) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
|
export const spriteIcon = (icon, className = '') => {
|
||||||
|
const classAttribute = className.length > 0 ? `class="${className}"` : '';
|
||||||
|
|
||||||
|
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
|
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ import Helper from '../helpers/repo_helper';
|
||||||
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
|
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
components: {
|
components: {
|
||||||
RepoSidebar,
|
RepoSidebar,
|
||||||
|
|
|
@ -9,7 +9,9 @@ import { visitUrl } from '../../lib/utils/url_utility';
|
||||||
export default {
|
export default {
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
|
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
PopupDialog,
|
PopupDialog,
|
||||||
|
|
|
@ -3,7 +3,9 @@ import Store from '../stores/repo_store';
|
||||||
import RepoMixin from '../mixins/repo_mixin';
|
import RepoMixin from '../mixins/repo_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
computed: {
|
computed: {
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
|
|
|
@ -5,7 +5,9 @@ import Service from '../services/repo_service';
|
||||||
import Helper from '../helpers/repo_helper';
|
import Helper from '../helpers/repo_helper';
|
||||||
|
|
||||||
const RepoEditor = {
|
const RepoEditor = {
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
if (Helper.monacoInstance) {
|
if (Helper.monacoInstance) {
|
||||||
|
@ -22,7 +24,8 @@ const RepoEditor = {
|
||||||
const monacoInstance = Helper.monaco.editor.create(this.$el, {
|
const monacoInstance = Helper.monaco.editor.create(this.$el, {
|
||||||
model: null,
|
model: null,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
contextmenu: false,
|
contextmenu: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Helper.monacoInstance = monacoInstance;
|
Helper.monacoInstance = monacoInstance;
|
||||||
|
@ -92,7 +95,7 @@ const RepoEditor = {
|
||||||
},
|
},
|
||||||
|
|
||||||
blobRaw() {
|
blobRaw() {
|
||||||
if (Helper.monacoInstance && !this.isTree) {
|
if (Helper.monacoInstance) {
|
||||||
this.setupEditor();
|
this.setupEditor();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,105 +1,76 @@
|
||||||
<script>
|
<script>
|
||||||
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
|
import timeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
|
import repoMixin from '../mixins/repo_mixin';
|
||||||
|
|
||||||
const RepoFile = {
|
export default {
|
||||||
mixins: [TimeAgoMixin],
|
mixins: [
|
||||||
|
repoMixin,
|
||||||
|
timeAgoMixin,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
file: {
|
file: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isMini: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
},
|
||||||
loading: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default() { return { tree: false }; },
|
|
||||||
},
|
|
||||||
hasFiles: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
activeFile: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
canShowFile() {
|
|
||||||
return !this.loading.tree || this.hasFiles;
|
|
||||||
},
|
|
||||||
|
|
||||||
fileIcon() {
|
fileIcon() {
|
||||||
const classObj = {
|
const classObj = {
|
||||||
'fa-spinner fa-spin': this.file.loading,
|
'fa-spinner fa-spin': this.file.loading,
|
||||||
[this.file.icon]: !this.file.loading,
|
[this.file.icon]: !this.file.loading,
|
||||||
|
'fa-folder-open': !this.file.loading && this.file.opened,
|
||||||
};
|
};
|
||||||
return classObj;
|
return classObj;
|
||||||
},
|
},
|
||||||
|
levelIndentation() {
|
||||||
fileIndentation() {
|
|
||||||
return {
|
return {
|
||||||
'margin-left': `${this.file.level * 10}px`,
|
marginLeft: `${this.file.level * 16}px`,
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activeFileClass() {
|
|
||||||
return {
|
|
||||||
active: this.activeFile.url === this.file.url,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked(file) {
|
linkClicked(file) {
|
||||||
this.$emit('linkclicked', file);
|
eventHub.$emit('fileNameClicked', file);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepoFile;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr
|
<tr
|
||||||
v-if="canShowFile"
|
|
||||||
class="file"
|
class="file"
|
||||||
:class="activeFileClass"
|
|
||||||
@click.prevent="linkClicked(file)">
|
@click.prevent="linkClicked(file)">
|
||||||
<td>
|
<td>
|
||||||
<i
|
<i
|
||||||
class="fa fa-fw file-icon"
|
class="fa fa-fw file-icon"
|
||||||
:class="fileIcon"
|
:class="fileIcon"
|
||||||
:style="fileIndentation"
|
:style="levelIndentation"
|
||||||
aria-label="file icon">
|
aria-hidden="true"
|
||||||
|
>
|
||||||
</i>
|
</i>
|
||||||
<a
|
<a
|
||||||
:href="file.url"
|
:href="file.url"
|
||||||
class="repo-file-name"
|
class="repo-file-name"
|
||||||
:title="file.url">
|
>
|
||||||
{{ file.name }}
|
{{ file.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<template v-if="!isMini">
|
<template v-if="!isMini">
|
||||||
<td class="hidden-sm hidden-xs">
|
<td class="hidden-sm hidden-xs">
|
||||||
<div class="commit-message">
|
<a
|
||||||
<a @click.stop :href="file.lastCommitUrl">
|
@click.stop
|
||||||
{{file.lastCommitMessage}}
|
:href="file.lastCommit.url"
|
||||||
|
class="commit-message"
|
||||||
|
>
|
||||||
|
{{ file.lastCommit.message }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="hidden-xs text-right">
|
<td class="commit-update hidden-xs text-right">
|
||||||
<span
|
<span :title="tooltipTitle(file.lastCommit.updatedAt)">
|
||||||
class="commit-update"
|
{{ timeFormated(file.lastCommit.updatedAt) }}
|
||||||
:title="tooltipTitle(file.lastCommitUpdate)">
|
|
||||||
{{timeFormated(file.lastCommitUpdate)}}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper';
|
||||||
import RepoMixin from '../mixins/repo_mixin';
|
import RepoMixin from '../mixins/repo_mixin';
|
||||||
|
|
||||||
const RepoFileButtons = {
|
const RepoFileButtons = {
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
|
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script>
|
|
||||||
const RepoFileOptions = {
|
|
||||||
props: {
|
|
||||||
isMini: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
projectName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RepoFileOptions;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tr v-if="isMini" class="repo-file-options">
|
|
||||||
<td>
|
|
||||||
<span class="title">{{projectName}}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
|
@ -1,43 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
const RepoLoadingFile = {
|
import repoMixin from '../mixins/repo_mixin';
|
||||||
props: {
|
|
||||||
loading: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: {},
|
|
||||||
},
|
|
||||||
hasFiles: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isMini: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
showGhostLines() {
|
|
||||||
return this.loading.tree && !this.hasFiles;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [
|
||||||
|
repoMixin,
|
||||||
|
],
|
||||||
methods: {
|
methods: {
|
||||||
lineOfCode(n) {
|
lineOfCode(n) {
|
||||||
return `skeleton-line-${n}`;
|
return `skeleton-line-${n}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepoLoadingFile;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr
|
<tr
|
||||||
v-if="showGhostLines"
|
class="loading-file"
|
||||||
class="loading-file">
|
aria-label="Loading files"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
class="animation-container animation-container-small">
|
class="animation-container animation-container-small">
|
||||||
|
@ -48,9 +28,8 @@ export default RepoLoadingFile;
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<template v-if="!isMini">
|
||||||
<td
|
<td
|
||||||
v-if="!isMini"
|
|
||||||
class="hidden-sm hidden-xs">
|
class="hidden-sm hidden-xs">
|
||||||
<div class="animation-container">
|
<div class="animation-container">
|
||||||
<div
|
<div
|
||||||
|
@ -62,9 +41,8 @@ export default RepoLoadingFile;
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
v-if="!isMini"
|
|
||||||
class="hidden-xs">
|
class="hidden-xs">
|
||||||
<div class="animation-container animation-container-small">
|
<div class="animation-container animation-container-small animation-container-right">
|
||||||
<div
|
<div
|
||||||
v-for="n in 6"
|
v-for="n in 6"
|
||||||
:key="n"
|
:key="n"
|
||||||
|
@ -72,5 +50,6 @@ export default RepoLoadingFile;
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
<script>
|
<script>
|
||||||
import RepoMixin from '../mixins/repo_mixin';
|
import eventHub from '../event_hub';
|
||||||
|
import repoMixin from '../mixins/repo_mixin';
|
||||||
|
|
||||||
const RepoPreviousDirectory = {
|
export default {
|
||||||
|
mixins: [
|
||||||
|
repoMixin,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
prevUrl: {
|
prevUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [RepoMixin],
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
colSpanCondition() {
|
colSpanCondition() {
|
||||||
return this.isMini ? undefined : 3;
|
return this.isMini ? undefined : 3;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked(file) {
|
linkClicked(file) {
|
||||||
this.$emit('linkclicked', file);
|
eventHub.$emit('goToPreviousDirectoryClicked', file);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepoPreviousDirectory;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr class="prev-directory">
|
<tr class="file prev-directory">
|
||||||
<td
|
<td
|
||||||
:colspan="colSpanCondition"
|
:colspan="colSpanCondition"
|
||||||
@click.prevent="linkClicked(prevUrl)">
|
class="table-cell"
|
||||||
<a :href="prevUrl">..</a>
|
@click.prevent="linkClicked(prevUrl)"
|
||||||
|
>
|
||||||
|
<a :href="prevUrl">...</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
import Store from '../stores/repo_store';
|
import Store from '../stores/repo_store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => Store,
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
html() {
|
html() {
|
||||||
return this.activeFile.html;
|
return this.activeFile.html;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'underscore';
|
||||||
import Service from '../services/repo_service';
|
import Service from '../services/repo_service';
|
||||||
import Helper from '../helpers/repo_helper';
|
import Helper from '../helpers/repo_helper';
|
||||||
import Store from '../stores/repo_store';
|
import Store from '../stores/repo_store';
|
||||||
|
import eventHub from '../event_hub';
|
||||||
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
||||||
import RepoFileOptions from './repo_file_options.vue';
|
|
||||||
import RepoFile from './repo_file.vue';
|
import RepoFile from './repo_file.vue';
|
||||||
import RepoLoadingFile from './repo_loading_file.vue';
|
import RepoLoadingFile from './repo_loading_file.vue';
|
||||||
import RepoMixin from '../mixins/repo_mixin';
|
import RepoMixin from '../mixins/repo_mixin';
|
||||||
|
@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin';
|
||||||
export default {
|
export default {
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
components: {
|
components: {
|
||||||
'repo-file-options': RepoFileOptions,
|
|
||||||
'repo-previous-directory': RepoPreviousDirectory,
|
'repo-previous-directory': RepoPreviousDirectory,
|
||||||
'repo-file': RepoFile,
|
'repo-file': RepoFile,
|
||||||
'repo-loading-file': RepoLoadingFile,
|
'repo-loading-file': RepoLoadingFile,
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener('popstate', this.checkHistory);
|
window.addEventListener('popstate', this.checkHistory);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
|
eventHub.$off('fileNameClicked', this.fileClicked);
|
||||||
|
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
|
||||||
window.removeEventListener('popstate', this.checkHistory);
|
window.removeEventListener('popstate', this.checkHistory);
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
eventHub.$on('fileNameClicked', this.fileClicked);
|
||||||
|
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return Store;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
flattendFiles() {
|
||||||
|
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
|
||||||
|
|
||||||
data: () => Store,
|
return _.chain(this.files)
|
||||||
|
.map(arr => [arr, mapFiles(arr)])
|
||||||
|
.flatten()
|
||||||
|
.value();
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkHistory() {
|
checkHistory() {
|
||||||
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
|
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
|
||||||
|
@ -52,21 +67,21 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
fileClicked(clickedFile, lineNumber) {
|
fileClicked(clickedFile, lineNumber) {
|
||||||
let file = clickedFile;
|
const file = clickedFile;
|
||||||
|
|
||||||
if (file.loading) return;
|
if (file.loading) return;
|
||||||
file.loading = true;
|
|
||||||
|
|
||||||
if (file.type === 'tree' && file.opened) {
|
if (file.type === 'tree' && file.opened) {
|
||||||
file = Store.removeChildFilesOfTree(file);
|
Helper.setDirectoryToClosed(file);
|
||||||
file.loading = false;
|
|
||||||
Store.setActiveLine(lineNumber);
|
Store.setActiveLine(lineNumber);
|
||||||
} else {
|
} else {
|
||||||
const openFile = Helper.getFileFromPath(file.url);
|
const openFile = Helper.getFileFromPath(file.url);
|
||||||
|
|
||||||
if (openFile) {
|
if (openFile) {
|
||||||
file.loading = false;
|
|
||||||
Store.setActiveFiles(openFile);
|
Store.setActiveFiles(openFile);
|
||||||
Store.setActiveLine(lineNumber);
|
Store.setActiveLine(lineNumber);
|
||||||
} else {
|
} else {
|
||||||
|
file.loading = true;
|
||||||
Service.url = file.url;
|
Service.url = file.url;
|
||||||
Helper.getContent(file)
|
Helper.getContent(file)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -81,7 +96,7 @@ export default {
|
||||||
|
|
||||||
goToPreviousDirectoryClicked(prevURL) {
|
goToPreviousDirectoryClicked(prevURL) {
|
||||||
Service.url = prevURL;
|
Service.url = prevURL;
|
||||||
Helper.getContent(null)
|
Helper.getContent(null, true)
|
||||||
.then(() => Helper.scrollTabsRight())
|
.then(() => Helper.scrollTabsRight())
|
||||||
.catch(Helper.loadingError);
|
.catch(Helper.loadingError);
|
||||||
},
|
},
|
||||||
|
@ -92,38 +107,43 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
|
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead v-if="!isMini">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="name">Name</th>
|
<th
|
||||||
<th class="hidden-sm hidden-xs last-commit">Last commit</th>
|
v-if="isMini"
|
||||||
<th class="hidden-xs last-update text-right">Last update</th>
|
class="repo-file-options title"
|
||||||
|
>
|
||||||
|
<strong class="clgray">
|
||||||
|
{{ projectName }}
|
||||||
|
</strong>
|
||||||
|
</th>
|
||||||
|
<template v-else>
|
||||||
|
<th class="name">
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th class="hidden-sm hidden-xs last-commit">
|
||||||
|
Last commit
|
||||||
|
</th>
|
||||||
|
<th class="hidden-xs last-update text-right">
|
||||||
|
Last update
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<repo-file-options
|
|
||||||
:is-mini="isMini"
|
|
||||||
:project-name="projectName"
|
|
||||||
/>
|
|
||||||
<repo-previous-directory
|
<repo-previous-directory
|
||||||
v-if="isRoot"
|
v-if="!isRoot && !loading.tree"
|
||||||
:prev-url="prevURL"
|
:prev-url="prevURL"
|
||||||
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
|
/>
|
||||||
<repo-loading-file
|
<repo-loading-file
|
||||||
|
v-if="!flattendFiles.length && loading.tree"
|
||||||
v-for="n in 5"
|
v-for="n in 5"
|
||||||
:key="n"
|
:key="n"
|
||||||
:loading="loading"
|
|
||||||
:has-files="!!files.length"
|
|
||||||
:is-mini="isMini"
|
|
||||||
/>
|
/>
|
||||||
<repo-file
|
<repo-file
|
||||||
v-for="file in files"
|
v-for="file in flattendFiles"
|
||||||
:key="file.id"
|
:key="file.id"
|
||||||
:file="file"
|
:file="file"
|
||||||
:is-mini="isMini"
|
|
||||||
@linkclicked="fileClicked(file)"
|
|
||||||
:is-tree="isTree"
|
|
||||||
:has-files="!!files.length"
|
|
||||||
:active-file="activeFile"
|
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -26,11 +26,13 @@ const RepoTab = {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
tabClicked: Store.setActiveFiles,
|
tabClicked(file) {
|
||||||
|
Store.setActiveFiles(file);
|
||||||
|
},
|
||||||
closeTab(file) {
|
closeTab(file) {
|
||||||
if (file.changed) return;
|
if (file.changed) return;
|
||||||
this.$emit('tabclosed', file);
|
|
||||||
|
Store.removeFromOpenedFiles(file);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -39,10 +41,13 @@ export default RepoTab;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li @click="tabClicked(tab)">
|
<li
|
||||||
<a
|
:class="{ active : tab.active }"
|
||||||
href="#0"
|
@click="tabClicked(tab)"
|
||||||
class="close"
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close-btn"
|
||||||
@click.stop.prevent="closeTab(tab)"
|
@click.stop.prevent="closeTab(tab)"
|
||||||
:aria-label="closeLabel">
|
:aria-label="closeLabel">
|
||||||
<i
|
<i
|
||||||
|
@ -50,7 +55,7 @@ export default RepoTab;
|
||||||
:class="changedClass"
|
:class="changedClass"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
|
|
|
@ -3,33 +3,26 @@ import Store from '../stores/repo_store';
|
||||||
import RepoTab from './repo_tab.vue';
|
import RepoTab from './repo_tab.vue';
|
||||||
import RepoMixin from '../mixins/repo_mixin';
|
import RepoMixin from '../mixins/repo_mixin';
|
||||||
|
|
||||||
const RepoTabs = {
|
export default {
|
||||||
mixins: [RepoMixin],
|
mixins: [RepoMixin],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
'repo-tab': RepoTab,
|
'repo-tab': RepoTab,
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
data: () => Store,
|
return Store;
|
||||||
|
|
||||||
methods: {
|
|
||||||
tabClosed(file) {
|
|
||||||
Store.removeFromOpenedFiles(file);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepoTabs;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul id="tabs">
|
<ul
|
||||||
|
id="tabs"
|
||||||
|
class="list-unstyled"
|
||||||
|
>
|
||||||
<repo-tab
|
<repo-tab
|
||||||
v-for="tab in openedFiles"
|
v-for="tab in openedFiles"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:tab="tab"
|
:tab="tab"
|
||||||
:class="{'active' : tab.active}"
|
|
||||||
@tabclosed="tabClosed"
|
|
||||||
/>
|
/>
|
||||||
<li class="tabs-divider" />
|
<li class="tabs-divider" />
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default new Vue();
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
|
||||||
import Service from '../services/repo_service';
|
import Service from '../services/repo_service';
|
||||||
import Store from '../stores/repo_store';
|
import Store from '../stores/repo_store';
|
||||||
import Flash from '../../flash';
|
import Flash from '../../flash';
|
||||||
|
@ -25,10 +26,6 @@ const RepoHelper = {
|
||||||
|
|
||||||
key: '',
|
key: '',
|
||||||
|
|
||||||
isTree(data) {
|
|
||||||
return Object.hasOwnProperty.call(data, 'blobs');
|
|
||||||
},
|
|
||||||
|
|
||||||
Time: window.performance
|
Time: window.performance
|
||||||
&& window.performance.now
|
&& window.performance.now
|
||||||
? window.performance
|
? window.performance
|
||||||
|
@ -58,13 +55,20 @@ const RepoHelper = {
|
||||||
},
|
},
|
||||||
|
|
||||||
setDirectoryOpen(tree, title) {
|
setDirectoryOpen(tree, title) {
|
||||||
const file = tree;
|
if (!tree) return;
|
||||||
if (!file) return undefined;
|
|
||||||
|
|
||||||
file.opened = true;
|
Object.assign(tree, {
|
||||||
file.icon = 'fa-folder-open';
|
opened: true,
|
||||||
RepoHelper.updateHistoryEntry(file.url, title);
|
});
|
||||||
return file;
|
|
||||||
|
RepoHelper.updateHistoryEntry(tree.url, title);
|
||||||
|
},
|
||||||
|
|
||||||
|
setDirectoryToClosed(entry) {
|
||||||
|
Object.assign(entry, {
|
||||||
|
opened: false,
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
isRenderable() {
|
isRenderable() {
|
||||||
|
@ -81,63 +85,23 @@ const RepoHelper = {
|
||||||
.catch(RepoHelper.loadingError);
|
.catch(RepoHelper.loadingError);
|
||||||
},
|
},
|
||||||
|
|
||||||
// when you open a directory you need to put the directory files under
|
getContent(treeOrFile, emptyFiles = false) {
|
||||||
// the directory... This will merge the list of the current directory and the new list.
|
|
||||||
getNewMergedList(inDirectory, currentList, newList) {
|
|
||||||
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
|
|
||||||
if (!inDirectory) return newListSorted;
|
|
||||||
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
|
|
||||||
if (!indexOfFile) return newListSorted;
|
|
||||||
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
|
|
||||||
},
|
|
||||||
|
|
||||||
// within the get new merged list this does the merging of the current list of files
|
|
||||||
// and the new list of files. The files are never "in" another directory they just
|
|
||||||
// appear like they are because of the margin.
|
|
||||||
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
|
|
||||||
newList.reverse().forEach((newFile) => {
|
|
||||||
const fileIndex = indexOfFile + 1;
|
|
||||||
const file = newFile;
|
|
||||||
file.level = inDirectory.level + 1;
|
|
||||||
oldList.splice(fileIndex, 0, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
return oldList;
|
|
||||||
},
|
|
||||||
|
|
||||||
compareFilesCaseInsensitive(a, b) {
|
|
||||||
const aName = a.name.toLowerCase();
|
|
||||||
const bName = b.name.toLowerCase();
|
|
||||||
if (a.level > 0) return 0;
|
|
||||||
if (aName < bName) { return -1; }
|
|
||||||
if (aName > bName) { return 1; }
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
isRoot(url) {
|
|
||||||
// the url we are requesting -> split by the project URL. Grab the right side.
|
|
||||||
const isRoot = !!url.split(Store.projectUrl)[1]
|
|
||||||
// remove the first "/"
|
|
||||||
.slice(1)
|
|
||||||
// split this by "/"
|
|
||||||
.split('/')
|
|
||||||
// remove the first two items of the array... usually /tree/master.
|
|
||||||
.slice(2)
|
|
||||||
// we want to know the length of the array.
|
|
||||||
// If greater than 0 not root.
|
|
||||||
.length;
|
|
||||||
return isRoot;
|
|
||||||
},
|
|
||||||
|
|
||||||
getContent(treeOrFile) {
|
|
||||||
let file = treeOrFile;
|
let file = treeOrFile;
|
||||||
|
|
||||||
|
if (!Store.files.length) {
|
||||||
|
Store.loading.tree = true;
|
||||||
|
}
|
||||||
|
|
||||||
return Service.getContent()
|
return Service.getContent()
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
|
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
|
||||||
|
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
|
||||||
|
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
|
||||||
|
Store.isInitialRoot = Store.isRoot;
|
||||||
|
}
|
||||||
|
|
||||||
Store.isTree = RepoHelper.isTree(data);
|
if (file && file.type === 'blob') {
|
||||||
if (!Store.isTree) {
|
|
||||||
if (!file) file = data;
|
if (!file) file = data;
|
||||||
Store.binary = data.binary;
|
Store.binary = data.binary;
|
||||||
|
|
||||||
|
@ -145,8 +109,7 @@ const RepoHelper = {
|
||||||
// file might be undefined
|
// file might be undefined
|
||||||
RepoHelper.setBinaryDataAsBase64(data);
|
RepoHelper.setBinaryDataAsBase64(data);
|
||||||
Store.setViewToPreview();
|
Store.setViewToPreview();
|
||||||
} else if (!Store.isPreviewView()) {
|
} else if (!Store.isPreviewView() && !data.render_error) {
|
||||||
if (!data.render_error) {
|
|
||||||
Service.getRaw(data.raw_path)
|
Service.getRaw(data.raw_path)
|
||||||
.then((rawResponse) => {
|
.then((rawResponse) => {
|
||||||
Store.blobRaw = rawResponse.data;
|
Store.blobRaw = rawResponse.data;
|
||||||
|
@ -154,29 +117,32 @@ const RepoHelper = {
|
||||||
RepoHelper.setFile(data, file);
|
RepoHelper.setFile(data, file);
|
||||||
}).catch(RepoHelper.loadingError);
|
}).catch(RepoHelper.loadingError);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Store.isPreviewView()) {
|
if (Store.isPreviewView()) {
|
||||||
RepoHelper.setFile(data, file);
|
RepoHelper.setFile(data, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the file tree is empty
|
|
||||||
if (Store.files.length === 0) {
|
|
||||||
const parentURL = Service.blobURLtoParentTree(Service.url);
|
|
||||||
Service.url = parentURL;
|
|
||||||
RepoHelper.getContent();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// it's a tree
|
Store.loading.tree = false;
|
||||||
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
|
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
|
||||||
file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
|
|
||||||
const newDirectory = RepoHelper.dataToListOfFiles(data);
|
if (emptyFiles) {
|
||||||
Store.addFilesToDirectory(file, Store.files, newDirectory);
|
Store.files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToDirectory(file, data);
|
||||||
|
|
||||||
Store.prevURL = Service.blobURLtoParentTree(Service.url);
|
Store.prevURL = Service.blobURLtoParentTree(Service.url);
|
||||||
}
|
}
|
||||||
}).catch(RepoHelper.loadingError);
|
}).catch(RepoHelper.loadingError);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addToDirectory(file, data) {
|
||||||
|
const tree = file || Store;
|
||||||
|
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
|
||||||
|
|
||||||
|
tree.files = files;
|
||||||
|
},
|
||||||
|
|
||||||
setFile(data, file) {
|
setFile(data, file) {
|
||||||
const newFile = data;
|
const newFile = data;
|
||||||
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
|
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
|
||||||
|
@ -190,57 +156,39 @@ const RepoHelper = {
|
||||||
Store.setActiveFiles(newFile);
|
Store.setActiveFiles(newFile);
|
||||||
},
|
},
|
||||||
|
|
||||||
serializeBlob(blob) {
|
serializeRepoEntity(type, entity, level = 0) {
|
||||||
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
|
|
||||||
simpleBlob.lastCommitMessage = blob.last_commit.message;
|
|
||||||
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
|
|
||||||
simpleBlob.loading = false;
|
|
||||||
|
|
||||||
return simpleBlob;
|
|
||||||
},
|
|
||||||
|
|
||||||
serializeTree(tree) {
|
|
||||||
return RepoHelper.serializeRepoEntity('tree', tree);
|
|
||||||
},
|
|
||||||
|
|
||||||
serializeSubmodule(submodule) {
|
|
||||||
return RepoHelper.serializeRepoEntity('submodule', submodule);
|
|
||||||
},
|
|
||||||
|
|
||||||
serializeRepoEntity(type, entity) {
|
|
||||||
const { url, name, icon, last_commit } = entity;
|
const { url, name, icon, last_commit } = entity;
|
||||||
const returnObj = {
|
|
||||||
|
return {
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
level,
|
||||||
icon: `fa-${icon}`,
|
icon: `fa-${icon}`,
|
||||||
level: 0,
|
files: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
opened: false,
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
lastCommit: last_commit ? {
|
||||||
|
url: `${Store.projectUrl}/commit/${last_commit.id}`,
|
||||||
|
message: last_commit.message,
|
||||||
|
updatedAt: last_commit.committed_date,
|
||||||
|
} : {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entity.last_commit) {
|
|
||||||
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
|
|
||||||
} else {
|
|
||||||
returnObj.lastCommitUrl = '';
|
|
||||||
}
|
|
||||||
return returnObj;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollTabsRight() {
|
scrollTabsRight() {
|
||||||
// wait for the transition. 0.1 seconds.
|
|
||||||
setTimeout(() => {
|
|
||||||
const tabs = document.getElementById('tabs');
|
const tabs = document.getElementById('tabs');
|
||||||
if (!tabs) return;
|
if (!tabs) return;
|
||||||
tabs.scrollLeft = tabs.scrollWidth;
|
tabs.scrollLeft = tabs.scrollWidth;
|
||||||
}, 200);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dataToListOfFiles(data) {
|
dataToListOfFiles(data, level) {
|
||||||
const { blobs, trees, submodules } = data;
|
const { blobs, trees, submodules } = data;
|
||||||
return [
|
return [
|
||||||
...blobs.map(blob => RepoHelper.serializeBlob(blob)),
|
...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
|
||||||
...trees.map(tree => RepoHelper.serializeTree(tree)),
|
...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
|
||||||
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
|
...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
|
||||||
import Service from './services/repo_service';
|
import Service from './services/repo_service';
|
||||||
import Store from './stores/repo_store';
|
import Store from './stores/repo_store';
|
||||||
import Repo from './components/repo.vue';
|
import Repo from './components/repo.vue';
|
||||||
|
@ -33,6 +34,8 @@ function setInitialStore(data) {
|
||||||
Store.onTopOfBranch = data.onTopOfBranch;
|
Store.onTopOfBranch = data.onTopOfBranch;
|
||||||
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
|
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
|
||||||
Store.customBranchURL = decodeURIComponent(data.blobUrl);
|
Store.customBranchURL = decodeURIComponent(data.blobUrl);
|
||||||
|
Store.isRoot = convertPermissionToBoolean(data.root);
|
||||||
|
Store.isInitialRoot = convertPermissionToBoolean(data.root);
|
||||||
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
|
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
|
||||||
Store.checkIsCommitable();
|
Store.checkIsCommitable();
|
||||||
Store.setBranchHash();
|
Store.setBranchHash();
|
||||||
|
|
|
@ -2,14 +2,13 @@ import Helper from '../helpers/repo_helper';
|
||||||
import Service from '../services/repo_service';
|
import Service from '../services/repo_service';
|
||||||
|
|
||||||
const RepoStore = {
|
const RepoStore = {
|
||||||
monaco: {},
|
|
||||||
monacoLoading: false,
|
monacoLoading: false,
|
||||||
service: '',
|
service: '',
|
||||||
canCommit: false,
|
canCommit: false,
|
||||||
onTopOfBranch: false,
|
onTopOfBranch: false,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
isTree: false,
|
isRoot: null,
|
||||||
isRoot: false,
|
isInitialRoot: null,
|
||||||
prevURL: '',
|
prevURL: '',
|
||||||
projectId: '',
|
projectId: '',
|
||||||
projectName: '',
|
projectName: '',
|
||||||
|
@ -39,23 +38,11 @@ const RepoStore = {
|
||||||
newMrTemplateUrl: '',
|
newMrTemplateUrl: '',
|
||||||
branchChanged: false,
|
branchChanged: false,
|
||||||
commitMessage: '',
|
commitMessage: '',
|
||||||
binaryTypes: {
|
|
||||||
png: false,
|
|
||||||
md: false,
|
|
||||||
svg: false,
|
|
||||||
unknown: false,
|
|
||||||
},
|
|
||||||
loading: {
|
loading: {
|
||||||
tree: false,
|
tree: false,
|
||||||
blob: false,
|
blob: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
resetBinaryTypes() {
|
|
||||||
Object.keys(RepoStore.binaryTypes).forEach((key) => {
|
|
||||||
RepoStore.binaryTypes[key] = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setBranchHash() {
|
setBranchHash() {
|
||||||
return Service.getBranch()
|
return Service.getBranch()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -72,10 +59,6 @@ const RepoStore = {
|
||||||
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
|
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
|
||||||
},
|
},
|
||||||
|
|
||||||
addFilesToDirectory(inDirectory, currentList, newList) {
|
|
||||||
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleRawPreview() {
|
toggleRawPreview() {
|
||||||
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
|
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
|
||||||
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
|
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
|
||||||
|
@ -129,30 +112,6 @@ const RepoStore = {
|
||||||
RepoStore.activeFileLabel = 'Display source';
|
RepoStore.activeFileLabel = 'Display source';
|
||||||
},
|
},
|
||||||
|
|
||||||
removeChildFilesOfTree(tree) {
|
|
||||||
let foundTree = false;
|
|
||||||
const treeToClose = tree;
|
|
||||||
let canStopSearching = false;
|
|
||||||
RepoStore.files = RepoStore.files.filter((file) => {
|
|
||||||
const isItTheTreeWeWant = file.url === treeToClose.url;
|
|
||||||
// if it's the next tree
|
|
||||||
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
|
|
||||||
canStopSearching = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (canStopSearching) return true;
|
|
||||||
|
|
||||||
if (isItTheTreeWeWant) foundTree = true;
|
|
||||||
|
|
||||||
if (foundTree) return file.level <= treeToClose.level;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
treeToClose.opened = false;
|
|
||||||
treeToClose.icon = 'fa-folder';
|
|
||||||
return treeToClose;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeFromOpenedFiles(file) {
|
removeFromOpenedFiles(file) {
|
||||||
if (file.type === 'tree') return;
|
if (file.type === 'tree') return;
|
||||||
let foundIndex;
|
let foundIndex;
|
||||||
|
@ -186,6 +145,7 @@ const RepoStore = {
|
||||||
if (openedFilesAlreadyExists) return;
|
if (openedFilesAlreadyExists) return;
|
||||||
|
|
||||||
openFile.changed = false;
|
openFile.changed = false;
|
||||||
|
openFile.active = true;
|
||||||
RepoStore.openedFiles.push(openFile);
|
RepoStore.openedFiles.push(openFile);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
@import "framework/animations";
|
@import "framework/animations";
|
||||||
@import "framework/avatar";
|
@import "framework/avatar";
|
||||||
@import "framework/asciidoctor";
|
@import "framework/asciidoctor";
|
||||||
|
@import "framework/banner";
|
||||||
@import "framework/blocks";
|
@import "framework/blocks";
|
||||||
@import "framework/buttons";
|
@import "framework/buttons";
|
||||||
@import "framework/badges";
|
@import "framework/badges";
|
||||||
|
|
|
@ -198,6 +198,13 @@ a {
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.animation-container-right {
|
||||||
|
.skeleton-line-2 {
|
||||||
|
left: 0;
|
||||||
|
right: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
.banner-callout {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.banner-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.dismiss-icon {
|
||||||
|
color: $gl-text-color;
|
||||||
|
font-size: $gl-font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-graphic {
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-non-empty-state {
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,10 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.file-holder-bottom-radius {
|
||||||
|
border-radius: 0 0 $border-radius-small $border-radius-small;
|
||||||
|
}
|
||||||
|
|
||||||
&.readme-holder {
|
&.readme-holder {
|
||||||
margin: $gl-padding 0;
|
margin: $gl-padding 0;
|
||||||
|
|
||||||
|
|
|
@ -281,6 +281,57 @@ ul.indent-list {
|
||||||
|
|
||||||
|
|
||||||
// Specific styles for tree list
|
// Specific styles for tree list
|
||||||
|
@keyframes spin-avatar {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups-list-tree-container {
|
||||||
|
.has-no-search-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: $gl-padding;
|
||||||
|
font-style: italic;
|
||||||
|
color: $well-light-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .group-list-tree > .group-row.has-children:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-list-tree .avatar-container.content-loading {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> a,
|
||||||
|
> a .avatar {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a .avatar {
|
||||||
|
border: 2px solid $white-normal;
|
||||||
|
|
||||||
|
&.identicon {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px outset $kdb-border;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin-avatar 3s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.group-list-tree {
|
.group-list-tree {
|
||||||
.folder-toggle-wrap {
|
.folder-toggle-wrap {
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -293,7 +344,7 @@ ul.indent-list {
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-caret,
|
.folder-caret,
|
||||||
.folder-icon {
|
.item-type-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,11 +352,11 @@ ul.indent-list {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-icon {
|
.item-type-icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .group-row:not(.has-subgroups) {
|
> .group-row:not(.has-children) {
|
||||||
.folder-caret .fa {
|
.folder-caret .fa {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -351,12 +402,23 @@ ul.indent-list {
|
||||||
top: 30px;
|
top: 30px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.being-removed {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-row {
|
.group-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
|
||||||
|
&.has-children {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: 1px solid $white-normal;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
.group-row-contents:not(:hover) {
|
.group-row-contents:not(:hover) {
|
||||||
|
@ -379,6 +441,25 @@ ul.indent-list {
|
||||||
.avatar-container > a {
|
.avatar-container > a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.has-more-items {
|
||||||
|
display: block;
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.group-list-tree {
|
||||||
|
li.group-row {
|
||||||
|
&.has-description {
|
||||||
|
.title {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
line-height: $list-text-height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -466,7 +466,7 @@ $new-sidebar-collapsed-width: 50px;
|
||||||
|
|
||||||
@media (max-width: $screen-xs-max) {
|
@media (max-width: $screen-xs-max) {
|
||||||
+ .breadcrumbs-links {
|
+ .breadcrumbs-links {
|
||||||
padding-left: 17px;
|
padding-left: $gl-padding;
|
||||||
border-left: 1px solid $gl-text-color-quaternary;
|
border-left: 1px solid $gl-text-color-quaternary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,6 +233,7 @@ $container-text-max-width: 540px;
|
||||||
$gl-avatar-size: 40px;
|
$gl-avatar-size: 40px;
|
||||||
$error-exclamation-point: $red-500;
|
$error-exclamation-point: $red-500;
|
||||||
$border-radius-default: 4px;
|
$border-radius-default: 4px;
|
||||||
|
$border-radius-small: 2px;
|
||||||
$settings-icon-size: 18px;
|
$settings-icon-size: 18px;
|
||||||
$provider-btn-not-active-color: $blue-500;
|
$provider-btn-not-active-color: $blue-500;
|
||||||
$link-underline-blue: $blue-500;
|
$link-underline-blue: $blue-500;
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-block {
|
.alert-block {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
border-right: 1px solid $border-color;
|
border-right: 1px solid $border-color;
|
||||||
border-left: 1px solid $border-color;
|
border-left: 1px solid $border-color;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: 2px;
|
border-radius: $border-radius-small $border-radius-small 0 0;
|
||||||
background: $gray-normal;
|
background: $gray-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,117 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.groups-header {
|
.group-nav-container .nav-controls {
|
||||||
@media (min-width: $screen-sm-min) {
|
display: flex;
|
||||||
.nav-links {
|
align-items: flex-start;
|
||||||
width: 35%;
|
padding: $gl-padding-top 0;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
|
||||||
|
.group-filter-form {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-controls {
|
.dropdown-menu-align-right {
|
||||||
width: 65%;
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-project-subgroup {
|
||||||
|
.dropdown-primary {
|
||||||
|
min-width: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
.dropdown-btn-icon {
|
||||||
|
pointer-events: none;
|
||||||
|
color: inherit;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
min-width: 280px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:not(.divider) {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.droplab-item-selected {
|
||||||
|
.icon-container {
|
||||||
|
.list-item-checkmark {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 8px 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $gray-darker;
|
||||||
|
color: $theme-gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
float: left;
|
||||||
|
padding-left: 6px;
|
||||||
|
|
||||||
|
.list-item-checkmark {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
font-weight: $gl-font-weight-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $screen-sm-max) {
|
||||||
|
&,
|
||||||
|
.dropdown,
|
||||||
|
.dropdown .dropdown-toggle,
|
||||||
|
.btn-new {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-filter-form,
|
||||||
|
.dropdown {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-filter-form,
|
||||||
|
.dropdown .dropdown-toggle,
|
||||||
|
.btn-new {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown .dropdown-toggle .fa-chevron-down {
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-project-subgroup {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.dropdown-primary {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
width: 100%;
|
||||||
|
max-width: inherit;
|
||||||
|
min-width: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,12 +72,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
margin-left: auto;
|
||||||
|
// Set height to match title height
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
// Border around images in issue and MR descriptions.
|
// Border around images in issue and MR descriptions.
|
||||||
.description img:not(.emoji) {
|
.description img:not(.emoji) {
|
||||||
border: 1px solid $white-normal;
|
border: 1px solid $white-normal;
|
||||||
|
|
|
@ -531,14 +531,13 @@ ul.notes {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
|
fill: $gray-darkest;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
@ -566,6 +565,7 @@ ul.notes {
|
||||||
|
|
||||||
.link-highlight {
|
.link-highlight {
|
||||||
color: $gl-link-color;
|
color: $gl-link-color;
|
||||||
|
fill: $gl-link-color;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: $gl-link-color;
|
fill: $gl-link-color;
|
||||||
|
|
|
@ -153,28 +153,13 @@
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
animation: swipeRightAppear ease-in 0.1s;
|
position: relative;
|
||||||
animation-iteration-count: 1;
|
|
||||||
transform-origin: 0% 50%;
|
|
||||||
list-style-type: none;
|
|
||||||
background: $gray-normal;
|
background: $gray-normal;
|
||||||
display: inline-block;
|
|
||||||
padding: #{$gl-padding / 2} $gl-padding;
|
padding: #{$gl-padding / 2} $gl-padding;
|
||||||
border-right: 1px solid $white-dark;
|
border-right: 1px solid $white-dark;
|
||||||
border-bottom: 1px solid $white-dark;
|
border-bottom: 1px solid $white-dark;
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.remove {
|
|
||||||
animation: swipeRightDissapear ease-in 0.1s;
|
|
||||||
animation-iteration-count: 1;
|
|
||||||
transform-origin: 0% 50%;
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: $white-light;
|
background: $white-light;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
@ -182,17 +167,21 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@include str-truncated(100px);
|
@include str-truncated(100px);
|
||||||
color: $black;
|
color: $gl-text-color;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
|
||||||
&.close {
|
|
||||||
width: auto;
|
|
||||||
font-size: 15px;
|
|
||||||
opacity: 1;
|
|
||||||
margin-right: -6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
font-size: $gl-font-size;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-icon:hover {
|
.close-icon:hover {
|
||||||
|
@ -201,9 +190,6 @@
|
||||||
|
|
||||||
.close-icon,
|
.close-icon,
|
||||||
.unsaved-icon {
|
.unsaved-icon {
|
||||||
float: right;
|
|
||||||
margin-top: 3px;
|
|
||||||
margin-left: 15px;
|
|
||||||
color: $gray-darkest;
|
color: $gray-darkest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,9 +208,7 @@
|
||||||
|
|
||||||
#repo-file-buttons {
|
#repo-file-buttons {
|
||||||
background-color: $white-light;
|
background-color: $white-light;
|
||||||
border-bottom: 1px solid $white-normal;
|
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
position: relative;
|
|
||||||
border-top: 1px solid $white-normal;
|
border-top: 1px solid $white-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,37 +271,23 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.table {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
animation: fadein 0.5s;
|
.repo-file-options {
|
||||||
cursor: pointer;
|
padding: 2px 16px;
|
||||||
|
|
||||||
&.repo-file-options td {
|
|
||||||
padding: 0;
|
|
||||||
border-top: none;
|
|
||||||
background: $gray-light;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: inline-block;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: $gl-font-weight-bold;
|
|
||||||
color: $gray-darkest;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 2px 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-icon {
|
.file-icon {
|
||||||
|
@ -329,11 +299,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@include str-truncated(250px);
|
@include str-truncated(250px);
|
||||||
color: $almost-black;
|
color: $almost-black;
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
module GroupTree
|
||||||
|
def render_group_tree(groups)
|
||||||
|
@groups = if params[:filter].present?
|
||||||
|
Gitlab::GroupHierarchy.new(groups.search(params[:filter]))
|
||||||
|
.base_and_ancestors
|
||||||
|
else
|
||||||
|
# Only show root groups if no parent-id is given
|
||||||
|
groups.where(parent_id: params[:parent_id])
|
||||||
|
end
|
||||||
|
@groups = @groups.with_selects_for_list(archived: params[:archived])
|
||||||
|
.sort(@sort = params[:sort])
|
||||||
|
.page(params[:page])
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.json do
|
||||||
|
serializer = GroupChildSerializer.new(current_user: current_user)
|
||||||
|
.with_pagination(request, response)
|
||||||
|
serializer.expand_hierarchy if params[:filter].present?
|
||||||
|
render json: serializer.represent(@groups)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,33 +1,8 @@
|
||||||
class Dashboard::GroupsController < Dashboard::ApplicationController
|
class Dashboard::GroupsController < Dashboard::ApplicationController
|
||||||
|
include GroupTree
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@sort = params[:sort] || 'created_desc'
|
groups = GroupsFinder.new(current_user, all_available: false).execute
|
||||||
|
render_group_tree(groups)
|
||||||
@groups =
|
|
||||||
if params[:parent_id] && Group.supports_nested_groups?
|
|
||||||
parent = Group.find_by(id: params[:parent_id])
|
|
||||||
|
|
||||||
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)
|
|
||||||
@groups = @groups.page(params[:page])
|
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.html
|
|
||||||
format.json do
|
|
||||||
render json: GroupSerializer
|
|
||||||
.new(current_user: @current_user)
|
|
||||||
.with_pagination(request, response)
|
|
||||||
.represent(@groups)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
class Explore::GroupsController < Explore::ApplicationController
|
class Explore::GroupsController < Explore::ApplicationController
|
||||||
def index
|
include GroupTree
|
||||||
@groups = GroupsFinder.new(current_user).execute
|
|
||||||
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
|
|
||||||
@groups = @groups.sort(@sort = params[:sort])
|
|
||||||
@groups = @groups.page(params[:page])
|
|
||||||
|
|
||||||
respond_to do |format|
|
def index
|
||||||
format.html
|
render_group_tree GroupsFinder.new(current_user).execute
|
||||||
format.json do
|
|
||||||
render json: {
|
|
||||||
html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups })
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
module Groups
|
||||||
|
class ChildrenController < Groups::ApplicationController
|
||||||
|
before_action :group
|
||||||
|
|
||||||
|
def index
|
||||||
|
parent = if params[:parent_id].present?
|
||||||
|
GroupFinder.new(current_user).execute(id: params[:parent_id])
|
||||||
|
else
|
||||||
|
@group
|
||||||
|
end
|
||||||
|
|
||||||
|
if parent.nil?
|
||||||
|
render_404
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
setup_children(parent)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
serializer = GroupChildSerializer
|
||||||
|
.new(current_user: current_user)
|
||||||
|
.with_pagination(request, response)
|
||||||
|
serializer.expand_hierarchy(parent) if params[:filter].present?
|
||||||
|
render json: serializer.represent(@children)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def setup_children(parent)
|
||||||
|
@children = GroupDescendantsFinder.new(current_user: current_user,
|
||||||
|
parent_group: parent,
|
||||||
|
params: params).execute
|
||||||
|
@children = @children.page(params[:page])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -46,15 +46,11 @@ class GroupsController < Groups::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
setup_projects
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html do
|
||||||
|
@has_children = GroupDescendantsFinder.new(current_user: current_user,
|
||||||
format.json do
|
parent_group: @group,
|
||||||
render json: {
|
params: params).has_children?
|
||||||
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
|
@ -64,13 +60,6 @@ class GroupsController < Groups::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def subgroups
|
|
||||||
return not_found unless Group.supports_nested_groups?
|
|
||||||
|
|
||||||
@nested_groups = GroupsFinder.new(current_user, parent: group).execute
|
|
||||||
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
|
@ -107,20 +96,6 @@ class GroupsController < Groups::ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def setup_projects
|
|
||||||
set_non_archived_param
|
|
||||||
params[:sort] ||= 'latest_activity_desc'
|
|
||||||
@sort = params[:sort]
|
|
||||||
|
|
||||||
options = {}
|
|
||||||
options[:only_owned] = true if params[:shared] == '0'
|
|
||||||
options[:only_shared] = true if params[:shared] == '1'
|
|
||||||
|
|
||||||
@projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
|
|
||||||
@projects = @projects.includes(:namespace)
|
|
||||||
@projects = @projects.page(params[:page]) if params[:name].blank?
|
|
||||||
end
|
|
||||||
|
|
||||||
def authorize_create_group!
|
def authorize_create_group!
|
||||||
allowed = if params[:parent_id].present?
|
allowed = if params[:parent_id].present?
|
||||||
parent = Group.find_by(id: params[:parent_id])
|
parent = Group.find_by(id: params[:parent_id])
|
||||||
|
|
|
@ -36,6 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
|
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
|
||||||
|
response.header['is-root'] = @path.empty?
|
||||||
|
|
||||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
|
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
|
||||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
||||||
|
|
|
@ -126,7 +126,7 @@ class ProjectsController < Projects::ApplicationController
|
||||||
return access_denied! unless can?(current_user, :remove_project, @project)
|
return access_denied! unless can?(current_user, :remove_project, @project)
|
||||||
|
|
||||||
::Projects::DestroyService.new(@project, current_user, {}).async_execute
|
::Projects::DestroyService.new(@project, current_user, {}).async_execute
|
||||||
flash[:alert] = _("Project '%{project_name}' will be deleted.") % { project_name: @project.name_with_namespace }
|
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
|
||||||
|
|
||||||
redirect_to dashboard_projects_path, status: 302
|
redirect_to dashboard_projects_path, status: 302
|
||||||
rescue Projects::DestroyService::DestroyError => ex
|
rescue Projects::DestroyService::DestroyError => ex
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
# GroupDescendantsFinder
|
||||||
|
#
|
||||||
|
# Used to find and filter all subgroups and projects of a passed parent group
|
||||||
|
# visible to a specified user.
|
||||||
|
#
|
||||||
|
# When passing a `filter` param, the search is performed over all nested levels
|
||||||
|
# of the `parent_group`. All ancestors for a search result are loaded
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# current_user: The user for which the children should be visible
|
||||||
|
# parent_group: The group to find children of
|
||||||
|
# params:
|
||||||
|
# Supports all params that the `ProjectsFinder` and `GroupProjectsFinder`
|
||||||
|
# support.
|
||||||
|
#
|
||||||
|
# filter: string - is aliased to `search` for consistency with the frontend
|
||||||
|
# archived: string - `only` or `true`.
|
||||||
|
# `non_archived` is passed to the `ProjectFinder`s if none
|
||||||
|
# was given.
|
||||||
|
class GroupDescendantsFinder
|
||||||
|
attr_reader :current_user, :parent_group, :params
|
||||||
|
|
||||||
|
def initialize(current_user: nil, parent_group:, params: {})
|
||||||
|
@current_user = current_user
|
||||||
|
@parent_group = parent_group
|
||||||
|
@params = params.reverse_merge(non_archived: params[:archived].blank?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
# The children array might be extended with the ancestors of projects when
|
||||||
|
# filtering. In that case, take the maximum so the array does not get limited
|
||||||
|
# Otherwise, allow paginating through all results
|
||||||
|
#
|
||||||
|
all_required_elements = children
|
||||||
|
all_required_elements |= ancestors_for_projects if params[:filter]
|
||||||
|
total_count = [all_required_elements.size, paginator.total_count].max
|
||||||
|
|
||||||
|
Kaminari.paginate_array(all_required_elements, total_count: total_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_children?
|
||||||
|
projects.any? || subgroups.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def children
|
||||||
|
@children ||= paginator.paginate(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginator
|
||||||
|
@paginator ||= Gitlab::MultiCollectionPaginator.new(subgroups, projects,
|
||||||
|
per_page: params[:per_page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_child_groups
|
||||||
|
GroupsFinder.new(current_user,
|
||||||
|
parent: parent_group,
|
||||||
|
all_available: true).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_visible_descendant_groups
|
||||||
|
groups_table = Group.arel_table
|
||||||
|
visible_to_user = groups_table[:visibility_level]
|
||||||
|
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
|
||||||
|
if current_user
|
||||||
|
authorized_groups = GroupsFinder.new(current_user,
|
||||||
|
all_available: false)
|
||||||
|
.execute.as('authorized')
|
||||||
|
authorized_to_user = groups_table.project(1).from(authorized_groups)
|
||||||
|
.where(authorized_groups[:id].eq(groups_table[:id]))
|
||||||
|
.exists
|
||||||
|
visible_to_user = visible_to_user.or(authorized_to_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
hierarchy_for_parent
|
||||||
|
.descendants
|
||||||
|
.where(visible_to_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subgroups_matching_filter
|
||||||
|
all_visible_descendant_groups
|
||||||
|
.search(params[:filter])
|
||||||
|
end
|
||||||
|
|
||||||
|
# When filtering we want all to preload all the ancestors upto the specified
|
||||||
|
# parent group.
|
||||||
|
#
|
||||||
|
# - root
|
||||||
|
# - subgroup
|
||||||
|
# - nested-group
|
||||||
|
# - project
|
||||||
|
#
|
||||||
|
# So when searching 'project', on the 'subgroup' page we want to preload
|
||||||
|
# 'nested-group' but not 'subgroup' or 'root'
|
||||||
|
def ancestors_for_groups(base_for_ancestors)
|
||||||
|
Gitlab::GroupHierarchy.new(base_for_ancestors)
|
||||||
|
.base_and_ancestors(upto: parent_group.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ancestors_for_projects
|
||||||
|
projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
|
||||||
|
groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
|
||||||
|
ancestors_for_groups(groups_to_load_ancestors_of)
|
||||||
|
.with_selects_for_list(archived: params[:archived])
|
||||||
|
end
|
||||||
|
|
||||||
|
def subgroups
|
||||||
|
return Group.none unless Group.supports_nested_groups?
|
||||||
|
|
||||||
|
# When filtering subgroups, we want to find all matches withing the tree of
|
||||||
|
# descendants to show to the user
|
||||||
|
groups = if params[:filter]
|
||||||
|
ancestors_for_groups(subgroups_matching_filter)
|
||||||
|
else
|
||||||
|
direct_child_groups
|
||||||
|
end
|
||||||
|
groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_child_projects
|
||||||
|
GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
|
||||||
|
.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
# Finds all projects nested under `parent_group` or any of its descendant
|
||||||
|
# groups
|
||||||
|
def projects_matching_filter
|
||||||
|
projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id))
|
||||||
|
params_with_search = params.merge(search: params[:filter])
|
||||||
|
|
||||||
|
ProjectsFinder.new(params: params_with_search,
|
||||||
|
current_user: current_user,
|
||||||
|
project_ids_relation: projects_nested_in_group).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def projects
|
||||||
|
projects = if params[:filter]
|
||||||
|
projects_matching_filter
|
||||||
|
else
|
||||||
|
direct_child_projects
|
||||||
|
end
|
||||||
|
projects.with_route.order_by(sort)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort
|
||||||
|
params.fetch(:sort, 'id_asc')
|
||||||
|
end
|
||||||
|
|
||||||
|
def hierarchy_for_parent
|
||||||
|
@hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id))
|
||||||
|
end
|
||||||
|
end
|
|
@ -34,7 +34,6 @@ class GroupProjectsFinder < ProjectsFinder
|
||||||
else
|
else
|
||||||
collection_without_user
|
collection_without_user
|
||||||
end
|
end
|
||||||
|
|
||||||
union(projects)
|
union(projects)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,34 @@ module ApplicationSettingsHelper
|
||||||
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
|
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def circuitbreaker_failure_count_help_text
|
||||||
|
health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path)
|
||||||
|
api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health"))
|
||||||
|
message = _("The number of failures of after which GitLab will completely "\
|
||||||
|
"prevent access to the storage. The number of failures can be "\
|
||||||
|
"reset in the admin interface: %{link_to_health_page} or using "\
|
||||||
|
"the %{api_documentation_link}.")
|
||||||
|
message = message % { link_to_health_page: health_link, api_documentation_link: api_link }
|
||||||
|
|
||||||
|
message.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def circuitbreaker_failure_wait_time_help_text
|
||||||
|
_("When access to a storage fails. GitLab will prevent access to the "\
|
||||||
|
"storage for the time specified here. This allows the filesystem to "\
|
||||||
|
"recover. Repositories on failing shards are temporarly unavailable")
|
||||||
|
end
|
||||||
|
|
||||||
|
def circuitbreaker_failure_reset_time_help_text
|
||||||
|
_("The time in seconds GitLab will keep failure information. When no "\
|
||||||
|
"failures occur during this time, information about the mount is reset.")
|
||||||
|
end
|
||||||
|
|
||||||
|
def circuitbreaker_storage_timeout_help_text
|
||||||
|
_("The time in seconds GitLab will try to access storage. After this time a "\
|
||||||
|
"timeout error will be raised.")
|
||||||
|
end
|
||||||
|
|
||||||
def visible_attributes
|
def visible_attributes
|
||||||
[
|
[
|
||||||
:admin_notification_email,
|
:admin_notification_email,
|
||||||
|
@ -116,6 +144,10 @@ module ApplicationSettingsHelper
|
||||||
:akismet_api_key,
|
:akismet_api_key,
|
||||||
:akismet_enabled,
|
:akismet_enabled,
|
||||||
:auto_devops_enabled,
|
:auto_devops_enabled,
|
||||||
|
:circuitbreaker_failure_count_threshold,
|
||||||
|
:circuitbreaker_failure_reset_time,
|
||||||
|
:circuitbreaker_failure_wait_time,
|
||||||
|
:circuitbreaker_storage_timeout,
|
||||||
:clientside_sentry_dsn,
|
:clientside_sentry_dsn,
|
||||||
:clientside_sentry_enabled,
|
:clientside_sentry_enabled,
|
||||||
:container_registry_token_expire_delay,
|
:container_registry_token_expire_delay,
|
||||||
|
|
|
@ -42,6 +42,17 @@ module SortingHelper
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def groups_sort_options_hash
|
||||||
|
options = {
|
||||||
|
sort_value_recently_created => sort_title_recently_created,
|
||||||
|
sort_value_oldest_created => sort_title_oldest_created,
|
||||||
|
sort_value_recently_updated => sort_title_recently_updated,
|
||||||
|
sort_value_oldest_updated => sort_title_oldest_updated
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
|
|
||||||
def member_sort_options_hash
|
def member_sort_options_hash
|
||||||
{
|
{
|
||||||
sort_value_access_level_asc => sort_title_access_level_asc,
|
sort_value_access_level_asc => sort_title_access_level_asc,
|
||||||
|
|
|
@ -33,6 +33,8 @@ class ApplicationSetting < ActiveRecord::Base
|
||||||
|
|
||||||
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
|
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
|
||||||
|
|
||||||
|
default_value_for :id, 1
|
||||||
|
|
||||||
validates :uuid, presence: true
|
validates :uuid, presence: true
|
||||||
|
|
||||||
validates :session_expire_delay,
|
validates :session_expire_delay,
|
||||||
|
@ -151,6 +153,13 @@ class ApplicationSetting < ActiveRecord::Base
|
||||||
presence: true,
|
presence: true,
|
||||||
numericality: { greater_than_or_equal_to: 0 }
|
numericality: { greater_than_or_equal_to: 0 }
|
||||||
|
|
||||||
|
validates :circuitbreaker_failure_count_threshold,
|
||||||
|
:circuitbreaker_failure_wait_time,
|
||||||
|
:circuitbreaker_failure_reset_time,
|
||||||
|
:circuitbreaker_storage_timeout,
|
||||||
|
presence: true,
|
||||||
|
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||||
|
|
||||||
SUPPORTED_KEY_TYPES.each do |type|
|
SUPPORTED_KEY_TYPES.each do |type|
|
||||||
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
module GroupDescendant
|
||||||
|
# Returns the hierarchy of a project or group in the from of a hash upto a
|
||||||
|
# given top.
|
||||||
|
#
|
||||||
|
# > project.hierarchy
|
||||||
|
# => { parent_group => { child_group => project } }
|
||||||
|
def hierarchy(hierarchy_top = nil, preloaded = nil)
|
||||||
|
preloaded ||= ancestors_upto(hierarchy_top)
|
||||||
|
expand_hierarchy_for_child(self, self, hierarchy_top, preloaded)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Merges all hierarchies of the given groups or projects into an array of
|
||||||
|
# hashes. All ancestors need to be loaded into the given `descendants` to avoid
|
||||||
|
# queries down the line.
|
||||||
|
#
|
||||||
|
# > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent])
|
||||||
|
# => { parent => [{ child_group => project}, child_group2] }
|
||||||
|
def self.build_hierarchy(descendants, hierarchy_top = nil)
|
||||||
|
descendants = Array.wrap(descendants).uniq
|
||||||
|
return [] if descendants.empty?
|
||||||
|
|
||||||
|
unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) }
|
||||||
|
raise ArgumentError.new('element is not a hierarchy')
|
||||||
|
end
|
||||||
|
|
||||||
|
all_hierarchies = descendants.map do |descendant|
|
||||||
|
descendant.hierarchy(hierarchy_top, descendants)
|
||||||
|
end
|
||||||
|
|
||||||
|
Gitlab::Utils::MergeHash.merge(all_hierarchies)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded)
|
||||||
|
parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id
|
||||||
|
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
|
||||||
|
|
||||||
|
if parent.nil? && !child.parent_id.nil?
|
||||||
|
raise ArgumentError.new('parent was not preloaded')
|
||||||
|
end
|
||||||
|
|
||||||
|
if parent.nil? && hierarchy_top.present?
|
||||||
|
raise ArgumentError.new('specified top is not part of the tree')
|
||||||
|
end
|
||||||
|
|
||||||
|
if parent && parent != hierarchy_top
|
||||||
|
expand_hierarchy_for_child(parent,
|
||||||
|
{ parent => hierarchy },
|
||||||
|
hierarchy_top,
|
||||||
|
preloaded)
|
||||||
|
else
|
||||||
|
hierarchy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,72 @@
|
||||||
|
module LoadedInGroupList
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def with_counts(archived:)
|
||||||
|
selects_including_counts = [
|
||||||
|
'namespaces.*',
|
||||||
|
"(#{project_count_sql(archived).to_sql}) AS preloaded_project_count",
|
||||||
|
"(#{member_count_sql.to_sql}) AS preloaded_member_count",
|
||||||
|
"(#{subgroup_count_sql.to_sql}) AS preloaded_subgroup_count"
|
||||||
|
]
|
||||||
|
|
||||||
|
select(selects_including_counts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_selects_for_list(archived: nil)
|
||||||
|
with_route.with_counts(archived: archived)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def project_count_sql(archived = nil)
|
||||||
|
projects = Project.arel_table
|
||||||
|
namespaces = Namespace.arel_table
|
||||||
|
|
||||||
|
base_count = projects.project(Arel.star.count.as('preloaded_project_count'))
|
||||||
|
.where(projects[:namespace_id].eq(namespaces[:id]))
|
||||||
|
if archived == 'only'
|
||||||
|
base_count.where(projects[:archived].eq(true))
|
||||||
|
elsif Gitlab::Utils.to_boolean(archived)
|
||||||
|
base_count
|
||||||
|
else
|
||||||
|
base_count.where(projects[:archived].not_eq(true))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def subgroup_count_sql
|
||||||
|
namespaces = Namespace.arel_table
|
||||||
|
children = namespaces.alias('children')
|
||||||
|
|
||||||
|
namespaces.project(Arel.star.count.as('preloaded_subgroup_count'))
|
||||||
|
.from(children)
|
||||||
|
.where(children[:parent_id].eq(namespaces[:id]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def member_count_sql
|
||||||
|
members = Member.arel_table
|
||||||
|
namespaces = Namespace.arel_table
|
||||||
|
|
||||||
|
members.project(Arel.star.count.as('preloaded_member_count'))
|
||||||
|
.where(members[:source_type].eq(Namespace.name))
|
||||||
|
.where(members[:source_id].eq(namespaces[:id]))
|
||||||
|
.where(members[:requested_at].eq(nil))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def children_count
|
||||||
|
@children_count ||= project_count + subgroup_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_count
|
||||||
|
@project_count ||= try(:preloaded_project_count) || projects.non_archived.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def subgroup_count
|
||||||
|
@subgroup_count ||= try(:preloaded_subgroup_count) || children.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def member_count
|
||||||
|
@member_count ||= try(:preloaded_member_count) || users.count
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,6 +6,8 @@ class Group < Namespace
|
||||||
include Avatarable
|
include Avatarable
|
||||||
include Referable
|
include Referable
|
||||||
include SelectForProjectAuthorization
|
include SelectForProjectAuthorization
|
||||||
|
include LoadedInGroupList
|
||||||
|
include GroupDescendant
|
||||||
|
|
||||||
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
|
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
|
||||||
alias_method :members, :group_members
|
alias_method :members, :group_members
|
||||||
|
|
|
@ -162,6 +162,13 @@ class Namespace < ActiveRecord::Base
|
||||||
.base_and_ancestors
|
.base_and_ancestors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# returns all ancestors upto but excluding the the given namespace
|
||||||
|
# when no namespace is given, all ancestors upto the top are returned
|
||||||
|
def ancestors_upto(top = nil)
|
||||||
|
Gitlab::GroupHierarchy.new(self.class.where(id: id))
|
||||||
|
.ancestors(upto: top)
|
||||||
|
end
|
||||||
|
|
||||||
def self_and_ancestors
|
def self_and_ancestors
|
||||||
return self.class.where(id: id) unless parent_id
|
return self.class.where(id: id) unless parent_id
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Project < ActiveRecord::Base
|
||||||
include ProjectFeaturesCompatibility
|
include ProjectFeaturesCompatibility
|
||||||
include SelectForProjectAuthorization
|
include SelectForProjectAuthorization
|
||||||
include Routable
|
include Routable
|
||||||
|
include GroupDescendant
|
||||||
|
|
||||||
extend Gitlab::ConfigHelper
|
extend Gitlab::ConfigHelper
|
||||||
extend Gitlab::CurrentSettings
|
extend Gitlab::CurrentSettings
|
||||||
|
@ -81,6 +82,8 @@ class Project < ActiveRecord::Base
|
||||||
belongs_to :creator, class_name: 'User'
|
belongs_to :creator, class_name: 'User'
|
||||||
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
|
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
|
||||||
belongs_to :namespace
|
belongs_to :namespace
|
||||||
|
alias_method :parent, :namespace
|
||||||
|
alias_attribute :parent_id, :namespace_id
|
||||||
|
|
||||||
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
|
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
|
||||||
has_many :boards, before_add: :validate_board_limit
|
has_many :boards, before_add: :validate_board_limit
|
||||||
|
@ -479,6 +482,13 @@ class Project < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# returns all ancestor-groups upto but excluding the given namespace
|
||||||
|
# when no namespace is given, all ancestors upto the top are returned
|
||||||
|
def ancestors_upto(top = nil)
|
||||||
|
Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
|
||||||
|
.base_and_ancestors(upto: top)
|
||||||
|
end
|
||||||
|
|
||||||
def lfs_enabled?
|
def lfs_enabled?
|
||||||
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
|
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
|
||||||
|
|
||||||
|
@ -1262,7 +1272,7 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
# self.forked_from_project will be nil before the project is saved, so
|
# self.forked_from_project will be nil before the project is saved, so
|
||||||
# we need to go through the relation
|
# we need to go through the relation
|
||||||
original_project = forked_project_link.forked_from_project
|
original_project = forked_project_link&.forked_from_project
|
||||||
return true unless original_project
|
return true unless original_project
|
||||||
|
|
||||||
level <= original_project.visibility_level
|
level <= original_project.visibility_level
|
||||||
|
@ -1549,10 +1559,6 @@ class Project < ActiveRecord::Base
|
||||||
map.public_path_for_source_path(path)
|
map.public_path_for_source_path(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parent
|
|
||||||
namespace
|
|
||||||
end
|
|
||||||
|
|
||||||
def parent_changed?
|
def parent_changed?
|
||||||
namespace_id_changed?
|
namespace_id_changed?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
class BaseSerializer
|
class BaseSerializer
|
||||||
def initialize(parameters = {})
|
attr_reader :params
|
||||||
@request = EntityRequest.new(parameters)
|
|
||||||
|
def initialize(params = {})
|
||||||
|
@params = params
|
||||||
|
@request = EntityRequest.new(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def represent(resource, opts = {}, entity_class = nil)
|
def represent(resource, opts = {}, entity_class = nil)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
module WithPagination
|
||||||
|
attr_accessor :paginator
|
||||||
|
|
||||||
|
def with_pagination(request, response)
|
||||||
|
tap { self.paginator = Gitlab::Serializer::Pagination.new(request, response) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginated?
|
||||||
|
paginator.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# super is `BaseSerializer#represent` here.
|
||||||
|
#
|
||||||
|
# we shouldn't try to paginate single resources
|
||||||
|
def represent(resource, opts = {})
|
||||||
|
if paginated? && resource.respond_to?(:page)
|
||||||
|
super(@paginator.paginate(resource), opts)
|
||||||
|
else
|
||||||
|
super(resource, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,6 @@
|
||||||
class EnvironmentSerializer < BaseSerializer
|
class EnvironmentSerializer < BaseSerializer
|
||||||
|
include WithPagination
|
||||||
|
|
||||||
Item = Struct.new(:name, :size, :latest)
|
Item = Struct.new(:name, :size, :latest)
|
||||||
|
|
||||||
entity EnvironmentEntity
|
entity EnvironmentEntity
|
||||||
|
@ -7,18 +9,10 @@ class EnvironmentSerializer < BaseSerializer
|
||||||
tap { @itemize = true }
|
tap { @itemize = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_pagination(request, response)
|
|
||||||
tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def itemized?
|
def itemized?
|
||||||
@itemize
|
@itemize
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated?
|
|
||||||
@paginator.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def represent(resource, opts = {})
|
def represent(resource, opts = {})
|
||||||
if itemized?
|
if itemized?
|
||||||
itemize(resource).map do |item|
|
itemize(resource).map do |item|
|
||||||
|
@ -27,8 +21,6 @@ class EnvironmentSerializer < BaseSerializer
|
||||||
latest: super(item.latest, opts) }
|
latest: super(item.latest, opts) }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
resource = @paginator.paginate(resource) if paginated?
|
|
||||||
|
|
||||||
super(resource, opts)
|
super(resource, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
class GroupChildEntity < Grape::Entity
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include RequestAwareEntity
|
||||||
|
|
||||||
|
expose :id, :name, :description, :visibility, :full_name,
|
||||||
|
:created_at, :updated_at, :avatar_url
|
||||||
|
|
||||||
|
expose :type do |instance|
|
||||||
|
type
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :can_edit do |instance|
|
||||||
|
return false unless request.respond_to?(:current_user)
|
||||||
|
|
||||||
|
can?(request.current_user, "admin_#{type}", instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :edit_path do |instance|
|
||||||
|
# We know `type` will be one either `project` or `group`.
|
||||||
|
# The `edit_polymorphic_path` helper would try to call the path helper
|
||||||
|
# with a plural: `edit_groups_path(instance)` or `edit_projects_path(instance)`
|
||||||
|
# while our methods are `edit_group_path` or `edit_group_path`
|
||||||
|
public_send("edit_#{type}_path", instance) # rubocop:disable GitlabSecurity/PublicSend
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :relative_path do |instance|
|
||||||
|
polymorphic_path(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :permission do |instance|
|
||||||
|
membership&.human_access
|
||||||
|
end
|
||||||
|
|
||||||
|
# Project only attributes
|
||||||
|
expose :star_count,
|
||||||
|
if: lambda { |_instance, _options| project? }
|
||||||
|
|
||||||
|
# Group only attributes
|
||||||
|
expose :children_count, :parent_id, :project_count, :subgroup_count,
|
||||||
|
unless: lambda { |_instance, _options| project? }
|
||||||
|
|
||||||
|
expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance|
|
||||||
|
leave_group_members_path(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :can_leave, unless: lambda { |_instance, _options| project? } do |instance|
|
||||||
|
if membership
|
||||||
|
can?(request.current_user, :destroy_group_member, membership)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :number_projects_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
|
||||||
|
number_with_delimiter(instance.project_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
|
||||||
|
number_with_delimiter(instance.member_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def membership
|
||||||
|
return unless request.current_user
|
||||||
|
|
||||||
|
@membership ||= request.current_user.members.find_by(source: object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def project?
|
||||||
|
object.is_a?(Project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
object.class.name.downcase
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,51 @@
|
||||||
|
class GroupChildSerializer < BaseSerializer
|
||||||
|
include WithPagination
|
||||||
|
|
||||||
|
attr_reader :hierarchy_root, :should_expand_hierarchy
|
||||||
|
|
||||||
|
entity GroupChildEntity
|
||||||
|
|
||||||
|
def expand_hierarchy(hierarchy_root = nil)
|
||||||
|
@hierarchy_root = hierarchy_root
|
||||||
|
@should_expand_hierarchy = true
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def represent(resource, opts = {}, entity_class = nil)
|
||||||
|
if should_expand_hierarchy
|
||||||
|
paginator.paginate(resource) if paginated?
|
||||||
|
represent_hierarchies(resource, opts)
|
||||||
|
else
|
||||||
|
super(resource, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def represent_hierarchies(children, opts)
|
||||||
|
if children.is_a?(GroupDescendant)
|
||||||
|
represent_hierarchy(children.hierarchy(hierarchy_root), opts).first
|
||||||
|
else
|
||||||
|
hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root)
|
||||||
|
# When an array was passed, we always want to represent an array.
|
||||||
|
# Even if the hierarchy only contains one element
|
||||||
|
represent_hierarchy(Array.wrap(hierarchies), opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def represent_hierarchy(hierarchy, opts)
|
||||||
|
serializer = self.class.new(params)
|
||||||
|
|
||||||
|
if hierarchy.is_a?(Hash)
|
||||||
|
hierarchy.map do |parent, children|
|
||||||
|
serializer.represent(parent, opts)
|
||||||
|
.merge(children: Array.wrap(serializer.represent_hierarchy(children, opts)))
|
||||||
|
end
|
||||||
|
elsif hierarchy.is_a?(Array)
|
||||||
|
hierarchy.flat_map { |child| serializer.represent_hierarchy(child, opts) }
|
||||||
|
else
|
||||||
|
serializer.represent(hierarchy, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +1,5 @@
|
||||||
class GroupSerializer < BaseSerializer
|
class GroupSerializer < BaseSerializer
|
||||||
|
include WithPagination
|
||||||
|
|
||||||
entity GroupEntity
|
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
|
end
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
class PipelineSerializer < BaseSerializer
|
class PipelineSerializer < BaseSerializer
|
||||||
|
include WithPagination
|
||||||
|
|
||||||
InvalidResourceError = Class.new(StandardError)
|
InvalidResourceError = Class.new(StandardError)
|
||||||
|
|
||||||
entity PipelineDetailsEntity
|
entity PipelineDetailsEntity
|
||||||
|
|
||||||
def with_pagination(request, response)
|
|
||||||
tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def paginated?
|
|
||||||
@paginator.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def represent(resource, opts = {})
|
def represent(resource, opts = {})
|
||||||
if resource.is_a?(ActiveRecord::Relation)
|
if resource.is_a?(ActiveRecord::Relation)
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ module Projects
|
||||||
|
|
||||||
refresh_forks_count(@project.forked_from_project)
|
refresh_forks_count(@project.forked_from_project)
|
||||||
|
|
||||||
@project.forked_project_link.destroy
|
|
||||||
@project.fork_network_member.destroy
|
@project.fork_network_member.destroy
|
||||||
|
@project.forked_project_link.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_forks_count(project)
|
def refresh_forks_count(project)
|
||||||
|
|
|
@ -530,6 +530,32 @@
|
||||||
= succeed "." do
|
= succeed "." do
|
||||||
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
|
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
|
||||||
|
|
||||||
|
%fieldset
|
||||||
|
%legend Git Storage Circuitbreaker settings
|
||||||
|
.form-group
|
||||||
|
= f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
|
||||||
|
.col-sm-10
|
||||||
|
= f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
|
||||||
|
.help-block
|
||||||
|
= circuitbreaker_failure_count_help_text
|
||||||
|
.form-group
|
||||||
|
= f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
|
||||||
|
.col-sm-10
|
||||||
|
= f.number_field :circuitbreaker_failure_wait_time, class: 'form-control'
|
||||||
|
.help-block
|
||||||
|
= circuitbreaker_failure_wait_time_help_text
|
||||||
|
.form-group
|
||||||
|
= f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
|
||||||
|
.col-sm-10
|
||||||
|
= f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
|
||||||
|
.help-block
|
||||||
|
= circuitbreaker_failure_reset_time_help_text
|
||||||
|
.form-group
|
||||||
|
= f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
|
||||||
|
.col-sm-10
|
||||||
|
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
|
||||||
|
.help-block
|
||||||
|
= circuitbreaker_storage_timeout_help_text
|
||||||
|
|
||||||
%fieldset
|
%fieldset
|
||||||
%legend Repository Checks
|
%legend Repository Checks
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
.top-area
|
.top-area
|
||||||
%ul.nav-links
|
%ul.nav-links
|
||||||
= nav_link(page: dashboard_groups_path) do
|
= nav_link(page: dashboard_groups_path) do
|
||||||
= link_to dashboard_groups_path, title: 'Your groups' do
|
= link_to dashboard_groups_path, title: _("Your groups") do
|
||||||
Your groups
|
Your groups
|
||||||
= nav_link(page: explore_groups_path) do
|
= nav_link(page: explore_groups_path) do
|
||||||
= link_to explore_groups_path, title: 'Explore public groups' do
|
= link_to explore_groups_path, title: _("Explore public groups") do
|
||||||
Explore public groups
|
Explore public groups
|
||||||
.nav-controls
|
.nav-controls
|
||||||
= render 'shared/groups/search_form'
|
= render 'shared/groups/search_form'
|
||||||
= render 'shared/groups/dropdown'
|
= render 'shared/groups/dropdown'
|
||||||
- if current_user.can_create_group?
|
- if current_user.can_create_group?
|
||||||
= link_to "New group", new_group_path, class: "btn btn-new"
|
= link_to _("New group"), new_group_path, class: "btn btn-new"
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
.groups-empty-state
|
|
||||||
= custom_icon("icon_empty_groups")
|
|
||||||
|
|
||||||
.text-content
|
|
||||||
%h4 A group is a collection of several projects.
|
|
||||||
%p If you organize your projects under a group, it works like a folder.
|
|
||||||
%p You can manage your group member’s permissions and access to each project in the group.
|
|
|
@ -1,9 +1,2 @@
|
||||||
.js-groups-list-holder
|
.js-groups-list-holder
|
||||||
#dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } }
|
#js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, 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' } }
|
||||||
.groups-list-loading
|
|
||||||
= icon('spinner spin', 'v-show' => 'isLoading')
|
|
||||||
%template{ 'v-if' => '!isLoading && isEmpty' }
|
|
||||||
%div{ 'v-cloak' => true }
|
|
||||||
= render 'empty_state'
|
|
||||||
%template{ 'v-else-if' => '!isLoading && !isEmpty' }
|
|
||||||
%groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' }
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
= webpack_bundle_tag 'common_vue'
|
= webpack_bundle_tag 'common_vue'
|
||||||
= webpack_bundle_tag 'groups'
|
= webpack_bundle_tag 'groups'
|
||||||
|
|
||||||
- if @groups.empty?
|
- if params[:filter].blank? && @groups.empty?
|
||||||
= render 'empty_state'
|
= render 'shared/groups/empty_state'
|
||||||
- else
|
- else
|
||||||
= render 'groups'
|
= render 'groups'
|
||||||
|
|
|
@ -1,6 +1,2 @@
|
||||||
.js-groups-list-holder
|
.js-groups-list-holder
|
||||||
%ul.content-list
|
#js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, 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' } }
|
||||||
- @groups.each do |group|
|
|
||||||
= render 'shared/groups/group', group: group
|
|
||||||
|
|
||||||
= paginate @groups, theme: 'gitlab'
|
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
- page_title "Groups"
|
- page_title "Groups"
|
||||||
- header_title "Groups", dashboard_groups_path
|
- header_title "Groups", dashboard_groups_path
|
||||||
|
|
||||||
|
= webpack_bundle_tag 'common_vue'
|
||||||
|
= webpack_bundle_tag 'groups'
|
||||||
|
|
||||||
- if current_user
|
- if current_user
|
||||||
= render 'dashboard/groups_head'
|
= render 'dashboard/groups_head'
|
||||||
- else
|
- else
|
||||||
|
@ -17,7 +20,7 @@
|
||||||
%p Below you will find all the groups that are public.
|
%p Below you will find all the groups that are public.
|
||||||
%p You can easily contribute to them by requesting to join these groups.
|
%p You can easily contribute to them by requesting to join these groups.
|
||||||
|
|
||||||
- if @groups.present?
|
- if params[:filter].blank? && @groups.empty?
|
||||||
= render 'groups'
|
|
||||||
- else
|
|
||||||
.nothing-here-block No public groups
|
.nothing-here-block No public groups
|
||||||
|
- else
|
||||||
|
= render 'groups'
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
= webpack_bundle_tag 'common_vue'
|
||||||
|
= webpack_bundle_tag 'groups'
|
||||||
|
|
||||||
|
.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' } }
|
|
@ -1,8 +0,0 @@
|
||||||
%ul.nav-links
|
|
||||||
= nav_link(page: group_path(@group)) do
|
|
||||||
= link_to group_path(@group) do
|
|
||||||
Projects
|
|
||||||
- if Group.supports_nested_groups?
|
|
||||||
= nav_link(page: subgroups_group_path(@group)) do
|
|
||||||
= link_to subgroups_group_path(@group) do
|
|
||||||
Subgroups
|
|
|
@ -1,5 +1,6 @@
|
||||||
- @no_container = true
|
- @no_container = true
|
||||||
- breadcrumb_title "Details"
|
- breadcrumb_title "Details"
|
||||||
|
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
|
||||||
|
|
||||||
= content_for :meta_tags do
|
= content_for :meta_tags do
|
||||||
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
|
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
|
||||||
|
@ -7,13 +8,38 @@
|
||||||
= render 'groups/home_panel'
|
= render 'groups/home_panel'
|
||||||
|
|
||||||
.groups-header{ class: container_class }
|
.groups-header{ class: container_class }
|
||||||
.top-area
|
.group-nav-container
|
||||||
= render 'groups/show_nav'
|
.nav-controls.clearfix
|
||||||
.nav-controls
|
= render "shared/groups/search_form"
|
||||||
= render 'shared/projects/search_form'
|
= render "shared/groups/dropdown", show_archive_options: true
|
||||||
= render 'shared/projects/dropdown'
|
|
||||||
- if can? current_user, :create_projects, @group
|
- if can? current_user, :create_projects, @group
|
||||||
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
|
- new_project_label = _("New project")
|
||||||
New Project
|
- new_subgroup_label = _("New subgroup")
|
||||||
|
- if can_create_subgroups
|
||||||
|
.btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
|
||||||
|
%input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
|
||||||
|
%button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } }
|
||||||
|
= icon("caret-down", class: "dropdown-btn-icon")
|
||||||
|
%ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
|
||||||
|
%li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
|
||||||
|
.menu-item
|
||||||
|
.icon-container
|
||||||
|
= icon("check", class: "list-item-checkmark")
|
||||||
|
.description
|
||||||
|
%strong= new_project_label
|
||||||
|
%span= s_("GroupsTree|Create a project in this group.")
|
||||||
|
%li.divider.droplap-item-ignore
|
||||||
|
%li{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
|
||||||
|
.menu-item
|
||||||
|
.icon-container
|
||||||
|
= icon("check", class: "list-item-checkmark")
|
||||||
|
.description
|
||||||
|
%strong= new_subgroup_label
|
||||||
|
%span= s_("GroupsTree|Create a subgroup in this group.")
|
||||||
|
- else
|
||||||
|
= link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
|
||||||
|
|
||||||
= render "projects", projects: @projects
|
- if params[:filter].blank? && !@has_children
|
||||||
|
= render "shared/groups/empty_state"
|
||||||
|
- else
|
||||||
|
= render "children", children: @children, group: @group
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
- breadcrumb_title "Details"
|
|
||||||
- @no_container = true
|
|
||||||
|
|
||||||
= render 'groups/home_panel'
|
|
||||||
|
|
||||||
.groups-header{ class: container_class }
|
|
||||||
.top-area
|
|
||||||
= render 'groups/show_nav'
|
|
||||||
.nav-controls
|
|
||||||
= form_tag request.path, method: :get do |f|
|
|
||||||
= search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
|
|
||||||
- if can?(current_user, :create_subgroup, @group)
|
|
||||||
= link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
|
|
||||||
New Subgroup
|
|
||||||
|
|
||||||
- if @nested_groups.present?
|
|
||||||
%ul.content-list
|
|
||||||
= render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
|
|
||||||
- else
|
|
||||||
.nothing-here-block
|
|
||||||
There are no subgroups to show.
|
|
|
@ -1,6 +1,6 @@
|
||||||
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
|
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
|
||||||
|
|
||||||
.file-holder.file.append-bottom-default
|
.file-holder-bottom-radius.file-holder.file.append-bottom-default
|
||||||
.js-file-title.file-title.clearfix{ data: { current_action: action } }
|
.js-file-title.file-title.clearfix{ data: { current_action: action } }
|
||||||
.editor-ref
|
.editor-ref
|
||||||
= icon('code-fork')
|
= icon('code-fork')
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
- if can?(current_user, :admin_cluster, @cluster)
|
||||||
|
.append-bottom-20
|
||||||
|
%label.append-bottom-10
|
||||||
|
= s_('ClusterIntegration|Google Container Engine')
|
||||||
|
%p
|
||||||
|
- link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
|
||||||
|
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
|
||||||
|
|
||||||
|
.well.form-group
|
||||||
|
%label.text-danger
|
||||||
|
= s_('ClusterIntegration|Remove cluster integration')
|
||||||
|
%p
|
||||||
|
= s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.')
|
||||||
|
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
|
|
@ -1,16 +1,29 @@
|
||||||
|
- @content_class = "limit-container-width" unless fluid_layout
|
||||||
- breadcrumb_title "Cluster"
|
- breadcrumb_title "Cluster"
|
||||||
- page_title _("Cluster")
|
- page_title _("Cluster")
|
||||||
|
|
||||||
|
- expanded = Rails.env.test?
|
||||||
|
|
||||||
- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
|
- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
|
||||||
.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
|
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
|
||||||
toggle_status: @cluster.enabled? ? 'true': 'false',
|
toggle_status: @cluster.enabled? ? 'true': 'false',
|
||||||
cluster_status: @cluster.status_name,
|
cluster_status: @cluster.status_name,
|
||||||
cluster_status_reason: @cluster.status_reason } }
|
cluster_status_reason: @cluster.status_reason } }
|
||||||
.col-sm-4
|
|
||||||
= render 'sidebar'
|
%section.settings
|
||||||
.col-sm-8
|
%h4= s_('ClusterIntegration|Enable cluster integration')
|
||||||
%label.append-bottom-10{ for: 'enable-cluster-integration' }
|
.settings-content.expanded
|
||||||
= s_('ClusterIntegration|Enable cluster integration')
|
|
||||||
|
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
|
||||||
|
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
|
||||||
|
%p.js-error-reason
|
||||||
|
|
||||||
|
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
|
||||||
|
= s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
|
||||||
|
|
||||||
|
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
|
||||||
|
= s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
|
||||||
|
|
||||||
%p
|
%p
|
||||||
- if @cluster.enabled?
|
- if @cluster.enabled?
|
||||||
- if can?(current_user, :update_cluster, @cluster)
|
- if can?(current_user, :update_cluster, @cluster)
|
||||||
|
@ -36,22 +49,14 @@
|
||||||
.form-group
|
.form-group
|
||||||
= field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
|
= field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
|
||||||
|
|
||||||
- if can?(current_user, :admin_cluster, @cluster)
|
%section.settings#js-cluster-details
|
||||||
%label.append-bottom-10{ for: 'google-container-engine' }
|
.settings-header
|
||||||
= s_('ClusterIntegration|Google Container Engine')
|
%h4= s_('ClusterIntegration|Cluster details')
|
||||||
%p
|
%button.btn.js-settings-toggle
|
||||||
- link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
|
= expanded ? 'Collapse' : 'Expand'
|
||||||
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
|
%p= s_('ClusterIntegration|See and edit the details for your cluster')
|
||||||
|
|
||||||
.hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' }
|
.settings-content.no-animate{ class: ('expanded' if expanded) }
|
||||||
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
|
|
||||||
%p.js-error-reason
|
|
||||||
|
|
||||||
.hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' }
|
|
||||||
= s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
|
|
||||||
|
|
||||||
.hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' }
|
|
||||||
= s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
|
|
||||||
|
|
||||||
.form_group.append-bottom-20
|
.form_group.append-bottom-20
|
||||||
%label.append-bottom-10{ for: 'cluter-name' }
|
%label.append-bottom-10{ for: 'cluter-name' }
|
||||||
|
@ -61,10 +66,11 @@
|
||||||
%span.input-group-addon.clipboard-addon
|
%span.input-group-addon.clipboard-addon
|
||||||
= clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
|
= clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
|
||||||
|
|
||||||
- if can?(current_user, :admin_cluster, @cluster)
|
%section.settings#js-cluster-advanced-settings
|
||||||
.well.form_group
|
.settings-header
|
||||||
%label.text-danger
|
%h4= s_('ClusterIntegration|Advanced settings')
|
||||||
= s_('ClusterIntegration|Remove cluster integration')
|
%button.btn.js-settings-toggle
|
||||||
%p
|
= expanded ? 'Collapse' : 'Expand'
|
||||||
= s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.')
|
%p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
|
||||||
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
|
.settings-content.no-animate{ class: ('expanded' if expanded) }
|
||||||
|
= render 'advanced_settings'
|
||||||
|
|
|
@ -24,10 +24,15 @@
|
||||||
%p
|
%p
|
||||||
You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
|
You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
|
||||||
|
|
||||||
|
- if show_auto_devops_callout?(@project)
|
||||||
|
%p
|
||||||
|
- link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
|
||||||
|
= s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link }
|
||||||
|
%p
|
||||||
|
= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
|
||||||
|
|
||||||
- if can?(current_user, :push_code, @project)
|
- if can?(current_user, :push_code, @project)
|
||||||
%div{ class: container_class }
|
%div{ class: container_class }
|
||||||
- if show_auto_devops_callout?(@project)
|
|
||||||
= render 'shared/auto_devops_callout'
|
|
||||||
.prepend-top-20
|
.prepend-top-20
|
||||||
.empty_wrapper
|
.empty_wrapper
|
||||||
%h3.page-title-empty
|
%h3.page-title-empty
|
||||||
|
|
|
@ -13,8 +13,6 @@
|
||||||
|
|
||||||
- if @project.merge_requests.exists?
|
- if @project.merge_requests.exists?
|
||||||
%div{ class: container_class }
|
%div{ class: container_class }
|
||||||
- if show_auto_devops_callout?(@project)
|
|
||||||
= render 'shared/auto_devops_callout'
|
|
||||||
.top-area
|
.top-area
|
||||||
= render 'shared/issuable/nav', type: :merge_requests
|
= render 'shared/issuable/nav', type: :merge_requests
|
||||||
.nav-controls
|
.nav-controls
|
||||||
|
|
|
@ -54,6 +54,10 @@
|
||||||
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
|
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
|
||||||
Import project from
|
Import project from
|
||||||
.import-buttons
|
.import-buttons
|
||||||
|
- if gitlab_project_import_enabled?
|
||||||
|
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
||||||
|
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
||||||
|
= icon('gitlab', text: 'GitLab export')
|
||||||
%div
|
%div
|
||||||
- if github_import_enabled?
|
- if github_import_enabled?
|
||||||
= link_to new_import_github_path, class: 'btn import_github' do
|
= link_to new_import_github_path, class: 'btn import_github' do
|
||||||
|
@ -87,10 +91,6 @@
|
||||||
- if git_import_enabled?
|
- if git_import_enabled?
|
||||||
%button.btn.js-toggle-button.import_git{ type: "button" }
|
%button.btn.js-toggle-button.import_git{ type: "button" }
|
||||||
= icon('git', text: 'Repo by URL')
|
= icon('git', text: 'Repo by URL')
|
||||||
- if gitlab_project_import_enabled?
|
|
||||||
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
|
||||||
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
|
||||||
= icon('gitlab', text: 'GitLab export')
|
|
||||||
.col-lg-12
|
.col-lg-12
|
||||||
.js-toggle-content.hide.toggle-import-form
|
.js-toggle-content.hide.toggle-import-form
|
||||||
%hr
|
%hr
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
- page_title "Pipelines"
|
- page_title "Pipelines"
|
||||||
|
|
||||||
%div{ 'class' => container_class }
|
%div{ 'class' => container_class }
|
||||||
- if show_auto_devops_callout?(@project)
|
|
||||||
= render 'shared/auto_devops_callout'
|
|
||||||
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
|
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
|
||||||
"help-page-path" => help_page_path('ci/quick_start/README'),
|
"help-page-path" => help_page_path('ci/quick_start/README'),
|
||||||
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
|
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
|
||||||
|
|
|
@ -12,7 +12,5 @@
|
||||||
= webpack_bundle_tag 'repo'
|
= webpack_bundle_tag 'repo'
|
||||||
|
|
||||||
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
|
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
|
||||||
- if show_auto_devops_callout?(@project) && !show_new_repo?
|
|
||||||
= render 'shared/auto_devops_callout'
|
|
||||||
= render 'projects/last_push'
|
= render 'projects/last_push'
|
||||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
|
.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
|
||||||
.bordered-box.landing.content-block
|
.banner-graphic
|
||||||
%button.btn.btn-default.close.js-close-callout{ type: 'button',
|
|
||||||
'aria-label' => 'Dismiss Auto DevOps box' }
|
|
||||||
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
|
|
||||||
.svg-container
|
|
||||||
= custom_icon('icon_autodevops')
|
= custom_icon('icon_autodevops')
|
||||||
.user-callout-copy
|
|
||||||
%h4= s_('AutoDevOps|Auto DevOps (Beta)')
|
.prepend-top-10.prepend-left-10.append-bottom-10
|
||||||
%p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
|
%h5= s_('AutoDevOps|Auto DevOps (Beta)')
|
||||||
|
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
|
||||||
%p
|
%p
|
||||||
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
|
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
|
||||||
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
||||||
|
.prepend-top-10
|
||||||
|
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
|
||||||
|
|
||||||
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
|
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
|
||||||
|
'aria-label' => 'Dismiss Auto DevOps box' }
|
||||||
|
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
.dropdown.inline.js-group-filter-dropdown-wrap
|
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
|
||||||
|
- if @sort.present?
|
||||||
|
- default_sort_by = @sort
|
||||||
|
- else
|
||||||
|
- if params[:sort]
|
||||||
|
- default_sort_by = params[:sort]
|
||||||
|
- else
|
||||||
|
- default_sort_by = sort_value_recently_created
|
||||||
|
|
||||||
|
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
|
||||||
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||||
%span.dropdown-label
|
%span.dropdown-label
|
||||||
- if @sort.present?
|
= sort_options_hash[default_sort_by]
|
||||||
= sort_options_hash[@sort]
|
|
||||||
- else
|
|
||||||
= sort_title_recently_created
|
|
||||||
= icon('chevron-down')
|
= icon('chevron-down')
|
||||||
%ul.dropdown-menu.dropdown-menu-align-right
|
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
|
||||||
%li
|
%li.dropdown-header
|
||||||
= link_to filter_groups_path(sort: sort_value_recently_created) do
|
= _("Sort by")
|
||||||
= sort_title_recently_created
|
- groups_sort_options_hash.each do |value, title|
|
||||||
= link_to filter_groups_path(sort: sort_value_oldest_created) do
|
%li.js-filter-sort-order
|
||||||
= sort_title_oldest_created
|
= link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
|
||||||
= link_to filter_groups_path(sort: sort_value_recently_updated) do
|
= title
|
||||||
= sort_title_recently_updated
|
- if show_archive_options
|
||||||
= link_to filter_groups_path(sort: sort_value_oldest_updated) do
|
%li.divider
|
||||||
= sort_title_oldest_updated
|
%li.js-filter-archived-projects
|
||||||
|
= link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do
|
||||||
|
Hide archived projects
|
||||||
|
%li.js-filter-archived-projects
|
||||||
|
= link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
|
||||||
|
Show archived projects
|
||||||
|
%li.js-filter-archived-projects
|
||||||
|
= link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
|
||||||
|
Show archived projects only
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
.groups-empty-state
|
||||||
|
= custom_icon("icon_empty_groups")
|
||||||
|
|
||||||
|
.text-content
|
||||||
|
%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.")
|
|
@ -11,7 +11,7 @@
|
||||||
= link_to edit_group_path(group), class: "btn" do
|
= link_to edit_group_path(group), class: "btn" do
|
||||||
= icon('cogs')
|
= icon('cogs')
|
||||||
|
|
||||||
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
|
= link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
|
||||||
= icon('sign-out')
|
= icon('sign-out')
|
||||||
|
|
||||||
.stats
|
.stats
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue