From 6a828f928694e7ba76aefbfa417049b3b94b6ef0 Mon Sep 17 00:00:00 2001
From: Samuel <samuel@soundmarker.com>
Date: Wed, 25 Mar 2020 15:21:05 +0100
Subject: [PATCH] Edit Community - Assign Roles/Groups (Angular)

---
 resources/i18n/en.json5                       |   6 +
 .../admin-access-control-routing.module.ts    |  14 ++-
 src/app/+admin/admin-routing.module.ts        |   4 +
 .../community-roles.component.html            |   7 ++
 .../community-roles.component.spec.ts         |  56 +++++++++
 .../community-roles.component.ts              |  45 ++++++-
 .../core/eperson/group-data.service.spec.ts   |   3 +-
 src/app/core/eperson/group-data.service.ts    |  82 +++++++++++-
 src/app/core/shared/collection.model.ts       |   8 ++
 src/app/core/shared/community.model.ts        |   9 ++
 .../comcol-role/comcol-role.component.html    |  42 +++++++
 .../comcol-role/comcol-role.component.scss    |   0
 .../comcol-role/comcol-role.component.spec.ts | 117 ++++++++++++++++++
 .../comcol-role/comcol-role.component.ts      |  94 ++++++++++++++
 .../comcol-role/comcol-role.ts                |  45 +++++++
 src/app/shared/shared.module.ts               |   2 +
 16 files changed, 523 insertions(+), 11 deletions(-)
 create mode 100644 src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts
 create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html
 create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss
 create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
 create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
 create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts

diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5
index ae3176d8b1..80326dc7b2 100644
--- a/resources/i18n/en.json5
+++ b/resources/i18n/en.json5
@@ -761,6 +761,12 @@
 
   "community.edit.tabs.roles.title": "Community Edit - Roles",
 
+  "community.edit.tabs.roles.none": "None",
+
+  "community.edit.tabs.roles.admin.name": "Administrators",
+
+  "community.edit.tabs.roles.admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).",
+
 
 
   "community.form.abstract": "Short Description",
diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts
index 93e65708bc..5af18c778f 100644
--- a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts
+++ b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts
@@ -3,19 +3,27 @@ import { RouterModule } from '@angular/router';
 import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
 import { GroupFormComponent } from './group-registry/group-form/group-form.component';
 import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
+import { URLCombiner } from '../../core/url-combiner/url-combiner';
+import { getAccessControlModulePath } from '../admin-routing.module';
+
+const GROUP_EDIT_PATH = 'groups';
+
+export function getGroupEditPath(id: string) {
+  return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
+}
 
 @NgModule({
   imports: [
     RouterModule.forChild([
       { path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
-      { path: 'groups', component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
+      { path: GROUP_EDIT_PATH, component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
       {
-        path: 'groups/:groupId',
+        path: `${GROUP_EDIT_PATH}/:groupId`,
         component: GroupFormComponent,
         data: {title: 'admin.registries.schema.title'}
       },
       {
-        path: 'groups/newGroup',
+        path: `${GROUP_EDIT_PATH}/newGroup`,
         component: GroupFormComponent,
         data: {title: 'admin.registries.schema.title'}
       },
diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts
index 285ebeb0d1..aa47c93102 100644
--- a/src/app/+admin/admin-routing.module.ts
+++ b/src/app/+admin/admin-routing.module.ts
@@ -12,6 +12,10 @@ export function getRegistriesModulePath() {
   return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
 }
 
+export function getAccessControlModulePath() {
+  return new URLCombiner(getAdminModulePath(), ACCESS_CONTROL_MODULE_PATH).toString();
+}
+
 @NgModule({
   imports: [
     RouterModule.forChild([
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html
index e69de29bb2..a2dffdadfe 100644
--- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html
+++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html
@@ -0,0 +1,7 @@
+<ds-comcol-role
+  *ngFor="let comcolRole of getComcolRoles()"
+  [dso]="community$ | async"
+  [comcolRole]="comcolRole"
+  class="card {{comcolRole.name}}"
+>
+</ds-comcol-role>
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts
new file mode 100644
index 0000000000..9849528c54
--- /dev/null
+++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts
@@ -0,0 +1,56 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { ActivatedRoute } from '@angular/router';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
+import { CommunityRolesComponent } from './community-roles.component';
+import { Community } from '../../../core/shared/community.model';
+import { By } from '@angular/platform-browser';
+import { RemoteData } from '../../../core/data/remote-data';
+
+describe('CommunityRolesComponent', () => {
+
+  let fixture: ComponentFixture<CommunityRolesComponent>;
+  let comp: CommunityRolesComponent;
+  let de: DebugElement;
+
+  beforeEach(() => {
+
+    const route = {
+      parent: {
+        data: observableOf({
+          dso: new RemoteData(
+            false,
+            false,
+            true,
+            undefined,
+            new Community(),
+          )
+        })
+      }
+    };
+
+    TestBed.configureTestingModule({
+      imports: [
+        TranslateModule.forRoot(),
+      ],
+      declarations: [
+        CommunityRolesComponent,
+      ],
+      providers: [
+        { provide: ActivatedRoute, useValue: route },
+      ],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CommunityRolesComponent);
+    comp = fixture.componentInstance;
+    de = fixture.debugElement;
+
+    fixture.detectChanges();
+  });
+
+  it('should display a community admin role component', () => {
+    expect(de.query(By.css('ds-comcol-role.admin'))).toBeDefined();
+  });
+});
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts
index afa1fe14d1..edf6d50e62 100644
--- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts
+++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts
@@ -1,4 +1,11 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Observable } from 'rxjs';
+import { first, map } from 'rxjs/operators';
+import { Community } from '../../../core/shared/community.model';
+import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
+import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
+import { RemoteData } from '../../../core/data/remote-data';
 
 /**
  * Component for managing a community's roles
@@ -7,6 +14,38 @@ import { Component } from '@angular/core';
   selector: 'ds-community-roles',
   templateUrl: './community-roles.component.html',
 })
-export class CommunityRolesComponent {
-  /* TODO: Implement Community Edit - Roles */
+export class CommunityRolesComponent implements OnInit {
+
+  dsoRD$: Observable<RemoteData<Community>>;
+
+  /**
+   * The community to manage, as an observable.
+   */
+  get community$(): Observable<Community> {
+    return this.dsoRD$.pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+    )
+  }
+
+  /**
+   * The different roles for the community.
+   */
+  getComcolRoles(): ComcolRole[] {
+    return [
+      ComcolRole.ADMIN,
+    ];
+  }
+
+  constructor(
+    protected route: ActivatedRoute,
+  ) {
+  }
+
+  ngOnInit(): void {
+    this.dsoRD$ = this.route.parent.data.pipe(
+      first(),
+      map((data) => data.dso),
+    );
+  }
 }
diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts
index b4a15a46d2..138cf547f2 100644
--- a/src/app/core/eperson/group-data.service.spec.ts
+++ b/src/app/core/eperson/group-data.service.spec.ts
@@ -82,7 +82,8 @@ describe('GroupDataService', () => {
       rdbService,
       store,
       null,
-      halService
+      halService,
+      null,
     );
   };
 
diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts
index 2dd7939547..18ad9ba22f 100644
--- a/src/app/core/eperson/group-data.service.ts
+++ b/src/app/core/eperson/group-data.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
 
 import { createSelector, select, Store } from '@ngrx/store';
 import { Observable } from 'rxjs';
-import { filter, map, take } from 'rxjs/operators';
+import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
 import {
   GroupRegistryCancelGroupAction,
   GroupRegistryEditGroupAction
@@ -21,16 +21,26 @@ import { DataService } from '../data/data.service';
 import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
 import { PaginatedList } from '../data/paginated-list';
 import { RemoteData } from '../data/remote-data';
-import { DeleteRequest, FindListOptions, FindListRequest, PostRequest, RestRequest } from '../data/request.models';
+import {
+  CreateRequest,
+  DeleteRequest,
+  FindListOptions,
+  FindListRequest,
+  PostRequest
+} from '../data/request.models';
 
 import { RequestService } from '../data/request.service';
 import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
 import { HALEndpointService } from '../shared/hal-endpoint.service';
-import { getResponseFromEntry } from '../shared/operators';
+import { configureRequest, getResponseFromEntry} from '../shared/operators';
 import { EPerson } from './models/eperson.model';
 import { Group } from './models/group.model';
 import { dataService } from '../cache/builders/build-decorators';
 import { GROUP } from './models/group.resource-type';
+import { DSONameService } from '../breadcrumbs/dso-name.service';
+import { Community } from '../shared/community.model';
+import { Collection } from '../shared/collection.model';
+import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
 
 const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
 const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
@@ -56,7 +66,8 @@ export class GroupDataService extends DataService<Group> {
     protected rdbService: RemoteDataBuildService,
     protected store: Store<any>,
     protected objectCache: ObjectCacheService,
-    protected halService: HALEndpointService
+    protected halService: HALEndpointService,
+    protected nameService: DSONameService,
   ) {
     super();
   }
@@ -309,4 +320,67 @@ export class GroupDataService extends DataService<Group> {
     return foundUUID;
   }
 
+  /**
+   * Create a group for a given role for a given community or collection.
+   *
+   * @param dso         The community or collection for which to create a group
+   * @param comcolRole  The role for which to create a group
+   */
+  createComcolGroup(dso: Community|Collection, comcolRole: ComcolRole): Observable<RestResponse> {
+
+    const requestId = this.requestService.generateRequestId();
+    const link = comcolRole.getEndpoint(dso);
+    const group = Object.assign(new Group(), {
+      metadata: {
+        'dc.description': [
+          {
+            value: `${this.nameService.getName(dso)} admin group`,
+          }
+        ],
+      },
+    });
+
+    return this.halService.getEndpoint(link).pipe(
+      distinctUntilChanged(),
+      take(1),
+      map((endpoint: string) =>
+        new CreateRequest(
+          requestId,
+          endpoint,
+          JSON.stringify(group),
+        )
+      ),
+      configureRequest(this.requestService),
+      tap(() => this.requestService.removeByHrefSubstring(link)),
+      switchMap((restRequest) => this.requestService.getByUUID(restRequest.uuid)),
+      getResponseFromEntry(),
+    );
+  }
+
+  /**
+   * Delete the group for a given role for a given community or collection.
+   *
+   * @param dso         The community or collection for which to delete the group
+   * @param comcolRole  The role for which to delete the group
+   */
+  deleteComcolGroup(dso: Community|Collection, comcolRole: ComcolRole): Observable<RestResponse> {
+
+    const requestId = this.requestService.generateRequestId();
+    const link = comcolRole.getEndpoint(dso);
+
+    return this.halService.getEndpoint(link).pipe(
+      distinctUntilChanged(),
+      take(1),
+      map((endpoint: string) =>
+        new DeleteRequest(
+          requestId,
+          endpoint,
+        )
+      ),
+      configureRequest(this.requestService),
+      tap(() => this.requestService.removeByHrefSubstring(link)),
+      switchMap((restRequest) => this.requestService.getByUUID(restRequest.uuid)),
+      getResponseFromEntry(),
+    );
+  }
 }
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts
index ba2f448bba..4e0b5ead83 100644
--- a/src/app/core/shared/collection.model.ts
+++ b/src/app/core/shared/collection.model.ts
@@ -15,6 +15,8 @@ import { RESOURCE_POLICY } from './resource-policy.resource-type';
 import { COMMUNITY } from './community.resource-type';
 import { Community } from './community.model';
 import { ChildHALResource } from './child-hal-resource.model';
+import { GROUP } from '../eperson/models/group.resource-type';
+import { Group } from '../eperson/models/group.model';
 
 @typedObject
 @inheritSerialization(DSpaceObject)
@@ -70,6 +72,12 @@ export class Collection extends DSpaceObject implements ChildHALResource {
   @link(COMMUNITY, false)
   parentCommunity?: Observable<RemoteData<Community>>;
 
+  /**
+   * The administrators group of this community.
+   */
+  @link(GROUP)
+  adminGroup?: Observable<RemoteData<Group>>;
+
   /**
    * The introductory text of this Collection
    * Corresponds to the metadata field dc.description
diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts
index e18ec743e8..bdcda70e9b 100644
--- a/src/app/core/shared/community.model.ts
+++ b/src/app/core/shared/community.model.ts
@@ -3,6 +3,8 @@ import { Observable } from 'rxjs';
 import { link, typedObject } from '../cache/builders/build-decorators';
 import { PaginatedList } from '../data/paginated-list';
 import { RemoteData } from '../data/remote-data';
+import { Group } from '../eperson/models/group.model';
+import { GROUP } from '../eperson/models/group.resource-type';
 import { Bitstream } from './bitstream.model';
 import { BITSTREAM } from './bitstream.resource-type';
 import { Collection } from './collection.model';
@@ -32,6 +34,7 @@ export class Community extends DSpaceObject implements ChildHALResource {
     logo: HALLink;
     subcommunities: HALLink;
     parentCommunity: HALLink;
+    adminGroup: HALLink;
     self: HALLink;
   };
 
@@ -63,6 +66,12 @@ export class Community extends DSpaceObject implements ChildHALResource {
   @link(COMMUNITY, false)
   parentCommunity?: Observable<RemoteData<Community>>;
 
+  /**
+   * The administrators group of this community.
+   */
+  @link(GROUP)
+  adminGroup?: Observable<RemoteData<Group>>;
+
   /**
    * The introductory text of this Community
    * Corresponds to the metadata field dc.description
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html
new file mode 100644
index 0000000000..26460d5367
--- /dev/null
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html
@@ -0,0 +1,42 @@
+<div class="card p-2">
+
+  <div class="card-body d-flex flex-column">
+
+    <div class="d-flex flex-row justify-content-between">
+
+      <div>
+        <p>{{'community.edit.tabs.roles.' + comcolRole.name + '.name' | translate}}</p>
+      </div>
+
+      <ng-container *ngVar="group$ | async as group">
+
+          <div *ngIf="!group">
+            {{'community.edit.tabs.roles.none' | translate}}
+          </div>
+
+          <a *ngIf="group"
+             href="{{editGroupLink$ | async}}">
+            {{group.name}}
+          </a>
+
+          <div *ngIf="!group"
+               class="btn btn-outline-dark create"
+               (click)="create()">create
+          </div>
+
+          <div *ngIf="group"
+               class="btn btn-outline-dark delete"
+               (click)="delete()">delete
+          </div>
+
+      </ng-container>
+
+    </div>
+
+    <div class="mt-2">
+      {{'community.edit.tabs.roles.' + comcolRole.name + '.description' | translate}}
+    </div>
+
+  </div>
+
+</div>
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
new file mode 100644
index 0000000000..ab5696312b
--- /dev/null
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
@@ -0,0 +1,117 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComcolRoleComponent } from './comcol-role.component';
+import { GroupDataService } from '../../../../core/eperson/group-data.service';
+import { By } from '@angular/platform-browser';
+import { SharedModule } from '../../../shared.module';
+import { TranslateModule } from '@ngx-translate/core';
+import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
+import { RequestService } from '../../../../core/data/request.service';
+import { LinkService } from '../../../../core/cache/builders/link.service';
+import { Community } from '../../../../core/shared/community.model';
+import { ComcolRole } from './comcol-role';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { Group } from '../../../../core/eperson/models/group.model';
+
+describe('ComcolRoleComponent', () => {
+
+  let fixture: ComponentFixture<ComcolRoleComponent>;
+  let comp: ComcolRoleComponent;
+  let de: DebugElement;
+  let groupService;
+  let linkService;
+
+  beforeEach(() => {
+
+    groupService = jasmine.createSpyObj('groupService', {
+      createComcolGroup: undefined,
+      deleteComcolGroup: undefined,
+    });
+
+    linkService = {
+      resolveLink: () => undefined,
+    };
+
+    TestBed.configureTestingModule({
+      imports: [
+        TranslateModule.forRoot(),
+        SharedModule,
+      ],
+      declarations: [
+      ],
+      providers: [
+        { provide: GroupDataService, useValue: groupService },
+        { provide: LinkService, useValue: linkService },
+        { provide: ChangeDetectorRef, useValue: {} },
+        { provide: RequestService, useValue: {} },
+      ], schemas: [
+        NO_ERRORS_SCHEMA
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ComcolRoleComponent);
+    comp = fixture.componentInstance;
+    de = fixture.debugElement;
+
+    comp.comcolRole = new ComcolRole(
+      'test name',
+      'test link name',
+    );
+
+    comp.dso = new Community();
+
+    fixture.detectChanges();
+  });
+
+  describe('when there is no group yet', () => {
+
+    it('should have a create button but no delete button', () => {
+      expect(de.query(By.css('.btn.create'))).toBeDefined();
+      expect(de.query(By.css('.btn.delete'))).toBeNull();
+    });
+
+    describe('when the create button is pressed', () => {
+
+      beforeEach(() => {
+        de.query(By.css('.btn.create')).nativeElement.click();
+      });
+
+      it('should call the groupService create method', () => {
+        expect(groupService.createComcolGroup).toHaveBeenCalled();
+      });
+    });
+  });
+
+  describe('when there is a group yet', () => {
+
+    beforeEach(() => {
+      Object.assign(comp.dso, {
+        'test link name': observableOf(new RemoteData(
+          false,
+          false,
+          true,
+          undefined,
+          new Group(),
+        )),
+      });
+
+      fixture.detectChanges();
+    });
+
+    it('should have a delete button but no create button', () => {
+      expect(de.query(By.css('.btn.delete'))).toBeDefined();
+      expect(de.query(By.css('.btn.create'))).toBeNull();
+    });
+
+    describe('when the delete button is pressed', () => {
+
+      beforeEach(() => {
+        de.query(By.css('.btn.delete')).nativeElement.click();
+      });
+
+      it('should call the groupService delete method', () => {
+        expect(groupService.deleteComcolGroup).toHaveBeenCalled();
+      });
+    });
+  });
+});
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
new file mode 100644
index 0000000000..044e2568e1
--- /dev/null
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
@@ -0,0 +1,94 @@
+import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { Group } from '../../../../core/eperson/models/group.model';
+import { Community } from '../../../../core/shared/community.model';
+import { EMPTY, Observable } from 'rxjs';
+import { getGroupEditPath } from '../../../../+admin/admin-access-control/admin-access-control-routing.module';
+import { GroupDataService } from '../../../../core/eperson/group-data.service';
+import { Collection } from '../../../../core/shared/collection.model';
+import { map } from 'rxjs/operators';
+import { followLink } from '../../../utils/follow-link-config.model';
+import { LinkService } from '../../../../core/cache/builders/link.service';
+import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
+import { ComcolRole } from './comcol-role';
+import { RequestService } from '../../../../core/data/request.service';
+
+/**
+ * Component for managing a community or collection role.
+ */
+@Component({
+  selector: 'ds-comcol-role',
+  styleUrls: ['./comcol-role.component.scss'],
+  templateUrl: './comcol-role.component.html'
+})
+export class ComcolRoleComponent implements OnInit {
+
+  /**
+   * The community or collection to manage.
+   */
+  @Input()
+  dso: Community|Collection;
+
+  /**
+   * The role to manage
+   */
+  @Input()
+  comcolRole: ComcolRole;
+
+  constructor(
+    protected groupService: GroupDataService,
+    protected linkService: LinkService,
+    protected cdr: ChangeDetectorRef,
+    protected requestService: RequestService,
+  ) {
+  }
+
+  /**
+   * The group for this role as an observable.
+   */
+  get group$(): Observable<Group> {
+
+    if (!this.dso[this.comcolRole.linkName]) {
+      return EMPTY;
+    }
+
+    return this.dso[this.comcolRole.linkName].pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+    );
+  }
+
+  /**
+   * The link to the group edit page as an observable.
+   */
+  get editGroupLink$(): Observable<string> {
+    return this.group$.pipe(
+      map((group) => getGroupEditPath(group.id)),
+    );
+  }
+
+  /**
+   * Create a group for this community or collection role.
+   */
+  create() {
+
+    this.groupService.createComcolGroup(this.dso, this.comcolRole)
+      .subscribe(() => {
+        this.linkService.resolveLink(this.dso, followLink(this.comcolRole.linkName));
+        this.cdr.detectChanges();
+      });
+  }
+
+  /**
+   * Delete the group for this community or collection role.
+   */
+  delete() {
+    this.groupService.deleteComcolGroup(this.dso, this.comcolRole)
+      .subscribe(() => {
+        this.cdr.detectChanges();
+      })
+  }
+
+  ngOnInit(): void {
+    this.linkService.resolveLink(this.dso, followLink(this.comcolRole.linkName));
+  }
+}
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts
new file mode 100644
index 0000000000..1c4412eaac
--- /dev/null
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts
@@ -0,0 +1,45 @@
+import { Community } from '../../../../core/shared/community.model';
+import { Collection } from '../../../../core/shared/collection.model';
+
+/**
+ * Class representing a community or collection role.
+ */
+export class ComcolRole {
+
+  /**
+   * The admin role.
+   */
+  public static ADMIN = new ComcolRole(
+    'admin',
+    'adminGroup',
+  );
+
+  /**
+   * @param name      The name for this community or collection role.
+   * @param linkName  The path linking to this community or collection role.
+   */
+  constructor(
+    public name,
+    public linkName,
+  ) {
+  }
+
+  /**
+   * Get the REST endpoint for managing this role for a given community or collection.
+   * @param dso
+   */
+  public getEndpoint(dso: Community | Collection) {
+
+    let linkPath;
+    switch (dso.type + '') {
+      case 'community':
+        linkPath = 'communities';
+        break;
+      case 'collection':
+        linkPath = 'collections';
+        break;
+    }
+
+    return `${linkPath}/${dso.uuid}/${this.linkName}`;
+  }
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index a136b7826c..c053c8732c 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -9,6 +9,7 @@ import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule
 import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
 
 import { NgxPaginationModule } from 'ngx-pagination';
+import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
 import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
 
 import { FileUploadModule } from 'ng2-file-upload';
@@ -252,6 +253,7 @@ const COMPONENTS = [
   EditComColPageComponent,
   DeleteComColPageComponent,
   ComcolPageBrowseByComponent,
+  ComcolRoleComponent,
   DsDynamicFormComponent,
   DsDynamicFormControlContainerComponent,
   DsDynamicListComponent,
-- 
GitLab