Browse Source

Merge pull request #375 from Squidex/refactoring/schema-categories

Categories refactored.
pull/372/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
f991316ccf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  2. 10
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  3. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  4. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  5. 24
      src/Squidex/app/shared/components/schema-category.component.html
  6. 68
      src/Squidex/app/shared/components/schema-category.component.ts
  7. 59
      src/Squidex/app/shared/state/schemas.state.spec.ts
  8. 73
      src/Squidex/app/shared/state/schemas.state.ts

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

@ -18,10 +18,8 @@
<ng-container content>
<ng-container *ngIf="schemasState.publishedSchemas | async; let schemas">
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[name]="category"
[schemas]="schemas"
[schemaCategory]="category"
[schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[forContent]="true">
</sqx-schema-category>
</ng-container>

10
src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts

@ -8,7 +8,11 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { AppsState, SchemasState } from '@app/shared';
import {
AppsState,
SchemaCategory,
SchemasState
} from '@app/shared';
@Component({
selector: 'sqx-schemas-page',
@ -28,8 +32,8 @@ export class SchemasPageComponent implements OnInit {
this.schemasState.load();
}
public trackByCategory(index: number, category: string) {
return category;
public trackByCategory(index: number, category: SchemaCategory) {
return category.name;
}
}

5
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -27,10 +27,9 @@
<ng-container content>
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[name]="category"
[schemas]="schemasState.schemas | async"
[schemaCategory]="category"
[schemasFilter]="schemasFilter.valueChanges | async"
(remove)="removeCategory(category)">
(remove)="removeCategory(category.name)">
</sqx-schema-category>
<form [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()">

5
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -16,6 +16,7 @@ import {
DialogModel,
MessageBus,
ResourceOwner,
SchemaCategory,
SchemaDto,
SchemasState
} from '@app/shared';
@ -94,8 +95,8 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
this.addSchemaDialog.show();
}
public trackByCategory(index: number, category: string) {
return category;
public trackByCategory(index: number, category: SchemaCategory) {
return category.name;
}
}

24
src/Squidex/app/shared/components/schema-category.component.html

@ -1,4 +1,4 @@
<div *ngIf="!forContent || snapshot.schemasFiltered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div *ngIf="!forContent || snapshot.filtered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div class="drop-indicator"></div>
<div class="header clearfix">
@ -6,18 +6,18 @@
<i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i>
</button>
<h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3>
<h3>{{schemaCategory.name}} ({{snapshot.filtered.length}})</h3>
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !forContent" (click)="emitRemove()">
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="schemaCategory.schemas.length === 0 && !forContent" (click)="emitRemove()">
<i class="icon-bin2"></i>
</button>
</div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema">
<li class="nav-item" dnd-draggable [dragEnabled]="!forContent && schema.canUpdateCategory" [dragData]="schema">
<ng-container *ngIf="!forContent; else simpleMode">
<li *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item" dnd-draggable [dragEnabled]="schema.canUpdateCategory" [dragData]="schema">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!forContent; else simpleMode">
<div class="row">
<div class="col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span>
</div>
@ -32,12 +32,16 @@
<span class="item-published" [class.unpublished]="!schema.isPublished"></span>
</div>
</div>
<ng-template #simpleMode>
<span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</ng-template>
</a>
</li>
</ng-container>
<ng-template #simpleMode>
<li *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</a>
</li>
</ng-template>
</ul>
</div>

68
src/Squidex/app/shared/components/schema-category.component.ts

@ -10,7 +10,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
import {
fadeAnimation,
ImmutableArray,
isSameCategory,
LocalStoreService,
SchemaCategory,
SchemaDetailsDto,
SchemaDto,
SchemasState,
@ -19,12 +21,9 @@ import {
} from '@app/shared/internal';
interface State {
displayName?: string;
filtered: ImmutableArray<SchemaDto>;
schemasFiltered: ImmutableArray<SchemaDto>;
schemasForCategory: ImmutableArray<SchemaDto>;
isOpen: boolean;
isOpen?: boolean;
}
@Component({
@ -38,36 +37,26 @@ interface State {
})
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
@Input()
public name: string;
@Input()
public forContent: boolean;
@Input()
public routeSingletonToContent = false;
public schemaCategory: SchemaCategory;
@Input()
public schemasFilter: string;
@Input()
public schemas: ImmutableArray<SchemaDto>;
public forContent: boolean;
@Output()
public remove = new EventEmitter();
public allowDrop = (schema: any) => {
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema);
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !isSameCategory(this.schemaCategory.name, schema);
}
constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState
) {
super(changeDetector, {
schemasFiltered: ImmutableArray.empty(),
schemasForCategory: ImmutableArray.empty(),
isOpen: true
});
super(changeDetector, { filtered: ImmutableArray.empty(), isOpen: true });
}
public ngOnInit() {
@ -81,52 +70,37 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes['schemas'] || changes['schemasFilter']) {
const isSameCategory = (schema: SchemaDto) => {
return (!this.name && !schema.category) || schema.category === this.name;
};
if (changes['schemaCategory'] || changes['schemasFilter']) {
let filtered = this.schemaCategory.schemas;
const query = this.schemasFilter;
const schemasForCategory = this.schemas.filter(x => isSameCategory(x));
const schemasFiltered = schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
if (this.forContent) {
filtered = filtered.filter(x => x.canReadContents);
}
let isOpen = false;
if (query) {
if (this.schemasFilter) {
filtered = filtered.filter(x => x.name.indexOf(this.schemasFilter) >= 0);
isOpen = true;
} else {
isOpen = !this.localStore.getBoolean(this.configKey());
}
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));
}
if (changes['name']) {
let displayName = 'Schemas';
if (this.name && this.name.length > 0) {
displayName = this.name;
isOpen = this.localStore.get(`schema-category.${this.schemaCategory.name}`) !== 'false';
}
this.next(s => ({ ...s, displayName }));
this.next(s => ({ ...s, isOpen, filtered }));
}
}
public schemaRoute(schema: SchemaDto) {
if (schema.isSingleton && this.routeSingletonToContent) {
if (schema.isSingleton && this.forContent) {
return [schema.name, schema.id];
} else {
return [schema.name];
}
}
private isSameCategory(schema: SchemaDto): boolean {
return ((!this.name && !schema.category) || schema.category === this.name) && (!this.forContent || schema.canReadContents);
}
public changeCategory(schema: SchemaDto) {
this.schemasState.changeCategory(schema, this.name);
this.schemasState.changeCategory(schema, this.schemaCategory.name);
}
public emitRemove() {
@ -138,6 +112,6 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
private configKey(): string {
return `squidex.schema.category.${this.name}.closed`;
return `squidex.schema.category.${this.schemaCategory.name}.closed`;
}
}

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

@ -9,11 +9,12 @@
import { of, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { SchemasState } from './schemas.state';
import { SchemaCategory, SchemasState } from './schemas.state';
import {
DialogService,
FieldDto,
ImmutableArray,
SchemaDetailsDto,
SchemasService,
UpdateSchemaCategoryDto,
@ -70,7 +71,13 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false });
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ name: 'category1', upper: 'CATEGORY1', schemas: ImmutableArray.of([schema1]) },
{ name: 'category2', upper: 'CATEGORY2', schemas: ImmutableArray.of([schema2]) }
]);
schemasService.verifyAll();
});
@ -84,7 +91,14 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy();
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ name: 'category1', upper: 'CATEGORY1', schemas: ImmutableArray.of([schema1]) },
{ name: 'category2', upper: 'CATEGORY2', schemas: ImmutableArray.of([schema2]) },
{ name: 'category3', upper: 'CATEGORY3', schemas: ImmutableArray.empty() }
]);
schemasService.verifyAll();
});
@ -112,13 +126,36 @@ describe('SchemasState', () => {
it('should add category', () => {
schemasState.addCategory('category3');
expect(schemasState.snapshot.categories).toEqual({ 'category1': false, 'category2': false, 'category3': true });
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ name: 'category1', upper: 'CATEGORY1', schemas: ImmutableArray.of([schema1]) },
{ name: 'category2', upper: 'CATEGORY2', schemas: ImmutableArray.of([schema2]) },
{ name: 'category3', upper: 'CATEGORY3', schemas: ImmutableArray.empty() }
]);
});
it('should not remove category with schemas', () => {
schemasState.addCategory('category1');
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ name: 'category1', upper: 'CATEGORY1', schemas: ImmutableArray.of([schema1]) },
{ name: 'category2', upper: 'CATEGORY2', schemas: ImmutableArray.of([schema2]) }
]);
});
it('should remove category', () => {
schemasState.removeCategory('category1');
schemasState.addCategory('category3');
schemasState.removeCategory('category3');
expect(schemasState.snapshot.categories).toEqual({ 'category2': false });
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ name: 'category1', upper: 'CATEGORY1', schemas: ImmutableArray.of([schema1]) },
{ name: 'category2', upper: 'CATEGORY2', schemas: ImmutableArray.of([schema2]) }
]);
});
it('should return schema on select and reload when already loaded', () => {
@ -494,3 +531,13 @@ describe('SchemasState', () => {
});
});
});
function getCategories(schemasState: SchemasState) {
let categories: SchemaCategory[];
schemasState.categories.subscribe(result => {
categories = result;
});
return categories!;
}

73
src/Squidex/app/shared/state/schemas.state.ts

@ -38,7 +38,7 @@ type AnyFieldDto = NestedFieldDto | RootFieldDto;
interface Snapshot {
// The schema categories.
categories: { [name: string]: boolean };
categories: string[];
// The current schemas.
schemas: SchemasList;
@ -54,8 +54,7 @@ interface Snapshot {
}
export type SchemasList = ImmutableArray<SchemaDto>;
export type Categories = { [name: string]: boolean };
export type SchemaCategory = { name: string; schemas: SchemasList; upper: string; };
function sameSchema(lhs: SchemaDetailsDto | null, rhs?: SchemaDetailsDto | null): boolean {
return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id && lhs.version === rhs.version);
@ -68,7 +67,7 @@ export class SchemasState extends State<Snapshot> {
}
public categories =
this.project2(x => x.categories, x => sortedCategoryNames(x));
this.project2(x => x, x => buildCategories(x.categories, x.schemas));
public selectedSchema =
this.project(x => x.selectedSchema, sameSchema);
@ -90,7 +89,7 @@ export class SchemasState extends State<Snapshot> {
private readonly dialogs: DialogService,
private readonly schemasService: SchemasService
) {
super({ schemas: ImmutableArray.empty(), categories: buildCategories({}) });
super({ schemas: ImmutableArray.empty(), categories: [] });
}
public select(idOrName: string | null): Observable<SchemaDetailsDto | null> {
@ -126,9 +125,7 @@ export class SchemasState extends State<Snapshot> {
return this.next(s => {
const schemas = ImmutableArray.of(items).sortByStringAsc(x => x.displayName);
const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, isLoaded: true, categories, canCreate };
return { ...s, schemas, isLoaded: true, canCreate };
});
}),
shareSubscribed(this.dialogs));
@ -140,9 +137,7 @@ export class SchemasState extends State<Snapshot> {
this.next(s => {
const schemas = s.schemas.push(created).sortByStringAsc(x => x.displayName);
const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, categories };
return { ...s, schemas };
});
}),
shareSubscribed(this.dialogs, { silent: true }));
@ -163,7 +158,7 @@ export class SchemasState extends State<Snapshot> {
public addCategory(name: string) {
this.next(s => {
const categories = addCategory(s.categories, name);
const categories = [...s.categories, name];
return { ...s, categories: categories };
});
@ -171,7 +166,7 @@ export class SchemasState extends State<Snapshot> {
public removeCategory(name: string) {
this.next(s => {
const categories = removeCategory(s.categories, name);
const categories = s.categories.filter(x => x !== name);
return { ...s, categories: categories };
});
@ -309,9 +304,7 @@ export class SchemasState extends State<Snapshot> {
schema :
s.selectedSchema;
const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, selectedSchema, categories };
return { ...s, schemas, selectedSchema };
});
}
@ -328,46 +321,32 @@ function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldD
}
}
function buildCategories(categories: { [name: string]: boolean }, schemas?: SchemasList) {
categories = { ...categories };
function buildCategories(categories: string[], schemas: SchemasList): SchemaCategory[] {
const uniqueCategories: { [name: string]: string } = {};
for (let category in categories) {
if (categories.hasOwnProperty(category)) {
if (!categories[category]) {
delete categories[category];
}
}
for (let category of categories) {
uniqueCategories[category] = category;
}
if (schemas) {
for (let schema of schemas.values) {
categories[schema.category || ''] = false;
}
for (let schema of schemas.values) {
uniqueCategories[schema.category || 'Schemas'] = schema.category;
}
return categories;
}
function addCategory(categories: Categories, category: string) {
categories = { ...categories };
categories[category] = true;
const result: SchemaCategory[] = [];
return categories;
}
for (let name in uniqueCategories) {
if (uniqueCategories.hasOwnProperty(name)) {
const key = uniqueCategories[name];
function removeCategory(categories: Categories, category: string) {
categories = { ...categories };
result.push({ name, upper: name.toUpperCase(), schemas: schemas.filter(x => isSameCategory(key, x))});
}
}
delete categories[category];
result.sort((a, b) => compareStringsAsc(a.upper, b.upper));
return categories;
return result;
}
function sortedCategoryNames(categories: Categories) {
const names = Object.keys(categories);
names.sort(compareStringsAsc);
return names;
export function isSameCategory(name: string, schema: SchemaDto): boolean {
return (!name && !schema.category) || schema.category === name;
}
Loading…
Cancel
Save