Browse Source

Fixed some imports and sorting with table header

pull/353/head
Sebastian Stehle 7 years ago
parent
commit
5255185fb9
  1. 4
      src/Squidex/app/features/assets/pages/assets-page.component.html
  2. 18
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  3. 16
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  4. 26
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  5. 16
      src/Squidex/app/features/content/shared/contents-selector.component.html
  6. 18
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  7. 2
      src/Squidex/app/framework/angular/http/caching.interceptor.ts
  8. 2
      src/Squidex/app/framework/angular/http/loading.interceptor.ts
  9. 4
      src/Squidex/app/framework/angular/pager.component.html
  10. 12
      src/Squidex/app/framework/state.ts
  11. 15
      src/Squidex/app/framework/utils/string-helper.spec.ts
  12. 4
      src/Squidex/app/framework/utils/string-helper.ts
  13. 4
      src/Squidex/app/shared/components/assets-selector.component.html
  14. 7
      src/Squidex/app/shared/components/assets-selector.component.ts
  15. 45
      src/Squidex/app/shared/components/search-form.component.html
  16. 122
      src/Squidex/app/shared/components/search-form.component.ts
  17. 52
      src/Squidex/app/shared/components/table-header.component.ts
  18. 1
      src/Squidex/app/shared/declarations.ts
  19. 1
      src/Squidex/app/shared/internal.ts
  20. 7
      src/Squidex/app/shared/module.ts
  21. 2
      src/Squidex/app/shared/services/rules.service.spec.ts
  22. 2
      src/Squidex/app/shared/state/assets.state.spec.ts
  23. 2
      src/Squidex/app/shared/state/assets.state.ts
  24. 2
      src/Squidex/app/shared/state/backups.state.spec.ts
  25. 2
      src/Squidex/app/shared/state/clients.state.spec.ts
  26. 2
      src/Squidex/app/shared/state/comments.state.spec.ts
  27. 2
      src/Squidex/app/shared/state/contents.state.ts
  28. 4
      src/Squidex/app/shared/state/contributors.state.spec.ts
  29. 0
      src/Squidex/app/shared/state/filter.state.spec.ts
  30. 204
      src/Squidex/app/shared/state/filter.state.ts
  31. 2
      src/Squidex/app/shared/state/languages.state.spec.ts
  32. 2
      src/Squidex/app/shared/state/patterns.state.spec.ts
  33. 2
      src/Squidex/app/shared/state/plans.state.spec.ts
  34. 7
      src/Squidex/app/shared/state/queries.spec.ts
  35. 4
      src/Squidex/app/shared/state/queries.ts
  36. 5
      src/Squidex/app/shared/state/roles.state.spec.ts
  37. 10
      src/Squidex/app/shared/state/rule-events.state.spec.ts
  38. 2
      src/Squidex/app/shared/state/rules.state.spec.ts
  39. 2
      src/Squidex/app/shared/state/schemas.state.spec.ts
  40. 9
      src/Squidex/app/shared/state/ui.state.spec.ts
  41. 4
      src/Squidex/app/theme/_bootstrap.scss

4
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -26,8 +26,8 @@
</div>
<div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize"
(queryChange)="search($event)"
[query]="assetsState.assetsQuery | async"
[filter]="filter"
(querySubmit)="search()"
[queries]="queries"
enableShortcut="true">
</sqx-search-form>

18
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -12,8 +12,10 @@ import { onErrorResumeNext } from 'rxjs/operators';
import {
AppsState,
AssetsState,
FilterState,
LocalStoreService,
Queries,
ResourceOwner,
UIState
} from '@app/shared';
@ -22,11 +24,13 @@ import {
styleUrls: ['./assets-page.component.scss'],
templateUrl: './assets-page.component.html'
})
export class AssetsPageComponent implements OnInit {
export class AssetsPageComponent extends ResourceOwner implements OnInit {
public assetsFilter = new FormControl();
public queries = new Queries(this.uiState, 'assets');
public filter = new FilterState();
public isListView: boolean;
constructor(
@ -35,19 +39,27 @@ export class AssetsPageComponent implements OnInit {
private readonly localStore: LocalStoreService,
private readonly uiState: UIState
) {
super();
this.isListView = this.localStore.getBoolean('squidex.assets.list-view');
}
public ngOnInit() {
this.assetsState.load().pipe(onErrorResumeNext()).subscribe();
this.own(
this.assetsState.assetsQuery
.subscribe(query => {
this.filter.setQuery(query);
}));
}
public reload() {
this.assetsState.load(true).pipe(onErrorResumeNext()).subscribe();
}
public search(query: string) {
this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe();
public search() {
this.assetsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe();
}
public selectTags(tags: string[]) {

16
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -22,8 +22,8 @@
</div>
<div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv"
(queryChange)="search($event)"
[query]="contentsState.contentsQuery | async"
[filter]="filter"
(querySubmit)="search()"
[queries]="schemaQueries"
(archivedChange)="goArchive($event)"
[archived]="contentsState.isArchive | async"
@ -54,13 +54,19 @@
<input type="checkbox" class="form-check" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of schema.listFields">
<span class="field">{{field.displayName}}</span>
<sqx-table-header [text]="field.displayName" sortable="true"
[sorting]="filter.sortMode(field) | async"
(sortingChange)="sort(field, $event)">
</sqx-table-header>
</th>
<th class="cell-time">
Updated
<sqx-table-header text="Updated" sortable="true"
[sorting]="filter.sortMode('lastModified') | async"
(sortingChange)="sort('lastModified', $event)">
</sqx-table-header>
</th>
<th class="cell-user">
By
<sqx-table-header text="By"></sqx-table-header>
</th>
<th class="cell-actions">
Actions

26
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -13,13 +13,16 @@ import {
AppsState,
ContentDto,
ContentsState,
FilterState,
ImmutableArray,
LanguagesState,
ModalModel,
Queries,
ResourceOwner,
RootFieldDto,
SchemaDetailsDto,
SchemasState,
Sorting,
UIState
} from '@app/shared';
@ -45,6 +48,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>;
public filter = new FilterState();
public isAllSelected = false;
@ViewChild('dueTimeSelector')
@ -64,6 +69,9 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.own(
this.schemasState.selectedSchema
.subscribe(schema => {
this.filter = new FilterState();
this.filter.setLanguage(this.language);
this.resetSelection();
this.schema = schema!;
@ -72,6 +80,12 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.contentsState.init().pipe(onErrorResumeNext()).subscribe();
}));
this.own(
this.contentsState.contentsQuery
.subscribe(query => {
this.filter.setQuery(query);
}));
this.own(
this.contentsState.contents
.subscribe(() => {
@ -83,6 +97,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
.subscribe(languages => {
this.languages = languages.map(x => x.language);
this.language = this.languages.at(0);
this.filter.setLanguage(this.language);
}));
}
@ -160,8 +176,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.contentsState.goNext().pipe(onErrorResumeNext()).subscribe();
}
public search(query: string) {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
public search() {
this.contentsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe();
}
public selectLanguage(language: AppLanguageDto) {
@ -200,6 +216,12 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.updateSelectionSummary();
}
public sort(field: string | RootFieldDto, sorting: Sorting) {
this.filter.setOrderField(field, sorting);
this.search();
}
public trackByContent(index: number, content: ContentDto): string {
return content.id;
}

16
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -11,9 +11,7 @@
</button>
</div>
<div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv"
[query]="contentsState.contentsQuery | async"
(queryChange)="search($event)"
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv" [filter]="filter" (querySubmit)="search()"
expandable="true">
</sqx-search-form>
</div>
@ -33,13 +31,19 @@
<input type="checkbox" class="form-check" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of schema.listFields">
<span class="field">{{field.displayName}}</span>
<sqx-table-header [text]="field.displayName" sortable="true"
[sorting]="filter.sortMode(field) | async"
(sortingChange)="sort(field, $event)">
</sqx-table-header>
</th>
<th class="cell-time">
Updated
<sqx-table-header text="Updated" sortable="true"
[sorting]="filter.sortMode('lastModified') | async"
(sortingChange)="sort('lastModified', $event)">
</sqx-table-header>
</th>
<th class="cell-user">
By
<sqx-table-header text="By"></sqx-table-header>
</th>
</tr>
</thead>

18
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -10,10 +10,12 @@ import { onErrorResumeNext } from 'rxjs/operators';
import {
ContentDto,
FilterState,
LanguageDto,
ManualContentsState,
ModalModel,
SchemaDetailsDto
RootFieldDto,
SchemaDetailsDto,
Sorting
} from '@app/shared';
@Component({
@ -37,7 +39,7 @@ export class ContentsSelectorComponent implements OnInit {
@Output()
public select = new EventEmitter<ContentDto[]>();
public searchModal = new ModalModel();
public filter = new FilterState();
public selectedItems: { [id: string]: ContentDto; } = {};
public selectionCount = 0;
@ -59,8 +61,8 @@ export class ContentsSelectorComponent implements OnInit {
this.contentsState.load(true).pipe(onErrorResumeNext()).subscribe();
}
public search(query: string) {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
public search() {
this.contentsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe();
}
public goNext() {
@ -109,6 +111,12 @@ export class ContentsSelectorComponent implements OnInit {
this.updateSelectionSummary();
}
public sort(field: string | RootFieldDto, sorting: Sorting) {
this.filter.setOrderField(field, sorting);
this.search();
}
private updateSelectionSummary() {
this.selectionCount = Object.keys(this.selectedItems).length;

2
src/Squidex/app/framework/angular/http/caching.interceptor.ts

@ -10,7 +10,7 @@ import { Injectable} from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Types } from './../../internal';
import { Types } from '@app/shared/internal';
@Injectable()
export class CachingInterceptor implements HttpInterceptor {

2
src/Squidex/app/framework/angular/http/loading.interceptor.ts

@ -10,7 +10,7 @@ import { Injectable} from '@angular/core';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoadingService, MathHelper } from './../../internal';
import { LoadingService, MathHelper } from '@app/framework/internal';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {

4
src/Squidex/app/framework/angular/pager.component.html

@ -1,6 +1,6 @@
<div class="clearfix" *ngIf="pager && pager.numberOfItems > 0 && (!hideWhenButtonsDisabled || pager.canGoPrev || pager.canGoNext)">
<div class="clearfix" *ngIf="pager && (!hideWhenButtonsDisabled || pager.canGoPrev || pager.canGoNext)">
<div class="float-right pagination">
<span 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()">
<i class="icon-angle-left"></i>

12
src/Squidex/app/framework/state.ts

@ -127,18 +127,18 @@ export class Model {
}
export class State<T extends {}> {
private readonly state: BehaviorSubject<T>;
private readonly initialState: T;
private readonly state: BehaviorSubject<Readonly<T>>;
private readonly initialState: Readonly<T>;
public get changes(): Observable<T> {
public get changes(): Observable<Readonly<T>> {
return this.state;
}
public get snapshot() {
public get snapshot(): Readonly<T> {
return this.state.value;
}
constructor(state: T) {
constructor(state: Readonly<T>) {
this.initialState = state;
this.state = new BehaviorSubject(state);
@ -148,7 +148,7 @@ export class State<T extends {}> {
this.next(this.initialState);
}
public next(update: ((v: T) => T) | object) {
public next(update: ((v: T) => Readonly<T>) | object) {
if (Types.isFunction(update)) {
this.state.next(update(this.state.value));
} else {

15
src/Squidex/app/framework/utils/string-helper.spec.ts

@ -38,4 +38,19 @@ describe('StringHelper', () => {
it('should return empty string if also fallback not found', () => {
expect(StringHelper.firstNonEmpty(null!, undefined!, '')).toBe('');
});
[
{ src: '', result: '' },
{ src: 'M', result: 'm' },
{ src: 'My', result: 'my' },
{ src: 'M-y', result: 'mY' },
{ src: 'MyProperty ', result: 'myProperty' },
{ src: 'My property', result: 'myProperty' },
{ src: 'My_property', result: 'myProperty' },
{ src: 'My-property', result: 'myProperty' }
].forEach(test => {
it(`should return convert ${test.src} to camel case`, () => {
expect(StringHelper.toCamelCase(test.src)).toBe(test.result);
});
});
});

4
src/Squidex/app/framework/utils/string-helper.ts

@ -19,4 +19,8 @@ export module StringHelper {
return '';
}
export function toCamelCase(value: string) {
return value.replace(/[^\s\-_]+/g, (w, i) => (i === 0 ? w[0].toLowerCase() : w[0].toUpperCase()) + w.slice(1)).replace(/[\s\-_]+/g, '').trim();
}
}

4
src/Squidex/app/shared/components/assets-selector.component.html

@ -21,9 +21,7 @@
</sqx-tag-editor>
</div>
<div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize"
(queryChange)="search($event)"
[query]="assetsState.assetsQuery | async"
<sqx-search-form formClass="form" placeholder="Search by asset name" fieldExample="fileSize" [filter]="filter" (querySubmit)="search()"
enableShortcut="true">
</sqx-search-form>
</div>

7
src/Squidex/app/shared/components/assets-selector.component.ts

@ -12,6 +12,7 @@ import {
AssetDto,
AssetsDialogState,
fadeAnimation,
FilterState,
LocalStoreService,
StatefulComponent
} from '@app/shared/internal';
@ -36,6 +37,8 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
@Output()
public select = new EventEmitter<AssetDto[]>();
public filter = new FilterState();
constructor(changeDector: ChangeDetectorRef,
public readonly assetsState: AssetsDialogState,
public readonly localStore: LocalStoreService
@ -55,8 +58,8 @@ export class AssetsSelectorComponent extends StatefulComponent<State> implements
this.assetsState.load(true).pipe(onErrorResumeNext()).subscribe();
}
public search(query: string) {
this.assetsState.search(query).pipe(onErrorResumeNext()).subscribe();
public search() {
this.assetsState.search(this.filter.apiFilter).pipe(onErrorResumeNext()).subscribe();
}
public emitComplete() {

45
src/Squidex/app/shared/components/search-form.component.html

@ -3,7 +3,10 @@
</ng-container>
<form [class]="formClass" (ngSubmit)="search()">
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" [placeholder]="placeholder" />
<input class="form-control form-control-expandable" #inputFind [placeholder]="placeholder"
[ngModel]="filter.query | async"
(ngModelChange)="filter.setQuery($event)"
[ngModelOptions]="{ standalone: true }" />
<ng-container *ngIf="expandable">
<a class="expand-search" (click)="searchModal.toggle()" #expand>
@ -23,7 +26,7 @@
</ng-container>
<ng-template #notBookmarked>
<a class="save-search" (click)="saveQuery()" *ngIf="contentsFilterValue | async">
<a class="save-search" (click)="saveQuery()" *ngIf="filter.query | async">
<i class="icon-star-empty"></i>
</a>
</ng-template>
@ -36,29 +39,33 @@
<div class="dropdown-menu" *sqxModalView="searchModal;onRoot:true" [sqxModalTarget]="inputFind">
<div class="form-horizontal">
<div [formGroup]="searchForm">
<div class="form-group row">
<label class="col-2 col-form-label" for="search">Text</label>
<div class="form-group row">
<label class="col-2 col-form-label" for="search">Text</label>
<div class="col-10">
<input type="text" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" />
</div>
<div class="col-10">
<input type="text" class="form-control" id="search" placeholder="Fulltext search"
[ngModel]="filter.fullText | async"
(ngModelChange)="filter.setFullText($event)" />
</div>
</div>
<div class="form-group row">
<label class="col-2 col-form-label" for="filter">Filter</label>
<div class="form-group row">
<label class="col-2 col-form-label" for="filter">Filter</label>
<div class="col-10">
<input type="text" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="{{fieldExample}} eq [VALUE]" />
</div>
<div class="col-10">
<input type="text" class="form-control" id="filter" placeholder="{{fieldExample}} eq [VALUE]"
[ngModel]="filter.filter | async"
(ngModelChange)="filter.setFilter($event)" />
</div>
</div>
<div class="form-group row">
<label class="col-2 col-form-label" for="orderBy">Order</label>
<div class="col-10">
<input type="text" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="{{fieldExample}} desc" />
</div>
<div class="form-group row">
<label class="col-2 col-form-label" for="orderBy">Order</label>
<div class="col-10">
<input type="text" class="form-control" id="orderBy" placeholder="{{fieldExample}} desc"
[ngModel]="filter.order | async"
(ngModelChange)="filter.setOrder($event)" />
</div>
</div>

122
src/Squidex/app/shared/components/search-form.component.ts

@ -5,25 +5,25 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs';
import {
DialogModel,
FilterState,
ModalModel,
Queries,
SaveQueryForm
} from '@app/shared/internal';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'sqx-search-form',
styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchFormComponent implements OnChanges, OnInit {
export class SearchFormComponent implements OnInit {
@Input()
public queries: Queries;
@ -37,10 +37,7 @@ export class SearchFormComponent implements OnChanges, OnInit {
public expandable = false;
@Input()
public query = '';
@Output()
public queryChange = new EventEmitter<string>();
public filter: FilterState;
@Input()
public archived = false;
@ -60,21 +57,14 @@ export class SearchFormComponent implements OnChanges, OnInit {
@Input()
public formClass = 'form-inline search-form';
public contentsFilter = new FormControl();
public contentsFilterValue = this.contentsFilter.valueChanges.pipe(shareReplay(1));
@Output()
public querySubmit = new EventEmitter();
public saveKey: Observable<string | null>;
public saveKey: Observable<string | undefined>;
public saveQueryDialog = new DialogModel();
public saveQueryForm = new SaveQueryForm(this.formBuilder);
public searchModal = new ModalModel();
public searchForm =
this.formBuilder.group({
odataOrderBy: '',
odataFilter: '',
odataSearch: ''
});
public saveQueryDialog = new ModalModel();
public saveQueryForm = new SaveQueryForm(this.formBuilder);
constructor(
private readonly formBuilder: FormBuilder
@ -83,14 +73,10 @@ export class SearchFormComponent implements OnChanges, OnInit {
public ngOnInit() {
if (this.queries) {
this.saveKey = this.queries.getSaveKey(this.contentsFilter.valueChanges);
this.saveKey = this.queries.getSaveKey(this.filter.query);
}
}
public ngOnChanges() {
this.invalidate(this.query);
}
public saveQuery() {
this.saveQueryForm.submitCompleted({});
this.saveQueryDialog.show();
@ -101,7 +87,7 @@ export class SearchFormComponent implements OnChanges, OnInit {
if (value) {
if (this.queries) {
this.queries.add(value.name, this.contentsFilter.value);
this.queries.add(value.name, this.filter.apiFilter!);
}
this.saveQueryForm.submitCompleted();
@ -111,84 +97,6 @@ export class SearchFormComponent implements OnChanges, OnInit {
}
public search() {
this.invalidate(this.contentsFilter.value);
this.queryChange.emit(this.contentsFilter.value);
}
private invalidate(query: string) {
if (query === this.contentsFilter.value) {
return;
}
let odataOrderBy = '';
let odataFilter = '';
let odataSearch = '';
if (this.query) {
const parts = this.query.split('&');
if (parts.length === 1 && parts[0][0] !== '$') {
odataSearch = parts[0];
} else {
for (let part of parts) {
const kvp = part.split('=');
if (kvp.length === 2) {
const key = kvp[0].toLowerCase();
if (key === '$filter') {
odataFilter = kvp[1];
} else if (key === '$orderby') {
odataOrderBy = kvp[1];
} else if (key === '$search') {
odataSearch = kvp[1];
}
}
}
}
}
this.searchForm.setValue({
odataFilter,
odataSearch,
odataOrderBy
}, { emitEvent: false });
this.contentsFilter.setValue(this.query);
}
public updateQuery() {
const odataOrderBy = this.searchForm.controls['odataOrderBy'].value;
const odataFilter = this.searchForm.controls['odataFilter'].value;
const odataSearch = this.searchForm.controls['odataSearch'].value;
let query = '';
if (odataSearch && !odataOrderBy && !odataFilter) {
query = odataSearch;
} else {
const parts: string[] = [];
if (odataSearch) {
parts.push(`$search=${odataSearch}`);
}
if (odataFilter) {
parts.push(`$filter=${odataFilter}`);
}
if (odataOrderBy) {
parts.push(`$orderby=${odataOrderBy}`);
}
query = parts.join('&');
}
if (query !== this.query) {
this.queryChange.emit(query);
}
this.contentsFilter.setValue(query);
this.querySubmit.emit();
}
}

52
src/Squidex/app/shared/components/table-header.component.ts

@ -0,0 +1,52 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Sorting } from '@app/shared/internal';
@Component({
selector: 'sqx-table-header',
template: `
<a *ngIf="sortable; else notSortable" (click)="sort()" class="pointer truncate">
<i *ngIf="sorting === 'Ascending'" class="icon-caret-down"></i>
<i *ngIf="sorting === 'Descending'" class="icon-caret-up"></i>
{{text}}
</a>
<ng-template #notSortable>
{{text}}
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent {
@Input()
public text: string;
@Input()
public sortable = false;
@Input()
public sorting: Sorting;
@Output()
public sortingChange = new EventEmitter<Sorting>();
public sort() {
if (!!this.sortable) {
if (!this.sorting || this.sorting !== 'Ascending') {
this.sorting = 'Ascending';
} else {
this.sorting = 'Descending';
}
this.sortingChange.emit(this.sorting);
}
}
}

1
src/Squidex/app/shared/declarations.ts

@ -24,5 +24,6 @@ export * from './components/pipes';
export * from './components/rich-editor.component';
export * from './components/schema-category.component';
export * from './components/search-form.component';
export * from './components/table-header.component';
export * from './internal';

1
src/Squidex/app/shared/internal.ts

@ -59,6 +59,7 @@ export * from './state/contents.forms';
export * from './state/contents.state';
export * from './state/contributors.forms';
export * from './state/contributors.state';
export * from './state/filter.state';
export * from './state/languages.forms';
export * from './state/languages.state';
export * from './state/patterns.forms';

7
src/Squidex/app/shared/module.ts

@ -78,6 +78,7 @@ import {
SchemasService,
SchemasState,
SearchFormComponent,
TableHeaderComponent,
TranslationsService,
UIService,
UIState,
@ -128,7 +129,8 @@ import {
UserPicturePipe,
UserPictureRefPipe,
RichEditorComponent,
SearchFormComponent
SearchFormComponent,
TableHeaderComponent
],
exports: [
AppFormComponent,
@ -150,6 +152,7 @@ import {
LanguageSelectorComponent,
MarkdownEditorComponent,
PermissionDirective,
RichEditorComponent,
RouterModule,
SchemaCategoryComponent,
SearchFormComponent,
@ -159,7 +162,7 @@ import {
UserNameRefPipe,
UserPicturePipe,
UserPictureRefPipe,
RichEditorComponent
TableHeaderComponent
],
providers: [
AssetsDialogState

2
src/Squidex/app/shared/services/rules.service.spec.ts

@ -16,13 +16,13 @@ import {
DateTime,
RuleDto,
RuleElementDto,
RuleElementPropertyDto,
RuleEventDto,
RuleEventsDto,
RulesService,
UpdateRuleDto,
Version
} from './../';
import { RuleElementPropertyDto } from './rules.service';
describe('RulesService', () => {
const now = DateTime.now();

2
src/Squidex/app/shared/state/assets.state.spec.ts

@ -18,7 +18,7 @@ import {
DialogService,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('AssetsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/assets.state.ts

@ -189,7 +189,7 @@ export class AssetsState extends State<Snapshot> {
return this.loadInternal();
}
public search(query: string): Observable<any> {
public search(query?: string): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0, 0, 30), assetsQuery: query }));
return this.loadInternal();

2
src/Squidex/app/shared/state/backups.state.spec.ts

@ -16,7 +16,7 @@ import {
BackupsState,
DateTime,
DialogService
} from '@app/shared';
} from './../';
describe('BackupsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/clients.state.spec.ts

@ -19,7 +19,7 @@ import {
UpdateAppClientDto,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('ClientsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/comments.state.spec.ts

@ -19,7 +19,7 @@ import {
ImmutableArray,
UpsertCommentDto,
Version
} from '@app/shared';
} from './../';
describe('CommentsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/contents.state.ts

@ -282,7 +282,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
return this.loadInternal();
}
public search(query: string): Observable<any> {
public search(query?: string): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: query }));
return this.loadInternal();

4
src/Squidex/app/shared/state/contributors.state.spec.ts

@ -15,12 +15,12 @@ import {
AppsState,
AssignContributorDto,
AuthService,
ContributorAssignedDto,
ContributorsState,
DialogService,
Version,
Versioned
} from '@app/shared';
import { ContributorAssignedDto } from '../services/app-contributors.service';
} from './../';
describe('ContributorsState', () => {
const app = 'my-app';

0
src/Squidex/app/shared/state/filter.state.spec.ts

204
src/Squidex/app/shared/state/filter.state.ts

@ -0,0 +1,204 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/internal/operators';
import {
State,
StringHelper,
Types
} from '@app/framework';
import { LanguageDto } from '../services/languages.service';
import { RootFieldDto } from '../services/schemas.service';
interface Snapshot {
language?: LanguageDto;
order?: string;
orderField?: string;
orderType?: string;
query?: string;
filter?: string;
fullText?: string;
}
export type Sorting = 'Ascending' | 'Descending' | 'None';
export class FilterState extends State<Snapshot> {
private readonly sortModes: { [key: string]: Observable<Sorting> } = {};
public query =
this.changes.pipe(map(x => x.query),
distinctUntilChanged());
public order =
this.changes.pipe(map(x => x.order),
distinctUntilChanged());
public filter =
this.changes.pipe(map(x => x.filter),
distinctUntilChanged());
public fullText =
this.changes.pipe(map(x => x.fullText),
distinctUntilChanged());
public get apiFilter() {
return this.snapshot.query;
}
public constructor() {
super({});
}
public sortMode(field: RootFieldDto | string) {
const key = Types.isString(field) ? field : field.fieldId.toString();
let result = this.sortModes[key];
if (!result) {
result = this.changes.pipe(map(x => sortMode(x, field)), distinctUntilChanged());
this.sortModes[key] = result;
}
return result;
}
public setQuery(query?: string) {
this.next(s => fromQuery(s, query));
}
public setOrder(order?: string) {
this.next(s => fromProperty(s, { order }));
}
public setFilter(filter?: string) {
this.next(s => fromProperty(s, { filter }));
}
public setFullText(fullText?: string) {
this.next(s => fromProperty(s, { fullText }));
}
public setLanguage(language: LanguageDto) {
this.next(s => ({ ...s, language }));
}
public setOrderField(field: RootFieldDto | string, sorting: Sorting) {
this.setOrder(getFieldSorting(this.snapshot, field, sorting));
}
}
function sortMode(snapshot: Snapshot, field: RootFieldDto | string): Sorting {
let path = getFieldPath(snapshot, field);
if (snapshot.orderField === path) {
if (snapshot.orderType === 'asc') {
return 'Ascending';
} else if (snapshot.orderType === 'desc') {
return 'Descending';
}
}
return 'None';
}
function getFieldSorting(snapshot: Snapshot, field: RootFieldDto | string, sorting: Sorting) {
if (sorting === 'Ascending') {
return `${getFieldPath(snapshot, field)} asc`;
} else {
return `${getFieldPath(snapshot, field)} desc`;
}
}
function getFieldPath(snapshot: Snapshot, field?: RootFieldDto | string) {
let path: string | undefined = undefined;
if (field) {
if (Types.isString(field)) {
path = field;
} else if (field.isLocalizable && snapshot.language) {
path = `data/${StringHelper.toCamelCase(field.name)}/${snapshot.language.iso2Code}`;
} else {
path = `data/${StringHelper.toCamelCase(field.name)}/iv`;
}
}
return path;
}
function fromQuery(previous: Snapshot, query?: string) {
const snapshot: Snapshot = { language: previous.language, query };
if (query) {
const parts = query.split('&');
if (parts.length === 1 && parts[0][0] !== '$') {
snapshot.fullText = parts[0];
} else {
for (let part of parts) {
const kvp = part.split('=');
if (kvp.length === 2) {
const key = kvp[0].toLowerCase();
if (key === '$filter') {
snapshot.filter = kvp[1];
} else if (key === '$orderby') {
snapshot.order = kvp[1];
} else if (key === '$search') {
snapshot.fullText = kvp[1];
}
}
}
}
}
return enrichOrderField(snapshot);
}
function fromProperty(previous: Snapshot, update: Partial<Snapshot>) {
const snapshot = { ...previous, ...update };
if (snapshot.fullText && !snapshot.order && !snapshot.filter) {
snapshot.query = snapshot.fullText;
} else {
const parts: string[] = [];
if (snapshot.fullText) {
parts.push(`$search=${snapshot.fullText}`);
}
if (snapshot.filter) {
parts.push(`$filter=${snapshot.filter}`);
}
if (snapshot.order) {
parts.push(`$orderby=${snapshot.order}`);
}
snapshot.query = parts.join('&');
}
return enrichOrderField(snapshot);
}
function enrichOrderField(snapshot: Snapshot) {
if (snapshot.order) {
const orderParts = snapshot.order.split(' ');
if (orderParts.length === 2) {
snapshot.orderField = orderParts[0];
snapshot.orderType = orderParts[1];
}
}
return snapshot;
}

2
src/Squidex/app/shared/state/languages.state.spec.ts

@ -21,7 +21,7 @@ import {
UpdateAppLanguageDto,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('LanguagesState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/patterns.state.spec.ts

@ -18,7 +18,7 @@ import {
PatternsState,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('PatternsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/plans.state.spec.ts

@ -20,7 +20,7 @@ import {
PlansState,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('PlansState', () => {
const app = 'my-app';

7
src/Squidex/app/shared/state/queries.spec.ts

@ -8,8 +8,11 @@
import { BehaviorSubject } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { Queries, Query } from './queries';
import { UIState } from './ui.state';
import {
Queries,
Query,
UIState
} from './../';
describe('Queries', () => {
const prefix = 'schemas.my-schema';

4
src/Squidex/app/shared/state/queries.ts

@ -60,7 +60,7 @@ export class Queries {
this.uiState.remove(`${this.prefix}.queries.${key}`);
}
public getSaveKey(filter$: Observable<string>): Observable<string | null> {
public getSaveKey(filter$: Observable<string | undefined>): Observable<string | undefined> {
return combineLatest(this.queries, filter$).pipe(
map(project => {
const filter = project[1];
@ -72,7 +72,7 @@ export class Queries {
}
}
}
return null;
return undefined;
}));
}
}

5
src/Squidex/app/shared/state/roles.state.spec.ts

@ -13,12 +13,13 @@ import {
AppRolesDto,
AppRolesService,
AppsState,
CreateAppRoleDto,
DialogService,
RolesState,
UpdateAppRoleDto,
Version,
Versioned
} from '@app/shared';
import { CreateAppRoleDto, UpdateAppRoleDto } from '../services/app-roles.service';
} from './../';
describe('RolesState', () => {
const app = 'my-app';

10
src/Squidex/app/shared/state/rule-events.state.spec.ts

@ -11,16 +11,12 @@ import { IMock, It, Mock, Times } from 'typemoq';
import {
AppsState,
DateTime,
DialogService
} from '@app/shared';
import { RuleEventsState } from './rule-events.state';
import {
DialogService,
RuleEventDto,
RuleEventsDto,
RuleEventsState,
RulesService
} from './../services/rules.service';
} from './../';
describe('RuleEventsState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/rules.state.spec.ts

@ -21,7 +21,7 @@ import {
UpdateRuleDto,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('RulesState', () => {
const app = 'my-app';

2
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -28,7 +28,7 @@ import {
UpdateSchemaDto,
Version,
Versioned
} from '@app/shared';
} from './../';
describe('SchemasState', () => {
const app = 'my-app';

9
src/Squidex/app/shared/state/ui.state.spec.ts

@ -8,10 +8,11 @@
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AppsState } from '@app/shared';
import { UIService } from './../services/ui.service';
import { UIState } from './ui.state';
import {
AppsState,
UIService,
UIState
} from './../';
describe('UIState', () => {
const app = 'my-app';

4
src/Squidex/app/theme/_bootstrap.scss

@ -94,6 +94,10 @@ a {
}
}
&.pointer {
cursor: pointer;
}
&.force {
& {
color: $color-theme-blue !important;

Loading…
Cancel
Save