Browse Source

Improve schema handling.

pull/782/head
Sebastian 4 years ago
parent
commit
7c84d34a1c
  1. 8
      frontend/app/features/content/pages/schemas/schemas-page.component.html
  2. 27
      frontend/app/features/content/pages/schemas/schemas-page.component.ts
  3. 6
      frontend/app/features/schemas/pages/schemas/schemas-page.component.html
  4. 14
      frontend/app/features/schemas/pages/schemas/schemas-page.component.ts
  5. 10
      frontend/app/shared/components/schema-category.component.html
  6. 31
      frontend/app/shared/components/schema-category.component.ts
  7. 106
      frontend/app/shared/state/schemas.state.spec.ts
  8. 59
      frontend/app/shared/state/schemas.state.ts

8
frontend/app/features/content/pages/schemas/schemas-page.component.html

@ -18,11 +18,9 @@
</li>
</ul>
<ng-container *ngIf="schemasState.publishedSchemas | async; let schemas">
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[schemaCategory]="category"
[schemasFilter]="schemaFilterFunction | async"
[forContent]="true">
<ng-container>
<sqx-schema-category *ngFor="let category of categories | async; trackBy: trackByCategory"
[schemaCategory]="category" [forContent]="true">
</sqx-schema-category>
</ng-container>
</ng-container>

27
frontend/app/features/content/pages/schemas/schemas-page.component.ts

@ -7,7 +7,8 @@
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { buildSchemaFilterFunction, LocalStoreService, SchemaCategory, SchemasState, Settings } from '@app/shared';
import { AppsState, getCategoryTree, LocalStoreService, SchemaCategory, SchemasState, Settings, value$ } from '@app/shared';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
@ -18,9 +19,26 @@ import { map } from 'rxjs/operators';
export class SchemasPageComponent {
public schemasFilter = new FormControl();
public schemaFilterFunction =
this.schemasFilter.valueChanges.pipe(
map(buildSchemaFilterFunction));
public schemas =
this.schemasState.schemas.pipe(
map(schemas => {
const app = this.appsState.snapshot.selectedApp!;
return schemas.filter(schema =>
schema.canReadContents &&
schema.isPublished &&
!app.roleProperties[Settings.AppProperties.HIDE_CONTENTS(schema.name)],
);
}));
public categories =
combineLatest([
value$(this.schemasFilter),
this.schemas,
this.schemasState.categoryNames,
], (filter, schemas, categories) => {
return getCategoryTree(schemas, categories, filter);
});
public isCollapsed: boolean;
@ -30,6 +48,7 @@ export class SchemasPageComponent {
constructor(
public readonly schemasState: SchemasState,
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
) {
this.isCollapsed = localStore.getBoolean(Settings.Local.SCHEMAS_COLLAPSED);

6
frontend/app/features/schemas/pages/schemas/schemas-page.component.html

@ -21,10 +21,8 @@
<ng-container>
<div cdkDropListGroup>
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[schemaCategory]="category"
[schemasFilter]="schemaFilterFunction | async"
(remove)="removeCategory(category.displayName)">
<sqx-schema-category *ngFor="let category of categories | async; trackBy: trackByCategory"
[schemaCategory]="category" (remove)="removeCategory(category.displayName)">
</sqx-schema-category>
</div>

14
frontend/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -8,7 +8,8 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { buildSchemaFilterFunction, CreateCategoryForm, DialogModel, MessageBus, ResourceOwner, SchemaCategory, SchemaDto, SchemasState } from '@app/shared';
import { CreateCategoryForm, DialogModel, getCategoryTree, MessageBus, ResourceOwner, SchemaCategory, SchemaDto, SchemasState, value$ } from '@app/shared';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { SchemaCloning } from './../messages';
@ -23,9 +24,14 @@ export class SchemasPageComponent extends ResourceOwner implements OnInit {
public schemasFilter = new FormControl();
public schemaFilterFunction =
this.schemasFilter.valueChanges.pipe(
map(buildSchemaFilterFunction));
public categories =
combineLatest([
value$(this.schemasFilter),
this.schemasState.schemas,
this.schemasState.categoryNames,
], (filter, schemas, categories) => {
return getCategoryTree(schemas, categories, filter);
});
public import: any;

10
frontend/app/shared/components/schema-category.component.html

@ -1,4 +1,4 @@
<ul *ngIf="!forContent || filteredSchemas.length > 0" class="nav nav-light flex-column"
<ul *ngIf="!forContent || schemas.length > 0" class="nav nav-light flex-column"
cdkDropList
cdkDropListSortingDisabled
[cdkDropListData]="schemaCategory.name"
@ -17,8 +17,8 @@
</div>
</div>
<div class="col-auto">
<ng-container *ngIf="schemaCategory.schemas.length > 0; else noSchemas">
<span class="badge rounded-pill badge-secondary">{{filteredSchemas.length}}</span>
<ng-container *ngIf="schemas.length > 0; else noSchemas">
<span class="badge rounded-pill badge-secondary">{{schemas.length}}</span>
</ng-container>
<ng-template #noSchemas>
<button type="button" class="btn btn-sm btn-text-secondary btn-remove" (click)="remove.emit()" *ngIf="schemaCategory.name">
@ -31,7 +31,7 @@
<div *ngIf="!isCollapsed" @fade [style.height]="getContainerHeight()">
<ng-container *ngIf="!forContent; else simpleMode">
<li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate" [style.height]="getItemHeight()"
<li *ngFor="let schema of schemas; trackBy: trackBySchema" class="nav-item truncate" [style.height]="getItemHeight()"
cdkDrag
cdkDragLockAxis="y"
[cdkDragData]="schema"
@ -46,7 +46,7 @@
</ng-container>
<ng-template #simpleMode>
<li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate">
<li *ngFor="let schema of schemas; trackBy: trackBySchema" class="nav-item truncate">
<a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left">
{{schema.displayName}}
</a>

31
frontend/app/shared/components/schema-category.component.ts

@ -7,9 +7,7 @@
import { CdkDragDrop, CdkDragStart } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasList, SchemasState } from '@app/shared/internal';
import { AppsState } from '../state/apps.state';
import { Settings } from '../state/settings';
import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasState } from '@app/shared/internal';
const ITEM_HEIGHT = 2.5;
@ -29,18 +27,16 @@ export class SchemaCategoryComponent implements OnChanges {
@Input()
public schemaCategory: SchemaCategory;
@Input()
public schemasFilter?: ((schema: SchemaDto) => boolean) | null;
@Input()
public forContent?: boolean | null;
public filteredSchemas: SchemasList;
public isCollapsed = false;
public get schemas() {
return this.schemaCategory.schemas;
}
constructor(
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState,
) {
@ -53,20 +49,7 @@ export class SchemaCategoryComponent implements OnChanges {
}
public ngOnChanges() {
this.filteredSchemas = this.schemaCategory.schemas;
if (this.forContent) {
const app = this.appsState.snapshot.selectedApp!;
this.filteredSchemas = this.filteredSchemas.filter(x => x.canReadContents && x.isPublished && x.type !== 'Component');
this.filteredSchemas = this.filteredSchemas.filter(x => !app.roleProperties[Settings.AppProperties.HIDE_CONTENTS(x.name)]);
}
const filter = this.schemasFilter;
if (filter) {
this.filteredSchemas = this.filteredSchemas.filter(x => filter(x));
if (this.schemaCategory.schemas.length < this.schemaCategory.schemaTotalCount) {
this.isCollapsed = false;
} else {
this.isCollapsed = this.localStore.getBoolean(this.configKey());
@ -103,7 +86,7 @@ export class SchemaCategoryComponent implements OnChanges {
}
public getContainerHeight() {
return `${ITEM_HEIGHT * this.filteredSchemas.length}rem`;
return `${ITEM_HEIGHT * this.schemas.length}rem`;
}
public trackBySchema(_index: number, schema: SchemaDto) {

106
frontend/app/shared/state/schemas.state.spec.ts

@ -10,7 +10,7 @@ import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { IMock, It, Mock, Times } from 'typemoq';
import { createSchema } from './../services/schemas.service.spec';
import { SchemaCategory, SchemasState } from './schemas.state';
import { getCategoryTree, SchemasState } from './schemas.state';
import { TestValues } from './_test-helpers';
describe('SchemasState', () => {
@ -58,15 +58,6 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas).toEqual(oldSchemas.items);
expect(schemasState.snapshot.isLoaded).toBeTruthy();
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
]);
schemasService.verifyAll();
});
@ -81,16 +72,6 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.isLoading).toBeFalsy();
expect(schemasState.snapshot.schemas).toEqual(oldSchemas.items);
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
{ displayName: 'schema-category3', name: 'schema-category3', schemas: [] },
]);
schemasService.verifyAll();
});
@ -150,53 +131,14 @@ describe('SchemasState', () => {
it('should add category', () => {
schemasState.addCategory('schema-category3');
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
{ displayName: 'schema-category3', name: 'schema-category3', schemas: [] },
]);
});
it('should not remove category with schemas', () => {
schemasState.removeCategory('schema-category1');
const categories = getCategories(schemasState);
expect(categories!).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
]);
expect([...schemasState.snapshot.categories]).toEqual(['schema-category3']);
});
it('should remove category', () => {
schemasState.addCategory('schema-category3');
const categories1 = getCategories(schemasState);
expect(categories1).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
{ displayName: 'schema-category3', name: 'schema-category3', schemas: [] },
]);
schemasState.removeCategory('schema-category3');
const categories2 = getCategories(schemasState);
expect(categories2).toEqual([
{ displayName: 'i18n:common.components', schemas: [] },
{ displayName: 'i18n:common.schemas', schemas: [] },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1] },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2] },
]);
expect([...schemasState.snapshot.categories]).toEqual([]);
});
it('should return schema on select and reload if already loaded', () => {
@ -550,14 +492,40 @@ describe('SchemasState', () => {
});
});
});
});
function getCategories(schemasState: SchemasState) {
let categories: ReadonlyArray<SchemaCategory>;
describe('Categories', () => {
it('should be build from schemas', () => {
const result = getCategoryTree([schema1, schema2], new Set<string>());
schemasState.categories.subscribe(result => {
categories = result;
});
expect(result).toEqual([
{ displayName: 'i18n:common.components', schemas: [], schemaTotalCount: 0 },
{ displayName: 'i18n:common.schemas', schemas: [], schemaTotalCount: 0 },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1], schemaTotalCount: 1 },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2], schemaTotalCount: 1 },
]);
});
it('should be build from schemas and custom name', () => {
const result = getCategoryTree([schema1, schema2], new Set<string>(['schema-category3']));
return categories!;
}
expect(result).toEqual([
{ displayName: 'i18n:common.components', schemas: [], schemaTotalCount: 0 },
{ displayName: 'i18n:common.schemas', schemas: [], schemaTotalCount: 0 },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1], schemaTotalCount: 1 },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [schema2], schemaTotalCount: 1 },
{ displayName: 'schema-category3', name: 'schema-category3', schemas: [], schemaTotalCount: 0 },
]);
});
it('should be build from schemas and filter', () => {
const result = getCategoryTree([schema1, schema2], new Set<string>(), '1');
expect(result).toEqual([
{ displayName: 'i18n:common.components', schemas: [], schemaTotalCount: 0 },
{ displayName: 'i18n:common.schemas', schemas: [], schemaTotalCount: 0 },
{ displayName: 'schema-category1', name: 'schema-category1', schemas: [schema1], schemaTotalCount: 1 },
{ displayName: 'schema-category2', name: 'schema-category2', schemas: [], schemaTotalCount: 1 }, // Filtered out
]);
});
});
});

59
frontend/app/shared/state/schemas.state.ts

@ -35,7 +35,6 @@ interface Snapshot {
}
export type SchemasList = ReadonlyArray<SchemaDto>;
export type SchemaCategory = { displayName: string; name?: string; schemas: SchemaDto[] };
@Injectable()
export class SchemasState extends State<Snapshot> {
@ -60,9 +59,6 @@ export class SchemasState extends State<Snapshot> {
public categoryNames =
this.project(x => x.categories);
public categories =
this.projectFrom2(this.schemas, this.categoryNames, (s, c) => buildCategories(c, s));
public get schemaId() {
return this.snapshot.selectedSchema?.id || '';
}
@ -367,18 +363,42 @@ function getField(x: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | nu
}
}
export type SchemaCategory = {
displayName: string;
name?: string;
schemas: SchemaDto[];
schemaTotalCount: number;
};
const SPECIAL_SCHEMAS = 'i18n:common.schemas';
const SPECIAL_COMPONENTS = 'i18n:common.components';
function buildCategories(categories: Set<string>, allSchemas: SchemasList): ReadonlyArray<SchemaCategory> {
export function getCategoryTree(allSchemas: ReadonlyArray<SchemaDto>, categories: Set<string>, filter?: string) {
let match = (_: SchemaDto) => true;
if (filter) {
const terms = filter.split(' ').map(filter => new RegExp(filter.trim(), 'i'));
const matches = (value: string | undefined | null) => {
return !!value && terms.every(term => value.search(term) >= 0);
};
match = schema =>
matches(schema.name) ||
matches(schema.properties.label) ||
matches(schema.properties.hints);
}
const schemas: SchemaCategory = {
displayName: SPECIAL_SCHEMAS,
schemas: [],
schemaTotalCount: 0,
};
const components: SchemaCategory = {
displayName: SPECIAL_COMPONENTS,
schemas: [],
schemaTotalCount: 0,
};
const result: SchemaCategory[] = [schemas, components];
@ -388,9 +408,18 @@ function buildCategories(categories: Set<string>, allSchemas: SchemasList): Read
displayName: name,
name,
schemas: [],
schemaTotalCount: 0,
});
}
function add(schema: SchemaDto, category: SchemaCategory) {
category.schemaTotalCount++;
if (match(schema)) {
category.schemas.push(schema);
}
}
for (const schema of allSchemas) {
const name = schema.category;
@ -402,16 +431,17 @@ function buildCategories(categories: Set<string>, allSchemas: SchemasList): Read
displayName: name,
name,
schemas: [],
schemaTotalCount: 0,
};
result.push(category);
}
category.schemas.push(schema);
add(schema, category);
} else if (schema.type === 'Component') {
components.schemas.push(schema);
add(schema, components);
} else {
schemas.schemas.push(schema);
add(schema, schemas);
}
}
@ -419,16 +449,3 @@ function buildCategories(categories: Set<string>, allSchemas: SchemasList): Read
return result;
}
export function buildSchemaFilterFunction(filter: string): (schema: SchemaDto) => boolean {
const terms = filter.split(' ').map(filter => new RegExp(filter.trim(), 'i'));
const matches = (value: string | undefined | null) => {
return !!value && terms.every(term => value.search(term) >= 0);
};
return (schema: SchemaDto) =>
matches(schema.name) ||
matches(schema.properties.label) ||
matches(schema.properties.hints);
}

Loading…
Cancel
Save