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'