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. 20
      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. 69
      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 content>
<ng-container *ngIf="schemasState.publishedSchemas | async; let schemas"> <ng-container *ngIf="schemasState.publishedSchemas | async; let schemas">
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory" <sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[name]="category" [schemaCategory]="category"
[schemas]="schemas"
[schemasFilter]="schemasFilter.valueChanges | async" [schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[forContent]="true"> [forContent]="true">
</sqx-schema-category> </sqx-schema-category>
</ng-container> </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 { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { AppsState, SchemasState } from '@app/shared'; import {
AppsState,
SchemaCategory,
SchemasState
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
@ -28,8 +32,8 @@ export class SchemasPageComponent implements OnInit {
this.schemasState.load(); this.schemasState.load();
} }
public trackByCategory(index: number, category: string) { public trackByCategory(index: number, category: SchemaCategory) {
return category; return category.name;
} }
} }

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

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

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

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

20
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="drop-indicator"></div>
<div class="header clearfix"> <div class="header clearfix">
@ -6,18 +6,18 @@
<i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i> <i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i>
</button> </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> <i class="icon-bin2"></i>
</button> </button>
</div> </div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade> <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"> <ng-container *ngIf="!forContent; else simpleMode">
<li class="nav-item" dnd-draggable [dragEnabled]="!forContent && schema.canUpdateCategory" [dragData]="schema"> <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"> <a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!forContent; else simpleMode"> <div class="row">
<div class="col-4"> <div class="col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span> <span class="schema-name schema-name-accent">{{schema.displayName}}</span>
</div> </div>
@ -32,12 +32,16 @@
<span class="item-published" [class.unpublished]="!schema.isPublished"></span> <span class="item-published" [class.unpublished]="!schema.isPublished"></span>
</div> </div>
</div> </div>
</a>
</li>
</ng-container>
<ng-template #simpleMode> <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> <span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</ng-template>
</a> </a>
</li> </li>
</ng-container> </ng-template>
</ul> </ul>
</div> </div>

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

@ -10,7 +10,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
import { import {
fadeAnimation, fadeAnimation,
ImmutableArray, ImmutableArray,
isSameCategory,
LocalStoreService, LocalStoreService,
SchemaCategory,
SchemaDetailsDto, SchemaDetailsDto,
SchemaDto, SchemaDto,
SchemasState, SchemasState,
@ -19,12 +21,9 @@ import {
} from '@app/shared/internal'; } from '@app/shared/internal';
interface State { interface State {
displayName?: string; filtered: ImmutableArray<SchemaDto>;
schemasFiltered: ImmutableArray<SchemaDto>; isOpen?: boolean;
schemasForCategory: ImmutableArray<SchemaDto>;
isOpen: boolean;
} }
@Component({ @Component({
@ -38,36 +37,26 @@ interface State {
}) })
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges { export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
@Input() @Input()
public name: string; public schemaCategory: SchemaCategory;
@Input()
public forContent: boolean;
@Input()
public routeSingletonToContent = false;
@Input() @Input()
public schemasFilter: string; public schemasFilter: string;
@Input() @Input()
public schemas: ImmutableArray<SchemaDto>; public forContent: boolean;
@Output() @Output()
public remove = new EventEmitter(); public remove = new EventEmitter();
public allowDrop = (schema: any) => { 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, constructor(changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState private readonly schemasState: SchemasState
) { ) {
super(changeDetector, { super(changeDetector, { filtered: ImmutableArray.empty(), isOpen: true });
schemasFiltered: ImmutableArray.empty(),
schemasForCategory: ImmutableArray.empty(),
isOpen: true
});
} }
public ngOnInit() { public ngOnInit() {
@ -81,52 +70,37 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
} }
public ngOnChanges(changes: SimpleChanges): void { public ngOnChanges(changes: SimpleChanges): void {
if (changes['schemas'] || changes['schemasFilter']) { if (changes['schemaCategory'] || changes['schemasFilter']) {
const isSameCategory = (schema: SchemaDto) => { let filtered = this.schemaCategory.schemas;
return (!this.name && !schema.category) || schema.category === this.name;
};
const query = this.schemasFilter; if (this.forContent) {
filtered = filtered.filter(x => x.canReadContents);
const schemasForCategory = this.schemas.filter(x => isSameCategory(x)); }
const schemasFiltered = schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
let isOpen = false; let isOpen = false;
if (query) { if (this.schemasFilter) {
filtered = filtered.filter(x => x.name.indexOf(this.schemasFilter) >= 0);
isOpen = true; isOpen = true;
} else { } else {
isOpen = !this.localStore.getBoolean(this.configKey()); isOpen = this.localStore.get(`schema-category.${this.schemaCategory.name}`) !== 'false';
}
this.next(s => ({ ...s, isOpen, schemasFiltered, schemasForCategory }));
}
if (changes['name']) {
let displayName = 'Schemas';
if (this.name && this.name.length > 0) {
displayName = this.name;
} }
this.next(s => ({ ...s, displayName })); this.next(s => ({ ...s, isOpen, filtered }));
} }
} }
public schemaRoute(schema: SchemaDto) { public schemaRoute(schema: SchemaDto) {
if (schema.isSingleton && this.routeSingletonToContent) { if (schema.isSingleton && this.forContent) {
return [schema.name, schema.id]; return [schema.name, schema.id];
} else { } else {
return [schema.name]; 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) { public changeCategory(schema: SchemaDto) {
this.schemasState.changeCategory(schema, this.name); this.schemasState.changeCategory(schema, this.schemaCategory.name);
} }
public emitRemove() { public emitRemove() {
@ -138,6 +112,6 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
} }
private configKey(): string { 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 { of, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { SchemasState } from './schemas.state'; import { SchemaCategory, SchemasState } from './schemas.state';
import { import {
DialogService, DialogService,
FieldDto, FieldDto,
ImmutableArray,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
UpdateSchemaCategoryDto, UpdateSchemaCategoryDto,
@ -70,7 +71,13 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items); expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy(); 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(); schemasService.verifyAll();
}); });
@ -84,7 +91,14 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items); expect(schemasState.snapshot.schemas.values).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy(); 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(); schemasService.verifyAll();
}); });
@ -112,13 +126,36 @@ describe('SchemasState', () => {
it('should add category', () => { it('should add category', () => {
schemasState.addCategory('category3'); 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', () => { 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', () => { 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!;
}

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

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