Browse Source

Feature/page size (#448)

* Define page size and performance improvements.

* More performance improvements.

* More improvements.
pull/450/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
4347b7cbe2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      frontend/app/_theme.html
  2. 2
      frontend/app/features/administration/pages/users/user.component.ts
  3. 2
      frontend/app/features/administration/pages/users/users-page.component.html
  4. 8
      frontend/app/features/administration/pages/users/users-page.component.ts
  5. 9
      frontend/app/features/administration/state/users.state.spec.ts
  6. 14
      frontend/app/features/administration/state/users.state.ts
  7. 8
      frontend/app/features/assets/pages/assets-page.component.ts
  8. 2
      frontend/app/features/content/pages/content/content-history-page.component.html
  9. 2
      frontend/app/features/content/pages/contents/contents-page.component.html
  10. 8
      frontend/app/features/content/pages/contents/contents-page.component.ts
  11. 4
      frontend/app/features/content/shared/array-item.component.ts
  12. 4
      frontend/app/features/content/shared/content-list-field.component.ts
  13. 4
      frontend/app/features/content/shared/content.component.ts
  14. 2
      frontend/app/features/content/shared/contents-selector.component.html
  15. 12
      frontend/app/features/content/shared/contents-selector.component.ts
  16. 7
      frontend/app/features/content/shared/due-time-selector.component.ts
  17. 2
      frontend/app/features/rules/pages/events/rule-events-page.component.html
  18. 8
      frontend/app/features/rules/pages/events/rule-events-page.component.ts
  19. 6
      frontend/app/features/schemas/pages/schema/schema-fields.component.ts
  20. 8
      frontend/app/features/settings/pages/clients/client-connect-form.component.ts
  21. 2
      frontend/app/features/settings/pages/clients/client.component.html
  22. 4
      frontend/app/features/settings/pages/clients/client.component.ts
  23. 2
      frontend/app/features/settings/pages/contributors/contributor-add-form.component.html
  24. 2
      frontend/app/features/settings/pages/contributors/contributor.component.ts
  25. 2
      frontend/app/features/settings/pages/contributors/contributors-page.component.html
  26. 8
      frontend/app/features/settings/pages/contributors/contributors-page.component.ts
  27. 6
      frontend/app/features/settings/pages/languages/language.component.ts
  28. 4
      frontend/app/features/settings/pages/patterns/pattern.component.ts
  29. 9
      frontend/app/features/settings/pages/plans/plan.component.ts
  30. 4
      frontend/app/features/settings/pages/roles/role.component.ts
  31. 2
      frontend/app/framework/angular/avatar.component.ts
  32. 4
      frontend/app/framework/angular/external-link.directive.ts
  33. 11
      frontend/app/framework/angular/forms/autocomplete.component.html
  34. 2
      frontend/app/framework/angular/forms/focus-on-init.directive.ts
  35. 11
      frontend/app/framework/angular/forms/tag-editor.component.html
  36. 25
      frontend/app/framework/angular/image-source.directive.ts
  37. 4
      frontend/app/framework/angular/modals/modal-dialog.component.html
  38. 31
      frontend/app/framework/angular/modals/modal-dialog.component.ts
  39. 2
      frontend/app/framework/angular/modals/tooltip.directive.ts
  40. 11
      frontend/app/framework/angular/pager.component.html
  41. 9
      frontend/app/framework/angular/pager.component.scss
  42. 17
      frontend/app/framework/angular/pager.component.ts
  43. 16
      frontend/app/framework/utils/pager.spec.ts
  44. 6
      frontend/app/framework/utils/pager.ts
  45. 124
      frontend/app/shared/components/asset.component.html
  46. 25
      frontend/app/shared/components/asset.component.ts
  47. 2
      frontend/app/shared/components/assets-list.component.html
  48. 15
      frontend/app/shared/components/assets-list.component.ts
  49. 4
      frontend/app/shared/components/assets-selector.component.ts
  50. 2
      frontend/app/shared/components/comment.component.html
  51. 2
      frontend/app/shared/components/history-list.component.html
  52. 11
      frontend/app/shared/components/pipes.ts
  53. 4
      frontend/app/shared/components/queries/filter-comparison.component.ts
  54. 4
      frontend/app/shared/components/queries/filter-logical.component.ts
  55. 4
      frontend/app/shared/components/search-form.component.ts
  56. 8
      frontend/app/shared/state/assets.state.spec.ts
  57. 22
      frontend/app/shared/state/assets.state.ts
  58. 14
      frontend/app/shared/state/contents.state.ts
  59. 29
      frontend/app/shared/state/contributors.state.spec.ts
  60. 39
      frontend/app/shared/state/contributors.state.ts
  61. 10
      frontend/app/shared/state/rule-events.state.spec.ts
  62. 14
      frontend/app/shared/state/rule-events.state.ts
  63. 4
      frontend/app/shared/state/schemas.state.spec.ts
  64. 6
      frontend/app/shell/pages/internal/internal-area.component.ts
  65. 2
      frontend/app/shell/pages/internal/profile-menu.component.html
  66. 10
      frontend/app/theme/_panels.scss

4
frontend/app/_theme.html

@ -1249,8 +1249,8 @@
Modal body text goes here. Modal body text goes here.
</div> </div>
<div class="modal-footer" [hidden]="!showFooter"> <div class="modal-footer">
<div class="clearfix" #footerElement> <div class="clearfix">
<button type="button" class="float-right btn btn-primary">Save changes</button> <button type="button" class="float-right btn btn-primary">Save changes</button>
<button type="button" class="float-left btn btn-secondary">Close</button> <button type="button" class="float-left btn btn-secondary">Close</button>
</div> </div>

2
frontend/app/features/administration/pages/users/user.component.ts

@ -16,7 +16,7 @@ import { UserDto, UsersState } from '@app/features/administration/internal';
template: ` template: `
<tr [routerLink]="user.id" routerLinkActive="active"> <tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [attr.src]="user | sqxUserDtoPicture" /> <img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture" />
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span> <span class="user-name table-cell">{{user.displayName}}</span>

2
frontend/app/features/administration/pages/users/users-page.component.html

@ -59,7 +59,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="usersState.usersPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [pager]="usersState.usersPager | async" (pagerChange)="usersState.setPager($event)"></sqx-pager>
</div> </div>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

8
frontend/app/features/administration/pages/users/users-page.component.ts

@ -35,14 +35,6 @@ export class UsersPageComponent implements OnInit {
this.usersState.search(this.usersFilter.value); this.usersState.search(this.usersFilter.value);
} }
public goPrev() {
this.usersState.goPrev();
}
public goNext() {
this.usersState.goNext();
}
public trackByUser(index: number, user: UserDto) { public trackByUser(index: number, user: UserDto) {
return user.id; return user.id;
} }

9
frontend/app/features/administration/state/users.state.spec.ts

@ -8,7 +8,7 @@
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService } from '@app/shared'; import { DialogService, Pager } from '@app/shared';
import { import {
UserDto, UserDto,
@ -87,16 +87,15 @@ describe('UsersState', () => {
expect(usersState.snapshot.selectedUser).toEqual(newUsers[0]); expect(usersState.snapshot.selectedUser).toEqual(newUsers[0]);
}); });
it('should load next page and prev page when paging', () => { it('should load with new pagination when paging', () => {
usersService.setup(x => x.getUsers(10, 0, undefined)) usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => of(oldUsers)).verifiable(Times.exactly(2)); .returns(() => of(oldUsers)).verifiable(Times.once());
usersService.setup(x => x.getUsers(10, 10, undefined)) usersService.setup(x => x.getUsers(10, 10, undefined))
.returns(() => of(new UsersDto(200, []))).verifiable(); .returns(() => of(new UsersDto(200, []))).verifiable();
usersState.load().subscribe(); usersState.load().subscribe();
usersState.goNext().subscribe(); usersState.setPager(new Pager(20, 1, 10)).subscribe();
usersState.goPrev().subscribe();
expect().nothing(); expect().nothing();
}); });

14
frontend/app/features/administration/state/users.state.ts

@ -69,7 +69,7 @@ export class UsersState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly usersService: UsersService private readonly usersService: UsersService
) { ) {
super({ users: [], usersPager: new Pager(0) }); super({ users: [], usersPager: Pager.DEFAULT });
} }
public select(id: string | null): Observable<UserDto | null> { public select(id: string | null): Observable<UserDto | null> {
@ -173,19 +173,13 @@ export class UsersState extends State<Snapshot> {
} }
public search(query: string): Observable<UsersResult> { public search(query: string): Observable<UsersResult> {
this.next(s => ({ ...s, usersPager: new Pager(0), usersQuery: query })); this.next(s => ({ ...s, usersPager: s.usersPager.reset(), usersQuery: query }));
return this.loadInternal(); return this.loadInternal();
} }
public goNext(): Observable<UsersResult> { public setPager(usersPager: Pager) {
this.next(s => ({ ...s, usersPager: s.usersPager.goNext() })); this.next(s => ({ ...s, usersPager }));
return this.loadInternal();
}
public goPrev(): Observable<UsersResult> {
this.next(s => ({ ...s, usersPager: s.usersPager.goPrev() }));
return this.loadInternal(); return this.loadInternal();
} }

8
frontend/app/features/assets/pages/assets-page.component.ts

@ -59,14 +59,6 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
this.assetsState.toggleTag(tag); this.assetsState.toggleTag(tag);
} }
public goNext() {
this.assetsState.goNext();
}
public goPrev() {
this.assetsState.goPrev();
}
public changeView(isListView: boolean) { public changeView(isListView: boolean) {
this.isListView = isListView; this.isListView = isListView;

2
frontend/app/features/content/pages/content/content-history-page.component.html

@ -6,7 +6,7 @@
<ng-container content> <ng-container content>
<div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters"> <div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto"> <div class="col-auto">
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [attr.src]="event.actor | sqxUserPictureRef" /> <img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [src]="event.actor | sqxUserPictureRef" />
</div> </div>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <div class="event-message">

2
frontend/app/features/content/pages/contents/contents-page.component.html

@ -101,7 +101,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [pager]="contentsState.contentsPager | async" (pagerChange)="contentsState.setPager($event)"></sqx-pager>
</div> </div>
</ng-container> </ng-container>

8
frontend/app/features/content/pages/contents/contents-page.component.ts

@ -137,14 +137,6 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
.subscribe(); .subscribe();
} }
public goPrev() {
this.contentsState.goPrev();
}
public goNext() {
this.contentsState.goNext();
}
public search(query: Query) { public search(query: Query) {
this.contentsState.search(query); this.contentsState.search(query);
} }

4
frontend/app/features/content/shared/array-item.component.ts

@ -146,13 +146,13 @@ export class ArrayItemComponent implements OnChanges, OnDestroy {
public collapse() { public collapse() {
this.isHidden = true; this.isHidden = true;
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
} }
public expand() { public expand() {
this.isHidden = false; this.isHidden = false;
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
} }
public emitClone() { public emitClone() {

4
frontend/app/features/content/shared/content-list-field.component.ts

@ -29,7 +29,7 @@ import {
<small class="truncate">{{content.created | sqxFromNow}}</small> <small class="truncate">{{content.created | sqxFromNow}}</small>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar"> <ng-container *ngSwitchCase="metaFields.createdByAvatar">
<img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [attr.src]="content.createdBy | sqxUserPictureRef" /> <img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [src]="content.createdBy | sqxUserPictureRef" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName"> <ng-container *ngSwitchCase="metaFields.createdByName">
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small> <small class="truncate">{{content.createdBy | sqxUserNameRef}}</small>
@ -38,7 +38,7 @@ import {
<small class="truncate">{{content.lastModified | sqxFromNow}}</small> <small class="truncate">{{content.lastModified | sqxFromNow}}</small>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar"> <ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" /> <img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [src]="content.lastModifiedBy | sqxUserPictureRef" />
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName"> <ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small> <small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small>

4
frontend/app/features/content/shared/content.component.ts

@ -104,11 +104,11 @@ export class ContentComponent implements OnChanges {
.subscribe(() => { .subscribe(() => {
this.patchForm.submitCompleted({ noReset: true}); this.patchForm.submitCompleted({ noReset: true});
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
}, error => { }, error => {
this.patchForm.submitFailed(error); this.patchForm.submitFailed(error);
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
}); });
} }
} }

2
frontend/app/features/content/shared/contents-selector.component.html

@ -77,7 +77,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [pager]="contentsState.contentsPager | async" (pagerChange)="contentsState.setPager($event)"></sqx-pager>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

12
frontend/app/features/content/shared/contents-selector.component.ts

@ -79,8 +79,6 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
} }
this.selectSchema(this.schemas[0]); this.selectSchema(this.schemas[0]);
this.changeDetector.detectChanges();
} }
public selectSchema(selected: string | SchemaDto) { public selectSchema(selected: string | SchemaDto) {
@ -98,7 +96,7 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.updateModel(); this.updateModel();
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
} }
}); });
} }
@ -111,14 +109,6 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.contentsState.search(query); this.contentsState.search(query);
} }
public goNext() {
this.contentsState.goNext();
}
public goPrev() {
this.contentsState.goPrev();
}
public isItemSelected(content: ContentDto) { public isItemSelected(content: ContentDto) {
return !!this.selectedItems[content.id]; return !!this.selectedItems[content.id];
} }

7
frontend/app/features/content/shared/due-time-selector.component.ts

@ -8,15 +8,12 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { DialogModel, fadeAnimation } from '@app/shared'; import { DialogModel } from '@app/shared';
@Component({ @Component({
selector: 'sqx-due-time-selector', selector: 'sqx-due-time-selector',
styleUrls: ['./due-time-selector.component.scss'], styleUrls: ['./due-time-selector.component.scss'],
templateUrl: './due-time-selector.component.html', templateUrl: './due-time-selector.component.html'
animations: [
fadeAnimation
]
}) })
export class DueTimeSelectorComponent { export class DueTimeSelectorComponent {
public dueTimeDialog = new DialogModel(); public dueTimeDialog = new DialogModel();

2
frontend/app/features/rules/pages/events/rule-events-page.component.html

@ -90,7 +90,7 @@
</tbody> </tbody>
</table> </table>
<sqx-pager [pager]="ruleEventsState.ruleEventsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [pager]="ruleEventsState.ruleEventsPager | async" (pagerChange)="ruleEventsState.setPager($event)"></sqx-pager>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

8
frontend/app/features/rules/pages/events/rule-events-page.component.ts

@ -43,14 +43,6 @@ export class RuleEventsPageComponent extends ResourceOwner implements OnInit {
this.ruleEventsState.load(true); this.ruleEventsState.load(true);
} }
public goNext() {
this.ruleEventsState.goNext();
}
public goPrev() {
this.ruleEventsState.goPrev();
}
public enqueue(event: RuleEventDto) { public enqueue(event: RuleEventDto) {
this.ruleEventsState.enqueue(event); this.ruleEventsState.enqueue(event);
} }

6
frontend/app/features/schemas/pages/schema/schema-fields.component.ts

@ -10,7 +10,6 @@ import { Component, Input, OnInit } from '@angular/core';
import { import {
DialogModel, DialogModel,
fadeAnimation,
FieldDto, FieldDto,
fieldTypes, fieldTypes,
PatternsState, PatternsState,
@ -22,10 +21,7 @@ import {
@Component({ @Component({
selector: 'sqx-schema-fields', selector: 'sqx-schema-fields',
styleUrls: ['./schema-fields.component.scss'], styleUrls: ['./schema-fields.component.scss'],
templateUrl: './schema-fields.component.html', templateUrl: './schema-fields.component.html'
animations: [
fadeAnimation
]
}) })
export class SchemaFieldsComponent implements OnInit { export class SchemaFieldsComponent implements OnInit {
public fieldTypes = fieldTypes; public fieldTypes = fieldTypes;

8
frontend/app/features/settings/pages/clients/client-connect-form.component.ts

@ -14,17 +14,13 @@ import {
ClientDto, ClientDto,
ClientsService, ClientsService,
DialogService, DialogService,
fadeAnimation,
RoleDto RoleDto
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-client-connect-form', selector: 'sqx-client-connect-form',
styleUrls: ['./client-connect-form.component.scss'], styleUrls: ['./client-connect-form.component.scss'],
templateUrl: './client-connect-form.component.html', templateUrl: './client-connect-form.component.html'
animations: [
fadeAnimation
]
}) })
export class ClientConnectFormComponent implements OnInit { export class ClientConnectFormComponent implements OnInit {
@Output() @Output()
@ -67,7 +63,7 @@ export class ClientConnectFormComponent implements OnInit {
.subscribe(dto => { .subscribe(dto => {
this.connectToken = dto; this.connectToken = dto;
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
}); });

2
frontend/app/features/settings/pages/clients/client.component.html

@ -42,7 +42,7 @@
Client Secret Client Secret
</label> </label>
<div class="col cell-input"> <div class="col cell-input">
<input readonly class="form-control" [attr.value]="client.secret" #inputSecret /> <input readonly class="form-control" [value]="client.secret" #inputSecret />
</div> </div>
<div class="col-auto cell-actions no-padding"> <div class="col-auto cell-actions no-padding">
<button type="button" class="btn btn-text" [sqxCopy]="inputSecret"> <button type="button" class="btn btn-text" [sqxCopy]="inputSecret">

4
frontend/app/features/settings/pages/clients/client.component.ts

@ -12,7 +12,6 @@ import {
ClientDto, ClientDto,
ClientsState, ClientsState,
DialogModel, DialogModel,
fadeAnimation,
RoleDto RoleDto
} from '@app/shared'; } from '@app/shared';
@ -20,9 +19,6 @@ import {
selector: 'sqx-client', selector: 'sqx-client',
styleUrls: ['./client.component.scss'], styleUrls: ['./client.component.scss'],
templateUrl: './client.component.html', templateUrl: './client.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ClientComponent { export class ClientComponent {

2
frontend/app/features/settings/pages/contributors/contributor-add-form.component.html

@ -5,7 +5,7 @@
<sqx-autocomplete [source]="usersDataSource" formControlName="user" inputName="contributor" placeholder="Find existing user or invite by email" displayProperty="displayName"> <sqx-autocomplete [source]="usersDataSource" formControlName="user" inputName="contributor" placeholder="Find existing user or invite by email" displayProperty="displayName">
<ng-template let-user="$implicit"> <ng-template let-user="$implicit">
<span class="autocomplete-user"> <span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" /> <img class="user-picture autocomplete-user-picture" [src]="user | sqxUserDtoPicture" />
<span class="user-name autocomplete-user-name">{{user.displayName}}</span> <span class="user-name autocomplete-user-name">{{user.displayName}}</span>
</span> </span>

2
frontend/app/features/settings/pages/contributors/contributor.component.ts

@ -20,7 +20,7 @@ import {
template: ` template: `
<tr> <tr>
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" title="{{contributor.contributorName}}" [attr.src]="contributor.contributorId | sqxUserPicture" /> <img class="user-picture" title="{{contributor.contributorName}}" [src]="contributor.contributorId | sqxUserPicture" />
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell" [innerHTML]="contributor.contributorName | sqxHighlight:search"></span> <span class="user-name table-cell" [innerHTML]="contributor.contributorName | sqxHighlight:search"></span>

2
frontend/app/features/settings/pages/contributors/contributors-page.component.html

@ -52,7 +52,7 @@
</tbody> </tbody>
</table> </table>
<sqx-pager [pager]="contributorsState.contributorsPager | async" [autoHide]="true" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [autoHide]="true" [pager]="contributorsState.contributorsPager | async" (pagerChange)="contributorsState.setPager($event)"></sqx-pager>
</ng-container> </ng-container>
<ng-template #noContributors> <ng-template #noContributors>

8
frontend/app/features/settings/pages/contributors/contributors-page.component.ts

@ -38,14 +38,6 @@ export class ContributorsPageComponent implements OnInit {
this.contributorsState.load(true); this.contributorsState.load(true);
} }
public goPrev() {
this.contributorsState.goPrev();
}
public goNext() {
this.contributorsState.goNext();
}
public search(query: string) { public search(query: string) {
this.contributorsState.search(query); this.contributorsState.search(query);
} }

6
frontend/app/features/settings/pages/languages/language.component.ts

@ -12,7 +12,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
AppLanguageDto, AppLanguageDto,
EditLanguageForm, EditLanguageForm,
fadeAnimation,
LanguageDto, LanguageDto,
LanguagesState, LanguagesState,
sorted sorted
@ -21,10 +20,7 @@ import {
@Component({ @Component({
selector: 'sqx-language', selector: 'sqx-language',
styleUrls: ['./language.component.scss'], styleUrls: ['./language.component.scss'],
templateUrl: './language.component.html', templateUrl: './language.component.html'
animations: [
fadeAnimation
]
}) })
export class LanguageComponent implements OnChanges { export class LanguageComponent implements OnChanges {
@Input() @Input()

4
frontend/app/features/settings/pages/patterns/pattern.component.ts

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
EditPatternForm, EditPatternForm,
fadeAnimation,
PatternDto, PatternDto,
PatternsState PatternsState
} from '@app/shared'; } from '@app/shared';
@ -19,9 +18,6 @@ import {
selector: 'sqx-pattern', selector: 'sqx-pattern',
styleUrls: ['./pattern.component.scss'], styleUrls: ['./pattern.component.scss'],
templateUrl: './pattern.component.html', templateUrl: './pattern.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PatternComponent implements OnChanges { export class PatternComponent implements OnChanges {

9
frontend/app/features/settings/pages/plans/plan.component.ts

@ -7,19 +7,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { import { PlanInfo, PlansState } from '@app/shared';
fadeAnimation,
PlanInfo,
PlansState
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-plan', selector: 'sqx-plan',
styleUrls: ['./plan.component.scss'], styleUrls: ['./plan.component.scss'],
templateUrl: './plan.component.html', templateUrl: './plan.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PlanComponent { export class PlanComponent {

4
frontend/app/features/settings/pages/roles/role.component.ts

@ -13,7 +13,6 @@ import {
AutocompleteComponent, AutocompleteComponent,
AutocompleteSource, AutocompleteSource,
EditPermissionsForm, EditPermissionsForm,
fadeAnimation,
RoleDto, RoleDto,
RolesState RolesState
} from '@app/shared'; } from '@app/shared';
@ -29,9 +28,6 @@ const Descriptions = {
selector: 'sqx-role', selector: 'sqx-role',
styleUrls: ['./role.component.scss'], styleUrls: ['./role.component.scss'],
templateUrl: './role.component.html', templateUrl: './role.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RoleComponent implements OnChanges { export class RoleComponent implements OnChanges {

2
frontend/app/framework/angular/avatar.component.ts

@ -15,7 +15,7 @@ import { picasso } from '@app/framework/internal';
<img <img
[style.width]="sizeInPx" [style.width]="sizeInPx"
[style.height]="sizeInPx" [style.height]="sizeInPx"
[attr.src]="imageSource | sqxSafeUrl" [src]="imageSource | sqxSafeUrl"
/> />
` `
}) })

4
frontend/app/framework/angular/external-link.directive.ts

@ -21,8 +21,8 @@ export class ExternalLinkDirective implements AfterViewInit {
} }
public ngAfterViewInit() { public ngAfterViewInit() {
this.renderer.setAttribute(this.element.nativeElement, 'target', '_blank'); this.renderer.setProperty(this.element.nativeElement, 'target', '_blank');
this.renderer.setAttribute(this.element.nativeElement, 'rel', 'noopener'); this.renderer.setProperty(this.element.nativeElement, 'rel', 'noopener');
if (this.type !== 'noicon') { if (this.type !== 'noicon') {
const icon = this.renderer.createElement('i'); const icon = this.renderer.createElement('i');

11
frontend/app/framework/angular/forms/autocomplete.component.html

@ -1,13 +1,12 @@
<span> <span>
<input type="text" class="form-control" (blur)="blur()" (keydown)="onKeyDown($event)" <input type="text" class="form-control" (blur)="blur()" (keydown)="onKeyDown($event)" #input
[sqxFocusOnInit]="autoFocus" [sqxFocusOnInit]="autoFocus"
[attr.name]="inputName" [name]="inputName" [placeholder]="placeholder"
[attr.placeholder]="placeholder" #input
[class.form-underlined]="underlined"
[formControl]="queryInput"
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
autocapitalize="off"> autocapitalize="off"
[class.form-underlined]="underlined"
[formControl]="queryInput">
<ng-container *sqxModal="snapshot.suggestedItems.length > 0" position="bottom-left"> <ng-container *sqxModal="snapshot.suggestedItems.length > 0" position="bottom-left">
<div class="control-dropdown" [sqxAnchoredTo]="input" position="bottom-left" #container @fade> <div class="control-dropdown" [sqxAnchoredTo]="input" position="bottom-left" #container @fade>

2
frontend/app/framework/angular/forms/focus-on-init.directive.ts

@ -25,7 +25,7 @@ export class FocusOnInitDirective implements AfterViewInit {
} }
public ngAfterViewInit() { public ngAfterViewInit() {
if (!this.enabled) { if (this.enabled === false) {
return; return;
} }

11
frontend/app/framework/angular/forms/tag-editor.component.html

@ -9,18 +9,17 @@
<input type="text" class="blank" #input <input type="text" class="blank" #input
(blur)="markTouched()" (blur)="markTouched()"
(cut)="onCut($event)"
(copy)="onCopy($event)" (copy)="onCopy($event)"
(paste)="onPaste($event)" (cut)="onCut($event)"
(focus)="focus()" (focus)="focus()"
(keydown)="onKeyDown($event)" (keydown)="onKeyDown($event)"
[formControl]="addInput" (paste)="onPaste($event)"
[attr.name]="inputName" [name]="inputName" [placeholder]="placeholder"
[attr.placeholder]="placeholder"
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"
spellcheck="false"> spellcheck="false"
[formControl]="addInput">
</div> </div>
<ng-container *sqxModal="snapshot.suggestedItems.length > 0"> <ng-container *sqxModal="snapshot.suggestedItems.length > 0">

25
frontend/app/framework/angular/image-source.directive.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { MathHelper, ResourceOwner } from '@app/framework/internal'; import { MathHelper, ResourceOwner } from '@app/framework/internal';
@ -33,6 +33,7 @@ export class ImageSourceDirective extends ResourceOwner implements OnChanges, On
public parent: any = null; public parent: any = null;
constructor( constructor(
private readonly zone: NgZone,
private readonly element: ElementRef, private readonly element: ElementRef,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
@ -50,10 +51,22 @@ export class ImageSourceDirective extends ResourceOwner implements OnChanges, On
this.parent = this.renderer.parentNode(this.element.nativeElement); this.parent = this.renderer.parentNode(this.element.nativeElement);
} }
this.own( this.zone.runOutsideAngular(() => {
this.renderer.listen(this.parent, 'resize', () => { this.own(
this.resize(); this.renderer.listen(this.parent, 'resize', () => {
})); this.resize();
}));
this.own(
this.renderer.listen(this.element.nativeElement, 'load', () => {
this.onLoad();
}));
this.own(
this.renderer.listen(this.element.nativeElement, 'error', () => {
this.onError();
}));
});
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -67,12 +80,10 @@ export class ImageSourceDirective extends ResourceOwner implements OnChanges, On
this.setImageSource(); this.setImageSource();
} }
@HostListener('load')
public onLoad() { public onLoad() {
this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible'); this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible');
} }
@HostListener('error')
public onError() { public onError() {
this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden'); this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden');

4
frontend/app/framework/angular/modals/modal-dialog.component.html

@ -11,7 +11,7 @@
</button> </button>
</div> </div>
<div class="modal-tabs {{tabsClass}} clearfix" #tabsElement [hidden]="!showTabs || !snapshot.hasTabs"> <div class="modal-tabs {{tabsClass}} clearfix" #tabsElement *ngIf="showTabs">
<ng-content select="[tabs]"></ng-content> <ng-content select="[tabs]"></ng-content>
</div> </div>
@ -19,7 +19,7 @@
<ng-content select="[content]"></ng-content> <ng-content select="[content]"></ng-content>
</div> </div>
<div class="modal-footer" [hidden]="!showFooter || !snapshot.hasFooter"> <div class="modal-footer" *ngIf="showFooter">
<div class="clearfix" #footerElement> <div class="clearfix" #footerElement>
<ng-content select="[footer]"></ng-content> <ng-content select="[footer]"></ng-content>
</div> </div>

31
frontend/app/framework/angular/modals/modal-dialog.component.ts

@ -5,14 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2, ViewChild } from '@angular/core';
import { fadeAnimation, StatefulComponent } from '@app/framework/internal'; import { fadeAnimation } from '@app/framework/internal';
interface State {
hasTabs: boolean;
hasFooter: boolean;
}
@Component({ @Component({
selector: 'sqx-modal-dialog', selector: 'sqx-modal-dialog',
@ -23,7 +18,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.Default changeDetection: ChangeDetectionStrategy.Default
}) })
export class ModalDialogComponent extends StatefulComponent<State> implements AfterViewInit { export class ModalDialogComponent implements AfterViewInit {
@Output() @Output()
public close = new EventEmitter(); public close = new EventEmitter();
@ -57,18 +52,22 @@ export class ModalDialogComponent extends StatefulComponent<State> implements Af
@ViewChild('footerElement', { static: false }) @ViewChild('footerElement', { static: false })
public footerElement: ElementRef<ParentNode>; public footerElement: ElementRef<ParentNode>;
constructor(changeDetector: ChangeDetectorRef) { constructor(
super(changeDetector, { private readonly renderer: Renderer2
hasTabs: false, ) {
hasFooter: false
});
} }
public ngAfterViewInit() { public ngAfterViewInit() {
const hasTabs = this.tabsElement.nativeElement.children.length > 0; this.hideWhenEmpty(this.tabsElement.nativeElement);
const hasFooter = this.footerElement.nativeElement.children.length > 0; this.hideWhenEmpty(this.footerElement.nativeElement);
}
private hideWhenEmpty(element: any) {
const isEmpty = element.children.length === 0;
this.next({ hasTabs, hasFooter }); if (isEmpty) {
this.renderer.setStyle(element, 'display', 'none');
}
} }
public emitClose() { public emitClose() {

2
frontend/app/framework/angular/modals/tooltip.directive.ts

@ -50,7 +50,7 @@ export class TooltipDirective {
private unsetAttribute() { private unsetAttribute() {
try { try {
this.renderer.setAttribute(this.element.nativeElement, 'title', ''); this.renderer.setProperty(this.element.nativeElement, 'title', '');
} catch (ex) { } catch (ex) {
return; return;
} }

11
frontend/app/framework/angular/pager.component.html

@ -2,11 +2,18 @@
<div class="float-right pagination"> <div class="float-right pagination">
<span *ngIf="pager.numberOfItems > 0" class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span> <span *ngIf="pager.numberOfItems > 0" class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="emitPrev()"> <button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="emitNext()"> <button type="button" class="btn btn-text-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</button> </button>
<select class="form-control form-control-sm" [ngModel]="pager.pageSize" (ngModelChange)="setPageSize($event)">
<option [ngValue]="10">10</option>
<option [ngValue]="20">20</option>
<option [ngValue]="30">30</option>
<option [ngValue]="50">50</option>
</select>
</div> </div>
</div> </div>

9
frontend/app/framework/angular/pager.component.scss

@ -1,2 +1,9 @@
@import '_mixins'; @import '_mixins';
@import '_vars'; @import '_vars';
.form-control {
display: inline-block;
margin-left: 2rem;
margin-top: .25rem;
width: auto;
}

17
frontend/app/framework/angular/pager.component.ts

@ -17,10 +17,7 @@ import { Pager } from '@app/framework/internal';
}) })
export class PagerComponent { export class PagerComponent {
@Output() @Output()
public nextPage = new EventEmitter(); public pagerChange = new EventEmitter<Pager>();
@Output()
public prevPage = new EventEmitter();
@Input() @Input()
public pager: Pager; public pager: Pager;
@ -28,11 +25,15 @@ export class PagerComponent {
@Input() @Input()
public autoHide = false; public autoHide = false;
public emitNext() { public goPrev() {
this.nextPage.emit(); this.pagerChange.emit(this.pager.goPrev());
}
public goNext() {
this.pagerChange.emit(this.pager.goNext());
} }
public emitPrev() { public setPageSize(pageSize: number) {
this.prevPage.emit(); this.pagerChange.emit(this.pager.setPageSize(pageSize));
} }
} }

16
frontend/app/framework/utils/pager.spec.ts

@ -195,4 +195,20 @@ describe('Pager', () => {
canGoPrev: true canGoPrev: true
}); });
}); });
it('should update page size', () => {
const pager_1 = new Pager(21, 0, 10);
const pager_2 = pager_1.setPageSize(30);
expect(Object.assign({}, pager_2)).toEqual(<any>{
page: 0,
pageSize: 30,
itemFirst: 1,
itemLast: 21,
skip: 0,
numberOfItems: 21,
canGoNext: false,
canGoPrev: false
});
});
}); });

6
frontend/app/framework/utils/pager.ts

@ -6,6 +6,8 @@
*/ */
export class Pager { export class Pager {
public static readonly DEFAULT = new Pager(0);
public canGoNext = false; public canGoNext = false;
public canGoPrev = false; public canGoPrev = false;
@ -54,6 +56,10 @@ export class Pager {
return new Pager(0, 0, this.pageSize); return new Pager(0, 0, this.pageSize);
} }
public setPageSize(pageSize: number): Pager {
return new Pager(this.numberOfItems, this.page, pageSize);
}
public setCount(numberOfItems: number): Pager { public setCount(numberOfItems: number): Pager {
return new Pager(numberOfItems, this.page, this.pageSize); return new Pager(numberOfItems, this.page, this.pageSize);
} }

124
frontend/app/shared/components/asset.component.html

@ -4,17 +4,21 @@
[sqxDropDisabled]="!asset || !asset.canUpload" [sqxDropDisabled]="!asset || !asset.canUpload"
[noDrop]="true"> [noDrop]="true">
<div class="card-body"> <div class="card-body">
<div class="file-preview" *ngIf="asset && snapshot.progress === 0" @fade> <div class="file-preview" *ngIf="asset && progress === 0">
<span class="file-type" *ngIf="asset.fileType"> <span class="file-type" *ngIf="asset.fileType">
{{asset.fileType}} {{asset.fileType}}
</span> </span>
<div *ngIf="asset.canPreview" class="file-image"> <ng-container *ngIf="asset.canPreview; else noPreview">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large"> <div class="file-image">
</div> <img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large">
<div *ngIf="!asset.canPreview" class="file-icon"> </div>
<img [attr.src]="asset | sqxFileIcon"> </ng-container>
</div> <ng-template #noPreview>
<div class="file-icon">
<img [src]="asset | sqxFileIcon">
</div>
</ng-template>
<div class="overlay"> <div class="overlay">
<div class="overlay-background"></div> <div class="overlay-background"></div>
@ -50,15 +54,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="drop-overlay align-items-center justify-content-center">
<div class="drop-overlay-background"></div>
<div class="drop-overlay-text">Drop to update</div>
</div>
</div> </div>
<div class="upload-progress" *ngIf="snapshot.progress > 0"> <div class="upload-progress" *ngIf="progress > 0">
<sqx-progress-bar mode="Circle" [value]="snapshot.progress"></sqx-progress-bar> <sqx-progress-bar mode="Circle" [value]="progress"></sqx-progress-bar>
</div>
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && snapshot.progress === 0">
<div class="drop-overlay-background"></div>
<div class="drop-overlay-text">Drop to update</div>
</div> </div>
</div> </div>
<div class="card-footer" (dblclick)="edit()"> <div class="card-footer" (dblclick)="edit()">
@ -86,53 +90,63 @@
[noDrop]="true"> [noDrop]="true">
<div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div> <div class="left-border" [class.hidden]="!isSelectable" [class.selected]="isSelected" ></div>
<div *ngIf="asset && asset.canPreview && snapshot.progress === 0" class="image drag-handle" [class.image-left]="!isSelectable" @fade> <ng-container *ngIf="asset && progress === 0">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small"> <ng-container *ngIf="asset.canPreview; else noPreview">
</div> <div class="image drag-handle" [class.image-left]="!isSelectable">
<div *ngIf="asset && !asset.canPreview && snapshot.progress === 0" class="image drag-handle image-padded" [class.image-left]="!isSelectable" @fade> <img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small">
<img class="icon" [attr.src]="asset | sqxFileIcon"> </div>
</div> </ng-container>
<ng-template #noPreview>
<div class="image drag-handle image-padded" [class.image-left]="!isSelectable">
<img class="icon" [src]="asset | sqxFileIcon">
</div>
</ng-template>
<table class="table-fixed">
<tr>
<td class="col-name">
<div class="file-name editable" (click)="edit()">
{{asset.fileName}}
</div>
</td>
<table class="table-fixed" *ngIf="asset && snapshot.progress === 0" @fade> <ng-container *ngIf="!isCompact">
<tr> <td class="col-info">
<td class="col-name"> <ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
<div class="file-name editable" (click)="edit()"> </td>
{{asset.fileName}} <td class="col-user">
</div> <img class="user-picture" title="{{asset.lastModifiedBy | sqxUserNameRef}}" [src]="asset.lastModifiedBy | sqxUserPictureRef" />
</td> </td>
<td class="col-info" *ngIf="!isCompact"> <td class="col-actions text-right">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}} <a class="btn btn-text-secondary" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon">
</td> <i class="icon-download"></i>
<td class="col-user" *ngIf="!isCompact"> </a>
<img class="user-picture" title="{{asset.lastModifiedBy | sqxUserNameRef}}" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" /> </td>
</td> </ng-container>
<td class="col-actions text-right" *ngIf="!isCompact">
<a class="btn btn-text-secondary" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon">
<i class="icon-download"></i>
</a>
</td>
<td class="col-actions text-right" *ngIf="!isDisabled || removeMode">
<button type="button" class="btn btn-text-danger" *ngIf="!isDisabled && !removeMode && asset.canDelete"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete asset"
confirmText="Do you really want to delete the asset?">
<i class="icon-bin2"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode">
<i class="icon-close"></i>
</button>
</td>
</tr>
</table>
<div class="upload-progress" *ngIf="snapshot.progress > 0"> <td class="col-actions text-right" *ngIf="!isDisabled || removeMode">
<sqx-progress-bar [value]="snapshot.progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar> <button type="button" class="btn btn-text-danger" *ngIf="!isDisabled && !removeMode && asset.canDelete"
</div> (sqxConfirmClick)="emitDelete()"
confirmTitle="Delete asset"
confirmText="Do you really want to delete the asset?">
<i class="icon-bin2"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode">
<i class="icon-close"></i>
</button>
</td>
</tr>
</table>
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && snapshot.progress === 0"> <div class="drop-overlay align-items-center justify-content-center">
<div class="drop-overlay-background"></div> <div class="drop-overlay-background"></div>
<div class="drop-overlay-text">Drop to update</div> <div class="drop-overlay-text">Drop to update</div>
</div> </div>
</ng-container>
<div class="upload-progress" *ngIf="progress > 0">
<sqx-progress-bar [value]="progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
</div>
</div> </div>
</ng-template> </ng-template>

25
frontend/app/shared/components/asset.component.ts

@ -12,26 +12,17 @@ import {
AssetUploaderState, AssetUploaderState,
DialogModel, DialogModel,
DialogService, DialogService,
fadeAnimation,
StatefulComponent,
Types, Types,
UploadCanceled UploadCanceled
} from '@app/shared/internal'; } from '@app/shared/internal';
interface State {
progress: number;
}
@Component({ @Component({
selector: 'sqx-asset', selector: 'sqx-asset',
styleUrls: ['./asset.component.scss'], styleUrls: ['./asset.component.scss'],
templateUrl: './asset.component.html', templateUrl: './asset.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetComponent extends StatefulComponent<State> implements OnInit { export class AssetComponent implements OnInit {
@Output() @Output()
public load = new EventEmitter<AssetDto>(); public load = new EventEmitter<AssetDto>();
@ -77,15 +68,15 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Input() @Input()
public allTags: ReadonlyArray<string>; public allTags: ReadonlyArray<string>;
public progress = 0;
public editDialog = new DialogModel(); public editDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(
private readonly assetUploader: AssetUploaderState, private readonly assetUploader: AssetUploaderState,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super(changeDetector, {
progress: 0
});
} }
public ngOnInit() { public ngOnInit() {
@ -167,7 +158,9 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
} }
private setProgress(progress: number) { private setProgress(progress: number) {
this.next(s => ({ ...s, progress })); this.progress = progress;
this.changeDetector.markForCheck();
} }
public updateAsset(asset: AssetDto, emitEvent: boolean) { public updateAsset(asset: AssetDto, emitEvent: boolean) {
@ -177,7 +170,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
this.emitUpdate(); this.emitUpdate();
} }
this.next(s => ({ ...s, progress: 0 })); this.setProgress(0);
this.cancelEdit(); this.cancelEdit();
} }

2
frontend/app/shared/components/assets-list.component.html

@ -35,4 +35,4 @@
</ng-container> </ng-container>
</div> </div>
<sqx-pager [autoHide]="true" [pager]="state.assetsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager> <sqx-pager [autoHide]="true" [pager]="state.assetsPager | async" (pagerChange)="state.setPager($event)"></sqx-pager>

15
frontend/app/shared/components/assets-list.component.ts

@ -6,7 +6,6 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AssetDto, AssetDto,
@ -48,7 +47,7 @@ export class AssetsListComponent {
setTimeout(() => { setTimeout(() => {
this.newFiles = this.newFiles.removed(file); this.newFiles = this.newFiles.removed(file);
this.changeDetector.detectChanges(); this.changeDetector.markForCheck();
}, 2000); }, 2000);
} else { } else {
this.newFiles = this.newFiles.removed(file); this.newFiles = this.newFiles.removed(file);
@ -58,19 +57,11 @@ export class AssetsListComponent {
} }
public search() { public search() {
this.state.load().pipe(onErrorResumeNext()).subscribe(); this.state.load();
} }
public delete(asset: AssetDto) { public delete(asset: AssetDto) {
this.state.delete(asset).pipe(onErrorResumeNext()).subscribe(); this.state.delete(asset);
}
public goNext() {
this.state.goNext().pipe(onErrorResumeNext()).subscribe();
}
public goPrev() {
this.state.goPrev().pipe(onErrorResumeNext()).subscribe();
} }
public update(asset: AssetDto) { public update(asset: AssetDto) {

4
frontend/app/shared/components/assets-selector.component.ts

@ -10,7 +10,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, On
import { import {
AssetDto, AssetDto,
AssetsState, AssetsState,
fadeAnimation,
LocalStoreService, LocalStoreService,
Query, Query,
StatefulComponent StatefulComponent
@ -27,9 +26,6 @@ interface State {
selector: 'sqx-assets-selector', selector: 'sqx-assets-selector',
styleUrls: ['./assets-selector.component.scss'], styleUrls: ['./assets-selector.component.scss'],
templateUrl: './assets-selector.component.html', templateUrl: './assets-selector.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit { export class AssetsSelectorComponent extends StatefulComponent<State> implements OnInit {

2
frontend/app/shared/components/comment.component.html

@ -1,6 +1,6 @@
<div class="comment row no-gutters"> <div class="comment row no-gutters">
<div class="col-auto"> <div class="col-auto">
<img class="user-picture" title="{{comment.user | sqxUserNameRef}}" [attr.src]="comment.user | sqxUserPictureRef" /> <img class="user-picture" title="{{comment.user | sqxUserNameRef}}" [src]="comment.user | sqxUserPictureRef" />
</div> </div>
<div class="col pl-2"> <div class="col pl-2">
<div class="comment-message"> <div class="comment-message">

2
frontend/app/shared/components/history-list.component.html

@ -1,7 +1,7 @@
<ng-container *ngIf="events"> <ng-container *ngIf="events">
<div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters"> <div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto"> <div class="col-auto">
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [attr.src]="event.actor | sqxUserPictureRef" /> <img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [src]="event.actor | sqxUserPictureRef" />
</div> </div>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <div class="event-message">

11
frontend/app/shared/components/pipes.ts

@ -69,6 +69,7 @@ class UserAsyncPipe implements OnDestroy {
private lastUserId: string; private lastUserId: string;
private lastValue: string | undefined = undefined; private lastValue: string | undefined = undefined;
private subscription: Subscription; private subscription: Subscription;
private current: Observable<string | null>;
constructor(loading: string, constructor(loading: string,
private readonly users: UsersProviderService, private readonly users: UsersProviderService,
@ -91,11 +92,17 @@ class UserAsyncPipe implements OnDestroy {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
this.subscription = transform(this.users).subscribe(value => { const pipe = transform(this.users);
this.subscription = pipe.subscribe(value => {
this.lastValue = value || undefined; this.lastValue = value || undefined;
this.changeDetector.markForCheck(); if (this.current === pipe) {
this.changeDetector.markForCheck();
}
}); });
this.current = pipe;
} }
return this.lastValue; return this.lastValue;

4
frontend/app/shared/components/queries/filter-comparison.component.ts

@ -8,7 +8,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { import {
fadeAnimation,
FilterComparison, FilterComparison,
LanguageDto, LanguageDto,
QueryFieldModel, QueryFieldModel,
@ -20,9 +19,6 @@ import {
selector: 'sqx-filter-comparison', selector: 'sqx-filter-comparison',
styleUrls: ['./filter-comparison.component.scss'], styleUrls: ['./filter-comparison.component.scss'],
templateUrl: './filter-comparison.component.html', templateUrl: './filter-comparison.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FilterComparisonComponent implements OnChanges { export class FilterComparisonComponent implements OnChanges {

4
frontend/app/shared/components/queries/filter-logical.component.ts

@ -10,7 +10,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { import {
fadeAnimation,
FilterLogical, FilterLogical,
FilterNode, FilterNode,
LanguageDto, LanguageDto,
@ -21,9 +20,6 @@ import {
selector: 'sqx-filter-logical', selector: 'sqx-filter-logical',
styleUrls: ['./filter-logical.component.scss'], styleUrls: ['./filter-logical.component.scss'],
templateUrl: './filter-logical.component.html', templateUrl: './filter-logical.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FilterLogicalComponent { export class FilterLogicalComponent {

4
frontend/app/shared/components/search-form.component.ts

@ -11,7 +11,6 @@ import { Observable } from 'rxjs';
import { import {
DialogModel, DialogModel,
fadeAnimation,
hasFilter, hasFilter,
LanguageDto, LanguageDto,
Queries, Queries,
@ -24,9 +23,6 @@ import {
selector: 'sqx-search-form', selector: 'sqx-search-form',
styleUrls: ['./search-form.component.scss'], styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html', templateUrl: './search-form.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SearchFormComponent implements OnChanges { export class SearchFormComponent implements OnChanges {

8
frontend/app/shared/state/assets.state.spec.ts

@ -13,6 +13,7 @@ import {
AssetsService, AssetsService,
AssetsState, AssetsState,
DialogService, DialogService,
Pager,
versioned versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -117,16 +118,15 @@ describe('AssetsState', () => {
expect(assetsState.isTagSelectionEmpty()).toBeTruthy(); expect(assetsState.isTagSelectionEmpty()).toBeTruthy();
}); });
it('should load next page and prev page when paging', () => { it('should load with new pagination when paging', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]))) assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([])))
.returns(() => of(new AssetsDto(200, []))).verifiable(Times.exactly(2)); .returns(() => of(new AssetsDto(200, []))).verifiable();
assetsService.setup(x => x.getAssets(app, 30, 30, undefined, It.isValue([]))) assetsService.setup(x => x.getAssets(app, 30, 30, undefined, It.isValue([])))
.returns(() => of(new AssetsDto(200, []))).verifiable(); .returns(() => of(new AssetsDto(200, []))).verifiable();
assetsState.load().subscribe(); assetsState.load().subscribe();
assetsState.goNext().subscribe(); assetsState.setPager(new Pager(60, 1, 30)).subscribe();
assetsState.goPrev().subscribe();
expect().nothing(); expect().nothing();
}); });

22
frontend/app/shared/state/assets.state.ts

@ -181,7 +181,9 @@ export class AssetsState extends State<Snapshot> {
tagsSelected[tag] = true; tagsSelected[tag] = true;
} }
return { ...s, assetsPager: new Pager(0, 0, 30), tagsSelected }; const assetsPager = s.assetsPager.reset();
return { ...s, assetsPager, tagsSelected };
}); });
return this.loadInternal(); return this.loadInternal();
@ -195,32 +197,28 @@ export class AssetsState extends State<Snapshot> {
tagsSelected[tag] = true; tagsSelected[tag] = true;
} }
return { ...s, assetsPager: new Pager(0, 0, 30), tagsSelected }; const assetsPager = s.assetsPager.reset();
return { ...s, assetsPager: assetsPager, tagsSelected };
}); });
return this.loadInternal(); return this.loadInternal();
} }
public resetTags(): Observable<any> { public resetTags(): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), tagsSelected: {} })); this.next(s => ({ ...s, assetsPager: s.assetsPager.reset(), tagsSelected: {} }));
return this.loadInternal(); return this.loadInternal();
} }
public search(query?: Query): Observable<any> { public search(query?: Query): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), assetsQuery: query, assetsQueryJson: encodeQuery(query) })); this.next(s => ({ ...s, assetsPager: s.assetsPager.reset(), assetsQuery: query, assetsQueryJson: encodeQuery(query) }));
return this.loadInternal();
}
public goNext(): Observable<any> {
this.next(s => ({ ...s, assetsPager: s.assetsPager.goNext() }));
return this.loadInternal(); return this.loadInternal();
} }
public goPrev(): Observable<any> { public setPager(assetsPager: Pager) {
this.next(s => ({ ...s, assetsPager: s.assetsPager.goPrev() })); this.next(s => ({ ...s, assetsPager }));
return this.loadInternal(); return this.loadInternal();
} }

14
frontend/app/shared/state/contents.state.ts

@ -97,7 +97,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ contents: [], contentsPager: new Pager(0), contentsQueryJson: '' }); super({ contents: [], contentsPager: Pager.DEFAULT, contentsQueryJson: '' });
} }
public select(id: string | null): Observable<ContentDto | null> { public select(id: string | null): Observable<ContentDto | null> {
@ -309,19 +309,13 @@ export abstract class ContentsStateBase extends State<Snapshot> {
} }
public search(contentsQuery?: Query): Observable<any> { public search(contentsQuery?: Query): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery, contentsQueryJson: encodeQuery(contentsQuery) })); this.next(s => ({ ...s, contentsPager: s.contentsPager.reset(), contentsQuery, contentsQueryJson: encodeQuery(contentsQuery) }));
return this.loadInternal(); return this.loadInternal();
} }
public goNext(): Observable<any> { public setPager(contentsPager: Pager) {
this.next(s => ({ ...s, contentsPager: s.contentsPager.goNext() })); this.next(s => ({ ...s, contentsPager }));
return this.loadInternal();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, contentsPager: s.contentsPager.goPrev() }));
return this.loadInternal(); return this.loadInternal();
} }

29
frontend/app/shared/state/contributors.state.spec.ts

@ -14,6 +14,7 @@ import {
ContributorsService, ContributorsService,
ContributorsState, ContributorsState,
DialogService, DialogService,
Pager,
versioned versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -60,6 +61,7 @@ describe('ContributorsState', () => {
contributorsState.load().subscribe(); contributorsState.load().subscribe();
expect(contributorsState.snapshot.contributors).toEqual(oldContributors.items); expect(contributorsState.snapshot.contributors).toEqual(oldContributors.items);
expect(contributorsState.snapshot.contributorsPager).toEqual(new Pager(20, 0, 10));
expect(contributorsState.snapshot.isLoaded).toBeTruthy(); expect(contributorsState.snapshot.isLoaded).toBeTruthy();
expect(contributorsState.snapshot.maxContributors).toBe(oldContributors.maxContributors); expect(contributorsState.snapshot.maxContributors).toBe(oldContributors.maxContributors);
expect(contributorsState.snapshot.version).toEqual(version); expect(contributorsState.snapshot.version).toEqual(version);
@ -67,7 +69,7 @@ describe('ContributorsState', () => {
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
}); });
it('should only current page of contributors', () => { it('should only show current page of contributors', () => {
contributorsState.load().subscribe(); contributorsState.load().subscribe();
let contributors: ReadonlyArray<ContributorDto>; let contributors: ReadonlyArray<ContributorDto>;
@ -77,12 +79,12 @@ describe('ContributorsState', () => {
}); });
expect(contributors!).toEqual(oldContributors.items.slice(0, 10)); expect(contributors!).toEqual(oldContributors.items.slice(0, 10));
expect(contributorsState.snapshot.page).toEqual(0); expect(contributorsState.snapshot.contributorsPager).toEqual(new Pager(20, 0, 10));
}); });
it('should show next of contributors when going next', () => { it('should show with new pagination when paging', () => {
contributorsState.load().subscribe(); contributorsState.load().subscribe();
contributorsState.goNext(); contributorsState.setPager(new Pager(20, 1, 10));
let contributors: ReadonlyArray<ContributorDto>; let contributors: ReadonlyArray<ContributorDto>;
@ -91,22 +93,7 @@ describe('ContributorsState', () => {
}); });
expect(contributors!).toEqual(oldContributors.items.slice(10, 20)); expect(contributors!).toEqual(oldContributors.items.slice(10, 20));
expect(contributorsState.snapshot.page).toEqual(1); expect(contributorsState.snapshot.contributorsPager).toEqual(new Pager(20, 1, 10));
});
it('should show next of contributors when going prev', () => {
contributorsState.load().subscribe();
contributorsState.goNext();
contributorsState.goPrev();
let contributors: ReadonlyArray<ContributorDto>;
contributorsState.contributorsPaged.subscribe(result => {
contributors = result;
});
expect(contributors!).toEqual(oldContributors.items.slice(0, 10));
expect(contributorsState.snapshot.page).toEqual(0);
}); });
it('should show filtered contributors when searching', () => { it('should show filtered contributors when searching', () => {
@ -120,7 +107,7 @@ describe('ContributorsState', () => {
}); });
expect(contributors!).toEqual(createContributors(4, 14).items); expect(contributors!).toEqual(createContributors(4, 14).items);
expect(contributorsState.snapshot.page).toEqual(0); expect(contributorsState.snapshot.contributorsPager.page).toEqual(0);
}); });
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {

39
frontend/app/shared/state/contributors.state.ts

@ -33,15 +33,15 @@ interface Snapshot {
// All loaded contributors. // All loaded contributors.
contributors: ContributorsList; contributors: ContributorsList;
// The pagination information.
contributorsPager: Pager;
// Indicates if the contributors are loaded. // Indicates if the contributors are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// The maximum allowed users. // The maximum allowed users.
maxContributors: number; maxContributors: number;
// The current page.
page: number;
// The search query. // The search query.
query?: string; query?: string;
@ -62,9 +62,6 @@ export class ContributorsState extends State<Snapshot> {
public contributors = public contributors =
this.project(x => x.contributors); this.project(x => x.contributors);
public page =
this.project(x => x.page);
public query = public query =
this.project(x => x.query); this.project(x => x.query);
@ -83,18 +80,18 @@ export class ContributorsState extends State<Snapshot> {
public filtered = public filtered =
this.projectFrom2(this.queryRegex, this.contributors, (q, c) => getFilteredContributors(c, q)); this.projectFrom2(this.queryRegex, this.contributors, (q, c) => getFilteredContributors(c, q));
public contributorsPaged =
this.projectFrom2(this.page, this.filtered, (p, c) => getPagedContributors(c, p));
public contributorsPager = public contributorsPager =
this.projectFrom2(this.page, this.filtered, (p, c) => new Pager(c.length, p, PAGE_SIZE)); this.project(x => x.contributorsPager);
public contributorsPaged =
this.projectFrom2(this.contributorsPager, this.filtered, (p, c) => getPagedContributors(c, p));
constructor( constructor(
private readonly contributorsService: ContributorsService, private readonly contributorsService: ContributorsService,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ contributors: [], page: 0, maxContributors: -1, version: Version.EMPTY }); super({ contributors: [], contributorsPager: Pager.DEFAULT, maxContributors: -1, version: Version.EMPTY });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -113,12 +110,8 @@ export class ContributorsState extends State<Snapshot> {
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public goNext() { public setPager(contributorsPager: Pager) {
this.next(s => ({ ...s, page: s.page + 1 })); this.next(s => ({ ...s, contributorsPager }));
}
public goPrev() {
this.next(s => ({ ...s, page: s.page - 1 }));
} }
public search(query: string) { public search(query: string) {
@ -149,15 +142,17 @@ export class ContributorsState extends State<Snapshot> {
} }
private replaceContributors(version: Version, payload: ContributorsPayload) { private replaceContributors(version: Version, payload: ContributorsPayload) {
this.next(() => { this.next(s => {
const { canCreate, items: contributors, maxContributors } = payload; const { canCreate, items: contributors, maxContributors } = payload;
const contributorsPager = s.contributorsPager.setCount(contributors.length);
return { return {
canCreate, canCreate,
contributors, contributors,
contributorsPager,
isLoaded: true, isLoaded: true,
maxContributors, maxContributors,
page: 0,
version version
}; };
}); });
@ -172,10 +167,8 @@ export class ContributorsState extends State<Snapshot> {
} }
} }
const PAGE_SIZE = 10; function getPagedContributors(contributors: ContributorsList, pager: Pager) {
return contributors.slice(pager.page * pager.pageSize, (pager.page + 1) * pager.pageSize);
function getPagedContributors(contributors: ContributorsList, page: number) {
return contributors.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
} }
function getFilteredContributors(contributors: ContributorsList, query?: RegExp) { function getFilteredContributors(contributors: ContributorsList, query?: RegExp) {

10
frontend/app/shared/state/rule-events.state.spec.ts

@ -10,6 +10,7 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
Pager,
RuleEventsDto, RuleEventsDto,
RuleEventsState, RuleEventsState,
RulesService RulesService
@ -62,17 +63,16 @@ describe('RuleEventsState', () => {
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
}); });
it('should load next page and prev page when paging', () => { it('should load with new pagination when paging', () => {
rulesService.setup(x => x.getEvents(app, 10, 10, undefined)) rulesService.setup(x => x.getEvents(app, 10, 10, undefined))
.returns(() => of(new RuleEventsDto(200, []))); .returns(() => of(new RuleEventsDto(200, [])));
ruleEventsState.goNext().subscribe(); ruleEventsState.setPager(new Pager(20, 1, 10));
ruleEventsState.goPrev().subscribe();
expect().nothing(); expect().nothing();
rulesService.verify(x => x.getEvents(app, 10, 10, undefined), Times.once()); rulesService.verify(x => x.getEvents(app, 10, 10, undefined), Times.once());
rulesService.verify(x => x.getEvents(app, 10, 0, undefined), Times.exactly(2)); rulesService.verify(x => x.getEvents(app, 10, 0, undefined), Times.once());
}); });
it('should load with rule id when filtered', () => { it('should load with rule id when filtered', () => {
@ -83,7 +83,7 @@ describe('RuleEventsState', () => {
expect().nothing(); expect().nothing();
rulesService.verify(x => x.getEvents(app, 10, 0, '12'), Times.exactly(1)); rulesService.verify(x => x.getEvents(app, 10, 0, '12'), Times.once());
}); });
it('should call service when enqueuing event', () => { it('should call service when enqueuing event', () => {

14
frontend/app/shared/state/rule-events.state.ts

@ -50,7 +50,7 @@ export class RuleEventsState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly rulesService: RulesService private readonly rulesService: RulesService
) { ) {
super({ ruleEvents: [], ruleEventsPager: new Pager(0) }); super({ ruleEvents: [], ruleEventsPager: Pager.DEFAULT });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -105,19 +105,13 @@ export class RuleEventsState extends State<Snapshot> {
return empty(); return empty();
} }
this.next(s => ({ ...s, ruleEventsPager: new Pager(0), ruleId })); this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.reset(), ruleId }));
return this.loadInternal(); return this.loadInternal();
} }
public goNext(): Observable<any> { public setPager(ruleEventsPager: Pager): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goNext() })); this.next(s => ({ ...s, ruleEventsPager }));
return this.loadInternal();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goPrev() }));
return this.loadInternal(); return this.loadInternal();
} }

4
frontend/app/shared/state/schemas.state.spec.ts

@ -207,7 +207,7 @@ describe('SchemasState', () => {
it('should return schema on get and cache it', () => { it('should return schema on get and cache it', () => {
schemasService.setup(x => x.getSchema(app, schema1.name)) schemasService.setup(x => x.getSchema(app, schema1.name))
.returns(() => of(schema)).verifiable(Times.exactly(1)); .returns(() => of(schema)).verifiable(Times.once());
schemasState.loadSchema(schema1.name, true).subscribe(); schemasState.loadSchema(schema1.name, true).subscribe();
schemasState.loadSchema(schema1.name, true).subscribe(); schemasState.loadSchema(schema1.name, true).subscribe();
@ -217,7 +217,7 @@ describe('SchemasState', () => {
it('should return schema on get and reuse it from select when caching', () => { it('should return schema on get and reuse it from select when caching', () => {
schemasService.setup(x => x.getSchema(app, schema1.name)) schemasService.setup(x => x.getSchema(app, schema1.name))
.returns(() => of(schema)).verifiable(Times.exactly(1)); .returns(() => of(schema)).verifiable(Times.once());
schemasState.select(schema1.name).subscribe(); schemasState.select(schema1.name).subscribe();
schemasState.loadSchema(schema1.name, true).subscribe(); schemasState.loadSchema(schema1.name, true).subscribe();

6
frontend/app/shell/pages/internal/internal-area.component.ts

@ -10,7 +10,6 @@ import { ActivatedRoute } from '@angular/router';
import { import {
DialogService, DialogService,
fadeAnimation,
LoadingService, LoadingService,
Notification, Notification,
ResourceOwner ResourceOwner
@ -19,10 +18,7 @@ import {
@Component({ @Component({
selector: 'sqx-internal-area', selector: 'sqx-internal-area',
styleUrls: ['./internal-area.component.scss'], styleUrls: ['./internal-area.component.scss'],
templateUrl: './internal-area.component.html', templateUrl: './internal-area.component.html'
animations: [
fadeAnimation
]
}) })
export class InternalAreaComponent extends ResourceOwner implements OnInit { export class InternalAreaComponent extends ResourceOwner implements OnInit {
constructor( constructor(

2
frontend/app/shell/pages/internal/profile-menu.component.html

@ -7,7 +7,7 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()"> <span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<span class="user"> <span class="user">
<img class="user-picture" [attr.src]="snapshot.profileId | sqxUserIdPicture" /> <img class="user-picture" [src]="snapshot.profileId | sqxUserIdPicture" />
<span>{{snapshot.profileDisplayName}}</span> <span>{{snapshot.profileDisplayName}}</span>
</span> </span>

10
frontend/app/theme/_panels.scss

@ -403,10 +403,12 @@ a {
} }
.grid-footer { .grid-footer {
border-top: 2px solid $color-border; & {
} border-top: 2px solid $color-border;
}
.pagination { .pagination {
margin: .25rem 0; margin: .25rem 0;
}
} }
} }
Loading…
Cancel
Save