diff options
Diffstat (limited to 'app/assets')
-rw-r--r-- | app/assets/javascripts/groups/components/group_folder.vue | 8 | ||||
-rw-r--r-- | app/assets/javascripts/groups/components/group_item.vue | 173 | ||||
-rw-r--r-- | app/assets/javascripts/groups/components/groups.vue | 1 | ||||
-rw-r--r-- | app/assets/javascripts/groups/components/project_folder.vue | 67 | ||||
-rw-r--r-- | app/assets/javascripts/groups/components/project_item.vue | 109 | ||||
-rw-r--r-- | app/assets/javascripts/groups/constants.js | 19 | ||||
-rw-r--r-- | app/assets/javascripts/groups/groups_filterable_list.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/groups/index.js | 13 | ||||
-rw-r--r-- | app/assets/javascripts/groups/stores/groups_store.js | 11 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/lists.scss | 97 |
10 files changed, 400 insertions, 102 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, }; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 0fb19344510..8e87704b96d 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -107,7 +107,7 @@ ul.content-list { color: $list-text-color; &.no-description { - .title { + > .group-row-contents .metadata .title { line-height: $list-text-height; } } @@ -116,6 +116,22 @@ ul.content-list { font-weight: $gl-font-weight-bold; } + > .group-row-contents .metadata { + .title { + line-height: inherit; + font-weight: 600; + + .access-type { + color: $gl-text-color-secondary; + } + } + + .description { + @include str-truncated; + margin-bottom: 0; + } + } + a { color: $gl-text-color; } @@ -124,13 +140,6 @@ ul.content-list { color: $blue-600; } - .description { - p { - @include str-truncated; - margin-bottom: 0; - } - } - .controls { @include new-style-dropdown; @@ -322,7 +331,7 @@ ul.indent-list { width: 0; position: absolute; top: 5px; - bottom: 0; + bottom: 30px; left: -16px; border-left: 2px solid $border-white-normal; } @@ -331,14 +340,7 @@ ul.indent-list { position: relative; &::before { - content: ""; - display: block; - width: 10px; - height: 0; - border-top: 2px solid $border-white-normal; - position: absolute; top: 30px; - left: -16px; } &:last-child::before { @@ -353,34 +355,67 @@ ul.indent-list { .group-row { padding: 0; border: none; - - &:last-of-type { - .group-row-contents:not(:hover) { - border-bottom: 1px solid transparent; - } - } } .group-row-contents { padding: 10px 10px 8px; - border-top: solid 1px transparent; - border-bottom: solid 1px $white-normal; + + .avatar-container > a { + width: 100%; + } + } + + .group-list-tree .group-row, + .content-list li.has-more-items-link { + &::before { + content: ""; + display: block; + width: 10px; + height: 0; + border-top: 2px solid $border-white-normal; + position: absolute; + left: -16px; + } + } + + .group-row-contents, + .content-list li.has-more-items-link { + border-top: solid 1px $white-normal; + border-bottom: solid 1px transparent; &:hover { border-color: $row-hover-border; background-color: $row-hover; cursor: pointer; } + } - .avatar-container > a { - width: 100%; + .content-list li.has-more-items-link { + padding: 19px; + font-weight: normal; + color: $gl-text-color-secondary; + + &::before { + bottom: 30px; } } -} -.js-groups-list-holder { - .groups-list-loading { - font-size: 34px; - text-align: center; + .project-list.has-sibling-groups { + &::before { + top: -30px; + bottom: 30px; + } } + + .group-list-tree.has-sibling-projects { + > .group-row:last-of-type::before { + height: 0; + } + } +} + +.projects-list-holder .projects-list-loading, +.groups-list-holder .groups-list-loading { + font-size: 34px; + text-align: center; } |