summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/groups/components/group_folder.vue8
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue173
-rw-r--r--app/assets/javascripts/groups/components/groups.vue1
-rw-r--r--app/assets/javascripts/groups/components/project_folder.vue67
-rw-r--r--app/assets/javascripts/groups/components/project_item.vue109
-rw-r--r--app/assets/javascripts/groups/constants.js19
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js4
-rw-r--r--app/assets/javascripts/groups/index.js13
-rw-r--r--app/assets/javascripts/groups/stores/groups_store.js11
9 files changed, 334 insertions, 71 deletions
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
index 7cc6c4b0359..cf0fc2a42fd 100644
--- a/app/assets/javascripts/groups/components/group_folder.vue
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -10,12 +10,18 @@ export default {
required: false,
default: () => ({}),
},
+ hasSiblingProjects: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
<template>
- <ul class="content-list group-list-tree">
+ <ul
+ class="content-list group-list-tree"
+ :class="{ 'has-sibling-projects': hasSiblingProjects }">
<group-item
v-for="(group, index) in groups"
:key="index"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 2060410e991..59df683ca4c 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,10 +1,19 @@
<script>
import identicon from '../../vue_shared/components/identicon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
import eventHub from '../event_hub';
+import { GROUP_VISIBILITY_TYPE, VISIBILITY_TYPE_ICON } from '../constants';
+import groupFolder from './group_folder.vue';
+import projectFolder from './project_folder.vue';
export default {
components: {
identicon,
+ groupFolder,
+ projectFolder,
+ },
+ directives: {
+ tooltip,
},
props: {
group: {
@@ -28,7 +37,7 @@ export default {
// Skip for buttons
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
- if (this.group.hasSubgroups) {
+ if (this.hasChildren) {
eventHub.$emit('toggleSubGroups', this.group);
} else {
window.location.href = this.group.groupPath;
@@ -55,17 +64,15 @@ export default {
return {
'group-row': true,
'is-open': this.group.isOpen,
- 'has-subgroups': this.group.hasSubgroups,
+ 'has-subgroups': this.hasChildren,
'no-description': !this.group.description,
};
},
visibilityIcon() {
- return {
- fa: true,
- 'fa-globe': this.group.visibility === 'public',
- 'fa-shield': this.group.visibility === 'internal',
- 'fa-lock': this.group.visibility === 'private',
- };
+ return VISIBILITY_TYPE_ICON[this.group.visibility];
+ },
+ visibilityTooltip() {
+ return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
fullPath() {
let fullPath = '';
@@ -93,12 +100,23 @@ export default {
return fullPath;
},
- hasGroups() {
+ hasSiblingGroups() {
return Object.keys(this.group.subGroups).length > 0;
},
+ hasProjects() {
+ return this.group.projectCount > 0;
+ },
+ hasChildren() {
+ return this.hasProjects || this.group.hasSubgroups;
+ },
hasAvatar() {
return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;
},
+ hasGroupAccess() {
+ return this.group.projects &&
+ this.group.projects.length >= 0 &&
+ this.group.permissions.humanGroupAccess;
+ },
},
};
</script>
@@ -109,95 +127,109 @@ export default {
:id="groupDomId"
:class="rowClass"
>
- <div
- class="group-row-contents">
- <div
- class="controls">
+ <div class="group-row-contents">
+ <div class="controls">
<a
v-if="group.canEdit"
+ v-tooltip
class="edit-group btn"
- :href="group.editPath">
+ data-container="body"
+ title="Edit group"
+ :href="group.editPath"
+ >
<i
class="fa fa-cogs"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
</a>
<a
+ v-tooltip
@click="onLeaveGroup"
:href="group.leavePath"
+ data-container="body"
class="leave-group btn"
- title="Leave this group">
+ title="Leave this group"
+ >
<i
class="fa fa-sign-out"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
</a>
</div>
- <div
- class="stats">
+ <div class="stats">
+ <span
+ v-tooltip
+ class="number-subgroups"
+ data-placement="top"
+ data-container="body"
+ title="Subgroups"
+ >
+ <i
+ class="fa fa-folder"
+ aria-hidden="true"/>
+ {{group.subGroupCount}}
+ </span>
<span
- class="number-projects">
+ v-tooltip
+ class="number-projects"
+ data-placement="top"
+ data-container="body"
+ title="Projects"
+ >
<i
class="fa fa-bookmark"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
{{group.numberProjects}}
</span>
<span
- class="number-users">
+ v-tooltip
+ class="number-users"
+ data-placement="top"
+ data-container="body"
+ title="Members"
+ >
<i
class="fa fa-users"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
{{group.numberUsers}}
</span>
<span
- class="group-visibility">
+ v-tooltip
+ class="group-visibility"
+ data-placement="left"
+ data-container="body"
+ :title="visibilityTooltip"
+ >
<i
+ class="fa"
:class="visibilityIcon"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
</span>
</div>
- <div
- class="folder-toggle-wrap">
+ <div class="folder-toggle-wrap">
<span
class="folder-caret"
- v-if="group.hasSubgroups">
+ v-if="hasChildren"
+ >
<i
v-if="group.isOpen"
class="fa fa-caret-down"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
<i
v-if="!group.isOpen"
class="fa fa-caret-right"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
</span>
<span class="folder-icon">
<i
v-if="group.isOpen"
class="fa fa-folder-open"
- aria-hidden="true"
- >
- </i>
+ aria-hidden="true"/>
<i
v-if="!group.isOpen"
class="fa fa-folder"
- aria-hidden="true">
- </i>
+ aria-hidden="true"/>
</span>
</div>
- <div
- class="avatar-container s40 hidden-xs">
+ <div class="avatar-container s40 hidden-xs">
<a
:href="group.groupPath">
<img
@@ -212,22 +244,37 @@ export default {
/>
</a>
</div>
- <div
- class="title">
- <a
- :href="group.groupPath">{{fullPath}}</a>
- <template v-if="group.permissions.humanGroupAccess">
- as
- <span class="access-type">{{group.permissions.humanGroupAccess}}</span>
- </template>
+ <div class="metadata">
+ <div class="title">
+ <a
+ :href="group.groupPath"
+ >
+ {{fullPath}}
+ </a>
+ <template v-if="hasGroupAccess">
+ <span class="access-type">as {{group.permissions.humanGroupAccess}}</span>
+ </template>
+ </div>
+ <div
+ v-if="group.description"
+ class="description"
+ >
+ {{group.description}}
+ </div>
</div>
- <div
- class="description">{{group.description}}</div>
</div>
<group-folder
- v-if="group.isOpen && hasGroups"
+ v-if="group.isOpen"
+ :has-sibling-projects="hasProjects"
:groups="group.subGroups"
- :baseGroup="group"
+ :base-group="group"
+ />
+ <project-folder
+ v-if="group.isOpen && hasProjects"
+ :group-path="group.groupPath"
+ :has-sibling-groups="hasSiblingGroups"
+ :projects="group.projects"
+ :project-count="group.projectCount"
/>
</li>
</template>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 36a04d4202f..f2ac2ed83a1 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -30,6 +30,7 @@ export default {
<div class="groups-list-tree-container">
<group-folder
:groups="groups"
+ :has-sibling-projects="false"
/>
<table-pagination
:change="change"
diff --git a/app/assets/javascripts/groups/components/project_folder.vue b/app/assets/javascripts/groups/components/project_folder.vue
new file mode 100644
index 00000000000..15128264516
--- /dev/null
+++ b/app/assets/javascripts/groups/components/project_folder.vue
@@ -0,0 +1,67 @@
+<script>
+import { MAX_PROJECT_COUNT } from '../constants';
+import projectItem from './project_item.vue';
+import identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ projectItem,
+ identicon,
+ },
+ props: {
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
+ hasSiblingGroups: {
+ type: Boolean,
+ required: true,
+ },
+ projectCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ hasMoreItems() {
+ return this.projectCount > MAX_PROJECT_COUNT;
+ },
+ countOfMoreProjects() {
+ return this.projectCount - MAX_PROJECT_COUNT;
+ },
+ moreProjectsLinkText() {
+ // eslint-disable-next-line no-underscore-dangle
+ return `${this.countOfMoreProjects} more ${this.n__('project', 'projects', this.countOfMoreProjects)}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <ul
+ class="content-list group-list-tree project-list"
+ :class="{ 'has-sibling-groups': hasSiblingGroups, 'has-more-items': hasMoreItems }"
+ >
+ <project-item
+ v-for="(project, index) in projects"
+ :key="index"
+ :project="project"
+ />
+ <li
+ v-if="hasMoreItems"
+ class="has-more-items-link"
+ >
+ <a
+ :href="this.groupPath">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true"/>
+ {{moreProjectsLinkText}}
+ </a>
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/groups/components/project_item.vue b/app/assets/javascripts/groups/components/project_item.vue
new file mode 100644
index 00000000000..337e1faeae1
--- /dev/null
+++ b/app/assets/javascripts/groups/components/project_item.vue
@@ -0,0 +1,109 @@
+<script>
+import tooltip from '../../vue_shared/directives/tooltip';
+import { PROJECT_VISIBILITY_TYPE, VISIBILITY_TYPE_ICON } from '../constants';
+import identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ identicon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ projectDomId() {
+ return `project-${this.project.id}`;
+ },
+ rowClass() {
+ return {
+ 'no-description': !this.project.description,
+ };
+ },
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[this.project.visibility];
+ },
+ visibilityTooltip() {
+ return PROJECT_VISIBILITY_TYPE[this.project.visibility];
+ },
+ hasAvatar() {
+ return this.project.avatar_url !== null;
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ class="group-row project-row"
+ :id="projectDomId"
+ :class="rowClass"
+ >
+ <div class="group-row-contents project-row-contents">
+ <div class="stats">
+ <span class="project-stars">
+ <i
+ class="fa fa-star"
+ aria-hidden="true"/>
+ {{project.star_count}}
+ </span>
+ <span
+ v-tooltip
+ class="project-visibility"
+ data-placement="left"
+ data-container="body"
+ :title="visibilityTooltip"
+ >
+ <i
+ class="fa"
+ :class="visibilityIcon"
+ aria-hidden="true"/>
+ </span>
+ </div>
+ <div class="folder-toggle-wrap">
+ <span class="folder-icon">
+ <i
+ class="fa fa-bookmark"
+ aria-hidden="true"/>
+ </span>
+ </div>
+ <div class="avatar-container s40 hidden-xs">
+ <a
+ :href="project.project_path"
+ >
+ <img
+ v-if="hasAvatar"
+ class="avatar s40"
+ alt="Project Avatar"
+ :src="project.avatar_url"
+ />
+ <identicon
+ v-else
+ :entity-id=project.id
+ :entity-name="project.name"
+ />
+ </a>
+ </div>
+ <div class="metadata">
+ <div class="title">
+ <a
+ :href="project.project_path"
+ >
+ {{project.name}}
+ </a>
+ </div>
+ <div
+ v-if="project.description"
+ class="description"
+ >
+ {{project.description}}
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
new file mode 100644
index 00000000000..c4c5eab011e
--- /dev/null
+++ b/app/assets/javascripts/groups/constants.js
@@ -0,0 +1,19 @@
+export const MAX_PROJECT_COUNT = 10;
+
+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',
+};
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index 439a931ddad..041f4fc54f3 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -2,12 +2,12 @@ import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
- constructor({ form, filter, holder, filterEndpoint, pagePath }) {
+ constructor({ form, filter, holder, filterDropdownSel, filterEndpoint, pagePath }) {
super(form, filter, holder);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
- this.$dropdown = $('.js-group-filter-dropdown-wrap');
+ this.$dropdown = $(filterDropdownSel);
}
getFilterEndpoint() {
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 00e1bd94c9c..2577d014364 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -1,6 +1,7 @@
/* global Flash */
import Vue from 'vue';
+import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
@@ -9,6 +10,8 @@ import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
+Vue.use(Translate);
+
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
@@ -40,6 +43,12 @@ document.addEventListener('DOMContentLoaded', () => {
isEmpty() {
return Object.keys(this.state.groups).length === 0;
},
+ isGroupsListEmpty() {
+ return this.isEmpty && !this.isLoading;
+ },
+ isGroupsListLoaded() {
+ return Object.keys(this.state.groups).length > 0 && !this.isLoading;
+ },
},
methods: {
fetchGroups(parentGroup) {
@@ -159,14 +168,16 @@ document.addEventListener('DOMContentLoaded', () => {
},
beforeMount() {
let groupFilterList = null;
+
const form = document.querySelector('form#group-filter-form');
const filter = document.querySelector('.js-groups-list-filter');
- const holder = document.querySelector('.js-groups-list-holder');
+ const holder = document.querySelector('.groups-list-holder');
const opts = {
form,
filter,
holder,
+ filterDropdownSel: '.js-group-filter-dropdown-wrap',
filterEndpoint: el.dataset.endpoint,
pagePath: el.dataset.path,
};
diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js
index 6eab6083e8f..e790c420ee2 100644
--- a/app/assets/javascripts/groups/stores/groups_store.js
+++ b/app/assets/javascripts/groups/stores/groups_store.js
@@ -59,7 +59,7 @@ export default class GroupsStore {
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
+ mappedGroups[`id${currentGroup.parentId}`].isOpen = false; // Keep group always collapsed
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[`id${currentGroup.id}`] = currentGroup;
} else {
@@ -93,7 +93,7 @@ export default class GroupsStore {
currentOrphan.id !== group.id
) {
group.subGroups[currentOrphan.id] = currentOrphan;
- group.isOpen = true;
+ group.isOpen = false;
currentOrphan.isOrphan = true;
found = true;
@@ -120,7 +120,7 @@ export default class GroupsStore {
}
decorateGroups(rawGroups) {
- this.groups = rawGroups.map(this.decorateGroup);
+ this.groups = rawGroups.map(this.decorateGroup.bind(this));
return this.groups;
}
@@ -132,7 +132,7 @@ export default class GroupsStore {
fullPath: rawGroup.full_path,
avatarUrl: rawGroup.avatar_url,
name: rawGroup.name,
- hasSubgroups: rawGroup.has_subgroups,
+ hasSubgroups: rawGroup.subgroup_count > 0,
canEdit: rawGroup.can_edit,
description: rawGroup.description,
webUrl: rawGroup.web_url,
@@ -149,6 +149,9 @@ export default class GroupsStore {
humanGroupAccess: rawGroup.permissions.human_group_access,
},
subGroups: {},
+ projectCount: rawGroup.project_count,
+ subGroupCount: rawGroup.subgroup_count,
+ projects: rawGroup.projects,
};
}