Add server paging scroll for users

pull/14/head
Florian Hartwich 2017-10-14 15:26:05 +02:00
parent c0a2004ea8
commit ccd97ae066
13 changed files with 134 additions and 188 deletions

View File

@ -33,7 +33,6 @@ router.use((req, res, next) => {
if (offsetString) { if (offsetString) {
console.log(offsetString)
if (!isNaN(offsetString)) { if (!isNaN(offsetString)) {
offset = parseInt(offsetString); offset = parseInt(offsetString);
if (offset < 0) { if (offset < 0) {

View File

@ -12,90 +12,56 @@ const apiAuthenticationMiddleware = require('../middleware/auth-middleware');
const checkHl = require('../middleware/permission-check').checkHl; const checkHl = require('../middleware/permission-check').checkHl;
const sortCollectionBy = require('../middleware/util').sortCollection; const sortCollectionBy = require('../middleware/util').sortCollection;
const offsetlimitMiddleware = require('../middleware/limitoffset-middleware-mongo'); const offsetlimitMiddleware = require('../middleware/limitoffset-middleware-mongo');
const filterHandlerCreator = require('../middleware/filter-handler-mongo');
const routerHandling = require('../middleware/router-handling'); const routerHandling = require('../middleware/router-handling');
// Mongoose Model using mongoDB // Mongoose Model using mongoDB
const UserModel = require('../models/user'); const UserModel = require('../models/user');
const RankModel = require('../models/rank'); const SquadModel = require('../models/squad');
const AwardingModel = require('../models/awarding'); const AwardingModel = require('../models/awarding');
const resultSet = {'__v': 0, 'updatedAt': 0, 'timestamp': 0};
const users = express.Router(); const users = express.Router();
users.get('/', filterHandlerCreator(UserModel.schema.paths));
users.get('/', offsetlimitMiddleware); users.get('/', offsetlimitMiddleware);
// routes ********************** // routes **********************
users.route('/') users.route('/')
.get((req, res, next) => { .get((req, res, next) => {
if (req.query.simple) { const userQuery = () => {
UserModel.find({}, res.locals.filter, res.locals.limitskip, (err, items) => { UserModel.find(dbFilter, res.locals.filter, res.locals.limitskip)
if (err) { .populate('squadId')
err.status = codes.servererror; .exec((err, users) => {
return next(err); if (err) return next(err);
} if (users.length === 0) {
// if the collection is empty we do not send empty arrays back. res.locals.items = users;
res.locals.processed = true;
res.locals.items = items; return next();
res.locals.processed = true;
next();
})
}
else {
const nameQuery = req.query.q;
const fractionFilter = req.query.fractFilter;
const squadFilter = req.query.squadId;
UserModel.find({}, res.locals.filter, res.locals.limitskip, (err, users) => {
if (err) return next(err);
if (users.length === 0) {
res.locals.items = users;
res.locals.processed = true;
next();
}
let resUsers = [];
let rowsLength = users.length;
users.forEach((user) => {
// filter by name
if (!nameQuery || (nameQuery && user.username.toLowerCase().includes(nameQuery.toLowerCase()))) {
getExtendedUser(user, next, (extUser) => {
// filter by squad
if (squadFilter) {
if (extUser.squad && extUser.squad._id.toString() === squadFilter) {
resUsers.push(extUser);
}
else {
rowsLength -= 1;
}
}
// filter by fraction
else if (!fractionFilter ||
(fractionFilter && extUser.squad && extUser.squad.fraction.toLowerCase() === fractionFilter) ||
(fractionFilter && fractionFilter === 'unassigned' && !extUser.squad)) {
resUsers.push(extUser);
} else {
rowsLength -= 1;
}
if (resUsers.length === rowsLength) {
resUsers = sortCollectionBy(resUsers, 'username');
res.locals.items = resUsers;
res.locals.processed = true;
return next();
}
});
} else {
rowsLength -= 1;
// no user matching query - return empty []
if (rowsLength === 0) {
res.locals.items = resUsers;
res.locals.processed = true;
next();
}
} }
//users = sortCollectionBy(users, 'username');
UserModel.count(dbFilter, (err, totalCount) => {
res.set('x-total-count', totalCount);
res.locals.items = users;
res.locals.processed = true;
return next();
})
}) })
};
if (!req.query.q) req.query.q = ''
const dbFilter = {username: {"$regex": req.query.q, "$options": "i"}};
if (req.query.squadId) dbFilter["squadId"] = {"$eq": req.query.squadId};
// squad / fracion filter setup
if (req.query.fractFilter && req.query.fractFilter !== 'UNASSIGNED' && !req.query.squadId) {
SquadModel.find({'fraction': req.query.fractFilter}, {_id: 1}, (err, squads) => {
dbFilter['squadId'] = {$in: squads.map(squad => squad.id)};
userQuery();
}) })
} else {
if (req.query.fractFilter === 'UNASSIGNED') {
dbFilter['squadId'] = {$eq: null};
}
userQuery();
} }
}) })
@ -108,7 +74,8 @@ users.route('/')
return next(err); return next(err);
} }
res.status(codes.created); res.status(codes.created);
getExtendedUser(user, next, (extUser) => {
UserModel.populate(user, {path: 'squadId'}, (err, extUser) => {
res.locals.items = extUser; res.locals.items = extUser;
res.locals.processed = true; res.locals.processed = true;
return next(); return next();
@ -121,26 +88,19 @@ users.route('/')
users.route('/:id') users.route('/:id')
.get((req, res, next) => { .get((req, res, next) => {
UserModel.findById(req.params.id).populate('squadId').exec((err, user) => {
UserModel.findById(req.params.id, (err, item) => {
if (err) { if (err) {
err.status = codes.servererror; err.status = codes.servererror;
return next(err); return next(err);
} }
else if (!item) { else if (!user) {
err = new Error("item not found"); err = new Error("item not found");
err.status = codes.notfound; err.status = codes.notfound;
return next(err); return next(err);
} else if (req.query.simple) {
res.locals.items = item;
next();
} }
getExtendedUser(item, next, (extUser) => { res.locals.items = user;
res.locals.items = extUser; res.locals.processed = true;
res.locals.processed = true; return next();
return next();
})
}); });
}) })
@ -165,29 +125,21 @@ users.route('/:id')
else if (!item) { else if (!item) {
err = new Error("item not found"); err = new Error("item not found");
err.status = codes.notfound; err.status = codes.notfound;
} else if (req.query.simple) { }
res.locals.items = item; UserModel.populate(item, {path: 'squadId'}, (err, extUser) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
if (!user) {
res.locals.items = {};
res.locals.processed = true;
return next();
}
res.locals.items = extUser;
res.locals.processed = true; res.locals.processed = true;
return next(); return next();
} })
else {
UserModel.findById(item._id, (err, user) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
if (!user) {
res.locals.items = {};
res.locals.processed = true;
return next();
}
getExtendedUser(user, next, (extUser) => {
res.locals.items = extUser;
res.locals.processed = true;
return next();
})
})
}
}) })
}) })
@ -201,7 +153,7 @@ users.route('/:id')
return; // prevent node to process this function further after next() has finished. return; // prevent node to process this function further after next() has finished.
} }
// main difference of PUT and PATCH is that PUT expects all data in request: checked by using the schema // main difference of PUT and PATCH is that PUT expects all data in request: checked by using the schema
var user = new UserModel(req.body); const user = new UserModel(req.body);
UserModel.findById(req.params.id, req.body, {new: true}, function (err, item) { UserModel.findById(req.params.id, req.body, {new: true}, function (err, item) {
// with parameter {new: true} the TweetNModel will return the new and changed object from the DB and not the old one. // with parameter {new: true} the TweetNModel will return the new and changed object from the DB and not the old one.
if (err) { if (err) {
@ -213,7 +165,7 @@ users.route('/:id')
err.status = codes.notfound; err.status = codes.notfound;
return next(err); return next(err);
} }
// optional task 3b: check that version is still accurate // check that version is still accurate
else if (user.__v !== item.__v) { else if (user.__v !== item.__v) {
err = new Error("version outdated. Meanwhile update on item happened. Please GET resource again") err = new Error("version outdated. Meanwhile update on item happened. Please GET resource again")
err.status = codes.conflict; err.status = codes.conflict;
@ -227,7 +179,7 @@ users.route('/:id')
} }
} }
// optional task 3: update updatedAt and increase version // update updatedAt and increase version
item.updatedAt = new Date(); item.updatedAt = new Date();
item.increment(); // this sets __v++ item.increment(); // this sets __v++
item.save(function (err) { item.save(function (err) {
@ -237,7 +189,8 @@ users.route('/:id')
err.status = codes.wrongrequest; err.status = codes.wrongrequest;
err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors); err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors);
} }
getExtendedUser(item, next, (extUser) => {
UserModel.populate(item, {path: 'squadId'}, (err, extUser) => {
res.locals.items = extUser; res.locals.items = extUser;
res.locals.processed = true; res.locals.processed = true;
return next(); return next();
@ -286,56 +239,4 @@ users.route('/:id')
// it looks for object(s) in res.locals.items and if they exist, they are send to the client as json // it looks for object(s) in res.locals.items and if they exist, they are send to the client as json
users.use(routerHandling.emptyResponse); users.use(routerHandling.emptyResponse);
/**
* Create model for single extended user and
* return via callback
*/
let getExtendedUser = (user, next, callback) => {
let extUser = {};
UserModel.findById(user._id, resultSet)
.populate('squadId', resultSet).exec((err, member) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
extUser._id = user._id;
extUser.username = user.username;
extUser.squad = member.squadId;
if (extUser.squad) {
RankModel.findOne({
level: member.rankLvl,
fraction: member.squadId.fraction
}, resultSet, (err, rank) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
extUser.rank = rank;
}).then(() => {
addAwards(extUser).then(() => {
callback(extUser);
})
})
} else {
extUser.rank = {level: user.rankLvl};
addAwards(extUser).then(() => {
callback(extUser);
})
}
})
};
let addAwards = (extUser) => {
return AwardingModel.find({userId: extUser._id}, resultSet, {sort: {date: 'desc'}})
.populate('decorationId', resultSet)
.exec((err, awards) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
extUser.awards = awards;
})
};
module.exports = users; module.exports = users;

View File

@ -33,6 +33,7 @@
"ngx-bootstrap": "^2.0.0-beta.6", "ngx-bootstrap": "^2.0.0-beta.6",
"ngx-clipboard": "^8.1.0", "ngx-clipboard": "^8.1.0",
"ngx-cookie-service": "^1.0.9", "ngx-cookie-service": "^1.0.9",
"ngx-infinite-scroll": "^0.5.2",
"rxjs": "^5.2.0", "rxjs": "^5.2.0",
"ts-helpers": "^1.1.1", "ts-helpers": "^1.1.1",
"typescript": "^2.3.4", "typescript": "^2.3.4",

View File

@ -26,7 +26,9 @@ export class AppComponent {
} }
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
this.loading = false; this.loading = false;
window.scrollTo(0, 0); // if (!router.url.includes('right')) {
// window.scrollTo(0, 0);
// }
} }
}); });
} }

View File

@ -11,7 +11,8 @@ export interface User {
_id?: string; _id?: string;
boardUserId?: number; boardUserId?: number;
username?: string; username?: string;
squad?: any; //Squad or id-string squadId?: any; //Squad or id-string
rankLvl?: number;
rank?: Rank; rank?: Rank;
awards?: Award[]; awards?: Award[];
} }

View File

@ -22,6 +22,9 @@ export class UserStore {
case LOAD: case LOAD:
return [...action.data]; return [...action.data];
case ADD: case ADD:
if (action.data instanceof Array) {
return users.concat(action.data);
}
return [...users, action.data]; return [...users, action.data];
case EDIT: case EDIT:
return users.map(user => { return users.map(user => {

View File

@ -11,13 +11,15 @@ export class UserService {
users$: Observable<User[]>; users$: Observable<User[]>;
totalCount = 0;
constructor(private http: HttpClient, constructor(private http: HttpClient,
private userStore: UserStore, private userStore: UserStore,
private config: AppConfig) { private config: AppConfig) {
this.users$ = userStore.items$; this.users$ = userStore.items$;
} }
findUsers(query = '', fractionFilter?, squadFilter?) { findUsers(query = '', fractionFilter?, squadFilter?, limit?, offset?, action?) {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.append('q', query); searchParams.append('q', query);
if (fractionFilter) { if (fractionFilter) {
@ -26,10 +28,17 @@ export class UserService {
if (squadFilter) { if (squadFilter) {
searchParams.append('squadId', squadFilter); searchParams.append('squadId', squadFilter);
} }
searchParams.append('limit', limit);
searchParams.append('offset', offset);
this.http.get(this.config.apiUserPath, searchParams) this.http.get(this.config.apiUserPath, searchParams)
.map(res => res.json()) .do((res) => {
.do((users) => { let headerCount = parseInt(res.headers.get('x-total-count'));
this.userStore.dispatch({type: LOAD, data: users}); console.log(headerCount)
if (headerCount) {
this.totalCount = headerCount;
}
let users = res.json();
this.userStore.dispatch({type: action, data: users});
}).subscribe(_ => { }).subscribe(_ => {
}); });

View File

@ -19,7 +19,7 @@
<select class="form-control" <select class="form-control"
name="squad" name="squad"
id="squad" id="squad"
[(ngModel)]="user.squad" [(ngModel)]="user.squadId"
[compareWith]="equals" [compareWith]="equals"
(change)="toggleRanks()"> (change)="toggleRanks()">
<option [value]="0">Ohne Fraktion/ Squad</option> <option [value]="0">Ohne Fraktion/ Squad</option>
@ -35,7 +35,7 @@
<label for="rank">Rang</label> <label for="rank">Rang</label>
<select class="form-control" <select class="form-control"
name="rank" name="rank"
id="rank" [ngModel]="user.rank?.level" id="rank" [ngModel]="user.rankLvl"
#rankLevel #rankLevel
style="min-width: 200px;"> style="min-width: 200px;">
<option *ngFor="let rank of ranks" [value]="rank.level">{{rank.name}}</option> <option *ngFor="let rank of ranks" [value]="rank.level">{{rank.name}}</option>

View File

@ -18,7 +18,7 @@ export class EditUserComponent {
subscription: Subscription; subscription: Subscription;
user: User = {username: '', squad: '0', rank: {level: 0}}; user: User = {username: '', squadId: '0', rankLvl: 0};
squads: Squad[] = []; squads: Squad[] = [];
@ -46,11 +46,11 @@ export class EditUserComponent {
.filter(id => id != undefined) .filter(id => id != undefined)
.flatMap(id => this.userService.getUser(id)) .flatMap(id => this.userService.getUser(id))
.subscribe(user => { .subscribe(user => {
if (!user.squad) { if (!user.squadId) {
user.squad = "0"; user.squadId = "0";
this.ranksDisplay = 'none'; this.ranksDisplay = 'none';
} else { } else {
this.rankService.findRanks('', user.squad.fraction).subscribe(ranks => { this.rankService.findRanks('', user.squadId.fraction).subscribe(ranks => {
this.ranks = ranks; this.ranks = ranks;
this.ranksDisplay = 'block'; this.ranksDisplay = 'block';
}); });
@ -64,8 +64,8 @@ export class EditUserComponent {
} }
toggleRanks() { toggleRanks() {
if (this.user.squad != '0') { if (this.user.squadId != '0') {
this.rankService.findRanks('', this.user.squad.fraction).subscribe( this.rankService.findRanks('', this.user.squadId.fraction).subscribe(
ranks => { ranks => {
this.ranks = ranks; this.ranks = ranks;
this.ranksDisplay = 'block'; this.ranksDisplay = 'block';
@ -83,8 +83,8 @@ export class EditUserComponent {
rankLvl: rankLevel, rankLvl: rankLevel,
squadId: null squadId: null
}; };
if (this.user.squad._id !== '0') { if (this.user.squadId._id !== '0') {
updateObject.squadId = this.user.squad._id updateObject.squadId = this.user.squadId._id
} }
if (this.user._id) { if (this.user._id) {
@ -107,7 +107,7 @@ export class EditUserComponent {
}, },
error => { error => {
// duplicated user error message // duplicated user error message
if (error._body.indexOf('duplicate') >= 0) { if (error._body.includes('duplicate')) {
this.error = "Benutzername existiert bereits"; this.error = "Benutzername existiert bereits";
this.showErrorLabel = true; this.showErrorLabel = true;
setTimeout(() => { setTimeout(() => {

View File

@ -6,9 +6,9 @@
<a>{{user.username}}</a> <a>{{user.username}}</a>
</span> </span>
<br> <br>
<small *ngIf="user.squad && user.squad.fraction == 'OPFOR'">CSAT - {{user.squad.name}}</small> <small *ngIf="user.squadId && user.squadId.fraction == 'OPFOR'">CSAT - {{user.squadId.name}}</small>
<small *ngIf="user.squad && user.squad.fraction == 'BLUFOR'">NATO - {{user.squad.name}}</small> <small *ngIf="user.squadId && user.squadId.fraction == 'BLUFOR'">NATO - {{user.squadId.name}}</small>
<small *ngIf="!user.squad">ohne Squad/Fraktion</small> <small *ngIf="!user.squadId">ohne Squad/Fraktion</small>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">

View File

@ -1,9 +1,9 @@
<div class="select-list"> <div class="select-list">
<div class="input-group list-header pull-left"> <div class="input-group list-header pull-left">
<div class="btn-group" (click)="filterUsers()"> <div class="btn-group" (click)="filterUsers()">
<label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="blufor" uncheckable>NATO</label> <label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="BLUFOR" uncheckable>NATO</label>
<label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="opfor" uncheckable>CSAT</label> <label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="OPFOR" uncheckable>CSAT</label>
<label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="unassigned" uncheckable>Ohne Squad</label> <label class="btn btn-success" [(ngModel)]="radioModel" btnRadio="UNASSIGNED" uncheckable>Ohne Squad</label>
</div> </div>
<a class="pull-right btn btn-success" (click)="openNewUserForm()">+</a> <a class="pull-right btn btn-success" (click)="openNewUserForm()">+</a>
</div> </div>
@ -21,7 +21,12 @@
</span> </span>
</div> </div>
<div> <div class="search-results"
data-infinite-scroll
debounce
[infiniteScrollDistance]="scrollDistance"
[infiniteScrollThrottle]="throttle"
(scrolled)="onScrollDown()">
<pjm-user-item *ngFor="let user of users$ | async" <pjm-user-item *ngFor="let user of users$ | async"
[user]="user" [user]="user"
(userDelete)="deleteUser(user)" (userDelete)="deleteUser(user)"

View File

@ -6,6 +6,7 @@ import {ActivatedRoute, Router} from "@angular/router";
import {Observable} from "rxjs/Observable"; import {Observable} from "rxjs/Observable";
import {UserService} from "../../services/user-service/user.service"; import {UserService} from "../../services/user-service/user.service";
import {User} from "../../models/model-interfaces"; import {User} from "../../models/model-interfaces";
import {ADD, LOAD} from "../../services/stores/user.store";
@Component({ @Component({
selector: 'squad-list', selector: 'squad-list',
@ -22,6 +23,14 @@ export class UserListComponent implements OnInit {
public radioModel: string; public radioModel: string;
throttle = 300;
scrollDistance = 1;
offset = 0;
limit = 20;
constructor(private userService: UserService, constructor(private userService: UserService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -42,9 +51,8 @@ export class UserListComponent implements OnInit {
Observable.merge(paramsStream, searchTermStream) Observable.merge(paramsStream, searchTermStream)
.distinctUntilChanged() .distinctUntilChanged()
.switchMap(query => this.userService.findUsers(query, this.radioModel)) .switchMap(query => this.filterUsers())
.subscribe(); .subscribe();
} }
openNewUserForm() { openNewUserForm() {
@ -70,8 +78,24 @@ export class UserListComponent implements OnInit {
} }
} }
filterUsers() { filterUsers(action?) {
this.users$ = this.userService.findUsers(this.searchTerm.value, this.radioModel); if (!action || (action && action === LOAD)) {
action = LOAD;
this.offset = 0;
this.limit = 20;
}
return this.users$ = this.userService.findUsers(this.searchTerm.value, this.radioModel,
null, this.limit, this.offset, action);
}
onScrollDown() {
if (this.offset + this.limit > this.userService.totalCount) {
this.limit = this.userService.totalCount - this.offset;
}
if (this.limit != 0) {
this.offset += this.limit;
this.filterUsers(ADD);
}
} }
adjustBrowserUrl(queryString = '') { adjustBrowserUrl(queryString = '') {

View File

@ -3,10 +3,11 @@ import {usersRouterModule, usersRoutingComponents} from './users.routing';
import {CommonModule} from "@angular/common"; import {CommonModule} from "@angular/common";
import {SharedModule} from "../shared.module"; import {SharedModule} from "../shared.module";
import {ButtonsModule} from "ngx-bootstrap"; import {ButtonsModule} from "ngx-bootstrap";
import {InfiniteScrollModule} from "ngx-infinite-scroll";
@NgModule({ @NgModule({
declarations: usersRoutingComponents, declarations: usersRoutingComponents,
imports: [CommonModule, SharedModule, ButtonsModule.forRoot(), usersRouterModule], imports: [CommonModule, SharedModule, ButtonsModule.forRoot(), InfiniteScrollModule, usersRouterModule],
}) })
export class UsersModule { export class UsersModule {
static routes = usersRouterModule; static routes = usersRouterModule;