Merge pull request #7541 from opf/feature/members_widget_on_dashboard

add members widget to dashboard

[ci skip]
pull/7544/head
Oliver Günther 5 years ago committed by GitHub
commit b66e4ae676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/assets/stylesheets/content/_attributes_key_value.sass
  2. 1
      app/models/queries/projects.rb
  3. 35
      app/models/queries/projects/filters/id_filter.rb
  4. 1
      config/locales/en.yml
  5. 1
      docs/api/apiv3/endpoints/projects.apib
  6. 9
      frontend/src/app/modules/common/path-helper/apiv3/apiv3-paths.ts
  7. 41
      frontend/src/app/modules/common/path-helper/apiv3/groups/apiv3-groups-path.ts
  8. 39
      frontend/src/app/modules/common/path-helper/apiv3/memberships/apiv3-memberships-paths.ts
  9. 4
      frontend/src/app/modules/common/path-helper/path-helper.service.ts
  10. 11
      frontend/src/app/modules/grids/openproject-grids.module.ts
  11. 64
      frontend/src/app/modules/grids/widgets/members/members.component.html
  12. 9
      frontend/src/app/modules/grids/widgets/members/members.component.sass
  13. 139
      frontend/src/app/modules/grids/widgets/members/members.component.ts
  14. 1
      frontend/src/app/modules/grids/widgets/news/news.component.ts
  15. 22
      frontend/src/app/modules/hal/dm-services/abstract-dm.service.ts
  16. 53
      frontend/src/app/modules/hal/dm-services/membership-dm.service.ts
  17. 2
      frontend/src/app/modules/hal/openproject-hal.module.ts
  18. 49
      frontend/src/app/modules/hal/resources/membership-resource.ts
  19. 32
      frontend/src/app/modules/hal/resources/role-resource.ts
  20. 8
      frontend/src/app/modules/hal/services/hal-resource.config.ts
  21. 2
      lib/api/v3/memberships/available_projects_api.rb
  22. 7
      modules/dashboards/lib/dashboards/grid_registration.rb
  23. 148
      modules/dashboards/spec/features/members_spec.rb
  24. 6
      modules/grids/config/locales/js-en.yml

@ -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%

@ -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

@ -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

@ -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"

@ -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.

@ -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) {
}

@ -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';
}
}

@ -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';
}

@ -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';
}

@ -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,

@ -0,0 +1,64 @@
<widget-header
[name]="widgetName"
[icon]="'group'"
(onRenamed)="renameWidget($event)">
<widget-menu
[resource]="resource">
</widget-menu>
</widget-header>
<div class="grid--widget-content">
<no-results *ngIf="noMembers"
[title]="text.noResults">
</no-results>
<div class="attributes-key-value" *ngFor="let usersByRole of usersByRole">
<div class="attributes-key-value--key -minimal">
{{usersByRole.role.name}}:
</div>
<div class="attributes-key-value--value-container">
<div class="attributes-key-value--value">
<span *ngFor="let principal of usersByRole.users; let last = last">
<ng-container *ngIf="isGroup(principal)">
{{userName(principal)}}
</ng-container>
<ng-container *ngIf="!isGroup(principal)">
<user-avatar [user]="principal"
data-class-list="avatar avatar-mini -spaced">
</user-avatar>
<a [href]="userPath(principal)"
[textContent]="userName(principal)">
</a>
</ng-container>
<ng-container *ngIf="!last">, </ng-container>
</span>
</div>
</div>
</div>
<div *ngIf="moreMembers"
class="members-widget--notification">
{{moreMembersText}}
</div>
</div>
<div class="grid--widget-footer">
<a *ngIf="membersAddable"
[href]="projectMembershipsPath + '?show_add_members=true'"
class="button -alt-highlight">
<i class="button--icon icon-add" aria-hidden="true"></i>
<span class="button--text"
[textContent]="text.add">
</span>
</a>
<a *ngIf="!noMembers"
[href]="projectMembershipsPath"
class="button -highlight-inverted">
<i class="button--icon icon-group" aria-hidden="true"></i>
<span class="button--text"
[textContent]="text.viewAll">
</span>
</a>
</div>

@ -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

@ -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;
}
}

@ -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({

@ -42,13 +42,27 @@ export abstract class AbstractDmService<T extends HalResource> implements DmServ
}
public list(params:DmListParameter|null):Promise<CollectionResource<T>> {
return this.listRequest(this.listUrl(), params) as Promise<CollectionResource<T>>;
}
public one(id:number):Promise<T> {
return this.halResourceService.get<T>(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<T extends HalResource> implements DmServ
queryPropsString = `?${queryProps.join('&')}`;
}
return this.halResourceService.get<CollectionResource<T>>(this.listUrl() + queryPropsString).toPromise();
}
public one(id:number):Promise<T> {
return this.halResourceService.get<T>(this.oneUrl(id).toString()).toPromise();
return queryPropsString;
}

@ -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<MembershipResource> {
public listAvailableProjects(params:DmListParameter|null) {
return this.listRequest(this.availableProjectsUrl(), params) as Promise<CollectionResource<ProjectResource>>;
}
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();
}
}

@ -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,

@ -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<unknown>;
updateImmediately(payload:unknown):Promise<unknown>;
delete():Promise<unknown>;
}
export interface MembershipResourceEmbedded {
principal:UserResource;
roles:RoleResource[];
project:ProjectResource;
}
export class MembershipResource extends HalResource {
}
export interface MembershipResource extends MembershipResourceLinks, MembershipResourceEmbedded {}

@ -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 {
}

@ -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
},

@ -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

@ -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)

@ -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

@ -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'

Loading…
Cancel
Save