Browse Source

A few UI fixes.

pull/592/head
Sebastian 5 years ago
parent
commit
4fd4a4d76d
  1. 70
      frontend/app/framework/angular/routers/router-2-state.spec.ts
  2. 58
      frontend/app/framework/angular/routers/router-2-state.ts
  3. 2
      frontend/app/framework/services/analytics.service.ts
  4. 2
      frontend/app/shared/components/forms/language-selector.component.html
  5. 4
      frontend/app/shared/components/forms/language-selector.component.scss
  6. 2
      frontend/app/shared/state/assets.state.ts
  7. 2
      frontend/app/shared/state/contents.state.ts
  8. 4
      frontend/app/shared/state/query.ts

70
frontend/app/framework/angular/routers/router-2-state.spec.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { NavigationExtras, Params, Router } from '@angular/router';
import { NavigationEnd, NavigationExtras, NavigationStart, Params, Router } from '@angular/router';
import { LocalStoreService, MathHelper, Pager } from '@app/framework/internal';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { State } from './../../state';
import { PagerSynchronizer, Router2State, StringKeysSynchronizer, StringSynchronizer } from './router-2-state';
@ -208,7 +208,8 @@ describe('Router2State', () => {
describe('Implementation', () => {
let localStore: IMock<LocalStoreService>;
let routerQueryParams: BehaviorSubject<Params>;
let routeActivated: any;
let routerEvents: Subject<any>;
let route: any;
let router: IMock<Router>;
let router2State: Router2State;
let state: State<any>;
@ -217,14 +218,16 @@ describe('Router2State', () => {
beforeEach(() => {
localStore = Mock.ofType<LocalStoreService>();
routerEvents = new Subject<any>();
router = Mock.ofType<Router>();
router.setup(x => x.events).returns(() => routerEvents);
state = new State<any>({});
routerQueryParams = new BehaviorSubject<Params>({});
routeActivated = { queryParams: routerQueryParams, id: MathHelper.guid() };
router2State = new Router2State(routeActivated, router.object, localStore.object);
route = { queryParams: routerQueryParams, id: MathHelper.guid() };
router2State = new Router2State(route, router.object, localStore.object);
router2State.mapTo(state)
.keep('keep')
.withString('state1', 'key1')
@ -242,8 +245,9 @@ describe('Router2State', () => {
it('should unsubscribe from route and state', () => {
router2State.ngOnDestroy();
expect(state.changes['observers'].length).toBe(0);
expect(routeActivated.queryParams.observers.length).toBe(0);
expect(state.changes['observers'].length).toEqual(0);
expect(route.queryParams.observers.length).toEqual(0);
expect(routerEvents.observers.length).toEqual(0);
});
it('Should sync from route', () => {
@ -279,6 +283,22 @@ describe('Router2State', () => {
expect(invoked).toEqual(1);
});
it('Should not sync again when no value has changed', () => {
routerQueryParams.next({
key1: 'hello',
key2: 'squidex'
});
routerQueryParams.next({
key1: 'hello',
key2: 'squidex',
key3: undefined,
key4: null
});
expect(invoked).toEqual(1);
});
it('Should sync again when new query changed', () => {
routerQueryParams.next({
key1: 'hello',
@ -327,7 +347,41 @@ describe('Router2State', () => {
state2: 'squidex'
});
expect(routeExtras!.relativeTo).toBeDefined();
expect(routeExtras!.replaceUrl).toBeTrue();
expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ key1: 'hello', key2: 'squidex' });
});
it('Should not sync when navigating', () => {
routerEvents.next(new NavigationStart(0, ''));
state.next({
state1: 'hello',
state2: 'squidex'
});
router.verify(x => x.navigate(It.isAny(), It.isAny()), Times.never());
expect().nothing();
});
it('Should sync from state delayed when navigating', () => {
let routeExtras: NavigationExtras;
router.setup(x => x.navigate([], It.isAny()))
.callback((_, extras) => { routeExtras = extras; });
routerEvents.next(new NavigationStart(0, ''));
state.next({
state1: 'hello',
state2: 'squidex'
});
router.verify(x => x.navigate(It.isAny(), It.isAny()), Times.never());
routerEvents.next(new NavigationEnd(0, '', ''));
expect(routeExtras!.replaceUrl).toBeTrue();
expect(routeExtras!.queryParamsHandling).toBe('merge');
expect(routeExtras!.queryParams).toEqual({ key1: 'hello', key2: 'squidex' });

58
frontend/app/framework/angular/routers/router-2-state.ts

@ -8,7 +8,7 @@
// tslint:disable: readonly-array
import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { ActivatedRoute, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, Router } from '@angular/router';
import { LocalStoreService, Pager, Types } from '@app/framework/internal';
import { State } from '@app/framework/state';
import { Subscription } from 'rxjs';
@ -177,6 +177,9 @@ export class Router2StateMap<T extends object> implements StateSynchronizerMap<T
private lastSyncedParams: Params | undefined;
private subscriptionChanges: Subscription;
private subscriptionQueryParams: Subscription;
private subscriptionEvents: Subscription;
private isNavigating = false;
private pendingParams?: Params;
constructor(
private readonly state: State<T>,
@ -194,6 +197,23 @@ export class Router2StateMap<T extends object> implements StateSynchronizerMap<T
this.subscriptionChanges =
this.state.changes
.subscribe(s => this.syncToRoute(s));
this.subscriptionEvents =
this.router.events
.subscribe(event => {
if (Types.is(event, NavigationStart)) {
this.isNavigating = true;
} else if (
Types.is(event, NavigationEnd) ||
Types.is(event, NavigationCancel) ||
Types.is(event, NavigationError)) {
this.isNavigating = false;
if (this.pendingParams) {
this.syncFromParams(this.pendingParams);
}
}
});
}
public destroy() {
@ -201,6 +221,7 @@ export class Router2StateMap<T extends object> implements StateSynchronizerMap<T
this.subscriptionQueryParams?.unsubscribe();
this.subscriptionChanges?.unsubscribe();
this.subscriptionEvents?.unsubscribe();
}
private syncToRoute(state: T) {
@ -224,27 +245,38 @@ export class Router2StateMap<T extends object> implements StateSynchronizerMap<T
return;
}
const queryParams: Params = {};
const query: Params = {};
for (const key in this.syncs) {
if (this.syncs.hasOwnProperty(key)) {
const { synchronizer, value } = this.syncs[key];
synchronizer.writeValue(value, queryParams);
synchronizer.writeValue(value, query);
}
}
this.lastSyncedParams = queryParams;
if (this.isNavigating) {
this.pendingParams = query;
} else {
this.syncFromParams(query);
}
}
private syncFromParams(query: Params) {
this.pendingParams = undefined;
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParams: query,
queryParamsHandling: 'merge',
replaceUrl: true
});
this.lastSyncedParams = cleanupParams(query);
}
private syncFromRoute(query: Params) {
query = cleanupParams(query);
if (Types.equals(this.lastSyncedParams, query)) {
return;
}
@ -305,4 +337,18 @@ export class Router2StateMap<T extends object> implements StateSynchronizerMap<T
return this;
}
}
function cleanupParams(query: Params) {
for (const key in query) {
if (query.hasOwnProperty(key)) {
const value = query[key];
if (Types.isNull(value) || Types.isUndefined(value)) {
delete query[key];
}
}
}
return query;
}

2
frontend/app/framework/services/analytics.service.ts

@ -55,7 +55,7 @@ export class AnalyticsService {
this.gtag('config', this.analyticsId, { anonymize_ip: true });
this.router.events.pipe(
filter(e => Types.is(e, NavigationEnd)))
filter(event => Types.is(event, NavigationEnd)))
.subscribe(() => {
this.gtag('config', this.analyticsId, { page_path: window.location.pathname, anonymize_ip: true });
});

2
frontend/app/shared/components/forms/language-selector.component.html

@ -9,7 +9,7 @@
{{selectedLanguage.iso2Code}}
</button>
<ng-container *sqxModal="dropdown">
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="button" @fade>
<div class="dropdown-item" *ngFor="let language of languages; trackBy: trackByLanguage" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<strong class="iso-code iso-code-dropdown">{{language.iso2Code}}</strong> ({{language.englishName}})

4
frontend/app/shared/components/forms/language-selector.component.scss

@ -2,6 +2,10 @@
cursor: pointer;
}
.dropdown-toggle {
min-width: 5rem;
}
.iso-code {
font-family: monospace;
}

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

@ -135,7 +135,7 @@ export class AssetsState extends State<Snapshot> {
.withPager('assetsPager', 'assets', 20)
.withString('parentId', 'parent')
.withStrings('tagsSelected', 'tags')
.withSynchronizer('assetsQuery', new QueryFullTextSynchronizer())
.withSynchronizer('assetsQuery', QueryFullTextSynchronizer.INSTANCE)
.whenSynced(() => this.loadInternal(false))
.build();
}

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

@ -128,7 +128,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
synchronizer.mapTo(this)
.keep('selectedContent')
.withPager('contentsPager', 'contents', 10)
.withSynchronizer('contentsQuery', new QuerySynchronizer())
.withSynchronizer('contentsQuery', QuerySynchronizer.INSTANCE)
.whenSynced(() => this.loadInternal(false))
.build();
}

4
frontend/app/shared/state/query.ts

@ -119,6 +119,8 @@ const DEFAULT_QUERY = {
};
export class QueryFullTextSynchronizer implements RouteSynchronizer {
public static readonly INSTANCE = new QueryFullTextSynchronizer();
public getValue(params: Params) {
const query = params['query'];
@ -137,6 +139,8 @@ export class QueryFullTextSynchronizer implements RouteSynchronizer {
}
export class QuerySynchronizer implements RouteSynchronizer {
public static readonly INSTANCE = new QuerySynchronizer();
public getValue(params: Params) {
const query = params['query'];

Loading…
Cancel
Save