diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html index ec3ffec5c..ac121fd9e 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.html @@ -18,10 +18,8 @@ diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts index 273951329..832464b46 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts +++ b/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; } } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index 5aa97c034..1c35ce05b 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -27,10 +27,9 @@ + (remove)="removeCategory(category.name)">
diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 7c8aa690f..10b07fade 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/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; } } diff --git a/src/Squidex/app/shared/components/schema-category.component.html b/src/Squidex/app/shared/components/schema-category.component.html index 406e91f13..339e189cf 100644 --- a/src/Squidex/app/shared/components/schema-category.component.html +++ b/src/Squidex/app/shared/components/schema-category.component.html @@ -1,4 +1,4 @@ -
+
@@ -6,18 +6,18 @@ -

{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})

+

{{schemaCategory.name}} ({{snapshot.filtered.length}})

-
diff --git a/src/Squidex/app/shared/components/schema-category.component.ts b/src/Squidex/app/shared/components/schema-category.component.ts index b5d714d9d..7d8e65e0c 100644 --- a/src/Squidex/app/shared/components/schema-category.component.ts +++ b/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; - schemasFiltered: ImmutableArray; - schemasForCategory: ImmutableArray; - - isOpen: boolean; + isOpen?: boolean; } @Component({ @@ -38,36 +37,26 @@ interface State { }) export class SchemaCategoryComponent extends StatefulComponent 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; + 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 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.get(`schema-category.${this.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; + 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 implements } private configKey(): string { - return `squidex.schema.category.${this.name}.closed`; + return `squidex.schema.category.${this.schemaCategory.name}.closed`; } } diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/src/Squidex/app/shared/state/schemas.state.spec.ts index 4e8c74c74..9fe337b67 100644 --- a/src/Squidex/app/shared/state/schemas.state.spec.ts +++ b/src/Squidex/app/shared/state/schemas.state.spec.ts @@ -9,10 +9,11 @@ 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, + ImmutableArray, SchemaDetailsDto, SchemasService, UpdateSchemaCategoryDto, @@ -69,7 +70,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(); }); @@ -83,7 +90,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(); }); @@ -111,13 +125,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', () => { @@ -482,4 +519,14 @@ describe('SchemasState', () => { }); }); }); -}); \ No newline at end of file +}); + +function getCategories(schemasState: SchemasState) { + let categories: SchemaCategory[]; + + schemasState.categories.subscribe(result => { + categories = result; + }); + + return categories!; +} diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index 68c3ea6d5..87dea5d3c 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/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; - -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 { } 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 { 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 { @@ -126,9 +125,7 @@ export class SchemasState extends State { 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 { 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 { 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 { 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 { 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; } \ No newline at end of file