{
public constructor(input?: any) {
super('users/group', input);
+ if (!input) {
+ // permissions are required for new groups
+ this.permissions = [];
+ }
}
public getTitle(): string {
diff --git a/client/src/app/site/users/components/group-list/group-list.component.html b/client/src/app/site/users/components/group-list/group-list.component.html
new file mode 100644
index 000000000..367d4fa9d
--- /dev/null
+++ b/client/src/app/site/users/components/group-list/group-list.component.html
@@ -0,0 +1,88 @@
+
+
+
+
+ Groups
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All your changes are saved immediately.
+
+
+
+
+
+
+ {{ app.name }}
+
+
+
+
+
+
diff --git a/client/src/app/site/users/components/group-list/group-list.component.scss b/client/src/app/site/users/components/group-list/group-list.component.scss
new file mode 100644
index 000000000..e42a0e912
--- /dev/null
+++ b/client/src/app/site/users/components/group-list/group-list.component.scss
@@ -0,0 +1,39 @@
+table {
+ width: 100%;
+
+ .mat-cell {
+ min-width: 80px;
+ }
+
+ .mat-column-perm {
+ min-width: 130px;
+ }
+
+ .inner-table {
+ width: 100%;
+ text-align: center;
+ }
+
+ .group-head-table-cell {
+ cursor: pointer;
+ }
+}
+
+.hint-text {
+ padding-top: 30px;
+ padding-left: 25px;
+ background-color: #ffffff; // put in theme later
+}
+
+.new-group-form {
+ text-align: center;
+ padding-top: 10px;
+ background-color: #ffffff; // put in theme later
+ button {
+ margin-left: 10px;
+ }
+}
+
+.scrollable-perm-matrix {
+ overflow: auto;
+}
diff --git a/client/src/app/site/users/components/group-list/group-list.component.spec.ts b/client/src/app/site/users/components/group-list/group-list.component.spec.ts
new file mode 100644
index 000000000..23ac7194b
--- /dev/null
+++ b/client/src/app/site/users/components/group-list/group-list.component.spec.ts
@@ -0,0 +1,26 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GroupListComponent } from './group-list.component';
+import { E2EImportsModule } from '../../../../../e2e-imports.module';
+
+describe('GroupListComponent', () => {
+ let component: GroupListComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [E2EImportsModule],
+ declarations: [GroupListComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GroupListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/users/components/group-list/group-list.component.ts b/client/src/app/site/users/components/group-list/group-list.component.ts
new file mode 100644
index 000000000..d11867868
--- /dev/null
+++ b/client/src/app/site/users/components/group-list/group-list.component.ts
@@ -0,0 +1,192 @@
+import { Component, OnInit } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { TranslateService } from '@ngx-translate/core';
+import { MatTableDataSource } from '@angular/material';
+import { FormGroup } from '@angular/forms';
+
+import { GroupRepositoryService } from '../../services/group-repository.service';
+import { ViewGroup } from '../../models/view-group';
+import { Group } from '../../../../shared/models/users/group';
+import { BaseComponent } from '../../../../base.component';
+
+/**
+ * Component for the Group-List and permission matrix
+ */
+@Component({
+ selector: 'os-group-list',
+ templateUrl: './group-list.component.html',
+ styleUrls: ['./group-list.component.scss']
+})
+export class GroupListComponent extends BaseComponent implements OnInit {
+ /**
+ * Holds all Groups
+ */
+ public groups: ViewGroup[];
+
+ /**
+ * The header rows that the table should show
+ */
+ public headerRowDef: string[] = [];
+
+ /**
+ * Show or hide the new groups box
+ */
+ public newGroup = false;
+
+ /**
+ * Show or hide edit Group features
+ */
+ public editGroup = false;
+
+ /**
+ * Store the group to edit
+ */
+ public selectedGroup: ViewGroup;
+
+ /**
+ * Constructor
+ *
+ * @param titleService Title Service
+ * @param translate Translations
+ * @param DS The Data Store
+ * @param constants Constants
+ */
+ public constructor(titleService: Title, translate: TranslateService, public repo: GroupRepositoryService) {
+ super(titleService, translate);
+ }
+
+ /**
+ * Trigger for the new Group button
+ */
+ public newGroupButton(): void {
+ this.editGroup = false;
+ this.newGroup = !this.newGroup;
+ }
+
+ /**
+ * Saves a newly created group.
+ * @param form form data given by the group
+ */
+ public submitNewGroup(form: FormGroup): void {
+ if (form.value) {
+ this.repo.create(form.value).subscribe(response => {
+ if (response) {
+ form.reset();
+ // commenting the next line would allow to create multiple groups without reopening the form
+ this.newGroup = false;
+ }
+ });
+ }
+ }
+
+ /**
+ * Saves an edited group.
+ * @param form form data given by the group
+ */
+ public submitEditedGroup(form: FormGroup): void {
+ if (form.value) {
+ const updateData = new Group({ name: form.value.name });
+
+ this.repo.update(updateData, this.selectedGroup).subscribe(response => {
+ if (response) {
+ this.cancelEditing();
+ }
+ });
+ }
+ }
+
+ /**
+ * Deletes the selected Group
+ */
+ public deleteSelectedGroup(): void {
+ this.repo.delete(this.selectedGroup).subscribe(response => this.cancelEditing());
+ }
+
+ /**
+ * Cancel the editing
+ */
+ public cancelEditing(): void {
+ this.editGroup = false;
+ }
+
+ /**
+ * Select group in head bar
+ */
+ public selectGroup(group: ViewGroup): void {
+ this.newGroup = false;
+ this.selectedGroup = group;
+ this.editGroup = true;
+ }
+
+ /**
+ * Triggers when a permission was toggled
+ * @param group
+ * @param perm
+ */
+ public togglePerm(viewGroup: ViewGroup, perm: string): void {
+ const updateData = new Group({ permissions: viewGroup.getAlteredPermissions(perm) });
+ this.repo.update(updateData, viewGroup).subscribe();
+ }
+
+ /**
+ * Update the rowDefinition after Reloading or changes
+ */
+ public updateRowDef(): void {
+ // reset the rowDef list first
+ this.headerRowDef = ['perm'];
+ this.groups.forEach(viewGroup => {
+ this.headerRowDef.push('' + viewGroup.name);
+ });
+ }
+
+ /**
+ * Required to detect changes in *ngFor loops
+ *
+ * @param group Corresponding group that was changed
+ */
+ public trackGroupArray(group: ViewGroup): number {
+ return group.id;
+ }
+
+ /**
+ * Converts a permission string into MatTableDataSource
+ * @param permissions
+ */
+ public getTableDataSource(permissions: string[]): MatTableDataSource {
+ const dataSource = new MatTableDataSource();
+ dataSource.data = permissions;
+ return dataSource;
+ }
+
+ /**
+ * Determine if a group is protected from deletion
+ * @param group ViewGroup
+ */
+ public isProtected(group: ViewGroup): boolean {
+ return group.id === 1 || group.id === 2;
+ }
+
+ /**
+ * Clicking escape while in #newGroupForm should toggle newGroup.
+ */
+ public keyDownFunction(event: KeyboardEvent): void {
+ if (event.keyCode === 27) {
+ this.newGroup = false;
+ }
+ }
+
+ /**
+ * Init function.
+ *
+ * Monitor the repository for changes and update the local groups array
+ */
+ public ngOnInit(): void {
+ super.setTitle('Groups');
+ this.repo.getViewModelListObservable().subscribe(newViewGroups => {
+ if (newViewGroups) {
+ this.groups = newViewGroups;
+ this.updateRowDef();
+ }
+ });
+ }
+}
diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts
index 71a81e97c..b1d5b4994 100644
--- a/client/src/app/site/users/components/user-list/user-list.component.ts
+++ b/client/src/app/site/users/components/user-list/user-list.component.ts
@@ -24,7 +24,8 @@ export class UserListComponent extends ListViewBaseComponent implement
{
text: 'Groups',
icon: 'users',
- action: 'toGroups'
+ action: 'toGroups',
+ perm: 'users.can_manage'
},
{
text: 'Import',
@@ -81,7 +82,7 @@ export class UserListComponent extends ListViewBaseComponent implement
* TODO: implement
*/
public toGroups(): void {
- console.log('to Groups');
+ this.router.navigate(['./groups'], { relativeTo: this.route });
}
/**
diff --git a/client/src/app/site/users/models/view-group.ts b/client/src/app/site/users/models/view-group.ts
new file mode 100644
index 000000000..926ce23d6
--- /dev/null
+++ b/client/src/app/site/users/models/view-group.ts
@@ -0,0 +1,76 @@
+import { BaseViewModel } from '../../base/base-view-model';
+import { Group } from '../../../shared/models/users/group';
+import { BaseModel } from '../../../shared/models/base/base-model';
+
+export class ViewGroup extends BaseViewModel {
+ private _group: Group;
+
+ public get group(): Group {
+ return this._group ? this._group : null;
+ }
+
+ public get id(): number {
+ return this.group ? this.group.id : null;
+ }
+
+ public get name(): string {
+ return this.group ? this.group.name : null;
+ }
+
+ /**
+ * required for renaming purpose
+ */
+ public set name(newName: string) {
+ if (this.group) {
+ this.group.name = newName;
+ }
+ }
+
+ public get permissions(): string[] {
+ return this.group ? this.group.permissions : null;
+ }
+
+ public constructor(group?: Group) {
+ super();
+ this._group = group;
+ }
+
+ /**
+ * Returns an array of permissions where the given perm is included
+ * or removed.
+ *
+ * Avoids touching the local DataStore.
+ *
+ * @param perm
+ */
+ public getAlteredPermissions(perm: string): string[] {
+ // clone the array, avoids altering the local dataStore
+ const currentPermissions = this.permissions.slice();
+
+ if (this.hasPermission(perm)) {
+ // remove the permission from currentPermissions-List
+ const indexOfPerm = currentPermissions.indexOf(perm);
+ if (indexOfPerm !== -1) {
+ currentPermissions.splice(indexOfPerm, 1);
+ return currentPermissions;
+ } else {
+ return currentPermissions;
+ }
+ } else {
+ currentPermissions.push(perm);
+ return currentPermissions;
+ }
+ }
+
+ public hasPermission(perm: string): boolean {
+ return this.permissions.includes(perm);
+ }
+
+ public getTitle(): string {
+ return this.name;
+ }
+
+ public updateValues(update: BaseModel): void {
+ console.log('ViewGroups wants to update Values with : ', update);
+ }
+}
diff --git a/client/src/app/site/users/services/group-repository.service.spec.ts b/client/src/app/site/users/services/group-repository.service.spec.ts
new file mode 100644
index 000000000..8a7047055
--- /dev/null
+++ b/client/src/app/site/users/services/group-repository.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { GroupRepositoryService } from './group-repository.service';
+import { E2EImportsModule } from '../../../../e2e-imports.module';
+
+describe('GroupRepositoryService', () => {
+ beforeEach(() =>
+ TestBed.configureTestingModule({
+ imports: [E2EImportsModule],
+ providers: [GroupRepositoryService]
+ }));
+
+ it('should be created', inject([GroupRepositoryService], (service: GroupRepositoryService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/client/src/app/site/users/services/group-repository.service.ts b/client/src/app/site/users/services/group-repository.service.ts
new file mode 100644
index 000000000..55b4e6093
--- /dev/null
+++ b/client/src/app/site/users/services/group-repository.service.ts
@@ -0,0 +1,140 @@
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { ViewGroup } from '../models/view-group';
+import { BaseRepository } from '../../base/base-repository';
+import { Group } from '../../../shared/models/users/group';
+import { DataStoreService } from '../../../core/services/data-store.service';
+import { DataSendService } from '../../../core/services/data-send.service';
+import { ConstantsService } from '../../../core/services/constants.service';
+
+/**
+ * Set rules to define the shape of an app permission
+ */
+interface AppPermission {
+ name: string;
+ permissions: string[];
+}
+
+/**
+ * Repository service for Groups
+ *
+ * Documentation partially provided in {@link BaseRepository}
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class GroupRepositoryService extends BaseRepository {
+ /**
+ * holds sorted permissions per app.
+ */
+ public appPermissions: AppPermission[] = [];
+
+ /**
+ * Constructor calls the parent constructor
+ * @param DS Store
+ * @param dataSend Sending Data
+ */
+ public constructor(DS: DataStoreService, private dataSend: DataSendService, private constants: ConstantsService) {
+ super(DS, Group);
+ this.sortPermsPerApp();
+ }
+
+ /**
+ * Add an entry to appPermissions
+ *
+ * @param appId number that indicates the app
+ * @param perm certain permission as string
+ * @param appName Indicates the header in the Permission Matrix
+ */
+ private addAppPerm(appId: number, perm: string, appName: string): void {
+ if (!this.appPermissions[appId]) {
+ this.appPermissions[appId] = {
+ name: appName,
+ permissions: []
+ };
+ }
+ this.appPermissions[appId].permissions.push(perm);
+ }
+
+ /**
+ * read the constants, add them to an array of apps
+ */
+ private sortPermsPerApp(): void {
+ this.constants.get('permissions').subscribe(perms => {
+ perms.forEach(perm => {
+ // extract the apps name
+ const permApp = perm.value.split('.')[0];
+ switch (permApp) {
+ case 'core':
+ if (perm.value.indexOf('projector') > -1) {
+ this.addAppPerm(0, perm, 'Projector');
+ } else {
+ this.addAppPerm(6, perm, 'General');
+ }
+ break;
+ case 'agenda':
+ this.addAppPerm(1, perm, 'Agenda');
+ break;
+ case 'motions':
+ this.addAppPerm(2, perm, 'Motions');
+ break;
+ case 'assignments':
+ this.addAppPerm(3, perm, 'Assignments');
+ break;
+ case 'mediafiles':
+ this.addAppPerm(4, perm, 'Mediafiles');
+ break;
+ case 'users':
+ this.addAppPerm(5, perm, 'Users');
+ break;
+ default:
+ // plugins
+ const displayName = `${permApp.charAt(0).toUpperCase}${permApp.slice(1)}`;
+ // check if the plugin exists as app
+ const result = this.appPermissions.findIndex(app => {
+ return app.name === displayName;
+ });
+ const pluginId = result === -1 ? this.appPermissions.length : result;
+ this.addAppPerm(pluginId, perm, displayName);
+ break;
+ }
+ });
+ });
+ }
+
+ /**
+ * creates and saves a new user
+ *
+ * @param groupData form value. Usually not yet a real user
+ */
+ public create(groupData: Partial): Observable {
+ const newGroup = new Group();
+ newGroup.patchValues(groupData);
+ return this.dataSend.createModel(newGroup);
+ }
+
+ /**
+ * Updates the given Group with the new permission
+ *
+ * @param permission the new permission
+ * @param viewGroup the selected Group
+ */
+ public update(groupData: Partial, viewGroup: ViewGroup): Observable {
+ const updateGroup = new Group();
+ updateGroup.patchValues(viewGroup.group);
+ updateGroup.patchValues(groupData);
+ return this.dataSend.updateModel(updateGroup, 'put');
+ }
+
+ /**
+ * Deletes a given group
+ */
+ public delete(viewGroup: ViewGroup): Observable {
+ return this.dataSend.delete(viewGroup.group);
+ }
+
+ public createViewModel(group: Group): ViewGroup {
+ return new ViewGroup(group);
+ }
+}
diff --git a/client/src/app/site/users/services/user-repository.service.ts b/client/src/app/site/users/services/user-repository.service.ts
index 735b16ffc..2d14e5c9a 100644
--- a/client/src/app/site/users/services/user-repository.service.ts
+++ b/client/src/app/site/users/services/user-repository.service.ts
@@ -46,9 +46,7 @@ export class UserRepositoryService extends BaseRepository {
}
/**
- * @ignore
- *
- * TODO: used over not-yet-existing detail view
+ * Deletes a given user
*/
public delete(viewUser: ViewUser): Observable {
return this.dataSend.delete(viewUser.user);
@@ -65,7 +63,7 @@ export class UserRepositoryService extends BaseRepository {
// collectionString of userData is still empty
newUser.patchValues(userData);
- // if the username is not presend, delete.
+ // if the username is not present, delete.
// The server will generate a one
if (!newUser.username) {
delete newUser.username;
diff --git a/client/src/app/site/users/users-routing.module.ts b/client/src/app/site/users/users-routing.module.ts
index 802e8256d..879acea2e 100644
--- a/client/src/app/site/users/users-routing.module.ts
+++ b/client/src/app/site/users/users-routing.module.ts
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserListComponent } from './components/user-list/user-list.component';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
+import { GroupListComponent } from './components/group-list/group-list.component';
const routes: Routes = [
{
@@ -12,6 +13,17 @@ const routes: Routes = [
path: 'new',
component: UserDetailComponent
},
+ {
+ path: 'groups',
+ component: GroupListComponent
+ /**
+ * FIXME: CRITICAL:
+ * Refreshing the page, even while having the required permission, will navigate you back to "/"
+ * Makes developing protected areas impossible.
+ * Has the be (temporarily) removed if this page should be edited.
+ */
+ // data: { basePerm: 'users.can_manage' }
+ },
{
path: ':id',
component: UserDetailComponent
diff --git a/client/src/app/site/users/users.module.ts b/client/src/app/site/users/users.module.ts
index d40a610ab..9306149c0 100644
--- a/client/src/app/site/users/users.module.ts
+++ b/client/src/app/site/users/users.module.ts
@@ -5,9 +5,10 @@ import { UsersRoutingModule } from './users-routing.module';
import { SharedModule } from '../../shared/shared.module';
import { UserListComponent } from './components/user-list/user-list.component';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
+import { GroupListComponent } from './components/group-list/group-list.component';
@NgModule({
imports: [CommonModule, UsersRoutingModule, SharedModule],
- declarations: [UserListComponent, UserDetailComponent]
+ declarations: [UserListComponent, UserDetailComponent, GroupListComponent]
})
export class UsersModule {}
diff --git a/client/src/assets/i18n/de.json b/client/src/assets/i18n/de.json
index 9b2afab4d..dcd485359 100644
--- a/client/src/assets/i18n/de.json
+++ b/client/src/assets/i18n/de.json
@@ -1,8 +1,6 @@
{
"Abort": "",
"About Me": "",
- "Agenda": "Tagesordnung",
- "Assignments": "Wahlen",
"Category": "",
"Change Password": "Passwort ändern",
"Changed version": "",
@@ -35,13 +33,11 @@
}
},
"FILTER": "",
- "Files": "Dateien",
"Final version": "",
"First Name": "",
"French": "Französisch",
"German": "Deutsch",
"Groups": "",
- "Home": "Startseite",
"Identifier": "",
"Initial Password": "",
"Inline": "",
@@ -71,7 +67,6 @@
"Original version": "",
"Outside": "",
"Participant Number": "",
- "Participants": "Teilnehmer",
"Personal Note": "",
"Personal note": "",
"Prefix": "",
@@ -85,7 +80,6 @@
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
- "Settings": "Einstellungen",
"State": "",
"Structure Level": "",
"Submitters": "",
diff --git a/client/src/assets/i18n/en.json b/client/src/assets/i18n/en.json
index 93643c7e3..bfeb06d5c 100644
--- a/client/src/assets/i18n/en.json
+++ b/client/src/assets/i18n/en.json
@@ -1,8 +1,6 @@
{
"Abort": "",
"About Me": "",
- "Agenda": "",
- "Assignments": "",
"Category": "",
"Change Password": "",
"Changed version": "",
@@ -35,13 +33,11 @@
}
},
"FILTER": "",
- "Files": "",
"Final version": "",
"First Name": "",
"French": "",
"German": "",
"Groups": "",
- "Home": "",
"Identifier": "",
"Initial Password": "",
"Inline": "",
@@ -71,7 +67,6 @@
"Original version": "",
"Outside": "",
"Participant Number": "",
- "Participants": "",
"Personal Note": "",
"Personal note": "",
"Prefix": "",
@@ -85,7 +80,6 @@
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
- "Settings": "",
"State": "",
"Structure Level": "",
"Submitters": "",
diff --git a/client/src/assets/i18n/fr.json b/client/src/assets/i18n/fr.json
index 93643c7e3..bfeb06d5c 100644
--- a/client/src/assets/i18n/fr.json
+++ b/client/src/assets/i18n/fr.json
@@ -1,8 +1,6 @@
{
"Abort": "",
"About Me": "",
- "Agenda": "",
- "Assignments": "",
"Category": "",
"Change Password": "",
"Changed version": "",
@@ -35,13 +33,11 @@
}
},
"FILTER": "",
- "Files": "",
"Final version": "",
"First Name": "",
"French": "",
"German": "",
"Groups": "",
- "Home": "",
"Identifier": "",
"Initial Password": "",
"Inline": "",
@@ -71,7 +67,6 @@
"Original version": "",
"Outside": "",
"Participant Number": "",
- "Participants": "",
"Personal Note": "",
"Personal note": "",
"Prefix": "",
@@ -85,7 +80,6 @@
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
- "Settings": "",
"State": "",
"Structure Level": "",
"Submitters": "",