diff --git a/app/assets/stylesheets/content/_attributes_key_value.sass b/app/assets/stylesheets/content/_attributes_key_value.sass index 076f9f207c..006538cc61 100644 --- a/app/assets/stylesheets/content/_attributes_key_value.sass +++ b/app/assets/stylesheets/content/_attributes_key_value.sass @@ -44,6 +44,10 @@ @include text-shortener flex: 0 1 auto + &.-minimal + @include text-shortener + flex: 0 1 auto + .attributes-key-value--value-container display: flex flex: 1 0 65% diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index 8b8f589d35..d226b70261 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -43,6 +43,7 @@ module Queries::Projects register.filter query, filters::LatestActivityAtFilter register.filter query, filters::PrincipalFilter register.filter query, filters::ParentFilter + register.filter query, filters::IdFilter register.order query, orders::DefaultOrder register.order query, orders::LatestActivityAtOrder diff --git a/app/models/queries/projects/filters/id_filter.rb b/app/models/queries/projects/filters/id_filter.rb new file mode 100644 index 0000000000..6ed494afe0 --- /dev/null +++ b/app/models/queries/projects/filters/id_filter.rb @@ -0,0 +1,35 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +class Queries::Projects::Filters::IdFilter < Queries::Projects::Filters::ProjectFilter + def type + :integer + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 69f1d6d37f..951727486f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1404,6 +1404,7 @@ en: label_member_new: "New member" label_member_all_admin: "(All roles due to admin status)" label_member_plural: "Members" + # TODO: remove once overview page is replaced label_view_all_members: "View all members" label_menu_item_name: "Name of menu item" label_message: "Message" diff --git a/docs/api/apiv3/endpoints/projects.apib b/docs/api/apiv3/endpoints/projects.apib index 841a641a6a..67dbc2a9d4 100644 --- a/docs/api/apiv3/endpoints/projects.apib +++ b/docs/api/apiv3/endpoints/projects.apib @@ -225,6 +225,7 @@ Returns a collection of projects. The collection can be filtered via query param + parent_id: filters projects by their parent. + principal: based on members of the project. + type_id: based on the types active in a project. + + id: based on projects' id. There might also be additional filters based on the custom fields that have been configured. diff --git a/frontend/src/app/modules/common/path-helper/apiv3/apiv3-paths.ts b/frontend/src/app/modules/common/path-helper/apiv3/apiv3-paths.ts index 8f1895492d..9d2cc3b4e2 100644 --- a/frontend/src/app/modules/common/path-helper/apiv3/apiv3-paths.ts +++ b/frontend/src/app/modules/common/path-helper/apiv3/apiv3-paths.ts @@ -41,6 +41,8 @@ import {Apiv3GridsPaths} from "core-app/modules/common/path-helper/apiv3/grids/a import {Apiv3NewsesPaths} from "core-app/modules/common/path-helper/apiv3/news/apiv3-newses-paths"; import {Apiv3TimeEntriesPaths} from "core-app/modules/common/path-helper/apiv3/time-entries/apiv3-time-entries-paths"; import {Apiv3VersionPaths} from "core-app/modules/common/path-helper/apiv3/versions/apiv3-version-paths"; +import {Apiv3MembershipsPaths} from "core-app/modules/common/path-helper/apiv3/memberships/apiv3-memberships-paths"; +import {Apiv3GroupsPaths} from "core-app/modules/common/path-helper/apiv3/groups/apiv3-groups-path"; export class ApiV3Paths { // Base path @@ -67,6 +69,9 @@ export class ApiV3Paths { // /api/v3/time_entries public readonly time_entries = new Apiv3TimeEntriesPaths(this.apiV3Base); + // /api/v3/memberships + public readonly memberships = new Apiv3MembershipsPaths(this.apiV3Base); + // /api/v3/news public readonly news = new Apiv3NewsesPaths(this.apiV3Base); @@ -94,6 +99,10 @@ export class ApiV3Paths { // /api/v3/grids public readonly grids = new Apiv3GridsPaths(this.apiV3Base); + // /api/v3/groups + public readonly groups = new Apiv3GroupsPaths(this.apiV3Base); + + constructor(readonly appBasePath:string) { } diff --git a/frontend/src/app/modules/common/path-helper/apiv3/groups/apiv3-groups-path.ts b/frontend/src/app/modules/common/path-helper/apiv3/groups/apiv3-groups-path.ts new file mode 100644 index 0000000000..13ebad22f3 --- /dev/null +++ b/frontend/src/app/modules/common/path-helper/apiv3/groups/apiv3-groups-path.ts @@ -0,0 +1,41 @@ +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import { + SimpleResourceCollection +} from 'core-app/modules/common/path-helper/apiv3/path-resources'; + +export class Apiv3GroupsPaths extends SimpleResourceCollection { + constructor(basePath:string) { + super(basePath, 'groups'); + } + + public toString():string { + throw 'Backend does not yet offer this end point'; + } +} diff --git a/frontend/src/app/modules/common/path-helper/apiv3/memberships/apiv3-memberships-paths.ts b/frontend/src/app/modules/common/path-helper/apiv3/memberships/apiv3-memberships-paths.ts new file mode 100644 index 0000000000..f4d093959a --- /dev/null +++ b/frontend/src/app/modules/common/path-helper/apiv3/memberships/apiv3-memberships-paths.ts @@ -0,0 +1,39 @@ +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import {SimpleResourceCollection} from 'core-app/modules/common/path-helper/apiv3/path-resources'; + +export class Apiv3MembershipsPaths extends SimpleResourceCollection { + constructor(basePath:string) { + super(basePath, 'memberships'); + } + + // /api/v3/memberships/available_projects + public readonly available_projects = this.path + '/available_projects'; + +} diff --git a/frontend/src/app/modules/common/path-helper/path-helper.service.ts b/frontend/src/app/modules/common/path-helper/path-helper.service.ts index 6ce7fcfc81..049fbd4666 100644 --- a/frontend/src/app/modules/common/path-helper/path-helper.service.ts +++ b/frontend/src/app/modules/common/path-helper/path-helper.service.ts @@ -102,6 +102,10 @@ export class PathHelperService { return this.projectPath(projectId) + '/work_packages/calendar'; } + public projectMembershipsPath(projectId:string) { + return this.projectPath(projectId) + '/members'; + } + public projectNewsPath(projectId:string) { return this.projectPath(projectId) + '/news'; } diff --git a/frontend/src/app/modules/grids/openproject-grids.module.ts b/frontend/src/app/modules/grids/openproject-grids.module.ts index a1b7a17c72..7a3ae9a786 100644 --- a/frontend/src/app/modules/grids/openproject-grids.module.ts +++ b/frontend/src/app/modules/grids/openproject-grids.module.ts @@ -65,6 +65,7 @@ import {WidgetProjectDetailsComponent} from "core-app/modules/grids/widgets/proj import {WidgetTimeEntriesProjectComponent} from "core-app/modules/grids/widgets/time-entries-current-user/project/time-entries-project.component"; import {WidgetSubprojectsComponent} from "core-app/modules/grids/widgets/subprojects/subprojects.component"; import {OpenprojectAttachmentsModule} from "core-app/modules/attachments/openproject-attachments.module"; +import {WidgetMembersComponent} from "core-app/modules/grids/widgets/members/members.component"; export const GRID_ROUTES:Ng2StateDeclaration[] = [ { @@ -95,6 +96,7 @@ export const GRID_ROUTES:Ng2StateDeclaration[] = [ DynamicModule.withComponents([ WidgetCustomTextComponent, WidgetDocumentsComponent, + WidgetMembersComponent, WidgetNewsComponent, WidgetWpTableQuerySpaceComponent, WidgetWpGraphComponent, @@ -128,6 +130,7 @@ export const GRID_ROUTES:Ng2StateDeclaration[] = [ // Widgets WidgetCustomTextComponent, WidgetDocumentsComponent, + WidgetMembersComponent, WidgetNewsComponent, WidgetWpCalendarComponent, WidgetWpOverviewComponent, @@ -298,6 +301,14 @@ export function registerWidgets(injector:Injector) { name: i18n.t('js.grid.widgets.documents.title') } }, + { + identifier: 'members', + component: WidgetMembersComponent, + title: i18n.t(`js.grid.widgets.members.title`), + properties: { + name: i18n.t('js.grid.widgets.members.title') + } + }, { identifier: 'news', component: WidgetNewsComponent, diff --git a/frontend/src/app/modules/grids/widgets/members/members.component.html b/frontend/src/app/modules/grids/widgets/members/members.component.html new file mode 100644 index 0000000000..409b01d9a1 --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/members/members.component.html @@ -0,0 +1,64 @@ + + + + + + +
+ + +
+
+ {{usersByRole.role.name}}: +
+
+
+ + + + {{userName(principal)}} + + + + + + + + + , + +
+
+
+
+ {{moreMembersText}} +
+
+ + diff --git a/frontend/src/app/modules/grids/widgets/members/members.component.sass b/frontend/src/app/modules/grids/widgets/members/members.component.sass new file mode 100644 index 0000000000..62d42fa591 --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/members/members.component.sass @@ -0,0 +1,9 @@ +.members-widget--notification + font-style: italic + margin-top: 0.5rem + +.grid--widget-footer + margin-top: 0.5rem + + .button + margin-bottom: 0 diff --git a/frontend/src/app/modules/grids/widgets/members/members.component.ts b/frontend/src/app/modules/grids/widgets/members/members.component.ts new file mode 100644 index 0000000000..0d9bd24648 --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/members/members.component.ts @@ -0,0 +1,139 @@ +import {AbstractWidgetComponent} from "core-app/modules/grids/widgets/abstract-widget.component"; +import {Component, OnInit, ChangeDetectorRef, Injector, ChangeDetectionStrategy} from '@angular/core'; +import {I18nService} from "core-app/modules/common/i18n/i18n.service"; +import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; +import {UserResource} from "core-app/modules/hal/resources/user-resource"; +import {CurrentProjectService} from "core-components/projects/current-project.service"; +import {DmListParameter} from "core-app/modules/hal/dm-services/dm.service.interface"; +import {MembershipResource} from "core-app/modules/hal/resources/membership-resource"; +import {MembershipDmService} from "core-app/modules/hal/dm-services/membership-dm.service"; +import {RoleResource} from "core-app/modules/hal/resources/role-resource"; + +const DISPLAYED_MEMBERS_LIMIT = 100; + +@Component({ + templateUrl: './members.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./members.component.sass'] +}) +export class WidgetMembersComponent extends AbstractWidgetComponent implements OnInit { + public text = { + add: this.i18n.t('js.grid.widgets.members.add'), + noResults: this.i18n.t('js.grid.widgets.members.no_results'), + viewAll: this.i18n.t('js.grid.widgets.members.view_all_members'), + }; + + public totalMembers:number; + public entriesByRoles:{[roleId:string]:{role:RoleResource, users:UserResource[]}} = {}; + private entriesLoaded = false; + public membersAddable:boolean = false; + + constructor(readonly pathHelper:PathHelperService, + readonly i18n:I18nService, + protected readonly injector:Injector, + readonly membershipDm:MembershipDmService, + readonly currentProject:CurrentProjectService, + readonly cdr:ChangeDetectorRef) { + super(i18n, injector); + } + + ngOnInit() { + this.membershipDm + .list(this.listMembersParams) + .then(collection => { + this.partitionEntriesByRole(collection.elements); + this.sortUsersByName(); + this.totalMembers = collection.total; + + this.entriesLoaded = true; + this.cdr.detectChanges(); + }); + + this.membershipDm + .listAvailableProjects(this.listAvailableProjectsParams) + .then(collection => { + this.membersAddable = collection.total > 0; + }) + .catch(() => { + // nothing bad, the user is just not allowed to add members to the project + + }); + } + + public userPath(user:UserResource) { + return this.pathHelper.userPath(user.id!); + } + + public userName(user:UserResource) { + return user.name; + } + + public get noMembers() { + return this.entriesLoaded && !Object.keys(this.entriesByRoles).length; + } + + public get moreMembers() { + return this.entriesLoaded && this.totalMembers > DISPLAYED_MEMBERS_LIMIT; + } + + public get moreMembersText() { + return I18n.t( + 'js.grid.widgets.members.too_many', + { count: DISPLAYED_MEMBERS_LIMIT, total: this.totalMembers } + ); + } + + public get projectMembershipsPath() { + return this.pathHelper.projectMembershipsPath(this.currentProject.identifier!); + } + + public get usersByRole() { + return Object.values(this.entriesByRoles); + } + + public isGroup(principal:UserResource) { + return this.pathHelper.api.v3.groups.id(principal.id!).toString() === principal.href; + } + + private partitionEntriesByRole(memberships:MembershipResource[]) { + memberships.forEach(membership => { + membership.roles.forEach((role) => { + if (!this.entriesByRoles[role.id!]) { + this.entriesByRoles[role.id!] = { role: role, users: [] }; + } + + this.entriesByRoles[role.id!].users.push(membership.principal); + }); + }); + } + + private sortUsersByName() { + Object.values(this.entriesByRoles).forEach(entry => { + entry.users.sort((a, b) => { + return this.userName(a).localeCompare(this.userName(b)); + }); + }); + } + + private get listMembersParams() { + let params:DmListParameter = { sortBy: [['created_on', 'desc']], pageSize: DISPLAYED_MEMBERS_LIMIT }; + + if (this.currentProject.id) { + params['filters'] = [['project_id', '=', [this.currentProject.id]]]; + } + + return params; + } + + private get listAvailableProjectsParams() { + // It would make sense to set the pageSize but the backend for projects + // returns an upaginated list which does not support that. + let params:DmListParameter = {}; + + if (this.currentProject.id) { + params['filters'] = [['id', '=', [this.currentProject.id]]]; + } + + return params; + } +} diff --git a/frontend/src/app/modules/grids/widgets/news/news.component.ts b/frontend/src/app/modules/grids/widgets/news/news.component.ts index b224595cff..1947fde7cc 100644 --- a/frontend/src/app/modules/grids/widgets/news/news.component.ts +++ b/frontend/src/app/modules/grids/widgets/news/news.component.ts @@ -9,7 +9,6 @@ import {UserCacheService} from "core-components/user/user-cache.service"; import {UserResource} from "core-app/modules/hal/resources/user-resource"; import {NewsDmService} from "core-app/modules/hal/dm-services/news-dm.service"; import {CurrentProjectService} from "core-components/projects/current-project.service"; -import {FilterOperator} from "core-components/api/api-v3/api-v3-filter-builder"; import {DmListParameter} from "core-app/modules/hal/dm-services/dm.service.interface"; @Component({ diff --git a/frontend/src/app/modules/hal/dm-services/abstract-dm.service.ts b/frontend/src/app/modules/hal/dm-services/abstract-dm.service.ts index af6b610c73..fde21f2aad 100644 --- a/frontend/src/app/modules/hal/dm-services/abstract-dm.service.ts +++ b/frontend/src/app/modules/hal/dm-services/abstract-dm.service.ts @@ -42,13 +42,27 @@ export abstract class AbstractDmService implements DmServ } public list(params:DmListParameter|null):Promise> { + return this.listRequest(this.listUrl(), params) as Promise>; + } + + + public one(id:number):Promise { + return this.halResourceService.get(this.oneUrl(id).toString()).toPromise(); + } + + protected listRequest(url:string, params:DmListParameter|null) { + return this.halResourceService.get(url + this.listParamsString(params)).toPromise(); + } + + protected listParamsString(params:DmListParameter|null):string { let queryProps = []; if (params && params.sortBy) { queryProps.push(`sortBy=${JSON.stringify(params.sortBy)}`); } - if (params && params.pageSize) { + // 0 should not be treated as false + if (params && params.pageSize !== undefined) { queryProps.push(`pageSize=${params.pageSize}`); } @@ -68,11 +82,7 @@ export abstract class AbstractDmService implements DmServ queryPropsString = `?${queryProps.join('&')}`; } - return this.halResourceService.get>(this.listUrl() + queryPropsString).toPromise(); - } - - public one(id:number):Promise { - return this.halResourceService.get(this.oneUrl(id).toString()).toPromise(); + return queryPropsString; } diff --git a/frontend/src/app/modules/hal/dm-services/membership-dm.service.ts b/frontend/src/app/modules/hal/dm-services/membership-dm.service.ts new file mode 100644 index 0000000000..a5a30b254c --- /dev/null +++ b/frontend/src/app/modules/hal/dm-services/membership-dm.service.ts @@ -0,0 +1,53 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +//++ + +import {Injectable} from '@angular/core'; +import {AbstractDmService} from "core-app/modules/hal/dm-services/abstract-dm.service"; +import {MembershipResource} from "core-app/modules/hal/resources/membership-resource"; +import {DmListParameter} from "core-app/modules/hal/dm-services/dm.service.interface"; +import {CollectionResource} from "core-app/modules/hal/resources/collection-resource"; +import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; + +@Injectable() +export class MembershipDmService extends AbstractDmService { + public listAvailableProjects(params:DmListParameter|null) { + return this.listRequest(this.availableProjectsUrl(), params) as Promise>; + } + + protected listUrl() { + return this.pathHelper.api.v3.memberships.toString(); + } + + protected oneUrl(id:number|string) { + return this.pathHelper.api.v3.memberships.id(id).toString(); + } + + protected availableProjectsUrl() { + return this.pathHelper.api.v3.memberships.available_projects.toString(); + } +} diff --git a/frontend/src/app/modules/hal/openproject-hal.module.ts b/frontend/src/app/modules/hal/openproject-hal.module.ts index 9f0e02c4cf..16277ed55a 100644 --- a/frontend/src/app/modules/hal/openproject-hal.module.ts +++ b/frontend/src/app/modules/hal/openproject-hal.module.ts @@ -52,6 +52,7 @@ import {NewsDmService} from './dm-services/news-dm.service'; import {StatusDmService} from "core-app/modules/hal/dm-services/status-dm.service"; import {VersionDmService} from "core-app/modules/hal/dm-services/version-dm.service"; import {QueryOrderDmService} from "core-app/modules/hal/dm-services/query-order-dm.service"; +import {MembershipDmService} from "core-app/modules/hal/dm-services/membership-dm.service"; @NgModule({ imports: [ @@ -67,6 +68,7 @@ import {QueryOrderDmService} from "core-app/modules/hal/dm-services/query-order- ConfigurationDmService, GridDmService, HelpTextDmService, + MembershipDmService, NewsDmService, PayloadDmService, ProjectDmService, diff --git a/frontend/src/app/modules/hal/resources/membership-resource.ts b/frontend/src/app/modules/hal/resources/membership-resource.ts new file mode 100644 index 0000000000..73dc384591 --- /dev/null +++ b/frontend/src/app/modules/hal/resources/membership-resource.ts @@ -0,0 +1,49 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +//++ + +import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; +import {UserResource} from "core-app/modules/hal/resources/user-resource"; +import {RoleResource} from "core-app/modules/hal/resources/role-resource"; +import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; + +export interface MembershipResourceLinks { + update(payload:unknown):Promise; + updateImmediately(payload:unknown):Promise; + delete():Promise; +} + +export interface MembershipResourceEmbedded { + principal:UserResource; + roles:RoleResource[]; + project:ProjectResource; +} + +export class MembershipResource extends HalResource { +} + +export interface MembershipResource extends MembershipResourceLinks, MembershipResourceEmbedded {} diff --git a/frontend/src/app/modules/hal/resources/role-resource.ts b/frontend/src/app/modules/hal/resources/role-resource.ts new file mode 100644 index 0000000000..568274cfa7 --- /dev/null +++ b/frontend/src/app/modules/hal/resources/role-resource.ts @@ -0,0 +1,32 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +//++ + +import {HalResource} from 'core-app/modules/hal/resources/hal-resource'; + +export class RoleResource extends HalResource { +} diff --git a/frontend/src/app/modules/hal/services/hal-resource.config.ts b/frontend/src/app/modules/hal/services/hal-resource.config.ts index ef9a684724..1e62fba725 100644 --- a/frontend/src/app/modules/hal/services/hal-resource.config.ts +++ b/frontend/src/app/modules/hal/services/hal-resource.config.ts @@ -59,6 +59,8 @@ import {GridResource} from "core-app/modules/hal/resources/grid-resource"; import {TimeEntryResource} from "core-app/modules/hal/resources/time-entry-resource"; import {NewsResource} from "core-app/modules/hal/resources/news-resource"; import {VersionResource} from "core-app/modules/hal/resources/version-resource"; +import {MembershipResource} from "core-app/modules/hal/resources/membership-resource"; +import {RoleResource} from "core-app/modules/hal/resources/role-resource"; const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInterface } = { WorkPackage: { @@ -166,6 +168,9 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter Post: { cls: PostResource }, + Role: { + cls: RoleResource + }, Grid: { cls: GridResource, }, @@ -175,6 +180,9 @@ const halResourceDefaultConfig:{ [typeName:string]:HalResourceFactoryConfigInter TimeEntry: { cls: TimeEntryResource }, + Membership: { + cls: MembershipResource + }, News: { cls: NewsResource }, diff --git a/lib/api/v3/memberships/available_projects_api.rb b/lib/api/v3/memberships/available_projects_api.rb index 1242e960b7..088b098c41 100644 --- a/lib/api/v3/memberships/available_projects_api.rb +++ b/lib/api/v3/memberships/available_projects_api.rb @@ -30,7 +30,7 @@ module API module V3 module Memberships class AvailableProjectsAPI < ::API::OpenProjectAPI - before do + after_validation do authorize :manage_members, global: true end diff --git a/modules/dashboards/lib/dashboards/grid_registration.rb b/modules/dashboards/lib/dashboards/grid_registration.rb index e2b536cd2e..e7a5216912 100644 --- a/modules/dashboards/lib/dashboards/grid_registration.rb +++ b/modules/dashboards/lib/dashboards/grid_registration.rb @@ -11,6 +11,7 @@ module Dashboards 'work_packages_calendar', 'work_packages_overview', 'time_entries_project', + 'members', 'news', 'documents', 'custom_text' @@ -56,6 +57,12 @@ module Dashboards allowed view_work_packages_lambda end + widget_strategy 'members' do + allowed ->(user, project) { + user.allowed_to?(:view_members, project) + } + end + widget_strategy 'news' do allowed ->(user, project) { user.allowed_to?(:view_news, project) diff --git a/modules/dashboards/spec/features/members_spec.rb b/modules/dashboards/spec/features/members_spec.rb new file mode 100644 index 0000000000..d45d59fad7 --- /dev/null +++ b/modules/dashboards/spec/features/members_spec.rb @@ -0,0 +1,148 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2018 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2017 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See docs/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +require_relative '../support/pages/dashboard' + +describe 'Members widget on dashboard', type: :feature, js: true do + let!(:project) { FactoryBot.create :project } + let!(:other_project) { FactoryBot.create :project } + + let!(:manager_user) do + FactoryBot.create :user, lastname: "Manager", member_in_project: project, member_through_role: role + end + let!(:no_edit_member_user) do + FactoryBot.create :user, lastname: "No_Edit", member_in_project: project, member_through_role: no_edit_member_role + end + let!(:no_view_member_user) do + FactoryBot.create :user, lastname: "No_View", member_in_project: project, member_through_role: no_view_member_role + end + let!(:invisible_user) do + FactoryBot.create :user, lastname: "Invisible", member_in_project: other_project, member_through_role: role + end + + let(:no_view_member_role) do + FactoryBot.create(:role, + permissions: %i[manage_dashboards + view_dashboards]) + end + let(:no_edit_member_role) do + FactoryBot.create(:role, + permissions: %i[manage_dashboards + view_dashboards + view_members]) + end + let(:role) do + FactoryBot.create(:role, + permissions: %i[manage_dashboards + view_dashboards + manage_members + view_members]) + end + let(:dashboard) do + Pages::Dashboard.new(project) + end + + before do + login_as manager_user + + dashboard.visit! + end + + def expect_all_members_visible(area) + within area do + expect(page) + .to have_content role.name + expect(page) + .to have_content manager_user.name + expect(page) + .to have_content no_edit_member_role + expect(page) + .to have_content no_edit_member_user.name + expect(page) + .to have_content no_view_member_role + expect(page) + .to have_content no_view_member_user.name + end + end + + it 'can add the widget and see the members if the permissions suffice' do + # within top-right area, add an additional widget + dashboard.add_widget(1, 3, 'Members') + + members_area = Components::Grids::GridArea.new('.grid--area.-widgeted:nth-of-type(1)') + members_area.expect_to_span(1, 3, 4, 5) + + members_area.resize_to(7, 4) + + expect_all_members_visible(members_area.area) + + expect(page) + .not_to have_content invisible_user.name + + within members_area.area do + expect(page) + .to have_link('Member') + end + + # A user without edit permission will see the members but cannot add one + login_as no_edit_member_user + + visit root_path + dashboard.visit! + + expect_all_members_visible(members_area.area) + + within members_area.area do + expect(page) + .to have_no_link('Member') + end + + # A user without view permission will not see any members + login_as no_view_member_user + + visit root_path + + dashboard.visit! + + within members_area.area do + expect(page) + .to have_no_content manager_user.name + + expect(page) + .to have_content('No visible members') + + expect(page) + .to have_no_link('Member') + + expect(page) + .to have_no_link('View all members') + end + end +end diff --git a/modules/grids/config/locales/js-en.yml b/modules/grids/config/locales/js-en.yml index e83bdf6361..e3873bcece 100644 --- a/modules/grids/config/locales/js-en.yml +++ b/modules/grids/config/locales/js-en.yml @@ -10,6 +10,12 @@ en: documents: title: 'Documents' no_results: 'No documents yet.' + members: + title: 'Members' + no_results: 'No visible members.' + view_all_members: 'View all members' + add: 'Member' + too_many: 'Displaying %{count} of %{total} members.' news: title: 'News' at: 'at'