From 118214a906d27af17ff709becbc12888e30dfd03 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 24 Nov 2018 14:05:52 +0100 Subject: [PATCH] Rework admin user management to use default two culumn layout (CC-68) --- api/routes/account.js | 76 +++++++++------- static/src/app/admin/admin.component.css | 27 ------ static/src/app/admin/admin.component.html | 66 +------------- static/src/app/admin/admin.component.ts | 65 +------------- static/src/app/admin/admin.module.ts | 30 ++++++- static/src/app/admin/admin.routing.ts | 37 ++++++++ .../edit-app-user/edit-app-user.component.css | 0 .../edit-app-user.component.html | 61 +++++++++++++ .../edit-app-user/edit-app-user.component.ts | 88 +++++++++++++++++++ .../user-list/app-user-item.component.css | 6 ++ .../user-list/app-user-item.component.html | 19 ++++ .../user-list/app-user-item.component.ts | 38 ++++++++ .../user-list/app-user-list.component.css | 0 .../user-list/app-user-list.component.html | 8 ++ .../user-list/app-user-list.component.ts | 71 +++++++++++++++ static/src/app/app.component.html | 14 ++- static/src/app/app.config.ts | 1 + static/src/app/models/model-interfaces.ts | 6 +- .../app-user-service/app-user.service.ts | 8 +- static/src/app/shared.module.ts | 13 ++- static/src/assets/i18n/admin/de.json | 25 ++++++ static/src/assets/i18n/admin/en.json | 25 ++++++ 22 files changed, 486 insertions(+), 198 deletions(-) create mode 100644 static/src/app/admin/admin.routing.ts create mode 100644 static/src/app/admin/edit-app-user/edit-app-user.component.css create mode 100644 static/src/app/admin/edit-app-user/edit-app-user.component.html create mode 100644 static/src/app/admin/edit-app-user/edit-app-user.component.ts create mode 100644 static/src/app/admin/user-list/app-user-item.component.css create mode 100644 static/src/app/admin/user-list/app-user-item.component.html create mode 100644 static/src/app/admin/user-list/app-user-item.component.ts create mode 100644 static/src/app/admin/user-list/app-user-list.component.css create mode 100644 static/src/app/admin/user-list/app-user-list.component.html create mode 100644 static/src/app/admin/user-list/app-user-list.component.ts create mode 100644 static/src/assets/i18n/admin/de.json create mode 100644 static/src/assets/i18n/admin/en.json diff --git a/api/routes/account.js b/api/routes/account.js index 687ae7e..0d637c4 100644 --- a/api/routes/account.js +++ b/api/routes/account.js @@ -15,15 +15,17 @@ const account = new express.Router(); account.route('/') .get((req, res, next) => { - AppUserModel.find({}, {}, {sort: {username: 1}}).populate('squad').exec((err, items) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - res.locals.items = items; - res.locals.processed = true; - next(); - }); + AppUserModel.find({}, {}, {sort: {username: 1}}) + .populate('squad') + .exec((err, items) => { + if (err) { + err.status = codes.servererror; + return next(err); + } + res.locals.items = items; + res.locals.processed = true; + next(); + }); }) .all( routerHandling.httpMethodNotAllowed @@ -31,33 +33,45 @@ account.route('/') // routes ********************** account.route('/:id') + .get((req, res, next) => { + AppUserModel.findById(req.params.id) + .populate('squad') + .exec((err, item) => { + if (err) { + err.status = codes.servererror; + } else if (!item) { + err = new Error('item not found'); + err.status = codes.notfound; + } + res.locals.items = item; + next(err); + }); + }) + .patch((req, res, next) => { if (!req.body || (req.body._id && req.body._id !== req.params.id)) { - // little bit different as in PUT. :id does not need to be in data, but if the _id and url id must match - const err = new Error('id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' + - req.body._id); + const err = new Error( + 'id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' + req.body._id); err.status = codes.notfound; - next(err); - return; // prevent node to process this function further after next() has finished. + return next(err); } - // increment version manually as we do not use .save(.) req.body.updatedAt = new Date(); req.body.$inc = {__v: 1}; - // PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no need to - // reset attributes that are missing. - AppUserModel.findByIdAndUpdate(req.params.id, req.body, {new: true}).populate('squad').exec((err, item) => { - if (err) { - err.status = codes.wrongrequest; - } else if (!item) { - err = new Error('appUser not found'); - err.status = codes.notfound; - } else { - res.locals.items = item; - } - next(err); - }); + AppUserModel.findByIdAndUpdate(req.params.id, req.body, {new: true}) + .populate('squad') + .exec((err, item) => { + if (err) { + err.status = codes.wrongrequest; + } else if (!item) { + err = new Error('appUser not found'); + err.status = codes.notfound; + } else { + res.locals.items = item; + } + next(err); + }); }) .delete((req, res, next) => { @@ -68,10 +82,8 @@ account.route('/:id') err = new Error('item not found'); err.status = codes.notfound; } - // we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last handler - // user.use(..) res.locals.processed = true; - next(err); // this works because err is in normal case undefined and that is the same as no parameter + next(err); }); }) @@ -79,8 +91,6 @@ account.route('/:id') routerHandling.httpMethodNotAllowed ); -// this middleware function can be used, if you like or remove it -// it looks for object(s) in res.locals.items and if they exist, they are send to the client as json account.use(routerHandling.emptyResponse); module.exports = account; diff --git a/static/src/app/admin/admin.component.css b/static/src/app/admin/admin.component.css index 722fd6e..e69de29 100644 --- a/static/src/app/admin/admin.component.css +++ b/static/src/app/admin/admin.component.css @@ -1,27 +0,0 @@ -.overview { - padding-bottom: 50px!important; -} - -.trash { - cursor: pointer; -} - -.table { - overflow-wrap: break-word; - table-layout: fixed; -} - -.table-container { - margin-top: 10px; - overflow-x: auto; - padding: 5px; -} - -.table-head { - background: #222222; - color: white; -} - -.cell-outline { - outline: 1px solid #D4D4D4; -} diff --git a/static/src/app/admin/admin.component.html b/static/src/app/admin/admin.component.html index 441a176..0680b43 100644 --- a/static/src/app/admin/admin.component.html +++ b/static/src/app/admin/admin.component.html @@ -1,65 +1 @@ -
- -

Admin Panel

- -
-
- - - - - - - - - - - - - - - - - - - - - -
UsernameActivatedSecretFraktion/ SquadPermission
- {{user.username}} - - - - {{user.secret}} - - - - - - -
-
-
- -
+ diff --git a/static/src/app/admin/admin.component.ts b/static/src/app/admin/admin.component.ts index 3269428..ddc9b5f 100644 --- a/static/src/app/admin/admin.component.ts +++ b/static/src/app/admin/admin.component.ts @@ -1,68 +1,11 @@ -import {Component, OnInit} from '@angular/core'; -import {AppUser, Squad} from '../models/model-interfaces'; -import {Observable} from 'rxjs/Observable'; -import {AppUserService} from '../services/app-user-service/app-user.service'; -import {SquadService} from '../services/army-management/squad.service'; -import {Fraction} from '../utils/fraction.enum'; -import {SnackBarService} from '../services/user-interface/snack-bar/snack-bar.service'; +import {Component} from '@angular/core'; @Component({ - selector: 'admin-panel', + selector: 'cc-admin-component', templateUrl: './admin.component.html', styleUrls: ['./admin.component.css', '../style/overview.css'] }) -export class AdminComponent implements OnInit { - - users$: Observable; - - squads: Squad[] = []; - - readonly fraction = Fraction; - - constructor(private appUserService: AppUserService, - private squadService: SquadService, - private snackBarService: SnackBarService) { - } - - ngOnInit() { - this.users$ = this.appUserService.getUsers(); - this.squadService.findSquads().subscribe(squads => { - this.squads = squads; - }); - } - - updateAppUser(user) { - const updateObject = { - _id: user._id, - squad: user.squad, - activated: user.activated, - permission: user.permission - }; - - if (updateObject.squad === '0') { - updateObject.squad = null; - } - - this.appUserService.updateUser(updateObject) - .subscribe(resUser => { - this.snackBarService.showSuccess('generic.save.success'); - }); - } - - deleteUser(user) { - if (confirm('Soll der Nutzer "' + user.username + '" wirklich gelöscht werden?')) { - this.appUserService.deleteUser(user) - .subscribe((res) => { - }); - } - } - - /** - * compare ngValue with ngModel to assign selected element - */ - equals(o1: Squad, o2: Squad) { - if (o1 && o2) { - return o1._id === o2._id; - } +export class AdminComponent { + constructor() { } } diff --git a/static/src/app/admin/admin.module.ts b/static/src/app/admin/admin.module.ts index 7e41389..3662c5a 100644 --- a/static/src/app/admin/admin.module.ts +++ b/static/src/app/admin/admin.module.ts @@ -1,14 +1,36 @@ import {NgModule} from '@angular/core'; -import {AdminComponent} from './admin.component'; import {SharedModule} from '../shared.module'; import {AppUserService} from '../services/app-user-service/app-user.service'; import {CommonModule} from '@angular/common'; -import {RouterModule} from '@angular/router'; +import {adminRouterModule, adminRoutingComponents} from './admin.routing'; +import {HttpClient} from '@angular/common/http'; +import {TranslateHttpLoader} from '@ngx-translate/http-loader'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; + +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/admin/', '.json'); +} @NgModule({ - declarations: [AdminComponent], - imports: [CommonModule, SharedModule, RouterModule.forChild([{path: '', component: AdminComponent}])], + declarations: adminRoutingComponents, + + imports: [ + CommonModule, + SharedModule, + adminRouterModule, + + TranslateModule.forChild({ + loader: { + provide: TranslateLoader, + useFactory: (createTranslateLoader), + deps: [HttpClient] + }, + isolate: true + }) + ], + providers: [AppUserService] }) export class AdminModule { + static routes = adminRouterModule; } diff --git a/static/src/app/admin/admin.routing.ts b/static/src/app/admin/admin.routing.ts new file mode 100644 index 0000000..7168182 --- /dev/null +++ b/static/src/app/admin/admin.routing.ts @@ -0,0 +1,37 @@ +import {RouterModule, Routes} from '@angular/router'; +import {ModuleWithProviders} from '@angular/core'; +import {AdminComponent} from './admin.component'; +import {AppUserListComponent} from './user-list/app-user-list.component'; +import {EditAppUserComponent} from './edit-app-user/edit-app-user.component'; +import {AppUserItemComponent} from './user-list/app-user-item.component'; + +export const adminRoutes: Routes = [ + { + path: 'users', + children: [ + { + path: '', + component: AdminComponent, + outlet: 'left', + children: [{ + path: '', + component: AppUserListComponent + }] + }, + { + path: 'new', + component: EditAppUserComponent, + outlet: 'right' + }, + { + path: 'edit/:id', + component: EditAppUserComponent, + outlet: 'right' + } + ] + }, +]; + +export const adminRouterModule: ModuleWithProviders = RouterModule.forChild(adminRoutes); + +export const adminRoutingComponents = [AdminComponent, AppUserListComponent, AppUserItemComponent, EditAppUserComponent]; diff --git a/static/src/app/admin/edit-app-user/edit-app-user.component.css b/static/src/app/admin/edit-app-user/edit-app-user.component.css new file mode 100644 index 0000000..e69de29 diff --git a/static/src/app/admin/edit-app-user/edit-app-user.component.html b/static/src/app/admin/edit-app-user/edit-app-user.component.html new file mode 100644 index 0000000..ffad96a --- /dev/null +++ b/static/src/app/admin/edit-app-user/edit-app-user.component.html @@ -0,0 +1,61 @@ +
+

{{'user.submit.headline.edit' | translate}}

+

{{'user.submit.headline.new' | translate}}

+ + + + + + + +
+ + + +
+ + + + + + + + User + SQL + HL + MT + Admin + + + + + + +
diff --git a/static/src/app/admin/edit-app-user/edit-app-user.component.ts b/static/src/app/admin/edit-app-user/edit-app-user.component.ts new file mode 100644 index 0000000..6831aa4 --- /dev/null +++ b/static/src/app/admin/edit-app-user/edit-app-user.component.ts @@ -0,0 +1,88 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {AppUser, Rank, Squad} from '../../models/model-interfaces'; +import {SquadService} from '../../services/army-management/squad.service'; +import {Subscription} from 'rxjs/Subscription'; +import {NgForm} from '@angular/forms'; +import {Fraction} from '../../utils/fraction.enum'; +import {SnackBarService} from '../../services/user-interface/snack-bar/snack-bar.service'; +import {AppUserService} from '../../services/app-user-service/app-user.service'; + + +@Component({ + templateUrl: './edit-app-user.component.html', + styleUrls: ['./edit-app-user.component.css', '../../style/entry-form.css', '../../style/overview.css'], +}) +export class EditAppUserComponent implements OnInit { + + @ViewChild(NgForm) form: NgForm; + + subscription: Subscription; + + appUser: AppUser = {}; + + appUserSquadId; + + squads: Squad[] = []; + + ranks: Rank[] = []; + + ranksDisplay = 'none'; + + error: string; + + readonly fraction = Fraction; + + constructor(private router: Router, + private route: ActivatedRoute, + private appUserService: AppUserService, + private squadService: SquadService, + private snackBarService: SnackBarService) { + } + + ngOnInit() { + this.subscription = this.route.params + .map(params => params['id']) + .filter(id => id !== undefined) + .flatMap(id => this.appUserService.getAppUser(id)) + .subscribe(appUser => { + this.appUser = appUser; + this.appUserSquadId = appUser.squad ? appUser.squad._id : 'null'; + }); + + this.squadService.findSquads().subscribe(squads => { + this.squads = squads; + }); + } + + saveUser() { + const updateObject: AppUser = { + _id: this.appUser._id, + username: this.appUser.username, + squad: this.appUserSquadId === 'null' ? null : this.appUserSquadId, + activated: this.appUser.activated, + permission: this.appUser.permission, + }; + + this.appUserService.updateUser(updateObject) + .subscribe(appUser => { + this.appUser = appUser; + this.appUserSquadId = appUser.squad ? appUser.squad._id : 'null'; + this.snackBarService.showSuccess('generic.save.success'); + }); + } + + cancel() { + this.router.navigate([this.appUser._id ? '../..' : '..'], {relativeTo: this.route}); + return false; + } + + /** + * compare ngValue with ngModel to assign selected element + */ + equals(o1: Squad, o2: Squad) { + if (o1 && o2) { + return o1._id === o2._id; + } + } +} diff --git a/static/src/app/admin/user-list/app-user-item.component.css b/static/src/app/admin/user-list/app-user-item.component.css new file mode 100644 index 0000000..458e2b3 --- /dev/null +++ b/static/src/app/admin/user-list/app-user-item.component.css @@ -0,0 +1,6 @@ +.icon-award { + width: 27px; + height: 42px; + display: block; + margin-right: 25px; +} diff --git a/static/src/app/admin/user-list/app-user-item.component.html b/static/src/app/admin/user-list/app-user-item.component.html new file mode 100644 index 0000000..2ede66e --- /dev/null +++ b/static/src/app/admin/user-list/app-user-item.component.html @@ -0,0 +1,19 @@ +
+ +
+
+ + {{appUser.username}} + +
+ {{fraction.OPFOR}} - {{appUser.squad.name}} + {{fraction.BLUFOR}} - {{appUser.squad.name}} + {{'users.list.item.label.no.squad' | translate}} +
+ +
+ +
+
+
diff --git a/static/src/app/admin/user-list/app-user-item.component.ts b/static/src/app/admin/user-list/app-user-item.component.ts new file mode 100644 index 0000000..8e0a708 --- /dev/null +++ b/static/src/app/admin/user-list/app-user-item.component.ts @@ -0,0 +1,38 @@ +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {AppUser} from '../../models/model-interfaces'; +import {Fraction} from '../../utils/fraction.enum'; + +@Component({ + selector: 'cc-app-user-item', + templateUrl: './app-user-item.component.html', + styleUrls: ['./app-user-item.component.css', '../../style/list-entry.css'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AppUserItemComponent { + + @Input() appUser: AppUser; + + @Input() selected: boolean; + + @Output() userSelected = new EventEmitter(); + @Output() userAward = new EventEmitter(); + @Output() userDelete = new EventEmitter(); + + readonly fraction = Fraction; + + constructor() { + } + + select() { + this.userSelected.emit(this.appUser._id); + } + + award() { + this.userAward.emit(this.appUser._id); + } + + delete() { + this.userDelete.emit(this.appUser); + } +} + diff --git a/static/src/app/admin/user-list/app-user-list.component.css b/static/src/app/admin/user-list/app-user-list.component.css new file mode 100644 index 0000000..e69de29 diff --git a/static/src/app/admin/user-list/app-user-list.component.html b/static/src/app/admin/user-list/app-user-list.component.html new file mode 100644 index 0000000..fcc64cb --- /dev/null +++ b/static/src/app/admin/user-list/app-user-list.component.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/static/src/app/admin/user-list/app-user-list.component.ts b/static/src/app/admin/user-list/app-user-list.component.ts new file mode 100644 index 0000000..9c440d5 --- /dev/null +++ b/static/src/app/admin/user-list/app-user-list.component.ts @@ -0,0 +1,71 @@ +import {Component} from '@angular/core'; + +import {FormControl} from '@angular/forms'; +import {ActivatedRoute, Router} from '@angular/router'; +import {Observable} from 'rxjs/Observable'; +import {AppUser, Squad} from '../../models/model-interfaces'; +import {Fraction} from '../../utils/fraction.enum'; +import {MatButtonToggleGroup} from '@angular/material'; +import {UIHelpers} from '../../utils/global.helpers'; +import {TranslateService} from '@ngx-translate/core'; +import {AppUserService} from '../../services/app-user-service/app-user.service'; +import {SquadService} from '../../services/army-management/squad.service'; + +@Component({ + selector: 'cc-app-user-list', + templateUrl: './app-user-list.component.html', + styleUrls: ['./app-user-list.component.css', '../../style/select-list.css'] +}) +export class AppUserListComponent { + + selectedUserId: string | number = null; + + appUsers$: Observable; + + readonly fraction = Fraction; + + searchTerm = new FormControl(); + + radioModel = ''; + + constructor(private appUserService: AppUserService, + private squadService: SquadService, + private router: Router, + private route: ActivatedRoute, + private translate: TranslateService) { + this.appUsers$ = this.appUserService.getUsers(); + } + + initObservable(observables: any) { + Observable.merge(observables.params as Observable, observables.searchTerm) + .distinctUntilChanged() + .switchMap(query => this.filterAppUsers()) + .subscribe(); + } + + openNewUserForm() { + this.selectedUserId = null; + this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route}); + } + + selectUser(userId: string) { + this.selectedUserId = userId; + this.router.navigate([{outlets: {'right': ['edit', userId]}}], {relativeTo: this.route}); + } + + deleteUser(user: AppUser) { + this.translate.get('users.list.delete.confirm', {name: user.username}).subscribe((confirmQuestion) => { + if (confirm(confirmQuestion)) { + this.appUserService.deleteUser(user) + .subscribe((res) => { + }); + } + }); + } + + filterAppUsers(group?: MatButtonToggleGroup) { + this.radioModel = UIHelpers.toggleReleaseButton(this.radioModel, group); + // TODO: Add filter attribute submit + return this.appUsers$ = this.appUserService.getUsers(); + } +} diff --git a/static/src/app/app.component.html b/static/src/app/app.component.html index 19a09f7..b370a38 100644 --- a/static/src/app/app.component.html +++ b/static/src/app/app.component.html @@ -89,8 +89,18 @@