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.
</div>
<div class="modal-footer" [hidden]="!showFooter">
<div class="clearfix" #footerElement>
<div class="modal-footer">
<div class="clearfix">
<button type="button" class="float-right btn btn-primary">Save changes</button>
<button type="button" class="float-left btn btn-secondary">Close</button>
</div>

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

@ -16,7 +16,7 @@ import { UserDto, UsersState } from '@app/features/administration/internal';
template: `
<tr [routerLink]="user.id" routerLinkActive="active">
<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 class="cell-auto">
<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 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>
</ng-container>
</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);
}
public goPrev() {
this.usersState.goPrev();
}
public goNext() {
this.usersState.goNext();
}
public trackByUser(index: number, user: UserDto) {
return user.id;
}

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

@ -8,7 +8,7 @@
import { of, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService } from '@app/shared';
import { DialogService, Pager } from '@app/shared';
import {
UserDto,
@ -87,16 +87,15 @@ describe('UsersState', () => {
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))
.returns(() => of(oldUsers)).verifiable(Times.exactly(2));
.returns(() => of(oldUsers)).verifiable(Times.once());
usersService.setup(x => x.getUsers(10, 10, undefined))
.returns(() => of(new UsersDto(200, []))).verifiable();
usersState.load().subscribe();
usersState.goNext().subscribe();
usersState.goPrev().subscribe();
usersState.setPager(new Pager(20, 1, 10)).subscribe();
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 usersService: UsersService
) {
super({ users: [], usersPager: new Pager(0) });
super({ users: [], usersPager: Pager.DEFAULT });
}
public select(id: string | null): Observable<UserDto | null> {
@ -173,19 +173,13 @@ export class UsersState extends State<Snapshot> {
}
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();
}
public goNext(): Observable<UsersResult> {
this.next(s => ({ ...s, usersPager: s.usersPager.goNext() }));
return this.loadInternal();
}
public goPrev(): Observable<UsersResult> {
this.next(s => ({ ...s, usersPager: s.usersPager.goPrev() }));
public setPager(usersPager: Pager) {
this.next(s => ({ ...s, usersPager }));
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);
}
public goNext() {
this.assetsState.goNext();
}
public goPrev() {
this.assetsState.goPrev();
}
public changeView(isListView: boolean) {
this.isListView = isListView;

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

@ -6,7 +6,7 @@
<ng-container content>
<div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters">
<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 class="col pl-2">
<div class="event-message">

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

@ -101,7 +101,7 @@
</div>
<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>
</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();
}
public goPrev() {
this.contentsState.goPrev();
}
public goNext() {
this.contentsState.goNext();
}
public search(query: 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() {
this.isHidden = true;
this.changeDetector.detectChanges();
this.changeDetector.markForCheck();
}
public expand() {
this.isHidden = false;
this.changeDetector.detectChanges();
this.changeDetector.markForCheck();
}
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>
</ng-container>
<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 *ngSwitchCase="metaFields.createdByName">
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small>
@ -38,7 +38,7 @@ import {
<small class="truncate">{{content.lastModified | sqxFromNow}}</small>
</ng-container>
<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 *ngSwitchCase="metaFields.lastModifiedByName">
<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(() => {
this.patchForm.submitCompleted({ noReset: true});
this.changeDetector.detectChanges();
this.changeDetector.markForCheck();
}, 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 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>
</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.changeDetector.detectChanges();
}
public selectSchema(selected: string | SchemaDto) {
@ -98,7 +96,7 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.updateModel();
this.changeDetector.detectChanges();
this.changeDetector.markForCheck();
}
});
}
@ -111,14 +109,6 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
this.contentsState.search(query);
}
public goNext() {
this.contentsState.goNext();
}
public goPrev() {
this.contentsState.goPrev();
}
public isItemSelected(content: ContentDto) {
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 { Observable, Subject } from 'rxjs';
import { DialogModel, fadeAnimation } from '@app/shared';
import { DialogModel } from '@app/shared';
@Component({
selector: 'sqx-due-time-selector',
styleUrls: ['./due-time-selector.component.scss'],
templateUrl: './due-time-selector.component.html',
animations: [
fadeAnimation
]
templateUrl: './due-time-selector.component.html'
})
export class DueTimeSelectorComponent {
public dueTimeDialog = new DialogModel();

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

@ -90,7 +90,7 @@
</tbody>
</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>
</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);
}
public goNext() {
this.ruleEventsState.goNext();
}
public goPrev() {
this.ruleEventsState.goPrev();
}
public enqueue(event: RuleEventDto) {
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 {
DialogModel,
fadeAnimation,
FieldDto,
fieldTypes,
PatternsState,
@ -22,10 +21,7 @@ import {
@Component({
selector: 'sqx-schema-fields',
styleUrls: ['./schema-fields.component.scss'],
templateUrl: './schema-fields.component.html',
animations: [
fadeAnimation
]
templateUrl: './schema-fields.component.html'
})
export class SchemaFieldsComponent implements OnInit {
public fieldTypes = fieldTypes;

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

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

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

@ -42,7 +42,7 @@
Client Secret
</label>
<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 class="col-auto cell-actions no-padding">
<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,
ClientsState,
DialogModel,
fadeAnimation,
RoleDto
} from '@app/shared';
@ -20,9 +19,6 @@ import {
selector: 'sqx-client',
styleUrls: ['./client.component.scss'],
templateUrl: './client.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
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">
<ng-template let-user="$implicit">
<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>

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

@ -20,7 +20,7 @@ import {
template: `
<tr>
<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 class="cell-auto">
<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>
</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-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);
}
public goPrev() {
this.contributorsState.goPrev();
}
public goNext() {
this.contributorsState.goNext();
}
public search(query: string) {
this.contributorsState.search(query);
}

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

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

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

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import {
EditPatternForm,
fadeAnimation,
PatternDto,
PatternsState
} from '@app/shared';
@ -19,9 +18,6 @@ import {
selector: 'sqx-pattern',
styleUrls: ['./pattern.component.scss'],
templateUrl: './pattern.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
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 {
fadeAnimation,
PlanInfo,
PlansState
} from '@app/shared';
import { PlanInfo, PlansState } from '@app/shared';
@Component({
selector: 'sqx-plan',
styleUrls: ['./plan.component.scss'],
templateUrl: './plan.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PlanComponent {

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

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

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

@ -15,7 +15,7 @@ import { picasso } from '@app/framework/internal';
<img
[style.width]="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() {
this.renderer.setAttribute(this.element.nativeElement, 'target', '_blank');
this.renderer.setAttribute(this.element.nativeElement, 'rel', 'noopener');
this.renderer.setProperty(this.element.nativeElement, 'target', '_blank');
this.renderer.setProperty(this.element.nativeElement, 'rel', 'noopener');
if (this.type !== 'noicon') {
const icon = this.renderer.createElement('i');

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

@ -1,13 +1,12 @@
<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"
[attr.name]="inputName"
[attr.placeholder]="placeholder" #input
[class.form-underlined]="underlined"
[formControl]="queryInput"
[name]="inputName" [placeholder]="placeholder"
autocomplete="off"
autocorrect="off"
autocapitalize="off">
autocapitalize="off"
[class.form-underlined]="underlined"
[formControl]="queryInput">
<ng-container *sqxModal="snapshot.suggestedItems.length > 0" position="bottom-left">
<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() {
if (!this.enabled) {
if (this.enabled === false) {
return;
}

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

@ -9,18 +9,17 @@
<input type="text" class="blank" #input
(blur)="markTouched()"
(cut)="onCut($event)"
(copy)="onCopy($event)"
(paste)="onPaste($event)"
(cut)="onCut($event)"
(focus)="focus()"
(keydown)="onKeyDown($event)"
[formControl]="addInput"
[attr.name]="inputName"
[attr.placeholder]="placeholder"
(paste)="onPaste($event)"
[name]="inputName" [placeholder]="placeholder"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false">
spellcheck="false"
[formControl]="addInput">
</div>
<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.
*/
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';
@ -33,6 +33,7 @@ export class ImageSourceDirective extends ResourceOwner implements OnChanges, On
public parent: any = null;
constructor(
private readonly zone: NgZone,
private readonly element: ElementRef,
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.own(
this.renderer.listen(this.parent, 'resize', () => {
this.resize();
}));
this.zone.runOutsideAngular(() => {
this.own(
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() {
@ -67,12 +80,10 @@ export class ImageSourceDirective extends ResourceOwner implements OnChanges, On
this.setImageSource();
}
@HostListener('load')
public onLoad() {
this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible');
}
@HostListener('error')
public onError() {
this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden');

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

@ -11,7 +11,7 @@
</button>
</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>
</div>
@ -19,7 +19,7 @@
<ng-content select="[content]"></ng-content>
</div>
<div class="modal-footer" [hidden]="!showFooter || !snapshot.hasFooter">
<div class="modal-footer" *ngIf="showFooter">
<div class="clearfix" #footerElement>
<ng-content select="[footer]"></ng-content>
</div>

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

@ -5,14 +5,9 @@
* 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';
interface State {
hasTabs: boolean;
hasFooter: boolean;
}
import { fadeAnimation } from '@app/framework/internal';
@Component({
selector: 'sqx-modal-dialog',
@ -23,7 +18,7 @@ interface State {
],
changeDetection: ChangeDetectionStrategy.Default
})
export class ModalDialogComponent extends StatefulComponent<State> implements AfterViewInit {
export class ModalDialogComponent implements AfterViewInit {
@Output()
public close = new EventEmitter();
@ -57,18 +52,22 @@ export class ModalDialogComponent extends StatefulComponent<State> implements Af
@ViewChild('footerElement', { static: false })
public footerElement: ElementRef<ParentNode>;
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
hasTabs: false,
hasFooter: false
});
constructor(
private readonly renderer: Renderer2
) {
}
public ngAfterViewInit() {
const hasTabs = this.tabsElement.nativeElement.children.length > 0;
const hasFooter = this.footerElement.nativeElement.children.length > 0;
this.hideWhenEmpty(this.tabsElement.nativeElement);
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() {

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

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

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

@ -2,11 +2,18 @@
<div class="float-right pagination">
<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>
</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>
</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>

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

@ -1,2 +1,9 @@
@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 {
@Output()
public nextPage = new EventEmitter();
@Output()
public prevPage = new EventEmitter();
public pagerChange = new EventEmitter<Pager>();
@Input()
public pager: Pager;
@ -28,11 +25,15 @@ export class PagerComponent {
@Input()
public autoHide = false;
public emitNext() {
this.nextPage.emit();
public goPrev() {
this.pagerChange.emit(this.pager.goPrev());
}
public goNext() {
this.pagerChange.emit(this.pager.goNext());
}
public emitPrev() {
this.prevPage.emit();
public setPageSize(pageSize: number) {
this.pagerChange.emit(this.pager.setPageSize(pageSize));
}
}

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

@ -195,4 +195,20 @@ describe('Pager', () => {
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 {
public static readonly DEFAULT = new Pager(0);
public canGoNext = false;
public canGoPrev = false;
@ -54,6 +56,10 @@ export class Pager {
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 {
return new Pager(numberOfItems, this.page, this.pageSize);
}

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

@ -4,17 +4,21 @@
[sqxDropDisabled]="!asset || !asset.canUpload"
[noDrop]="true">
<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">
{{asset.fileType}}
</span>
<div *ngIf="asset.canPreview" class="file-image">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large">
</div>
<div *ngIf="!asset.canPreview" class="file-icon">
<img [attr.src]="asset | sqxFileIcon">
</div>
<ng-container *ngIf="asset.canPreview; else noPreview">
<div class="file-image">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large">
</div>
</ng-container>
<ng-template #noPreview>
<div class="file-icon">
<img [src]="asset | sqxFileIcon">
</div>
</ng-template>
<div class="overlay">
<div class="overlay-background"></div>
@ -50,15 +54,15 @@
</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 class="upload-progress" *ngIf="snapshot.progress > 0">
<sqx-progress-bar mode="Circle" [value]="snapshot.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 class="upload-progress" *ngIf="progress > 0">
<sqx-progress-bar mode="Circle" [value]="progress"></sqx-progress-bar>
</div>
</div>
<div class="card-footer" (dblclick)="edit()">
@ -86,53 +90,63 @@
[noDrop]="true">
<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>
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small">
</div>
<div *ngIf="asset && !asset.canPreview && snapshot.progress === 0" class="image drag-handle image-padded" [class.image-left]="!isSelectable" @fade>
<img class="icon" [attr.src]="asset | sqxFileIcon">
</div>
<ng-container *ngIf="asset && progress === 0">
<ng-container *ngIf="asset.canPreview; else noPreview">
<div class="image drag-handle" [class.image-left]="!isSelectable">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small">
</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>
<tr>
<td class="col-name">
<div class="file-name editable" (click)="edit()">
{{asset.fileName}}
</div>
</td>
<td class="col-info" *ngIf="!isCompact">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
</td>
<td class="col-user" *ngIf="!isCompact">
<img class="user-picture" title="{{asset.lastModifiedBy | sqxUserNameRef}}" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" />
</td>
<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>
<ng-container *ngIf="!isCompact">
<td class="col-info">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
</td>
<td class="col-user">
<img class="user-picture" title="{{asset.lastModifiedBy | sqxUserNameRef}}" [src]="asset.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="col-actions text-right">
<a class="btn btn-text-secondary" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon">
<i class="icon-download"></i>
</a>
</td>
</ng-container>
<div class="upload-progress" *ngIf="snapshot.progress > 0">
<sqx-progress-bar [value]="snapshot.progress" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
</div>
<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="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-text">Drop to update</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>
</ng-template>

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

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

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

@ -35,4 +35,4 @@
</ng-container>
</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 { onErrorResumeNext } from 'rxjs/operators';
import {
AssetDto,
@ -48,7 +47,7 @@ export class AssetsListComponent {
setTimeout(() => {
this.newFiles = this.newFiles.removed(file);
this.changeDetector.detectChanges();
this.changeDetector.markForCheck();
}, 2000);
} else {
this.newFiles = this.newFiles.removed(file);
@ -58,19 +57,11 @@ export class AssetsListComponent {
}
public search() {
this.state.load().pipe(onErrorResumeNext()).subscribe();
this.state.load();
}
public delete(asset: AssetDto) {
this.state.delete(asset).pipe(onErrorResumeNext()).subscribe();
}
public goNext() {
this.state.goNext().pipe(onErrorResumeNext()).subscribe();
}
public goPrev() {
this.state.goPrev().pipe(onErrorResumeNext()).subscribe();
this.state.delete(asset);
}
public update(asset: AssetDto) {

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

@ -10,7 +10,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, On
import {
AssetDto,
AssetsState,
fadeAnimation,
LocalStoreService,
Query,
StatefulComponent
@ -27,9 +26,6 @@ interface State {
selector: 'sqx-assets-selector',
styleUrls: ['./assets-selector.component.scss'],
templateUrl: './assets-selector.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
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="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 class="col pl-2">
<div class="comment-message">

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

@ -1,7 +1,7 @@
<ng-container *ngIf="events">
<div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters">
<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 class="col pl-2">
<div class="event-message">

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

@ -69,6 +69,7 @@ class UserAsyncPipe implements OnDestroy {
private lastUserId: string;
private lastValue: string | undefined = undefined;
private subscription: Subscription;
private current: Observable<string | null>;
constructor(loading: string,
private readonly users: UsersProviderService,
@ -91,11 +92,17 @@ class UserAsyncPipe implements OnDestroy {
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.changeDetector.markForCheck();
if (this.current === pipe) {
this.changeDetector.markForCheck();
}
});
this.current = pipe;
}
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 {
fadeAnimation,
FilterComparison,
LanguageDto,
QueryFieldModel,
@ -20,9 +19,6 @@ import {
selector: 'sqx-filter-comparison',
styleUrls: ['./filter-comparison.component.scss'],
templateUrl: './filter-comparison.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
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 {
fadeAnimation,
FilterLogical,
FilterNode,
LanguageDto,
@ -21,9 +20,6 @@ import {
selector: 'sqx-filter-logical',
styleUrls: ['./filter-logical.component.scss'],
templateUrl: './filter-logical.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FilterLogicalComponent {

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

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

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

@ -13,6 +13,7 @@ import {
AssetsService,
AssetsState,
DialogService,
Pager,
versioned
} from '@app/shared/internal';
@ -117,16 +118,15 @@ describe('AssetsState', () => {
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([])))
.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([])))
.returns(() => of(new AssetsDto(200, []))).verifiable();
assetsState.load().subscribe();
assetsState.goNext().subscribe();
assetsState.goPrev().subscribe();
assetsState.setPager(new Pager(60, 1, 30)).subscribe();
expect().nothing();
});

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

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

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

@ -14,6 +14,7 @@ import {
ContributorsService,
ContributorsState,
DialogService,
Pager,
versioned
} from '@app/shared/internal';
@ -60,6 +61,7 @@ describe('ContributorsState', () => {
contributorsState.load().subscribe();
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.maxContributors).toBe(oldContributors.maxContributors);
expect(contributorsState.snapshot.version).toEqual(version);
@ -67,7 +69,7 @@ describe('ContributorsState', () => {
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();
let contributors: ReadonlyArray<ContributorDto>;
@ -77,12 +79,12 @@ describe('ContributorsState', () => {
});
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.goNext();
contributorsState.setPager(new Pager(20, 1, 10));
let contributors: ReadonlyArray<ContributorDto>;
@ -91,22 +93,7 @@ describe('ContributorsState', () => {
});
expect(contributors!).toEqual(oldContributors.items.slice(10, 20));
expect(contributorsState.snapshot.page).toEqual(1);
});
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);
expect(contributorsState.snapshot.contributorsPager).toEqual(new Pager(20, 1, 10));
});
it('should show filtered contributors when searching', () => {
@ -120,7 +107,7 @@ describe('ContributorsState', () => {
});
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', () => {

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

@ -33,15 +33,15 @@ interface Snapshot {
// All loaded contributors.
contributors: ContributorsList;
// The pagination information.
contributorsPager: Pager;
// Indicates if the contributors are loaded.
isLoaded?: boolean;
// The maximum allowed users.
maxContributors: number;
// The current page.
page: number;
// The search query.
query?: string;
@ -62,9 +62,6 @@ export class ContributorsState extends State<Snapshot> {
public contributors =
this.project(x => x.contributors);
public page =
this.project(x => x.page);
public query =
this.project(x => x.query);
@ -83,18 +80,18 @@ export class ContributorsState extends State<Snapshot> {
public filtered =
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 =
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(
private readonly contributorsService: ContributorsService,
private readonly appsState: AppsState,
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> {
@ -113,12 +110,8 @@ export class ContributorsState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public goNext() {
this.next(s => ({ ...s, page: s.page + 1 }));
}
public goPrev() {
this.next(s => ({ ...s, page: s.page - 1 }));
public setPager(contributorsPager: Pager) {
this.next(s => ({ ...s, contributorsPager }));
}
public search(query: string) {
@ -149,15 +142,17 @@ export class ContributorsState extends State<Snapshot> {
}
private replaceContributors(version: Version, payload: ContributorsPayload) {
this.next(() => {
this.next(s => {
const { canCreate, items: contributors, maxContributors } = payload;
const contributorsPager = s.contributorsPager.setCount(contributors.length);
return {
canCreate,
contributors,
contributorsPager,
isLoaded: true,
maxContributors,
page: 0,
version
};
});
@ -172,10 +167,8 @@ export class ContributorsState extends State<Snapshot> {
}
}
const PAGE_SIZE = 10;
function getPagedContributors(contributors: ContributorsList, page: number) {
return contributors.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
function getPagedContributors(contributors: ContributorsList, pager: Pager) {
return contributors.slice(pager.page * pager.pageSize, (pager.page + 1) * pager.pageSize);
}
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 {
DialogService,
Pager,
RuleEventsDto,
RuleEventsState,
RulesService
@ -62,17 +63,16 @@ describe('RuleEventsState', () => {
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))
.returns(() => of(new RuleEventsDto(200, [])));
ruleEventsState.goNext().subscribe();
ruleEventsState.goPrev().subscribe();
ruleEventsState.setPager(new Pager(20, 1, 10));
expect().nothing();
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', () => {
@ -83,7 +83,7 @@ describe('RuleEventsState', () => {
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', () => {

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 rulesService: RulesService
) {
super({ ruleEvents: [], ruleEventsPager: new Pager(0) });
super({ ruleEvents: [], ruleEventsPager: Pager.DEFAULT });
}
public load(isReload = false): Observable<any> {
@ -105,19 +105,13 @@ export class RuleEventsState extends State<Snapshot> {
return empty();
}
this.next(s => ({ ...s, ruleEventsPager: new Pager(0), ruleId }));
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.reset(), ruleId }));
return this.loadInternal();
}
public goNext(): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goNext() }));
return this.loadInternal();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.goPrev() }));
public setPager(ruleEventsPager: Pager): Observable<any> {
this.next(s => ({ ...s, ruleEventsPager }));
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', () => {
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();
@ -217,7 +217,7 @@ describe('SchemasState', () => {
it('should return schema on get and reuse it from select when caching', () => {
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.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 {
DialogService,
fadeAnimation,
LoadingService,
Notification,
ResourceOwner
@ -19,10 +18,7 @@ import {
@Component({
selector: 'sqx-internal-area',
styleUrls: ['./internal-area.component.scss'],
templateUrl: './internal-area.component.html',
animations: [
fadeAnimation
]
templateUrl: './internal-area.component.html'
})
export class InternalAreaComponent extends ResourceOwner implements OnInit {
constructor(

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

@ -7,7 +7,7 @@
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<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>

10
frontend/app/theme/_panels.scss

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